feat(alerts): keep alerting until left-behind items are acknowledged
This commit is contained in:
@@ -25,6 +25,10 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
||||
public static final String ACTION_DATA_CHANGED = "com.wytehat.btlogger.DATA_CHANGED";
|
||||
public static final String ACTION_CONFIG_CHANGED = "com.wytehat.btlogger.CONFIG_CHANGED";
|
||||
public static final String ACTION_RANGE_MODE = "com.wytehat.btlogger.RANGE_MODE";
|
||||
public static final String ACTION_ACK_LEFT_BEHIND = "com.wytehat.btlogger.ACK_LEFT_BEHIND";
|
||||
public static final String EXTRA_ADDRESS = "address";
|
||||
/** How often an unacknowledged left-behind item re-alerts. */
|
||||
private static final long ALERT_REPEAT_MS = 60000L;
|
||||
private static final int NOTIFICATION_ID = 42;
|
||||
private static final String CHANNEL_ID = "bluetooth_tracking";
|
||||
private static final String ALERT_CHANNEL_ID = "tracked_item_disconnects";
|
||||
@@ -37,6 +41,9 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
||||
private int phoneBattery = 100;
|
||||
private final Handler handler = new Handler();
|
||||
private final HashMap<String, Long> lastPeriodicLog = new HashMap<String, Long>();
|
||||
/** Items reported left behind, address -> display name, until acknowledged. */
|
||||
private final HashMap<String, String> leftBehind = new HashMap<String, String>();
|
||||
private boolean alertLoopRunning;
|
||||
|
||||
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
@@ -45,6 +52,10 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
||||
rangeModeActive = intent.getBooleanExtra("active", false);
|
||||
return;
|
||||
}
|
||||
if (ACTION_ACK_LEFT_BEHIND.equals(action)) {
|
||||
acknowledgeLeftBehind(intent.getStringExtra(EXTRA_ADDRESS));
|
||||
return;
|
||||
}
|
||||
if (ACTION_CONFIG_CHANGED.equals(action)) {
|
||||
restartBackgroundScan();
|
||||
configureLocation();
|
||||
@@ -84,7 +95,7 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
||||
DeviceRecord previous = db.get(address);
|
||||
db.disconnected(address, broadcastName, now, fix, infer(previous));
|
||||
if (previous != null && previous.tracked == 1 && previous.connected == 1)
|
||||
showDisconnectAlert(previous, infer(previous));
|
||||
markLeftBehind(previous, infer(previous));
|
||||
captureFresh(address, now);
|
||||
configureLocation();
|
||||
} else if (BluetoothDevice.ACTION_FOUND.equals(action)) {
|
||||
@@ -175,6 +186,7 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
filter.addAction(ACTION_CONFIG_CHANGED);
|
||||
filter.addAction(ACTION_RANGE_MODE);
|
||||
filter.addAction(ACTION_ACK_LEFT_BEHIND);
|
||||
registerReceiver(receiver, filter);
|
||||
receiverRegistered = true;
|
||||
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
|
||||
@@ -192,6 +204,7 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
||||
|
||||
public void onDestroy() {
|
||||
handler.removeCallbacks(scanTask);
|
||||
handler.removeCallbacks(alertTask);
|
||||
try { if (bluetooth != null) bluetooth.stopLeScan(leScanCallback); }
|
||||
catch (Exception ignored) { }
|
||||
if (receiverRegistered) unregisterReceiver(receiver);
|
||||
@@ -418,16 +431,99 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
||||
return first.getTime() >= second.getTime() ? first : second;
|
||||
}
|
||||
|
||||
/**
|
||||
* FLAG_IMMUTABLE (API 23+) is referenced by value because this module
|
||||
* still compiles against SDK 21.
|
||||
*/
|
||||
private int pendingFlags() {
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
if (Build.VERSION.SDK_INT >= 23) flags |= 0x04000000;
|
||||
return flags;
|
||||
}
|
||||
|
||||
private int alertId(String address) {
|
||||
return 1000 + Math.abs(address.hashCode() % 100000);
|
||||
}
|
||||
|
||||
private PendingIntent acknowledgeIntent(String address) {
|
||||
Intent ack = new Intent(ACTION_ACK_LEFT_BEHIND).setPackage(getPackageName());
|
||||
if (address != null) ack.putExtra(EXTRA_ADDRESS, address);
|
||||
return PendingIntent.getBroadcast(this,
|
||||
address == null ? 0 : alertId(address), ack, pendingFlags());
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the item as left behind and keeps alerting until the user
|
||||
* taps "Acknowledge Left Behind" - a swipe alone does not clear it.
|
||||
*/
|
||||
private void markLeftBehind(DeviceRecord record, String reason) {
|
||||
if (record == null || record.address == null) return;
|
||||
leftBehind.put(record.address, record.displayName());
|
||||
showDisconnectAlert(record, reason);
|
||||
updateForegroundNotification();
|
||||
startAlertLoop();
|
||||
}
|
||||
|
||||
private void acknowledgeLeftBehind(String address) {
|
||||
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
if (address == null) {
|
||||
for (String pending : leftBehind.keySet().toArray(new String[0])) {
|
||||
try { manager.cancel(alertId(pending)); } catch (Exception ignored) { }
|
||||
}
|
||||
leftBehind.clear();
|
||||
} else {
|
||||
leftBehind.remove(address);
|
||||
try { manager.cancel(alertId(address)); } catch (Exception ignored) { }
|
||||
}
|
||||
if (leftBehind.isEmpty()) {
|
||||
handler.removeCallbacks(alertTask);
|
||||
alertLoopRunning = false;
|
||||
}
|
||||
updateForegroundNotification();
|
||||
}
|
||||
|
||||
private void startAlertLoop() {
|
||||
if (alertLoopRunning) return;
|
||||
alertLoopRunning = true;
|
||||
handler.postDelayed(alertTask, ALERT_REPEAT_MS);
|
||||
}
|
||||
|
||||
/** Re-fires every outstanding alert until each one is acknowledged. */
|
||||
private final Runnable alertTask = new Runnable() {
|
||||
public void run() {
|
||||
if (leftBehind.isEmpty()) { alertLoopRunning = false; return; }
|
||||
for (String address : leftBehind.keySet().toArray(new String[0])) {
|
||||
DeviceRecord record = db.get(address);
|
||||
if (record == null) continue;
|
||||
showDisconnectAlert(record, "Still left behind - not acknowledged");
|
||||
}
|
||||
updateForegroundNotification();
|
||||
handler.postDelayed(this, ALERT_REPEAT_MS);
|
||||
}
|
||||
};
|
||||
|
||||
private void updateForegroundNotification() {
|
||||
try {
|
||||
NotificationManager manager =
|
||||
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
manager.notify(NOTIFICATION_ID, buildNotification());
|
||||
} catch (Exception ignored) { }
|
||||
}
|
||||
|
||||
private void showDisconnectAlert(DeviceRecord record, String reason) {
|
||||
Intent open = new Intent(this, MainActivity.class);
|
||||
PendingIntent pending = PendingIntent.getActivity(this, record.address.hashCode(), open,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
Notification.Builder builder = new Notification.Builder(this)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentTitle(record.displayName() + " disconnected")
|
||||
.setContentTitle(record.displayName() + " left behind")
|
||||
.setContentText(reason == null ? "Your tracked item is no longer connected" : reason)
|
||||
.setContentIntent(pending).setAutoCancel(true)
|
||||
.setDefaults(Notification.DEFAULT_ALL).setPriority(Notification.PRIORITY_HIGH);
|
||||
if (Build.VERSION.SDK_INT >= 16) {
|
||||
builder.addAction(0, "Acknowledge Left Behind",
|
||||
acknowledgeIntent(record.address));
|
||||
}
|
||||
NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
try {
|
||||
@@ -445,12 +541,24 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
||||
|
||||
private Notification buildNotification() {
|
||||
Intent open = new Intent(this, MainActivity.class);
|
||||
PendingIntent pending = PendingIntent.getActivity(this, 0, open,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
PendingIntent pending = PendingIntent.getActivity(this, 0, open, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
int outstanding = leftBehind.size();
|
||||
String title = outstanding > 0
|
||||
? outstanding + (outstanding == 1 ? " item left behind" : " items left behind")
|
||||
: "Bluetooth Item Finder";
|
||||
String text = outstanding > 0
|
||||
? "Alerting until acknowledged: " + describeLeftBehind()
|
||||
: "Watching your tracked items";
|
||||
Notification.Builder builder = new Notification.Builder(this)
|
||||
.setSmallIcon(R.drawable.ic_launcher).setContentTitle("Bluetooth Item Finder")
|
||||
.setContentText("Watching your tracked items").setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_launcher).setContentTitle(title)
|
||||
.setContentText(text).setOngoing(true)
|
||||
.setContentIntent(pending);
|
||||
if (Build.VERSION.SDK_INT >= 16 && outstanding > 0) {
|
||||
builder.addAction(0,
|
||||
outstanding == 1 ? "Acknowledge Left Behind" : "Acknowledge All",
|
||||
acknowledgeIntent(outstanding == 1
|
||||
? leftBehind.keySet().iterator().next() : null));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
try {
|
||||
Class<?> channelClass = Class.forName("android.app.NotificationChannel");
|
||||
@@ -467,5 +575,14 @@ public class BluetoothTrackingService extends Service implements LocationListene
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private String describeLeftBehind() {
|
||||
StringBuilder names = new StringBuilder();
|
||||
for (String name : leftBehind.values()) {
|
||||
if (names.length() > 0) names.append(", ");
|
||||
names.append(name);
|
||||
}
|
||||
return names.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user