Skip to content

Commit c53da67

Browse files
authored
Use alarm manager for scheduling (#116)
* Use alarm manager for scheduling * Boot receiver * Refactors
1 parent 97ea46b commit c53da67

6 files changed

Lines changed: 205 additions & 60 deletions

File tree

‎client/app/src/main/AndroidManifest.xml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
1111
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
1212
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
13+
<uses-permission android:name="android.permission.WAKE_LOCK" />
1314
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
1415

1516
<application
@@ -22,6 +23,16 @@
2223
android:supportsRtl="true"
2324
android:theme="@style/Theme.LocationHistory">
2425

26+
<receiver
27+
android:name=".BootReceiver"
28+
android:enabled="true"
29+
android:exported="true">
30+
<intent-filter>
31+
<action android:name="android.intent.action.BOOT_COMPLETED" />
32+
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
33+
</intent-filter>
34+
</receiver>
35+
2536
<activity
2637
android:name=".MainActivity"
2738
android:exported="true"
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package com.jackpf.locationhistory.client;
2+
3+
import android.app.AlarmManager;
4+
import android.app.PendingIntent;
5+
import android.content.Context;
6+
import android.content.Intent;
7+
import android.os.Build;
8+
import android.os.PowerManager;
9+
import android.os.SystemClock;
10+
11+
import com.google.common.util.concurrent.AsyncCallable;
12+
import com.google.common.util.concurrent.Futures;
13+
import com.google.common.util.concurrent.ListenableFuture;
14+
import com.google.common.util.concurrent.MoreExecutors;
15+
import com.jackpf.locationhistory.client.util.Logger;
16+
17+
public class BeaconScheduler {
18+
private final Logger log = new Logger(this);
19+
private final AlarmManager alarmManager;
20+
private final PowerManager.WakeLock wakeLock;
21+
private final long wakelockTimeout;
22+
23+
private static final String WAKELOCK_TAG = "BeaconScheduler:wakelock";
24+
25+
public BeaconScheduler(AlarmManager alarmManager,
26+
PowerManager.WakeLock wakeLock,
27+
long wakelockTimeout) {
28+
this.alarmManager = alarmManager;
29+
this.wakeLock = wakeLock;
30+
this.wakelockTimeout = wakelockTimeout;
31+
}
32+
33+
public static BeaconScheduler create(Context context, long wakelockTimeout) {
34+
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
35+
36+
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
37+
PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
38+
wakeLock.setReferenceCounted(false); // Ensure we don't double-acquire
39+
40+
return new BeaconScheduler(
41+
alarmManager,
42+
wakeLock,
43+
wakelockTimeout
44+
);
45+
}
46+
47+
public <T> ListenableFuture<T> runWithWakeLock(AsyncCallable<T> taskFactory) {
48+
acquireLock();
49+
50+
try {
51+
ListenableFuture<T> taskFuture = taskFactory.call();
52+
taskFuture.addListener(this::releaseLock, MoreExecutors.directExecutor());
53+
return taskFuture;
54+
} catch (Exception e) {
55+
releaseLock();
56+
return Futures.immediateFailedFuture(e);
57+
}
58+
}
59+
60+
private PendingIntent createPendingIntent(Context context, Class<?> cls, String action) {
61+
Intent intent = new Intent(context, cls);
62+
intent.setAction(action);
63+
64+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
65+
return PendingIntent.getForegroundService(
66+
context,
67+
0,
68+
intent,
69+
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
70+
);
71+
} else {
72+
return PendingIntent.getService(
73+
context,
74+
0,
75+
intent,
76+
PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0)
77+
);
78+
}
79+
}
80+
81+
private void schedulePendingIntent(PendingIntent pendingIntent, long delayMillis) {
82+
long triggerAtMillis = SystemClock.elapsedRealtime() + delayMillis;
83+
84+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
85+
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
86+
} else {
87+
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
88+
}
89+
}
90+
91+
public void scheduleNext(Context context, Class<?> cls, String action, long delayMillis) {
92+
log.d("Scheduling next task via alarm in %dms", delayMillis);
93+
94+
PendingIntent pendingIntent = createPendingIntent(context, cls, action);
95+
schedulePendingIntent(pendingIntent, delayMillis);
96+
}
97+
98+
private void acquireLock() {
99+
if (!wakeLock.isHeld()) {
100+
wakeLock.acquire(wakelockTimeout);
101+
}
102+
}
103+
104+
private void releaseLock() {
105+
if (wakeLock.isHeld()) {
106+
wakeLock.release();
107+
}
108+
}
109+
}
Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
package com.jackpf.locationhistory.client;
22

33
import android.app.Service;
4+
import android.content.Context;
45
import android.content.Intent;
56
import android.content.SharedPreferences;
67
import android.content.pm.ServiceInfo;
78
import android.os.Build;
8-
import android.os.Handler;
9-
import android.os.HandlerThread;
109
import android.os.IBinder;
1110

