Loading core/res/AndroidManifest.xml +4 −0 Original line number Diff line number Diff line Loading @@ -4929,6 +4929,10 @@ android:resource="@xml/autofill_compat_accessibility_service" /> </service> <service android:name="com.google.android.startop.iorap.IorapForwardingService$IorapdJobServiceProxy" android:permission="android.permission.BIND_JOB_SERVICE" > </service> </application> </manifest> startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java +315 −2 Original line number Diff line number Diff line Loading @@ -19,6 +19,10 @@ package com.google.android.startop.iorap; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobService; import android.app.job.JobScheduler; import android.content.ComponentName; import android.content.Context; import android.content.Intent; Loading @@ -42,13 +46,16 @@ import com.android.server.wm.ActivityMetricsLaunchObserver.Temperature; import com.android.server.wm.ActivityMetricsLaunchObserverRegistry; import com.android.server.wm.ActivityTaskManagerInternal; import java.util.concurrent.TimeUnit; import java.util.HashMap; /** * System-server-local proxy into the {@code IIorap} native service. */ public class IorapForwardingService extends SystemService { public static final String TAG = "IorapForwardingService"; /** $> adb shell 'setprop log.tag.IorapdForwardingService VERBOSE' */ /** $> adb shell 'setprop log.tag.IorapForwardingService VERBOSE' */ public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); /** $> adb shell 'setprop ro.iorapd.enable true' */ private static boolean IS_ENABLED = SystemProperties.getBoolean("ro.iorapd.enable", true); Loading @@ -56,12 +63,20 @@ public class IorapForwardingService extends SystemService { private static boolean WTF_CRASH = SystemProperties.getBoolean( "iorapd.forwarding_service.wtf_crash", false); // "Unique" job ID from the service name. Also equal to 283673059. public static final int JOB_ID_IORAPD = encodeEnglishAlphabetStringIntoInt("iorapd"); // Run every 24 hours. public static final long JOB_INTERVAL_MS = TimeUnit.HOURS.toMillis(24); private IIorap mIorapRemote; private final Object mLock = new Object(); /** Handle onBinderDeath by periodically trying to reconnect. */ private final Handler mHandler = new BinderConnectionHandler(IoThread.getHandler().getLooper()); private volatile IorapdJobService mJobService; // Write-once (null -> non-null forever). private volatile static IorapForwardingService sSelfService; // Write once (null -> non-null). /** * Initializes the system service. * <p> Loading @@ -73,6 +88,15 @@ public class IorapForwardingService extends SystemService { */ public IorapForwardingService(Context context) { super(context); if (DEBUG) { Log.v(TAG, "IorapForwardingService (Context=" + context.toString() + ")"); } if (sSelfService != null) { throw new AssertionError("only one service instance allowed"); } sSelfService = this; } //<editor-fold desc="Providers"> Loading Loading @@ -117,6 +141,10 @@ public class IorapForwardingService extends SystemService { public void binderDied() { Log.w(TAG, "iorapd has died"); retryConnectToRemoteAndConfigure(/*attempts*/0); if (mJobService != null) { mJobService.onIorapdDisconnected(); } } }; } Loading @@ -139,6 +167,24 @@ public class IorapForwardingService extends SystemService { retryConnectToRemoteAndConfigure(/*attempts*/0); } @Override public void onBootPhase(int phase) { if (phase == PHASE_BOOT_COMPLETED) { if (DEBUG) { Log.v(TAG, "onBootPhase(PHASE_BOOT_COMPLETED)"); } if (isIorapEnabled()) { // Set up a recurring background job. This has to be done in a later phase since it // has a dependency the job scheduler. // // Doing this too early can result in a ServiceNotFoundException for 'jobservice' // or a null reference for #getSystemService(JobScheduler.class) mJobService = new IorapdJobService(getContext()); } } } private class BinderConnectionHandler extends Handler { public BinderConnectionHandler(android.os.Looper looper) { super(looper); Loading Loading @@ -336,6 +382,227 @@ public class IorapForwardingService extends SystemService { } } /** * Debugging: * * $> adb shell dumpsys jobscheduler * * Search for 'IorapdJobServiceProxy'. * * JOB #1000/283673059: 6e54ed android/com.google.android.startop.iorap.IorapForwardingService$IorapdJobServiceProxy * ^ ^ ^ * (uid, job id) ComponentName(package/class) * * Forcing the job to be run, ignoring constraints: * * $> adb shell cmd jobscheduler run -f android 283673059 * ^ ^ * package job_id * * ------------------------------------------------------------ * * This class is instantiated newly by the JobService every time * it wants to run a new job. * * We need to forward invocations to the current running instance of * IorapForwardingService#IorapdJobService. * * Visibility: Must be accessible from android.app.AppComponentFactory */ public static class IorapdJobServiceProxy extends JobService { public IorapdJobServiceProxy() { getActualIorapdJobService().bindProxy(this); } @NonNull private IorapdJobService getActualIorapdJobService() { // Can't ever be null, because the guarantee is that the // IorapForwardingService is always running. // We are in the same process as Job Service. return sSelfService.mJobService; } // Called by system to start the job. @Override public boolean onStartJob(JobParameters params) { return getActualIorapdJobService().onStartJob(params); } // Called by system to prematurely stop the job. @Override public boolean onStopJob(JobParameters params) { return getActualIorapdJobService().onStopJob(params); } } private class IorapdJobService extends JobService { private final ComponentName IORAPD_COMPONENT_NAME; private final Object mLock = new Object(); // Jobs currently running remotely on iorapd. // They were started by the JobScheduler and need to be finished. private final HashMap<RequestId, JobParameters> mRunningJobs = new HashMap<>(); private final JobInfo IORAPD_JOB_INFO; private volatile IorapdJobServiceProxy mProxy; public void bindProxy(IorapdJobServiceProxy proxy) { mProxy = proxy; } // Create a new job service which immediately schedules a 24-hour idle maintenance mode // background job to execute. public IorapdJobService(Context context) { if (DEBUG) { Log.v(TAG, "IorapdJobService (Context=" + context.toString() + ")"); } // Schedule the proxy class to be instantiated by the JobScheduler // when it is time to invoke background jobs for IorapForwardingService. // This also needs a BIND_JOB_SERVICE permission in // frameworks/base/core/res/AndroidManifest.xml IORAPD_COMPONENT_NAME = new ComponentName(context, IorapdJobServiceProxy.class); JobInfo.Builder builder = new JobInfo.Builder(JOB_ID_IORAPD, IORAPD_COMPONENT_NAME); builder.setPeriodic(JOB_INTERVAL_MS); builder.setPrefetch(true); builder.setRequiresCharging(true); builder.setRequiresDeviceIdle(true); builder.setRequiresStorageNotLow(true); IORAPD_JOB_INFO = builder.build(); JobScheduler js = context.getSystemService(JobScheduler.class); js.schedule(IORAPD_JOB_INFO); Log.d(TAG, "BgJob Scheduled (jobId=" + JOB_ID_IORAPD + ", interval: " + JOB_INTERVAL_MS + "ms)"); } // Called by system to start the job. @Override public boolean onStartJob(JobParameters params) { // Tell iorapd to start a background job. Log.d(TAG, "Starting background job: " + params.toString()); // We wait until that job's sequence ID returns to us with 'Completed', RequestId request; synchronized (mLock) { // TODO: would be cleaner if we got the request from the 'invokeRemote' function. // Better yet, consider a Pair<RequestId, Future<TaskResult>> or similar. request = RequestId.nextValueForSequence(); mRunningJobs.put(request, params); } if (!invokeRemote( () -> mIorapRemote.onJobScheduledEvent(request, JobScheduledEvent.createIdleMaintenance( JobScheduledEvent.TYPE_START_JOB, params)) )) { synchronized (mLock) { mRunningJobs.remove(request); // Avoid memory leaks. } // Something went wrong on the remote side. Treat the job as being // 'already finished' (i.e. immediately release wake lock). return false; } // True -> keep the wakelock acquired until #jobFinished is called. return true; } // Called by system to prematurely stop the job. @Override public boolean onStopJob(JobParameters params) { // As this is unexpected behavior, print a warning. Log.w(TAG, "onStopJob(params=" + params.toString() + ")"); // No longer track this job (avoids a memory leak). boolean wasTracking = false; synchronized (mLock) { for (HashMap.Entry<RequestId, JobParameters> entry : mRunningJobs.entrySet()) { if (entry.getValue().getJobId() == params.getJobId()) { mRunningJobs.remove(entry.getKey()); wasTracking = true; } } } // Notify iorapd to stop (abort) the job. if (wasTracking) { invokeRemote(() -> mIorapRemote.onJobScheduledEvent(RequestId.nextValueForSequence(), JobScheduledEvent.createIdleMaintenance( JobScheduledEvent.TYPE_STOP_JOB, params)) ); } else { // Even weirder. This could only be considered "correct" if iorapd reported success // concurrently to the JobService requesting an onStopJob. Log.e(TAG, "Untracked onStopJob request"); // see above Log.w for the params. } // Yes, retry the job at a later time no matter what. return true; } // Listen to *all* task completes for all requests. // The majority of these might be unrelated to background jobs. public void onIorapdTaskCompleted(RequestId requestId) { JobParameters jobParameters; synchronized (mLock) { jobParameters = mRunningJobs.remove(requestId); } // Typical case: This was a task callback unrelated to our jobs. if (jobParameters == null) { return; } if (DEBUG) { Log.v(TAG, String.format("IorapdJobService#onIorapdTaskCompleted(%s), found params=%s", requestId, jobParameters)); } Log.d(TAG, "Finished background job: " + jobParameters.toString()); // Job is successful and periodic. Do not 'reschedule' according to the back-off // criteria. // // This releases the wakelock that was acquired in #onStartJob. IorapdJobServiceProxy proxy = mProxy; if (proxy != null) { proxy.jobFinished(jobParameters, /*reschedule*/false); } // Cannot call 'jobFinished' on 'this' because it was not constructed // from the JobService, so it would get an NPE when calling mEngine. } public void onIorapdDisconnected() { synchronized (mLock) { mRunningJobs.clear(); } if (DEBUG) { Log.v(TAG, String.format("IorapdJobService#onIorapdDisconnected")); } // TODO: should we try to resubmit all incomplete jobs after it's reconnected? } } private class RemoteTaskListener extends ITaskListener.Stub { @Override public void onProgress(RequestId requestId, TaskResult result) throws RemoteException { Loading @@ -354,18 +621,24 @@ public class IorapForwardingService extends SystemService { String.format("RemoteTaskListener#onComplete(%s, %s)", requestId, result)); } if (mJobService != null) { mJobService.onIorapdTaskCompleted(requestId); } // TODO: implement rest. } } /** Allow passing lambdas to #invokeRemote */ private interface RemoteRunnable { // TODO: run(RequestId) ? void run() throws RemoteException; } private static void invokeRemote(RemoteRunnable r) { private static boolean invokeRemote(RemoteRunnable r) { try { r.run(); return true; } catch (RemoteException e) { // This could be a logic error (remote side returning error), which we need to fix. // Loading @@ -377,6 +650,7 @@ public class IorapForwardingService extends SystemService { // // DeadObjectExceptions are recovered from using DeathRecipient and #linkToDeath. handleRemoteError(e); return false; } } Loading @@ -389,4 +663,43 @@ public class IorapForwardingService extends SystemService { Log.wtf(TAG, t); } } // Encode A-Z bitstring into bits. Every character is bits. // Characters outside of the range [a,z] are considered out of range. // // The least significant bits hold the last character. // First 2 bits are left as 0. private static int encodeEnglishAlphabetStringIntoInt(String name) { int value = 0; final int CHARS_PER_INT = 6; final int BITS_PER_CHAR = 5; // Note: 2 top bits are unused, this also means our values are non-negative. final char CHAR_LOWER = 'a'; final char CHAR_UPPER = 'z'; if (name.length() > CHARS_PER_INT) { throw new IllegalArgumentException( "String too long. Cannot encode more than 6 chars: " + name); } for (int i = 0; i < name.length(); ++i) { char c = name.charAt(i); if (c < CHAR_LOWER || c > CHAR_UPPER) { throw new IllegalArgumentException("String has out-of-range [a-z] chars: " + name); } // Avoid sign extension during promotion. int cur_value = (c & 0xFFFF) - (CHAR_LOWER & 0xFFFF); if (cur_value >= (1 << BITS_PER_CHAR)) { throw new AssertionError("wtf? i=" + i + ", name=" + name); } value = value << BITS_PER_CHAR; value = value | cur_value; } return value; } } startop/iorap/src/com/google/android/startop/iorap/JobScheduledEvent.java 0 → 100644 +154 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.startop.iorap; import android.app.job.JobParameters; import android.annotation.NonNull; import android.os.Parcelable; import android.os.Parcel; import android.annotation.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Forward JobService events to iorapd. <br /><br /> * * iorapd sometimes need to use background jobs. Forwarding these events to iorapd * notifies iorapd when it is an opportune time to execute these background jobs. * * @hide */ public class JobScheduledEvent implements Parcelable { /** JobService#onJobStarted */ public static final int TYPE_START_JOB = 0; /** JobService#onJobStopped */ public static final int TYPE_STOP_JOB = 1; private static final int TYPE_MAX = 0; /** @hide */ @IntDef(flag = true, prefix = { "TYPE_" }, value = { TYPE_START_JOB, TYPE_STOP_JOB, }) @Retention(RetentionPolicy.SOURCE) public @interface Type {} @Type public final int type; /** @see JobParameters#getJobId() */ public final int jobId; /** Device is 'idle' and it's charging (plugged in). */ public static final int SORT_IDLE_MAINTENANCE = 0; private static final int SORT_MAX = 0; /** @hide */ @IntDef(flag = true, prefix = { "SORT_" }, value = { SORT_IDLE_MAINTENANCE, }) @Retention(RetentionPolicy.SOURCE) public @interface Sort {} /** * Roughly corresponds to the {@code extras} fields in a JobParameters. */ @Sort public final int sort; /** * Creates a {@link #SORT_IDLE_MAINTENANCE} event from the type and job parameters. * * Only the job ID is retained from {@code jobParams}, all other param info is dropped. */ @NonNull public static JobScheduledEvent createIdleMaintenance(@Type int type, JobParameters jobParams) { return new JobScheduledEvent(type, jobParams.getJobId(), SORT_IDLE_MAINTENANCE); } private JobScheduledEvent(@Type int type, int jobId, @Sort int sort) { this.type = type; this.jobId = jobId; this.sort = sort; checkConstructorArguments(); } private void checkConstructorArguments() { CheckHelpers.checkTypeInRange(type, TYPE_MAX); // No check for 'jobId': any int is valid. CheckHelpers.checkTypeInRange(sort, SORT_MAX); } @Override public boolean equals(Object other) { if (this == other) { return true; } else if (other instanceof JobScheduledEvent) { return equals((JobScheduledEvent) other); } return false; } private boolean equals(JobScheduledEvent other) { return type == other.type && jobId == other.jobId && sort == other.sort; } @Override public String toString() { return String.format("{type: %d, jobId: %d, sort: %d}", type, jobId, sort); } //<editor-fold desc="Binder boilerplate"> @Override public void writeToParcel(Parcel out, int flags) { out.writeInt(type); out.writeInt(jobId); out.writeInt(sort); // We do not parcel the entire JobParameters here because there is no C++ equivalent // of that class [which the iorapd side of the binder interface requires]. } private JobScheduledEvent(Parcel in) { this.type = in.readInt(); this.jobId = in.readInt(); this.sort = in.readInt(); checkConstructorArguments(); } @Override public int describeContents() { return 0; } public static final Parcelable.Creator<JobScheduledEvent> CREATOR = new Parcelable.Creator<JobScheduledEvent>() { public JobScheduledEvent createFromParcel(Parcel in) { return new JobScheduledEvent(in); } public JobScheduledEvent[] newArray(int size) { return new JobScheduledEvent[size]; } }; //</editor-fold> } startop/iorap/src/com/google/android/startop/iorap/RequestId.java +5 −0 Original line number Diff line number Diff line Loading @@ -74,6 +74,11 @@ public class RequestId implements Parcelable { return String.format("{requestId: %d}", requestId); } @Override public int hashCode() { return Long.hashCode(requestId); } @Override public boolean equals(Object other) { if (this == other) { Loading Loading
core/res/AndroidManifest.xml +4 −0 Original line number Diff line number Diff line Loading @@ -4929,6 +4929,10 @@ android:resource="@xml/autofill_compat_accessibility_service" /> </service> <service android:name="com.google.android.startop.iorap.IorapForwardingService$IorapdJobServiceProxy" android:permission="android.permission.BIND_JOB_SERVICE" > </service> </application> </manifest>
startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java +315 −2 Original line number Diff line number Diff line Loading @@ -19,6 +19,10 @@ package com.google.android.startop.iorap; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobService; import android.app.job.JobScheduler; import android.content.ComponentName; import android.content.Context; import android.content.Intent; Loading @@ -42,13 +46,16 @@ import com.android.server.wm.ActivityMetricsLaunchObserver.Temperature; import com.android.server.wm.ActivityMetricsLaunchObserverRegistry; import com.android.server.wm.ActivityTaskManagerInternal; import java.util.concurrent.TimeUnit; import java.util.HashMap; /** * System-server-local proxy into the {@code IIorap} native service. */ public class IorapForwardingService extends SystemService { public static final String TAG = "IorapForwardingService"; /** $> adb shell 'setprop log.tag.IorapdForwardingService VERBOSE' */ /** $> adb shell 'setprop log.tag.IorapForwardingService VERBOSE' */ public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); /** $> adb shell 'setprop ro.iorapd.enable true' */ private static boolean IS_ENABLED = SystemProperties.getBoolean("ro.iorapd.enable", true); Loading @@ -56,12 +63,20 @@ public class IorapForwardingService extends SystemService { private static boolean WTF_CRASH = SystemProperties.getBoolean( "iorapd.forwarding_service.wtf_crash", false); // "Unique" job ID from the service name. Also equal to 283673059. public static final int JOB_ID_IORAPD = encodeEnglishAlphabetStringIntoInt("iorapd"); // Run every 24 hours. public static final long JOB_INTERVAL_MS = TimeUnit.HOURS.toMillis(24); private IIorap mIorapRemote; private final Object mLock = new Object(); /** Handle onBinderDeath by periodically trying to reconnect. */ private final Handler mHandler = new BinderConnectionHandler(IoThread.getHandler().getLooper()); private volatile IorapdJobService mJobService; // Write-once (null -> non-null forever). private volatile static IorapForwardingService sSelfService; // Write once (null -> non-null). /** * Initializes the system service. * <p> Loading @@ -73,6 +88,15 @@ public class IorapForwardingService extends SystemService { */ public IorapForwardingService(Context context) { super(context); if (DEBUG) { Log.v(TAG, "IorapForwardingService (Context=" + context.toString() + ")"); } if (sSelfService != null) { throw new AssertionError("only one service instance allowed"); } sSelfService = this; } //<editor-fold desc="Providers"> Loading Loading @@ -117,6 +141,10 @@ public class IorapForwardingService extends SystemService { public void binderDied() { Log.w(TAG, "iorapd has died"); retryConnectToRemoteAndConfigure(/*attempts*/0); if (mJobService != null) { mJobService.onIorapdDisconnected(); } } }; } Loading @@ -139,6 +167,24 @@ public class IorapForwardingService extends SystemService { retryConnectToRemoteAndConfigure(/*attempts*/0); } @Override public void onBootPhase(int phase) { if (phase == PHASE_BOOT_COMPLETED) { if (DEBUG) { Log.v(TAG, "onBootPhase(PHASE_BOOT_COMPLETED)"); } if (isIorapEnabled()) { // Set up a recurring background job. This has to be done in a later phase since it // has a dependency the job scheduler. // // Doing this too early can result in a ServiceNotFoundException for 'jobservice' // or a null reference for #getSystemService(JobScheduler.class) mJobService = new IorapdJobService(getContext()); } } } private class BinderConnectionHandler extends Handler { public BinderConnectionHandler(android.os.Looper looper) { super(looper); Loading Loading @@ -336,6 +382,227 @@ public class IorapForwardingService extends SystemService { } } /** * Debugging: * * $> adb shell dumpsys jobscheduler * * Search for 'IorapdJobServiceProxy'. * * JOB #1000/283673059: 6e54ed android/com.google.android.startop.iorap.IorapForwardingService$IorapdJobServiceProxy * ^ ^ ^ * (uid, job id) ComponentName(package/class) * * Forcing the job to be run, ignoring constraints: * * $> adb shell cmd jobscheduler run -f android 283673059 * ^ ^ * package job_id * * ------------------------------------------------------------ * * This class is instantiated newly by the JobService every time * it wants to run a new job. * * We need to forward invocations to the current running instance of * IorapForwardingService#IorapdJobService. * * Visibility: Must be accessible from android.app.AppComponentFactory */ public static class IorapdJobServiceProxy extends JobService { public IorapdJobServiceProxy() { getActualIorapdJobService().bindProxy(this); } @NonNull private IorapdJobService getActualIorapdJobService() { // Can't ever be null, because the guarantee is that the // IorapForwardingService is always running. // We are in the same process as Job Service. return sSelfService.mJobService; } // Called by system to start the job. @Override public boolean onStartJob(JobParameters params) { return getActualIorapdJobService().onStartJob(params); } // Called by system to prematurely stop the job. @Override public boolean onStopJob(JobParameters params) { return getActualIorapdJobService().onStopJob(params); } } private class IorapdJobService extends JobService { private final ComponentName IORAPD_COMPONENT_NAME; private final Object mLock = new Object(); // Jobs currently running remotely on iorapd. // They were started by the JobScheduler and need to be finished. private final HashMap<RequestId, JobParameters> mRunningJobs = new HashMap<>(); private final JobInfo IORAPD_JOB_INFO; private volatile IorapdJobServiceProxy mProxy; public void bindProxy(IorapdJobServiceProxy proxy) { mProxy = proxy; } // Create a new job service which immediately schedules a 24-hour idle maintenance mode // background job to execute. public IorapdJobService(Context context) { if (DEBUG) { Log.v(TAG, "IorapdJobService (Context=" + context.toString() + ")"); } // Schedule the proxy class to be instantiated by the JobScheduler // when it is time to invoke background jobs for IorapForwardingService. // This also needs a BIND_JOB_SERVICE permission in // frameworks/base/core/res/AndroidManifest.xml IORAPD_COMPONENT_NAME = new ComponentName(context, IorapdJobServiceProxy.class); JobInfo.Builder builder = new JobInfo.Builder(JOB_ID_IORAPD, IORAPD_COMPONENT_NAME); builder.setPeriodic(JOB_INTERVAL_MS); builder.setPrefetch(true); builder.setRequiresCharging(true); builder.setRequiresDeviceIdle(true); builder.setRequiresStorageNotLow(true); IORAPD_JOB_INFO = builder.build(); JobScheduler js = context.getSystemService(JobScheduler.class); js.schedule(IORAPD_JOB_INFO); Log.d(TAG, "BgJob Scheduled (jobId=" + JOB_ID_IORAPD + ", interval: " + JOB_INTERVAL_MS + "ms)"); } // Called by system to start the job. @Override public boolean onStartJob(JobParameters params) { // Tell iorapd to start a background job. Log.d(TAG, "Starting background job: " + params.toString()); // We wait until that job's sequence ID returns to us with 'Completed', RequestId request; synchronized (mLock) { // TODO: would be cleaner if we got the request from the 'invokeRemote' function. // Better yet, consider a Pair<RequestId, Future<TaskResult>> or similar. request = RequestId.nextValueForSequence(); mRunningJobs.put(request, params); } if (!invokeRemote( () -> mIorapRemote.onJobScheduledEvent(request, JobScheduledEvent.createIdleMaintenance( JobScheduledEvent.TYPE_START_JOB, params)) )) { synchronized (mLock) { mRunningJobs.remove(request); // Avoid memory leaks. } // Something went wrong on the remote side. Treat the job as being // 'already finished' (i.e. immediately release wake lock). return false; } // True -> keep the wakelock acquired until #jobFinished is called. return true; } // Called by system to prematurely stop the job. @Override public boolean onStopJob(JobParameters params) { // As this is unexpected behavior, print a warning. Log.w(TAG, "onStopJob(params=" + params.toString() + ")"); // No longer track this job (avoids a memory leak). boolean wasTracking = false; synchronized (mLock) { for (HashMap.Entry<RequestId, JobParameters> entry : mRunningJobs.entrySet()) { if (entry.getValue().getJobId() == params.getJobId()) { mRunningJobs.remove(entry.getKey()); wasTracking = true; } } } // Notify iorapd to stop (abort) the job. if (wasTracking) { invokeRemote(() -> mIorapRemote.onJobScheduledEvent(RequestId.nextValueForSequence(), JobScheduledEvent.createIdleMaintenance( JobScheduledEvent.TYPE_STOP_JOB, params)) ); } else { // Even weirder. This could only be considered "correct" if iorapd reported success // concurrently to the JobService requesting an onStopJob. Log.e(TAG, "Untracked onStopJob request"); // see above Log.w for the params. } // Yes, retry the job at a later time no matter what. return true; } // Listen to *all* task completes for all requests. // The majority of these might be unrelated to background jobs. public void onIorapdTaskCompleted(RequestId requestId) { JobParameters jobParameters; synchronized (mLock) { jobParameters = mRunningJobs.remove(requestId); } // Typical case: This was a task callback unrelated to our jobs. if (jobParameters == null) { return; } if (DEBUG) { Log.v(TAG, String.format("IorapdJobService#onIorapdTaskCompleted(%s), found params=%s", requestId, jobParameters)); } Log.d(TAG, "Finished background job: " + jobParameters.toString()); // Job is successful and periodic. Do not 'reschedule' according to the back-off // criteria. // // This releases the wakelock that was acquired in #onStartJob. IorapdJobServiceProxy proxy = mProxy; if (proxy != null) { proxy.jobFinished(jobParameters, /*reschedule*/false); } // Cannot call 'jobFinished' on 'this' because it was not constructed // from the JobService, so it would get an NPE when calling mEngine. } public void onIorapdDisconnected() { synchronized (mLock) { mRunningJobs.clear(); } if (DEBUG) { Log.v(TAG, String.format("IorapdJobService#onIorapdDisconnected")); } // TODO: should we try to resubmit all incomplete jobs after it's reconnected? } } private class RemoteTaskListener extends ITaskListener.Stub { @Override public void onProgress(RequestId requestId, TaskResult result) throws RemoteException { Loading @@ -354,18 +621,24 @@ public class IorapForwardingService extends SystemService { String.format("RemoteTaskListener#onComplete(%s, %s)", requestId, result)); } if (mJobService != null) { mJobService.onIorapdTaskCompleted(requestId); } // TODO: implement rest. } } /** Allow passing lambdas to #invokeRemote */ private interface RemoteRunnable { // TODO: run(RequestId) ? void run() throws RemoteException; } private static void invokeRemote(RemoteRunnable r) { private static boolean invokeRemote(RemoteRunnable r) { try { r.run(); return true; } catch (RemoteException e) { // This could be a logic error (remote side returning error), which we need to fix. // Loading @@ -377,6 +650,7 @@ public class IorapForwardingService extends SystemService { // // DeadObjectExceptions are recovered from using DeathRecipient and #linkToDeath. handleRemoteError(e); return false; } } Loading @@ -389,4 +663,43 @@ public class IorapForwardingService extends SystemService { Log.wtf(TAG, t); } } // Encode A-Z bitstring into bits. Every character is bits. // Characters outside of the range [a,z] are considered out of range. // // The least significant bits hold the last character. // First 2 bits are left as 0. private static int encodeEnglishAlphabetStringIntoInt(String name) { int value = 0; final int CHARS_PER_INT = 6; final int BITS_PER_CHAR = 5; // Note: 2 top bits are unused, this also means our values are non-negative. final char CHAR_LOWER = 'a'; final char CHAR_UPPER = 'z'; if (name.length() > CHARS_PER_INT) { throw new IllegalArgumentException( "String too long. Cannot encode more than 6 chars: " + name); } for (int i = 0; i < name.length(); ++i) { char c = name.charAt(i); if (c < CHAR_LOWER || c > CHAR_UPPER) { throw new IllegalArgumentException("String has out-of-range [a-z] chars: " + name); } // Avoid sign extension during promotion. int cur_value = (c & 0xFFFF) - (CHAR_LOWER & 0xFFFF); if (cur_value >= (1 << BITS_PER_CHAR)) { throw new AssertionError("wtf? i=" + i + ", name=" + name); } value = value << BITS_PER_CHAR; value = value | cur_value; } return value; } }
startop/iorap/src/com/google/android/startop/iorap/JobScheduledEvent.java 0 → 100644 +154 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.startop.iorap; import android.app.job.JobParameters; import android.annotation.NonNull; import android.os.Parcelable; import android.os.Parcel; import android.annotation.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Forward JobService events to iorapd. <br /><br /> * * iorapd sometimes need to use background jobs. Forwarding these events to iorapd * notifies iorapd when it is an opportune time to execute these background jobs. * * @hide */ public class JobScheduledEvent implements Parcelable { /** JobService#onJobStarted */ public static final int TYPE_START_JOB = 0; /** JobService#onJobStopped */ public static final int TYPE_STOP_JOB = 1; private static final int TYPE_MAX = 0; /** @hide */ @IntDef(flag = true, prefix = { "TYPE_" }, value = { TYPE_START_JOB, TYPE_STOP_JOB, }) @Retention(RetentionPolicy.SOURCE) public @interface Type {} @Type public final int type; /** @see JobParameters#getJobId() */ public final int jobId; /** Device is 'idle' and it's charging (plugged in). */ public static final int SORT_IDLE_MAINTENANCE = 0; private static final int SORT_MAX = 0; /** @hide */ @IntDef(flag = true, prefix = { "SORT_" }, value = { SORT_IDLE_MAINTENANCE, }) @Retention(RetentionPolicy.SOURCE) public @interface Sort {} /** * Roughly corresponds to the {@code extras} fields in a JobParameters. */ @Sort public final int sort; /** * Creates a {@link #SORT_IDLE_MAINTENANCE} event from the type and job parameters. * * Only the job ID is retained from {@code jobParams}, all other param info is dropped. */ @NonNull public static JobScheduledEvent createIdleMaintenance(@Type int type, JobParameters jobParams) { return new JobScheduledEvent(type, jobParams.getJobId(), SORT_IDLE_MAINTENANCE); } private JobScheduledEvent(@Type int type, int jobId, @Sort int sort) { this.type = type; this.jobId = jobId; this.sort = sort; checkConstructorArguments(); } private void checkConstructorArguments() { CheckHelpers.checkTypeInRange(type, TYPE_MAX); // No check for 'jobId': any int is valid. CheckHelpers.checkTypeInRange(sort, SORT_MAX); } @Override public boolean equals(Object other) { if (this == other) { return true; } else if (other instanceof JobScheduledEvent) { return equals((JobScheduledEvent) other); } return false; } private boolean equals(JobScheduledEvent other) { return type == other.type && jobId == other.jobId && sort == other.sort; } @Override public String toString() { return String.format("{type: %d, jobId: %d, sort: %d}", type, jobId, sort); } //<editor-fold desc="Binder boilerplate"> @Override public void writeToParcel(Parcel out, int flags) { out.writeInt(type); out.writeInt(jobId); out.writeInt(sort); // We do not parcel the entire JobParameters here because there is no C++ equivalent // of that class [which the iorapd side of the binder interface requires]. } private JobScheduledEvent(Parcel in) { this.type = in.readInt(); this.jobId = in.readInt(); this.sort = in.readInt(); checkConstructorArguments(); } @Override public int describeContents() { return 0; } public static final Parcelable.Creator<JobScheduledEvent> CREATOR = new Parcelable.Creator<JobScheduledEvent>() { public JobScheduledEvent createFromParcel(Parcel in) { return new JobScheduledEvent(in); } public JobScheduledEvent[] newArray(int size) { return new JobScheduledEvent[size]; } }; //</editor-fold> }
startop/iorap/src/com/google/android/startop/iorap/RequestId.java +5 −0 Original line number Diff line number Diff line Loading @@ -74,6 +74,11 @@ public class RequestId implements Parcelable { return String.format("{requestId: %d}", requestId); } @Override public int hashCode() { return Long.hashCode(requestId); } @Override public boolean equals(Object other) { if (this == other) { Loading