Merge "Fixing the failing tests" into qt-dev
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 349ecc1..09d581f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -867,7 +867,7 @@
<string name="storage_clear_user_data_text">Clear storage</string>
<!-- Individual application info screen, button label under Storage heading. Text label for button [CHAR LIMIT=35] -->
<string name="storage_clear_cache_btn_text">Clear cache</string>
- <!-- Individual application screen, confirmation dialog title. Displays when user selects to "Clear data". [CHAR LIMIT=35] -->
+ <!-- Individual application screen, confirmation dialog title. Displays when user selects to "Clear data". [CHAR LIMIT=45] -->
<string name="storage_clear_data_dlg_title">Delete app data?</string>
<!-- Individual application screen, confirmation dialog message. Displays when user selects to "Clear data". It warns the user of the consequences of clearing the data for an app. [CHAR LIMIT=200] -->
<string name="storage_clear_data_dlg_text">All this app\u2019s data will be deleted permanently. This includes all files, settings, accounts, databases, etc.</string>
diff --git a/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridge.java b/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridge.java
new file mode 100644
index 0000000..e278a3a
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridge.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.settings.common.Logger;
+import com.android.internal.util.ArrayUtils;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Bridges {@link AppOpsManager} app operation permission information into {@link
+ * AppEntry#extraInfo} as {@link PermissionState} objects.
+ */
+public class AppStateAppOpsBridge extends AppStateBaseBridge {
+
+ private static final Logger LOG = new Logger(AppStateAppOpsBridge.class);
+
+ private final Context mContext;
+ private final IPackageManager mIPackageManager;
+ private final List<UserHandle> mProfiles;
+ private final AppOpsManager mAppOpsManager;
+ private final int mAppOpsOpCode;
+ private final String mPermission;
+
+ /**
+ * Constructor.
+ *
+ * @param appOpsOpCode the {@link AppOpsManager} op code constant to fetch information for.
+ * @param permission the {@link android.Manifest.permission} required to perform the
+ * operation.
+ * @param callback a {@link Callback} which will be notified when the information is
+ * finished loading.
+ */
+ public AppStateAppOpsBridge(Context context, ApplicationsState appState, int appOpsOpCode,
+ String permission, Callback callback) {
+ this(context, appState, appOpsOpCode, permission, callback, AppGlobals.getPackageManager());
+ }
+
+ @VisibleForTesting
+ AppStateAppOpsBridge(Context context, ApplicationsState appState, int appOpsOpCode,
+ String permission, Callback callback, IPackageManager packageManager) {
+ super(appState, callback);
+ mContext = context;
+ mIPackageManager = packageManager;
+ mProfiles = UserManager.get(context).getUserProfiles();
+ mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+ mAppOpsOpCode = appOpsOpCode;
+ mPermission = permission;
+ }
+
+ @Override
+ protected void loadExtraInfo(List<AppEntry> entries) {
+ SparseArray<Map<String, PermissionState>> packageToStatesMapByProfileId =
+ getPackageToStateMapsByProfileId();
+ loadAppOpModes(packageToStatesMapByProfileId);
+
+ for (AppEntry entry : entries) {
+ Map<String, PermissionState> packageStatesMap = packageToStatesMapByProfileId.get(
+ UserHandle.getUserId(entry.info.uid));
+ entry.extraInfo = (packageStatesMap != null) ? packageStatesMap.get(
+ entry.info.packageName) : null;
+ }
+ }
+
+ private SparseArray<Map<String, PermissionState>> getPackageToStateMapsByProfileId() {
+ SparseArray<Map<String, PermissionState>> entries = new SparseArray<>();
+ try {
+ for (UserHandle profile : mProfiles) {
+ int profileId = profile.getIdentifier();
+ List<PackageInfo> packageInfos = getPackageInfos(profileId);
+ Map<String, PermissionState> entriesForProfile = new ArrayMap<>();
+ entries.put(profileId, entriesForProfile);
+ for (PackageInfo packageInfo : packageInfos) {
+ boolean isAvailable = mIPackageManager.isPackageAvailable(
+ packageInfo.packageName,
+ profileId);
+ if (shouldIgnorePackage(packageInfo) || !isAvailable) {
+ LOG.d("Ignoring " + packageInfo.packageName + " isAvailable="
+ + isAvailable);
+ continue;
+ }
+ PermissionState newEntry = new PermissionState();
+ newEntry.mRequestedPermissions = packageInfo.requestedPermissions;
+ entriesForProfile.put(packageInfo.packageName, newEntry);
+ }
+ }
+ } catch (RemoteException e) {
+ LOG.w("PackageManager is dead. Can't get list of packages requesting "
+ + mPermission, e);
+ }
+ return entries;
+ }
+
+ @SuppressWarnings("unchecked") // safe by specification.
+ private List<PackageInfo> getPackageInfos(int profileId) throws RemoteException {
+ return mIPackageManager.getPackagesHoldingPermissions(new String[]{mPermission},
+ PackageManager.GET_PERMISSIONS, profileId).getList();
+ }
+
+ private boolean shouldIgnorePackage(PackageInfo packageInfo) {
+ return packageInfo.packageName.equals("android")
+ || packageInfo.packageName.equals(mContext.getPackageName())
+ || !ArrayUtils.contains(packageInfo.requestedPermissions, mPermission);
+ }
+
+ /** Sets the {@link PermissionState#mAppOpMode} field. */
+ private void loadAppOpModes(
+ SparseArray<Map<String, PermissionState>> packageToStateMapsByProfileId) {
+ // Find out which packages have been granted permission from AppOps.
+ List<AppOpsManager.PackageOps> packageOps = mAppOpsManager.getPackagesForOps(
+ new int[]{mAppOpsOpCode});
+ if (packageOps == null) {
+ return;
+ }
+ for (AppOpsManager.PackageOps packageOp : packageOps) {
+ int userId = UserHandle.getUserId(packageOp.getUid());
+ Map<String, PermissionState> packageStateMap = packageToStateMapsByProfileId.get(
+ userId);
+ if (packageStateMap == null) {
+ // Profile is not for the current user.
+ continue;
+ }
+ PermissionState permissionState = packageStateMap.get(packageOp.getPackageName());
+ if (permissionState == null) {
+ LOG.w("AppOp permission exists for package " + packageOp.getPackageName()
+ + " of user " + userId + " but package doesn't exist or did not request "
+ + mPermission + " access");
+ continue;
+ }
+ if (packageOp.getOps().size() < 1) {
+ LOG.w("No AppOps permission exists for package " + packageOp.getPackageName());
+ continue;
+ }
+ permissionState.mAppOpMode = packageOp.getOps().get(0).getMode();
+ }
+ }
+
+ /**
+ * Data class for use in {@link AppEntry#extraInfo} which indicates whether
+ * the app operation used to construct the data bridge is permitted for the associated
+ * application.
+ */
+ public static class PermissionState {
+ private String[] mRequestedPermissions;
+ private int mAppOpMode = AppOpsManager.MODE_DEFAULT;
+
+ /** Returns {@code true} if the entry's application is allowed to perform the operation. */
+ public boolean isPermissible() {
+ // Default behavior is permissible as long as the package requested this permission.
+ if (mAppOpMode == AppOpsManager.MODE_DEFAULT) {
+ return true;
+ }
+ return mAppOpMode == AppOpsManager.MODE_ALLOWED;
+ }
+
+ /** Returns the permissions requested by the entry's application. */
+ public String[] getRequestedPermissions() {
+ return mRequestedPermissions;
+ }
+ }
+}
diff --git a/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridgeTest.java b/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridgeTest.java
new file mode 100644
index 0000000..8c75c7c
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridgeTest.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.applications.specialaccess.AppStateAppOpsBridge.PermissionState;
+import com.android.car.settings.testutils.ShadowAppOpsManager;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.AdditionalMatchers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowUserManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Unit test for {@link AppStateAppOpsBridge}. */
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowAppOpsManager.class})
+public class AppStateAppOpsBridgeTest {
+
+ private static final int APP_OP_CODE = AppOpsManager.OP_WRITE_SETTINGS;
+ private static final String PERMISSION = Manifest.permission.WRITE_SETTINGS;
+
+ @Mock
+ private IPackageManager mIPackageManager;
+ @Mock
+ private ApplicationsState mApplicationsState;
+ @Mock
+ private ApplicationsState.Session mSession;
+ @Mock
+ private ParceledListSlice<PackageInfo> mParceledPackages;
+ @Mock
+ private ParceledListSlice<PackageInfo> mParceledPackagesOtherProfile;
+
+ private List<PackageInfo> mPackages;
+ private ArrayList<AppEntry> mAppEntries;
+
+ private Context mContext;
+ private AppOpsManager mAppOpsManager;
+ private AppStateAppOpsBridge mBridge;
+
+ @Before
+ public void setUp() throws RemoteException {
+ MockitoAnnotations.initMocks(this);
+ mPackages = new ArrayList<>();
+ when(mIPackageManager.getPackagesHoldingPermissions(
+ AdditionalMatchers.aryEq(new String[]{PERMISSION}),
+ eq(PackageManager.GET_PERMISSIONS),
+ eq(UserHandle.myUserId())))
+ .thenReturn(mParceledPackages);
+ when(mParceledPackages.getList()).thenReturn(mPackages);
+
+ mAppEntries = new ArrayList<>();
+ when(mApplicationsState.newSession(any())).thenReturn(mSession);
+ when(mApplicationsState.getBackgroundLooper()).thenReturn(Looper.getMainLooper());
+ when(mSession.getAllApps()).thenReturn(mAppEntries);
+
+ mContext = RuntimeEnvironment.application;
+ mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+ mBridge = new AppStateAppOpsBridge(mContext, mApplicationsState, APP_OP_CODE, PERMISSION,
+ mock(AppStateBaseBridge.Callback.class), mIPackageManager);
+ }
+
+ @Test
+ public void androidPackagesIgnored() throws RemoteException {
+ String packageName = "android";
+ int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
+ PackageInfo packageInfo = createPackageInfo(packageName, uid);
+ addPackageWithPermission(packageInfo, AppOpsManager.MODE_ALLOWED);
+ addEntry(packageInfo);
+
+ AppEntry entry = mAppEntries.get(0);
+
+ mBridge.start();
+
+ assertThat(entry.extraInfo).isNull();
+ }
+
+ @Test
+ public void selfPackageIgnored() throws RemoteException {
+ String packageName = mContext.getPackageName();
+ int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
+ PackageInfo packageInfo = createPackageInfo(packageName, uid);
+ addPackageWithPermission(packageInfo, AppOpsManager.MODE_ALLOWED);
+ addEntry(packageInfo);
+
+ AppEntry entry = mAppEntries.get(0);
+
+ mBridge.start();
+
+ assertThat(entry.extraInfo).isNull();
+ }
+
+ @Test
+ public void packagesNotRequestingPermissionIgnored() throws RemoteException {
+ String packageName = "test.package";
+ int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
+ PackageInfo packageInfo = createPackageInfo(packageName, uid);
+ packageInfo.requestedPermissions = null;
+ mPackages.add(packageInfo);
+ when(mIPackageManager.isPackageAvailable(packageInfo.packageName,
+ UserHandle.myUserId())).thenReturn(true);
+ addEntry(packageInfo);
+
+ AppEntry entry = mAppEntries.get(0);
+
+ mBridge.start();
+
+ assertThat(entry.extraInfo).isNull();
+ }
+
+ @Test
+ public void unavailablePackageIgnored() throws RemoteException {
+ String packageName = "test.package";
+ int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
+ PackageInfo packageInfo = createPackageInfo(packageName, uid);
+ addPackageWithPermission(packageInfo, AppOpsManager.MODE_ALLOWED);
+ addEntry(packageInfo);
+
+ when(mIPackageManager.isPackageAvailable(packageInfo.packageName,
+ UserHandle.myUserId())).thenReturn(false);
+
+ AppEntry entry = mAppEntries.get(0);
+
+ mBridge.start();
+
+ assertThat(entry.extraInfo).isNull();
+ }
+
+ @Test
+ public void loadsAppOpsExtraInfo_modeAllowed_isPermissible() throws RemoteException {
+ String packageName = "test.package";
+ int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
+ PackageInfo packageInfo = createPackageInfo(packageName, uid);
+ addPackageWithPermission(packageInfo, AppOpsManager.MODE_ALLOWED);
+ addEntry(packageInfo);
+
+ AppEntry entry = mAppEntries.get(0);
+ assertThat(entry.extraInfo).isNull();
+
+ mBridge.start();
+
+ assertThat(entry.extraInfo).isNotNull();
+ assertThat(((PermissionState) entry.extraInfo).isPermissible()).isTrue();
+ }
+
+ @Test
+ public void loadsAppOpsExtraInfo_modeDefault_isPermissible() throws RemoteException {
+ String packageName = "test.package";
+ int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
+ PackageInfo packageInfo = createPackageInfo(packageName, uid);
+ addPackageWithPermission(packageInfo, AppOpsManager.MODE_DEFAULT);
+ addEntry(packageInfo);
+
+ AppEntry entry = mAppEntries.get(0);
+ assertThat(entry.extraInfo).isNull();
+
+ mBridge.start();
+
+ assertThat(entry.extraInfo).isNotNull();
+ assertThat(((PermissionState) entry.extraInfo).isPermissible()).isTrue();
+ }
+
+ @Test
+ public void loadsAppOpsExtraInfo_modeIgnored_isNotPermissible() throws RemoteException {
+ String packageName = "test.package";
+ int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
+ PackageInfo packageInfo = createPackageInfo(packageName, uid);
+ addPackageWithPermission(packageInfo, AppOpsManager.MODE_IGNORED);
+ addEntry(packageInfo);
+
+ AppEntry entry = mAppEntries.get(0);
+ assertThat(entry.extraInfo).isNull();
+
+ mBridge.start();
+
+ assertThat(entry.extraInfo).isNotNull();
+ assertThat(((PermissionState) entry.extraInfo).isPermissible()).isFalse();
+ }
+
+ @Test
+ public void loadsAppOpsExtraInfo_multipleApps() throws RemoteException {
+ String packageName1 = "test.package1";
+ int uid1 = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
+ PackageInfo packageInfo1 = createPackageInfo(packageName1, uid1);
+ addPackageWithPermission(packageInfo1, AppOpsManager.MODE_ALLOWED);
+ addEntry(packageInfo1);
+
+ String packageName2 = "test.package2";
+ int uid2 = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 2);
+ PackageInfo packageInfo2 = createPackageInfo(packageName2, uid2);
+ addPackageWithPermission(packageInfo2, AppOpsManager.MODE_ALLOWED);
+ addEntry(packageInfo2);
+
+ AppEntry entry1 = mAppEntries.get(0);
+ AppEntry entry2 = mAppEntries.get(1);
+
+ mBridge.start();
+
+ assertThat(entry1.extraInfo).isNotNull();
+ assertThat(entry2.extraInfo).isNotNull();
+ }
+
+ @Test
+ public void loadsAppOpExtraInfo_multipleProfiles() throws RemoteException {
+ String packageName1 = "test.package1";
+ int uid1 = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
+ PackageInfo packageInfo1 = createPackageInfo(packageName1, uid1);
+ addPackageWithPermission(packageInfo1, AppOpsManager.MODE_ALLOWED);
+ addEntry(packageInfo1);
+
+ // Add a package for another profile.
+ int otherUserId = UserHandle.myUserId() + 1;
+ String packageName2 = "test.package2";
+ int uid2 = UserHandle.getUid(otherUserId, /* appId= */ 2);
+ PackageInfo packageInfo2 = createPackageInfo(packageName2, uid2);
+ when(mIPackageManager.getPackagesHoldingPermissions(
+ AdditionalMatchers.aryEq(new String[]{PERMISSION}),
+ eq(PackageManager.GET_PERMISSIONS),
+ eq(otherUserId)))
+ .thenReturn(mParceledPackagesOtherProfile);
+ when(mParceledPackagesOtherProfile.getList()).thenReturn(
+ Collections.singletonList(packageInfo2));
+ when(mIPackageManager.isPackageAvailable(packageInfo2.packageName,
+ otherUserId)).thenReturn(true);
+ mAppOpsManager.setMode(APP_OP_CODE, packageInfo2.applicationInfo.uid,
+ packageInfo2.packageName, AppOpsManager.MODE_ALLOWED);
+ addEntry(packageInfo2);
+
+ AppEntry entry1 = mAppEntries.get(0);
+ AppEntry entry2 = mAppEntries.get(1);
+
+ getShadowUserManager().addUserProfile(UserHandle.of(otherUserId));
+ // Recreate the bridge so it has all user profiles.
+ mBridge = new AppStateAppOpsBridge(mContext, mApplicationsState, APP_OP_CODE, PERMISSION,
+ mock(AppStateBaseBridge.Callback.class), mIPackageManager);
+
+ mBridge.start();
+
+ assertThat(entry1.extraInfo).isNotNull();
+ assertThat(entry2.extraInfo).isNotNull();
+ }
+
+ @Test
+ public void appEntryNotIncluded_extraInfoCleared() {
+ String packageName = "test.package";
+ int uid = UserHandle.getUid(UserHandle.myUserId(), /* appId= */ 1);
+ PackageInfo packageInfo = createPackageInfo(packageName, uid);
+ addEntry(packageInfo);
+
+ AppEntry entry = mAppEntries.get(0);
+ entry.extraInfo = new Object();
+
+ mBridge.start();
+
+ assertThat(entry.extraInfo).isNull();
+ }
+
+ private PackageInfo createPackageInfo(String packageName, int uid) {
+ ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.packageName = packageName;
+ applicationInfo.uid = uid;
+
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.packageName = packageName;
+ packageInfo.applicationInfo = applicationInfo;
+ packageInfo.requestedPermissions = new String[]{PERMISSION};
+
+ return packageInfo;
+ }
+
+ private void addPackageWithPermission(PackageInfo packageInfo, int mode)
+ throws RemoteException {
+ mPackages.add(packageInfo);
+ when(mIPackageManager.isPackageAvailable(packageInfo.packageName,
+ UserHandle.myUserId())).thenReturn(true);
+ mAppOpsManager.setMode(APP_OP_CODE, packageInfo.applicationInfo.uid,
+ packageInfo.packageName, mode);
+ }
+
+ private void addEntry(PackageInfo packageInfo) {
+ AppEntry appEntry = mock(AppEntry.class);
+ appEntry.info = packageInfo.applicationInfo;
+ mAppEntries.add(appEntry);
+ }
+
+ private ShadowUserManager getShadowUserManager() {
+ return Shadows.shadowOf(UserManager.get(mContext));
+ }
+}
diff --git a/tests/robotests/src/com/android/car/settings/testutils/ShadowAppOpsManager.java b/tests/robotests/src/com/android/car/settings/testutils/ShadowAppOpsManager.java
new file mode 100644
index 0000000..9e2ca4f
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/testutils/ShadowAppOpsManager.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2019 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.car.settings.testutils;
+
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.OpEntry;
+import android.app.AppOpsManager.PackageOps;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Table;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@Implements(value = AppOpsManager.class)
+public class ShadowAppOpsManager {
+
+ private Table<Integer, InternalKey, Integer> mOpToKeyToMode = HashBasedTable.create();
+
+ @Implementation
+ protected void setMode(int code, int uid, String packageName, int mode) {
+ InternalKey key = new InternalKey(uid, packageName);
+ mOpToKeyToMode.put(code, key, mode);
+ }
+
+ @Implementation
+ protected List<PackageOps> getPackagesForOps(int[] ops) {
+ if (ops == null) {
+ return Collections.emptyList();
+ }
+ ImmutableList.Builder<PackageOps> result = new ImmutableList.Builder<>();
+ for (int i = 0; i < ops.length; i++) {
+ int op = ops[i];
+ Map<InternalKey, Integer> keyToModeMap = mOpToKeyToMode.rowMap().get(op);
+ if (keyToModeMap == null) {
+ continue;
+ }
+ for (InternalKey key : keyToModeMap.keySet()) {
+ Integer mode = keyToModeMap.get(key);
+ if (mode == null) {
+ mode = AppOpsManager.opToDefaultMode(op);
+ }
+ OpEntry opEntry = new OpEntry(op, mode);
+ PackageOps packageOp = new PackageOps(key.mPackageName, key.mUid,
+ Collections.singletonList(opEntry));
+ result.add(packageOp);
+ }
+ }
+ return result.build();
+ }
+
+ private static class InternalKey {
+ private int mUid;
+ private String mPackageName;
+
+ InternalKey(int uid, String packageName) {
+ mUid = uid;
+ mPackageName = packageName;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof InternalKey) {
+ InternalKey that = (InternalKey) obj;
+ return mUid == that.mUid && mPackageName.equals(that.mPackageName);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mUid, mPackageName);
+ }
+ }
+}