From 54c65485055d77428ae1120307be7cf38bc7d2ec Mon Sep 17 00:00:00 2001 From: Oli Lan Date: Fri, 26 Feb 2021 15:42:59 +0000 Subject: [PATCH] Add system API to attribute source of clip data. This adds a system API to allow the source package to be included when clip data is set on CLipboardManager. This is needed to ensure that the clipboard access notifications can be properly attributed. For example, when a copy is performed through the share sheet, the data should be attributed to the app that opened the share sheet, not Android System. Bug: 180577866 Test: new tests added to ClipboardManagerTest in CTS Change-Id: Ic0ef2ea218e1dcb738b401f61cd20cd5bba6baec --- core/api/system-current.txt | 4 ++ core/api/test-current.txt | 4 ++ .../android/content/ClipboardManager.java | 46 +++++++++++++++ core/java/android/content/IClipboard.aidl | 4 ++ .../server/clipboard/ClipboardService.java | 59 +++++++++++++++---- 5 files changed, 106 insertions(+), 11 deletions(-) diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 3b0fa019319a6..ab00affab881d 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2120,6 +2120,10 @@ package android.content { method @NonNull public final android.os.UserHandle getSendingUser(); } + public class ClipboardManager extends android.text.ClipboardManager { + method @RequiresPermission(android.Manifest.permission.SET_CLIP_SOURCE) public void setPrimaryClipAsPackage(@NonNull android.content.ClipData, @NonNull String); + } + public abstract class ContentProvider implements android.content.ComponentCallbacks2 { method public int checkUriPermission(@NonNull android.net.Uri, int, int); } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index b7e294aed6b51..75b9525b110c1 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -658,6 +658,10 @@ package android.content { field @Nullable public android.util.ArraySet whitelistedActivitiesForAugmentedAutofill; } + public class ClipboardManager extends android.text.ClipboardManager { + method @Nullable @RequiresPermission(android.Manifest.permission.SET_CLIP_SOURCE) public String getPrimaryClipSource(); + } + public final class ContentCaptureOptions implements android.os.Parcelable { ctor public ContentCaptureOptions(int); ctor public ContentCaptureOptions(int, int, int, int, int, @Nullable android.util.ArraySet); diff --git a/core/java/android/content/ClipboardManager.java b/core/java/android/content/ClipboardManager.java index 7f73238a41e37..cadbd609ff0bc 100644 --- a/core/java/android/content/ClipboardManager.java +++ b/core/java/android/content/ClipboardManager.java @@ -16,9 +16,13 @@ package android.content; +import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.os.Handler; import android.os.RemoteException; @@ -108,6 +112,31 @@ public class ClipboardManager extends android.text.ClipboardManager { } } + /** + * Sets the current primary clip on the clipboard, attributed to the specified {@code + * sourcePackage}. The primary clip is the clip that is involved in normal cut and paste + * operations. + * + * @param clip The clipped data item to set. + * @param sourcePackage The package name of the app that is the source of the clip data. + * @throws IllegalArgumentException if the clip is null or contains no items. + * + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.SET_CLIP_SOURCE) + public void setPrimaryClipAsPackage(@NonNull ClipData clip, @NonNull String sourcePackage) { + try { + Objects.requireNonNull(clip); + Objects.requireNonNull(sourcePackage); + clip.prepareToLeaveProcess(true); + mService.setPrimaryClipAsPackage( + clip, mContext.getOpPackageName(), mContext.getUserId(), sourcePackage); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Clears any current primary clip on the clipboard. * @@ -234,6 +263,23 @@ public class ClipboardManager extends android.text.ClipboardManager { } } + /** + * Returns the package name of the source of the current primary clip, or null if there is no + * primary clip or if a source is not available. + * + * @hide + */ + @TestApi + @Nullable + @RequiresPermission(Manifest.permission.SET_CLIP_SOURCE) + public String getPrimaryClipSource() { + try { + return mService.getPrimaryClipSource(mContext.getOpPackageName(), mContext.getUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + @UnsupportedAppUsage void reportPrimaryClipChanged() { Object[] listeners; diff --git a/core/java/android/content/IClipboard.aidl b/core/java/android/content/IClipboard.aidl index 0d5a46016f195..102b8e798a5cd 100644 --- a/core/java/android/content/IClipboard.aidl +++ b/core/java/android/content/IClipboard.aidl @@ -27,6 +27,8 @@ import android.content.IOnPrimaryClipChangedListener; */ interface IClipboard { void setPrimaryClip(in ClipData clip, String callingPackage, int userId); + void setPrimaryClipAsPackage(in ClipData clip, String callingPackage, int userId, + String sourcePackage); void clearPrimaryClip(String callingPackage, int userId); ClipData getPrimaryClip(String pkg, int userId); ClipDescription getPrimaryClipDescription(String callingPackage, int userId); @@ -40,4 +42,6 @@ interface IClipboard { * Returns true if the clipboard contains text; false otherwise. */ boolean hasClipboardText(String callingPackage, int userId); + + String getPrimaryClipSource(String callingPackage, int userId); } diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java index 088249e811715..cfc0c92cabaeb 100644 --- a/services/core/java/com/android/server/clipboard/ClipboardService.java +++ b/services/core/java/com/android/server/clipboard/ClipboardService.java @@ -379,20 +379,37 @@ public class ClipboardService extends SystemService { @Override public void setPrimaryClip(ClipData clip, String callingPackage, @UserIdInt int userId) { synchronized (this) { - if (clip == null || clip.getItemCount() <= 0) { - throw new IllegalArgumentException("No items"); - } - final int intendingUid = getIntendingUid(callingPackage, userId); - final int intendingUserId = UserHandle.getUserId(intendingUid); - if (!clipboardAccessAllowed(AppOpsManager.OP_WRITE_CLIPBOARD, callingPackage, - intendingUid, intendingUserId)) { - return; - } - checkDataOwnerLocked(clip, intendingUid); - setPrimaryClipInternal(clip, intendingUid, callingPackage); + checkAndSetPrimaryClipLocked(clip, callingPackage, userId, callingPackage); } } + @Override + public void setPrimaryClipAsPackage( + ClipData clip, String callingPackage, @UserIdInt int userId, String sourcePackage) { + getContext().enforceCallingOrSelfPermission(Manifest.permission.SET_CLIP_SOURCE, + "Requires SET_CLIP_SOURCE permission"); + + synchronized (this) { + checkAndSetPrimaryClipLocked(clip, callingPackage, userId, sourcePackage); + } + } + + @GuardedBy("this") + private void checkAndSetPrimaryClipLocked( + ClipData clip, String callingPackage, @UserIdInt int userId, String sourcePackage) { + if (clip == null || clip.getItemCount() <= 0) { + throw new IllegalArgumentException("No items"); + } + final int intendingUid = getIntendingUid(callingPackage, userId); + final int intendingUserId = UserHandle.getUserId(intendingUid); + if (!clipboardAccessAllowed(AppOpsManager.OP_WRITE_CLIPBOARD, callingPackage, + intendingUid, intendingUserId)) { + return; + } + checkDataOwnerLocked(clip, intendingUid); + setPrimaryClipInternal(clip, intendingUid, sourcePackage); + } + @Override public void clearPrimaryClip(String callingPackage, @UserIdInt int userId) { synchronized (this) { @@ -492,6 +509,26 @@ public class ClipboardService extends SystemService { return false; } } + + @Override + public String getPrimaryClipSource(String callingPackage, int userId) { + getContext().enforceCallingOrSelfPermission(Manifest.permission.SET_CLIP_SOURCE, + "Requires SET_CLIP_SOURCE permission"); + synchronized (this) { + final int intendingUid = getIntendingUid(callingPackage, userId); + final int intendingUserId = UserHandle.getUserId(intendingUid); + if (!clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, callingPackage, + intendingUid, intendingUserId, false) + || isDeviceLocked(intendingUserId)) { + return null; + } + PerUserClipboard clipboard = getClipboard(intendingUserId); + if (clipboard.primaryClip != null) { + return clipboard.mPrimaryClipPackage; + } + return null; + } + } }; private PerUserClipboard getClipboard(@UserIdInt int userId) {