diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index 48ceef06ea986..2392c9aa185c1 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -626,6 +626,9 @@ public class StatusBarManager { * foreground ({@link ActivityManager.RunningAppProcessInfo#IMPORTANCE_FOREGROUND} * and the {@link android.service.quicksettings.TileService} must be exported. * + * Note: the system can choose to auto-deny a request if the user has denied that specific + * request (user, ComponentName) enough times before. + * * @param tileServiceComponentName {@link ComponentName} of the * {@link android.service.quicksettings.TileService} for the request. * @param tileLabel label of the tile to show to the user. diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 232ea0977abad..4eae939f9e34c 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -64,6 +64,7 @@ import android.service.quicksettings.TileService; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.IndentingPrintWriter; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; @@ -139,6 +140,8 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D private int mCurrentUserId; private boolean mTracingEnabled; + private final TileRequestTracker mTileRequestTracker; + private final SparseArray mDisplayUiState = new SparseArray<>(); @GuardedBy("mLock") private IUdfpsHbmListener mUdfpsHbmListener; @@ -245,6 +248,8 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D mActivityTaskManager = LocalServices.getService(ActivityTaskManagerInternal.class); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + + mTileRequestTracker = new TileRequestTracker(mContext); } @Override @@ -1765,11 +1770,26 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D mCurrentRequestAddTilePackages.put(packageName, currentTime); } + if (mTileRequestTracker.shouldBeDenied(userId, componentName)) { + if (clearTileAddRequest(packageName)) { + try { + callback.onTileRequest(StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED); + } catch (RemoteException e) { + Slog.e(TAG, "requestAddTile - callback", e); + } + } + return; + } + IAddTileResultCallback proxyCallback = new IAddTileResultCallback.Stub() { @Override public void onTileRequest(int i) { if (i == StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED) { i = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED; + } else if (i == StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED) { + mTileRequestTracker.addDenial(userId, componentName); + } else if (i == StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED) { + mTileRequestTracker.resetRequests(userId, componentName); } if (clearTileAddRequest(packageName)) { try { @@ -1961,6 +1981,8 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D pw.println(" " + requests.get(i) + ","); } pw.println(" ]"); + IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); + mTileRequestTracker.dump(fd, ipw.increaseIndent(), args); } } diff --git a/services/core/java/com/android/server/statusbar/TileRequestTracker.java b/services/core/java/com/android/server/statusbar/TileRequestTracker.java new file mode 100644 index 0000000000000..d5ace3f7d1778 --- /dev/null +++ b/services/core/java/com/android/server/statusbar/TileRequestTracker.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2021 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.statusbar; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.IndentingPrintWriter; +import android.util.SparseArrayMap; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.io.FileDescriptor; + +/** + * Tracks user denials of requests from {@link StatusBarManagerService#requestAddTile}. + * + * After a certain number of denials for a particular pair (user,ComponentName), requests will be + * auto-denied without showing a dialog to the user. + */ +public class TileRequestTracker { + + @VisibleForTesting + static final int MAX_NUM_DENIALS = 3; + + private final Context mContext; + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private final SparseArrayMap mTrackingMap = new SparseArrayMap<>(); + @GuardedBy("mLock") + private final ArraySet mComponentsToRemove = new ArraySet<>(); + + private final BroadcastReceiver mUninstallReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + return; + } + + Uri data = intent.getData(); + String packageName = data.getEncodedSchemeSpecificPart(); + + if (!intent.hasExtra(Intent.EXTRA_UID)) { + return; + } + int userId = UserHandle.getUserId(intent.getIntExtra(Intent.EXTRA_UID, -1)); + synchronized (mLock) { + mComponentsToRemove.clear(); + final int elementsForUser = mTrackingMap.numElementsForKey(userId); + final int userKeyIndex = mTrackingMap.indexOfKey(userId); + for (int compKeyIndex = 0; compKeyIndex < elementsForUser; compKeyIndex++) { + ComponentName c = mTrackingMap.keyAt(userKeyIndex, compKeyIndex); + if (c.getPackageName().equals(packageName)) { + mComponentsToRemove.add(c); + } + } + final int compsToRemoveNum = mComponentsToRemove.size(); + for (int i = 0; i < compsToRemoveNum; i++) { + ComponentName c = mComponentsToRemove.valueAt(i); + mTrackingMap.delete(userId, c); + } + } + } + }; + + TileRequestTracker(Context context) { + mContext = context; + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + intentFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED); + intentFilter.addDataScheme("package"); + mContext.registerReceiverAsUser(mUninstallReceiver, UserHandle.ALL, intentFilter, null, + null); + } + + /** + * Return whether this combination of {@code userId} and {@link ComponentName} should be + * auto-denied. + */ + boolean shouldBeDenied(int userId, ComponentName componentName) { + synchronized (mLock) { + return mTrackingMap.getOrDefault(userId, componentName, 0) >= MAX_NUM_DENIALS; + } + } + + /** + * Add a new denial instance for a given {@code userId} and {@link ComponentName}. + */ + void addDenial(int userId, ComponentName componentName) { + synchronized (mLock) { + int current = mTrackingMap.getOrDefault(userId, componentName, 0); + mTrackingMap.add(userId, componentName, current + 1); + } + } + + /** + * Reset the number of denied request for a given {@code userId} and {@link ComponentName}. + */ + void resetRequests(int userId, ComponentName componentName) { + synchronized (mLock) { + mTrackingMap.delete(userId, componentName); + } + } + + void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) { + pw.println("TileRequestTracker:"); + pw.increaseIndent(); + synchronized (mLock) { + mTrackingMap.forEach((user, componentName, value) -> { + pw.println("user=" + user + ", " + componentName.toShortString() + ": " + value); + }); + } + pw.decreaseIndent(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/statusbar/NoBroadcastContextWrapper.java b/services/tests/servicestests/src/com/android/server/statusbar/NoBroadcastContextWrapper.java index ffe9ef5a76490..f172279c50bb9 100644 --- a/services/tests/servicestests/src/com/android/server/statusbar/NoBroadcastContextWrapper.java +++ b/services/tests/servicestests/src/com/android/server/statusbar/NoBroadcastContextWrapper.java @@ -23,6 +23,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Handler; import android.os.UserHandle; +import android.testing.TestableContext; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -34,9 +35,9 @@ import java.util.ArrayList; * * Instead, it keeps a list of the registrations for querying. */ -class NoBroadcastContextWrapper extends ContextWrapper { +class NoBroadcastContextWrapper extends TestableContext { - private ArrayList mRegistrationList = + ArrayList mRegistrationList = new ArrayList<>(); NoBroadcastContextWrapper(Context context) { diff --git a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java index 65a367400b4ad..c293b5e7c5930 100644 --- a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -82,8 +83,7 @@ public class StatusBarManagerServiceTest { @Rule public final TestableContext mContext = - new TestableContext( - new NoBroadcastContextWrapper(InstrumentationRegistry.getContext()), null); + new NoBroadcastContextWrapper(InstrumentationRegistry.getContext()); @Mock private ActivityTaskManagerInternal mActivityTaskManagerInternal; @@ -519,6 +519,62 @@ public class StatusBarManagerServiceTest { callback.mUserResponse); } + @Test + public void testInstaDenialAfterManyDenials() throws RemoteException { + int user = 10; + mockEverything(user); + + for (int i = 0; i < TileRequestTracker.MAX_NUM_DENIALS; i++) { + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, + new Callback()); + + verify(mMockStatusBar, times(i + 1)).requestAddTile( + eq(TEST_COMPONENT), + eq(APP_NAME), + eq(TILE_LABEL), + eq(mIcon), + mAddTileResultCallbackCaptor.capture() + ); + mAddTileResultCallbackCaptor.getValue().onTileRequest( + StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED); + } + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + // Only called MAX_NUM_DENIALS times + verify(mMockStatusBar, times(TileRequestTracker.MAX_NUM_DENIALS)).requestAddTile( + any(), + any(), + any(), + any(), + mAddTileResultCallbackCaptor.capture() + ); + assertEquals(StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED, + callback.mUserResponse); + } + + @Test + public void testDialogDismissalNotCountingAgainstDenials() throws RemoteException { + int user = 10; + mockEverything(user); + + for (int i = 0; i < TileRequestTracker.MAX_NUM_DENIALS * 2; i++) { + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, + new Callback()); + + verify(mMockStatusBar, times(i + 1)).requestAddTile( + eq(TEST_COMPONENT), + eq(APP_NAME), + eq(TILE_LABEL), + eq(mIcon), + mAddTileResultCallbackCaptor.capture() + ); + mAddTileResultCallbackCaptor.getValue().onTileRequest( + StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED); + } + } + private void mockUidCheck() { mockUidCheck(TEST_PACKAGE); } diff --git a/services/tests/servicestests/src/com/android/server/statusbar/TileRequestTrackerTest.java b/services/tests/servicestests/src/com/android/server/statusbar/TileRequestTrackerTest.java new file mode 100644 index 0000000000000..dac6df916844d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/statusbar/TileRequestTrackerTest.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2021 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.statusbar; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.UserHandle; + +import androidx.test.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.MockitoAnnotations; + +@RunWith(JUnit4.class) +public class TileRequestTrackerTest { + + private static final String TEST_PACKAGE = "test_pkg"; + private static final String TEST_SERVICE = "test_svc"; + private static final String TEST_SERVICE_OTHER = "test_svc_other"; + private static final ComponentName TEST_COMPONENT = new ComponentName(TEST_PACKAGE, + TEST_SERVICE); + private static final ComponentName TEST_COMPONENT_OTHER = new ComponentName(TEST_PACKAGE, + TEST_SERVICE_OTHER); + private static final ComponentName TEST_COMPONENT_OTHER_PACKAGE = new ComponentName("other", + TEST_SERVICE); + private static final int USER_ID = 0; + private static final int USER_ID_OTHER = 10; + private static final int APP_UID = 12345; + private static final int USER_UID = UserHandle.getUid(USER_ID, APP_UID); + private static final int USER_OTHER_UID = UserHandle.getUid(USER_ID_OTHER, APP_UID); + + @Rule + public final NoBroadcastContextWrapper mContext = + new NoBroadcastContextWrapper(InstrumentationRegistry.getContext()); + + private TileRequestTracker mTileRequestTracker; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mTileRequestTracker = new TileRequestTracker(mContext); + } + + @Test + public void testBroadcastReceiverRegistered() { + NoBroadcastContextWrapper.BroadcastReceiverRegistration reg = getReceiverRegistration(); + + assertEquals(UserHandle.ALL, reg.mUser); + assertNull(reg.mBroadcastPermission); + assertNotNull(reg.mReceiver); + + IntentFilter filter = reg.mIntentFilter; + assertEquals(2, filter.countActions()); + assertTrue(filter.hasAction(Intent.ACTION_PACKAGE_REMOVED)); + assertTrue(filter.hasAction(Intent.ACTION_PACKAGE_DATA_CLEARED)); + assertTrue(filter.hasDataScheme("package")); + } + + @Test + public void testNoDenialsFromStart() { + // Certainly not an exhaustive test + assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT)); + assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID_OTHER, TEST_COMPONENT)); + assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT_OTHER)); + assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID_OTHER, TEST_COMPONENT_OTHER)); + } + + @Test + public void testNoDenialBeforeMax() { + for (int i = 1; i < TileRequestTracker.MAX_NUM_DENIALS; i++) { + mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT); + } + + assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT)); + } + + @Test + public void testDenialOnMax() { + for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) { + mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT); + } + assertTrue(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT)); + } + + @Test + public void testDenialPerUser() { + for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) { + mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT); + } + + assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID_OTHER, TEST_COMPONENT)); + } + + @Test + public void testDenialPerComponent() { + for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) { + mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT); + } + + assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT_OTHER)); + } + + @Test + public void testPackageUninstallRemovesDenials_allComponents() { + for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) { + mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT); + mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT_OTHER); + } + + Intent intent = new Intent(Intent.ACTION_PACKAGE_REMOVED); + intent.putExtra(Intent.EXTRA_UID, USER_UID); + intent.setData(Uri.parse("package:" + TEST_PACKAGE)); + getReceiverRegistration().mReceiver.onReceive(mContext, intent); + + assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT)); + assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT_OTHER)); + } + + @Test + public void testPackageUninstallRemoveDenials_differentUsers() { + for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) { + mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT); + mTileRequestTracker.addDenial(USER_ID_OTHER, TEST_COMPONENT); + } + + Intent intent = new Intent(Intent.ACTION_PACKAGE_REMOVED); + intent.putExtra(Intent.EXTRA_UID, USER_OTHER_UID); + intent.setData(Uri.parse("package:" + TEST_PACKAGE)); + getReceiverRegistration().mReceiver.onReceive(mContext, intent); + + // User 0 package was not removed + assertTrue(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT)); + // User 10 package was removed + assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID_OTHER, TEST_COMPONENT)); + } + + @Test + public void testPackageUninstallRemoveDenials_differentPackages() { + for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) { + mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT); + mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT_OTHER_PACKAGE); + } + + Intent intent = new Intent(Intent.ACTION_PACKAGE_REMOVED); + intent.putExtra(Intent.EXTRA_UID, USER_UID); + intent.setData(Uri.parse("package:" + TEST_PACKAGE)); + getReceiverRegistration().mReceiver.onReceive(mContext, intent); + + // Package TEST_PACKAGE removed + assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT)); + // Package "other" not removed + assertTrue(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT_OTHER_PACKAGE)); + } + + @Test + public void testPackageUpdateDoesntRemoveDenials() { + for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) { + mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT); + } + + Intent intent = new Intent(Intent.ACTION_PACKAGE_REMOVED); + intent.putExtra(Intent.EXTRA_REPLACING, true); + intent.putExtra(Intent.EXTRA_UID, USER_UID); + intent.setData(Uri.parse("package:" + TEST_PACKAGE)); + getReceiverRegistration().mReceiver.onReceive(mContext, intent); + + assertTrue(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT)); + } + + @Test + public void testClearPackageDataRemovesDenials() { + for (int i = 1; i <= TileRequestTracker.MAX_NUM_DENIALS; i++) { + mTileRequestTracker.addDenial(USER_ID, TEST_COMPONENT); + } + + Intent intent = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED); + intent.putExtra(Intent.EXTRA_UID, USER_UID); + intent.setData(Uri.parse("package:" + TEST_PACKAGE)); + getReceiverRegistration().mReceiver.onReceive(mContext, intent); + + assertFalse(mTileRequestTracker.shouldBeDenied(USER_ID, TEST_COMPONENT)); + } + + private NoBroadcastContextWrapper.BroadcastReceiverRegistration getReceiverRegistration() { + assertEquals(1, mContext.mRegistrationList.size()); + return mContext.mRegistrationList.get(0); + } +}