Make provider icon sizes consistent

Fixes an issue where the icons under "additional providers"
are not the same size. Also, fixes the unit tests for
CredentialManagerPreferenceController.

https://hsv.googleplex.com/4797855484084224

Test: unit tests and manual on device tests
Bug: 278772478
Change-Id: I88d8694189a7529ccc2d92bf1b74bb5bf2268f46
diff --git a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
index b0905ba..97a7f16 100644
--- a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
+++ b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
@@ -34,6 +34,7 @@
 import android.credentials.CredentialManager;
 import android.credentials.CredentialProviderInfo;
 import android.credentials.SetEnabledProvidersException;
+import android.credentials.flags.Flags;
 import android.database.ContentObserver;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
@@ -108,6 +109,7 @@
     private final List<ServiceInfo> mPendingServiceInfos = new ArrayList<>();
     private final Handler mHandler = new Handler();
     private final SettingContentObserver mSettingsContentObserver;
+    private final ImageUtils.IconResizer mIconResizer;
 
     private @Nullable FragmentManager mFragmentManager = null;
     private @Nullable Delegate mDelegate = null;
@@ -116,6 +118,7 @@
 
     private boolean mVisibility = false;
     private boolean mIsWorkProfile = false;
+    private boolean mSimulateConnectedForTests = false;
 
     public CredentialManagerPreferenceController(Context context, String preferenceKey) {
         super(context, preferenceKey);
@@ -129,6 +132,13 @@
                 new SettingContentObserver(mHandler, context.getContentResolver());
         mSettingsContentObserver.register();
         mSettingsPackageMonitor.register(context, context.getMainLooper(), false);
+        mIconResizer = getResizer(context);
+    }
+
+    private static ImageUtils.IconResizer getResizer(Context context) {
+        final Resources resources = context.getResources();
+        int size = (int) resources.getDimension(android.R.dimen.app_icon_size);
+        return new ImageUtils.IconResizer(size, size, resources.getDisplayMetrics());
     }
 
     private @Nullable CredentialManager getCredentialManager(Context context, boolean isTest) {
@@ -147,7 +157,7 @@
 
     @Override
     public int getAvailabilityStatus() {
-        if (mCredentialManager == null) {
+        if (!isConnected()) {
             return UNSUPPORTED_ON_DEVICE;
         }
 
@@ -174,7 +184,11 @@
 
     @VisibleForTesting
     public boolean isConnected() {
-        return mCredentialManager != null;
+        return mCredentialManager != null || mSimulateConnectedForTests;
+    }
+
+    public void setSimulateConnectedForTests(boolean simulateConnectedForTests) {
+        mSimulateConnectedForTests = simulateConnectedForTests;
     }
 
     /**
@@ -293,7 +307,7 @@
 
         NewProviderConfirmationDialogFragment fragment =
                 newNewProviderConfirmationDialogFragment(
-                        serviceInfo.packageName, appName, /* setActivityResult= */ true);
+                        serviceInfo.packageName, appName, /* shouldSetActivityResult= */ true);
         if (fragment == null || mFragmentManager == null) {
             return;
         }
@@ -365,7 +379,8 @@
         }
     }
 
-    private void setVisibility(boolean newVisibility) {
+    @VisibleForTesting
+    public void setVisibility(boolean newVisibility) {
         if (newVisibility == mVisibility) {
             return;
         }
@@ -377,6 +392,11 @@
     }
 
     @VisibleForTesting
+    public boolean getVisibility() {
+        return mVisibility;
+    }
+
+    @VisibleForTesting
     void setAvailableServices(
             List<CredentialProviderInfo> availableServices, String flagOverrideForTest) {
         mFlagOverrideForTest = flagOverrideForTest;
@@ -574,6 +594,17 @@
         return enabledServices;
     }
 
+    @VisibleForTesting
+    public @NonNull Drawable processIcon(@Nullable Drawable icon) {
+        // If we didn't get an icon then we should use the default app icon.
+        if (icon == null) {
+            icon = mPm.getDefaultActivityIcon();
+        }
+
+        Drawable providerIcon = Utils.getSafeIcon(icon);
+        return mIconResizer.createIconThumbnail(providerIcon);
+    }
+
     private CombiPreference addProviderPreference(
             @NonNull Context prefContext,
             @NonNull CharSequence title,
@@ -584,13 +615,14 @@
         final CombiPreference pref =
                 new CombiPreference(prefContext, mEnabledPackageNames.contains(packageName));
         pref.setTitle(title);
+        pref.setLayoutResource(R.layout.preference_icon_credman);
 
-        if (icon != null) {
+        if (Flags.newSettingsUi()) {
+            pref.setIcon(processIcon(icon));
+        } else if (icon != null) {
             pref.setIcon(icon);
         }
 
-        pref.setLayoutResource(R.layout.preference_icon_credman);
-
         if (subtitle != null) {
             pref.setSummary(subtitle);
         }
@@ -711,13 +743,13 @@
             newNewProviderConfirmationDialogFragment(
                     @NonNull String packageName,
                     @NonNull CharSequence appName,
-                    boolean setActivityResult) {
+                    boolean shouldSetActivityResult) {
         DialogHost host =
                 new DialogHost() {
                     @Override
                     public void onDialogClick(int whichButton) {
                         completeEnableProviderDialogBox(
-                                whichButton, packageName, setActivityResult);
+                                whichButton, packageName, shouldSetActivityResult);
                     }
 
                     @Override
@@ -728,8 +760,8 @@
     }
 
     @VisibleForTesting
-    void completeEnableProviderDialogBox(
-            int whichButton, String packageName, boolean setActivityResult) {
+    int completeEnableProviderDialogBox(
+            int whichButton, String packageName, boolean shouldSetActivityResult) {
         int activityResult = -1;
         if (whichButton == DialogInterface.BUTTON_POSITIVE) {
             if (togglePackageNameEnabled(packageName)) {
@@ -746,7 +778,7 @@
                 final DialogFragment fragment = newErrorDialogFragment();
 
                 if (fragment == null || mFragmentManager == null) {
-                    return;
+                    return activityResult;
                 }
 
                 fragment.show(mFragmentManager, ErrorDialogFragment.TAG);
@@ -758,9 +790,11 @@
 
         // If the dialog is being shown because of the intent we should
         // return a result.
-        if (activityResult == -1 || !setActivityResult) {
+        if (activityResult == -1 || !shouldSetActivityResult) {
             setActivityResult(activityResult);
         }
+
+        return activityResult;
     }
 
     private @Nullable ErrorDialogFragment newErrorDialogFragment() {
@@ -1002,6 +1036,7 @@
             }
         }
 
+        @VisibleForTesting
         public boolean isChecked() {
             return mChecked;
         }
diff --git a/src/com/android/settings/applications/credentials/ImageUtils.java b/src/com/android/settings/applications/credentials/ImageUtils.java
new file mode 100644
index 0000000..a7803a8
--- /dev/null
+++ b/src/com/android/settings/applications/credentials/ImageUtils.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2024 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.settings.applications.credentials;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PaintFlagsDrawFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.PaintDrawable;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.NonNull;
+
+/** Handles resizing of images for CredMan settings. */
+public class ImageUtils {
+
+    /**
+     * Utility class to resize icons to match default icon size. Code is mostly borrowed from
+     * Launcher and ActivityPicker.
+     */
+    public static class IconResizer {
+        private final int mIconWidth;
+        private final int mIconHeight;
+
+        private final DisplayMetrics mMetrics;
+        private final Rect mOldBounds = new Rect();
+        private final Canvas mCanvas = new Canvas();
+
+        public IconResizer(int width, int height, DisplayMetrics metrics) {
+            mCanvas.setDrawFilter(
+                    new PaintFlagsDrawFilter(Paint.DITHER_FLAG, Paint.FILTER_BITMAP_FLAG));
+
+            mMetrics = metrics;
+            mIconWidth = width;
+            mIconHeight = height;
+        }
+
+        /**
+         * Returns a Drawable representing the thumbnail of the specified Drawable. The size of the
+         * thumbnail is defined by the dimension android.R.dimen.app_icon_size.
+         *
+         * <p>This method is not thread-safe and should be invoked on the UI thread only.
+         *
+         * @param icon The icon to get a thumbnail of.
+         * @return A thumbnail for the specified icon or the icon itself if the thumbnail could not
+         *     be created.
+         */
+        public Drawable createIconThumbnail(Drawable icon) {
+            int width = mIconWidth;
+            int height = mIconHeight;
+
+            if (icon == null) {
+                return new EmptyDrawable(width, height);
+            }
+
+            try {
+                if (icon instanceof PaintDrawable) {
+                    PaintDrawable painter = (PaintDrawable) icon;
+                    painter.setIntrinsicWidth(width);
+                    painter.setIntrinsicHeight(height);
+                } else if (icon instanceof BitmapDrawable) {
+                    // Ensure the bitmap has a density.
+                    BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
+                    Bitmap bitmap = bitmapDrawable.getBitmap();
+                    if (bitmap.getDensity() == Bitmap.DENSITY_NONE) {
+                        bitmapDrawable.setTargetDensity(mMetrics);
+                    }
+                }
+                int iconWidth = icon.getIntrinsicWidth();
+                int iconHeight = icon.getIntrinsicHeight();
+
+                if (iconWidth > 0 && iconHeight > 0) {
+                    if (width < iconWidth || height < iconHeight) {
+                        final float ratio = (float) iconWidth / iconHeight;
+
+                        if (iconWidth > iconHeight) {
+                            height = (int) (width / ratio);
+                        } else if (iconHeight > iconWidth) {
+                            width = (int) (height * ratio);
+                        }
+
+                        final Bitmap.Config c =
+                                icon.getOpacity() != PixelFormat.OPAQUE
+                                        ? Bitmap.Config.ARGB_8888
+                                        : Bitmap.Config.RGB_565;
+                        final Bitmap thumb = Bitmap.createBitmap(mIconWidth, mIconHeight, c);
+                        final Canvas canvas = mCanvas;
+                        canvas.setBitmap(thumb);
+
+                        // Copy the old bounds to restore them later
+                        // If we were to do oldBounds = icon.getBounds(),
+                        // the call to setBounds() that follows would
+                        // change the same instance and we would lose the
+                        // old bounds.
+                        mOldBounds.set(icon.getBounds());
+                        final int x = (mIconWidth - width) / 2;
+                        final int y = (mIconHeight - height) / 2;
+                        icon.setBounds(x, y, x + width, y + height);
+                        icon.draw(canvas);
+                        icon.setBounds(mOldBounds);
+
+                        // Create the new resized drawable.
+                        icon = createBitmapDrawable(thumb);
+                    } else if (iconWidth < width && iconHeight < height) {
+                        final Bitmap.Config c = Bitmap.Config.ARGB_8888;
+                        final Bitmap thumb = Bitmap.createBitmap(mIconWidth, mIconHeight, c);
+                        final Canvas canvas = mCanvas;
+                        canvas.setBitmap(thumb);
+                        mOldBounds.set(icon.getBounds());
+
+                        // Set the bounds for the new icon.
+                        final int x = (width - iconWidth) / 2;
+                        final int y = (height - iconHeight) / 2;
+                        icon.setBounds(x, y, x + iconWidth, y + iconHeight);
+                        icon.draw(canvas);
+                        icon.setBounds(mOldBounds);
+
+                        // Create the new resized drawable.
+                        icon = createBitmapDrawable(thumb);
+                    }
+                }
+
+            } catch (Throwable t) {
+                icon = new EmptyDrawable(width, height);
+            }
+
+            return icon;
+        }
+
+        private BitmapDrawable createBitmapDrawable(Bitmap thumb) {
+            BitmapDrawable icon = new BitmapDrawable(thumb);
+            icon.setTargetDensity(mMetrics);
+            mCanvas.setBitmap(null);
+            return icon;
+        }
+    }
+
+    public static class EmptyDrawable extends Drawable {
+        private final int mWidth;
+        private final int mHeight;
+
+        EmptyDrawable(int width, int height) {
+            mWidth = width;
+            mHeight = height;
+        }
+
+        @Override
+        public int getIntrinsicWidth() {
+            return mWidth;
+        }
+
+        @Override
+        public int getIntrinsicHeight() {
+            return mHeight;
+        }
+
+        @Override
+        public int getMinimumWidth() {
+            return mWidth;
+        }
+
+        @Override
+        public int getMinimumHeight() {
+            return mHeight;
+        }
+
+        @Override
+        public void draw(@NonNull Canvas canvas) {}
+
+        @Override
+        public void setAlpha(int alpha) {}
+
+        @Override
+        public void setColorFilter(@NonNull ColorFilter cf) {}
+
+        @Override
+        public int getOpacity() {
+            return PixelFormat.TRANSLUCENT;
+        }
+    }
+}
diff --git a/tests/unit/res/drawable/credman_icon_1_1.xml b/tests/unit/res/drawable/credman_icon_1_1.xml
new file mode 100644
index 0000000..49f2d2e
--- /dev/null
+++ b/tests/unit/res/drawable/credman_icon_1_1.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="#ff0000"/>
+    <size android:width="1dp" android:height="1dp" />
+</shape>
diff --git a/tests/unit/res/drawable/credman_icon_32_32.xml b/tests/unit/res/drawable/credman_icon_32_32.xml
new file mode 100644
index 0000000..2612ed2
--- /dev/null
+++ b/tests/unit/res/drawable/credman_icon_32_32.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="#ff0000"/>
+    <size android:width="128dp" android:height="128dp" />
+</shape>
diff --git a/tests/unit/src/com/android/settings/applications/credentials/CredentialManagerPreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/credentials/CredentialManagerPreferenceControllerTest.java
index 7d400ad..acf590b 100644
--- a/tests/unit/src/com/android/settings/applications/credentials/CredentialManagerPreferenceControllerTest.java
+++ b/tests/unit/src/com/android/settings/applications/credentials/CredentialManagerPreferenceControllerTest.java
@@ -17,8 +17,10 @@
 package com.android.settings.applications.credentials;
 
 import static com.android.settings.core.BasePreferenceController.AVAILABLE;
-import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Mockito.spy;
 
 import android.app.Activity;
@@ -27,7 +29,9 @@
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
 import android.credentials.CredentialProviderInfo;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Looper;
 import android.provider.Settings;
@@ -38,6 +42,9 @@
 import androidx.preference.PreferenceScreen;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.settings.tests.unit.R;
 
 import com.google.android.collect.Lists;
 
@@ -48,7 +55,6 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
@@ -58,14 +64,13 @@
     private PreferenceScreen mScreen;
     private PreferenceCategory mCredentialsPreferenceCategory;
     private CredentialManagerPreferenceController.Delegate mDelegate;
-    private Optional<Integer> mReceivedResultCode;
 
     private static final String TEST_PACKAGE_NAME_A = "com.android.providerA";
     private static final String TEST_PACKAGE_NAME_B = "com.android.providerB";
     private static final String TEST_PACKAGE_NAME_C = "com.android.providerC";
     private static final String TEST_TITLE_APP_A = "test app A";
     private static final String TEST_TITLE_APP_B = "test app B";
-    private static final String TEST_TITLE_SERVICE_C = "test service C1";
+    private static final String TEST_TITLE_APP_C = "test app C1";
     private static final String PRIMARY_INTENT = "android.settings.CREDENTIAL_PROVIDER";
     private static final String ALTERNATE_INTENT = "android.settings.SYNC_SETTINGS";
 
@@ -79,12 +84,9 @@
         mCredentialsPreferenceCategory = new PreferenceCategory(mContext);
         mCredentialsPreferenceCategory.setKey("credentials_test");
         mScreen.addPreference(mCredentialsPreferenceCategory);
-        mReceivedResultCode = Optional.empty();
         mDelegate =
                 new CredentialManagerPreferenceController.Delegate() {
-                    public void setActivityResult(int resultCode) {
-                        mReceivedResultCode = Optional.of(resultCode);
-                    }
+                    public void setActivityResult(int resultCode) {}
 
                     public void forceDelegateRefresh() {}
                 };
@@ -94,11 +96,15 @@
     // Tests that getAvailabilityStatus() does not throw an exception if it's called before the
     // Controller is initialized (this can happen during indexing).
     public void getAvailabilityStatus_withoutInit_returnsUnavailable() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare(); // needed to create the preference screen
+        }
+
         CredentialManagerPreferenceController controller =
                 new CredentialManagerPreferenceController(
                         mContext, mCredentialsPreferenceCategory.getKey());
         assertThat(controller.isConnected()).isFalse();
-        assertThat(controller.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
+        assertThat(controller.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
     }
 
     @Test
@@ -106,14 +112,17 @@
         CredentialManagerPreferenceController controller =
                 createControllerWithServices(Collections.emptyList());
         assertThat(controller.isConnected()).isFalse();
-        assertThat(controller.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
+        assertThat(controller.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
     }
 
     @Test
     public void getAvailabilityStatus_withServices_returnsAvailable() {
         CredentialManagerPreferenceController controller =
                 createControllerWithServices(Lists.newArrayList(createCredentialProviderInfo()));
-        assertThat(controller.isConnected()).isFalse();
+        controller.setSimulateConnectedForTests(true);
+        assertThat(controller.isConnected()).isTrue();
+        controller.setVisibility(true);
+        assertThat(controller.getVisibility()).isTrue();
         assertThat(controller.getAvailabilityStatus()).isEqualTo(AVAILABLE);
     }
 
@@ -139,8 +148,11 @@
                         "com.android.provider2", "ClassA", "Service Title", "Summary Text");
         CredentialManagerPreferenceController controller =
                 createControllerWithServices(Lists.newArrayList(providerInfo1, providerInfo2));
+        controller.setSimulateConnectedForTests(true);
+        assertThat(controller.isConnected()).isTrue();
+        controller.setVisibility(true);
+        assertThat(controller.getVisibility()).isTrue();
         assertThat(controller.getAvailabilityStatus()).isEqualTo(AVAILABLE);
-        assertThat(controller.isConnected()).isFalse();
 
         // Test the data is correct.
         assertThat(providerInfo1.isEnabled()).isFalse();
@@ -180,8 +192,11 @@
                                 createCredentialProviderInfo("com.android.provider4", "ClassA"),
                                 createCredentialProviderInfo("com.android.provider5", "ClassA"),
                                 createCredentialProviderInfo("com.android.provider6", "ClassA")));
+        controller.setSimulateConnectedForTests(true);
+        assertThat(controller.isConnected()).isTrue();
+        controller.setVisibility(true);
+        assertThat(controller.getVisibility()).isTrue();
         assertThat(controller.getAvailabilityStatus()).isEqualTo(AVAILABLE);
-        assertThat(controller.isConnected()).isFalse();
 
         // Ensure that we stay under 5 providers.
         assertThat(controller.togglePackageNameEnabled("com.android.provider1")).isTrue();
@@ -246,8 +261,11 @@
                         "com.android.provider2", "ClassA", "Service Title", true);
         CredentialManagerPreferenceController controller =
                 createControllerWithServices(Lists.newArrayList(providerInfo1, providerInfo2));
+        controller.setSimulateConnectedForTests(true);
+        assertThat(controller.isConnected()).isTrue();
+        controller.setVisibility(true);
+        assertThat(controller.getVisibility()).isTrue();
         assertThat(controller.getAvailabilityStatus()).isEqualTo(AVAILABLE);
-        assertThat(controller.isConnected()).isFalse();
 
         // Test the data is correct.
         assertThat(providerInfo1.isEnabled()).isFalse();
@@ -282,22 +300,16 @@
                         "test service B");
         CredentialProviderInfo serviceC1 =
                 createCredentialProviderInfoWithAppLabel(
-                        TEST_PACKAGE_NAME_C,
-                        "CredManProviderC1",
-                        "test app C1",
-                        TEST_TITLE_SERVICE_C);
+                        TEST_PACKAGE_NAME_C, "CredManProviderC1", "test app C1", TEST_TITLE_APP_C);
         CredentialProviderInfo serviceC2 =
                 createCredentialProviderInfoWithAppLabel(
-                        TEST_PACKAGE_NAME_C,
-                        "CredManProviderC2",
-                        "test app C2",
-                        TEST_TITLE_SERVICE_C);
+                        TEST_PACKAGE_NAME_C, "CredManProviderC2", "test app C2", TEST_TITLE_APP_C);
         CredentialProviderInfo serviceC3 =
                 createCredentialProviderInfoBuilder(
                                 TEST_PACKAGE_NAME_C,
                                 "CredManProviderC3",
                                 "test app C3",
-                                TEST_TITLE_SERVICE_C)
+                                TEST_TITLE_APP_C)
                         .setEnabled(true)
                         .build();
 
@@ -321,7 +333,7 @@
         assertThat(prefs.get(TEST_PACKAGE_NAME_B).getTitle()).isEqualTo(TEST_TITLE_APP_B);
         assertThat(prefs.get(TEST_PACKAGE_NAME_B).isChecked()).isFalse();
         assertThat(prefs.containsKey(TEST_PACKAGE_NAME_C)).isTrue();
-        assertThat(prefs.get(TEST_PACKAGE_NAME_C).getTitle()).isEqualTo(TEST_TITLE_SERVICE_C);
+        assertThat(prefs.get(TEST_PACKAGE_NAME_C).getTitle()).isEqualTo(TEST_TITLE_APP_C);
         assertThat(prefs.get(TEST_PACKAGE_NAME_C).isChecked()).isTrue();
     }
 
@@ -347,9 +359,10 @@
         Intent intent = new Intent(PRIMARY_INTENT);
         intent.setData(Uri.parse("package:" + packageName));
         assertThat(controller.verifyReceivedIntent(intent)).isTrue();
-        controller.completeEnableProviderDialogBox(
-                DialogInterface.BUTTON_POSITIVE, packageName, true);
-        assertThat(mReceivedResultCode.get()).isEqualTo(Activity.RESULT_OK);
+        int resultCode =
+                controller.completeEnableProviderDialogBox(
+                        DialogInterface.BUTTON_POSITIVE, packageName, true);
+        assertThat(resultCode).isEqualTo(Activity.RESULT_OK);
     }
 
     @Test
