Custom Theme 10/n: check for existing theme

If the currently defined custom theme is equivalent to an
existing one, suggest using that one instead of creating a
new custom one.

Bug: 124796742
Change-Id: Ib8436d129f359a53f284f907a566dd16293df6ef
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 01f57d6..b8a3e28 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -148,4 +148,22 @@
     <!-- Label for a dialog which asks the user the destination (home screen, lock screen, both)
         where to set the theme's bundled image as wallpaper. [CHAR LIMIT=50] -->
     <string name="set_theme_wallpaper_dialog_message">Set style wallpaper</string>
+
+    <!-- Title for a dialog box asking the user to use an existing, equivalent theme (style) instead
+        of their customly defined one. [CHAR_LIMIT=30] -->
+    <string name="use_style_instead_title">Use <xliff:g name="style_name">%1$s</xliff:g> instead?</string>
+
+    <!-- Body for a dialog box asking the user to use an existing, equivalent theme (style) instead
+        of their customly defined one. [CHAR_LIMIT=NONE] -->
+    <string name="use_style_instead_body">The components you chose match the <xliff:g name="style_name">%1$s</xliff:g> style. Do you want to use <xliff:g name="style_name">%1$s</xliff:g> instead?</string>
+
+    <!-- Label for a button in the dialog box that asks the user to use an existing, theme (style)
+        instead of their customly defined one. The button let's the user use the suggested theme
+        [CHAR_LIMIT=15] -->
+    <string name="use_style_button">Use <xliff:g name="style_name">%1$s</xliff:g></string>
+
+    <!-- Label for a button in the dialog box that asks the user to use an existing, theme (style)
+        instead of their customly defined one. The button dismisses the dialog and goes back to the
+        previous screen. [CHAR_LIMIT=15]  -->
+    <string name="no_thanks">No, thanks</string>
 </resources>
diff --git a/src/com/android/customization/model/theme/DefaultThemeProvider.java b/src/com/android/customization/model/theme/DefaultThemeProvider.java
index 6bb4926..bcecc45 100644
--- a/src/com/android/customization/model/theme/DefaultThemeProvider.java
+++ b/src/com/android/customization/model/theme/DefaultThemeProvider.java
@@ -544,4 +544,17 @@
        OverlayInfo info = mOverlayInfos.get(packageName);
        return info != null ? info.category : null;
     }
+
+    @Override
+    public ThemeBundle findEquivalent(ThemeBundle other) {
+        if (mThemes == null) {
+            return null;
+        }
+        for (ThemeBundle theme : mThemes) {
+            if (theme.isEquivalent(other)) {
+                return theme;
+            }
+        }
+        return null;
+    }
 }
diff --git a/src/com/android/customization/model/theme/ThemeBundle.java b/src/com/android/customization/model/theme/ThemeBundle.java
index 50b70cb..2b217a0 100644
--- a/src/com/android/customization/model/theme/ThemeBundle.java
+++ b/src/com/android/customization/model/theme/ThemeBundle.java
@@ -124,6 +124,21 @@
         return R.layout.theme_option;
     }
 
+    /**
+     * This is similar to #equals() but it only compares this theme's packages with the other, that
+     * is, it will return true if applying this theme has the same effect of applying the given one.
+     */
+    public boolean isEquivalent(ThemeBundle other) {
+        if (other == null) {
+            return false;
+        }
+        if (mIsDefault) {
+            return other.isDefault() || TextUtils.isEmpty(other.getSerializedPackages());
+        }
+        // Map#equals ensures keys and values are compared.
+        return mPackagesByCategory.equals(other.mPackagesByCategory);
+    }
+
     public PreviewInfo getPreviewInfo() {
         return mPreviewInfo;
     }
diff --git a/src/com/android/customization/model/theme/ThemeBundleProvider.java b/src/com/android/customization/model/theme/ThemeBundleProvider.java
index 7a7ba28..cd33a81 100644
--- a/src/com/android/customization/model/theme/ThemeBundleProvider.java
+++ b/src/com/android/customization/model/theme/ThemeBundleProvider.java
@@ -44,4 +44,6 @@
     void removeCustomTheme(CustomTheme theme);
 
     @Nullable Builder parseCustomTheme(String serializedTheme);
+
+    ThemeBundle findEquivalent(ThemeBundle other);
 }
diff --git a/src/com/android/customization/model/theme/ThemeManager.java b/src/com/android/customization/model/theme/ThemeManager.java
index 313798f..28e7bb9 100644
--- a/src/com/android/customization/model/theme/ThemeManager.java
+++ b/src/com/android/customization/model/theme/ThemeManager.java
@@ -232,4 +232,13 @@
     public void removeCustomTheme(CustomTheme theme) {
         mProvider.removeCustomTheme(theme);
     }
+
+    /**
+     * @return an existing ThemeBundle that matches the same packages as the given one, if one
+     * exists, or {@code null} otherwise.
+     */
+    @Nullable
+    public ThemeBundle findThemeByPackages(ThemeBundle other) {
+        return mProvider.findEquivalent(other);
+    }
 }
