diff options
| author | 2017-04-13 18:04:31 -0700 | |
|---|---|---|
| committer | 2017-04-17 16:14:58 -0700 | |
| commit | 342e6037109a53830277d8de6ecf0e39578c143c (patch) | |
| tree | 6d8aff8af82a831906df8b295048d747e34e1c8c | |
| parent | d8837b6fca23847fcd4dd295ccbb33ef9d4edcec (diff) | |
Finish impl of job queue: handle URI permissions.
The job queue now handles URI permissions associated with
the Intent of each job. Just (kind-of) like Service!
Also do the second pass of locking in job scheduler, getting
rid of all the async dispatching on a handler, and just executing
calls right in line with simple locking. This probably fixes
a few other race issues, and allows us to make sure that we
always finish a job correctly when dequeuing the last work (we
will always atomically dequeue and finish, so no new work can
slip in between).
And fix a little debug output in IntentFilter.
Test: ran CtsJobSchedulerTestCases, added new test for URI perms.
Change-Id: I52f700ef0cd5be3ff70050f9c0f5fe3e8a5ccac1
6 files changed, 538 insertions, 420 deletions
diff --git a/core/java/android/app/job/JobWorkItem.java b/core/java/android/app/job/JobWorkItem.java index 4bb057e689b2..05687ee9aace 100644 --- a/core/java/android/app/job/JobWorkItem.java +++ b/core/java/android/app/job/JobWorkItem.java @@ -27,6 +27,7 @@ import android.os.Parcelable; final public class JobWorkItem implements Parcelable { final Intent mIntent; int mWorkId; + Object mGrants; /** * Create a new piece of work. @@ -57,6 +58,20 @@ final public class JobWorkItem implements Parcelable { return mWorkId; } + /** + * @hide + */ + public void setGrants(Object grants) { + mGrants = grants; + } + + /** + * @hide + */ + public Object getGrants() { + return mGrants; + } + public String toString() { return "JobWorkItem{id=" + mWorkId + " intent=" + mIntent + "}"; } diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index 23e54baab622..d64f018c3f2e 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -1787,7 +1787,7 @@ public class IntentFilter implements Parcelable { sb.append(", mHasPartialTypes="); sb.append(mHasPartialTypes); du.println(sb.toString()); } - { + if (getAutoVerify()) { sb.setLength(0); sb.append(prefix); sb.append("AutoVerify="); sb.append(getAutoVerify()); du.println(sb.toString()); diff --git a/services/core/java/com/android/server/job/GrantedUriPermissions.java b/services/core/java/com/android/server/job/GrantedUriPermissions.java new file mode 100644 index 000000000000..e413d8d1d8c0 --- /dev/null +++ b/services/core/java/com/android/server/job/GrantedUriPermissions.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2017 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.android.server.job; + +import android.app.IActivityManager; +import android.content.ClipData; +import android.content.ContentProvider; +import android.content.Intent; +import android.net.Uri; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Slog; + +import java.io.PrintWriter; +import java.util.ArrayList; + +public class GrantedUriPermissions { + private final int mGrantFlags; + private final int mSourceUserId; + private final String mTag; + private final IBinder mPermissionOwner; + private final ArrayList<Uri> mUris = new ArrayList<>(); + + private GrantedUriPermissions(IActivityManager am, int grantFlags, int uid, String tag) + throws RemoteException { + mGrantFlags = grantFlags; + mSourceUserId = UserHandle.getUserId(uid); + mTag = tag; + mPermissionOwner = am.newUriPermissionOwner("job: " + tag); + } + + public void revoke(IActivityManager am) { + for (int i = mUris.size()-1; i >= 0; i--) { + try { + am.revokeUriPermissionFromOwner(mPermissionOwner, mUris.get(i), + mGrantFlags, mSourceUserId); + } catch (RemoteException e) { + } + } + mUris.clear(); + } + + public static boolean checkGrantFlags(int grantFlags) { + return (grantFlags & (Intent.FLAG_GRANT_WRITE_URI_PERMISSION + |Intent.FLAG_GRANT_READ_URI_PERMISSION)) != 0; + } + + public static GrantedUriPermissions createFromIntent(IActivityManager am, Intent intent, + int sourceUid, String targetPackage, int targetUserId, String tag) { + int grantFlags = intent.getFlags(); + if (!checkGrantFlags(grantFlags)) { + return null; + } + + GrantedUriPermissions perms = null; + + Uri data = intent.getData(); + if (data != null) { + perms = grantUri(am, data, sourceUid, targetPackage, targetUserId, grantFlags, tag, + perms); + } + + ClipData clip = intent.getClipData(); + if (clip != null) { + perms = grantClip(am, clip, sourceUid, targetPackage, targetUserId, grantFlags, tag, + perms); + } + + return perms; + } + + public static GrantedUriPermissions createFromClip(IActivityManager am, ClipData clip, + int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag) { + if (!checkGrantFlags(grantFlags)) { + return null; + } + GrantedUriPermissions perms = null; + if (clip != null) { + perms = grantClip(am, clip, sourceUid, targetPackage, targetUserId, grantFlags, + tag, perms); + } + return perms; + } + + private static GrantedUriPermissions grantClip(IActivityManager am, ClipData clip, + int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag, + GrantedUriPermissions curPerms) { + final int N = clip.getItemCount(); + for (int i = 0; i < N; i++) { + curPerms = grantItem(am, clip.getItemAt(i), sourceUid, targetPackage, targetUserId, + grantFlags, tag, curPerms); + } + return curPerms; + } + + private static GrantedUriPermissions grantUri(IActivityManager am, Uri uri, + int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag, + GrantedUriPermissions curPerms) { + try { + int sourceUserId = ContentProvider.getUserIdFromUri(uri, + UserHandle.getUserId(sourceUid)); + uri = ContentProvider.getUriWithoutUserId(uri); + if (curPerms == null) { + curPerms = new GrantedUriPermissions(am, grantFlags, sourceUid, tag); + } + am.grantUriPermissionFromOwner(curPerms.mPermissionOwner, sourceUid, targetPackage, + uri, grantFlags, sourceUserId, targetUserId); + curPerms.mUris.add(uri); + } catch (RemoteException e) { + Slog.e("JobScheduler", "AM dead"); + } + return curPerms; + } + + private static GrantedUriPermissions grantItem(IActivityManager am, ClipData.Item item, + int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag, + GrantedUriPermissions curPerms) { + if (item.getUri() != null) { + curPerms = grantUri(am, item.getUri(), sourceUid, targetPackage, targetUserId, + grantFlags, tag, curPerms); + } + Intent intent = item.getIntent(); + if (intent != null && intent.getData() != null) { + curPerms = grantUri(am, intent.getData(), sourceUid, targetPackage, targetUserId, + grantFlags, tag, curPerms); + } + return curPerms; + } + + // Dumpsys infrastructure + public void dump(PrintWriter pw, String prefix) { + pw.print(prefix); pw.print("mGrantFlags=0x"); pw.print(Integer.toHexString(mGrantFlags)); + pw.print(" mSourceUserId="); pw.println(mSourceUserId); + pw.print(prefix); pw.print("mTag="); pw.println(mTag); + pw.print(prefix); pw.print("mPermissionOwner="); pw.println(mPermissionOwner); + for (int i = 0; i < mUris.size(); i++) { + pw.print(prefix); pw.print("#"); pw.print(i); pw.print(": "); + pw.println(mUris.get(i)); + } + } +} diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java index d01de3c9157d..98b36a08380e 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -601,7 +601,7 @@ public final class JobSchedulerService extends com.android.server.SystemService // Fast path: we are adding work to an existing job, and the JobInfo is not // changing. We can just directly enqueue this work in to the job. if (toCancel.getJob().equals(job)) { - toCancel.enqueueWorkLocked(work); + toCancel.enqueueWorkLocked(ActivityManager.getService(), work); return JobScheduler.RESULT_SUCCESS; } } @@ -625,7 +625,7 @@ public final class JobSchedulerService extends com.android.server.SystemService } if (work != null) { // If work has been supplied, enqueue it into the new job. - jobStatus.enqueueWorkLocked(work); + jobStatus.enqueueWorkLocked(ActivityManager.getService(), work); } startTrackingJobLocked(jobStatus, toCancel); mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); @@ -758,7 +758,7 @@ public final class JobSchedulerService extends com.android.server.SystemService final JobStatus executing = jsc.getRunningJob(); if (executing != null && (executing.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) == 0) { - jsc.cancelExecutingJob(JobParameters.REASON_DEVICE_IDLE); + jsc.cancelExecutingJobLocked(JobParameters.REASON_DEVICE_IDLE); } } } else { @@ -921,7 +921,7 @@ public final class JobSchedulerService extends com.android.server.SystemService private boolean stopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, boolean writeBack) { // Deal with any remaining work items in the old job. - jobStatus.stopTrackingJobLocked(incomingJob); + jobStatus.stopTrackingJobLocked(ActivityManager.getService(), incomingJob); // Remove from store as well as controllers. final boolean removed = mJobs.remove(jobStatus, writeBack); @@ -939,7 +939,7 @@ public final class JobSchedulerService extends com.android.server.SystemService JobServiceContext jsc = mActiveServices.get(i); final JobStatus executing = jsc.getRunningJob(); if (executing != null && executing.matches(job.getUid(), job.getJobId())) { - jsc.cancelExecutingJob(reason); + jsc.cancelExecutingJobLocked(reason); return true; } } @@ -1561,7 +1561,7 @@ public final class JobSchedulerService extends com.android.server.SystemService Slog.d(TAG, "preempting job: " + mActiveServices.get(i).getRunningJob()); } // preferredUid will be set to uid of currently running job. - mActiveServices.get(i).preemptExecutingJob(); + mActiveServices.get(i).preemptExecutingJobLocked(); preservePreferredUid = true; } else { final JobStatus pendingJob = contextIdToJobMap[i]; diff --git a/services/core/java/com/android/server/job/JobServiceContext.java b/services/core/java/com/android/server/job/JobServiceContext.java index c7ef0e26971f..9144966da409 100644 --- a/services/core/java/com/android/server/job/JobServiceContext.java +++ b/services/core/java/com/android/server/job/JobServiceContext.java @@ -44,8 +44,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IBatteryStats; import com.android.server.job.controllers.JobStatus; -import java.util.concurrent.atomic.AtomicBoolean; - /** * Handles client binding and lifecycle of a job. Jobs execute one at a time on an instance of this * class. @@ -56,19 +54,15 @@ import java.util.concurrent.atomic.AtomicBoolean; * job lands, and again when it is complete. * - Cancelling is trickier, because there are also interactions from the client. It's possible * the {@link com.android.server.job.JobServiceContext.JobServiceHandler} tries to process a - * {@link #MSG_CANCEL} after the client has already finished. This is handled by having - * {@link com.android.server.job.JobServiceContext.JobServiceHandler#handleCancelH} check whether + * {@link #doCancelLocked(int)} after the client has already finished. This is handled by having + * {@link com.android.server.job.JobServiceContext.JobServiceHandler#handleCancelLocked} check whether * the context is still valid. - * To mitigate this, tearing down the context removes all messages from the handler, including any - * tardy {@link #MSG_CANCEL}s. Additionally, we avoid sending duplicate onStopJob() + * To mitigate this, we avoid sending duplicate onStopJob() * calls to the client after they've specified jobFinished(). */ public class JobServiceContext extends IJobCallback.Stub implements ServiceConnection { private static final boolean DEBUG = JobSchedulerService.DEBUG; private static final String TAG = "JobServiceContext"; - /** Define the maximum # of jobs allowed to run on a service at once. */ - private static final int defaultMaxActiveJobsPerService = - ActivityManager.isLowRamDeviceStatic() ? 1 : 3; /** Amount of time a job is allowed to execute for before being considered timed-out. */ private static final long EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000; // 10mins. /** Amount of time the JobScheduler waits for the initial service launch+bind. */ @@ -90,14 +84,6 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne // Messages that result from interactions with the client service. /** System timed out waiting for a response. */ private static final int MSG_TIMEOUT = 0; - /** Received a callback from client. */ - private static final int MSG_CALLBACK = 1; - /** Run through list and start any ready jobs.*/ - private static final int MSG_SERVICE_BOUND = 2; - /** Cancel a job. */ - private static final int MSG_CANCEL = 3; - /** Shutdown the job. Used when the client crashes and we can't die gracefully.*/ - private static final int MSG_SHUTDOWN_EXECUTION = 4; public static final int NO_PREFERRED_UID = -1; @@ -115,7 +101,7 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne private JobParameters mParams; @VisibleForTesting int mVerb; - private AtomicBoolean mCancelled = new AtomicBoolean(); + private boolean mCancelled; /** * All the information maintained about the job currently being executed. @@ -245,14 +231,12 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne } /** Called externally when a job that was scheduled for execution should be cancelled. */ - void cancelExecutingJob(int reason) { - mCallbackHandler.obtainMessage(MSG_CANCEL, reason, 0 /* unused */).sendToTarget(); + void cancelExecutingJobLocked(int reason) { + doCancelLocked(reason); } - void preemptExecutingJob() { - Message m = mCallbackHandler.obtainMessage(MSG_CANCEL); - m.arg1 = JobParameters.REASON_PREEMPT; - m.sendToTarget(); + void preemptExecutingJobLocked() { + doCancelLocked(JobParameters.REASON_PREEMPT); } int getPreferredUid() { @@ -273,59 +257,54 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne @Override public void jobFinished(int jobId, boolean reschedule) { - if (!verifyCallingUid()) { - return; - } - mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0) - .sendToTarget(); + doCallback(reschedule); } @Override public void acknowledgeStopMessage(int jobId, boolean reschedule) { - if (!verifyCallingUid()) { - return; - } - mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0) - .sendToTarget(); + doCallback(reschedule); } @Override public void acknowledgeStartMessage(int jobId, boolean ongoing) { - if (!verifyCallingUid()) { - return; - } - mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, ongoing ? 1 : 0).sendToTarget(); + doCallback(ongoing); } @Override public JobWorkItem dequeueWork(int jobId) { - if (!verifyCallingUid()) { - throw new SecurityException("Bad calling uid: " + Binder.getCallingUid()); - } - JobWorkItem work = null; - boolean stillWorking = false; - synchronized (mLock) { - if (mRunningJob != null) { - work = mRunningJob.dequeueWorkLocked(); - stillWorking = mRunningJob.hasExecutingWorkLocked(); + final int callingUid = Binder.getCallingUid(); + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + if (!verifyCallingUidLocked(callingUid)) { + throw new SecurityException("Bad calling uid: " + callingUid); + } + + final JobWorkItem work = mRunningJob.dequeueWorkLocked(); + if (work == null && !mRunningJob.hasExecutingWorkLocked()) { + // This will finish the job. + doCallbackLocked(false); + } + return work; } + } finally { + Binder.restoreCallingIdentity(ident); } - if (work == null && !stillWorking) { - jobFinished(jobId, false); - } - return work; } @Override public boolean completeWork(int jobId, int workId) { - if (!verifyCallingUid()) { - throw new SecurityException("Bad calling uid: " + Binder.getCallingUid()); - } - synchronized (mLock) { - if (mRunningJob != null) { - return mRunningJob.completeWorkLocked(workId); + final int callingUid = Binder.getCallingUid(); + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + if (!verifyCallingUidLocked(callingUid)) { + throw new SecurityException("Bad calling uid: " + callingUid); + } + return mRunningJob.completeWorkLocked(ActivityManager.getService(), workId); } - return false; + } finally { + Binder.restoreCallingIdentity(ident); } } @@ -344,20 +323,20 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne // looper and at this point we can't get any binder callbacks from the client. Better // safe than sorry. runningJob = mRunningJob; - } - if (runningJob == null || !name.equals(runningJob.getServiceComponent())) { - mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget(); - return; - } - this.service = IJobService.Stub.asInterface(service); - final PowerManager pm = - (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); - PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, - runningJob.getTag()); - wl.setWorkSource(new WorkSource(runningJob.getSourceUid())); - wl.setReferenceCounted(false); - wl.acquire(); - synchronized (mLock) { + + if (runningJob == null || !name.equals(runningJob.getServiceComponent())) { + closeAndCleanupJobLocked(true /* needsReschedule */); + return; + } + this.service = IJobService.Stub.asInterface(service); + final PowerManager pm = + (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + runningJob.getTag()); + wl.setWorkSource(new WorkSource(runningJob.getSourceUid())); + wl.setReferenceCounted(false); + wl.acquire(); + // We use a new wakelock instance per job. In rare cases there is a race between // teardown following job completion/cancellation and new job service spin-up // such that if we simply assign mWakeLock to be the new instance, we orphan @@ -369,14 +348,16 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne mWakeLock.release(); } mWakeLock = wl; + doServiceBoundLocked(); } - mCallbackHandler.obtainMessage(MSG_SERVICE_BOUND).sendToTarget(); } /** If the client service crashes we reschedule this job and clean up. */ @Override public void onServiceDisconnected(ComponentName name) { - mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget(); + synchronized (mLock) { + closeAndCleanupJobLocked(true /* needsReschedule */); + } } /** @@ -384,22 +365,18 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne * whether the client exercising the callback is the client we expect. * @return True if the binder calling is coming from the client we expect. */ - private boolean verifyCallingUid() { - synchronized (mLock) { - if (mRunningJob == null || Binder.getCallingUid() != mRunningJob.getUid()) { - if (DEBUG) { - Slog.d(TAG, "Stale callback received, ignoring."); - } - return false; + private boolean verifyCallingUidLocked(final int callingUid) { + if (mRunningJob == null || callingUid != mRunningJob.getUid()) { + if (DEBUG) { + Slog.d(TAG, "Stale callback received, ignoring."); } - return true; + return false; } + return true; } /** - * Handles the lifecycle of the JobService binding/callbacks, etc. The convention within this - * class is to append 'H' to each function name that can only be called on this handler. This - * isn't strictly necessary because all of these functions are private, but helps clarity. + * Scheduling of async messages (basically timeouts at this point). */ private class JobServiceHandler extends Handler { JobServiceHandler(Looper looper) { @@ -409,293 +386,277 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne @Override public void handleMessage(Message message) { switch (message.what) { - case MSG_SERVICE_BOUND: - doServiceBound(); - break; - case MSG_CALLBACK: - doCallback(message.arg2); - break; - case MSG_CANCEL: - doCancel(message.arg1); - break; case MSG_TIMEOUT: synchronized (mLock) { - handleOpTimeoutH(); + handleOpTimeoutLocked(); } break; - case MSG_SHUTDOWN_EXECUTION: - closeAndCleanupJobH(true /* needsReschedule */); - break; default: Slog.e(TAG, "Unrecognised message: " + message); } } + } - void doServiceBound() { + void doServiceBoundLocked() { + removeOpTimeOutLocked(); + handleServiceBoundLocked(); + } + + void doCallback(boolean reschedule) { + final int callingUid = Binder.getCallingUid(); + final long ident = Binder.clearCallingIdentity(); + try { synchronized (mLock) { - removeOpTimeOutLocked(); - handleServiceBoundH(); + if (!verifyCallingUidLocked(callingUid)) { + return; + } + doCallbackLocked(reschedule); } + } finally { + Binder.restoreCallingIdentity(ident); } + } - void doCallback(int arg2) { - synchronized (mLock) { - if (DEBUG) { - Slog.d(TAG, "MSG_CALLBACK of : " + mRunningJob - + " v:" + VERB_STRINGS[mVerb]); - } - removeOpTimeOutLocked(); + void doCallbackLocked(boolean reschedule) { + if (DEBUG) { + Slog.d(TAG, "doCallback of : " + mRunningJob + + " v:" + VERB_STRINGS[mVerb]); + } + removeOpTimeOutLocked(); - if (mVerb == VERB_STARTING) { - final boolean workOngoing = arg2 == 1; - handleStartedH(workOngoing); - } else if (mVerb == VERB_EXECUTING || - mVerb == VERB_STOPPING) { - final boolean reschedule = arg2 == 1; - handleFinishedH(reschedule); - } else { - if (DEBUG) { - Slog.d(TAG, "Unrecognised callback: " + mRunningJob); - } - } + if (mVerb == VERB_STARTING) { + handleStartedLocked(reschedule); + } else if (mVerb == VERB_EXECUTING || + mVerb == VERB_STOPPING) { + handleFinishedLocked(reschedule); + } else { + if (DEBUG) { + Slog.d(TAG, "Unrecognised callback: " + mRunningJob); } } + } - void doCancel(int arg1) { - synchronized (mLock) { - if (mVerb == VERB_FINISHED) { - if (DEBUG) { - Slog.d(TAG, - "Trying to process cancel for torn-down context, ignoring."); - } - return; - } - mParams.setStopReason(arg1); - if (arg1 == JobParameters.REASON_PREEMPT) { - mPreferredUid = mRunningJob != null ? mRunningJob.getUid() : - NO_PREFERRED_UID; - } - handleCancelH(); + void doCancelLocked(int arg1) { + if (mVerb == VERB_FINISHED) { + if (DEBUG) { + Slog.d(TAG, + "Trying to process cancel for torn-down context, ignoring."); } - + return; + } + mParams.setStopReason(arg1); + if (arg1 == JobParameters.REASON_PREEMPT) { + mPreferredUid = mRunningJob != null ? mRunningJob.getUid() : + NO_PREFERRED_UID; } + handleCancelLocked(); + } - /** Start the job on the service. */ - private void handleServiceBoundH() { + /** Start the job on the service. */ + private void handleServiceBoundLocked() { + if (DEBUG) { + Slog.d(TAG, "handleServiceBound for " + mRunningJob.toShortString()); + } + if (mVerb != VERB_BINDING) { + Slog.e(TAG, "Sending onStartJob for a job that isn't pending. " + + VERB_STRINGS[mVerb]); + closeAndCleanupJobLocked(false /* reschedule */); + return; + } + if (mCancelled) { if (DEBUG) { - Slog.d(TAG, "MSG_SERVICE_BOUND for " + mRunningJob.toShortString()); - } - if (mVerb != VERB_BINDING) { - Slog.e(TAG, "Sending onStartJob for a job that isn't pending. " - + VERB_STRINGS[mVerb]); - closeAndCleanupJobH(false /* reschedule */); - return; - } - if (mCancelled.get()) { - if (DEBUG) { - Slog.d(TAG, "Job cancelled while waiting for bind to complete. " - + mRunningJob); - } - closeAndCleanupJobH(true /* reschedule */); - return; - } - try { - mVerb = VERB_STARTING; - scheduleOpTimeOutLocked(); - service.startJob(mParams); - } catch (Exception e) { - // We catch 'Exception' because client-app malice or bugs might induce a wide - // range of possible exception-throw outcomes from startJob() and its handling - // of the client's ParcelableBundle extras. - Slog.e(TAG, "Error sending onStart message to '" + - mRunningJob.getServiceComponent().getShortClassName() + "' ", e); + Slog.d(TAG, "Job cancelled while waiting for bind to complete. " + + mRunningJob); } + closeAndCleanupJobLocked(true /* reschedule */); + return; } + try { + mVerb = VERB_STARTING; + scheduleOpTimeOutLocked(); + service.startJob(mParams); + } catch (Exception e) { + // We catch 'Exception' because client-app malice or bugs might induce a wide + // range of possible exception-throw outcomes from startJob() and its handling + // of the client's ParcelableBundle extras. + Slog.e(TAG, "Error sending onStart message to '" + + mRunningJob.getServiceComponent().getShortClassName() + "' ", e); + } + } - /** - * State behaviours. - * VERB_STARTING -> Successful start, change job to VERB_EXECUTING and post timeout. - * _PENDING -> Error - * _EXECUTING -> Error - * _STOPPING -> Error - */ - private void handleStartedH(boolean workOngoing) { - switch (mVerb) { - case VERB_STARTING: - mVerb = VERB_EXECUTING; - if (!workOngoing) { - // Job is finished already so fast-forward to handleFinished. - handleFinishedH(false); - return; - } - if (mCancelled.get()) { - if (DEBUG) { - Slog.d(TAG, "Job cancelled while waiting for onStartJob to complete."); - } - // Cancelled *while* waiting for acknowledgeStartMessage from client. - handleCancelH(); - return; + /** + * State behaviours. + * VERB_STARTING -> Successful start, change job to VERB_EXECUTING and post timeout. + * _PENDING -> Error + * _EXECUTING -> Error + * _STOPPING -> Error + */ + private void handleStartedLocked(boolean workOngoing) { + switch (mVerb) { + case VERB_STARTING: + mVerb = VERB_EXECUTING; + if (!workOngoing) { + // Job is finished already so fast-forward to handleFinished. + handleFinishedLocked(false); + return; + } + if (mCancelled) { + if (DEBUG) { + Slog.d(TAG, "Job cancelled while waiting for onStartJob to complete."); } - scheduleOpTimeOutLocked(); - break; - default: - Slog.e(TAG, "Handling started job but job wasn't starting! Was " - + VERB_STRINGS[mVerb] + "."); + // Cancelled *while* waiting for acknowledgeStartMessage from client. + handleCancelLocked(); return; - } + } + scheduleOpTimeOutLocked(); + break; + default: + Slog.e(TAG, "Handling started job but job wasn't starting! Was " + + VERB_STRINGS[mVerb] + "."); + return; } + } - /** - * VERB_EXECUTING -> Client called jobFinished(), clean up and notify done. - * _STOPPING -> Successful finish, clean up and notify done. - * _STARTING -> Error - * _PENDING -> Error - */ - private void handleFinishedH(boolean reschedule) { - switch (mVerb) { - case VERB_EXECUTING: - case VERB_STOPPING: - closeAndCleanupJobH(reschedule); - break; - default: - Slog.e(TAG, "Got an execution complete message for a job that wasn't being" + - "executed. Was " + VERB_STRINGS[mVerb] + "."); - } + /** + * VERB_EXECUTING -> Client called jobFinished(), clean up and notify done. + * _STOPPING -> Successful finish, clean up and notify done. + * _STARTING -> Error + * _PENDING -> Error + */ + private void handleFinishedLocked(boolean reschedule) { + switch (mVerb) { + case VERB_EXECUTING: + case VERB_STOPPING: + closeAndCleanupJobLocked(reschedule); + break; + default: + Slog.e(TAG, "Got an execution complete message for a job that wasn't being" + + "executed. Was " + VERB_STRINGS[mVerb] + "."); } + } - /** - * A job can be in various states when a cancel request comes in: - * VERB_BINDING -> Cancelled before bind completed. Mark as cancelled and wait for - * {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)} - * _STARTING -> Mark as cancelled and wait for - * {@link JobServiceContext#acknowledgeStartMessage(int, boolean)} - * _EXECUTING -> call {@link #sendStopMessageH}}, but only if there are no callbacks - * in the message queue. - * _ENDING -> No point in doing anything here, so we ignore. - */ - private void handleCancelH() { - if (JobSchedulerService.DEBUG) { - Slog.d(TAG, "Handling cancel for: " + mRunningJob.getJobId() + " " - + VERB_STRINGS[mVerb]); - } - switch (mVerb) { - case VERB_BINDING: - case VERB_STARTING: - mCancelled.set(true); - break; - case VERB_EXECUTING: - if (hasMessages(MSG_CALLBACK)) { - // If the client has called jobFinished, ignore this cancel. - return; - } - sendStopMessageH(); - break; - case VERB_STOPPING: - // Nada. - break; - default: - Slog.e(TAG, "Cancelling a job without a valid verb: " + mVerb); - break; - } + /** + * A job can be in various states when a cancel request comes in: + * VERB_BINDING -> Cancelled before bind completed. Mark as cancelled and wait for + * {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)} + * _STARTING -> Mark as cancelled and wait for + * {@link JobServiceContext#acknowledgeStartMessage(int, boolean)} + * _EXECUTING -> call {@link #sendStopMessageLocked}}, but only if there are no callbacks + * in the message queue. + * _ENDING -> No point in doing anything here, so we ignore. + */ + private void handleCancelLocked() { + if (JobSchedulerService.DEBUG) { + Slog.d(TAG, "Handling cancel for: " + mRunningJob.getJobId() + " " + + VERB_STRINGS[mVerb]); } + switch (mVerb) { + case VERB_BINDING: + case VERB_STARTING: + mCancelled = true; + break; + case VERB_EXECUTING: + sendStopMessageLocked(); + break; + case VERB_STOPPING: + // Nada. + break; + default: + Slog.e(TAG, "Cancelling a job without a valid verb: " + mVerb); + break; + } + } - /** Process MSG_TIMEOUT here. */ - private void handleOpTimeoutH() { - switch (mVerb) { - case VERB_BINDING: - Slog.e(TAG, "Time-out while trying to bind " + mRunningJob.toShortString() + - ", dropping."); - closeAndCleanupJobH(false /* needsReschedule */); - break; - case VERB_STARTING: - // Client unresponsive - wedged or failed to respond in time. We don't really - // know what happened so let's log it and notify the JobScheduler - // FINISHED/NO-RETRY. - Slog.e(TAG, "No response from client for onStartJob '" + - mRunningJob.toShortString()); - closeAndCleanupJobH(false /* needsReschedule */); - break; - case VERB_STOPPING: - // At least we got somewhere, so fail but ask the JobScheduler to reschedule. - Slog.e(TAG, "No response from client for onStopJob, '" + - mRunningJob.toShortString()); - closeAndCleanupJobH(true /* needsReschedule */); - break; - case VERB_EXECUTING: - // Not an error - client ran out of time. - Slog.i(TAG, "Client timed out while executing (no jobFinished received)." + - " sending onStop. " + mRunningJob.toShortString()); - mParams.setStopReason(JobParameters.REASON_TIMEOUT); - sendStopMessageH(); - break; - default: - Slog.e(TAG, "Handling timeout for an invalid job state: " + - mRunningJob.toShortString() + ", dropping."); - closeAndCleanupJobH(false /* needsReschedule */); - } + /** Process MSG_TIMEOUT here. */ + private void handleOpTimeoutLocked() { + switch (mVerb) { + case VERB_BINDING: + Slog.e(TAG, "Time-out while trying to bind " + mRunningJob.toShortString() + + ", dropping."); + closeAndCleanupJobLocked(false /* needsReschedule */); + break; + case VERB_STARTING: + // Client unresponsive - wedged or failed to respond in time. We don't really + // know what happened so let's log it and notify the JobScheduler + // FINISHED/NO-RETRY. + Slog.e(TAG, "No response from client for onStartJob '" + + mRunningJob.toShortString()); + closeAndCleanupJobLocked(false /* needsReschedule */); + break; + case VERB_STOPPING: + // At least we got somewhere, so fail but ask the JobScheduler to reschedule. + Slog.e(TAG, "No response from client for onStopJob, '" + + mRunningJob.toShortString()); + closeAndCleanupJobLocked(true /* needsReschedule */); + break; + case VERB_EXECUTING: + // Not an error - client ran out of time. + Slog.i(TAG, "Client timed out while executing (no jobFinished received)." + + " sending onStop. " + mRunningJob.toShortString()); + mParams.setStopReason(JobParameters.REASON_TIMEOUT); + sendStopMessageLocked(); + break; + default: + Slog.e(TAG, "Handling timeout for an invalid job state: " + + mRunningJob.toShortString() + ", dropping."); + closeAndCleanupJobLocked(false /* needsReschedule */); } + } - /** - * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING -> - * VERB_STOPPING. - */ - private void sendStopMessageH() { - removeOpTimeOutLocked(); - if (mVerb != VERB_EXECUTING) { - Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob); - closeAndCleanupJobH(false /* reschedule */); - return; - } - try { - mVerb = VERB_STOPPING; - scheduleOpTimeOutLocked(); - service.stopJob(mParams); - } catch (RemoteException e) { - Slog.e(TAG, "Error sending onStopJob to client.", e); - // The job's host app apparently crashed during the job, so we should reschedule. - closeAndCleanupJobH(true /* reschedule */); - } + /** + * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING -> + * VERB_STOPPING. + */ + private void sendStopMessageLocked() { + removeOpTimeOutLocked(); + if (mVerb != VERB_EXECUTING) { + Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob); + closeAndCleanupJobLocked(false /* reschedule */); + return; + } + try { + mVerb = VERB_STOPPING; + scheduleOpTimeOutLocked(); + service.stopJob(mParams); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending onStopJob to client.", e); + // The job's host app apparently crashed during the job, so we should reschedule. + closeAndCleanupJobLocked(true /* reschedule */); } + } - /** - * The provided job has finished, either by calling - * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)} - * or from acknowledging the stop message we sent. Either way, we're done tracking it and - * we want to clean up internally. - */ - private void closeAndCleanupJobH(boolean reschedule) { - final JobStatus completedJob; - synchronized (mLock) { - if (mVerb == VERB_FINISHED) { - return; - } - completedJob = mRunningJob; - mJobPackageTracker.noteInactive(completedJob); - try { - mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(), - mRunningJob.getSourceUid()); - } catch (RemoteException e) { - // Whatever. - } - if (mWakeLock != null) { - mWakeLock.release(); - } - mContext.unbindService(JobServiceContext.this); - mWakeLock = null; - mRunningJob = null; - mParams = null; - mVerb = VERB_FINISHED; - mCancelled.set(false); - service = null; - mAvailable = true; - removeOpTimeOutLocked(); - removeMessages(MSG_CALLBACK); - removeMessages(MSG_SERVICE_BOUND); - removeMessages(MSG_CANCEL); - removeMessages(MSG_SHUTDOWN_EXECUTION); - mCompletedListener.onJobCompletedLocked(completedJob, reschedule); - } + /** + * The provided job has finished, either by calling + * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)} + * or from acknowledging the stop message we sent. Either way, we're done tracking it and + * we want to clean up internally. + */ + private void closeAndCleanupJobLocked(boolean reschedule) { + final JobStatus completedJob; + if (mVerb == VERB_FINISHED) { + return; } + completedJob = mRunningJob; + mJobPackageTracker.noteInactive(completedJob); + try { + mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(), + mRunningJob.getSourceUid()); + } catch (RemoteException e) { + // Whatever. + } + if (mWakeLock != null) { + mWakeLock.release(); + } + mContext.unbindService(JobServiceContext.this); + mWakeLock = null; + mRunningJob = null; + mParams = null; + mVerb = VERB_FINISHED; + mCancelled = false; + service = null; + mAvailable = true; + removeOpTimeOutLocked(); + mCompletedListener.onJobCompletedLocked(completedJob, reschedule); } /** diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java index e8cc078b7eb1..1ab66b9abb7f 100644 --- a/services/core/java/com/android/server/job/controllers/JobStatus.java +++ b/services/core/java/com/android/server/job/controllers/JobStatus.java @@ -26,7 +26,6 @@ import android.content.ContentProvider; import android.content.Intent; import android.net.Uri; import android.os.Binder; -import android.os.IBinder; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; @@ -35,6 +34,8 @@ import android.util.ArraySet; import android.util.Slog; import android.util.TimeUtils; +import com.android.server.job.GrantedUriPermissions; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; @@ -103,7 +104,7 @@ public final class JobStatus { final String tag; - private IBinder permissionOwner; + private GrantedUriPermissions uriPerms; private boolean prepared; /** @@ -284,12 +285,17 @@ public final class JobStatus { earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis); } - public void enqueueWorkLocked(JobWorkItem work) { + public void enqueueWorkLocked(IActivityManager am, JobWorkItem work) { if (pendingWork == null) { pendingWork = new ArrayList<>(); } work.setWorkId(nextPendingWorkId); nextPendingWorkId++; + if (work.getIntent() != null + && GrantedUriPermissions.checkGrantFlags(work.getIntent().getFlags())) { + work.setGrants(GrantedUriPermissions.createFromIntent(am, work.getIntent(), sourceUid, + sourcePackageName, sourceUserId, toShortString())); + } pendingWork.add(work); } @@ -311,12 +317,20 @@ public final class JobStatus { return executingWork != null && executingWork.size() > 0; } - public boolean completeWorkLocked(int workId) { + private static void ungrantWorkItem(IActivityManager am, JobWorkItem work) { + if (work.getGrants() != null) { + ((GrantedUriPermissions)work.getGrants()).revoke(am); + } + } + + public boolean completeWorkLocked(IActivityManager am, int workId) { if (executingWork != null) { final int N = executingWork.size(); for (int i = 0; i < N; i++) { - if (executingWork.get(i).getWorkId() == workId) { + JobWorkItem work = executingWork.get(i); + if (work.getWorkId() == workId) { executingWork.remove(i); + ungrantWorkItem(am, work); return true; } } @@ -324,15 +338,36 @@ public final class JobStatus { return false; } - public void stopTrackingJobLocked(JobStatus incomingJob) { + private static void ungrantWorkList(IActivityManager am, ArrayList<JobWorkItem> list) { + if (list != null) { + final int N = list.size(); + for (int i = 0; i < N; i++) { + ungrantWorkItem(am, list.get(i)); + } + } + } + + public void stopTrackingJobLocked(IActivityManager am, JobStatus incomingJob) { if (incomingJob != null) { - // We are replacing with a new job -- transfer the work! - incomingJob.pendingWork = pendingWork; + // We are replacing with a new job -- transfer the work! We do any executing + // work first, since that was originally at the front of the pending work. + if (executingWork != null && executingWork.size() > 0) { + incomingJob.pendingWork = executingWork; + } + if (incomingJob.pendingWork == null) { + incomingJob.pendingWork = pendingWork; + } else if (pendingWork != null && pendingWork.size() > 0) { + incomingJob.pendingWork.addAll(pendingWork); + } pendingWork = null; + executingWork = null; incomingJob.nextPendingWorkId = nextPendingWorkId; } else { // We are completely stopping the job... need to clean up work. - // XXX remove perms when that is impl. + ungrantWorkList(am, pendingWork); + pendingWork = null; + ungrantWorkList(am, executingWork); + executingWork = null; } } @@ -344,10 +379,8 @@ public final class JobStatus { prepared = true; final ClipData clip = job.getClipData(); if (clip != null) { - final int N = clip.getItemCount(); - for (int i = 0; i < N; i++) { - grantItemLocked(am, clip.getItemAt(i), sourceUid, sourcePackageName, sourceUserId); - } + uriPerms = GrantedUriPermissions.createFromClip(am, clip, sourceUid, sourcePackageName, + sourceUserId, job.getClipGrantFlags(), toShortString()); } } @@ -357,14 +390,9 @@ public final class JobStatus { return; } prepared = false; - if (permissionOwner != null) { - final ClipData clip = job.getClipData(); - if (clip != null) { - final int N = clip.getItemCount(); - for (int i = 0; i < N; i++) { - revokeItemLocked(am, clip.getItemAt(i)); - } - } + if (uriPerms != null) { + uriPerms.revoke(am); + uriPerms = null; } } @@ -372,57 +400,6 @@ public final class JobStatus { return prepared; } - private final void grantUriLocked(IActivityManager am, Uri uri, int sourceUid, - String targetPackage, int targetUserId) { - try { - int sourceUserId = ContentProvider.getUserIdFromUri(uri, - UserHandle.getUserId(sourceUid)); - uri = ContentProvider.getUriWithoutUserId(uri); - if (permissionOwner == null) { - permissionOwner = am.newUriPermissionOwner("job: " + toShortString()); - } - am.grantUriPermissionFromOwner(permissionOwner, sourceUid, targetPackage, - uri, job.getClipGrantFlags(), sourceUserId, targetUserId); - } catch (RemoteException e) { - Slog.e("JobScheduler", "AM dead"); - } - } - - private final void grantItemLocked(IActivityManager am, ClipData.Item item, int sourceUid, - String targetPackage, int targetUserId) { - if (item.getUri() != null) { - grantUriLocked(am, item.getUri(), sourceUid, targetPackage, targetUserId); - } - Intent intent = item.getIntent(); - if (intent != null && intent.getData() != null) { - grantUriLocked(am, intent.getData(), sourceUid, targetPackage, targetUserId); - } - } - - private final void revokeUriLocked(IActivityManager am, Uri uri) { - int userId = ContentProvider.getUserIdFromUri(uri, - UserHandle.getUserId(Binder.getCallingUid())); - long ident = Binder.clearCallingIdentity(); - try { - uri = ContentProvider.getUriWithoutUserId(uri); - am.revokeUriPermissionFromOwner(permissionOwner, uri, - job.getClipGrantFlags(), userId); - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(ident); - } - } - - private final void revokeItemLocked(IActivityManager am, ClipData.Item item) { - if (item.getUri() != null) { - revokeUriLocked(am, item.getUri()); - } - Intent intent = item.getIntent(); - if (intent != null && intent.getData() != null) { - revokeUriLocked(am, intent.getData()); - } - } - public JobInfo getJob() { return job; } @@ -833,6 +810,15 @@ public final class JobStatus { } } + private void dumpJobWorkItem(PrintWriter pw, String prefix, JobWorkItem work, int index) { + pw.print(prefix); pw.print(" #"); pw.print(index); pw.print(": #"); + pw.print(work.getWorkId()); pw.print(" "); pw.println(work.getIntent()); + if (work.getGrants() != null) { + pw.print(prefix); pw.println(" URI grants:"); + ((GrantedUriPermissions)work.getGrants()).dump(pw, prefix + " "); + } + } + // Dumpsys infrastructure public void dump(PrintWriter pw, String prefix, boolean full) { pw.print(prefix); UserHandle.formatUid(pw, callingUid); @@ -898,6 +884,10 @@ public final class JobStatus { job.getClipData().toShortString(b); pw.println(b); } + if (uriPerms != null) { + pw.print(prefix); pw.println(" Granted URI permissions:"); + uriPerms.dump(pw, prefix + " "); + } if (job.getNetworkType() != JobInfo.NETWORK_TYPE_NONE) { pw.print(prefix); pw.print(" Network type: "); pw.println(job.getNetworkType()); } @@ -950,17 +940,13 @@ public final class JobStatus { if (pendingWork != null && pendingWork.size() > 0) { pw.print(prefix); pw.println("Pending work:"); for (int i = 0; i < pendingWork.size(); i++) { - JobWorkItem work = pendingWork.get(i); - pw.print(prefix); pw.print(" #"); pw.print(i); pw.print(": #"); - pw.print(work.getWorkId()); pw.print(" "); pw.println(work.getIntent()); + dumpJobWorkItem(pw, prefix, pendingWork.get(i), i); } } if (executingWork != null && executingWork.size() > 0) { pw.print(prefix); pw.println("Executing work:"); for (int i = 0; i < executingWork.size(); i++) { - JobWorkItem work = executingWork.get(i); - pw.print(prefix); pw.print(" #"); pw.print(i); pw.print(": #"); - pw.print(work.getWorkId()); pw.print(" "); pw.println(work.getIntent()); + dumpJobWorkItem(pw, prefix, executingWork.get(i), i); } } pw.print(prefix); pw.print("Earliest run time: "); |