From c60f8e38507c58621e45ece859d081855c7c8765 Mon Sep 17 00:00:00 2001 From: Julian Winkler Date: Sat, 4 Oct 2025 13:05:00 +0200 Subject: [PATCH] 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. --- src/api-impl/android/app/ActivityManager.java | 13 ++++- src/api-impl/android/app/AlarmManager.java | 48 +++++++++++++++-- src/api-impl/android/app/job/JobInfo.java | 48 ++++++++++++++++- .../android/app/job/JobParameters.java | 16 ++++++ .../android/app/job/JobScheduler.java | 54 ++++++++++++++++++- src/api-impl/android/app/job/JobService.java | 29 +++++++++- src/api-impl/android/content/Intent.java | 9 +++- .../android/os/PersistableBundle.java | 5 ++ .../security/keystore/AndroidKeyStore.java | 4 +- src/api-impl/android/view/View.java | 2 + src/api-impl/android/widget/TimePicker.java | 17 ++++++ src/api-impl/meson.build | 2 + 12 files changed, 232 insertions(+), 15 deletions(-) create mode 100644 src/api-impl/android/app/job/JobParameters.java create mode 100644 src/api-impl/android/widget/TimePicker.java diff --git a/src/api-impl/android/app/ActivityManager.java b/src/api-impl/android/app/ActivityManager.java index d9c97cef..1cc4224d 100644 --- a/src/api-impl/android/app/ActivityManager.java +++ b/src/api-impl/android/app/ActivityManager.java @@ -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 getRunningAppProcesses() { - return null; + return Arrays.asList(new RunningAppProcessInfo(Process.myPid(), Context.this_application.getPackageName())); } public boolean isLowRamDevice() {return false;} diff --git a/src/api-impl/android/app/AlarmManager.java b/src/api-impl/android/app/AlarmManager.java index 270a14e1..09c2ad91 100644 --- a/src/api-impl/android/app/AlarmManager.java +++ b/src/api-impl/android/app/AlarmManager.java @@ -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); + } } diff --git a/src/api-impl/android/app/job/JobInfo.java b/src/api-impl/android/app/job/JobInfo.java index 3a621fe2..dc0af786 100644 --- a/src/api-impl/android/app/job/JobInfo.java +++ b/src/api-impl/android/app/job/JobInfo.java @@ -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; } } } diff --git a/src/api-impl/android/app/job/JobParameters.java b/src/api-impl/android/app/job/JobParameters.java new file mode 100644 index 00000000..bc244f02 --- /dev/null +++ b/src/api-impl/android/app/job/JobParameters.java @@ -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(); + } +} diff --git a/src/api-impl/android/app/job/JobScheduler.java b/src/api-impl/android/app/job/JobScheduler.java index 2f4c2325..89d2db52 100644 --- a/src/api-impl/android/app/job/JobScheduler.java +++ b/src/api-impl/android/app/job/JobScheduler.java @@ -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 pendingJobs = new HashMap<>(); + private static Map, 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 getAllPendingJobs() { - return new ArrayList(); + 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 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; + } + } + }); + } } } diff --git a/src/api-impl/android/app/job/JobService.java b/src/api-impl/android/app/job/JobService.java index 3fbfc461..403c284a 100644 --- a/src/api-impl/android/app/job/JobService.java +++ b/src/api-impl/android/app/job/JobService.java @@ -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()); + } + } } diff --git a/src/api-impl/android/content/Intent.java b/src/api-impl/android/content/Intent.java index 37e214b6..23d4057b 100644 --- a/src/api-impl/android/content/Intent.java +++ b/src/api-impl/android/content/Intent.java @@ -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; + } } diff --git a/src/api-impl/android/os/PersistableBundle.java b/src/api-impl/android/os/PersistableBundle.java index 62191054..55c531e7 100644 --- a/src/api-impl/android/os/PersistableBundle.java +++ b/src/api-impl/android/os/PersistableBundle.java @@ -1,4 +1,9 @@ package android.os; public class PersistableBundle extends BaseBundle { + + @Override + public synchronized String toString() { + return "PersistableBundle[" + mMap.toString() + "]"; + } } diff --git a/src/api-impl/android/security/keystore/AndroidKeyStore.java b/src/api-impl/android/security/keystore/AndroidKeyStore.java index 63a5f2bf..9c228bbc 100644 --- a/src/api-impl/android/security/keystore/AndroidKeyStore.java +++ b/src/api-impl/android/security/keystore/AndroidKeyStore.java @@ -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 diff --git a/src/api-impl/android/view/View.java b/src/api-impl/android/view/View.java index d4850411..9d983e31 100644 --- a/src/api-impl/android/view/View.java +++ b/src/api-impl/android/view/View.java @@ -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) {} } diff --git a/src/api-impl/android/widget/TimePicker.java b/src/api-impl/android/widget/TimePicker.java new file mode 100644 index 00000000..5f32fa73 --- /dev/null +++ b/src/api-impl/android/widget/TimePicker.java @@ -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); + } + +} diff --git a/src/api-impl/meson.build b/src/api-impl/meson.build index 2bdb8446..d9aabd6b 100644 --- a/src/api-impl/meson.build +++ b/src/api-impl/meson.build @@ -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',