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("×tamp=");
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;
}
}
}