1211
import androidx.annotation.NonNull;
@@ -18,6 +17,7 @@
1817
import com.google.common.util.concurrent.Futures;
1918
import com.google.common.util.concurrent.ListenableFuture;
2019
import com.jackpf.locationhistory.client.config.ConfigRepository;
20+
import com.jackpf.locationhistory.client.permissions.AppRequirementsUtil;
2121
import com.jackpf.locationhistory.client.ui.Notifications;
2222
import com.jackpf.locationhistory.client.util.Logger;
2323
import com.jackpf.locationhistory.client.worker.BeaconResult;
@@ -29,47 +29,55 @@
2929
import java.util.concurrent.TimeUnit;
3030

3131
public class BeaconService extends Service {
32-
private final Logger log = new Logger(this);
33-
private HandlerThread handlerThread;
34-
private Handler handler;
32+
private static final Logger log = new Logger("BeaconService");
3533
private ExecutorService executorService;
3634
private ConfigRepository configRepository;
35+
private BeaconScheduler beaconScheduler;
3736
private static final int PERSISTENT_NOTIFICATION_ID = 1;
37+
private static final String ACTION_RUN_TASK = "com.jackpf.locationhistory.client.ACTION_BEACON_SERVICE";
38+
private static final long WAKELOCK_TIMEOUT = TimeUnit.MINUTES.toMillis(10);
3839

3940
private static final String START_MESSAGE = "Beacon task started";
4041
private static final String SUCCESS_MESSAGE = "Beacon task completed successfully";
4142
private static final String FAILED_MESSAGE = "Beacon task failed";
4243
private static final String RETRY_MESSAGE = "Beacon task failed with retry";
4344

44-
private final Runnable beaconTaskRunner = () -> {
45-
log.i(START_MESSAGE);
46-
log.appendEventToFile(BeaconService.this, START_MESSAGE);
47-
48-
ListenableFuture<BeaconResult> beaconResult = BeaconTask
49-
.runSafe(BeaconService.this, executorService);
50-
51-
Futures.addCallback(beaconResult, new FutureCallback<BeaconResult>() {
52-
@Override
53-
public void onSuccess(BeaconResult result) {
54-
log.i("%s: %s", SUCCESS_MESSAGE, result);
55-
log.appendEventToFile(BeaconService.this, "%s: %s", SUCCESS_MESSAGE, result);
56-
scheduleNext(regularDelayMillis());
57-
}
58-
59-
@Override
60-
public void onFailure(@NonNull Throwable t) {
61-
if (t instanceof RetryableException) {
62-
log.w(RETRY_MESSAGE, t);
63-
log.appendEventToFile(BeaconService.this, "%s: %s", RETRY_MESSAGE, t.getMessage());
64-
scheduleNext(retryDelayMillis());
65-
} else {
66-
log.e(FAILED_MESSAGE, t);
67-
log.appendEventToFile(BeaconService.this, "%s: %s", FAILED_MESSAGE, t.getMessage());
68-
scheduleNext(regularDelayMillis());
69-
}
70-
}
71-
}, ContextCompat.getMainExecutor(BeaconService.this));
72-
};
45+
private final Runnable beaconTask = () ->
46+
beaconScheduler.runWithWakeLock(() -> {
47+
log.i(START_MESSAGE);
48+
log.appendEventToFile(BeaconService.this, START_MESSAGE);
49+
50+
ListenableFuture<BeaconResult> beaconResult = BeaconTask
51+
.runSafe(BeaconService.this, executorService);
52+
53+
Futures.addCallback(beaconResult, new FutureCallback<BeaconResult>() {
54+
@Override
55+
public void onSuccess(BeaconResult result) {
56+
log.i("%s: %s", SUCCESS_MESSAGE, result);
57+
log.appendEventToFile(BeaconService.this, "%s: %s", SUCCESS_MESSAGE, result);
58+
scheduleNext(regularDelayMillis());
59+
}
60+
61+
@Override
62+
public void onFailure(@NonNull Throwable t) {
63+
if (t instanceof RetryableException) {
64+
log.w(RETRY_MESSAGE, t);
65+
log.appendEventToFile(BeaconService.this, "%s: %s", RETRY_MESSAGE, t.getMessage());
66+
scheduleNext(retryDelayMillis());
67+
} else {
68+
log.e(FAILED_MESSAGE, t);
69+
log.appendEventToFile(BeaconService.this, "%s: %s", FAILED_MESSAGE, t.getMessage());
70+
scheduleNext(regularDelayMillis());
71+
}
72+
}
73+
}, ContextCompat.getMainExecutor(BeaconService.this));
74+
75+
return beaconResult;
76+
});
77+
78+
private void scheduleNext(long delayMillis) {
79+
beaconScheduler.scheduleNext(this, BeaconService.class, ACTION_RUN_TASK, delayMillis);
80+
}
7381

7482
private final SharedPreferences.OnSharedPreferenceChangeListener configChangeListener = (prefs, key) -> {
7583
if (key != null &&
@@ -81,13 +89,6 @@ public void onFailure(@NonNull Throwable t) {
8189
}
8290
};
8391

84-
private void scheduleNext(long delayMillis) {
85-
log.d("Scheduling next beacon task in %dms", delayMillis);
86-
87-
handler.removeCallbacks(beaconTaskRunner);
88-
handler.postDelayed(beaconTaskRunner, delayMillis);
89-
}
90-
9192
private long regularDelayMillis() {
9293
return TimeUnit.MINUTES.toMillis(configRepository.getUpdateIntervalMinutes());
9394
}
@@ -106,28 +107,27 @@ public IBinder onBind(Intent intent) {
106107
public void onCreate() {
107108
super.onCreate();
108109

109-
handlerThread = new HandlerThread("ServiceTimer");
110-
handlerThread.start();
111-
handler = new Handler(handlerThread.getLooper());
112110
executorService = Executors.newSingleThreadExecutor();
113111

114112
configRepository = new ConfigRepository(this);
115113
configRepository.registerOnSharedPreferenceChangeListener(configChangeListener);
114+
115+
beaconScheduler = BeaconScheduler.create(this, WAKELOCK_TIMEOUT);
116116
}
117117

118118
@Override
119119
public int onStartCommand(Intent intent, int flags, int startId) {
120120
super.onStartCommand(intent, flags, startId);
121121

122122
Notifications notifications = new Notifications(this);
123-
124123
ServiceCompat.startForeground(this,
125124
PERSISTENT_NOTIFICATION_ID,
126125
notifications.createPersistentNotification(getString(R.string.persistent_notification_title), getString(R.string.persistent_notification_message)),
127126
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION : 0);
128127

129-
handler.removeCallbacks(beaconTaskRunner);
130-
handler.post(beaconTaskRunner);
128+
if (intent == null || ACTION_RUN_TASK.equals(intent.getAction())) {
129+
beaconTask.run();
130+
}
131131

132132
return START_STICKY;
133133
}
@@ -136,10 +136,24 @@ public int onStartCommand(Intent intent, int flags, int startId) {
136136
public void onDestroy() {
137137
super.onDestroy();
138138

139-
if (handler != null) handler.removeCallbacks(beaconTaskRunner);
140-
if (handlerThread != null) handlerThread.quitSafely();
141139
if (executorService != null) executorService.shutdown();
142140
if (configRepository != null)
143141
configRepository.unregisterOnSharedPreferenceChangeListener(configChangeListener);
144142
}
143+
144+
public static void startForeground(Context context) {
145+
log.d("Starting service");
146+
Intent intent = new Intent(context, BeaconService.class);
147+
intent.setAction(ACTION_RUN_TASK);
148+
ContextCompat.startForegroundService(context, intent);
149+
}
150+
151+
public static void startForegroundIfPermissionsGranted(Context context) {
152+
if (AppRequirementsUtil.allGranted(context, AppRequirements.getRequirements(context))) {
153+
log.d("Permissions granted");
154+
startForeground(context);
155+
} else {
156+
log.w("Permissions not yet granted");
157+
}
158+
}
145159
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.jackpf.locationhistory.client;
2+
3+
import android.content.BroadcastReceiver;
4+
import android.content.Context;
5+
import android.content.Intent;
6+
7+
import com.jackpf.locationhistory.client.util.Logger;
8+
9+
public class BootReceiver extends BroadcastReceiver {
10+
private final Logger log = new Logger(this);
11+
12+
@Override
13+
public void onReceive(Context context, Intent intent) {
14+
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()) ||
15+
"android.intent.action.QUICKBOOT_POWERON".equals(intent.getAction())) {
16+
log.i("Device rebooted. Restarting service...");
17+
18+
BeaconService.startForegroundIfPermissionsGranted(context);
19+
}
20+
}
21+
}

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package com.jackpf.locationhistory.client;
22

3-
import android.content.Intent;
43
import android.os.Bundle;
54

65
import androidx.appcompat.app.AppCompatActivity;
7-
import androidx.core.content.ContextCompat;
86

97
import com.google.android.material.color.DynamicColors;
10-
import com.jackpf.locationhistory.client.permissions.AppRequirementsUtil;
118
import com.jackpf.locationhistory.client.util.Logger;
129

1310
public class MainActivity extends AppCompatActivity {
@@ -25,11 +22,6 @@ protected void onCreate(Bundle bundle) {
2522
protected void onResume() {
2623
super.onResume();
2724

28-
if (AppRequirementsUtil.allGranted(this, AppRequirements.getRequirements(this))) {
29-
log.d("Permissions granted, starting service");
30-
ContextCompat.startForegroundService(this, new Intent(this, BeaconService.class));
31-
} else {
32-
log.w("Permissions not yet granted, not starting service");
33-
}
25+
BeaconService.startForegroundIfPermissionsGranted(this);
3426
}
3527
}

‎client/app/src/main/java/com/jackpf/locationhistory/client/service/DeviceStateService.java‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ private ListenableFuture<RegisterDeviceResponse> registerDevice(DeviceState devi
4040
}
4141

4242
public ListenableFuture<DeviceState> onDeviceStateReady(DeviceState deviceState) {
43-
log.d("Checking device state");
44-
4543
if (deviceState.isReady()) {
4644
log.d("Device is already ready");
4745
return Futures.immediateFuture(deviceState);

0 commit comments

Comments
 (0)