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
This commit is contained in:
Fabian Kozynski
2021-12-03 17:04:11 -05:00
parent f4c585f919
commit 3a738b180e
6 changed files with 439 additions and 4 deletions

View File

@@ -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.

View File

@@ -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<UiState> 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);
}
}

View File

@@ -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<ComponentName, Integer> mTrackingMap = new SparseArrayMap<>();
@GuardedBy("mLock")
private final ArraySet<ComponentName> 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();
}
}

View File

@@ -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<BroadcastReceiverRegistration> mRegistrationList =
ArrayList<BroadcastReceiverRegistration> mRegistrationList =
new ArrayList<>();
NoBroadcastContextWrapper(Context context) {

View File

@@ -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);
}

View File

@@ -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);
}
}