From 3af3a9c52b6be8980b823a6cdb1f010c3078f1de Mon Sep 17 00:00:00 2001 From: Jack Farrelly Date: Sat, 24 Jan 2026 01:03:37 +0100 Subject: [PATCH 1/6] Listen for passive locations --- .../locationhistory/client/AppExecutors.java | 41 ++++++++++ .../locationhistory/client/BeaconService.java | 76 +++++++++++++++---- .../location/PassiveLocationListener.java | 52 +++++++++++++ .../client/worker/BeaconTask.java | 32 ++++++-- server/.idea/.gitignore | 10 --- server/.idea/misc.xml | 5 +- server/.idea/sbt.xml | 2 - server/.idea/scala_compiler.xml | 2 +- server/.idea/scala_settings.xml | 2 - 9 files changed, 182 insertions(+), 40 deletions(-) create mode 100644 client/app/src/main/java/com/jackpf/locationhistory/client/AppExecutors.java create mode 100644 client/app/src/main/java/com/jackpf/locationhistory/client/location/PassiveLocationListener.java delete mode 100644 server/.idea/.gitignore diff --git a/client/app/src/main/java/com/jackpf/locationhistory/client/AppExecutors.java b/client/app/src/main/java/com/jackpf/locationhistory/client/AppExecutors.java new file mode 100644 index 00000000..5833ea17 --- /dev/null +++ b/client/app/src/main/java/com/jackpf/locationhistory/client/AppExecutors.java @@ -0,0 +1,41 @@ +package com.jackpf.locationhistory.client; + +import android.os.Handler; +import android.os.Looper; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class AppExecutors { + private static final AppExecutors INSTANCE = new AppExecutors(); + + private final ExecutorService background; + private final Executor mainThread; + + private AppExecutors() { + this.background = Executors.newCachedThreadPool(); + this.mainThread = new MainThreadExecutor(); + } + + public static AppExecutors getInstance() { + return INSTANCE; + } + + public ExecutorService background() { + return background; + } + + public Executor mainThread() { + return mainThread; + } + + private static class MainThreadExecutor implements Executor { + private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(Runnable command) { + mainThreadHandler.post(command); + } + } +} diff --git a/client/app/src/main/java/com/jackpf/locationhistory/client/BeaconService.java b/client/app/src/main/java/com/jackpf/locationhistory/client/BeaconService.java index d2cd7946..40933c3f 100644 --- a/client/app/src/main/java/com/jackpf/locationhistory/client/BeaconService.java +++ b/client/app/src/main/java/com/jackpf/locationhistory/client/BeaconService.java @@ -5,6 +5,8 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ServiceInfo; +import android.location.Location; +import android.location.LocationManager; import android.os.Build; import android.os.IBinder; @@ -17,14 +19,18 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.jackpf.locationhistory.client.config.ConfigRepository; +import com.jackpf.locationhistory.client.location.LocationData; +import com.jackpf.locationhistory.client.location.PassiveLocationListener; import com.jackpf.locationhistory.client.permissions.AppRequirement; import com.jackpf.locationhistory.client.permissions.AppRequirementsUtil; +import com.jackpf.locationhistory.client.permissions.PermissionsManager; import com.jackpf.locationhistory.client.ui.Notifications; import com.jackpf.locationhistory.client.util.Logger; import com.jackpf.locationhistory.client.worker.BeaconResult; import com.jackpf.locationhistory.client.worker.BeaconTask; import com.jackpf.locationhistory.client.worker.RetryableException; +import java.io.IOException; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -35,13 +41,17 @@ public class BeaconService extends Service { private ExecutorService executorService; private ConfigRepository configRepository; private BeaconScheduler beaconScheduler; + private BeaconTask beaconTask; + private PassiveLocationListener passiveLocationListener; + private static final long PASSIVE_LISTENER_MIN_TIME_MS = TimeUnit.MINUTES.toMillis(1); + private static final float PASSIVE_LISTENER_MIN_DISTANCE_M = 0L; private static final int PERSISTENT_NOTIFICATION_ID = 1; private static final String ACTION_RUN_TASK = "com.jackpf.locationhistory.client.ACTION_BEACON_SERVICE"; + private static final String ACTION_PASSIVE_LOCATION = "com.jackpf.locationhistory.client.ACTION_PASSIVE_LOCATION"; - private final Runnable beaconTask = () -> + private final Runnable beaconTaskRunnable = () -> beaconScheduler.runWithWakeLock(() -> { - ListenableFuture beaconResult = BeaconTask - .runSafe(BeaconService.this, executorService); + ListenableFuture beaconResult = beaconTask.run(); Futures.addCallback(beaconResult, new FutureCallback() { @Override @@ -102,24 +112,51 @@ public void onCreate() { configRepository.registerOnSharedPreferenceChangeListener(configChangeListener); beaconScheduler = BeaconScheduler.create(this, BeaconScheduler.DEFAULT_WAKELOCK_TIMEOUT); + passiveLocationListener = new PassiveLocationListener(this, new PermissionsManager(this), + ACTION_PASSIVE_LOCATION, BeaconService.class); + + // Listen for passive location updates + try { + passiveLocationListener.startMonitoring(PASSIVE_LISTENER_MIN_TIME_MS, PASSIVE_LISTENER_MIN_DISTANCE_M); + } catch (Exception e) { + log.e("Unable to start passive listener"); + } + } + + private void handleRunAction() { + beaconTaskRunnable.run(); + } + + private void handlePassiveLocationAction(Intent intent) { + if (intent.hasExtra(LocationManager.KEY_LOCATION_CHANGED)) { + Location location = (Location) intent.getExtras().get(LocationManager.KEY_LOCATION_CHANGED); + log.i("Passive location received: %s", location); + if (location != null) { + LocationData locationData = new LocationData(location, "passive", "passive"); + beaconTask.passiveRun(locationData); + } + } else { + log.w("Passive location intent had no location data"); + } } @Override public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); + createPersistentNotification(); - Notifications notifications = new Notifications(this); - ServiceCompat.startForeground(this, - PERSISTENT_NOTIFICATION_ID, - notifications.createPersistentNotification( - getString(R.string.persistent_notification_title), - configRepository.inHighAccuracyMode() ? getString(R.string.persistent_notification_high_accuracy_mode) - : getString(R.string.persistent_notification_message) - ), - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION : 0); + try { + beaconTask = BeaconTask.create(this, executorService); + } catch (IOException e) { + log.e("Unable to create beacon task", e); + } if (intent == null || ACTION_RUN_TASK.equals(intent.getAction())) { - beaconTask.run(); + handleRunAction(); + } else if (ACTION_PASSIVE_LOCATION.equals(intent.getAction())) { + handlePassiveLocationAction(intent); + /* TODO For handleRunAction we should check the last run time + * if it was < our interval, schedule next for interval - how long ago last run time was */ } return START_STICKY; @@ -132,6 +169,19 @@ public void onDestroy() { if (executorService != null) executorService.shutdown(); if (configRepository != null) configRepository.unregisterOnSharedPreferenceChangeListener(configChangeListener); + if (passiveLocationListener != null) passiveLocationListener.stopMonitoring(); + } + + private void createPersistentNotification() { + Notifications notifications = new Notifications(this); + ServiceCompat.startForeground(this, + PERSISTENT_NOTIFICATION_ID, + notifications.createPersistentNotification( + getString(R.string.persistent_notification_title), + configRepository.inHighAccuracyMode() ? getString(R.string.persistent_notification_high_accuracy_mode) + : getString(R.string.persistent_notification_message) + ), + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION : 0); } public static void startForeground(Context context) { diff --git a/client/app/src/main/java/com/jackpf/locationhistory/client/location/PassiveLocationListener.java b/client/app/src/main/java/com/jackpf/locationhistory/client/location/PassiveLocationListener.java new file mode 100644 index 00000000..c73b34ea --- /dev/null +++ b/client/app/src/main/java/com/jackpf/locationhistory/client/location/PassiveLocationListener.java @@ -0,0 +1,52 @@ +package com.jackpf.locationhistory.client.location; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.location.LocationManager; +import android.os.Build; + +import com.jackpf.locationhistory.client.permissions.PermissionsManager; +import com.jackpf.locationhistory.client.util.Logger; + +public class PassiveLocationListener { + private final Logger log = new Logger(this); + + private final LocationManager locationManager; + private final PermissionsManager permissionsManager; + private final PendingIntent locationIntent; + + public PassiveLocationListener(Context context, PermissionsManager permissionsManager, String action, Class cls) { + this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + this.permissionsManager = permissionsManager; + + Intent intent = new Intent(context, cls); + intent.setAction(action); + this.locationIntent = PendingIntent.getService( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0) + ); + } + + public void startMonitoring(long minTimeMs, float minDistanceM) throws Exception { + if (!permissionsManager.hasLocationPermissions()) { + throw new SecurityException("No permission for passive location"); + } + + log.i("Registering passive location listener"); + + locationManager.requestLocationUpdates( + LocationManager.PASSIVE_PROVIDER, + minTimeMs, + minDistanceM, + locationIntent + ); + } + + public void stopMonitoring() { + log.i("Removing passive location listener"); + locationManager.removeUpdates(locationIntent); + } +} diff --git a/client/app/src/main/java/com/jackpf/locationhistory/client/worker/BeaconTask.java b/client/app/src/main/java/com/jackpf/locationhistory/client/worker/BeaconTask.java index 66b4119a..bcba66b2 100644 --- a/client/app/src/main/java/com/jackpf/locationhistory/client/worker/BeaconTask.java +++ b/client/app/src/main/java/com/jackpf/locationhistory/client/worker/BeaconTask.java @@ -66,7 +66,7 @@ public static BeaconTask create(@NonNull Context context, Executor executor) thr ); } - public ListenableFuture run() { + private ListenableFuture runInternal(AsyncFunction beaconTask) { log.i(START_MESSAGE); log.appendEventToFile(START_MESSAGE); @@ -76,19 +76,35 @@ public ListenableFuture run() { } BeaconContext beaconContext = beaconContextFactory.call(); - ListenableFuture beaconResult = onDeviceReady(beaconContext, () -> - requestLocationUpdate(beaconContext, (locationData) -> - handleLocationUpdate(beaconContext, locationData, () -> - Futures.immediateFuture(new BeaconResult(beaconContext.getDeviceState(), locationData)) - ) - ) - ); + ListenableFuture beaconResult = beaconTask.apply(beaconContext); beaconResult.addListener(storeDeviceStateListener(beaconContext), executor); Futures.addCallback(beaconResult, loggingCallback(), executor); return beaconResult; }, executor); } + /** + * Full flow: fetch location & update + */ + public ListenableFuture run() { + return runInternal(beaconContext -> onDeviceReady(beaconContext, () -> + requestLocationUpdate(beaconContext, (locationData) -> + handleLocationUpdate(beaconContext, locationData, () -> + Futures.immediateFuture(new BeaconResult(beaconContext.getDeviceState(), locationData)) + ) + ) + )); + } + + /** + * Handle passive locations + */ + public ListenableFuture passiveRun(LocationData locationData) { + return runInternal(beaconContext -> handleLocationUpdate(beaconContext, locationData, () -> + Futures.immediateFuture(new BeaconResult(beaconContext.getDeviceState(), locationData)) + )); + } + private Runnable storeDeviceStateListener(BeaconContext beaconContext) { return () -> beaconContext.getDeviceState().storeToConfig(beaconContext.getConfigRepository()); } diff --git a/server/.idea/.gitignore b/server/.idea/.gitignore deleted file mode 100644 index ab1f4164..00000000 --- a/server/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/server/.idea/misc.xml b/server/.idea/misc.xml index 5c824b34..b97f8864 100644 --- a/server/.idea/misc.xml +++ b/server/.idea/misc.xml @@ -1,7 +1,4 @@ - - - + \ No newline at end of file diff --git a/server/.idea/sbt.xml b/server/.idea/sbt.xml index 558319b3..b9b7e76b 100644 --- a/server/.idea/sbt.xml +++ b/server/.idea/sbt.xml @@ -12,8 +12,6 @@ diff --git a/server/.idea/scala_compiler.xml b/server/.idea/scala_compiler.xml index 92412f46..afcc79e0 100644 --- a/server/.idea/scala_compiler.xml +++ b/server/.idea/scala_compiler.xml @@ -2,7 +2,7 @@