Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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> beaconResult = BeaconTask
.runSafe(BeaconService.this, executorService);
ListenableFuture<BeaconResult> beaconResult = beaconTask.run();

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

private final Consumer<LocationData> passiveBeaconTaskRunnable = locationData -> {
beaconScheduler.runWithWakeLock(() -> beaconTask.passiveRun(locationData));
};

private void scheduleNext(long delayMillis) {
beaconScheduler.scheduleNext(this, BeaconService.class, ACTION_RUN_TASK, delayMillis);
}
Expand All @@ -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);
Comment on lines +109 to +110

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of Math.min here is redundant. millisToNextRun is calculated as Math.max(configRepository.getUpdateIntervalMillis() - millisSinceLastRun, 0). Since millisSinceLastRun is non-negative, millisToNextRun will always be less than or equal to configRepository.getUpdateIntervalMillis(). Therefore, the outer Math.min call can be removed to simplify the code.

Suggested change
long millisToNextRun = Math.max(configRepository.getUpdateIntervalMillis() - millisSinceLastRun, 0);
return Math.min(configRepository.getUpdateIntervalMillis(), millisToNextRun);
return Math.max(configRepository.getUpdateIntervalMillis() - millisSinceLastRun, 0);

}

private long retryDelayMillis() {
Expand All @@ -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;
}
Comment thread
jackpf marked this conversation as resolved.

// 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);
Comment thread
jackpf marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Retrieving the Location object can be simplified by using intent.getParcelableExtra(). This avoids the need for getExtras() and an explicit cast, making the code cleaner and more direct.

Suggested change
Location location = (Location) intent.getExtras().get(LocationManager.KEY_LOCATION_CHANGED);
Location location = intent.getParcelableExtra(LocationManager.KEY_LOCATION_CHANGED);

log.i("Passive location received: %s", location);

if (location != null) {
passiveBeaconTaskRunnable.accept(LocationData.passive(location));
}
Comment thread
jackpf marked this conversation as resolved.
} 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;
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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
);
Comment thread
jackpf marked this conversation as resolved.
}

public void stopMonitoring() {
log.i("Removing passive location listener");
locationManager.removeUpdates(locationIntent);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.jackpf.locationhistory.client.push.ObservableUnifiedPushState;

import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.annotation.Nullable;

Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public static BeaconTask create(@NonNull Context context, Executor executor) thr
);
}

public ListenableFuture<BeaconResult> run() {
private <T> ListenableFuture<T> runInternal(AsyncFunction<BeaconContext, T> beaconTask) {
log.i(START_MESSAGE);
log.appendEventToFile(START_MESSAGE);

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

BeaconContext beaconContext = beaconContextFactory.call();
ListenableFuture<BeaconResult> beaconResult = onDeviceReady(beaconContext, () ->
requestLocationUpdate(beaconContext, (locationData) ->
handleLocationUpdate(beaconContext, locationData, () ->
Futures.immediateFuture(new BeaconResult(beaconContext.getDeviceState(), locationData))
)
)
);
ListenableFuture<T> beaconResult = beaconTask.apply(beaconContext);
beaconResult.addListener(storeDeviceStateListener(beaconContext), executor);
Futures.addCallback(beaconResult, loggingCallback(), executor);
return beaconResult;
}, executor);
}

/**
* Full flow: fetch location & update
*/
public ListenableFuture<BeaconResult> run() {
return runInternal(beaconContext -> onDeviceReady(beaconContext, () ->
requestLocationUpdate(beaconContext, (locationData) ->
handleLocationUpdate(beaconContext, locationData, () ->
Futures.immediateFuture(new BeaconResult(beaconContext.getDeviceState(), locationData))
)
)
));
}

/**
* Handle passive locations
*/
public ListenableFuture<BeaconResult> 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());
}
Expand Down
10 changes: 0 additions & 10 deletions server/.idea/.gitignore

This file was deleted.

5 changes: 1 addition & 4 deletions server/.idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading