diff options
4 files changed, 478 insertions, 2 deletions
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 82d058d6eefe..e5ff29bebaf6 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -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> diff --git a/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java b/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java index 22fc15985b05..31bd34199784 100644 --- a/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java +++ b/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java @@ -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; @@ -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); @@ -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> @@ -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"> @@ -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(); + } } }; } @@ -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); @@ -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 { @@ -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. // @@ -377,6 +650,7 @@ public class IorapForwardingService extends SystemService { // // DeadObjectExceptions are recovered from using DeathRecipient and #linkToDeath. handleRemoteError(e); + return false; } } @@ -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; + } } diff --git a/startop/iorap/src/com/google/android/startop/iorap/JobScheduledEvent.java b/startop/iorap/src/com/google/android/startop/iorap/JobScheduledEvent.java new file mode 100644 index 000000000000..9b3bfcb0fa4a --- /dev/null +++ b/startop/iorap/src/com/google/android/startop/iorap/JobScheduledEvent.java @@ -0,0 +1,154 @@ +/* + * 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> +} diff --git a/startop/iorap/src/com/google/android/startop/iorap/RequestId.java b/startop/iorap/src/com/google/android/startop/iorap/RequestId.java index adb3a910f7fe..503e1c633581 100644 --- a/startop/iorap/src/com/google/android/startop/iorap/RequestId.java +++ b/startop/iorap/src/com/google/android/startop/iorap/RequestId.java @@ -75,6 +75,11 @@ public class RequestId implements Parcelable { } @Override + public int hashCode() { + return Long.hashCode(requestId); + } + + @Override public boolean equals(Object other) { if (this == other) { return true; |