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 new file mode 100644 index 0000000000000..f172279c50bb9 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/statusbar/NoBroadcastContextWrapper.java @@ -0,0 +1,130 @@ +/* + * 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 android.testing.TestableContext; + +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 TestableContext { + + 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..c293b5e7c5930 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/statusbar/StatusBarManagerServiceTest.java @@ -0,0 +1,659 @@ +/* + * 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.times; +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 NoBroadcastContextWrapper(InstrumentationRegistry.getContext()); + + @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); + } + + @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); + } + + 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; + } + } +} 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); + } +}