Merge "Finish impl of job queue: handle URI permissions." into oc-dev

am: 6e8f116647

Change-Id: I9d053f2e96712646fe2935ddc2c3729690a06be1
This commit is contained in:
Dianne Hackborn
2017-04-18 01:19:50 +00:00
committed by android-build-merger
6 changed files with 532 additions and 423 deletions

View File

@@ -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 + "}";
}

View File

@@ -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());

View File

@@ -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));
}
}
}

View File

@@ -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;
}
}
@@ -1564,7 +1564,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];

View File

@@ -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,295 +386,279 @@ 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();
}
}
void doCallback(int arg2) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "MSG_CALLBACK 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);
}
}
}
}
void doCancel(int arg1) {
synchronized (mLock) {
if (mVerb == VERB_FINISHED) {
if (DEBUG) {
Slog.d(TAG,
"Trying to process cancel for torn-down context, ignoring.");
}
if (!verifyCallingUidLocked(callingUid)) {
return;
}
mParams.setStopReason(arg1);
if (arg1 == JobParameters.REASON_PREEMPT) {
mPreferredUid = mRunningJob != null ? mRunningJob.getUid() :
NO_PREFERRED_UID;
}
handleCancelH();
doCallbackLocked(reschedule);
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}
/** Start the job on the service. */
private void handleServiceBoundH() {
void doCallbackLocked(boolean reschedule) {
if (DEBUG) {
Slog.d(TAG, "doCallback of : " + mRunningJob
+ " v:" + VERB_STRINGS[mVerb]);
}
removeOpTimeOutLocked();
if (mVerb == VERB_STARTING) {
handleStartedLocked(reschedule);
} else if (mVerb == VERB_EXECUTING ||
mVerb == VERB_STOPPING) {
handleFinishedLocked(reschedule);
} else {
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, "Unrecognised callback: " + mRunningJob);
}
}
}
/**
* 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;
}
scheduleOpTimeOutLocked();
break;
default:
Slog.e(TAG, "Handling started job but job wasn't starting! Was "
+ VERB_STRINGS[mVerb] + ".");
return;
void doCancelLocked(int arg1) {
if (mVerb == VERB_FINISHED) {
if (DEBUG) {
Slog.d(TAG,
"Trying to process cancel for torn-down context, ignoring.");
}
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] + ".");
}
mParams.setStopReason(arg1);
if (arg1 == JobParameters.REASON_PREEMPT) {
mPreferredUid = mRunningJob != null ? mRunningJob.getUid() :
NO_PREFERRED_UID;
}
handleCancelLocked();
}
/**
* 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;
}
/** Start the job on the service. */
private void handleServiceBoundLocked() {
if (DEBUG) {
Slog.d(TAG, "handleServiceBound for " + mRunningJob.toShortString());
}
/** 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 */);
}
if (mVerb != VERB_BINDING) {
Slog.e(TAG, "Sending onStartJob for a job that isn't pending. "
+ VERB_STRINGS[mVerb]);
closeAndCleanupJobLocked(false /* reschedule */);
return;
}
/**
* 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 */);
if (mCancelled) {
if (DEBUG) {
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);
}
}
/**
* 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) {
/**
* 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;
}
completedJob = mRunningJob;
mJobPackageTracker.noteInactive(completedJob);
try {
mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(),
mRunningJob.getSourceUid());
} catch (RemoteException e) {
// Whatever.
if (mCancelled) {
if (DEBUG) {
Slog.d(TAG, "Job cancelled while waiting for onStartJob to complete.");
}
// Cancelled *while* waiting for acknowledgeStartMessage from client.
handleCancelLocked();
return;
}
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);
}
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 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 #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 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 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 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);
}
/**
* Called when sending a message to the client, over whose execution we have no control. If
* we haven't received a response in a certain amount of time, we want to give up and carry

View File

@@ -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,7 +338,16 @@ 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! We do any executing
// work first, since that was originally at the front of the pending work.
@@ -341,7 +364,10 @@ public final class JobStatus {
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;
}
}
@@ -353,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());
}
}
@@ -366,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;
}
}
@@ -381,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;
}
@@ -842,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);
@@ -907,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());
}
@@ -959,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: ");