@@ -363,9 +376,10 @@
         Intent intent = new Intent(PRIMARY_INTENT);
         intent.setData(Uri.parse("package:" + packageName));
         assertThat(controller.verifyReceivedIntent(intent)).isTrue();
-        controller.completeEnableProviderDialogBox(
-                DialogInterface.BUTTON_NEGATIVE, packageName, true);
-        assertThat(mReceivedResultCode.get()).isEqualTo(Activity.RESULT_CANCELED);
+        int resultCode =
+                controller.completeEnableProviderDialogBox(
+                        DialogInterface.BUTTON_NEGATIVE, packageName, true);
+        assertThat(resultCode).isEqualTo(Activity.RESULT_CANCELED);
     }
 
     @Test
@@ -390,9 +404,10 @@
         Intent intent = new Intent(ALTERNATE_INTENT);
         intent.setData(Uri.parse("package:" + packageName));
         assertThat(controller.verifyReceivedIntent(intent)).isTrue();
-        controller.completeEnableProviderDialogBox(
-                DialogInterface.BUTTON_POSITIVE, packageName, true);
-        assertThat(mReceivedResultCode.get()).isEqualTo(Activity.RESULT_OK);
+        int resultCode =
+                controller.completeEnableProviderDialogBox(
+                        DialogInterface.BUTTON_POSITIVE, packageName, true);
+        assertThat(resultCode).isEqualTo(Activity.RESULT_OK);
     }
 
     @Test
