diff options
6 files changed, 348 insertions, 6 deletions
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index a5a2221e5532..ada8b000a26b 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -39,6 +39,7 @@ <permission name="android.permission.MODIFY_PHONE_STATE"/> <permission name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> <permission name="android.permission.OBSERVE_NETWORK_POLICY"/> + <permission name="android.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS" /> <permission name="android.permission.OVERRIDE_WIFI_CONFIG"/> <permission name="android.permission.PACKAGE_USAGE_STATS" /> <permission name="android.permission.READ_DREAM_STATE"/> diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 0a02848184cd..667433c46e50 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -239,6 +239,7 @@ <!-- Listen app op changes --> <uses-permission android:name="android.permission.WATCH_APPOPS" /> + <uses-permission android:name="android.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS" /> <!-- to read and change hvac values in a car --> <uses-permission android:name="android.car.permission.CONTROL_CAR_CLIMATE" /> diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java index fc7cc7ee55d3..ce8e285ff52f 100644 --- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java @@ -18,6 +18,7 @@ package com.android.systemui.appops; import android.app.AppOpsManager; import android.content.Context; +import android.content.pm.PackageManager; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; @@ -25,6 +26,8 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; +import androidx.annotation.WorkerThread; + import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.Dumpable; @@ -62,6 +65,7 @@ public class AppOpsControllerImpl implements AppOpsController, private H mBGHandler; private final List<AppOpsController.Callback> mCallbacks = new ArrayList<>(); private final ArrayMap<Integer, Set<Callback>> mCallbacksByCode = new ArrayMap<>(); + private final PermissionFlagsCache mFlagsCache; private boolean mListening; @GuardedBy("mActiveItems") @@ -81,8 +85,11 @@ public class AppOpsControllerImpl implements AppOpsController, public AppOpsControllerImpl( Context context, @Background Looper bgLooper, - DumpManager dumpManager) { + DumpManager dumpManager, + PermissionFlagsCache cache + ) { mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + mFlagsCache = cache; mBGHandler = new H(bgLooper); final int numOps = OPS.length; for (int i = 0; i < numOps; i++) { @@ -229,10 +236,66 @@ public class AppOpsControllerImpl implements AppOpsController, } /** + * Does the app-op code refer to a user sensitive permission for the specified user id + * and package. Only user sensitive permission should be shown to the user by default. + * + * @param appOpCode The code of the app-op. + * @param uid The uid of the user. + * @param packageName The name of the package. + * + * @return {@code true} iff the app-op item is user sensitive + */ + private boolean isUserSensitive(int appOpCode, int uid, String packageName) { + String permission = AppOpsManager.opToPermission(appOpCode); + if (permission == null) { + return false; + } + int permFlags = mFlagsCache.getPermissionFlags(permission, + packageName, uid); + return (permFlags & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED) != 0; + } + + /** + * Does the app-op item refer to an operation that should be shown to the user. + * Only specficic ops (like SYSTEM_ALERT_WINDOW) or ops that refer to user sensitive + * permission should be shown to the user by default. + * + * @param item The item + * + * @return {@code true} iff the app-op item should be shown to the user + */ + private boolean isUserVisible(AppOpItem item) { + return isUserVisible(item.getCode(), item.getUid(), item.getPackageName()); + } + + + /** + * Does the app-op, uid and package name, refer to an operation that should be shown to the + * user. Only specficic ops (like {@link AppOpsManager.OP_SYSTEM_ALERT_WINDOW}) or + * ops that refer to user sensitive permission should be shown to the user by default. + * + * @param item The item + * + * @return {@code true} iff the app-op for should be shown to the user + */ + private boolean isUserVisible(int appOpCode, int uid, String packageName) { + // currently OP_SYSTEM_ALERT_WINDOW does not correspond to a platform permission + // which may be user senstive, so for now always show it to the user. + if (appOpCode == AppOpsManager.OP_SYSTEM_ALERT_WINDOW) { + return true; + } + + return isUserSensitive(appOpCode, uid, packageName); + } + + /** * Returns a copy of the list containing all the active AppOps that the controller tracks. * + * Call from a worker thread as it may perform long operations. + * * @return List of active AppOps information */ + @WorkerThread public List<AppOpItem> getActiveAppOps() { return getActiveAppOpsForUser(UserHandle.USER_ALL); } @@ -241,10 +304,13 @@ public class AppOpsControllerImpl implements AppOpsController, * Returns a copy of the list containing all the active AppOps that the controller tracks, for * a given user id. * + * Call from a worker thread as it may perform long operations. + * * @param userId User id to track, can be {@link UserHandle#USER_ALL} * * @return List of active AppOps information for that user id */ + @WorkerThread public List<AppOpItem> getActiveAppOpsForUser(int userId) { List<AppOpItem> list = new ArrayList<>(); synchronized (mActiveItems) { @@ -252,7 +318,8 @@ public class AppOpsControllerImpl implements AppOpsController, for (int i = 0; i < numActiveItems; i++) { AppOpItem item = mActiveItems.get(i); if ((userId == UserHandle.USER_ALL - || UserHandle.getUserId(item.getUid()) == userId)) { + || UserHandle.getUserId(item.getUid()) == userId) + && isUserVisible(item)) { list.add(item); } } @@ -262,7 +329,8 @@ public class AppOpsControllerImpl implements AppOpsController, for (int i = 0; i < numNotedItems; i++) { AppOpItem item = mNotedItems.get(i); if ((userId == UserHandle.USER_ALL - || UserHandle.getUserId(item.getUid()) == userId)) { + || UserHandle.getUserId(item.getUid()) == userId) + && isUserVisible(item)) { list.add(item); } } @@ -310,7 +378,7 @@ public class AppOpsControllerImpl implements AppOpsController, } private void notifySuscribers(int code, int uid, String packageName, boolean active) { - if (mCallbacksByCode.containsKey(code)) { + if (mCallbacksByCode.containsKey(code) && isUserVisible(code, uid, packageName)) { if (DEBUG) Log.d(TAG, "Notifying of change in package " + packageName); for (Callback cb: mCallbacksByCode.get(code)) { cb.onActiveStateChanged(code, uid, packageName, active); diff --git a/packages/SystemUI/src/com/android/systemui/appops/PermissionFlagsCache.kt b/packages/SystemUI/src/com/android/systemui/appops/PermissionFlagsCache.kt new file mode 100644 index 000000000000..9248b4f88a36 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/appops/PermissionFlagsCache.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2020 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.systemui.appops + +import android.content.pm.PackageManager +import android.os.UserHandle +import androidx.annotation.WorkerThread +import com.android.systemui.dagger.qualifiers.Background +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton + +private data class PermissionFlagKey( + val permission: String, + val packageName: String, + val uid: Int +) + +/** + * Cache for PackageManager's PermissionFlags. + * + * After a specific `{permission, package, uid}` has been requested, updates to it will be tracked, + * and changes to the uid will trigger new requests (in the background). + */ +@Singleton +class PermissionFlagsCache @Inject constructor( + private val packageManager: PackageManager, + @Background private val executor: Executor +) : PackageManager.OnPermissionsChangedListener { + + private val permissionFlagsCache = + mutableMapOf<Int, MutableMap<PermissionFlagKey, Int>>() + private var listening = false + + override fun onPermissionsChanged(uid: Int) { + executor.execute { + // Only track those that we've seen before + val keys = permissionFlagsCache.get(uid) + if (keys != null) { + keys.mapValuesTo(keys) { + getFlags(it.key) + } + } + } + } + + /** + * Retrieve permission flags from cache or PackageManager. There parameters will be passed + * directly to [PackageManager]. + * + * Calls to this method should be done from a background thread. + */ + @WorkerThread + fun getPermissionFlags(permission: String, packageName: String, uid: Int): Int { + if (!listening) { + listening = true + packageManager.addOnPermissionsChangeListener(this) + } + val key = PermissionFlagKey(permission, packageName, uid) + return permissionFlagsCache.getOrPut(uid, { mutableMapOf() }).get(key) ?: run { + getFlags(key).also { + permissionFlagsCache.get(uid)?.put(key, it) + } + } + } + + private fun getFlags(key: PermissionFlagKey): Int { + return packageManager.getPermissionFlags(key.permission, key.packageName, + UserHandle.getUserHandleForUid(key.uid)) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java index e0049d1349f1..4fdc06e64e2c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java @@ -26,11 +26,14 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.app.AppOpsManager; +import android.content.pm.PackageManager; import android.os.Looper; import android.os.UserHandle; import android.testing.AndroidTestingRunner; @@ -56,6 +59,7 @@ public class AppOpsControllerTest extends SysuiTestCase { private static final String TEST_PACKAGE_NAME = "test"; private static final int TEST_UID = UserHandle.getUid(0, 0); private static final int TEST_UID_OTHER = UserHandle.getUid(1, 0); + private static final int TEST_UID_NON_USER_SENSITIVE = UserHandle.getUid(2, 0); @Mock private AppOpsManager mAppOpsManager; @@ -65,6 +69,10 @@ public class AppOpsControllerTest extends SysuiTestCase { private AppOpsControllerImpl.H mMockHandler; @Mock private DumpManager mDumpManager; + @Mock + private PermissionFlagsCache mFlagsCache; + @Mock + private PackageManager mPackageManager; private AppOpsControllerImpl mController; private TestableLooper mTestableLooper; @@ -76,8 +84,22 @@ public class AppOpsControllerTest extends SysuiTestCase { getContext().addMockSystemService(AppOpsManager.class, mAppOpsManager); - mController = - new AppOpsControllerImpl(mContext, mTestableLooper.getLooper(), mDumpManager); + // All permissions of TEST_UID and TEST_UID_OTHER are user sensitive. None of + // TEST_UID_NON_USER_SENSITIVE are user sensitive. + getContext().setMockPackageManager(mPackageManager); + when(mFlagsCache.getPermissionFlags(anyString(), anyString(), eq(TEST_UID))).thenReturn( + PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED); + when(mFlagsCache.getPermissionFlags(anyString(), anyString(), eq(TEST_UID_OTHER))) + .thenReturn(PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED); + when(mFlagsCache.getPermissionFlags(anyString(), anyString(), + eq(TEST_UID_NON_USER_SENSITIVE))).thenReturn(0); + + mController = new AppOpsControllerImpl( + mContext, + mTestableLooper.getLooper(), + mDumpManager, + mFlagsCache + ); } @Test @@ -173,6 +195,26 @@ public class AppOpsControllerTest extends SysuiTestCase { } @Test + public void nonUserSensitiveOpsAreIgnored() { + mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO, + TEST_UID_NON_USER_SENSITIVE, TEST_PACKAGE_NAME, true); + assertEquals(0, mController.getActiveAppOpsForUser( + UserHandle.getUserId(TEST_UID_NON_USER_SENSITIVE)).size()); + } + + @Test + public void nonUserSensitiveOpsNotNotified() { + mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback); + mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO, + TEST_UID_NON_USER_SENSITIVE, TEST_PACKAGE_NAME, true); + + mTestableLooper.processAllMessages(); + + verify(mCallback, never()) + .onActiveStateChanged(anyInt(), anyInt(), anyString(), anyBoolean()); + } + + @Test public void opNotedScheduledForRemoval() { mController.setBGHandler(mMockHandler); mController.onOpNoted(AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, diff --git a/packages/SystemUI/tests/src/com/android/systemui/appops/PermissionFlagsCacheTest.kt b/packages/SystemUI/tests/src/com/android/systemui/appops/PermissionFlagsCacheTest.kt new file mode 100644 index 000000000000..0fb0ce087ee3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/appops/PermissionFlagsCacheTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2020 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.systemui.appops + +import android.content.pm.PackageManager +import android.os.UserHandle +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class PermissionFlagsCacheTest : SysuiTestCase() { + + companion object { + const val TEST_PERMISSION = "test_permission" + const val TEST_PACKAGE = "test_package" + const val TEST_UID1 = 1000 + const val TEST_UID2 = UserHandle.PER_USER_RANGE + 1000 + } + + @Mock + private lateinit var packageManager: PackageManager + + private lateinit var executor: FakeExecutor + private lateinit var flagsCache: PermissionFlagsCache + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + executor = FakeExecutor(FakeSystemClock()) + + flagsCache = PermissionFlagsCache(packageManager, executor) + executor.runAllReady() + } + + @Test + fun testNotListeningByDefault() { + verify(packageManager, never()).addOnPermissionsChangeListener(any()) + } + + @Test + fun testGetCorrectFlags() { + `when`(packageManager.getPermissionFlags(anyString(), anyString(), any())).thenReturn(0) + `when`(packageManager.getPermissionFlags( + TEST_PERMISSION, + TEST_PACKAGE, + UserHandle.getUserHandleForUid(TEST_UID1)) + ).thenReturn(1) + + assertEquals(1, flagsCache.getPermissionFlags(TEST_PERMISSION, TEST_PACKAGE, TEST_UID1)) + assertNotEquals(1, flagsCache.getPermissionFlags(TEST_PERMISSION, TEST_PACKAGE, TEST_UID2)) + } + + @Test + fun testFlagIsCached() { + flagsCache.getPermissionFlags(TEST_PERMISSION, TEST_PACKAGE, TEST_UID1) + + flagsCache.getPermissionFlags(TEST_PERMISSION, TEST_PACKAGE, TEST_UID1) + + verify(packageManager, times(1)).getPermissionFlags( + TEST_PERMISSION, + TEST_PACKAGE, + UserHandle.getUserHandleForUid(TEST_UID1) + ) + } + + @Test + fun testListeningAfterFirstRequest() { + flagsCache.getPermissionFlags(TEST_PERMISSION, TEST_PACKAGE, TEST_UID1) + + verify(packageManager).addOnPermissionsChangeListener(any()) + } + + @Test + fun testListeningOnlyOnce() { + flagsCache.getPermissionFlags(TEST_PERMISSION, TEST_PACKAGE, TEST_UID1) + + flagsCache.getPermissionFlags(TEST_PERMISSION, TEST_PACKAGE, TEST_UID2) + + verify(packageManager, times(1)).addOnPermissionsChangeListener(any()) + } + + @Test + fun testUpdateFlag() { + assertEquals(0, flagsCache.getPermissionFlags(TEST_PERMISSION, TEST_PACKAGE, TEST_UID1)) + + `when`(packageManager.getPermissionFlags( + TEST_PERMISSION, + TEST_PACKAGE, + UserHandle.getUserHandleForUid(TEST_UID1)) + ).thenReturn(1) + + flagsCache.onPermissionsChanged(TEST_UID1) + + executor.runAllReady() + + assertEquals(1, flagsCache.getPermissionFlags(TEST_PERMISSION, TEST_PACKAGE, TEST_UID1)) + } + + @Test + fun testUpdateFlag_notUpdatedIfUidHasNotBeenRequestedBefore() { + flagsCache.getPermissionFlags(TEST_PERMISSION, TEST_PACKAGE, TEST_UID1) + + flagsCache.onPermissionsChanged(TEST_UID2) + + executor.runAllReady() + + verify(packageManager, never()).getPermissionFlags( + TEST_PERMISSION, + TEST_PACKAGE, + UserHandle.getUserHandleForUid(TEST_UID2) + ) + } +}
\ No newline at end of file |