diff --git a/api/system-current.txt b/api/system-current.txt index 01e50c00cc968..85877501cf47a 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -9203,6 +9203,7 @@ package android.view.contentcapture { public final class ContentCaptureManager { method public boolean isContentCaptureFeatureEnabled(); + method public void setContentCaptureFeatureEnabled(boolean); } public final class UserDataRemovalRequest implements android.os.Parcelable { diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index 07dedd65cf38d..fde0cedf7db76 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -15,6 +15,7 @@ */ package android.view.contentcapture; +import static android.view.contentcapture.ContentCaptureHelper.DEBUG; import static android.view.contentcapture.ContentCaptureHelper.VERBOSE; import android.annotation.NonNull; @@ -52,6 +53,13 @@ public final class ContentCaptureManager { private static final String TAG = ContentCaptureManager.class.getSimpleName(); + /** @hide */ + public static final int RESULT_CODE_TRUE = 1; + /** @hide */ + public static final int RESULT_CODE_FALSE = 2; + /** @hide */ + public static final int RESULT_CODE_NOT_SERVICE = -1; + /** * Timeout for calls to system_server. */ @@ -184,6 +192,10 @@ public final class ContentCaptureManager { * it on {@link android.app.Activity#onCreate(android.os.Bundle, android.os.PersistableBundle)}. */ public void setContentCaptureEnabled(boolean enabled) { + if (DEBUG) { + Log.d(TAG, "setContentCaptureEnabled(): setting to " + enabled + " for " + mContext); + } + synchronized (mLock) { mFlags |= enabled ? 0 : ContentCaptureContext.FLAG_DISABLED_BY_APP; } @@ -195,6 +207,9 @@ public final class ContentCaptureManager { *

This method is typically used by the Content Capture Service settings page, so it can * provide a toggle to enable / disable it. * + * @throws SecurityException if caller is not the app that owns the Content Capture service + * associated with the user. + * * @hide */ @SystemApi @@ -202,13 +217,54 @@ public final class ContentCaptureManager { if (mService == null) return false; final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); + final int resultCode; try { mService.isContentCaptureFeatureEnabled(resultReceiver); - return resultReceiver.getIntResult() == 1; + resultCode = resultReceiver.getIntResult(); } catch (RemoteException e) { - // Unable to retrieve component name in a reasonable amount of time. throw e.rethrowFromSystemServer(); } + switch (resultCode) { + case RESULT_CODE_TRUE: + return true; + case RESULT_CODE_FALSE: + return false; + case RESULT_CODE_NOT_SERVICE: + throw new SecurityException("caller is not user's ContentCapture service"); + default: + throw new IllegalStateException("received invalid result: " + resultCode); + } + } + + /** + * Sets whether Content Capture is enabled for the given user. + * + * @throws SecurityException if caller is not the app that owns the Content Capture service + * associated with the user. + * + * @hide + */ + @SystemApi + public void setContentCaptureFeatureEnabled(boolean enabled) { + if (DEBUG) Log.d(TAG, "setContentCaptureFeatureEnabled(): setting to " + enabled); + + final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); + final int resultCode; + try { + mService.setContentCaptureFeatureEnabled(enabled, resultReceiver); + resultCode = resultReceiver.getIntResult(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + switch (resultCode) { + case RESULT_CODE_TRUE: + // Our work is done here, in our void existance... + return; + case RESULT_CODE_NOT_SERVICE: + throw new SecurityException("caller is not user's ContentCapture service"); + default: + throw new IllegalStateException("received invalid result: " + resultCode); + } } /** diff --git a/core/java/android/view/contentcapture/IContentCaptureManager.aidl b/core/java/android/view/contentcapture/IContentCaptureManager.aidl index e3b0372a8cc7f..26cf34c1b88e1 100644 --- a/core/java/android/view/contentcapture/IContentCaptureManager.aidl +++ b/core/java/android/view/contentcapture/IContentCaptureManager.aidl @@ -67,4 +67,9 @@ oneway interface IContentCaptureManager { * Returns whether the content capture feature is enabled for the calling user. */ void isContentCaptureFeatureEnabled(in IResultReceiver result); + + /** + * Sets whether the content capture feature is enabled for the given user. + */ + void setContentCaptureFeatureEnabled(boolean enabled, in IResultReceiver result); } diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java index 57c9d92c631da..75ee99f8d911b 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java @@ -26,6 +26,8 @@ import android.app.ActivityManagerInternal; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.UserInfo; import android.database.ContentObserver; import android.os.Binder; @@ -40,6 +42,7 @@ import android.provider.Settings; import android.util.LocalLog; import android.util.Slog; import android.util.SparseBooleanArray; +import android.view.contentcapture.ContentCaptureManager; import android.view.contentcapture.IContentCaptureManager; import android.view.contentcapture.UserDataRemovalRequest; @@ -278,6 +281,57 @@ public final class ContentCaptureManagerService extends return mAm; } + @GuardedBy("mLock") + private boolean assertCalledByServiceLocked(@NonNull String methodName, @UserIdInt int userId, + int callingUid, @NonNull IResultReceiver result) { + final boolean isService = isCalledByServiceLocked(methodName, userId, callingUid); + if (isService) return true; + + try { + result.send(ContentCaptureManager.RESULT_CODE_NOT_SERVICE, + /* resultData= */ null); + } catch (RemoteException e) { + Slog.w(mTag, "Unable to send isContentCaptureFeatureEnabled(): " + e); + } + return false; + } + + @GuardedBy("mLock") + private boolean isCalledByServiceLocked(@NonNull String methodName, @UserIdInt int userId, + int callingUid) { + + final String serviceName = mServiceNameResolver.getServiceName(userId); + if (serviceName == null) { + Slog.e(mTag, methodName + ": called by UID " + callingUid + + ", but there's no service set for user " + userId); + return false; + } + + final ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName); + if (serviceComponent == null) { + Slog.w(mTag, methodName + ": invalid service name: " + serviceName); + return false; + } + + final String servicePackageName = serviceComponent.getPackageName(); + + final PackageManager pm = getContext().getPackageManager(); + final int serviceUid; + try { + serviceUid = pm.getPackageUidAsUser(servicePackageName, UserHandle.getCallingUserId()); + } catch (NameNotFoundException e) { + Slog.w(mTag, methodName + ": could not verify UID for " + serviceName); + return false; + } + if (callingUid != serviceUid) { + Slog.e(mTag, methodName + ": called by UID " + callingUid + ", but service UID is " + + serviceUid); + return false; + } + + return true; + } + @Override // from AbstractMasterSystemService protected void dumpLocked(String prefix, PrintWriter pw) { super.dumpLocked(prefix, pw); @@ -352,15 +406,45 @@ public final class ContentCaptureManagerService extends final int userId = UserHandle.getCallingUserId(); boolean enabled; synchronized (mLock) { + final boolean isService = assertCalledByServiceLocked( + "isContentCaptureFeatureEnabled()", userId, Binder.getCallingUid(), result); + if (!isService) return; + enabled = !isDisabledBySettingsLocked(userId); } try { - result.send(enabled ? 1 : 0, /* resultData= */null); + result.send(enabled ? ContentCaptureManager.RESULT_CODE_TRUE + : ContentCaptureManager.RESULT_CODE_FALSE, /* resultData= */null); } catch (RemoteException e) { Slog.w(mTag, "Unable to send isContentCaptureFeatureEnabled(): " + e); } } + @Override + public void setContentCaptureFeatureEnabled(boolean enabled, + @NonNull IResultReceiver result) { + final int userId = UserHandle.getCallingUserId(); + final boolean isService; + synchronized (mLock) { + isService = assertCalledByServiceLocked("setContentCaptureFeatureEnabled()", userId, + Binder.getCallingUid(), result); + } + if (!isService) return; + + final long token = Binder.clearCallingIdentity(); + try { + Settings.Secure.putStringForUser(getContext().getContentResolver(), + Settings.Secure.CONTENT_CAPTURE_ENABLED, Boolean.toString(enabled), userId); + } finally { + Binder.restoreCallingIdentity(token); + } + try { + result.send(ContentCaptureManager.RESULT_CODE_TRUE, /* resultData= */null); + } catch (RemoteException e) { + Slog.w(mTag, "Unable to send setContentCaptureFeatureEnabled(): " + e); + } + } + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(getContext(), mTag, pw)) return;