diff --git a/core/java/android/ext/SettingsIntents.java b/core/java/android/ext/SettingsIntents.java
index ab42fdfa0038..5f62b664f442 100644
--- a/core/java/android/ext/SettingsIntents.java
+++ b/core/java/android/ext/SettingsIntents.java
@@ -9,6 +9,7 @@ public class SettingsIntents {
public static final String APP_NATIVE_DEBUGGING = "android.settings.OPEN_APP_NATIVE_DEBUGGING_SETTINGS";
public static final String APP_MEMTAG = "android.settings.OPEN_APP_MEMTAG_SETTINGS";
public static final String APP_HARDENED_MALLOC = "android.settings.OPEN_APP_HARDENED_MALLOC_SETTINGS";
+ public static final String APP_CLIPBOARD = "android.settings.OPEN_APP_CLIPBOARD_SETTINGS";
public static Intent getAppIntent(String action, String pkgName) {
var i = new Intent(action);
diff --git a/core/java/android/ext/settings/ExtSettings.java b/core/java/android/ext/settings/ExtSettings.java
index 4e00270fb2f7..92a3536d1d10 100644
--- a/core/java/android/ext/settings/ExtSettings.java
+++ b/core/java/android/ext/settings/ExtSettings.java
@@ -137,6 +137,10 @@ public boolean validateValue(String val) {
public static final BoolSysProperty ALLOW_NATIVE_DEBUG_BY_DEFAULT = new BoolSysProperty(
"persist.native_debug", defaultBool(R.bool.setting_default_allow_native_debugging));
+ public static final BoolSetting DENY_CLIPBOARD_READ_BY_DEFAULT = new BoolSetting(
+ Setting.Scope.PER_USER, "deny_clipboard_read",
+ defaultBool(R.bool.setting_default_deny_clipboard_read));
+
// AppCompatConfig specifies which hardening features are compatible/incompatible with a
// specific app.
// This setting controls whether incompatible hardening features would be disabled by default
diff --git a/core/java/android/ext/settings/app/AswDenyClipboardRead.java b/core/java/android/ext/settings/app/AswDenyClipboardRead.java
new file mode 100644
index 000000000000..6807047a5a97
--- /dev/null
+++ b/core/java/android/ext/settings/app/AswDenyClipboardRead.java
@@ -0,0 +1,37 @@
+package android.ext.settings.app;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.GosPackageState;
+import android.content.pm.GosPackageStateBase;
+import android.ext.settings.ExtSettings;
+
+/** @hide */
+public class AswDenyClipboardRead extends AppSwitch {
+ public static final AswDenyClipboardRead I = new AswDenyClipboardRead();
+
+ private AswDenyClipboardRead() {
+ gosPsFlag = GosPackageState.FLAG_DENY_CLIPBOARD_READ;
+ gosPsFlagNonDefault = GosPackageState.FLAG_DENY_CLIPBOARD_READ_NON_DEFAULT;
+ gosPsFlagSuppressNotif = GosPackageState.FLAG_DENY_CLIPBOARD_READ_SUPPRESS_NOTIF;
+ }
+
+ @Override
+ public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo,
+ @Nullable GosPackageStateBase ps, StateInfo si) {
+ if (appInfo.isSystemApp()) {
+ si.immutabilityReason = IR_IS_SYSTEM_APP;
+ return false;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo,
+ @Nullable GosPackageStateBase ps, StateInfo si) {
+ si.defaultValueReason = DVR_DEFAULT_SETTING;
+ return ExtSettings.DENY_CLIPBOARD_READ_BY_DEFAULT.get(ctx, userId);
+ }
+}
diff --git a/core/res/res/values/config_ext.xml b/core/res/res/values/config_ext.xml
index 104593554d27..78eced9f5c81 100644
--- a/core/res/res/values/config_ext.xml
+++ b/core/res/res/values/config_ext.xml
@@ -38,4 +38,7 @@
app.grapheneos.apps
+
+ false
+
diff --git a/core/res/res/values/string_ext.xml b/core/res/res/values/string_ext.xml
index 19a4e2aa1a2d..391850465360 100644
--- a/core/res/res/values/string_ext.xml
+++ b/core/res/res/values/string_ext.xml
@@ -46,4 +46,7 @@
hardened_malloc detected an error in %1$s
+ %1$s tried to read clipboard
+
+
diff --git a/services/core/java/com/android/server/clipboard/ClipboardAccessHelper.java b/services/core/java/com/android/server/clipboard/ClipboardAccessHelper.java
new file mode 100644
index 000000000000..5b0f888ca755
--- /dev/null
+++ b/services/core/java/com/android/server/clipboard/ClipboardAccessHelper.java
@@ -0,0 +1,41 @@
+package com.android.server.clipboard;
+
+import android.content.ClipData;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.GosPackageState;
+import android.content.pm.PackageManagerInternal;
+import android.ext.SettingsIntents;
+import android.ext.settings.app.AswDenyClipboardRead;
+import android.os.Process;
+
+import com.android.internal.R;
+import com.android.server.LocalServices;
+import com.android.server.ext.AppSwitchNotification;
+import com.android.server.pm.pkg.GosPackageStatePm;
+
+public class ClipboardAccessHelper {
+ static final ClipData dummyClip = ClipData.newPlainText(null, "");
+
+ static boolean isReadBlockedForPackage(Context ctx, String pkgName, int userId) {
+ var pmi = LocalServices.getService(PackageManagerInternal.class);
+ ApplicationInfo appInfo = pmi.getApplicationInfo(pkgName, 0, Process.SYSTEM_UID, userId);
+ if (appInfo == null) {
+ return true;
+ }
+ GosPackageStatePm ps = pmi.getGosPackageState(pkgName, userId);
+ return AswDenyClipboardRead.I.get(ctx, userId, appInfo, ps);
+ }
+
+ static void maybeNotifyAccessDenied(Context ctx, String pkgName, int userId) {
+ var pmi = LocalServices.getService(PackageManagerInternal.class);
+ ApplicationInfo appInfo = pmi.getApplicationInfo(pkgName, 0, Process.SYSTEM_UID, userId);
+ if (appInfo == null) {
+ return;
+ }
+ var n = AppSwitchNotification.create(ctx, appInfo, SettingsIntents.APP_CLIPBOARD);
+ n.titleRes = R.string.notif_clipboard_read_deny_title;
+ n.gosPsFlagSuppressNotif = GosPackageState.FLAG_DENY_CLIPBOARD_READ_SUPPRESS_NOTIF;
+ n.maybeShow();
+ }
+}
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index 56a94ec06ad4..154a0c924ada 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -682,7 +682,12 @@ public ClipData getPrimaryClip(
if (clipboard == null) {
return null;
}
- showAccessNotificationLocked(pkg, intendingUid, intendingUserId, clipboard);
+ boolean isReadBlocked = isReadBlockedForPkg(intendingUid, pkg, userId, clipboard);
+ showAccessNotificationLocked(pkg, intendingUid, intendingUserId, clipboard,
+ isReadBlocked);
+ if (isReadBlocked) {
+ return clipboard.primaryClip != null ? ClipboardAccessHelper.dummyClip : null;
+ }
notifyTextClassifierLocked(clipboard, pkg, intendingUid);
if (clipboard.primaryClip != null) {
scheduleAutoClear(userId, intendingUid, intendingDeviceId);
@@ -710,8 +715,13 @@ public ClipDescription getPrimaryClipDescription(
}
synchronized (mLock) {
Clipboard clipboard = getClipboardLocked(intendingUserId, intendingDeviceId);
- return (clipboard != null && clipboard.primaryClip != null)
- ? clipboard.primaryClip.getDescription() : null;
+ ClipDescription clipDesc = null;
+ if (clipboard != null && clipboard.primaryClip != null) {
+ clipDesc = isReadBlockedForPkg(intendingUid, callingPackage, userId, clipboard)
+ ? ClipboardAccessHelper.dummyClip.getDescription()
+ : clipboard.primaryClip.getDescription();
+ }
+ return clipDesc;
}
}
@@ -809,7 +819,8 @@ public boolean hasClipboardText(
}
synchronized (mLock) {
Clipboard clipboard = getClipboardLocked(intendingUserId, intendingDeviceId);
- if (clipboard != null && clipboard.primaryClip != null) {
+ if (clipboard != null && clipboard.primaryClip != null
+ && !isReadBlockedForPkg(intendingUid, callingPackage, userId, clipboard)) {
CharSequence text = clipboard.primaryClip.getItemAt(0).getText();
return text != null && text.length() > 0;
}
@@ -838,7 +849,8 @@ public String getPrimaryClipSource(
}
synchronized (mLock) {
Clipboard clipboard = getClipboardLocked(intendingUserId, intendingDeviceId);
- if (clipboard != null && clipboard.primaryClip != null) {
+ if (clipboard != null && clipboard.primaryClip != null
+ && !isReadBlockedForPkg(intendingUid, callingPackage, userId, clipboard)) {
return clipboard.mPrimaryClipPackage;
}
return null;
@@ -1047,6 +1059,14 @@ private void sendClipChangedBroadcast(Clipboard clipboard) {
ListenerInfo li = (ListenerInfo)
clipboard.primaryClipListeners.getBroadcastCookie(i);
+ if (isReadBlockedForPkg(
+ li.mUid,
+ li.mPackageName,
+ UserHandle.getUserId(li.mUid),
+ clipboard)) {
+ continue;
+ }
+
if (clipboardAccessAllowed(
AppOpsManager.OP_READ_CLIPBOARD,
li.mPackageName,
@@ -1438,7 +1458,7 @@ private boolean isDefaultIme(int userId, String packageName) {
*/
@GuardedBy("mLock")
private void showAccessNotificationLocked(String callingPackage, int uid, @UserIdInt int userId,
- Clipboard clipboard) {
+ Clipboard clipboard, boolean isReadBlocked) {
if (clipboard.primaryClip == null) {
return;
}
@@ -1472,6 +1492,18 @@ private void showAccessNotificationLocked(String callingPackage, int uid, @UserI
clipboard.deviceId) == uid) {
return;
}
+
+ // Shows a system notification instead, when clipboard access is blocked.
+ if (isReadBlocked) {
+ String message =
+ getContext().getString(R.string.notif_clipboard_read_deny_title, callingPackage);
+ Slog.d(TAG, message);
+ Binder.withCleanCallingIdentity(
+ () -> ClipboardAccessHelper.maybeNotifyAccessDenied(getContext(),
+ callingPackage, userId));
+ return;
+ }
+
// Don't notify if already notified for this uid and clip.
if (clipboard.mNotifiedUids.get(uid)) {
return;
@@ -1615,4 +1647,9 @@ private TextClassificationManager createTextClassificationManagerAsUser(@UserIdI
Context context = getContext().createContextAsUser(UserHandle.of(userId), /* flags= */ 0);
return context.getSystemService(TextClassificationManager.class);
}
+
+ private boolean isReadBlockedForPkg(int uid, String pkg, int userId, Clipboard clipboard) {
+ return !UserHandle.isSameApp(uid, clipboard.primaryClipUid)
+ && ClipboardAccessHelper.isReadBlockedForPackage(getContext(), pkg, userId);
+ }
}