Implement issue #36590595: Add ability to associated a ClipData with JobInfo
Yum!
Also needed to have a Context.revokeUriPermission() variant that is sane,
so reasonable CTS tests can be written.
Test: new ClipDataJobTest added.
Change-Id: Ia3135ea788a6e32c971bae7dab3a844d0ef4139c
diff --git a/api/current.txt b/api/current.txt
index ec2a0e6..e2f8e3a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6740,6 +6740,8 @@
public class JobInfo implements android.os.Parcelable {
method public int describeContents();
method public int getBackoffPolicy();
+ method public android.content.ClipData getClipData();
+ method public int getClipGrantFlags();
method public android.os.PersistableBundle getExtras();
method public long getFlexMillis();
method public int getId();
@@ -6778,6 +6780,7 @@
method public android.app.job.JobInfo.Builder addTriggerContentUri(android.app.job.JobInfo.TriggerContentUri);
method public android.app.job.JobInfo build();
method public android.app.job.JobInfo.Builder setBackoffCriteria(long, int);
+ method public android.app.job.JobInfo.Builder setClipData(android.content.ClipData, int);
method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle);
method public android.app.job.JobInfo.Builder setMinimumLatency(long);
method public android.app.job.JobInfo.Builder setOverrideDeadline(long);
@@ -6806,6 +6809,8 @@
public class JobParameters implements android.os.Parcelable {
method public int describeContents();
+ method public android.content.ClipData getClipData();
+ method public int getClipGrantFlags();
method public android.os.PersistableBundle getExtras();
method public int getJobId();
method public android.os.Bundle getTransientExtras();
@@ -8841,6 +8846,7 @@
method public abstract deprecated void removeStickyBroadcast(android.content.Intent);
method public abstract deprecated void removeStickyBroadcastAsUser(android.content.Intent, android.os.UserHandle);
method public abstract void revokeUriPermission(android.net.Uri, int);
+ method public abstract void revokeUriPermission(java.lang.String, android.net.Uri, int);
method public abstract void sendBroadcast(android.content.Intent);
method public abstract void sendBroadcast(android.content.Intent, java.lang.String);
method public abstract void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle);
@@ -9033,6 +9039,7 @@
method public deprecated void removeStickyBroadcast(android.content.Intent);
method public deprecated void removeStickyBroadcastAsUser(android.content.Intent, android.os.UserHandle);
method public void revokeUriPermission(android.net.Uri, int);
+ method public void revokeUriPermission(java.lang.String, android.net.Uri, int);
method public void sendBroadcast(android.content.Intent);
method public void sendBroadcast(android.content.Intent, java.lang.String);
method public void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle);
@@ -40664,6 +40671,7 @@
method public void removeStickyBroadcast(android.content.Intent);
method public void removeStickyBroadcastAsUser(android.content.Intent, android.os.UserHandle);
method public void revokeUriPermission(android.net.Uri, int);
+ method public void revokeUriPermission(java.lang.String, android.net.Uri, int);
method public void sendBroadcast(android.content.Intent);
method public void sendBroadcast(android.content.Intent, java.lang.String);
method public void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle);
diff --git a/api/system-current.txt b/api/system-current.txt
index 2890041..1f37f9b 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -7167,6 +7167,8 @@
public class JobInfo implements android.os.Parcelable {
method public int describeContents();
method public int getBackoffPolicy();
+ method public android.content.ClipData getClipData();
+ method public int getClipGrantFlags();
method public android.os.PersistableBundle getExtras();
method public long getFlexMillis();
method public int getId();
@@ -7205,6 +7207,7 @@
method public android.app.job.JobInfo.Builder addTriggerContentUri(android.app.job.JobInfo.TriggerContentUri);
method public android.app.job.JobInfo build();
method public android.app.job.JobInfo.Builder setBackoffCriteria(long, int);
+ method public android.app.job.JobInfo.Builder setClipData(android.content.ClipData, int);
method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle);
method public android.app.job.JobInfo.Builder setMinimumLatency(long);
method public android.app.job.JobInfo.Builder setOverrideDeadline(long);
@@ -7233,6 +7236,8 @@
public class JobParameters implements android.os.Parcelable {
method public int describeContents();
+ method public android.content.ClipData getClipData();
+ method public int getClipGrantFlags();
method public android.os.PersistableBundle getExtras();
method public int getJobId();
method public android.os.Bundle getTransientExtras();
@@ -9333,6 +9338,7 @@
method public abstract deprecated void removeStickyBroadcast(android.content.Intent);
method public abstract deprecated void removeStickyBroadcastAsUser(android.content.Intent, android.os.UserHandle);
method public abstract void revokeUriPermission(android.net.Uri, int);
+ method public abstract void revokeUriPermission(java.lang.String, android.net.Uri, int);
method public abstract void sendBroadcast(android.content.Intent);
method public abstract void sendBroadcast(android.content.Intent, java.lang.String);
method public abstract void sendBroadcast(android.content.Intent, java.lang.String, android.os.Bundle);
@@ -9539,6 +9545,7 @@
method public deprecated void removeStickyBroadcast(android.content.Intent);
method public deprecated void removeStickyBroadcastAsUser(android.content.Intent, android.os.UserHandle);
method public void revokeUriPermission(android.net.Uri, int);
+ method public void revokeUriPermission(java.lang.String, android.net.Uri, int);
method public void sendBroadcast(android.content.Intent);
method public void sendBroadcast(android.content.Intent, java.lang.String);
method public void sendBroadcast(android.content.Intent, java.lang.String, android.os.Bundle);
@@ -44099,6 +44106,7 @@
method public void removeStickyBroadcast(android.content.Intent);
method public void removeStickyBroadcastAsUser(android.content.Intent, android.os.UserHandle);
method public void revokeUriPermission(android.net.Uri, int);
+ method public void revokeUriPermission(java.lang.String, android.net.Uri, int);
method public void sendBroadcast(android.content.Intent);
method public void sendBroadcast(android.content.Intent, java.lang.String);
method public void sendBroadcast(android.content.Intent, java.lang.String, android.os.Bundle);
diff --git a/api/test-current.txt b/api/test-current.txt
index 69ea620..c2fb7e1 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -6769,6 +6769,8 @@
public class JobInfo implements android.os.Parcelable {
method public int describeContents();
method public int getBackoffPolicy();
+ method public android.content.ClipData getClipData();
+ method public int getClipGrantFlags();
method public android.os.PersistableBundle getExtras();
method public long getFlexMillis();
method public int getId();
@@ -6807,6 +6809,7 @@
method public android.app.job.JobInfo.Builder addTriggerContentUri(android.app.job.JobInfo.TriggerContentUri);
method public android.app.job.JobInfo build();
method public android.app.job.JobInfo.Builder setBackoffCriteria(long, int);
+ method public android.app.job.JobInfo.Builder setClipData(android.content.ClipData, int);
method public android.app.job.JobInfo.Builder setExtras(android.os.PersistableBundle);
method public android.app.job.JobInfo.Builder setMinimumLatency(long);
method public android.app.job.JobInfo.Builder setOverrideDeadline(long);
@@ -6835,6 +6838,8 @@
public class JobParameters implements android.os.Parcelable {
method public int describeContents();
+ method public android.content.ClipData getClipData();
+ method public int getClipGrantFlags();
method public android.os.PersistableBundle getExtras();
method public int getJobId();
method public android.os.Bundle getTransientExtras();
@@ -8873,6 +8878,7 @@
method public abstract deprecated void removeStickyBroadcast(android.content.Intent);
method public abstract deprecated void removeStickyBroadcastAsUser(android.content.Intent, android.os.UserHandle);
method public abstract void revokeUriPermission(android.net.Uri, int);
+ method public abstract void revokeUriPermission(java.lang.String, android.net.Uri, int);
method public abstract void sendBroadcast(android.content.Intent);
method public abstract void sendBroadcast(android.content.Intent, java.lang.String);
method public abstract void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle);
@@ -9066,6 +9072,7 @@
method public deprecated void removeStickyBroadcast(android.content.Intent);
method public deprecated void removeStickyBroadcastAsUser(android.content.Intent, android.os.UserHandle);
method public void revokeUriPermission(android.net.Uri, int);
+ method public void revokeUriPermission(java.lang.String, android.net.Uri, int);
method public void sendBroadcast(android.content.Intent);
method public void sendBroadcast(android.content.Intent, java.lang.String);
method public void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle);
@@ -40868,6 +40875,7 @@
method public void removeStickyBroadcast(android.content.Intent);
method public void removeStickyBroadcastAsUser(android.content.Intent, android.os.UserHandle);
method public void revokeUriPermission(android.net.Uri, int);
+ method public void revokeUriPermission(java.lang.String, android.net.Uri, int);
method public void sendBroadcast(android.content.Intent);
method public void sendBroadcast(android.content.Intent, java.lang.String);
method public void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle);
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 467ba99..5a7246a 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1790,7 +1790,18 @@
public void revokeUriPermission(Uri uri, int modeFlags) {
try {
ActivityManager.getService().revokeUriPermission(
- mMainThread.getApplicationThread(),
+ mMainThread.getApplicationThread(), null,
+ ContentProvider.getUriWithoutUserId(uri), modeFlags, resolveUserId(uri));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public void revokeUriPermission(String targetPackage, Uri uri, int modeFlags) {
+ try {
+ ActivityManager.getService().revokeUriPermission(
+ mMainThread.getApplicationThread(), targetPackage,
ContentProvider.getUriWithoutUserId(uri), modeFlags, resolveUserId(uri));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 0a5e4be..f4d26fd 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -172,7 +172,8 @@
in IBinder callerToken);
void grantUriPermission(in IApplicationThread caller, in String targetPkg, in Uri uri,
int mode, int userId);
- void revokeUriPermission(in IApplicationThread caller, in Uri uri, int mode, int userId);
+ void revokeUriPermission(in IApplicationThread caller, in String targetPkg, in Uri uri,
+ int mode, int userId);
void setActivityController(in IActivityController watcher, boolean imAMonkey);
void showWaitingForDebugger(in IApplicationThread who, boolean waiting);
/*
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index 78e4c0d..96eb0ea 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -20,6 +20,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.ClipData;
import android.content.ComponentName;
import android.net.Uri;
import android.os.Bundle;
@@ -197,6 +198,8 @@
private final int jobId;
private final PersistableBundle extras;
private final Bundle transientExtras;
+ private final ClipData clipData;
+ private final int clipGrantFlags;
private final ComponentName service;
private final int constraintFlags;
private final TriggerContentUri[] triggerContentUris;
@@ -240,6 +243,21 @@
}
/**
+ * ClipData of information that is returned to your application at execution time,
+ * but not persisted by the system.
+ */
+ public ClipData getClipData() {
+ return clipData;
+ }
+
+ /**
+ * Permission grants that go along with {@link #getClipData}.
+ */
+ public int getClipGrantFlags() {
+ return clipGrantFlags;
+ }
+
+ /**
* Name of the service endpoint that will be called back into by the JobScheduler.
*/
public ComponentName getService() {
@@ -415,6 +433,13 @@
jobId = in.readInt();
extras = in.readPersistableBundle();
transientExtras = in.readBundle();
+ if (in.readInt() != 0) {
+ clipData = ClipData.CREATOR.createFromParcel(in);
+ clipGrantFlags = in.readInt();
+ } else {
+ clipData = null;
+ clipGrantFlags = 0;
+ }
service = in.readParcelable(null);
constraintFlags = in.readInt();
triggerContentUris = in.createTypedArray(TriggerContentUri.CREATOR);
@@ -439,6 +464,8 @@
jobId = b.mJobId;
extras = b.mExtras.deepCopy();
transientExtras = b.mTransientExtras.deepCopy();
+ clipData = b.mClipData;
+ clipGrantFlags = b.mClipGrantFlags;
service = b.mJobService;
constraintFlags = b.mConstraintFlags;
triggerContentUris = b.mTriggerContentUris != null
@@ -471,6 +498,13 @@
out.writeInt(jobId);
out.writePersistableBundle(extras);
out.writeBundle(transientExtras);
+ if (clipData != null) {
+ out.writeInt(1);
+ clipData.writeToParcel(out, flags);
+ out.writeInt(clipGrantFlags);
+ } else {
+ out.writeInt(0);
+ }
out.writeParcelable(service, flags);
out.writeInt(constraintFlags);
out.writeTypedArray(triggerContentUris, flags);
@@ -597,6 +631,8 @@
private final ComponentName mJobService;
private PersistableBundle mExtras = PersistableBundle.EMPTY;
private Bundle mTransientExtras = Bundle.EMPTY;
+ private ClipData mClipData;
+ private int mClipGrantFlags;
private int mPriority = PRIORITY_DEFAULT;
private int mFlags;
// Requirements.
@@ -669,6 +705,34 @@
}
/**
+ * Set a {@link ClipData} associated with this Job.
+ *
+ * <p>The main purpose of providing a ClipData is to allow granting of
+ * URI permissions for data associated with the clip. The exact kind
+ * of permission grant to perform is specified through <var>grantFlags</var>.
+ *
+ * <p>If the ClipData contains items that are Intents, any
+ * grant flags in those Intents will be ignored. Only flags provided as an argument
+ * to this method are respected, and will be applied to all Uri or
+ * Intent items in the clip (or sub-items of the clip).
+ *
+ * <p>Because setting this property is not compatible with persisted
+ * jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when
+ * {@link android.app.job.JobInfo.Builder#build()} is called.</p>
+ *
+ * @param clip The new clip to set. May be null to clear the current clip.
+ * @param grantFlags The desired permissions to grant for any URIs. This should be
+ * a combination of {@link android.content.Intent#FLAG_GRANT_READ_URI_PERMISSION},
+ * {@link android.content.Intent#FLAG_GRANT_WRITE_URI_PERMISSION}, and
+ * {@link android.content.Intent#FLAG_GRANT_PREFIX_URI_PERMISSION}.
+ */
+ public Builder setClipData(ClipData clip, int grantFlags) {
+ mClipData = clip;
+ mClipGrantFlags = grantFlags;
+ return this;
+ }
+
+ /**
* Set some description of the kind of network type your job needs to have.
* Not calling this function means the network is not necessary, as the default is
* {@link #NETWORK_TYPE_NONE}.
@@ -892,25 +956,33 @@
"constraints, this is not allowed.");
}
// Check that a deadline was not set on a periodic job.
- if (mIsPeriodic && (mMaxExecutionDelayMillis != 0L)) {
- throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " +
- "periodic job.");
+ if (mIsPeriodic) {
+ if (mMaxExecutionDelayMillis != 0L) {
+ throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " +
+ "periodic job.");
+ }
+ if (mMinLatencyMillis != 0L) {
+ throw new IllegalArgumentException("Can't call setMinimumLatency() on a " +
+ "periodic job");
+ }
+ if (mTriggerContentUris != null) {
+ throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
+ "periodic job");
+ }
}
- if (mIsPeriodic && (mMinLatencyMillis != 0L)) {
- throw new IllegalArgumentException("Can't call setMinimumLatency() on a " +
- "periodic job");
- }
- if (mIsPeriodic && (mTriggerContentUris != null)) {
- throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
- "periodic job");
- }
- if (mIsPersisted && (mTriggerContentUris != null)) {
- throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
- "persisted job");
- }
- if (mIsPersisted && !mTransientExtras.isEmpty()) {
- throw new IllegalArgumentException("Can't call setTransientExtras() on a " +
- "persisted job");
+ if (mIsPersisted) {
+ if (mTriggerContentUris != null) {
+ throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
+ "persisted job");
+ }
+ if (!mTransientExtras.isEmpty()) {
+ throw new IllegalArgumentException("Can't call setTransientExtras() on a " +
+ "persisted job");
+ }
+ if (mClipData != null) {
+ throw new IllegalArgumentException("Can't call setClipData() on a " +
+ "persisted job");
+ }
}
if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) {
throw new IllegalArgumentException("An idle mode job will not respect any" +
diff --git a/core/java/android/app/job/JobParameters.java b/core/java/android/app/job/JobParameters.java
index ba168b7..8d52d3b 100644
--- a/core/java/android/app/job/JobParameters.java
+++ b/core/java/android/app/job/JobParameters.java
@@ -17,6 +17,7 @@
package android.app.job;
import android.app.job.IJobCallback;
+import android.content.ClipData;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
@@ -44,6 +45,8 @@
private final int jobId;
private final PersistableBundle extras;
private final Bundle transientExtras;
+ private final ClipData clipData;
+ private final int clipGrantFlags;
private final IBinder callback;
private final boolean overrideDeadlineExpired;
private final Uri[] mTriggeredContentUris;
@@ -53,11 +56,14 @@
/** @hide */
public JobParameters(IBinder callback, int jobId, PersistableBundle extras,
- Bundle transientExtras, boolean overrideDeadlineExpired, Uri[] triggeredContentUris,
+ Bundle transientExtras, ClipData clipData, int clipGrantFlags,
+ boolean overrideDeadlineExpired, Uri[] triggeredContentUris,
String[] triggeredContentAuthorities) {
this.jobId = jobId;
this.extras = extras;
this.transientExtras = transientExtras;
+ this.clipData = clipData;
+ this.clipGrantFlags = clipGrantFlags;
this.callback = callback;
this.overrideDeadlineExpired = overrideDeadlineExpired;
this.mTriggeredContentUris = triggeredContentUris;
@@ -98,6 +104,24 @@
}
/**
+ * @return The clip you passed in when constructing this job with
+ * {@link android.app.job.JobInfo.Builder#setClipData(ClipData, int)}. Will be null
+ * if it was not set.
+ */
+ public ClipData getClipData() {
+ return clipData;
+ }
+
+ /**
+ * @return The clip grant flags you passed in when constructing this job with
+ * {@link android.app.job.JobInfo.Builder#setClipData(ClipData, int)}. Will be 0
+ * if it was not set.
+ */
+ public int getClipGrantFlags() {
+ return clipGrantFlags;
+ }
+
+ /**
* For jobs with {@link android.app.job.JobInfo.Builder#setOverrideDeadline(long)} set, this
* provides an easy way to tell whether the job is being executed due to the deadline
* expiring. Note: If the job is running because its deadline expired, it implies that its
@@ -140,6 +164,13 @@
jobId = in.readInt();
extras = in.readPersistableBundle();
transientExtras = in.readBundle();
+ if (in.readInt() != 0) {
+ clipData = ClipData.CREATOR.createFromParcel(in);
+ clipGrantFlags = in.readInt();
+ } else {
+ clipData = null;
+ clipGrantFlags = 0;
+ }
callback = in.readStrongBinder();
overrideDeadlineExpired = in.readInt() == 1;
mTriggeredContentUris = in.createTypedArray(Uri.CREATOR);
@@ -162,6 +193,13 @@
dest.writeInt(jobId);
dest.writePersistableBundle(extras);
dest.writeBundle(transientExtras);
+ if (clipData != null) {
+ dest.writeInt(1);
+ clipData.writeToParcel(dest, flags);
+ dest.writeInt(clipGrantFlags);
+ } else {
+ dest.writeInt(0);
+ }
dest.writeStrongBinder(callback);
dest.writeInt(overrideDeadlineExpired ? 1 : 0);
dest.writeTypedArray(mTriggeredContentUris, flags);
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 1803bbe..dbbfe30 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4078,8 +4078,8 @@
/**
* Remove all permissions to access a particular content provider Uri
- * that were previously added with {@link #grantUriPermission}. The given
- * Uri will match all previously granted Uris that are the same or a
+ * that were previously added with {@link #grantUriPermission} or <em>any other</em> mechanism.
+ * The given Uri will match all previously granted Uris that are the same or a
* sub-path of the given Uri. That is, revoking "content://foo/target" will
* revoke both "content://foo/target" and "content://foo/target/sub", but not
* "content://foo". It will not remove any prefix grants that exist at a
@@ -4089,10 +4089,16 @@
* regular permission access to a Uri, but had received access to it through
* a specific Uri permission grant, you could not revoke that grant with this
* function and a {@link SecurityException} would be thrown. As of
- * {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this function will not throw a security exception,
- * but will remove whatever permission grants to the Uri had been given to the app
+ * {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this function will not throw a security
+ * exception, but will remove whatever permission grants to the Uri had been given to the app
* (or none).</p>
*
+ * <p>Unlike {@link #revokeUriPermission(String, Uri, int)}, this method impacts all permission
+ * grants matching the given Uri, for any package they had been granted to, through any
+ * mechanism this had happened (such as indirectly through the clipboard, activity launch,
+ * service start, etc). That means this can be potentially dangerous to use, as it can
+ * revoke grants that another app could be strongly expecting to stick around.</p>
+ *
* @param uri The Uri you would like to revoke access to.
* @param modeFlags The desired access modes. Any combination of
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION
@@ -4105,6 +4111,34 @@
public abstract void revokeUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags);
/**
+ * Remove permissions to access a particular content provider Uri
+ * that were previously added with {@link #grantUriPermission} for a specific target
+ * package. The given Uri will match all previously granted Uris that are the same or a
+ * sub-path of the given Uri. That is, revoking "content://foo/target" will
+ * revoke both "content://foo/target" and "content://foo/target/sub", but not
+ * "content://foo". It will not remove any prefix grants that exist at a
+ * higher level.
+ *
+ * <p>Unlike {@link #revokeUriPermission(Uri, int)}, this method will <em>only</em>
+ * revoke permissions that had been explicitly granted through {@link #grantUriPermission}
+ * and only for the package specified. Any matching grants that have happened through
+ * other mechanisms (clipboard, activity launching, service starting, etc) will not be
+ * removed.</p>
+ *
+ * @param toPackage The package you had previously granted access to.
+ * @param uri The Uri you would like to revoke access to.
+ * @param modeFlags The desired access modes. Any combination of
+ * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION
+ * Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+ * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION
+ * Intent.FLAG_GRANT_WRITE_URI_PERMISSION}.
+ *
+ * @see #grantUriPermission
+ */
+ public abstract void revokeUriPermission(String toPackage, Uri uri,
+ @Intent.AccessUriMode int modeFlags);
+
+ /**
* Determine whether a particular process and user ID has been granted
* permission to access a specific URI. This only checks for permissions
* that have been explicitly granted -- if the given process/uid has
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 75784a6..53b021c 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -781,6 +781,11 @@
}
@Override
+ public void revokeUriPermission(String targetPackage, Uri uri, int modeFlags) {
+ mBase.revokeUriPermission(targetPackage, uri, modeFlags);
+ }
+
+ @Override
public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) {
return mBase.checkUriPermission(uri, pid, uid, modeFlags);
}
diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java
index e82fe03..6f388e2 100644
--- a/core/java/android/os/BaseBundle.java
+++ b/core/java/android/os/BaseBundle.java
@@ -311,6 +311,20 @@
}
/**
+ * @hide this should probably be the implementation of isEmpty(). To do that we
+ * need to ensure we always use the special empty parcel form when the bundle is
+ * empty. (This may already be the case, but to be safe we'll do this later when
+ * we aren't trying to stabilize.)
+ */
+ public boolean maybeIsEmpty() {
+ if (isParcelled()) {
+ return isEmptyParcel();
+ } else {
+ return isEmpty();
+ }
+ }
+
+ /**
* Removes all elements from the mapping of this Bundle.
*/
public void clear() {
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index c1292e7..9b5ff29 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -1209,4 +1209,18 @@
}
return "Bundle[" + mMap.toString() + "]";
}
+
+ /**
+ * @hide
+ */
+ public synchronized String toShortString() {
+ if (mParcelledData != null) {
+ if (isEmptyParcel()) {
+ return "EMPTY_PARCEL";
+ } else {
+ return "mParcelledData.dataSize=" + mParcelledData.dataSize();
+ }
+ }
+ return mMap.toString();
+ }
}
diff --git a/core/java/android/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java
index 75f9c11..3ed5b17 100644
--- a/core/java/android/os/PersistableBundle.java
+++ b/core/java/android/os/PersistableBundle.java
@@ -309,4 +309,16 @@
}
return "PersistableBundle[" + mMap.toString() + "]";
}
+
+ /** @hide */
+ synchronized public String toShortString() {
+ if (mParcelledData != null) {
+ if (isEmptyParcel()) {
+ return "EMPTY_PARCEL";
+ } else {
+ return "mParcelledData.dataSize=" + mParcelledData.dataSize();
+ }
+ }
+ return mMap.toString();
+ }
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b4da152..71ad674 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -2386,7 +2386,7 @@
} break;
case DELETE_DUMPHEAP_MSG: {
revokeUriPermission(ActivityThread.currentActivityThread().getApplicationThread(),
- DumpHeapActivity.JAVA_URI,
+ null, DumpHeapActivity.JAVA_URI,
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
UserHandle.myUserId());
@@ -8944,7 +8944,8 @@
}
}
- private void revokeUriPermissionLocked(int callingUid, GrantUri grantUri, final int modeFlags) {
+ private void revokeUriPermissionLocked(String targetPackage, int callingUid, GrantUri grantUri,
+ final int modeFlags) {
if (DEBUG_URI_PERMISSION) Slog.v(TAG_URI_PERMISSION,
"Revoking all granted permissions to " + grantUri);
@@ -8965,8 +8966,11 @@
final ArrayMap<GrantUri, UriPermission> perms = mGrantedUriPermissions.get(callingUid);
if (perms != null) {
boolean persistChanged = false;
- for (Iterator<UriPermission> it = perms.values().iterator(); it.hasNext();) {
- final UriPermission perm = it.next();
+ for (int i = perms.size()-1; i >= 0; i--) {
+ final UriPermission perm = perms.valueAt(i);
+ if (targetPackage != null && !targetPackage.equals(perm.targetPkg)) {
+ continue;
+ }
if (perm.uri.sourceUserId == grantUri.sourceUserId
&& perm.uri.uri.isPathPrefixMatch(grantUri.uri)) {
if (DEBUG_URI_PERMISSION) Slog.v(TAG_URI_PERMISSION,
@@ -8975,7 +8979,7 @@
persistChanged |= perm.revokeModes(
modeFlags | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION, false);
if (perm.modeFlags == 0) {
- it.remove();
+ perms.removeAt(i);
}
}
}
@@ -8992,29 +8996,30 @@
boolean persistChanged = false;
// Go through all of the permissions and remove any that match.
- int N = mGrantedUriPermissions.size();
- for (int i = 0; i < N; i++) {
+ for (int i = mGrantedUriPermissions.size()-1; i >= 0; i--) {
final int targetUid = mGrantedUriPermissions.keyAt(i);
final ArrayMap<GrantUri, UriPermission> perms = mGrantedUriPermissions.valueAt(i);
- for (Iterator<UriPermission> it = perms.values().iterator(); it.hasNext();) {
- final UriPermission perm = it.next();
+ for (int j = perms.size()-1; j >= 0; j--) {
+ final UriPermission perm = perms.valueAt(j);
+ if (targetPackage != null && !targetPackage.equals(perm.targetPkg)) {
+ continue;
+ }
if (perm.uri.sourceUserId == grantUri.sourceUserId
&& perm.uri.uri.isPathPrefixMatch(grantUri.uri)) {
if (DEBUG_URI_PERMISSION) Slog.v(TAG_URI_PERMISSION,
"Revoking " + perm.targetUid + " permission to " + perm.uri);
persistChanged |= perm.revokeModes(
- modeFlags | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION, true);
+ modeFlags | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION,
+ targetPackage == null);
if (perm.modeFlags == 0) {
- it.remove();
+ perms.removeAt(j);
}
}
}
if (perms.isEmpty()) {
- mGrantedUriPermissions.remove(targetUid);
- N--;
- i--;
+ mGrantedUriPermissions.removeAt(i);
}
}
@@ -9028,8 +9033,8 @@
* @param userId The userId in which the uri is to be resolved.
*/
@Override
- public void revokeUriPermission(IApplicationThread caller, Uri uri, final int modeFlags,
- int userId) {
+ public void revokeUriPermission(IApplicationThread caller, String targetPackage, Uri uri,
+ final int modeFlags, int userId) {
enforceNotIsolatedCaller("revokeUriPermission");
synchronized(this) {
final ProcessRecord r = getRecordForAppLocked(caller);
@@ -9056,7 +9061,8 @@
return;
}
- revokeUriPermissionLocked(r.uid, new GrantUri(userId, uri, false), modeFlags);
+ revokeUriPermissionLocked(targetPackage, r.uid, new GrantUri(userId, uri, false),
+ modeFlags);
}
}
@@ -21164,7 +21170,7 @@
public void run() {
revokeUriPermission(ActivityThread.currentActivityThread()
.getApplicationThread(),
- DumpHeapActivity.JAVA_URI,
+ null, DumpHeapActivity.JAVA_URI,
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
UserHandle.myUserId());
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index 46d7bfc..2de9aae 100644
--- a/services/core/java/com/android/server/job/JobSchedulerService.java
+++ b/services/core/java/com/android/server/job/JobSchedulerService.java
@@ -616,6 +616,9 @@
}
}
+ // This may throw a SecurityException.
+ jobStatus.prepare(ActivityManager.getService());
+
toCancel = mJobs.getJobByUidAndJobId(uId, job.getId());
if (toCancel != null) {
cancelJobImpl(toCancel, jobStatus);
@@ -712,6 +715,7 @@
private void cancelJobImpl(JobStatus cancelled, JobStatus incomingJob) {
if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString());
+ cancelled.unprepare(ActivityManager.getService());
stopTrackingJob(cancelled, incomingJob, true /* writeBack */);
synchronized (mLock) {
// Remove from pending queue.
@@ -892,6 +896,9 @@
*/
private void startTrackingJob(JobStatus jobStatus, JobStatus lastJob) {
synchronized (mLock) {
+ if (!jobStatus.isPrepared()) {
+ Slog.wtf(TAG, "Not yet prepared when started tracking: " + jobStatus);
+ }
final boolean update = mJobs.add(jobStatus);
if (mReadyToRock) {
for (int i = 0; i < mControllers.size(); i++) {
@@ -1078,11 +1085,22 @@
// that may cause ordering problems if the app removes jobStatus while in here.
if (needsReschedule) {
JobStatus rescheduled = getRescheduleJobForFailure(jobStatus);
+ try {
+ rescheduled.prepare(ActivityManager.getService());
+ } catch (SecurityException e) {
+ Slog.w(TAG, "Unable to regrant job permissions for " + rescheduled);
+ }
startTrackingJob(rescheduled, jobStatus);
} else if (jobStatus.getJob().isPeriodic()) {
JobStatus rescheduledPeriodic = getRescheduleJobForPeriodic(jobStatus);
+ try {
+ rescheduledPeriodic.prepare(ActivityManager.getService());
+ } catch (SecurityException e) {
+ Slog.w(TAG, "Unable to regrant job permissions for " + rescheduledPeriodic);
+ }
startTrackingJob(rescheduledPeriodic, jobStatus);
}
+ jobStatus.unprepare(ActivityManager.getService());
reportActive();
mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget();
}
diff --git a/services/core/java/com/android/server/job/JobServiceContext.java b/services/core/java/com/android/server/job/JobServiceContext.java
index 1337046..19b9888 100644
--- a/services/core/java/com/android/server/job/JobServiceContext.java
+++ b/services/core/java/com/android/server/job/JobServiceContext.java
@@ -17,6 +17,7 @@
package com.android.server.job;
import android.app.ActivityManager;
+import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.IJobCallback;
import android.app.job.IJobService;
@@ -187,9 +188,10 @@
triggeredAuthorities = new String[job.changedAuthorities.size()];
job.changedAuthorities.toArray(triggeredAuthorities);
}
- mParams = new JobParameters(this, job.getJobId(), job.getExtras(),
- job.getTransientExtras(), isDeadlineExpired,
- triggeredUris, triggeredAuthorities);
+ final JobInfo ji = job.getJob();
+ mParams = new JobParameters(this, job.getJobId(), ji.getExtras(),
+ ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(),
+ isDeadlineExpired, triggeredUris, triggeredAuthorities);
mExecutionStartTimeElapsed = SystemClock.elapsedRealtime();
mVerb = VERB_BINDING;
diff --git a/services/core/java/com/android/server/job/JobStore.java b/services/core/java/com/android/server/job/JobStore.java
index ccfc287..c0264df 100644
--- a/services/core/java/com/android/server/job/JobStore.java
+++ b/services/core/java/com/android/server/job/JobStore.java
@@ -16,6 +16,8 @@
package com.android.server.job;
+import android.app.ActivityManager;
+import android.app.IActivityManager;
import android.content.ComponentName;
import android.app.job.JobInfo;
import android.content.Context;
@@ -294,7 +296,7 @@
addAttributesToJobTag(out, jobStatus);
writeConstraintsToXml(out, jobStatus);
writeExecutionCriteriaToXml(out, jobStatus);
- writeBundleToXml(jobStatus.getExtras(), out);
+ writeBundleToXml(jobStatus.getJob().getExtras(), out);
out.endTag(null, "job");
}
out.endTag(null, "job-info");
@@ -449,8 +451,11 @@
synchronized (mLock) {
jobs = readJobMapImpl(fis);
if (jobs != null) {
+ IActivityManager am = ActivityManager.getService();
for (int i=0; i<jobs.size(); i++) {
- this.jobSet.add(jobs.get(i));
+ JobStatus js = jobs.get(i);
+ js.prepare(am);
+ this.jobSet.add(js);
}
}
}
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 ebb53a1..47630d0 100644
--- a/services/core/java/com/android/server/job/controllers/JobStatus.java
+++ b/services/core/java/com/android/server/job/controllers/JobStatus.java
@@ -17,19 +17,25 @@
package com.android.server.job.controllers;
import android.app.AppGlobals;
+import android.app.IActivityManager;
import android.app.job.JobInfo;
+import android.content.ClipData;
import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.Intent;
import android.net.Uri;
-import android.os.Bundle;
-import android.os.PersistableBundle;
+import android.os.Binder;
+import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.format.DateUtils;
import android.util.ArraySet;
+import android.util.Slog;
import android.util.TimeUtils;
import java.io.PrintWriter;
+import java.util.Arrays;
/**
* Uniquely identifies a job internally.
@@ -43,6 +49,8 @@
* @hide
*/
public final class JobStatus {
+ static final String TAG = "JobSchedulerService";
+
public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE;
public static final long NO_EARLIEST_RUNTIME = 0L;
@@ -88,6 +96,9 @@
final String tag;
+ private IBinder permissionOwner;
+ private boolean prepared;
+
/**
* Earliest point in the future at which this job will be eligible to run. A value of 0
* indicates there is no delay constraint. See {@link #hasTimingDelayConstraint()}.
@@ -241,6 +252,93 @@
earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis);
}
+ public void prepare(IActivityManager am) {
+ if (prepared) {
+ Slog.wtf(TAG, "Already prepared: " + this);
+ return;
+ }
+ 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);
+ }
+ }
+ }
+
+ public void unprepare(IActivityManager am) {
+ if (!prepared) {
+ Slog.wtf(TAG, "Hasn't been prepared: " + this);
+ 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));
+ }
+ }
+ }
+ }
+
+ public boolean isPrepared() {
+ 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;
}
@@ -295,14 +393,6 @@
return tag;
}
- public PersistableBundle getExtras() {
- return job.getExtras();
- }
-
- public Bundle getTransientExtras() {
- return job.getTransientExtras();
- }
-
public int getPriority() {
return job.getPriority();
}
@@ -505,22 +595,64 @@
@Override
public String toString() {
- return String.valueOf(hashCode()).substring(0, 3) + ".."
- + ":[" + job.getService()
- + ",jId=" + job.getId()
- + ",u" + getUserId()
- + ",suid=" + getSourceUid()
- + ",R=(" + formatRunTime(earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME)
- + "," + formatRunTime(latestRunTimeElapsedMillis, NO_LATEST_RUNTIME) + ")"
- + ",N=" + job.getNetworkType() + ",C=" + job.isRequireCharging()
- + ",BL=" + job.isRequireBatteryNotLow()
- + ",I=" + job.isRequireDeviceIdle()
- + ",U=" + (job.getTriggerContentUris() != null)
- + ",F=" + numFailures + ",P=" + job.isPersisted()
- + ",ANI=" + ((satisfiedConstraints&CONSTRAINT_APP_NOT_IDLE) != 0)
- + ",DND=" + ((satisfiedConstraints&CONSTRAINT_DEVICE_NOT_DOZING) != 0)
- + (isReady() ? "(READY)" : "")
- + "]";
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("JobStatus{");
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ sb.append(" #");
+ UserHandle.formatUid(sb, callingUid);
+ sb.append("/");
+ sb.append(job.getId());
+ sb.append(' ');
+ sb.append(batteryName);
+ sb.append(" u=");
+ sb.append(getUserId());
+ sb.append(" s=");
+ sb.append(getSourceUid());
+ if (earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME
+ || latestRunTimeElapsedMillis != NO_LATEST_RUNTIME) {
+ sb.append(" TIME=");
+ sb.append(formatRunTime(earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME));
+ sb.append("-");
+ sb.append(formatRunTime(latestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME));
+ }
+ if (job.getNetworkType() != JobInfo.NETWORK_TYPE_NONE) {
+ sb.append(" NET=");
+ sb.append(job.getNetworkType());
+ }
+ if (job.isRequireCharging()) {
+ sb.append(" CHARGING");
+ }
+ if (job.isRequireBatteryNotLow()) {
+ sb.append(" BATNOTLOW");
+ }
+ if (job.isRequireStorageNotLow()) {
+ sb.append(" STORENOTLOW");
+ }
+ if (job.isRequireDeviceIdle()) {
+ sb.append(" IDLE");
+ }
+ if (job.isPersisted()) {
+ sb.append(" PERSISTED");
+ }
+ if ((satisfiedConstraints&CONSTRAINT_APP_NOT_IDLE) == 0) {
+ sb.append(" WAIT:APP_NOT_IDLE");
+ }
+ if ((satisfiedConstraints&CONSTRAINT_DEVICE_NOT_DOZING) == 0) {
+ sb.append(" WAIT:DEV_NOT_DOZING");
+ }
+ if (job.getTriggerContentUris() != null) {
+ sb.append(" URIS=");
+ sb.append(Arrays.toString(job.getTriggerContentUris()));
+ }
+ if (numFailures != 0) {
+ sb.append(" failures=");
+ sb.append(numFailures);
+ }
+ if (isReady()) {
+ sb.append(" READY");
+ }
+ sb.append("}");
+ return sb.toString();
}
private String formatRunTime(long runtime, long defaultValue) {
@@ -613,8 +745,9 @@
pw.print(" user="); pw.print(getSourceUserId());
pw.print(" pkg="); pw.println(getSourcePackageName());
if (full) {
- pw.print(prefix); pw.println("JobInfo:"); pw.print(prefix);
- pw.print(" Service: "); pw.println(job.getService().flattenToShortString());
+ pw.print(prefix); pw.println("JobInfo:");
+ pw.print(prefix); pw.print(" Service: ");
+ pw.println(job.getService().flattenToShortString());
if (job.isPeriodic()) {
pw.print(prefix); pw.print(" PERIODIC: interval=");
TimeUtils.formatDuration(job.getIntervalMillis(), pw);
@@ -654,6 +787,20 @@
pw.println();
}
}
+ if (job.getExtras() != null && !job.getExtras().maybeIsEmpty()) {
+ pw.print(prefix); pw.print(" Extras: ");
+ pw.println(job.getExtras().toShortString());
+ }
+ if (job.getTransientExtras() != null && !job.getTransientExtras().maybeIsEmpty()) {
+ pw.print(prefix); pw.print(" Transient extras: ");
+ pw.println(job.getTransientExtras().toShortString());
+ }
+ if (job.getClipData() != null) {
+ pw.print(prefix); pw.print(" Clip data: ");
+ StringBuilder b = new StringBuilder(128);
+ job.getClipData().toShortString(b);
+ pw.println(b);
+ }
if (job.getNetworkType() != JobInfo.NETWORK_TYPE_NONE) {
pw.print(prefix); pw.print(" Network type: "); pw.println(job.getNetworkType());
}
diff --git a/test-runner/src/android/test/mock/MockContext.java b/test-runner/src/android/test/mock/MockContext.java
index dca74ff..a8eb986 100644
--- a/test-runner/src/android/test/mock/MockContext.java
+++ b/test-runner/src/android/test/mock/MockContext.java
@@ -653,6 +653,11 @@
}
@Override
+ public void revokeUriPermission(String targetPackage, Uri uri, int modeFlags) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) {
throw new UnsupportedOperationException();
}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
index 06272c8..cc5a0c3 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
@@ -1659,6 +1659,12 @@
}
@Override
+ public void revokeUriPermission(String arg0, Uri arg1, int arg2) {
+ // pass
+
+ }
+
+ @Override
public void sendBroadcast(Intent arg0) {
// pass