diff --git a/client/app/src/main/java/com/jackpf/locationhistory/client/BeaconScheduler.java b/client/app/src/main/java/com/jackpf/locationhistory/client/BeaconScheduler.java index 6d77c534..1b8526e3 100644 --- a/client/app/src/main/java/com/jackpf/locationhistory/client/BeaconScheduler.java +++ b/client/app/src/main/java/com/jackpf/locationhistory/client/BeaconScheduler.java @@ -92,7 +92,7 @@ private void schedulePendingIntent(PendingIntent pendingIntent, long delayMillis } public void scheduleNext(Context context, Class cls, String action, long delayMillis) { - log.d("Scheduling next task via alarm in %dms", delayMillis); + log.d("Scheduling next task for now + %dms", delayMillis); PendingIntent pendingIntent = createPendingIntent(context, cls, action); schedulePendingIntent(pendingIntent, delayMillis); 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..53a94519 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,31 +19,41 @@ 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; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; public class BeaconService extends Service { private static final Logger log = new Logger("BeaconService"); private ExecutorService executorService; private ConfigRepository configRepository; private BeaconScheduler beaconScheduler; + @Nullable + 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 = 0.0f; 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 @@ -62,6 +74,10 @@ public void onFailure(@NonNull Throwable t) { return beaconResult; }); + private final Consumer passiveBeaconTaskRunnable = locationData -> { + beaconScheduler.runWithWakeLock(() -> beaconTask.passiveRun(locationData)); + }; + private void scheduleNext(long delayMillis) { beaconScheduler.scheduleNext(this, BeaconService.class, ACTION_RUN_TASK, delayMillis); } @@ -78,8 +94,20 @@ private void scheduleNext(long delayMillis) { } }; + private boolean hasRecentRun() { + long millisSinceLastRun = System.currentTimeMillis() - configRepository.getLastRunTimestamp(); + long updateIntervalMillis = configRepository.getUpdateIntervalMillis(); + return millisSinceLastRun < updateIntervalMillis; + } + private long regularDelayMillis() { - return TimeUnit.MINUTES.toMillis(configRepository.getUpdateIntervalMinutes()); + return configRepository.getUpdateIntervalMillis(); + } + + private long rescheduledDelayMillis() { + long millisSinceLastRun = System.currentTimeMillis() - configRepository.getLastRunTimestamp(); + long millisToNextRun = Math.max(configRepository.getUpdateIntervalMillis() - millisSinceLastRun, 0); + return Math.min(configRepository.getUpdateIntervalMillis(), millisToNextRun); } private long retryDelayMillis() { @@ -102,24 +130,59 @@ public void onCreate() { configRepository.registerOnSharedPreferenceChangeListener(configChangeListener); beaconScheduler = BeaconScheduler.create(this, BeaconScheduler.DEFAULT_WAKELOCK_TIMEOUT); + try { + beaconTask = BeaconTask.create(this, executorService); + } catch (IOException e) { + log.e("Unable to create beacon task", e); + scheduleNext(retryDelayMillis()); + return; + } + + // Listen for passive location updates + passiveLocationListener = new PassiveLocationListener(this, new PermissionsManager(this), + ACTION_PASSIVE_LOCATION, BeaconService.class); + passiveLocationListener.startMonitoring(PASSIVE_LISTENER_MIN_TIME_MS, PASSIVE_LISTENER_MIN_DISTANCE_M); + } + + private void handleRunAction() { + if (!hasRecentRun()) { + beaconTaskRunnable.run(); + } else { + log.d("Re-scheduling due to recent run"); + scheduleNext(rescheduledDelayMillis()); + } + } + + 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) { + passiveBeaconTaskRunnable.accept(LocationData.passive(location)); + } + } 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); - 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); + // Can be null if init failed in onCreate + if (beaconTask == null) { + log.e("Empty beacon task"); + stopSelf(); + return START_NOT_STICKY; + } + + createPersistentNotification(); if (intent == null || ACTION_RUN_TASK.equals(intent.getAction())) { - beaconTask.run(); + handleRunAction(); + } else if (ACTION_PASSIVE_LOCATION.equals(intent.getAction())) { + handlePassiveLocationAction(intent); } return START_STICKY; @@ -132,6 +195,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/config/ConfigRepository.java b/client/app/src/main/java/com/jackpf/locationhistory/client/config/ConfigRepository.java index 21ce356d..095b49e7 100644 --- a/client/app/src/main/java/com/jackpf/locationhistory/client/config/ConfigRepository.java +++ b/client/app/src/main/java/com/jackpf/locationhistory/client/config/ConfigRepository.java @@ -88,16 +88,12 @@ public void setServerPort(int port) { prefs.edit().putInt(SERVER_PORT_KEY, port).apply(); } - public long getLastRunTimestamp() { - return prefs.getLong(LAST_RUN_TIMESTAMP_KEY, 0L); - } - - public long getUpdateIntervalMinutes() { - return prefs.getLong(UPDATE_INTERVAL_KEY, 15L); + public long getUpdateIntervalMillis() { + return prefs.getLong(UPDATE_INTERVAL_KEY, TimeUnit.MINUTES.toMillis(15)); } - public void setUpdateIntervalMinutes(long minutes) { - prefs.edit().putLong(UPDATE_INTERVAL_KEY, minutes).apply(); + public void setUpdateIntervalMillis(long millis) { + prefs.edit().putLong(UPDATE_INTERVAL_KEY, millis).apply(); } public long getHighAccuracyTriggeredAt() { @@ -114,6 +110,10 @@ public boolean inHighAccuracyMode() { && System.currentTimeMillis() - highAccuracyTriggeredAt < HIGH_ACCURACY_DURATION; } + public long getLastRunTimestamp() { + return prefs.getLong(LAST_RUN_TIMESTAMP_KEY, 0L); + } + public void setLastRunTimestamp(long lastRunTime) { prefs.edit().putLong(LAST_RUN_TIMESTAMP_KEY, lastRunTime).apply(); } diff --git a/client/app/src/main/java/com/jackpf/locationhistory/client/location/LocationData.java b/client/app/src/main/java/com/jackpf/locationhistory/client/location/LocationData.java index 8c1ee4cf..0ad8e412 100644 --- a/client/app/src/main/java/com/jackpf/locationhistory/client/location/LocationData.java +++ b/client/app/src/main/java/com/jackpf/locationhistory/client/location/LocationData.java @@ -6,7 +6,14 @@ @Value public class LocationData { + private static final String PASSIVE_SOURCE = "passive"; + private static final String PASSIVE_PROVIDER = "passive"; + Location location; String source; String provider; + + public static LocationData passive(Location location) { + return new LocationData(location, PASSIVE_SOURCE, PASSIVE_PROVIDER); + } } 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..5c69907f --- /dev/null +++ b/client/app/src/main/java/com/jackpf/locationhistory/client/location/PassiveLocationListener.java @@ -0,0 +1,54 @@ +package com.jackpf.locationhistory.client.location; + +import android.annotation.SuppressLint; +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.S ? PendingIntent.FLAG_MUTABLE : 0) + ); + } + + @SuppressLint("MissingPermission") + public void startMonitoring(long minTimeMs, float minDistanceM) throws SecurityException { + 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/ui/SettingsFragment.java b/client/app/src/main/java/com/jackpf/locationhistory/client/ui/SettingsFragment.java index 86a4bfdc..3410feac 100644 --- a/client/app/src/main/java/com/jackpf/locationhistory/client/ui/SettingsFragment.java +++ b/client/app/src/main/java/com/jackpf/locationhistory/client/ui/SettingsFragment.java @@ -17,6 +17,7 @@ import com.jackpf.locationhistory.client.push.ObservableUnifiedPushState; import java.util.List; +import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -66,7 +67,7 @@ public void onDestroyView() { private void setupInputs() { binding.serverHostInput.setText(viewModel.getConfig().getServerHost()); binding.serverPortInput.setText(String.valueOf(viewModel.getConfig().getServerPort())); - binding.updateFrequencyInput.setText(Long.toString(viewModel.getConfig().getUpdateIntervalMinutes())); + binding.updateFrequencyInput.setText(Long.toString(TimeUnit.MILLISECONDS.toMinutes(viewModel.getConfig().getUpdateIntervalMillis()))); binding.testButton.setOnClickListener(v -> viewModel.testConnection( binding.serverHostInput.getText().toString(), diff --git a/client/app/src/main/java/com/jackpf/locationhistory/client/ui/SettingsViewModel.java b/client/app/src/main/java/com/jackpf/locationhistory/client/ui/SettingsViewModel.java index e05b4ae4..8841a075 100644 --- a/client/app/src/main/java/com/jackpf/locationhistory/client/ui/SettingsViewModel.java +++ b/client/app/src/main/java/com/jackpf/locationhistory/client/ui/SettingsViewModel.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; public class SettingsViewModel extends AndroidViewModel { private final Logger log = new Logger(this); @@ -78,11 +79,11 @@ public void testConnection(String host, String portText) { } } - public void saveSettings(String host, String portText, String updateInterval) { + public void saveSettings(String host, String portText, String updateIntervalMinutes) { try { configRepository.setServerHost(host); configRepository.setServerPort(Integer.parseInt(portText)); - configRepository.setUpdateIntervalMinutes(Long.parseLong(updateInterval)); + configRepository.setUpdateIntervalMillis(TimeUnit.MINUTES.toMillis(Long.parseLong(updateIntervalMinutes))); events.postValue(new SettingsViewEvent.Toast(R.string.toast_saved)); } catch (NumberFormatException e) { 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 @@