diff --git a/src/com/android/customization/picker/theme/CustomThemeActivity.java b/src/com/android/customization/picker/theme/CustomThemeActivity.java
index 9797e8e..e7df1e6 100644
--- a/src/com/android/customization/picker/theme/CustomThemeActivity.java
+++ b/src/com/android/customization/picker/theme/CustomThemeActivity.java
@@ -15,6 +15,7 @@
  */
 package com.android.customization.picker.theme;
 
+import android.app.AlertDialog;
 import android.content.Intent;
 import android.os.Bundle;
 import android.util.Log;
@@ -31,6 +32,7 @@
 import com.android.customization.model.CustomizationManager.Callback;
 import com.android.customization.model.theme.DefaultThemeProvider;
 import com.android.customization.model.theme.OverlayManagerCompat;
+import com.android.customization.model.theme.ThemeBundle;
 import com.android.customization.model.theme.ThemeBundle.Builder;
 import com.android.customization.model.theme.ThemeBundleProvider;
 import com.android.customization.model.theme.ThemeManager;
@@ -99,6 +101,7 @@
                 new WallpaperSetter(injector.getWallpaperPersister(this),
                         injector.getPreferences(this), mUserEventLogger, false),
                 new OverlayManagerCompat(this));
+        mThemeManager.fetchOptions(null, false);
         setContentView(R.layout.activity_custom_theme);
         mApplyButton = findViewById(R.id.next_button);
         mApplyButton.setOnClickListener(view -> onNextOrApply());
@@ -147,26 +150,32 @@
                     navigateToStep(mCurrentStep + 1);
                 } else {
                     // We're on the last step, apply theme and leave
-                    // TODO: Verify that custom theme doesn't collide with existing one
-                    //  (compare overlay packages)
-                    mThemeManager.apply(mCustomThemeManager.buildPartialCustomTheme(
-                            CustomThemeActivity.this), new Callback() {
-                        @Override
-                        public void onSuccess() {
-                            Toast.makeText(CustomThemeActivity.this, R.string.applied_theme_msg,
-                                    Toast.LENGTH_LONG).show();
-                            setResult(RESULT_THEME_APPLIED);
-                            finish();
-                        }
+                    CustomTheme themeToApply = mCustomThemeManager.buildPartialCustomTheme(
+                            CustomThemeActivity.this);
 
-                        @Override
-                        public void onError(@Nullable Throwable throwable) {
-                            Log.w(TAG, "Error applying custom theme", throwable);
-                            Toast.makeText(CustomThemeActivity.this,
-                                    R.string.apply_theme_error_msg,
-                                    Toast.LENGTH_LONG).show();
-                        }
-                    });
+                    // If the current theme is equal to the original theme being edited, then
+                    // don't search for an equivalent, let the user apply the same one by keeping
+                    // it null.
+                    ThemeBundle equivalent = (mCustomThemeManager.getOriginalTheme() != null
+                            && mCustomThemeManager.getOriginalTheme().isEquivalent(themeToApply))
+                                ? null : mThemeManager.findThemeByPackages(themeToApply);
+
+                    if (equivalent != null) {
+                        AlertDialog.Builder builder =
+                                new AlertDialog.Builder(CustomThemeActivity.this);
+                        builder.setTitle(getString(R.string.use_style_instead_title,
+                                    equivalent.getTitle()))
+                                .setMessage(getString(R.string.use_style_instead_body,
+                                        equivalent.getTitle()))
+                                .setPositiveButton(getString(R.string.use_style_button,
+                                        equivalent.getTitle()),
+                                        (dialogInterface, i) -> applyTheme(equivalent))
+                                .setNegativeButton(R.string.no_thanks, null)
+                                .create()
+                                .show();
+                    } else {
+                        applyTheme(themeToApply);
+                    }
                 }
             }
 
@@ -179,6 +188,26 @@
         });
     }
 
+    private void applyTheme(ThemeBundle themeToApply) {
+        mThemeManager.apply(themeToApply, new Callback() {
+            @Override
+            public void onSuccess() {
+                Toast.makeText(CustomThemeActivity.this, R.string.applied_theme_msg,
+                        Toast.LENGTH_LONG).show();
+                setResult(RESULT_THEME_APPLIED);
+                finish();
+            }
+
+            @Override
+            public void onError(@Nullable Throwable throwable) {
+                Log.w(TAG, "Error applying custom theme", throwable);
+                Toast.makeText(CustomThemeActivity.this,
+                        R.string.apply_theme_error_msg,
+                        Toast.LENGTH_LONG).show();
+            }
+        });
+    }
+
     private CustomThemeComponentFragment getCurrentStepFragment() {
         return (CustomThemeComponentFragment)
                 getSupportFragmentManager().findFragmentById(R.id.fragment_container);