Skip to content

Commit 9c6bab8

Browse files
authored
Listen for passive locations (#127)
* Listen for passive locations * Accidental commit * Fixes * Smarter rescheduling * Better null checks * Fix version check
1 parent aea02a4 commit 9c6bab8

13 files changed

Lines changed: 191 additions & 53 deletions

File tree

‎client/app/src/main/java/com/jackpf/locationhistory/client/BeaconScheduler.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ private void schedulePendingIntent(PendingIntent pendingIntent, long delayMillis
9292
}
9393

9494
public void scheduleNext(Context context, Class<?> cls, String action, long delayMillis) {
95-
log.d("Scheduling next task via alarm in %dms", delayMillis);
95+
log.d("Scheduling next task for now + %dms", delayMillis);
9696

9797
PendingIntent pendingIntent = createPendingIntent(context, cls, action);
9898
schedulePendingIntent(pendingIntent, delayMillis);

‎client/app/src/main/java/com/jackpf/locationhistory/client/BeaconService.java‎

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import android.content.Intent;
66
import android.content.SharedPreferences;
77
import android.content.pm.ServiceInfo;
8+
import android.location.Location;
9+
import android.location.LocationManager;
810
import android.os.Build;
911
import android.os.IBinder;
1012

@@ -17,31 +19,41 @@
1719
import com.google.common.util.concurrent.Futures;
1820
import com.google.common.util.concurrent.ListenableFuture;
1921
import com.jackpf.locationhistory.client.config.ConfigRepository;
22+
import com.jackpf.locationhistory.client.location.LocationData;
23+
import com.jackpf.locationhistory.client.location.PassiveLocationListener;
2024
import com.jackpf.locationhistory.client.permissions.AppRequirement;
2125
import com.jackpf.locationhistory.client.permissions.AppRequirementsUtil;
26+
import com.jackpf.locationhistory.client.permissions.PermissionsManager;
2227
import com.jackpf.locationhistory.client.ui.Notifications;
2328
import com.jackpf.locationhistory.client.util.Logger;
2429
import com.jackpf.locationhistory.client.worker.BeaconResult;
2530
import com.jackpf.locationhistory.client.worker.BeaconTask;
2631
import com.jackpf.locationhistory.client.worker.RetryableException;
2732

33+
import java.io.IOException;
2834
import java.util.List;
2935
import java.util.concurrent.ExecutorService;
3036
import java.util.concurrent.Executors;
3137
import java.util.concurrent.TimeUnit;
38+
import java.util.function.Consumer;
3239

3340
public class BeaconService extends Service {
3441
private static final Logger log = new Logger("BeaconService");
3542
private ExecutorService executorService;
3643
private ConfigRepository configRepository;
3744
private BeaconScheduler beaconScheduler;
45+
@Nullable
46+
private BeaconTask beaconTask;
47+
private PassiveLocationListener passiveLocationListener;
48+
private static final long PASSIVE_LISTENER_MIN_TIME_MS = TimeUnit.MINUTES.toMillis(1);
49+
private static final float PASSIVE_LISTENER_MIN_DISTANCE_M = 0.0f;
3850
private static final int PERSISTENT_NOTIFICATION_ID = 1;
3951
private static final String ACTION_RUN_TASK = "com.jackpf.locationhistory.client.ACTION_BEACON_SERVICE";
52+
private static final String ACTION_PASSIVE_LOCATION = "com.jackpf.locationhistory.client.ACTION_PASSIVE_LOCATION";
4053

41-
private final Runnable beaconTask = () ->
54+
private final Runnable beaconTaskRunnable = () ->
4255
beaconScheduler.runWithWakeLock(() -> {
43-
ListenableFuture<BeaconResult> beaconResult = BeaconTask
44-
.runSafe(BeaconService.this, executorService);
56+
ListenableFuture<BeaconResult> beaconResult = beaconTask.run();
4557

4658
Futures.addCallback(beaconResult, new FutureCallback<BeaconResult>() {
4759
@Override
@@ -62,6 +74,10 @@ public void onFailure(@NonNull Throwable t) {
6274
return beaconResult;
6375
});
6476

77+
private final Consumer<LocationData> passiveBeaconTaskRunnable = locationData -> {
78+
beaconScheduler.runWithWakeLock(() -> beaconTask.passiveRun(locationData));
79+
};
80+
6581
private void scheduleNext(long delayMillis) {
6682
beaconScheduler.scheduleNext(this, BeaconService.class, ACTION_RUN_TASK, delayMillis);
6783
}
@@ -78,8 +94,20 @@ private void scheduleNext(long delayMillis) {
7894
}
7995
};
8096

97+
private boolean hasRecentRun() {
98+
long millisSinceLastRun = System.currentTimeMillis() - configRepository.getLastRunTimestamp();
99+
long updateIntervalMillis = configRepository.getUpdateIntervalMillis();
100+
return millisSinceLastRun < updateIntervalMillis;
101+
}
102+
81103
private long regularDelayMillis() {
82-
return TimeUnit.MINUTES.toMillis(configRepository.getUpdateIntervalMinutes());
104+
return configRepository.getUpdateIntervalMillis();
105+
}
106+
107+
private long rescheduledDelayMillis() {
108+
long millisSinceLastRun = System.currentTimeMillis() - configRepository.getLastRunTimestamp();
109+
long millisToNextRun = Math.max(configRepository.getUpdateIntervalMillis() - millisSinceLastRun, 0);
110+
return Math.min(configRepository.getUpdateIntervalMillis(), millisToNextRun);
83111
}
84112

85113
private long retryDelayMillis() {
@@ -102,24 +130,59 @@ public void onCreate() {
102130
configRepository.registerOnSharedPreferenceChangeListener(configChangeListener);
103131

104132
beaconScheduler = BeaconScheduler.create(this, BeaconScheduler.DEFAULT_WAKELOCK_TIMEOUT);
133+
try {
134+
beaconTask = BeaconTask.create(this, executorService);
135+
} catch (IOException e) {
136+
log.e("Unable to create beacon task", e);
137+
scheduleNext(retryDelayMillis());
138+
return;
139+
}
140+
141+
// Listen for passive location updates
142+
passiveLocationListener = new PassiveLocationListener(this, new PermissionsManager(this),
143+
ACTION_PASSIVE_LOCATION, BeaconService.class);
144+
passiveLocationListener.startMonitoring(PASSIVE_LISTENER_MIN_TIME_MS, PASSIVE_LISTENER_MIN_DISTANCE_M);
145+
}
146+
147+
private void handleRunAction() {
148+
if (!hasRecentRun()) {
149+
beaconTaskRunnable.run();
150+
} else {
151+
log.d("Re-scheduling due to recent run");
152+
scheduleNext(rescheduledDelayMillis());
153+
}
154+
}
155+
156+
private void handlePassiveLocationAction(Intent intent) {
157+
if (intent.hasExtra(LocationManager.KEY_LOCATION_CHANGED)) {
158+
Location location = (Location) intent.getExtras().get(LocationManager.KEY_LOCATION_CHANGED);
159+
log.i("Passive location received: %s", location);
160+
161+
if (location != null) {
162+
passiveBeaconTaskRunnable.accept(LocationData.passive(location));
163+
}
164+
} else {
165+
log.w("Passive location intent had no location data");
166+
}
105167
}
106168

107169
@Override
108170
public int onStartCommand(Intent intent, int flags, int startId) {
109171
super.onStartCommand(intent, flags, startId);
110172

111-
Notifications notifications = new Notifications(this);
112-
ServiceCompat.startForeground(this,
113-
PERSISTENT_NOTIFICATION_ID,
114-
notifications.createPersistentNotification(
115-
getString(R.string.persistent_notification_title),
116-
configRepository.inHighAccuracyMode() ? getString(R.string.persistent_notification_high_accuracy_mode)
117-
: getString(R.string.persistent_notification_message)
118-
),
119-
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION : 0);
173+
// Can be null if init failed in onCreate
174+
if (beaconTask == null) {
175+
log.e("Empty beacon task");
176+
stopSelf();
177+
return START_NOT_STICKY;
178+
}
179+
180+
createPersistentNotification();
120181

121182
if (intent == null || ACTION_RUN_TASK.equals(intent.getAction())) {
122-
beaconTask.run();
183+
handleRunAction();
184+
} else if (ACTION_PASSIVE_LOCATION.equals(intent.getAction())) {
185+
handlePassiveLocationAction(intent);
123186
}
124187

125188
return START_STICKY;
@@ -132,6 +195,19 @@ public void onDestroy() {
132195
if (executorService != null) executorService.shutdown();
133196
if (configRepository != null)
134197
configRepository.unregisterOnSharedPreferenceChangeListener(configChangeListener);
198+
if (passiveLocationListener != null) passiveLocationListener.stopMonitoring();
199+
}
200+
201+
private void createPersistentNotification() {
202+
Notifications notifications = new Notifications(this);
203+
ServiceCompat.startForeground(this,
204+
PERSISTENT_NOTIFICATION_ID,
205+
notifications.createPersistentNotification(
206+
getString(R.string.persistent_notification_title),
207+
configRepository.inHighAccuracyMode() ? getString(R.string.persistent_notification_high_accuracy_mode)
208+
: getString(R.string.persistent_notification_message)
209+
),
210+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION : 0);
135211
}
136212

137213
public static void startForeground(Context context) {

‎client/app/src/main/java/com/jackpf/locationhistory/client/config/ConfigRepository.java‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,12 @@ public void setServerPort(int port) {
8888
prefs.edit().putInt(SERVER_PORT_KEY, port).apply();
8989
}
9090

91-
public long getLastRunTimestamp() {
92-
return prefs.getLong(LAST_RUN_TIMESTAMP_KEY, 0L);
93-
}
94-
95-
public long getUpdateIntervalMinutes() {
96-
return prefs.getLong(UPDATE_INTERVAL_KEY, 15L);
91+
public long getUpdateIntervalMillis() {
92+
return prefs.getLong(UPDATE_INTERVAL_KEY, TimeUnit.MINUTES.toMillis(15));
9793
}
9894

99-
public void setUpdateIntervalMinutes(long minutes) {
100-
prefs.edit().putLong(UPDATE_INTERVAL_KEY, minutes).apply();
95+
public void setUpdateIntervalMillis(long millis) {
96+
prefs.edit().putLong(UPDATE_INTERVAL_KEY, millis).apply();
10197
}
10298

10399
public long getHighAccuracyTriggeredAt() {
@@ -114,6 +110,10 @@ public boolean inHighAccuracyMode() {
114110
&& System.currentTimeMillis() - highAccuracyTriggeredAt < HIGH_ACCURACY_DURATION;
115111
}
116112

113+
public long getLastRunTimestamp() {
114+
return prefs.getLong(LAST_RUN_TIMESTAMP_KEY, 0L);
115+
}
116+
117117
public void setLastRunTimestamp(long lastRunTime) {
118118
prefs.edit().putLong(LAST_RUN_TIMESTAMP_KEY, lastRunTime).apply();
119119
}

‎client/app/src/main/java/com/jackpf/locationhistory/client/location/LocationData.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66

77
@Value
88
public class LocationData {
9+
private static final String PASSIVE_SOURCE = "passive";
10+
private static final String PASSIVE_PROVIDER = "passive";
11+
912
Location location;
1013
String source;
1114
String provider;
15+
16+
public static LocationData passive(Location location) {
17+
return new LocationData(location, PASSIVE_SOURCE, PASSIVE_PROVIDER);
18+
}
1219
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package com.jackpf.locationhistory.client.location;
2+
3+
import android.annotation.SuppressLint;
4+
import android.app.PendingIntent;
5+
import android.content.Context;
6+
import android.content.Intent;
7+
import android.location.LocationManager;
8+
import android.os.Build;
9+
10+
import com.jackpf.locationhistory.client.permissions.PermissionsManager;
11+
import com.jackpf.locationhistory.client.util.Logger;
12+
13+
public class PassiveLocationListener {
14+
private final Logger log = new Logger(this);
15+
16+
private final LocationManager locationManager;
17+
private final PermissionsManager permissionsManager;
18+
private final PendingIntent locationIntent;
19+
20+
public PassiveLocationListener(Context context, PermissionsManager permissionsManager, String action, Class<?> cls) {
21+
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
22+
this.permissionsManager = permissionsManager;
23+
24+
Intent intent = new Intent(context, cls);
25+
intent.setAction(action);
26+
this.locationIntent = PendingIntent.getService(
27+
context,
28+
0,
29+
intent,
30+
PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0)
31+
);
32+
}
33+
34+
@SuppressLint("MissingPermission")
35+
public void startMonitoring(long minTimeMs, float minDistanceM) throws SecurityException {
36+
if (!permissionsManager.hasLocationPermissions()) {
37+
throw new SecurityException("No permission for passive location");
38+
}
39+
40+
log.i("Registering passive location listener");
41+
42+
locationManager.requestLocationUpdates(
43+
LocationManager.PASSIVE_PROVIDER,
44+
minTimeMs,
45+
minDistanceM,
46+
locationIntent
47+
);
48+
}
49+
50+
public void stopMonitoring() {
51+
log.i("Removing passive location listener");
52+
locationManager.removeUpdates(locationIntent);
53+
}
54+
}

‎client/app/src/main/java/com/jackpf/locationhistory/client/ui/SettingsFragment.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.jackpf.locationhistory.client.push.ObservableUnifiedPushState;
1818

1919
import java.util.List;
20+
import java.util.concurrent.TimeUnit;
2021

2122
import javax.annotation.Nullable;
2223

@@ -66,7 +67,7 @@ public void onDestroyView() {
6667
private void setupInputs() {
6768
binding.serverHostInput.setText(viewModel.getConfig().getServerHost());
6869
binding.serverPortInput.setText(String.valueOf(viewModel.getConfig().getServerPort()));
69-
binding.updateFrequencyInput.setText(Long.toString(viewModel.getConfig().getUpdateIntervalMinutes()));
70+
binding.updateFrequencyInput.setText(Long.toString(TimeUnit.MILLISECONDS.toMinutes(viewModel.getConfig().getUpdateIntervalMillis())));
7071

7172
binding.testButton.setOnClickListener(v -> viewModel.testConnection(
7273
binding.serverHostInput.getText().toString(),

‎client/app/src/main/java/com/jackpf/locationhistory/client/ui/SettingsViewModel.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.io.IOException;
2323
import java.util.Arrays;
2424
import java.util.List;
25+
import java.util.concurrent.TimeUnit;
2526

2627
public class SettingsViewModel extends AndroidViewModel {
2728
private final Logger log = new Logger(this);
@@ -78,11 +79,11 @@ public void testConnection(String host, String portText) {
7879
}
7980
}
8081

81-
public void saveSettings(String host, String portText, String updateInterval) {
82+
public void saveSettings(String host, String portText, String updateIntervalMinutes) {
8283
try {
8384
configRepository.setServerHost(host);
8485
configRepository.setServerPort(Integer.parseInt(portText));
85-
configRepository.setUpdateIntervalMinutes(Long.parseLong(updateInterval));
86+
configRepository.setUpdateIntervalMillis(TimeUnit.MINUTES.toMillis(Long.parseLong(updateIntervalMinutes)));
8687

8788
events.postValue(new SettingsViewEvent.Toast(R.string.toast_saved));
8889
} catch (NumberFormatException e) {

‎client/app/src/main/java/com/jackpf/locationhistory/client/worker/BeaconTask.java‎

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public static BeaconTask create(@NonNull Context context, Executor executor) thr
6666
);
6767
}
6868

69-
public ListenableFuture<BeaconResult> run() {
69+
private <T> ListenableFuture<T> runInternal(AsyncFunction<BeaconContext, T> beaconTask) {
7070
log.i(START_MESSAGE);
7171
log.appendEventToFile(START_MESSAGE);
7272

@@ -76,19 +76,35 @@ public ListenableFuture<BeaconResult> run() {
7676
}
7777

7878
BeaconContext beaconContext = beaconContextFactory.call();
79-
ListenableFuture<BeaconResult> beaconResult = onDeviceReady(beaconContext, () ->
80-
requestLocationUpdate(beaconContext, (locationData) ->
81-
handleLocationUpdate(beaconContext, locationData, () ->
82-
Futures.immediateFuture(new BeaconResult(beaconContext.getDeviceState(), locationData))
83-
)
84-
)
85-
);
79+
ListenableFuture<T> beaconResult = beaconTask.apply(beaconContext);
8680
beaconResult.addListener(storeDeviceStateListener(beaconContext), executor);
8781
Futures.addCallback(beaconResult, loggingCallback(), executor);
8882
return beaconResult;
8983
}, executor);
9084
}
9185

86+
/**
87+
* Full flow: fetch location & update
88+
*/
89+
public ListenableFuture<BeaconResult> run() {
90+
return runInternal(beaconContext -> onDeviceReady(beaconContext, () ->
91+
requestLocationUpdate(beaconContext, (locationData) ->
92+
handleLocationUpdate(beaconContext, locationData, () ->
93+
Futures.immediateFuture(new BeaconResult(beaconContext.getDeviceState(), locationData))
94+
)
95+
)
96+
));
97+
}
98+
99+
/**
100+
* Handle passive locations
101+
*/
102+
public ListenableFuture<BeaconResult> passiveRun(LocationData locationData) {
103+
return runInternal(beaconContext -> handleLocationUpdate(beaconContext, locationData, () ->
104+
Futures.immediateFuture(new BeaconResult(beaconContext.getDeviceState(), locationData))
105+
));
106+
}
107+
92108
private Runnable storeDeviceStateListener(BeaconContext beaconContext) {
93109
return () -> beaconContext.getDeviceState().storeToConfig(beaconContext.getConfigRepository());
94110
}

‎server/.idea/.gitignore‎

Lines changed: 0 additions & 10 deletions
This file was deleted.

‎server/.idea/misc.xml‎

Lines changed: 1 addition & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)