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