From 152e9397237ad280baba4ad1f012b841191fe87e Mon Sep 17 00:00:00 2001 From: Antony Sargent Date: Thu, 15 Dec 2022 14:34:34 -0800 Subject: [PATCH] VirtualDevice clipboard support This CL refactors ClipboardService to provide siloed clipboard storage for VirtualDevices, adding an additional dimension for selecting clip storage alongside the existing userId dimension. This behavior is turned off by default behind a DeviceConfig flag. Bug: 262038440 Test: atest ClipboardManagerTest ClipboardManagerListenerTest StreamedAppClipboardTest Change-Id: I88de5a9930448f741169cd48d3804a9d936dfa76 --- .../android/content/ClipboardManager.java | 45 +- core/java/android/content/IClipboard.aidl | 26 +- .../companion/virtual/VirtualDeviceImpl.java | 3 +- .../virtual/VirtualDeviceManagerService.java | 17 +- .../server/clipboard/ClipboardService.java | 399 ++++++++++++++---- .../virtual/VirtualDeviceManagerInternal.java | 11 +- .../VirtualDeviceManagerServiceTest.java | 27 ++ 7 files changed, 414 insertions(+), 114 deletions(-) diff --git a/core/java/android/content/ClipboardManager.java b/core/java/android/content/ClipboardManager.java index 85af87722ed2e..06a3b1e1cc667 100644 --- a/core/java/android/content/ClipboardManager.java +++ b/core/java/android/content/ClipboardManager.java @@ -70,6 +70,23 @@ public class ClipboardManager extends android.text.ClipboardManager { */ public static final boolean DEVICE_CONFIG_DEFAULT_SHOW_ACCESS_NOTIFICATIONS = true; + /** + * DeviceConfig property, within the clipboard namespace, that determines whether VirtualDevices + * are allowed to have siloed Clipboards for the apps running on them. If false, then clipboard + * access is blocked entirely for apps running on VirtualDevices. + * + * @hide + */ + public static final String DEVICE_CONFIG_ALLOW_VIRTUALDEVICE_SILOS = + "allow_virtualdevice_silos"; + + /** + * Default value for the DEVICE_CONFIG_ALLOW_VIRTUALDEVICE_SILOS property. + * + * @hide + */ + public static final boolean DEVICE_CONFIG_DEFAULT_ALLOW_VIRTUALDEVICE_SILOS = false; + private final Context mContext; private final Handler mHandler; private final IClipboard mService; @@ -133,7 +150,8 @@ public class ClipboardManager extends android.text.ClipboardManager { clip, mContext.getOpPackageName(), mContext.getAttributionTag(), - mContext.getUserId()); + mContext.getUserId(), + mContext.getDeviceId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -162,6 +180,7 @@ public class ClipboardManager extends android.text.ClipboardManager { mContext.getOpPackageName(), mContext.getAttributionTag(), mContext.getUserId(), + mContext.getDeviceId(), sourcePackage); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -178,7 +197,8 @@ public class ClipboardManager extends android.text.ClipboardManager { mService.clearPrimaryClip( mContext.getOpPackageName(), mContext.getAttributionTag(), - mContext.getUserId()); + mContext.getUserId(), + mContext.getDeviceId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -197,7 +217,8 @@ public class ClipboardManager extends android.text.ClipboardManager { return mService.getPrimaryClip( mContext.getOpPackageName(), mContext.getAttributionTag(), - mContext.getUserId()); + mContext.getUserId(), + mContext.getDeviceId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -217,7 +238,8 @@ public class ClipboardManager extends android.text.ClipboardManager { return mService.getPrimaryClipDescription( mContext.getOpPackageName(), mContext.getAttributionTag(), - mContext.getUserId()); + mContext.getUserId(), + mContext.getDeviceId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -234,7 +256,8 @@ public class ClipboardManager extends android.text.ClipboardManager { return mService.hasPrimaryClip( mContext.getOpPackageName(), mContext.getAttributionTag(), - mContext.getUserId()); + mContext.getUserId(), + mContext.getDeviceId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -248,7 +271,8 @@ public class ClipboardManager extends android.text.ClipboardManager { mPrimaryClipChangedServiceListener, mContext.getOpPackageName(), mContext.getAttributionTag(), - mContext.getUserId()); + mContext.getUserId(), + mContext.getDeviceId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -266,7 +290,8 @@ public class ClipboardManager extends android.text.ClipboardManager { mPrimaryClipChangedServiceListener, mContext.getOpPackageName(), mContext.getAttributionTag(), - mContext.getUserId()); + mContext.getUserId(), + mContext.getDeviceId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -306,7 +331,8 @@ public class ClipboardManager extends android.text.ClipboardManager { return mService.hasClipboardText( mContext.getOpPackageName(), mContext.getAttributionTag(), - mContext.getUserId()); + mContext.getUserId(), + mContext.getDeviceId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -326,7 +352,8 @@ public class ClipboardManager extends android.text.ClipboardManager { return mService.getPrimaryClipSource( mContext.getOpPackageName(), mContext.getAttributionTag(), - mContext.getUserId()); + mContext.getUserId(), + mContext.getDeviceId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/content/IClipboard.aidl b/core/java/android/content/IClipboard.aidl index 46ece2bc3f5ee..b2216d4953e6c 100644 --- a/core/java/android/content/IClipboard.aidl +++ b/core/java/android/content/IClipboard.aidl @@ -26,22 +26,26 @@ import android.content.IOnPrimaryClipChangedListener; * {@hide} */ interface IClipboard { - void setPrimaryClip(in ClipData clip, String callingPackage, String attributionTag, int userId); - void setPrimaryClipAsPackage(in ClipData clip, String callingPackage, String attributionTag, int userId, - String sourcePackage); - void clearPrimaryClip(String callingPackage, String attributionTag, int userId); - ClipData getPrimaryClip(String pkg, String attributionTag, int userId); - ClipDescription getPrimaryClipDescription(String callingPackage, String attributionTag, int userId); - boolean hasPrimaryClip(String callingPackage, String attributionTag, int userId); + void setPrimaryClip(in ClipData clip, String callingPackage, String attributionTag, int userId, + int deviceId); + void setPrimaryClipAsPackage(in ClipData clip, String callingPackage, String attributionTag, + int userId, int deviceId, String sourcePackage); + void clearPrimaryClip(String callingPackage, String attributionTag, int userId, int deviceId); + ClipData getPrimaryClip(String pkg, String attributionTag, int userId, int deviceId); + ClipDescription getPrimaryClipDescription(String callingPackage, String attributionTag, + int userId, int deviceId); + boolean hasPrimaryClip(String callingPackage, String attributionTag, int userId, int deviceId); void addPrimaryClipChangedListener(in IOnPrimaryClipChangedListener listener, - String callingPackage, String attributionTag, int userId); + String callingPackage, String attributionTag, int userId, int deviceId); void removePrimaryClipChangedListener(in IOnPrimaryClipChangedListener listener, - String callingPackage, String attributionTag, int userId); + String callingPackage, String attributionTag, int userId, int deviceId); /** * Returns true if the clipboard contains text; false otherwise. */ - boolean hasClipboardText(String callingPackage, String attributionTag, int userId); + boolean hasClipboardText(String callingPackage, String attributionTag, int userId, + int deviceId); - String getPrimaryClipSource(String callingPackage, String attributionTag, int userId); + String getPrimaryClipSource(String callingPackage, String attributionTag, int userId, + int deviceId); } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 5985ce45e4d5b..3100277eb7624 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -88,7 +88,6 @@ import java.io.PrintWriter; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.function.Consumer; @@ -114,7 +113,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private final CameraAccessController mCameraAccessController; private VirtualAudioController mVirtualAudioController; @VisibleForTesting - final Set mVirtualDisplayIds = new ArraySet<>(); + final ArraySet mVirtualDisplayIds = new ArraySet<>(); private final OnDeviceCloseListener mOnDeviceCloseListener; private final IBinder mAppToken; private final VirtualDeviceParams mParams; diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 2395814cc4bf7..f39c32df18441 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -67,7 +67,6 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -471,7 +470,7 @@ public class VirtualDeviceManagerService extends SystemService { } @Override - public @NonNull Set getDeviceIdsForUid(int uid) { + public @NonNull ArraySet getDeviceIdsForUid(int uid) { ArraySet result = new ArraySet<>(); synchronized (mVirtualDeviceManagerLock) { int size = mVirtualDevices.size(); @@ -586,6 +585,20 @@ public class VirtualDeviceManagerService extends SystemService { return false; } + @Override + public @NonNull ArraySet getDisplayIdsForDevice(int deviceId) { + synchronized (mVirtualDeviceManagerLock) { + int size = mVirtualDevices.size(); + for (int i = 0; i < size; i++) { + VirtualDeviceImpl device = mVirtualDevices.valueAt(i); + if (device.getDeviceId() == deviceId) { + return new ArraySet<>(device.mVirtualDisplayIds); + } + } + } + return new ArraySet<>(); + } + @Override public void registerVirtualDisplayListener( @NonNull VirtualDisplayListener listener) { diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java index 6f9a17682dd7b..ecff0a7905592 100644 --- a/services/core/java/com/android/server/clipboard/ClipboardService.java +++ b/services/core/java/com/android/server/clipboard/ClipboardService.java @@ -17,6 +17,10 @@ package com.android.server.clipboard; import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; +import static android.companion.virtual.VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED; +import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_DEFAULT; +import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_INVALID; +import static android.companion.virtual.VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID; import android.Manifest; import android.annotation.NonNull; @@ -29,6 +33,8 @@ import android.app.AppOpsManager; import android.app.IUriGrantsManager; import android.app.KeyguardManager; import android.app.UriGrantsManager; +import android.companion.virtual.VirtualDeviceManager; +import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipDescription; import android.content.ClipboardManager; @@ -39,11 +45,13 @@ import android.content.Context; import android.content.IClipboard; import android.content.IOnPrimaryClipChangedListener; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.graphics.drawable.Drawable; +import android.hardware.display.DisplayManager; import android.net.Uri; import android.os.Binder; import android.os.Bundle; @@ -64,10 +72,13 @@ import android.provider.DeviceConfig; import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Pair; import android.util.SafetyProtectionUtils; import android.util.Slog; -import android.util.SparseArray; +import android.util.SparseArrayMap; import android.util.SparseBooleanArray; +import android.view.Display; import android.view.autofill.AutofillManagerInternal; import android.view.textclassifier.TextClassificationContext; import android.view.textclassifier.TextClassificationManager; @@ -130,7 +141,9 @@ public class ClipboardService extends SystemService { private final IUriGrantsManager mUgm; private final UriGrantsManagerInternal mUgmInternal; private final WindowManagerInternal mWm; - private final VirtualDeviceManagerInternal mVdm; + private final VirtualDeviceManagerInternal mVdmInternal; + private final VirtualDeviceManager mVdm; + private BroadcastReceiver mVirtualDeviceRemovedReceiver; private final IUserManager mUm; private final PackageManager mPm; private final AppOpsManager mAppOps; @@ -141,11 +154,15 @@ public class ClipboardService extends SystemService { private final Handler mWorkerHandler; @GuardedBy("mLock") - private final SparseArray mClipboards = new SparseArray<>(); + // Maps (userId, deviceId) to Clipboard. + private final SparseArrayMap mClipboards = new SparseArrayMap<>(); @GuardedBy("mLock") private boolean mShowAccessNotifications = ClipboardManager.DEVICE_CONFIG_DEFAULT_SHOW_ACCESS_NOTIFICATIONS; + @GuardedBy("mLock") + private boolean mAllowVirtualDeviceSilos = + ClipboardManager.DEVICE_CONFIG_DEFAULT_ALLOW_VIRTUALDEVICE_SILOS; @GuardedBy("mLock") private int mMaxClassificationLength = DEFAULT_MAX_CLASSIFICATION_LENGTH; @@ -163,7 +180,9 @@ public class ClipboardService extends SystemService { mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class); mWm = LocalServices.getService(WindowManagerInternal.class); // Can be null; not all products have CDM + VirtualDeviceManager - mVdm = LocalServices.getService(VirtualDeviceManagerInternal.class); + mVdmInternal = LocalServices.getService(VirtualDeviceManagerInternal.class); + mVdm = (mVdmInternal == null) ? null : getContext().getSystemService( + VirtualDeviceManager.class); mPm = getContext().getPackageManager(); mUm = (IUserManager) ServiceManager.getService(Context.USER_SERVICE); mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE); @@ -174,7 +193,7 @@ public class ClipboardService extends SystemService { if (IS_EMULATOR) { mEmulatorClipboardMonitor = new EmulatorClipboardMonitor((clip) -> { synchronized (mLock) { - setPrimaryClipInternalLocked(getClipboardLocked(0), clip, + setPrimaryClipInternalLocked(getClipboardLocked(0, DEVICE_ID_DEFAULT), clip, android.os.Process.SYSTEM_UID, null); } }); @@ -194,12 +213,39 @@ public class ClipboardService extends SystemService { @Override public void onStart() { publishBinderService(Context.CLIPBOARD_SERVICE, new ClipboardImpl()); + if (mVdmInternal != null) { + registerVirtualDeviceRemovedListener(); + } + } + + private void registerVirtualDeviceRemovedListener() { + if (mVirtualDeviceRemovedReceiver != null) { + return; + } + mVirtualDeviceRemovedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!intent.getAction().equals(ACTION_VIRTUAL_DEVICE_REMOVED)) { + return; + } + final int removedDeviceId = + intent.getIntExtra(EXTRA_VIRTUAL_DEVICE_ID, DEVICE_ID_INVALID); + synchronized (mLock) { + for (int i = mClipboards.numMaps() - 1; i >= 0; i--) { + mClipboards.delete(mClipboards.keyAt(i), removedDeviceId); + } + } + } + }; + IntentFilter filter = new IntentFilter(ACTION_VIRTUAL_DEVICE_REMOVED); + getContext().registerReceiver(mVirtualDeviceRemovedReceiver, filter, + Context.RECEIVER_NOT_EXPORTED); } @Override public void onUserStopped(@NonNull TargetUser user) { synchronized (mLock) { - mClipboards.remove(user.getUserIdentifier()); + mClipboards.delete(user.getUserIdentifier()); } } @@ -209,6 +255,10 @@ public class ClipboardService extends SystemService { DeviceConfig.NAMESPACE_CLIPBOARD, ClipboardManager.DEVICE_CONFIG_SHOW_ACCESS_NOTIFICATIONS, ClipboardManager.DEVICE_CONFIG_DEFAULT_SHOW_ACCESS_NOTIFICATIONS); + mAllowVirtualDeviceSilos = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_CLIPBOARD, + ClipboardManager.DEVICE_CONFIG_ALLOW_VIRTUALDEVICE_SILOS, + ClipboardManager.DEVICE_CONFIG_DEFAULT_ALLOW_VIRTUALDEVICE_SILOS); mMaxClassificationLength = DeviceConfig.getInt(DeviceConfig.NAMESPACE_CLIPBOARD, PROPERTY_MAX_CLASSIFICATION_LENGTH, DEFAULT_MAX_CLASSIFICATION_LENGTH); } @@ -226,8 +276,9 @@ public class ClipboardService extends SystemService { } } - private class PerUserClipboard { - final int userId; + private static class Clipboard { + public final int userId; + public final int deviceId; final RemoteCallbackList primaryClipListeners = new RemoteCallbackList(); @@ -254,8 +305,9 @@ public class ClipboardService extends SystemService { /** The text classifier session that is used to annotate the text in the primary clip. */ TextClassifier mTextClassifier; - PerUserClipboard(int userId) { + Clipboard(int userId, int deviceId) { this.userId = userId; + this.deviceId = deviceId; } } @@ -332,6 +384,54 @@ public class ClipboardService extends SystemService { UserHandle.getAppId(Binder.getCallingUid())); } + /** + * Determines which deviceId to use for selecting a Clipboard, depending on where a given app + * is running. + * + * @param requestedDeviceId the requested deviceId passed in from the client side + * @param uid the intended app uid + * @return a deviceId to use in selecting the appropriate clipboard, or + * DEVICE_ID_INVALID if this uid should not be allowed access. A value of DEVICE_ID_DEFAULT + * means just use the "regular" clipboard. + */ + private int getIntendingDeviceId(int requestedDeviceId, int uid) { + if (mVdmInternal == null) { + return DEVICE_ID_DEFAULT; + } + + ArraySet virtualDeviceIds = mVdmInternal.getDeviceIdsForUid(uid); + + synchronized (mLock) { + if (!mAllowVirtualDeviceSilos + && (!virtualDeviceIds.isEmpty() || requestedDeviceId != DEVICE_ID_DEFAULT)) { + return DEVICE_ID_INVALID; + } + } + + if (requestedDeviceId != DEVICE_ID_DEFAULT) { + // Privileged apps that own the VirtualDevices, or regular apps running on it, can + // request it by id. + if (mVdmInternal.getDeviceOwnerUid(requestedDeviceId) == uid + || virtualDeviceIds.contains(requestedDeviceId)) { + return requestedDeviceId; + } + return DEVICE_ID_INVALID; + } + + // The common case is apps running normally (not on a VirtualDevice). + if (virtualDeviceIds.isEmpty()) { + return DEVICE_ID_DEFAULT; + } + + // If an app is running on more than one VirtualDevice, it isn't clear which clipboard they + // should use. + if (virtualDeviceIds.size() > 1) { + return DEVICE_ID_INVALID; + } + + return virtualDeviceIds.valueAt(0); + } + /** * To handle the difference between userId and intendingUserId, uid and intendingUid. * @@ -364,8 +464,10 @@ public class ClipboardService extends SystemService { ClipData clip, String callingPackage, String attributionTag, - @UserIdInt int userId) { - checkAndSetPrimaryClip(clip, callingPackage, attributionTag, userId, callingPackage); + @UserIdInt int userId, + int deviceId) { + checkAndSetPrimaryClip(clip, callingPackage, attributionTag, userId, deviceId, + callingPackage); } @Override @@ -374,10 +476,12 @@ public class ClipboardService extends SystemService { String callingPackage, String attributionTag, @UserIdInt int userId, + int deviceId, String sourcePackage) { getContext().enforceCallingOrSelfPermission(Manifest.permission.SET_CLIP_SOURCE, "Requires SET_CLIP_SOURCE permission"); - checkAndSetPrimaryClip(clip, callingPackage, attributionTag, userId, sourcePackage); + checkAndSetPrimaryClip(clip, callingPackage, attributionTag, userId, deviceId, + sourcePackage); } private void checkAndSetPrimaryClip( @@ -385,41 +489,46 @@ public class ClipboardService extends SystemService { String callingPackage, String attributionTag, @UserIdInt int userId, + int deviceId, 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); + final int intendingDeviceId = getIntendingDeviceId(deviceId, intendingUid); if (!clipboardAccessAllowed( AppOpsManager.OP_WRITE_CLIPBOARD, callingPackage, attributionTag, intendingUid, - intendingUserId)) { + intendingUserId, + intendingDeviceId)) { return; } checkDataOwner(clip, intendingUid); synchronized (mLock) { - scheduleAutoClear(userId, intendingUid); - setPrimaryClipInternalLocked(clip, intendingUid, sourcePackage); + scheduleAutoClear(userId, intendingUid, intendingDeviceId); + setPrimaryClipInternalLocked(clip, intendingUid, intendingDeviceId, sourcePackage); } } - private void scheduleAutoClear(@UserIdInt int userId, int intendingUid) { + private void scheduleAutoClear( + @UserIdInt int userId, int intendingUid, int intendingDeviceId) { final long oldIdentity = Binder.clearCallingIdentity(); try { if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_CLIPBOARD, PROPERTY_AUTO_CLEAR_ENABLED, true)) { + Pair userIdDeviceId = new Pair<>(userId, intendingDeviceId); mClipboardClearHandler.removeEqualMessages(ClipboardClearHandler.MSG_CLEAR, - userId); + userIdDeviceId); Message clearMessage = Message.obtain( mClipboardClearHandler, ClipboardClearHandler.MSG_CLEAR, userId, intendingUid, - userId); + userIdDeviceId); mClipboardClearHandler.sendMessageDelayed(clearMessage, getTimeoutForAutoClear()); } @@ -436,52 +545,57 @@ public class ClipboardService extends SystemService { @Override public void clearPrimaryClip( - String callingPackage, String attributionTag, @UserIdInt int userId) { + String callingPackage, String attributionTag, @UserIdInt int userId, int deviceId) { final int intendingUid = getIntendingUid(callingPackage, userId); final int intendingUserId = UserHandle.getUserId(intendingUid); + final int intendingDeviceId = getIntendingDeviceId(deviceId, intendingUid); if (!clipboardAccessAllowed( AppOpsManager.OP_WRITE_CLIPBOARD, callingPackage, attributionTag, intendingUid, - intendingUserId)) { + intendingUserId, + intendingDeviceId)) { return; } synchronized (mLock) { mClipboardClearHandler.removeEqualMessages(ClipboardClearHandler.MSG_CLEAR, - userId); - setPrimaryClipInternalLocked(null, intendingUid, callingPackage); + new Pair<>(userId, deviceId)); + setPrimaryClipInternalLocked(null, intendingUid, intendingDeviceId, callingPackage); } } @Override - public ClipData getPrimaryClip(String pkg, String attributionTag, @UserIdInt int userId) { + public ClipData getPrimaryClip( + String pkg, String attributionTag, @UserIdInt int userId, int deviceId) { final int intendingUid = getIntendingUid(pkg, userId); final int intendingUserId = UserHandle.getUserId(intendingUid); + final int intendingDeviceId = getIntendingDeviceId(deviceId, intendingUid); if (!clipboardAccessAllowed( AppOpsManager.OP_READ_CLIPBOARD, pkg, attributionTag, intendingUid, - intendingUserId) + intendingUserId, + intendingDeviceId) || isDeviceLocked(intendingUserId)) { return null; } synchronized (mLock) { try { - addActiveOwnerLocked(intendingUid, pkg); + addActiveOwnerLocked(intendingUid, intendingDeviceId, pkg); } catch (SecurityException e) { // Permission could not be granted - URI may be invalid Slog.i(TAG, "Could not grant permission to primary clip. Clearing clipboard."); - setPrimaryClipInternalLocked(null, intendingUid, pkg); + setPrimaryClipInternalLocked(null, intendingUid, intendingDeviceId, pkg); return null; } - PerUserClipboard clipboard = getClipboardLocked(intendingUserId); + Clipboard clipboard = getClipboardLocked(intendingUserId, intendingDeviceId); showAccessNotificationLocked(pkg, intendingUid, intendingUserId, clipboard); notifyTextClassifierLocked(clipboard, pkg, intendingUid); if (clipboard.primaryClip != null) { - scheduleAutoClear(userId, intendingUid); + scheduleAutoClear(userId, intendingUid, intendingDeviceId); } return clipboard.primaryClip; } @@ -489,21 +603,23 @@ public class ClipboardService extends SystemService { @Override public ClipDescription getPrimaryClipDescription( - String callingPackage, String attributionTag, @UserIdInt int userId) { + String callingPackage, String attributionTag, @UserIdInt int userId, int deviceId) { final int intendingUid = getIntendingUid(callingPackage, userId); final int intendingUserId = UserHandle.getUserId(intendingUid); + final int intendingDeviceId = getIntendingDeviceId(deviceId, intendingUid); if (!clipboardAccessAllowed( AppOpsManager.OP_READ_CLIPBOARD, callingPackage, attributionTag, intendingUid, intendingUserId, + intendingDeviceId, false) || isDeviceLocked(intendingUserId)) { return null; } synchronized (mLock) { - PerUserClipboard clipboard = getClipboardLocked(intendingUserId); + Clipboard clipboard = getClipboardLocked(intendingUserId, intendingDeviceId); return clipboard.primaryClip != null ? clipboard.primaryClip.getDescription() : null; } @@ -511,21 +627,23 @@ public class ClipboardService extends SystemService { @Override public boolean hasPrimaryClip( - String callingPackage, String attributionTag, @UserIdInt int userId) { + String callingPackage, String attributionTag, @UserIdInt int userId, int deviceId) { final int intendingUid = getIntendingUid(callingPackage, userId); final int intendingUserId = UserHandle.getUserId(intendingUid); + final int intendingDeviceId = getIntendingDeviceId(deviceId, intendingUid); if (!clipboardAccessAllowed( AppOpsManager.OP_READ_CLIPBOARD, callingPackage, attributionTag, intendingUid, intendingUserId, + intendingDeviceId, false) || isDeviceLocked(intendingUserId)) { return false; } synchronized (mLock) { - return getClipboardLocked(intendingUserId).primaryClip != null; + return getClipboardLocked(intendingUserId, intendingDeviceId).primaryClip != null; } } @@ -534,11 +652,19 @@ public class ClipboardService extends SystemService { IOnPrimaryClipChangedListener listener, String callingPackage, String attributionTag, - @UserIdInt int userId) { + @UserIdInt int userId, + int deviceId) { final int intendingUid = getIntendingUid(callingPackage, userId); final int intendingUserId = UserHandle.getUserId(intendingUid); + final int intendingDeviceId = getIntendingDeviceId(deviceId, intendingUid); + if (intendingDeviceId == DEVICE_ID_INVALID) { + Slog.i(TAG, "addPrimaryClipChangedListener invalid deviceId for userId:" + + userId + " uid:" + intendingUid + " callingPackage:" + callingPackage + + " requestedDeviceId:" + deviceId); + return; + } synchronized (mLock) { - getClipboardLocked(intendingUserId) + getClipboardLocked(intendingUserId, intendingDeviceId) .primaryClipListeners .register( listener, @@ -551,29 +677,41 @@ public class ClipboardService extends SystemService { IOnPrimaryClipChangedListener listener, String callingPackage, String attributionTag, - @UserIdInt int userId) { + @UserIdInt int userId, + int deviceId) { + final int intendingUid = getIntendingUid(callingPackage, userId); final int intendingUserId = getIntendingUserId(callingPackage, userId); + final int intendingDeviceId = getIntendingDeviceId(deviceId, intendingUid); + if (intendingDeviceId == DEVICE_ID_INVALID) { + Slog.i(TAG, "removePrimaryClipChangedListener invalid deviceId for userId:" + + userId + " uid:" + intendingUid + " callingPackage:" + callingPackage); + return; + } synchronized (mLock) { - getClipboardLocked(intendingUserId).primaryClipListeners.unregister(listener); + getClipboardLocked(intendingUserId, + intendingDeviceId).primaryClipListeners.unregister(listener); } } @Override - public boolean hasClipboardText(String callingPackage, String attributionTag, int userId) { + public boolean hasClipboardText( + String callingPackage, String attributionTag, int userId, int deviceId) { final int intendingUid = getIntendingUid(callingPackage, userId); final int intendingUserId = UserHandle.getUserId(intendingUid); + final int intendingDeviceId = getIntendingDeviceId(deviceId, intendingUid); if (!clipboardAccessAllowed( AppOpsManager.OP_READ_CLIPBOARD, callingPackage, attributionTag, intendingUid, intendingUserId, + intendingDeviceId, false) || isDeviceLocked(intendingUserId)) { return false; } synchronized (mLock) { - PerUserClipboard clipboard = getClipboardLocked(intendingUserId); + Clipboard clipboard = getClipboardLocked(intendingUserId, intendingDeviceId); if (clipboard.primaryClip != null) { CharSequence text = clipboard.primaryClip.getItemAt(0).getText(); return text != null && text.length() > 0; @@ -584,23 +722,25 @@ public class ClipboardService extends SystemService { @Override public String getPrimaryClipSource( - String callingPackage, String attributionTag, int userId) { + String callingPackage, String attributionTag, int userId, int deviceId) { getContext().enforceCallingOrSelfPermission(Manifest.permission.SET_CLIP_SOURCE, "Requires SET_CLIP_SOURCE permission"); final int intendingUid = getIntendingUid(callingPackage, userId); final int intendingUserId = UserHandle.getUserId(intendingUid); + final int intendingDeviceId = getIntendingDeviceId(deviceId, intendingUid); if (!clipboardAccessAllowed( AppOpsManager.OP_READ_CLIPBOARD, callingPackage, attributionTag, intendingUid, intendingUserId, + intendingDeviceId, false) || isDeviceLocked(intendingUserId)) { return null; } synchronized (mLock) { - PerUserClipboard clipboard = getClipboardLocked(intendingUserId); + Clipboard clipboard = getClipboardLocked(intendingUserId, intendingDeviceId); if (clipboard.primaryClip != null) { return clipboard.mPrimaryClipPackage; } @@ -621,11 +761,13 @@ public class ClipboardService extends SystemService { case MSG_CLEAR: final int userId = msg.arg1; final int intendingUid = msg.arg2; + final int intendingDeviceId = ((Pair) msg.obj).second; synchronized (mLock) { - if (getClipboardLocked(userId).primaryClip != null) { + if (getClipboardLocked(userId, intendingDeviceId).primaryClip != null) { FrameworkStatsLog.write(FrameworkStatsLog.CLIPBOARD_CLEARED, FrameworkStatsLog.CLIPBOARD_CLEARED__SOURCE__AUTO_CLEAR); - setPrimaryClipInternalLocked(null, intendingUid, null); + setPrimaryClipInternalLocked( + null, intendingUid, intendingDeviceId, null); } } break; @@ -637,13 +779,13 @@ public class ClipboardService extends SystemService { }; @GuardedBy("mLock") - private PerUserClipboard getClipboardLocked(@UserIdInt int userId) { - PerUserClipboard puc = mClipboards.get(userId); - if (puc == null) { - puc = new PerUserClipboard(userId); - mClipboards.put(userId, puc); + private Clipboard getClipboardLocked(@UserIdInt int userId, int deviceId) { + Clipboard clipboard = mClipboards.get(userId, deviceId); + if (clipboard == null) { + clipboard = new Clipboard(userId, deviceId); + mClipboards.add(userId, deviceId, clipboard); } - return puc; + return clipboard; } List getRelatedProfiles(@UserIdInt int userId) { @@ -675,19 +817,20 @@ public class ClipboardService extends SystemService { void setPrimaryClipInternal(@Nullable ClipData clip, int uid) { synchronized (mLock) { - setPrimaryClipInternalLocked(clip, uid, null); + setPrimaryClipInternalLocked(clip, uid, DEVICE_ID_DEFAULT, null); } } @GuardedBy("mLock") private void setPrimaryClipInternalLocked( - @Nullable ClipData clip, int uid, @Nullable String sourcePackage) { + @Nullable ClipData clip, int uid, int deviceId, @Nullable String sourcePackage) { mEmulatorClipboardMonitor.accept(clip); final int userId = UserHandle.getUserId(uid); // Update this user - setPrimaryClipInternalLocked(getClipboardLocked(userId), clip, uid, sourcePackage); + setPrimaryClipInternalLocked(getClipboardLocked(userId, deviceId), clip, uid, + sourcePackage); // Update related users List related = getRelatedProfiles(userId); @@ -722,7 +865,7 @@ public class ClipboardService extends SystemService { UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE, id); if (canCopyIntoProfile) { setPrimaryClipInternalNoClassifyLocked( - getClipboardLocked(id), clip, uid, sourcePackage); + getClipboardLocked(id, deviceId), clip, uid, sourcePackage); } } } @@ -730,7 +873,7 @@ public class ClipboardService extends SystemService { } } - void setPrimaryClipInternal(PerUserClipboard clipboard, @Nullable ClipData clip, + void setPrimaryClipInternal(Clipboard clipboard, @Nullable ClipData clip, int uid) { synchronized (mLock) { setPrimaryClipInternalLocked(clipboard, clip, uid, null); @@ -738,18 +881,18 @@ public class ClipboardService extends SystemService { } @GuardedBy("mLock") - private void setPrimaryClipInternalLocked(PerUserClipboard clipboard, @Nullable ClipData clip, + private void setPrimaryClipInternalLocked(Clipboard clipboard, @Nullable ClipData clip, int uid, @Nullable String sourcePackage) { final int userId = UserHandle.getUserId(uid); if (clip != null) { - startClassificationLocked(clip, userId); + startClassificationLocked(clip, userId, clipboard.deviceId); } setPrimaryClipInternalNoClassifyLocked(clipboard, clip, uid, sourcePackage); } @GuardedBy("mLock") - private void setPrimaryClipInternalNoClassifyLocked(PerUserClipboard clipboard, + private void setPrimaryClipInternalNoClassifyLocked(Clipboard clipboard, @Nullable ClipData clip, int uid, @Nullable String sourcePackage) { revokeUris(clipboard); clipboard.activePermissionOwners.clear(); @@ -775,7 +918,7 @@ public class ClipboardService extends SystemService { sendClipChangedBroadcast(clipboard); } - private void sendClipChangedBroadcast(PerUserClipboard clipboard) { + private void sendClipChangedBroadcast(Clipboard clipboard) { final long ident = Binder.clearCallingIdentity(); final int n = clipboard.primaryClipListeners.beginBroadcast(); try { @@ -789,7 +932,8 @@ public class ClipboardService extends SystemService { li.mPackageName, li.mAttributionTag, li.mUid, - UserHandle.getUserId(li.mUid))) { + UserHandle.getUserId(li.mUid), + clipboard.deviceId)) { clipboard.primaryClipListeners.getBroadcastItem(i) .dispatchPrimaryClipChanged(); } @@ -805,7 +949,8 @@ public class ClipboardService extends SystemService { } @GuardedBy("mLock") - private void startClassificationLocked(@NonNull ClipData clip, @UserIdInt int userId) { + private void startClassificationLocked(@NonNull ClipData clip, @UserIdInt int userId, + int deviceId) { CharSequence text = (clip.getItemCount() == 0) ? null : clip.getItemAt(0).getText(); if (TextUtils.isEmpty(text) || text.length() > mMaxClassificationLength) { clip.getDescription().setClassificationStatus( @@ -830,12 +975,13 @@ public class ClipboardService extends SystemService { ClipDescription.CLASSIFICATION_NOT_PERFORMED); return; } - mWorkerHandler.post(() -> doClassification(text, clip, classifier, userId)); + mWorkerHandler.post(() -> doClassification(text, clip, classifier, userId, deviceId)); } @WorkerThread private void doClassification( - CharSequence text, ClipData clip, TextClassifier classifier, @UserIdInt int userId) { + CharSequence text, ClipData clip, TextClassifier classifier, @UserIdInt int userId, + int deviceId) { TextLinks.Request request = new TextLinks.Request.Builder(text).build(); TextLinks links = classifier.generateLinks(request); @@ -852,7 +998,7 @@ public class ClipboardService extends SystemService { } synchronized (mLock) { - PerUserClipboard clipboard = getClipboardLocked(userId); + Clipboard clipboard = getClipboardLocked(userId, deviceId); if (clipboard.primaryClip == clip) { applyClassificationAndSendBroadcastLocked( clipboard, confidences, links, classifier); @@ -867,7 +1013,7 @@ public class ClipboardService extends SystemService { final boolean canCopyIntoProfile = !hasRestriction( UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE, id); if (canCopyIntoProfile) { - PerUserClipboard relatedClipboard = getClipboardLocked(id); + Clipboard relatedClipboard = getClipboardLocked(id, deviceId); if (hasTextLocked(relatedClipboard, text)) { applyClassificationAndSendBroadcastLocked( relatedClipboard, confidences, links, classifier); @@ -882,7 +1028,7 @@ public class ClipboardService extends SystemService { @GuardedBy("mLock") private void applyClassificationAndSendBroadcastLocked( - PerUserClipboard clipboard, ArrayMap confidences, TextLinks links, + Clipboard clipboard, ArrayMap confidences, TextLinks links, TextClassifier classifier) { clipboard.mTextClassifier = classifier; clipboard.primaryClip.getDescription().setConfidenceScores(confidences); @@ -893,7 +1039,7 @@ public class ClipboardService extends SystemService { } @GuardedBy("mLock") - private boolean hasTextLocked(PerUserClipboard clipboard, @NonNull CharSequence text) { + private boolean hasTextLocked(Clipboard clipboard, @NonNull CharSequence text) { return clipboard.primaryClip != null && clipboard.primaryClip.getItemCount() > 0 && text.equals(clipboard.primaryClip.getItemAt(0).getText()); @@ -972,7 +1118,7 @@ public class ClipboardService extends SystemService { } @GuardedBy("mLock") - private void addActiveOwnerLocked(int uid, String pkg) { + private void addActiveOwnerLocked(int uid, int deviceId, String pkg) { final IPackageManager pm = AppGlobals.getPackageManager(); final int targetUserHandle = UserHandle.getCallingUserId(); final long oldIdentity = Binder.clearCallingIdentity(); @@ -990,7 +1136,7 @@ public class ClipboardService extends SystemService { } finally { Binder.restoreCallingIdentity(oldIdentity); } - PerUserClipboard clipboard = getClipboardLocked(UserHandle.getUserId(uid)); + Clipboard clipboard = getClipboardLocked(UserHandle.getUserId(uid), deviceId); if (clipboard.primaryClip != null && !clipboard.activePermissionOwners.contains(pkg)) { final int N = clipboard.primaryClip.getItemCount(); for (int i=0; i toastContexts = getToastContexts(clipboard); Binder.withCleanCallingIdentity(() -> { try { CharSequence callingAppLabel = mPm.getApplicationLabel( @@ -1183,23 +1355,72 @@ public class ClipboardService extends SystemService { String message = getContext().getString(R.string.pasted_from_clipboard, callingAppLabel); Slog.i(TAG, message); - Toast toastToShow; - if (SafetyProtectionUtils.shouldShowSafetyProtectionResources(getContext())) { - Drawable safetyProtectionIcon = getContext() - .getDrawable(R.drawable.ic_safety_protection); - toastToShow = Toast.makeCustomToastWithIcon(getContext(), - UiThread.get().getLooper(), message, - Toast.LENGTH_SHORT, safetyProtectionIcon); - } else { - toastToShow = Toast.makeText( - getContext(), UiThread.get().getLooper(), message, - Toast.LENGTH_SHORT); + for (int i = 0; i < toastContexts.size(); i++) { + Context toastContext = toastContexts.valueAt(i); + Toast toastToShow; + if (SafetyProtectionUtils.shouldShowSafetyProtectionResources(getContext())) { + Drawable safetyProtectionIcon = getContext() + .getDrawable(R.drawable.ic_safety_protection); + toastToShow = Toast.makeCustomToastWithIcon(toastContext, + UiThread.get().getLooper(), message, + Toast.LENGTH_SHORT, safetyProtectionIcon); + } else { + toastToShow = Toast.makeText( + toastContext, UiThread.get().getLooper(), message, + Toast.LENGTH_SHORT); + } + toastToShow.show(); } - toastToShow.show(); } catch (PackageManager.NameNotFoundException e) { // do nothing } }); + + clipboard.mNotifiedUids.put(uid, true); + } + + /** + * Returns the context(s) to use for toasts related to this clipboard. Normally this will just + * contain a single context referencing the default display. + * + * If the clipboard is for a VirtualDevice, we attempt to return the single DisplayContext for + * the focused VirtualDisplay for that device, but might need to return the contexts for + * multiple displays if the VirtualDevice has several but none of them were focused. + */ + private ArraySet getToastContexts(Clipboard clipboard) throws IllegalStateException { + ArraySet contexts = new ArraySet<>(); + + if (clipboard.deviceId != DEVICE_ID_DEFAULT) { + DisplayManager displayManager = getContext().getSystemService(DisplayManager.class); + + int topFocusedDisplayId = mWm.getTopFocusedDisplayId(); + ArraySet displayIds = mVdmInternal.getDisplayIdsForDevice(clipboard.deviceId); + + if (displayIds.contains(topFocusedDisplayId)) { + Display display = displayManager.getDisplay(topFocusedDisplayId); + if (display != null) { + contexts.add(getContext().createDisplayContext(display)); + return contexts; + } + } + + for (int i = 0; i < displayIds.size(); i++) { + Display display = displayManager.getDisplay(displayIds.valueAt(i)); + if (display != null) { + contexts.add(getContext().createDisplayContext(display)); + } + } + if (!contexts.isEmpty()) { + return contexts; + } + Slog.e(TAG, "getToastContexts Couldn't find any VirtualDisplays for VirtualDevice " + + clipboard.deviceId); + // Since we couldn't find any VirtualDisplays to use at all, just fall through to using + // the default display below. + } + + contexts.add(getContext()); + return contexts; } /** @@ -1221,7 +1442,7 @@ public class ClipboardService extends SystemService { /** Potentially notifies the text classifier that an app is accessing a text clip. */ @GuardedBy("mLock") private void notifyTextClassifierLocked( - PerUserClipboard clipboard, String callingPackage, int callingUid) { + Clipboard clipboard, String callingPackage, int callingUid) { if (clipboard.primaryClip == null) { return; } diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java index 974c04bc45f51..001cb10e1b295 100644 --- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java +++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.companion.virtual.IVirtualDevice; import android.os.LocaleList; +import android.util.ArraySet; import java.util.Set; @@ -84,7 +85,7 @@ public abstract class VirtualDeviceManagerInternal { * *Note* this only checks VirtualDevices, and does not include information about whether * the app is running on the default device or not. */ - public abstract @NonNull Set getDeviceIdsForUid(int uid); + public abstract @NonNull ArraySet getDeviceIdsForUid(int uid); /** * Notifies that a virtual display is created. @@ -132,4 +133,12 @@ public abstract class VirtualDeviceManagerInternal { * Returns true if the {@code displayId} is owned by any virtual device */ public abstract boolean isDisplayOwnedByAnyVirtualDevice(int displayId); + + /** + * Gets the ids of VirtualDisplays owned by a VirtualDevice. + * + * @param deviceId which device we're asking about + * @return the set of display ids for all VirtualDisplays owned by the device + */ + public abstract @NonNull ArraySet getDisplayIdsForDevice(int deviceId); } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index dad7977a8bb3d..3791b3527b669 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -1595,6 +1595,33 @@ public class VirtualDeviceManagerServiceTest { verify(mSoundEffectListener).onPlaySoundEffect(AudioManager.FX_KEY_CLICK); } + @Test + public void getDisplayIdsForDevice_invalidDeviceId_emptyResult() { + ArraySet displayIds = mLocalService.getDisplayIdsForDevice(VIRTUAL_DEVICE_ID_2); + assertThat(displayIds).isEmpty(); + } + + @Test + public void getDisplayIdsForDevice_noDisplays_emptyResult() { + ArraySet displayIds = mLocalService.getDisplayIdsForDevice(VIRTUAL_DEVICE_ID_1); + assertThat(displayIds).isEmpty(); + } + + @Test + public void getDisplayIdsForDevice_oneDisplay_resultContainsCorrectDisplayId() { + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1); + ArraySet displayIds = mLocalService.getDisplayIdsForDevice(VIRTUAL_DEVICE_ID_1); + assertThat(displayIds).containsExactly(DISPLAY_ID_1); + } + + @Test + public void getDisplayIdsForDevice_twoDisplays_resultContainsCorrectDisplayIds() { + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1); + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_2); + ArraySet displayIds = mLocalService.getDisplayIdsForDevice(VIRTUAL_DEVICE_ID_1); + assertThat(displayIds).containsExactly(DISPLAY_ID_1, DISPLAY_ID_2); + } + private VirtualDeviceImpl createVirtualDevice(int virtualDeviceId, int ownerUid) { VirtualDeviceParams params = new VirtualDeviceParams.Builder() .setBlockedActivities(getBlockedActivities())