From f4c585f9195ed22439e26e87b9dc1d71a26957d9 Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Fri, 3 Dec 2021 16:26:39 -0500 Subject: [PATCH 1/2] Add tests for StatusBarManagerService These tests only test the requestAddTile functionality. This is a setup CL before adding tests for new functionality. NoBroadcastContextWrapper is part of that setup. Bug: 208671346 Test: atest StatusBarManagerServiceTileRequestTest Change-Id: Ie1e5c19c0fb03735efc808e4da6febb5f536811f --- .../statusbar/NoBroadcastContextWrapper.java | 129 ++++ .../StatusBarManagerServiceTest.java | 603 ++++++++++++++++++ 2 files changed, 732 insertions(+) create mode 100644 services/tests/servicestests/src/com/android/server/statusbar/NoBroadcastContextWrapper.java create mode 100644 services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java diff --git a/services/tests/servicestests/src/com/android/server/statusbar/NoBroadcastContextWrapper.java b/services/tests/servicestests/src/com/android/server/statusbar/NoBroadcastContextWrapper.java new file mode 100644 index 0000000000000..ffe9ef5a76490 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/statusbar/NoBroadcastContextWrapper.java @@ -0,0 +1,129 @@ +/* + * 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.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.UserHandle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; + +/** + * {@link ContextWrapper} that doesn't register {@link BroadcastReceiver}. + * + * Instead, it keeps a list of the registrations for querying. + */ +class NoBroadcastContextWrapper extends ContextWrapper { + + private ArrayList mRegistrationList = + new ArrayList<>(); + + NoBroadcastContextWrapper(Context context) { + super(context); + } + + @Override + public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter) { + return registerReceiver(receiver, filter, 0); + } + + @Override + public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter, + int flags) { + return registerReceiver(receiver, filter, null, null, flags); + } + + @Override + public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter, + @Nullable String broadcastPermission, @Nullable Handler scheduler) { + return registerReceiver(receiver, filter, broadcastPermission, scheduler, 0); + } + + @Override + public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter, + @Nullable String broadcastPermission, @Nullable Handler scheduler, int flags) { + return registerReceiverAsUser(receiver, getUser(), filter, broadcastPermission, scheduler, + flags); + } + + @Nullable + @Override + public Intent registerReceiverForAllUsers(@Nullable BroadcastReceiver receiver, + @NonNull IntentFilter filter, @Nullable String broadcastPermission, + @Nullable Handler scheduler) { + return registerReceiverForAllUsers(receiver, filter, broadcastPermission, scheduler, 0); + } + + @Nullable + @Override + public Intent registerReceiverForAllUsers(@Nullable BroadcastReceiver receiver, + @NonNull IntentFilter filter, @Nullable String broadcastPermission, + @Nullable Handler scheduler, int flags) { + return registerReceiverAsUser(receiver, UserHandle.ALL, filter, broadcastPermission, + scheduler, flags); + } + + @Override + public Intent registerReceiverAsUser(@Nullable BroadcastReceiver receiver, UserHandle user, + IntentFilter filter, @Nullable String broadcastPermission, + @Nullable Handler scheduler) { + return registerReceiverAsUser(receiver, user, filter, broadcastPermission, + scheduler, 0); + } + + @Override + public Intent registerReceiverAsUser(@Nullable BroadcastReceiver receiver, UserHandle user, + IntentFilter filter, @Nullable String broadcastPermission, + @Nullable Handler scheduler, int flags) { + BroadcastReceiverRegistration reg = new BroadcastReceiverRegistration( + receiver, user, filter, broadcastPermission, scheduler, flags + ); + mRegistrationList.add(reg); + return null; + } + + @Override + public void unregisterReceiver(BroadcastReceiver receiver) { + mRegistrationList.removeIf((reg) -> reg.mReceiver == receiver); + } + + static class BroadcastReceiverRegistration { + final BroadcastReceiver mReceiver; + final UserHandle mUser; + final IntentFilter mIntentFilter; + final String mBroadcastPermission; + final Handler mHandler; + final int mFlags; + + BroadcastReceiverRegistration(BroadcastReceiver receiver, UserHandle user, + IntentFilter intentFilter, String broadcastPermission, Handler handler, int flags) { + mReceiver = receiver; + mUser = user; + mIntentFilter = intentFilter; + mBroadcastPermission = broadcastPermission; + mHandler = handler; + mFlags = flags; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java new file mode 100644 index 0000000000000..65a367400b4ad --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java @@ -0,0 +1,603 @@ +/* + * 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 android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; +import static android.app.ActivityManager.PROCESS_STATE_TOP; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.any; +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.verify; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.app.ActivityManagerInternal; +import android.app.StatusBarManager; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.graphics.drawable.Icon; +import android.hardware.display.DisplayManager; +import android.os.Binder; +import android.os.Looper; +import android.os.RemoteException; +import android.os.UserHandle; +import android.service.quicksettings.TileService; +import android.testing.TestableContext; + +import androidx.test.InstrumentationRegistry; + +import com.android.internal.statusbar.IAddTileResultCallback; +import com.android.internal.statusbar.IStatusBar; +import com.android.server.LocalServices; +import com.android.server.policy.GlobalActionsProvider; +import com.android.server.wm.ActivityTaskManagerInternal; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(JUnit4.class) +public class StatusBarManagerServiceTest { + + private static final String TEST_PACKAGE = "test_pkg"; + private static final String TEST_SERVICE = "test_svc"; + private static final ComponentName TEST_COMPONENT = new ComponentName(TEST_PACKAGE, + TEST_SERVICE); + private static final CharSequence APP_NAME = "AppName"; + private static final CharSequence TILE_LABEL = "Tile label"; + + @Rule + public final TestableContext mContext = + new TestableContext( + new NoBroadcastContextWrapper(InstrumentationRegistry.getContext()), null); + + @Mock + private ActivityTaskManagerInternal mActivityTaskManagerInternal; + @Mock + private PackageManagerInternal mPackageManagerInternal; + @Mock + private ActivityManagerInternal mActivityManagerInternal; + @Mock + private ApplicationInfo mApplicationInfo; + @Mock + private IStatusBar.Stub mMockStatusBar; + @Captor + private ArgumentCaptor mAddTileResultCallbackCaptor; + + private Icon mIcon; + private StatusBarManagerService mStatusBarManagerService; + + @BeforeClass + public static void oneTimeInitialization() { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class); + LocalServices.addService(ActivityTaskManagerInternal.class, mActivityTaskManagerInternal); + LocalServices.removeServiceForTest(ActivityManagerInternal.class); + LocalServices.addService(ActivityManagerInternal.class, mActivityManagerInternal); + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternal); + + when(mMockStatusBar.asBinder()).thenReturn(mMockStatusBar); + when(mApplicationInfo.loadLabel(any())).thenReturn(APP_NAME); + + mStatusBarManagerService = new StatusBarManagerService(mContext); + LocalServices.removeServiceForTest(StatusBarManagerInternal.class); + LocalServices.removeServiceForTest(GlobalActionsProvider.class); + + mContext.getSystemService(DisplayManager.class).unregisterDisplayListener( + mStatusBarManagerService); + + mStatusBarManagerService.registerStatusBar(mMockStatusBar); + + mIcon = Icon.createWithResource(mContext, android.R.drawable.btn_plus); + } + + @Test + public void testHandleIncomingUserCalled() { + int fakeUser = 17; + try { + mStatusBarManagerService.requestAddTile( + TEST_COMPONENT, + TILE_LABEL, + mIcon, + fakeUser, + new Callback() + ); + fail("Should have SecurityException from uid check"); + } catch (SecurityException e) { + verify(mActivityManagerInternal).handleIncomingUser( + eq(Binder.getCallingPid()), + eq(Binder.getCallingUid()), + eq(fakeUser), + eq(false), + eq(ActivityManagerInternal.ALLOW_NON_FULL), + anyString(), + eq(TEST_PACKAGE) + ); + } + } + + @Test + public void testCheckUid_pass() { + when(mPackageManagerInternal.getPackageUid(TEST_PACKAGE, 0, mContext.getUserId())) + .thenReturn(Binder.getCallingUid()); + try { + mStatusBarManagerService.requestAddTile( + TEST_COMPONENT, + TILE_LABEL, + mIcon, + mContext.getUserId(), + new Callback() + ); + } catch (SecurityException e) { + fail("No SecurityException should be thrown"); + } + } + + @Test + public void testCheckUid_pass_differentUser() { + int otherUserUid = UserHandle.getUid(17, UserHandle.getAppId(Binder.getCallingUid())); + when(mPackageManagerInternal.getPackageUid(TEST_PACKAGE, 0, mContext.getUserId())) + .thenReturn(otherUserUid); + try { + mStatusBarManagerService.requestAddTile( + TEST_COMPONENT, + TILE_LABEL, + mIcon, + mContext.getUserId(), + new Callback() + ); + } catch (SecurityException e) { + fail("No SecurityException should be thrown"); + } + } + + @Test + public void testCheckUid_fail() { + when(mPackageManagerInternal.getPackageUid(TEST_PACKAGE, 0, mContext.getUserId())) + .thenReturn(Binder.getCallingUid() + 1); + try { + mStatusBarManagerService.requestAddTile( + TEST_COMPONENT, + TILE_LABEL, + mIcon, + mContext.getUserId(), + new Callback() + ); + fail("Should throw SecurityException"); + } catch (SecurityException e) { + // pass + } + } + + @Test + public void testCurrentUser_fail() { + mockUidCheck(); + int user = 0; + when(mActivityManagerInternal.getCurrentUserId()).thenReturn(user + 1); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER, + callback.mUserResponse); + } + + @Test + public void testCurrentUser_pass() { + mockUidCheck(); + int user = 0; + when(mActivityManagerInternal.getCurrentUserId()).thenReturn(user); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + assertNotEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER, + callback.mUserResponse); + } + + @Test + public void testValidComponent_fail_noComponentFound() { + int user = 10; + mockUidCheck(); + mockCurrentUserCheck(user); + IntentMatcher im = new IntentMatcher( + new Intent(TileService.ACTION_QS_TILE).setComponent(TEST_COMPONENT)); + when(mPackageManagerInternal.resolveService(argThat(im), nullable(String.class), eq(0), + eq(user), anyInt())).thenReturn(null); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT, callback.mUserResponse); + } + + @Test + public void testValidComponent_fail_notEnabled() { + int user = 10; + mockUidCheck(); + mockCurrentUserCheck(user); + + ResolveInfo r = makeResolveInfo(); + r.serviceInfo.permission = Manifest.permission.BIND_QUICK_SETTINGS_TILE; + + IntentMatcher im = new IntentMatcher( + new Intent(TileService.ACTION_QS_TILE).setComponent(TEST_COMPONENT)); + when(mPackageManagerInternal.resolveService(argThat(im), nullable(String.class), eq(0), + eq(user), anyInt())).thenReturn(r); + when(mPackageManagerInternal.getComponentEnabledSetting(TEST_COMPONENT, + Binder.getCallingUid(), user)).thenReturn( + PackageManager.COMPONENT_ENABLED_STATE_DISABLED); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT, callback.mUserResponse); + } + + @Test + public void testValidComponent_fail_noPermission() { + int user = 10; + mockUidCheck(); + mockCurrentUserCheck(user); + + ResolveInfo r = makeResolveInfo(); + + IntentMatcher im = new IntentMatcher( + new Intent(TileService.ACTION_QS_TILE).setComponent(TEST_COMPONENT)); + when(mPackageManagerInternal.resolveService(argThat(im), nullable(String.class), eq(0), + eq(user), anyInt())).thenReturn(r); + when(mPackageManagerInternal.getComponentEnabledSetting(TEST_COMPONENT, + Binder.getCallingUid(), user)).thenReturn( + PackageManager.COMPONENT_ENABLED_STATE_ENABLED); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT, callback.mUserResponse); + } + + @Test + public void testValidComponent_fail_notExported() { + int user = 10; + mockUidCheck(); + mockCurrentUserCheck(user); + + ResolveInfo r = makeResolveInfo(); + r.serviceInfo.permission = Manifest.permission.BIND_QUICK_SETTINGS_TILE; + r.serviceInfo.exported = false; + + IntentMatcher im = new IntentMatcher( + new Intent(TileService.ACTION_QS_TILE).setComponent(TEST_COMPONENT)); + when(mPackageManagerInternal.resolveService(argThat(im), nullable(String.class), eq(0), + eq(user), anyInt())).thenReturn(r); + when(mPackageManagerInternal.getComponentEnabledSetting(TEST_COMPONENT, + Binder.getCallingUid(), user)).thenReturn( + PackageManager.COMPONENT_ENABLED_STATE_ENABLED); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT, callback.mUserResponse); + } + + @Test + public void testValidComponent_pass() { + int user = 10; + mockUidCheck(); + mockCurrentUserCheck(user); + + ResolveInfo r = makeResolveInfo(); + r.serviceInfo.permission = Manifest.permission.BIND_QUICK_SETTINGS_TILE; + r.serviceInfo.exported = true; + + IntentMatcher im = new IntentMatcher( + new Intent(TileService.ACTION_QS_TILE).setComponent(TEST_COMPONENT)); + when(mPackageManagerInternal.resolveService(argThat(im), nullable(String.class), eq(0), + eq(user), anyInt())).thenReturn(r); + when(mPackageManagerInternal.getComponentEnabledSetting(TEST_COMPONENT, + Binder.getCallingUid(), user)).thenReturn( + PackageManager.COMPONENT_ENABLED_STATE_ENABLED); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + assertNotEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT, + callback.mUserResponse); + } + + @Test + public void testAppInForeground_fail() { + int user = 10; + mockUidCheck(); + mockCurrentUserCheck(user); + mockComponentInfo(user); + + when(mActivityManagerInternal.getUidProcessState(Binder.getCallingUid())).thenReturn( + PROCESS_STATE_FOREGROUND_SERVICE); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND, + callback.mUserResponse); + } + + @Test + public void testAppInForeground_pass() { + int user = 10; + mockUidCheck(); + mockCurrentUserCheck(user); + mockComponentInfo(user); + + when(mActivityManagerInternal.getUidProcessState(Binder.getCallingUid())).thenReturn( + PROCESS_STATE_TOP); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + assertNotEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND, + callback.mUserResponse); + } + + @Test + public void testRequestToStatusBar() throws RemoteException { + int user = 10; + mockEverything(user); + + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, + new Callback()); + + verify(mMockStatusBar).requestAddTile( + eq(TEST_COMPONENT), + eq(APP_NAME), + eq(TILE_LABEL), + eq(mIcon), + any() + ); + } + + @Test + public void testRequestInProgress_samePackage() throws RemoteException { + int user = 10; + mockEverything(user); + + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, + new Callback()); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + assertEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS, + callback.mUserResponse); + } + + @Test + public void testRequestInProgress_differentPackage() throws RemoteException { + int user = 10; + mockEverything(user); + ComponentName otherComponent = new ComponentName("a", "b"); + mockUidCheck(otherComponent.getPackageName()); + mockComponentInfo(user, otherComponent); + + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, + new Callback()); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(otherComponent, TILE_LABEL, mIcon, user, callback); + + assertNotEquals(StatusBarManager.TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS, + callback.mUserResponse); + } + + @Test + public void testResponseForwardedToCallback_tileAdded() throws RemoteException { + int user = 10; + mockEverything(user); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + verify(mMockStatusBar).requestAddTile( + eq(TEST_COMPONENT), + eq(APP_NAME), + eq(TILE_LABEL), + eq(mIcon), + mAddTileResultCallbackCaptor.capture() + ); + + mAddTileResultCallbackCaptor.getValue().onTileRequest( + StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED); + assertEquals(StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED, callback.mUserResponse); + } + + @Test + public void testResponseForwardedToCallback_tileNotAdded() throws RemoteException { + int user = 10; + mockEverything(user); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + verify(mMockStatusBar).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); + assertEquals(StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED, + callback.mUserResponse); + } + + @Test + public void testResponseForwardedToCallback_tileAlreadyAdded() throws RemoteException { + int user = 10; + mockEverything(user); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + verify(mMockStatusBar).requestAddTile( + eq(TEST_COMPONENT), + eq(APP_NAME), + eq(TILE_LABEL), + eq(mIcon), + mAddTileResultCallbackCaptor.capture() + ); + + mAddTileResultCallbackCaptor.getValue().onTileRequest( + StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED); + assertEquals(StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED, + callback.mUserResponse); + } + + @Test + public void testResponseForwardedToCallback_dialogDismissed() throws RemoteException { + int user = 10; + mockEverything(user); + + Callback callback = new Callback(); + mStatusBarManagerService.requestAddTile(TEST_COMPONENT, TILE_LABEL, mIcon, user, callback); + + verify(mMockStatusBar).requestAddTile( + eq(TEST_COMPONENT), + eq(APP_NAME), + eq(TILE_LABEL), + eq(mIcon), + mAddTileResultCallbackCaptor.capture() + ); + + mAddTileResultCallbackCaptor.getValue().onTileRequest( + StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED); + // This gets translated to TILE_NOT_ADDED + assertEquals(StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED, + callback.mUserResponse); + } + + private void mockUidCheck() { + mockUidCheck(TEST_PACKAGE); + } + + private void mockUidCheck(String packageName) { + when(mPackageManagerInternal.getPackageUid(eq(packageName), anyInt(), anyInt())) + .thenReturn(Binder.getCallingUid()); + } + + private void mockCurrentUserCheck(int user) { + when(mActivityManagerInternal.getCurrentUserId()).thenReturn(user); + } + + private void mockComponentInfo(int user) { + mockComponentInfo(user, TEST_COMPONENT); + } + + private ResolveInfo makeResolveInfo() { + ResolveInfo r = new ResolveInfo(); + r.serviceInfo = new ServiceInfo(); + r.serviceInfo.applicationInfo = mApplicationInfo; + return r; + } + + private void mockComponentInfo(int user, ComponentName componentName) { + ResolveInfo r = makeResolveInfo(); + r.serviceInfo.exported = true; + r.serviceInfo.permission = Manifest.permission.BIND_QUICK_SETTINGS_TILE; + + IntentMatcher im = new IntentMatcher( + new Intent(TileService.ACTION_QS_TILE).setComponent(componentName)); + when(mPackageManagerInternal.resolveService(argThat(im), nullable(String.class), eq(0), + eq(user), anyInt())).thenReturn(r); + when(mPackageManagerInternal.getComponentEnabledSetting(componentName, + Binder.getCallingUid(), user)).thenReturn( + PackageManager.COMPONENT_ENABLED_STATE_ENABLED); + } + + private void mockProcessState() { + when(mActivityManagerInternal.getUidProcessState(Binder.getCallingUid())).thenReturn( + PROCESS_STATE_TOP); + } + + private void mockEverything(int user) { + mockUidCheck(); + mockCurrentUserCheck(user); + mockComponentInfo(user); + mockProcessState(); + } + + private static class Callback extends IAddTileResultCallback.Stub { + int mUserResponse = -1; + + @Override + public void onTileRequest(int userResponse) throws RemoteException { + if (mUserResponse != -1) { + throw new IllegalStateException( + "Setting response to " + userResponse + " but it already has " + + mUserResponse); + } + mUserResponse = userResponse; + } + } + + private static class IntentMatcher implements ArgumentMatcher { + private final Intent mIntent; + + IntentMatcher(Intent intent) { + mIntent = intent; + } + + @Override + public boolean matches(Intent argument) { + return argument != null && argument.filterEquals(mIntent); + } + + @Override + public String toString() { + return "Expected: " + mIntent; + } + } +} From 3a738b180eb939a52617876b85785f2f278d4b87 Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Fri, 3 Dec 2021 17:04:11 -0500 Subject: [PATCH 2/2] Auto-deny requests after some user denials After the user denies the request to add a tile (a particular ComponentName) a certain number of times, auto deny further requests for that user,component pair. Cancelling the dialog (by going back/click outside) don't count against. This information is reset when the package is uninstalled or data is cleared for the package. In the future, this information will be persisted across restarts. Fixes: 208671346 Test: manual Test: atest com.android.server.statusbar Change-Id: I1828d1d3e462fc9b4af94c4350df895994d02b6c --- core/java/android/app/StatusBarManager.java | 3 + .../statusbar/StatusBarManagerService.java | 22 ++ .../server/statusbar/TileRequestTracker.java | 138 +++++++++++ .../statusbar/NoBroadcastContextWrapper.java | 5 +- .../StatusBarManagerServiceTest.java | 60 ++++- .../statusbar/TileRequestTrackerTest.java | 215 ++++++++++++++++++ 6 files changed, 439 insertions(+), 4 deletions(-) create mode 100644 services/core/java/com/android/server/statusbar/TileRequestTracker.java create mode 100644 services/tests/servicestests/src/com/android/server/statusbar/TileRequestTrackerTest.java 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); + } +}