3

I am trying to do realtime bluetooth location tracking using Android. I want to scan for bluetooth le and upload to a server in real time even when the phone is locked.

When running the app however, after 30 minutes the network request queue stops being processed because of Android's built-in Doze Mode.

I've tried moving my background service to the foreground but to no avail.

Any ideas how I can do constant network in Doze Mode? This won't be a public app so won't be submitted to Google Play, it will only be installed to Enterprise devices.

My Service Class:

package REDACTED.beaconlistener;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.RejectedExecutionException;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;

public class BroadcastService extends Service {

    private static final String TAG = "BROADCAST";
    private static final String strURL = "REDACTED";
    private long lastAlertTimestamp;

    private ScanCallback callback;
    private Timer timer;
    private BluetoothLeScanner scanner;
    private ScanSettings settings;
    private PowerManager.WakeLock wakeLock;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        Intent notificationIntent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
        Notification notification = new Notification.Builder(this)
                .setContentTitle("My Notification")
                .setContentText("My Message")
                .setSmallIcon(R.drawable.ic_launcher_foreground)
                .setContentIntent(pendingIntent)
                .setTicker("TICKER TEXT")
                .build();

        startForeground(999, notification);

        lastAlertTimestamp = Calendar.getInstance().getTime().getTime() / 1000;

        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            Log.e(TAG, "Bluetooth LE not supported!");return;
        }

        BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();

        if (!bluetoothAdapter.isEnabled()) {
            bluetoothAdapter.enable();
        }

        scanner = bluetoothAdapter.getBluetoothLeScanner();
        settings = new ScanSettings.Builder()
                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                .build();
        callback = new ScanCallback() {
            @Override
            public void onScanResult(int callbackType, ScanResult result) {
                if (result.getDevice().getAddress().substring(0, 8).equals("0C:F3:EE")) {
                    // Its a pill!
                    String[] vcodeArr = result.getDevice().getAddress().substring(9).split(":");
                    String vcode = vcodeArr[2] + vcodeArr[1] + vcodeArr[0];

                    Log.d("SCANNER", vcode + " : " + String.valueOf(result.getRssi() + 128));
                    Long ts = System.currentTimeMillis()/1000;

                    PingUrlTask task = new PingUrlTask();

                    try {
                        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, strURL, vcode, String.valueOf(result.getRssi() + 128), getUniquePsuedoID(), ts.toString());
                    } catch(RejectedExecutionException e) {
                        Log.e("BROADCAST", "RejectedExecutionException");
                    }
                }
            }

            @Override
            public void onBatchScanResults(List<ScanResult> results) {
                for (ScanResult sr : results) {
                    this.onScanResult(0, sr);
                }
            }

            @Override
            public void onScanFailed(int errorCode) {
                // Do Nothing.
                Log.e("SCANNER", String.valueOf(errorCode));
            }
        };

        timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                try {
                    new PingUrlTask().execute(strURL, null, String.valueOf(0), getUniquePsuedoID());
                } catch(Exception e) {
                    Log.e("BROADCAST", e.getMessage());
                    e.printStackTrace();
                }
            }
        }, 10000, 10000);

        try {
            PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
            wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "BeaconListener");
        } catch(NullPointerException e) {
            // Do nothing.
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        ping("Service Started");

        try {
            wakeLock.acquire();
        } catch(Exception e) {
            // Do nothing.
        }

//        bluetoothAdapter.startLeScan(callback);
        List<ScanFilter> filter = new ArrayList<ScanFilter>();
        scanner.startScan(filter, settings, callback);

        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        try {
            wakeLock.release();
        } catch(Exception e) {
            // Do nothing.
        }

//        bluetoothAdapter.stopLeScan(callback);
        scanner.stopScan(callback);
        timer.cancel();

        ping("Service Stopped");
    }

    public String getUniquePsuedoID() {
        // If all else fails, if the user does have lower than API 9 (lower
        // than Gingerbread), has reset their device or 'Secure.ANDROID_ID'
        // returns 'null', then simply the ID returned will be solely based
        // off their Android device information. This is where the collisions
        // can happen.
        // Thanks http://www.pocketmagic.net/?p=1662!
        // Try not to use DISPLAY, HOST or ID - these items could change.
        // If there are collisions, there will be overlapping data
        String m_szDevIDShort = "35" + (Build.BOARD.length() % 10) + (Build.BRAND.length() % 10) + (Build.CPU_ABI.length() % 10) + (Build.DEVICE.length() % 10) + (Build.MANUFACTURER.length() % 10) + (Build.MODEL.length() % 10) + (Build.PRODUCT.length() % 10);

        // Thanks to @Roman SL!
        // https://stackoverflow.com/a/4789483/950427
        // Only devices with API >= 9 have android.os.Build.SERIAL
        // http://developer.android.com/reference/android/os/Build.html#SERIAL
        // If a user upgrades software or roots their device, there will be a duplicate entry
        String serial = null;
        try {
            serial = android.os.Build.class.getField("SERIAL").get(null).toString();

            // Go ahead and return the serial for api => 9
            return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
        } catch (Exception exception) {
            // String needs to be initialized
            serial = "serial"; // some value
        }

        // Thanks @Joe!
        // https://stackoverflow.com/a/2853253/950427
        // Finally, combine the values we have found by using the UUID class to create a unique identifier
        return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
    }

    private void ping(String text) {
        Log.d("BROADCAST", text);

//        new PingUrlTask().execute(strURL);
    }

    private void createPushNotification(String title, String text) {
        Intent intent = new Intent(getApplicationContext(), MainActivity.class);
        PendingIntent contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Builder b = new NotificationCompat.Builder(getApplicationContext());

        b.setAutoCancel(true)
                .setDefaults(Notification.DEFAULT_ALL)
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.drawable.ic_launcher_foreground)
                .setTicker("Hearty365")
                .setContentTitle(title)
                .setContentText(text)
                .setDefaults(Notification.DEFAULT_LIGHTS| Notification.DEFAULT_SOUND)
                .setContentIntent(contentIntent)
                .setContentInfo("Info")
                .setPriority(Notification.PRIORITY_HIGH);


        NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify(1, b.build());
    }

    private class PingUrlTask extends AsyncTask<String, Void, Void> {

        @Override
        protected Void doInBackground(final String... strURL) {
            Log.d("BROADCAST", "PingUrlTask");
            Log.d("BROADCAST", strURL[0]);

            try {
                URL requestUrl = new URL(strURL[0]);
                HttpsURLConnection conn = (HttpsURLConnection) requestUrl.openConnection();
                conn.setRequestMethod("POST");
                conn.setReadTimeout(95 * 1000);
                conn.setConnectTimeout(95 * 1000);
                conn.setDoInput(true);
                conn.setDoOutput(true);
                conn.setRequestProperty("Accept", "application/json");
                conn.setRequestProperty("X-Environment", "android");
                conn.setHostnameVerifier(new HostnameVerifier() {
                    @Override
                    public boolean verify(String s, SSLSession sslSession) {
                        return true;
                    }
                });
                conn.setSSLSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault());

                if (strURL.length != 5) {
                    return null;
                }

                // Write Post data
                StringBuilder queryParams = new StringBuilder();
                queryParams.append("vcode=");
                queryParams.append(strURL[1]);
                queryParams.append("&rssi=");
                queryParams.append(strURL[2]);
                queryParams.append("&deviceId=");
                queryParams.append(strURL[3]);
                queryParams.append("&timestamp=");
                queryParams.append(strURL[4]);
                queryParams.append("&time=");
                queryParams.append(lastAlertTimestamp);
                Log.d("BROADCAST", queryParams.toString());
                DataOutputStream dStream = new DataOutputStream(conn.getOutputStream());
                dStream.writeBytes(queryParams.toString());

                conn.connect();

                // Read response
                Log.d(TAG, String.valueOf(conn.getResponseCode()));
                if (conn.getResponseCode() == 200) {
                    StringBuilder result = new StringBuilder();
                    InputStream input = conn.getInputStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(input));
                    String line;
                    while((line = reader.readLine()) != null) {
                        result.append(line);
                    }

                    Log.d(TAG, result.toString());

                    try {
                        JSONObject obj = new JSONObject(result.toString());

                        lastAlertTimestamp = obj.getLong("serverTime");

                        JSONArray alerts = obj.getJSONArray("alerts");
                        for(int i = 0; i < alerts.length(); i++) {
                            JSONObject alert = alerts.getJSONObject(i);

                            createPushNotification(alert.getString("alert_title"), alert.getString("alert_message"));
                        }
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }

                dStream.flush();
                dStream.close();

                Log.d("BROADCAST", String.valueOf(conn.getResponseCode()));
            } catch(Exception e) {
                Log.e("BROADCAST", e.getMessage());
                e.printStackTrace();
            }

//            Handler handler = new Handler(Looper.getMainLooper());
//            handler.post(new Runnable() {
//                @Override
//                public void run() {
//                    if (MainActivity.mWebView != null) {
//                        try {
//                            JSONObject payload = new JSONObject();
//                            payload.put("vcode", strURL[1]);
//                            payload.put("rssi", strURL[2]);
//                            payload.put("deviceId", strURL[3]);
//                            payload.put("timestamp", strURL[4]);
//                            payload.put("time", lastAlertTimestamp);
//                            String command = String.format("androidData(%s)", payload.toString());
//
//                            Log.d(TAG, command);
//                            MainActivity.mWebView.evaluateJavascript(command, null);
//                        } catch(Exception e) {
//                            Log.d(TAG, e.getMessage());
//                            e.printStackTrace();
//                        }
//                    } else {
//                        Log.w(TAG, "No WebView!");
//                    }
//                }
//            });

            return null;
        }
    }
}
Jamesking56
  • 3,683
  • 5
  • 30
  • 61
  • *I've tried moving my background service to the foreground but to no avail.* - give it another try. Foreground services are exempt from doze – Tim Feb 27 '18 at 10:06
  • @TimCastelijns In the `onCreate` method you can see my attempt at starting the service foreground, is this correct? – Jamesking56 Feb 27 '18 at 10:07
  • @Jamesking56, did you eventually solve the problem? We are facing the same issue. If you just need enterprise app I would suggest you whitelist your app in battery optimization list. – BernieDADA Dec 28 '18 at 20:37
  • @BernieDADA sorry for the lateness of my reply, I think I ended up with some outrageous hacks to get the device to stop entering Doze mode. Something that probably wouldn't pass any sort of app store validation – Jamesking56 Nov 13 '19 at 14:56

0 Answers0