@@ -406,9 +421,10 @@
         Intent intent = new Intent(ALTERNATE_INTENT);
         intent.setData(Uri.parse("package:" + packageName));
         assertThat(controller.verifyReceivedIntent(intent)).isTrue();
-        controller.completeEnableProviderDialogBox(
-                DialogInterface.BUTTON_NEGATIVE, packageName, true);
-        assertThat(mReceivedResultCode.get()).isEqualTo(Activity.RESULT_CANCELED);
+        int resultCode =
+                controller.completeEnableProviderDialogBox(
+                        DialogInterface.BUTTON_NEGATIVE, packageName, true);
+        assertThat(resultCode).isEqualTo(Activity.RESULT_CANCELED);
     }
 
     @Test
@@ -422,7 +438,6 @@
         Intent intent = new Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE);
         intent.setData(Uri.parse("package:" + packageName));
         assertThat(controller.verifyReceivedIntent(intent)).isFalse();
-        assertThat(mReceivedResultCode.isPresent()).isFalse();
     }
 
     @Test
@@ -433,7 +448,65 @@
 
         // Use a null intent.
         assertThat(controller.verifyReceivedIntent(null)).isFalse();
-        assertThat(mReceivedResultCode.isPresent()).isFalse();
+    }
+
+    @Test
+    public void testIconResizer_resizeLargeImage() throws Throwable {
+        CredentialProviderInfo cpi = createCredentialProviderInfo();
+        CredentialManagerPreferenceController controller =
+                createControllerWithServices(Lists.newArrayList(cpi));
+
+        final Drawable d =
+                InstrumentationRegistry.getInstrumentation()
+                        .getContext()
+                        .getResources()
+                        .getDrawable(R.drawable.credman_icon_32_32);
+        assertThat(d).isNotNull();
+        assertThat(d.getIntrinsicHeight() >= 0).isTrue();
+        assertThat(d.getIntrinsicWidth() >= 0).isTrue();
+
+        Drawable thumbnail = controller.processIcon(d);
+        assertThat(thumbnail).isNotNull();
+        assertThat(thumbnail.getIntrinsicHeight()).isEqualTo(getIconSize());
+        assertThat(thumbnail.getIntrinsicWidth()).isEqualTo(getIconSize());
+    }
+
+    @Test
+    public void testIconResizer_resizeNullImage() throws Throwable {
+        CredentialProviderInfo cpi = createCredentialProviderInfo();
+        CredentialManagerPreferenceController controller =
+                createControllerWithServices(Lists.newArrayList(cpi));
+
+        Drawable thumbnail = controller.processIcon(null);
+        assertThat(thumbnail).isNotNull();
+        assertThat(thumbnail.getIntrinsicHeight()).isEqualTo(getIconSize());
+        assertThat(thumbnail.getIntrinsicWidth()).isEqualTo(getIconSize());
+    }
+
+    @Test
+    public void testIconResizer_resizeSmallImage() throws Throwable {
+        CredentialProviderInfo cpi = createCredentialProviderInfo();
+        CredentialManagerPreferenceController controller =
+                createControllerWithServices(Lists.newArrayList(cpi));
+
+        final Drawable d =
+                InstrumentationRegistry.getInstrumentation()
+                        .getContext()
+                        .getResources()
+                        .getDrawable(R.drawable.credman_icon_1_1);
+        assertThat(d).isNotNull();
+        assertThat(d.getIntrinsicHeight() >= 0).isTrue();
+        assertThat(d.getIntrinsicWidth() >= 0).isTrue();
+
+        Drawable thumbnail = controller.processIcon(null);
+        assertThat(thumbnail).isNotNull();
+        assertThat(thumbnail.getIntrinsicHeight()).isEqualTo(getIconSize());
+        assertThat(thumbnail.getIntrinsicWidth()).isEqualTo(getIconSize());
+    }
+
+    private int getIconSize() {
+        final Resources resources = mContext.getResources();
+        return (int) resources.getDimension(android.R.dimen.app_icon_size);
     }
 
     private CredentialManagerPreferenceController createControllerWithServices(
@@ -443,6 +516,10 @@
 
     private CredentialManagerPreferenceController createControllerWithServicesAndAddServiceOverride(
             List<CredentialProviderInfo> availableServices, String addServiceOverride) {
+        if (Looper.myLooper() == null) {
+            Looper.prepare(); // needed to create the preference screen
+        }
+
         CredentialManagerPreferenceController controller =
                 new CredentialManagerPreferenceController(
                         mContext, mCredentialsPreferenceCategory.getKey());