blob: 093ecd57124f612ec8025dac17ec456662818837 [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.clipboard;
import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.annotation.WorkerThread;
import android.app.ActivityManagerInternal;
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.IUriGrantsManager;
import android.app.KeyguardManager;
import android.app.UriGrantsManager;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.IClipboard;
import android.content.IOnPrimaryClipChangedListener;
import android.content.Intent;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.IUserManager;
import android.os.Parcel;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.view.autofill.AutofillManagerInternal;
import android.view.textclassifier.TextClassificationContext;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextClassifierEvent;
import android.view.textclassifier.TextLinks;
import android.widget.Toast;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.UiThread;
import com.android.server.contentcapture.ContentCaptureManagerInternal;
import com.android.server.uri.UriGrantsManagerInternal;
import com.android.server.wm.WindowManagerInternal;
import java.util.HashSet;
import java.util.List;
import java.util.function.Consumer;
/**
* Implementation of the clipboard for copy and paste.
* <p>
* Caution: exception for clipboard data and isInternalSysWindowAppWithWindowFocus, any of data
* is accessed by userId or uid should be in * the try segment between
* Binder.clearCallingIdentity and Binder.restoreCallingIdentity.
* </p>
*/
public class ClipboardService extends SystemService {
private static final String TAG = "ClipboardService";
private static final boolean IS_EMULATOR =
SystemProperties.getBoolean("ro.boot.qemu", false);
// DeviceConfig properties
private static final String PROPERTY_MAX_CLASSIFICATION_LENGTH = "max_classification_length";
private static final int DEFAULT_MAX_CLASSIFICATION_LENGTH = 400;
private final ActivityManagerInternal mAmInternal;
private final IUriGrantsManager mUgm;
private final UriGrantsManagerInternal mUgmInternal;
private final WindowManagerInternal mWm;
private final IUserManager mUm;
private final PackageManager mPm;
private final AppOpsManager mAppOps;
private final ContentCaptureManagerInternal mContentCaptureInternal;
private final AutofillManagerInternal mAutofillInternal;
private final IBinder mPermissionOwner;
private final Consumer<ClipData> mEmulatorClipboardMonitor;
private final Handler mWorkerHandler;
@GuardedBy("mLock")
private final SparseArray<PerUserClipboard> mClipboards = new SparseArray<>();
@GuardedBy("mLock")
private boolean mShowAccessNotifications =
ClipboardManager.DEVICE_CONFIG_DEFAULT_SHOW_ACCESS_NOTIFICATIONS;
@GuardedBy("mLock")
private int mMaxClassificationLength = DEFAULT_MAX_CLASSIFICATION_LENGTH;
private final Object mLock = new Object();
/**
* Instantiates the clipboard.
*/
public ClipboardService(Context context) {
super(context);
mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
mUgm = UriGrantsManager.getService();
mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class);
mWm = LocalServices.getService(WindowManagerInternal.class);
mPm = getContext().getPackageManager();
mUm = (IUserManager) ServiceManager.getService(Context.USER_SERVICE);
mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
mContentCaptureInternal = LocalServices.getService(ContentCaptureManagerInternal.class);
mAutofillInternal = LocalServices.getService(AutofillManagerInternal.class);
final IBinder permOwner = mUgmInternal.newUriPermissionOwner("clipboard");
mPermissionOwner = permOwner;
if (IS_EMULATOR) {
mEmulatorClipboardMonitor = new EmulatorClipboardMonitor((clip) -> {
synchronized (mLock) {
setPrimaryClipInternalLocked(getClipboardLocked(0), clip,
android.os.Process.SYSTEM_UID, null);
}
});
} else {
mEmulatorClipboardMonitor = (clip) -> {};
}
updateConfig();
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_CLIPBOARD,
getContext().getMainExecutor(), properties -> updateConfig());
HandlerThread workerThread = new HandlerThread(TAG);
workerThread.start();
mWorkerHandler = workerThread.getThreadHandler();
}
@Override
public void onStart() {
publishBinderService(Context.CLIPBOARD_SERVICE, new ClipboardImpl());
}
@Override
public void onUserStopped(@NonNull TargetUser user) {
synchronized (mLock) {
mClipboards.remove(user.getUserIdentifier());
}
}
private void updateConfig() {
synchronized (mLock) {
mShowAccessNotifications = DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_CLIPBOARD,
ClipboardManager.DEVICE_CONFIG_SHOW_ACCESS_NOTIFICATIONS,
ClipboardManager.DEVICE_CONFIG_DEFAULT_SHOW_ACCESS_NOTIFICATIONS);
mMaxClassificationLength = DeviceConfig.getInt(DeviceConfig.NAMESPACE_CLIPBOARD,
PROPERTY_MAX_CLASSIFICATION_LENGTH, DEFAULT_MAX_CLASSIFICATION_LENGTH);
}
}
private class ListenerInfo {
final int mUid;
final String mPackageName;
ListenerInfo(int uid, String packageName) {
mUid = uid;
mPackageName = packageName;
}
}
private class PerUserClipboard {
final int userId;
final RemoteCallbackList<IOnPrimaryClipChangedListener> primaryClipListeners
= new RemoteCallbackList<IOnPrimaryClipChangedListener>();
/** Current primary clip. */
ClipData primaryClip;
/** UID that set {@link #primaryClip}. */
int primaryClipUid = android.os.Process.NOBODY_UID;
/** Package of the app that set {@link #primaryClip}. */
String mPrimaryClipPackage;
/** Uids that have already triggered a toast notification for {@link #primaryClip} */
final SparseBooleanArray mNotifiedUids = new SparseBooleanArray();
/**
* Uids that have already triggered a notification to text classifier for
* {@link #primaryClip}.
*/
final SparseBooleanArray mNotifiedTextClassifierUids = new SparseBooleanArray();
final HashSet<String> activePermissionOwners
= new HashSet<String>();
/** The text classifier session that is used to annotate the text in the primary clip. */
TextClassifier mTextClassifier;
PerUserClipboard(int userId) {
this.userId = userId;
}
}
/**
* To check if the application has granted the INTERNAL_SYSTEM_WINDOW permission and window
* focus.
* <p>
* All of applications granted INTERNAL_SYSTEM_WINDOW has the risk to leak clip information to
* the other user because INTERNAL_SYSTEM_WINDOW is signature level. i.e. platform key. Because
* some of applications have both of INTERNAL_SYSTEM_WINDOW and INTERACT_ACROSS_USERS_FULL at
* the same time, that means they show the same window to all of users.
* </p><p>
* Unfortunately, all of applications with INTERNAL_SYSTEM_WINDOW starts very early and then
* the real window show is belong to user 0 rather user X. The result of
* WindowManager.isUidFocused checking user X window is false.
* </p>
* @return true if the app granted INTERNAL_SYSTEM_WINDOW permission.
*/
private boolean isInternalSysWindowAppWithWindowFocus(String callingPackage) {
// Shell can access the clipboard for testing purposes.
if (mPm.checkPermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW,
callingPackage) == PackageManager.PERMISSION_GRANTED) {
if (mWm.isUidFocused(Binder.getCallingUid())) {
return true;
}
}
return false;
}
/**
* To get the validate current userId.
* <p>
* The intending userId needs to be validated by ActivityManagerInternal.handleIncomingUser.
* To check if the uid of the process have the permission to run as the userId.
* e.x. INTERACT_ACROSS_USERS_FULL or INTERACT_ACROSS_USERS permission granted.
* </p>
* <p>
* The application with the granted INTERNAL_SYSTEM_WINDOW permission should run as the output
* of ActivityManagerInternal.handleIncomingUser rather the userId of Binder.getCAllingUid().
* To use the userId of Binder.getCallingUid() is the root cause that leaks the information
* comes from user 0 to user X.
* </p>
*
* @param packageName the package name of the calling side
* @param userId the userId passed by the calling side
* @return return the intending userId that has been validated by ActivityManagerInternal.
*/
@UserIdInt
private int getIntendingUserId(String packageName, @UserIdInt int userId) {
final int callingUid = Binder.getCallingUid();
final int callingUserId = UserHandle.getUserId(callingUid);
if (!UserManager.supportsMultipleUsers() || callingUserId == userId) {
return callingUserId;
}
int intendingUserId = callingUserId;
intendingUserId = mAmInternal.handleIncomingUser(Binder.getCallingPid(),
Binder.getCallingUid(), userId, false /* allow all */, ALLOW_FULL_ONLY,
"checkClipboardServiceCallingUser", packageName);
return intendingUserId;
}
/**
* To get the current running uid who is intend to run as.
* In ording to distinguish the nameing and reducing the confusing names, the client client
* side pass userId that is intend to run as,
* @return return IntentingUid = validated intenting userId +
* UserHandle.getAppId(Binder.getCallingUid())
*/
private int getIntendingUid(String packageName, @UserIdInt int userId) {
return UserHandle.getUid(getIntendingUserId(packageName, userId),
UserHandle.getAppId(Binder.getCallingUid()));
}
/**
* To handle the difference between userId and intendingUserId, uid and intendingUid.
*
* userId means that comes from the calling side and should be validated by
* ActivityManagerInternal.handleIncomingUser.
* After validation of ActivityManagerInternal.handleIncomingUser, the userId is called
* 'intendingUserId' and the uid is called 'intendingUid'.
*/
private class ClipboardImpl extends IClipboard.Stub {
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
try {
return super.onTransact(code, data, reply, flags);
} catch (RuntimeException e) {
if (!(e instanceof SecurityException)) {
Slog.wtf("clipboard", "Exception: ", e);
}
throw e;
}
}
@Override
public void setPrimaryClip(ClipData clip, String callingPackage, @UserIdInt int userId) {
checkAndSetPrimaryClip(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");
checkAndSetPrimaryClip(clip, callingPackage, userId, sourcePackage);
}
private void checkAndSetPrimaryClip(
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;
}
checkDataOwner(clip, intendingUid);
synchronized (mLock) {
setPrimaryClipInternalLocked(clip, intendingUid, sourcePackage);
}
}
@Override
public void clearPrimaryClip(String callingPackage, @UserIdInt int userId) {
final int intendingUid = getIntendingUid(callingPackage, userId);
final int intendingUserId = UserHandle.getUserId(intendingUid);
if (!clipboardAccessAllowed(AppOpsManager.OP_WRITE_CLIPBOARD, callingPackage,
intendingUid, intendingUserId)) {
return;
}
synchronized (mLock) {
setPrimaryClipInternalLocked(null, intendingUid, callingPackage);
}
}
@Override
public ClipData getPrimaryClip(String pkg, @UserIdInt int userId) {
final int intendingUid = getIntendingUid(pkg, userId);
final int intendingUserId = UserHandle.getUserId(intendingUid);
if (!clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, pkg,
intendingUid, intendingUserId)
|| isDeviceLocked(intendingUserId)) {
return null;
}
synchronized (mLock) {
try {
addActiveOwnerLocked(intendingUid, 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);
return null;
}
PerUserClipboard clipboard = getClipboardLocked(intendingUserId);
showAccessNotificationLocked(pkg, intendingUid, intendingUserId, clipboard);
notifyTextClassifierLocked(clipboard, pkg, intendingUid);
return clipboard.primaryClip;
}
}
@Override
public ClipDescription getPrimaryClipDescription(String callingPackage,
@UserIdInt int userId) {
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;
}
synchronized (mLock) {
PerUserClipboard clipboard = getClipboardLocked(intendingUserId);
return clipboard.primaryClip != null
? clipboard.primaryClip.getDescription() : null;
}
}
@Override
public boolean hasPrimaryClip(String callingPackage, @UserIdInt int userId) {
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 false;
}
synchronized (mLock) {
return getClipboardLocked(intendingUserId).primaryClip != null;
}
}
@Override
public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener,
String callingPackage, @UserIdInt int userId) {
final int intendingUid = getIntendingUid(callingPackage, userId);
final int intendingUserId = UserHandle.getUserId(intendingUid);
synchronized (mLock) {
getClipboardLocked(intendingUserId).primaryClipListeners.register(listener,
new ListenerInfo(intendingUid, callingPackage));
}
}
@Override
public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener listener,
String callingPackage, @UserIdInt int userId) {
final int intendingUserId = getIntendingUserId(callingPackage, userId);
synchronized (mLock) {
getClipboardLocked(intendingUserId).primaryClipListeners.unregister(listener);
}
}
@Override
public boolean hasClipboardText(String callingPackage, int userId) {
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 false;
}
synchronized (mLock) {
PerUserClipboard clipboard = getClipboardLocked(intendingUserId);
if (clipboard.primaryClip != null) {
CharSequence text = clipboard.primaryClip.getItemAt(0).getText();
return text != null && text.length() > 0;
}
return false;
}
}
@Override
public String getPrimaryClipSource(String callingPackage, int userId) {
getContext().enforceCallingOrSelfPermission(Manifest.permission.SET_CLIP_SOURCE,
"Requires SET_CLIP_SOURCE permission");
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;
}
synchronized (mLock) {
PerUserClipboard clipboard = getClipboardLocked(intendingUserId);
if (clipboard.primaryClip != null) {
return clipboard.mPrimaryClipPackage;
}
return null;
}
}
};
@GuardedBy("mLock")
private PerUserClipboard getClipboardLocked(@UserIdInt int userId) {
PerUserClipboard puc = mClipboards.get(userId);
if (puc == null) {
puc = new PerUserClipboard(userId);
mClipboards.put(userId, puc);
}
return puc;
}
List<UserInfo> getRelatedProfiles(@UserIdInt int userId) {
final List<UserInfo> related;
final long origId = Binder.clearCallingIdentity();
try {
related = mUm.getProfiles(userId, true);
} catch (RemoteException e) {
Slog.e(TAG, "Remote Exception calling UserManager: " + e);
return null;
} finally{
Binder.restoreCallingIdentity(origId);
}
return related;
}
/** Check if the user has the given restriction set. Default to true if error occured during
* calling UserManager, so it fails safe.
*/
private boolean hasRestriction(String restriction, int userId) {
try {
return mUm.hasUserRestriction(restriction, userId);
} catch (RemoteException e) {
Slog.e(TAG, "Remote Exception calling UserManager.getUserRestrictions: ", e);
// Fails safe
return true;
}
}
void setPrimaryClipInternal(@Nullable ClipData clip, int uid) {
synchronized (mLock) {
setPrimaryClipInternalLocked(clip, uid, null);
}
}
@GuardedBy("mLock")
private void setPrimaryClipInternalLocked(
@Nullable ClipData clip, int uid, @Nullable String sourcePackage) {
mEmulatorClipboardMonitor.accept(clip);
final int userId = UserHandle.getUserId(uid);
if (clip != null) {
startClassificationLocked(clip, userId);
}
// Update this user
setPrimaryClipInternalLocked(getClipboardLocked(userId), clip, uid, sourcePackage);
// Update related users
List<UserInfo> related = getRelatedProfiles(userId);
if (related != null) {
int size = related.size();
if (size > 1) { // Related profiles list include the current profile.
final boolean canCopy = !hasRestriction(
UserManager.DISALLOW_CROSS_PROFILE_COPY_PASTE, userId);
// Copy clip data to related users if allowed. If disallowed, then remove
// primary clip in related users to prevent pasting stale content.
if (!canCopy) {
clip = null;
} else if (clip == null) {
// do nothing for canCopy == true and clip == null case
// To prevent from NPE happen in 'new ClipData(clip)' when run
// android.content.cts.ClipboardManagerTest#testClearPrimaryClip
} else {
// We want to fix the uris of the related user's clip without changing the
// uris of the current user's clip.
// So, copy the ClipData, and then copy all the items, so that nothing
// is shared in memory.
clip = new ClipData(clip);
for (int i = clip.getItemCount() - 1; i >= 0; i--) {
clip.setItemAt(i, new ClipData.Item(clip.getItemAt(i)));
}
clip.fixUrisLight(userId);
}
for (int i = 0; i < size; i++) {
int id = related.get(i).id;
if (id != userId) {
final boolean canCopyIntoProfile = !hasRestriction(
UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE, id);
if (canCopyIntoProfile) {
setPrimaryClipInternalLocked(
getClipboardLocked(id), clip, uid, sourcePackage);
}
}
}
}
}
}
void setPrimaryClipInternal(PerUserClipboard clipboard, @Nullable ClipData clip,
int uid) {
synchronized ("mLock") {
setPrimaryClipInternalLocked(clipboard, clip, uid, null);
}
}
@GuardedBy("mLock")
private void setPrimaryClipInternalLocked(PerUserClipboard clipboard, @Nullable ClipData clip,
int uid, @Nullable String sourcePackage) {
revokeUris(clipboard);
clipboard.activePermissionOwners.clear();
if (clip == null && clipboard.primaryClip == null) {
return;
}
clipboard.primaryClip = clip;
clipboard.mNotifiedUids.clear();
clipboard.mNotifiedTextClassifierUids.clear();
if (clip != null) {
clipboard.primaryClipUid = uid;
clipboard.mPrimaryClipPackage = sourcePackage;
} else {
clipboard.primaryClipUid = android.os.Process.NOBODY_UID;
clipboard.mPrimaryClipPackage = null;
}
if (clip != null) {
final ClipDescription description = clip.getDescription();
if (description != null) {
description.setTimestamp(System.currentTimeMillis());
}
}
sendClipChangedBroadcast(clipboard);
}
private void sendClipChangedBroadcast(PerUserClipboard clipboard) {
final long ident = Binder.clearCallingIdentity();
final int n = clipboard.primaryClipListeners.beginBroadcast();
try {
for (int i = 0; i < n; i++) {
try {
ListenerInfo li = (ListenerInfo)
clipboard.primaryClipListeners.getBroadcastCookie(i);
if (clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, li.mPackageName,
li.mUid, UserHandle.getUserId(li.mUid))) {
clipboard.primaryClipListeners.getBroadcastItem(i)
.dispatchPrimaryClipChanged();
}
} catch (RemoteException e) {
// The RemoteCallbackList will take care of removing
// the dead object for us.
}
}
} finally {
clipboard.primaryClipListeners.finishBroadcast();
Binder.restoreCallingIdentity(ident);
}
}
@GuardedBy("mLock")
private void startClassificationLocked(@NonNull ClipData clip, @UserIdInt int userId) {
CharSequence text = (clip.getItemCount() == 0) ? null : clip.getItemAt(0).getText();
if (TextUtils.isEmpty(text) || text.length() > mMaxClassificationLength) {
clip.getDescription().setClassificationStatus(
ClipDescription.CLASSIFICATION_NOT_PERFORMED);
return;
}
TextClassifier classifier;
final long ident = Binder.clearCallingIdentity();
try {
classifier = createTextClassificationManagerAsUser(userId)
.createTextClassificationSession(
new TextClassificationContext.Builder(
getContext().getPackageName(),
TextClassifier.WIDGET_TYPE_CLIPBOARD
).build()
);
} finally {
Binder.restoreCallingIdentity(ident);
}
if (text.length() > classifier.getMaxGenerateLinksTextLength()) {
clip.getDescription().setClassificationStatus(
ClipDescription.CLASSIFICATION_NOT_PERFORMED);
return;
}
mWorkerHandler.post(() -> doClassification(text, clip, classifier, userId));
}
@WorkerThread
private void doClassification(
CharSequence text, ClipData clip, TextClassifier classifier, @UserIdInt int userId) {
TextLinks.Request request = new TextLinks.Request.Builder(text).build();
TextLinks links = classifier.generateLinks(request);
// Find the highest confidence for each entity in the text.
ArrayMap<String, Float> confidences = new ArrayMap<>();
for (TextLinks.TextLink link : links.getLinks()) {
for (int i = 0; i < link.getEntityCount(); i++) {
String entity = link.getEntity(i);
float conf = link.getConfidenceScore(entity);
if (conf > confidences.getOrDefault(entity, 0f)) {
confidences.put(entity, conf);
}
}
}
synchronized (mLock) {
PerUserClipboard clipboard = getClipboardLocked(userId);
if (clipboard.primaryClip == clip) {
applyClassificationAndSendBroadcastLocked(
clipboard, confidences, links, classifier);
// Also apply to related profiles if needed
List<UserInfo> related = getRelatedProfiles(userId);
if (related != null) {
int size = related.size();
for (int i = 0; i < size; i++) {
int id = related.get(i).id;
if (id != userId) {
final boolean canCopyIntoProfile = !hasRestriction(
UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE, id);
if (canCopyIntoProfile) {
PerUserClipboard relatedClipboard = getClipboardLocked(id);
if (hasTextLocked(relatedClipboard, text)) {
applyClassificationAndSendBroadcastLocked(
relatedClipboard, confidences, links, classifier);
}
}
}
}
}
}
}
}
@GuardedBy("mLock")
private void applyClassificationAndSendBroadcastLocked(
PerUserClipboard clipboard, ArrayMap<String, Float> confidences, TextLinks links,
TextClassifier classifier) {
clipboard.mTextClassifier = classifier;
clipboard.primaryClip.getDescription().setConfidenceScores(confidences);
if (!links.getLinks().isEmpty()) {
clipboard.primaryClip.getItemAt(0).setTextLinks(links);
}
sendClipChangedBroadcast(clipboard);
}
@GuardedBy("mLock")
private boolean hasTextLocked(PerUserClipboard clipboard, @NonNull CharSequence text) {
return clipboard.primaryClip != null
&& clipboard.primaryClip.getItemCount() > 0
&& text.equals(clipboard.primaryClip.getItemAt(0).getText());
}
private boolean isDeviceLocked(@UserIdInt int userId) {
final long token = Binder.clearCallingIdentity();
try {
final KeyguardManager keyguardManager = getContext().getSystemService(
KeyguardManager.class);
return keyguardManager != null && keyguardManager.isDeviceLocked(userId);
} finally {
Binder.restoreCallingIdentity(token);
}
}
private void checkUriOwner(Uri uri, int sourceUid) {
if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
final long ident = Binder.clearCallingIdentity();
try {
// This will throw SecurityException if caller can't grant
mUgmInternal.checkGrantUriPermission(sourceUid, null,
ContentProvider.getUriWithoutUserId(uri),
Intent.FLAG_GRANT_READ_URI_PERMISSION,
ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid)));
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private void checkItemOwner(ClipData.Item item, int uid) {
if (item.getUri() != null) {
checkUriOwner(item.getUri(), uid);
}
Intent intent = item.getIntent();
if (intent != null && intent.getData() != null) {
checkUriOwner(intent.getData(), uid);
}
}
private void checkDataOwner(ClipData data, int uid) {
final int N = data.getItemCount();
for (int i=0; i<N; i++) {
checkItemOwner(data.getItemAt(i), uid);
}
}
private void grantUriPermission(Uri uri, int sourceUid, String targetPkg,
int targetUserId) {
if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
final long ident = Binder.clearCallingIdentity();
try {
mUgm.grantUriPermissionFromOwner(mPermissionOwner, sourceUid, targetPkg,
ContentProvider.getUriWithoutUserId(uri),
Intent.FLAG_GRANT_READ_URI_PERMISSION,
ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid)),
targetUserId);
} catch (RemoteException ignored) {
// Ignored because we're in same process
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private void grantItemPermission(ClipData.Item item, int sourceUid, String targetPkg,
int targetUserId) {
if (item.getUri() != null) {
grantUriPermission(item.getUri(), sourceUid, targetPkg, targetUserId);
}
Intent intent = item.getIntent();
if (intent != null && intent.getData() != null) {
grantUriPermission(intent.getData(), sourceUid, targetPkg, targetUserId);
}
}
@GuardedBy("mLock")
private void addActiveOwnerLocked(int uid, String pkg) {
final IPackageManager pm = AppGlobals.getPackageManager();
final int targetUserHandle = UserHandle.getCallingUserId();
final long oldIdentity = Binder.clearCallingIdentity();
try {
PackageInfo pi = pm.getPackageInfo(pkg, 0, targetUserHandle);
if (pi == null) {
throw new IllegalArgumentException("Unknown package " + pkg);
}
if (!UserHandle.isSameApp(pi.applicationInfo.uid, uid)) {
throw new SecurityException("Calling uid " + uid
+ " does not own package " + pkg);
}
} catch (RemoteException e) {
// Can't happen; the package manager is in the same process
} finally {
Binder.restoreCallingIdentity(oldIdentity);
}
PerUserClipboard clipboard = getClipboardLocked(UserHandle.getUserId(uid));
if (clipboard.primaryClip != null && !clipboard.activePermissionOwners.contains(pkg)) {
final int N = clipboard.primaryClip.getItemCount();
for (int i=0; i<N; i++) {
grantItemPermission(clipboard.primaryClip.getItemAt(i), clipboard.primaryClipUid,
pkg, UserHandle.getUserId(uid));
}
clipboard.activePermissionOwners.add(pkg);
}
}
private void revokeUriPermission(Uri uri, int sourceUid) {
if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
final long ident = Binder.clearCallingIdentity();
try {
mUgmInternal.revokeUriPermissionFromOwner(mPermissionOwner,
ContentProvider.getUriWithoutUserId(uri),
Intent.FLAG_GRANT_READ_URI_PERMISSION,
ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid)));
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private void revokeItemPermission(ClipData.Item item, int sourceUid) {
if (item.getUri() != null) {
revokeUriPermission(item.getUri(), sourceUid);
}
Intent intent = item.getIntent();
if (intent != null && intent.getData() != null) {
revokeUriPermission(intent.getData(), sourceUid);
}
}
private void revokeUris(PerUserClipboard clipboard) {
if (clipboard.primaryClip == null) {
return;
}
final int N = clipboard.primaryClip.getItemCount();
for (int i=0; i<N; i++) {
revokeItemPermission(clipboard.primaryClip.getItemAt(i), clipboard.primaryClipUid);
}
}
private boolean clipboardAccessAllowed(int op, String callingPackage, int uid,
@UserIdInt int userId) {
return clipboardAccessAllowed(op, callingPackage, uid, userId, true);
}
private boolean clipboardAccessAllowed(int op, String callingPackage, int uid,
@UserIdInt int userId, boolean shouldNoteOp) {
boolean allowed;
// First, verify package ownership to ensure use below is safe.
mAppOps.checkPackage(uid, callingPackage);
// Shell can access the clipboard for testing purposes.
if (mPm.checkPermission(android.Manifest.permission.READ_CLIPBOARD_IN_BACKGROUND,
callingPackage) == PackageManager.PERMISSION_GRANTED) {
allowed = true;
} else {
// The default IME is always allowed to access the clipboard.
allowed = isDefaultIme(userId, callingPackage);
}
switch (op) {
case AppOpsManager.OP_READ_CLIPBOARD:
// Clipboard can only be read by applications with focus..
// or the application have the INTERNAL_SYSTEM_WINDOW and INTERACT_ACROSS_USERS_FULL
// at the same time. e.x. SystemUI. It needs to check the window focus of
// Binder.getCallingUid(). Without checking, the user X can't copy any thing from
// INTERNAL_SYSTEM_WINDOW to the other applications.
if (!allowed) {
allowed = mWm.isUidFocused(uid)
|| isInternalSysWindowAppWithWindowFocus(callingPackage);
}
if (!allowed && mContentCaptureInternal != null) {
// ...or the Content Capture Service
// The uid parameter of mContentCaptureInternal.isContentCaptureServiceForUser
// is used to check if the uid has the permission BIND_CONTENT_CAPTURE_SERVICE.
// if the application has the permission, let it to access user's clipboard.
// To passed synthesized uid user#10_app#systemui may not tell the real uid.
// userId must pass intending userId. i.e. user#10.
allowed = mContentCaptureInternal.isContentCaptureServiceForUser(uid, userId);
}
if (!allowed && mAutofillInternal != null) {
// ...or the Augmented Autofill Service
// The uid parameter of mAutofillInternal.isAugmentedAutofillServiceForUser
// is used to check if the uid has the permission BIND_AUTOFILL_SERVICE.
// if the application has the permission, let it to access user's clipboard.
// To passed synthesized uid user#10_app#systemui may not tell the real uid.
// userId must pass intending userId. i.e. user#10.
allowed = mAutofillInternal.isAugmentedAutofillServiceForUser(uid, userId);
}
break;
case AppOpsManager.OP_WRITE_CLIPBOARD:
// Writing is allowed without focus.
allowed = true;
break;
default:
throw new IllegalArgumentException("Unknown clipboard appop " + op);
}
if (!allowed) {
Slog.e(TAG, "Denying clipboard access to " + callingPackage
+ ", application is not in focus nor is it a system service for "
+ "user " + userId);
return false;
}
// Finally, check the app op.
int appOpsResult;
if (shouldNoteOp) {
appOpsResult = mAppOps.noteOp(op, uid, callingPackage);
} else {
appOpsResult = mAppOps.checkOp(op, uid, callingPackage);
}
return appOpsResult == AppOpsManager.MODE_ALLOWED;
}
private boolean isDefaultIme(int userId, String packageName) {
String defaultIme = Settings.Secure.getStringForUser(getContext().getContentResolver(),
Settings.Secure.DEFAULT_INPUT_METHOD, userId);
if (!TextUtils.isEmpty(defaultIme)) {
final String imePkg = ComponentName.unflattenFromString(defaultIme).getPackageName();
return imePkg.equals(packageName);
}
return false;
}
/**
* Shows a toast to inform the user that an app has accessed the clipboard. This is only done if
* the setting is enabled, and if the accessing app is not the source of the data and is not the
* IME, the content capture service, or the autofill service. The notification is also only
* shown once per clip for each app.
*/
@GuardedBy("mLock")
private void showAccessNotificationLocked(String callingPackage, int uid, @UserIdInt int userId,
PerUserClipboard clipboard) {
if (clipboard.primaryClip == null) {
return;
}
if (Settings.Secure.getInt(getContext().getContentResolver(),
Settings.Secure.CLIPBOARD_SHOW_ACCESS_NOTIFICATIONS,
(mShowAccessNotifications ? 1 : 0)) == 0) {
return;
}
// Don't notify if the app accessing the clipboard is the same as the current owner.
if (UserHandle.isSameApp(uid, clipboard.primaryClipUid)) {
return;
}
// Exclude special cases: IME, ContentCapture, Autofill.
if (isDefaultIme(userId, callingPackage)) {
return;
}
if (mContentCaptureInternal != null
&& mContentCaptureInternal.isContentCaptureServiceForUser(uid, userId)) {
return;
}
if (mAutofillInternal != null
&& mAutofillInternal.isAugmentedAutofillServiceForUser(uid, userId)) {
return;
}
// Don't notify if already notified for this uid and clip.
if (clipboard.mNotifiedUids.get(uid)) {
return;
}
clipboard.mNotifiedUids.put(uid, true);
Binder.withCleanCallingIdentity(() -> {
try {
CharSequence callingAppLabel = mPm.getApplicationLabel(
mPm.getApplicationInfoAsUser(callingPackage, 0, userId));
String message =
getContext().getString(R.string.pasted_from_clipboard, callingAppLabel);
Slog.i(TAG, message);
Toast.makeText(
getContext(), UiThread.get().getLooper(), message, Toast.LENGTH_SHORT)
.show();
} catch (PackageManager.NameNotFoundException e) {
// do nothing
}
});
}
/**
* Returns true if the provided {@link ClipData} represents a single piece of text. That is, if
* there is only on {@link ClipData.Item}, and that item contains a non-empty piece of text and
* no URI or Intent. Note that HTML may be provided along with text so the presence of
* HtmlText in the clip does not prevent this method returning true.
*/
private static boolean isText(@NonNull ClipData data) {
if (data.getItemCount() > 1) {
return false;
}
ClipData.Item item = data.getItemAt(0);
return !TextUtils.isEmpty(item.getText()) && item.getUri() == null
&& item.getIntent() == null;
}
/** Potentially notifies the text classifier that an app is accessing a text clip. */
@GuardedBy("mLock")
private void notifyTextClassifierLocked(
PerUserClipboard clipboard, String callingPackage, int callingUid) {
if (clipboard.primaryClip == null) {
return;
}
ClipData.Item item = clipboard.primaryClip.getItemAt(0);
if (item == null) {
return;
}
if (!isText(clipboard.primaryClip)) {
return;
}
TextClassifier textClassifier = clipboard.mTextClassifier;
// Don't notify text classifier if we haven't used it to annotate the text in the clip.
if (textClassifier == null) {
return;
}
// Don't notify text classifier if the app reading the clipboard does not have the focus.
if (!mWm.isUidFocused(callingUid)) {
return;
}
// Don't notify text classifier again if already notified for this uid and clip.
if (clipboard.mNotifiedTextClassifierUids.get(callingUid)) {
return;
}
clipboard.mNotifiedTextClassifierUids.put(callingUid, true);
Binder.withCleanCallingIdentity(() -> {
TextClassifierEvent.TextLinkifyEvent pasteEvent =
new TextClassifierEvent.TextLinkifyEvent.Builder(
TextClassifierEvent.TYPE_READ_CLIPBOARD)
.setEventContext(new TextClassificationContext.Builder(
callingPackage, TextClassifier.WIDGET_TYPE_CLIPBOARD)
.build())
.setExtras(
Bundle.forPair("source_package", clipboard.mPrimaryClipPackage))
.build();
textClassifier.onTextClassifierEvent(pasteEvent);
});
}
private TextClassificationManager createTextClassificationManagerAsUser(@UserIdInt int userId) {
Context context = getContext().createContextAsUser(UserHandle.of(userId), /* flags= */ 0);
return context.getSystemService(TextClassificationManager.class);
}
}