implement AlarmManager and JobScheduler

This is needed for the bootstrap job when setting up WhatsApp in
companion device mode.

The implementation is based on `Handler.postDelayed()`, so jobs and
alarms are not persistent for now.
This commit is contained in:
Julian Winkler
2025-10-04 13:05:00 +02:00
parent a09aa53ecf
commit c60f8e3850
12 changed files with 232 additions and 15 deletions

View File

@@ -1,12 +1,14 @@
package android.app;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
import android.graphics.Bitmap;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.Process;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Collections;
@@ -14,6 +16,13 @@ public class ActivityManager {
public static class RunningAppProcessInfo{
public int importance;
public int pid;
public String processName;
private RunningAppProcessInfo(int pid, String processName) {
this.pid = pid;
this.processName = processName;
}
}
public static class TaskDescription {
@@ -21,7 +30,7 @@ public class ActivityManager {
}
public List<RunningAppProcessInfo> getRunningAppProcesses() {
return null;
return Arrays.asList(new RunningAppProcessInfo(Process.myPid(), Context.this_application.getPackageName()));
}
public boolean isLowRamDevice() {return false;}

View File

@@ -1,13 +1,51 @@
package android.app;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.text.format.DateUtils;
import android.util.Slog;
public class AlarmManager {
public void cancel(PendingIntent operation) {}
private static final String TAG = "AlarmManager";
public void setInexactRepeating(int type, long triggerTime, long interval, PendingIntent operation) {}
public void cancel(PendingIntent operation) {
Slog.i(TAG, "cancel(" + operation + ") called");
}
public void setExact(int type, long triggerTime, PendingIntent operation) {}
public void setInexactRepeating(int type, long triggerTime, long interval, PendingIntent operation) {
Slog.i(TAG, "setInexactRepeating(" + type + ", " + triggerTime + ", " + interval + ", " + operation + ") called");
long delay = triggerTime - ((type == 2 || type == 3) ? SystemClock.elapsedRealtime() : System.currentTimeMillis());
Slog.i(TAG, "setInexactRepeating() delay: " + DateUtils.formatElapsedTime(delay) + " interval: " + DateUtils.formatElapsedTime(interval));
Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
Slog.i(TAG, "delivering repeating alarm: " + operation);
operation.send();
handler.postDelayed(this, interval);
}
}, delay);
}
public void set(int type, long triggerTime, PendingIntent operation) {}
public void setExact(int type, long triggerTime, PendingIntent operation) {
Slog.i(TAG, "setExact(" + type + ", " + triggerTime + ", " + operation + ") called");
long delay = triggerTime - ((type == 2 || type == 3) ? SystemClock.elapsedRealtime() : System.currentTimeMillis());
Slog.i(TAG, "setExact() delay: " + DateUtils.formatElapsedTime(delay));
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
Slog.i(TAG, "delivering alarm: " + operation);
operation.send();
}
}, delay);
}
public void setExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation) {}
public void set(int type, long triggerTime, PendingIntent operation) {
setExact(type, triggerTime, operation);
}
public void setExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation) {
setExact(type, triggerAtMillis, operation);
}
}

View File

@@ -5,20 +5,63 @@ import android.os.PersistableBundle;
public class JobInfo {
private ComponentName service;
long initialBackoffMillis;
int backoffPolicy;
private PersistableBundle extras;
long periodicMillis;
private int id;
boolean running;
long minLatencyMillis;
public JobInfo() {}
public ComponentName getService() {
return service;
}
public PersistableBundle getExtras() {
return extras;
}
public int getId() {
return id;
}
public String toString() {
return "JobInfo{"
+ "jobService=" + service
+ ", initialBackoffMillis=" + initialBackoffMillis
+ ", backoffPolicy=" + backoffPolicy
+ ", extras=" + extras
+ ", periodicMillis=" + periodicMillis
+ ", id=" + id
+ '}';
}
public static final class Builder {
public Builder(int jobId, ComponentName jobService) {}
private JobInfo jobInfo;
public Builder(int jobId, ComponentName jobService) {
jobInfo = new JobInfo();
jobInfo.id = jobId;
jobInfo.service = jobService;
}
public Builder setBackoffCriteria(long initialBackoffMillis, int backoffPolicy) {
jobInfo.initialBackoffMillis = initialBackoffMillis;
jobInfo.backoffPolicy = backoffPolicy;
return this;
}
public Builder setExtras(PersistableBundle extras) {
jobInfo.extras = extras;
return this;
}
public Builder setMinimumLatency(long minLatencyMillis) {
jobInfo.minLatencyMillis = minLatencyMillis;
return this;
}
@@ -27,6 +70,7 @@ public class JobInfo {
}
public Builder setPeriodic(long dummy) {
jobInfo.periodicMillis = dummy;
return this;
}
@@ -55,7 +99,7 @@ public class JobInfo {
}
public JobInfo build() {
return new JobInfo();
return jobInfo;
}
}
}

View File

@@ -0,0 +1,16 @@
package android.app.job;
import android.os.PersistableBundle;
public class JobParameters {
JobInfo jobInfo;
JobParameters(JobInfo jobInfo) {
this.jobInfo = jobInfo;
}
public PersistableBundle getExtras() {
return jobInfo.getExtras();
}
}

View File

@@ -1,9 +1,21 @@
package android.app.job;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Slog;
public class JobScheduler {
private static final String TAG = "JobScheduler";
static Map<Integer,JobInfo> pendingJobs = new HashMap<>();
private static Map<Class<? extends JobService>, JobService> runningServices = new HashMap<>();
/**
* Retrieve all jobs that have been scheduled by the calling application.
*
@@ -11,7 +23,7 @@ public class JobScheduler {
* currently started as well as those that are still waiting to run.
*/
public List<JobInfo> getAllPendingJobs() {
return new ArrayList<JobInfo>();
return new ArrayList<>(pendingJobs.values());
};
public int enqueue(JobInfo job, JobWorkItem work) {
@@ -19,9 +31,47 @@ public class JobScheduler {
}
public int schedule(JobInfo job) {
Slog.i(TAG, "JobScheduler.schedule() called with job: " + job);
if (pendingJobs.containsKey(job.getId()))
return 1; //RESULT_SUCCESS
pendingJobs.put(job.getId(), job);
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
try {
String className = job.getService().getClassName();
Class <? extends JobService> cls = Class.forName(className).asSubclass(JobService.class);
if (!runningServices.containsKey(cls)) {
JobService service = cls.getConstructor().newInstance();
service.attachBaseContext(new Context());
service.onCreate();
runningServices.put(cls, service);
}
job.running = true;
boolean result = runningServices.get(cls).onStartJob(new JobParameters(job));
Slog.i(TAG, "onStartJob() returned " + result);
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
}
}, job.minLatencyMillis);
return 1; //RESULT_SUCCESS
}
public void cancel(int dummy) {
public void cancel(int id) {
JobInfo job = pendingJobs.remove(id);
if (job != null && job.running) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
JobService service = runningServices.get(job.getService().getClass());
if (service != null) {
JobParameters params = new JobParameters(job);
service.onStopJob(params);
job.running = false;
}
}
});
}
}
}

View File

@@ -1,4 +1,31 @@
package android.app.job;
public class JobService {
import android.app.Service;
import android.os.Handler;
import android.os.Looper;
import android.util.Slog;
public abstract class JobService extends Service {
private static final String TAG = "JobService";
public abstract boolean onStartJob(JobParameters params);
public abstract boolean onStopJob(JobParameters params);
public void jobFinished(JobParameters params, boolean needsReschedule) {
Slog.i(TAG, "jobFinished(" + params + ", " + needsReschedule + ") called");
params.jobInfo.running = false;
if (needsReschedule || params.jobInfo.periodicMillis != 0) {
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
params.jobInfo.running = true;
boolean result = onStartJob(params);
Slog.i(TAG, "onStartJob() returned " + result);
}
}, needsReschedule ? params.jobInfo.initialBackoffMillis : params.jobInfo.periodicMillis);
} else {
JobScheduler.pendingJobs.remove(params.jobInfo.getId());
}
}
}

View File

@@ -26,6 +26,7 @@ public class Intent implements Parcelable {
private int flags;
private String type;
private String packageName;
private Intent selector;
public Intent() {}
public Intent(Intent o) {
@@ -402,11 +403,17 @@ public class Intent implements Parcelable {
return null;
}
public void setSelector(Intent selector) {}
public void setSelector(Intent selector) {
this.selector = selector;
}
public void setClipData(ClipData clip) {}
public String resolveType(Context context) {
return type;
}
public Intent getSelector() {
return selector;
}
}

View File

@@ -1,4 +1,9 @@
package android.os;
public class PersistableBundle extends BaseBundle {
@Override
public synchronized String toString() {
return "PersistableBundle[" + mMap.toString() + "]";
}
}

View File

@@ -94,13 +94,13 @@ public class AndroidKeyStore extends KeyStoreSpi {
@Override
public boolean engineIsKeyEntry(String alias) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'engineIsKeyEntry'");
return map.containsKey(alias);
}
@Override
public boolean engineIsCertificateEntry(String alias) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'engineIsCertificateEntry'");
return false;
}
@Override

View File

@@ -2221,4 +2221,6 @@ public class View implements Drawable.Callback {
public WindowInsets computeSystemWindowInsets(WindowInsets insets, Rect contentInsets) { return insets; }
public boolean isDuplicateParentStateEnabled() { return false; }
public void setBackgroundTintMode(PorterDuff.Mode tintMode) {}
}

View File

@@ -0,0 +1,17 @@
package android.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
public class TimePicker extends View {
public TimePicker(Context context) {
super(context);
}
public TimePicker(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
}
}

View File

@@ -64,6 +64,7 @@ srcs = [
'android/app/backup/BackupAgentHelper.java',
'android/app/backup/BackupManager.java',
'android/app/job/JobInfo.java',
'android/app/job/JobParameters.java',
'android/app/job/JobScheduler.java',
'android/app/job/JobService.java',
'android/app/job/JobWorkItem.java',
@@ -644,6 +645,7 @@ srcs = [
'android/widget/TableRow.java',
'android/widget/TextSwitcher.java',
'android/widget/TextView.java',
'android/widget/TimePicker.java',
'android/widget/Toast.java',
'android/widget/ToggleButton.java',
'android/widget/Toolbar.java',