Custom Theme 2/n: infrastructure for loading options

Adds support to read components' options from overlays.
Only FontOptions are implemented in this CL, the rest are coming in
subsequent CLs.

Bug: 124796742

Change-Id: If70b86f85d360b1ad7fb4c8cd3f1e45c94ca1856
diff --git a/src/com/android/customization/model/ResourceConstants.java b/src/com/android/customization/model/ResourceConstants.java
index 2822f4a..ad16881 100644
--- a/src/com/android/customization/model/ResourceConstants.java
+++ b/src/com/android/customization/model/ResourceConstants.java
@@ -37,6 +37,8 @@
      */
     String SYSUI_PACKAGE = "com.android.systemui";
 
+    String [] DEFAULT_TARGET_PACKAGES = {ANDROID_PACKAGE, SETTINGS_PACKAGE, SYSUI_PACKAGE};
+
     /**
      * Name of the system resource for icon mask
      */
@@ -56,4 +58,6 @@
      * Secure Setting used to store the currently set theme.
      */
     String THEME_SETTING = Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES;
+    String CONFIG_BODY_FONT_FAMILY = "config_bodyFontFamily";
+    String CONFIG_HEADLINE_FONT_FAMILY = "config_headlineFontFamily";
 }
diff --git a/src/com/android/customization/model/theme/DefaultThemeProvider.java b/src/com/android/customization/model/theme/DefaultThemeProvider.java
index 3dcb90e..a48c67b 100644
--- a/src/com/android/customization/model/theme/DefaultThemeProvider.java
+++ b/src/com/android/customization/model/theme/DefaultThemeProvider.java
@@ -35,8 +35,10 @@
 import androidx.annotation.Nullable;
 
 import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+import com.android.customization.model.ResourceConstants;
 import com.android.customization.model.ResourcesApkProvider;
 import com.android.customization.model.theme.ThemeBundle.Builder;
+import com.android.customization.model.theme.custom.CustomTheme;
 import com.android.wallpaper.R;
 import com.android.wallpaper.asset.ResourceAsset;
 
@@ -75,8 +77,6 @@
 
     private static final String ACCENT_COLOR_LIGHT_NAME = "accent_device_default_light";
     private static final String ACCENT_COLOR_DARK_NAME = "accent_device_default_dark";
-    private static final String CONFIG_BODY_FONT_FAMILY = "config_bodyFontFamily";
-    private static final String CONFIG_HEADLINE_FONT_FAMILY = "config_headlineFontFamily";
     private static final String[] SYSUI_ICONS_FOR_PREVIEW = {
             "ic_qs_bluetooth_on",
             "ic_dnd",
@@ -133,9 +133,11 @@
                 if (!TextUtils.isEmpty(fontOverlayPackage)) {
                     builder.addOverlayPackage(getOverlayCategory(fontOverlayPackage),
                                 fontOverlayPackage)
-                            .setBodyFontFamily(loadTypeface(CONFIG_BODY_FONT_FAMILY,
+                            .setBodyFontFamily(loadTypeface(
+                                    ResourceConstants.CONFIG_BODY_FONT_FAMILY,
                                     fontOverlayPackage))
-                            .setHeadlineFontFamily(loadTypeface(CONFIG_HEADLINE_FONT_FAMILY,
+                            .setHeadlineFontFamily(loadTypeface(
+                                    ResourceConstants.CONFIG_HEADLINE_FONT_FAMILY,
                                     fontOverlayPackage));
                 }
 
@@ -270,15 +272,17 @@
 
         try {
             builder.addOverlayPackage(getOverlayCategory(fontOverlayPackage), fontOverlayPackage)
-                    .setBodyFontFamily(loadTypeface(CONFIG_BODY_FONT_FAMILY,
+                    .setBodyFontFamily(loadTypeface(ResourceConstants.CONFIG_BODY_FONT_FAMILY,
                             fontOverlayPackage))
-                    .setHeadlineFontFamily(loadTypeface(CONFIG_HEADLINE_FONT_FAMILY,
+                    .setHeadlineFontFamily(loadTypeface(
+                            ResourceConstants.CONFIG_HEADLINE_FONT_FAMILY,
                             fontOverlayPackage));
         } catch (NameNotFoundException | NotFoundException e) {
             Log.d(TAG, "Didn't find font overlay for default theme, will use system default", e);
             String headlineFontFamily = system.getString(system.getIdentifier(
-                    CONFIG_HEADLINE_FONT_FAMILY,"string", ANDROID_PACKAGE));
-            String bodyFontFamily = system.getString(system.getIdentifier(CONFIG_BODY_FONT_FAMILY,
+                    ResourceConstants.CONFIG_HEADLINE_FONT_FAMILY,"string", ANDROID_PACKAGE));
+            String bodyFontFamily = system.getString(system.getIdentifier(
+                    ResourceConstants.CONFIG_BODY_FONT_FAMILY,
                     "string", ANDROID_PACKAGE));
             builder.setHeadlineFontFamily(Typeface.create(headlineFontFamily, Typeface.NORMAL))
                     .setBodyFontFamily(Typeface.create(bodyFontFamily, Typeface.NORMAL));
diff --git a/src/com/android/customization/model/theme/OverlayManagerCompat.java b/src/com/android/customization/model/theme/OverlayManagerCompat.java
index a32f120..c2e4c8e 100644
--- a/src/com/android/customization/model/theme/OverlayManagerCompat.java
+++ b/src/com/android/customization/model/theme/OverlayManagerCompat.java
@@ -22,6 +22,10 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.customization.model.ResourceConstants;
+
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -32,6 +36,7 @@
  */
 public class OverlayManagerCompat {
     private final OverlayManager mOverlayManager;
+    private Map<Integer, Map<String, List<OverlayInfo>>> mOverlayByUser;
 
     public OverlayManagerCompat(Context context) {
         mOverlayManager = context.getSystemService(OverlayManager.class);
@@ -59,6 +64,7 @@
      */
     @Nullable
     public String getEnabledPackageName(String targetPackageName, String category) {
+        // Can't use mOverlayByUser map as the enabled state might change
         List<OverlayInfo> overlayInfos = getOverlayInfosForTarget(targetPackageName,
                 UserHandle.myUserId());
         for (OverlayInfo overlayInfo : overlayInfos) {
@@ -81,13 +87,42 @@
         return overlays;
     }
 
+    public List<String> getOverlayPackagesForCategory(String category, int userId,
+            String... targetPackages) {
+        List<String> overlays = new ArrayList<>();
+        ensureCategoryMapForUser(userId);
+        for (String target : targetPackages) {
+            for (OverlayInfo info
+                    : mOverlayByUser.get(userId).getOrDefault(target, Collections.emptyList())) {
+                if (category.equals(info.category)) {
+                    overlays.add(info.packageName);
+                }
+            }
+        }
+        return overlays;
+    }
+
+    private void ensureCategoryMapForUser(int userId) {
+        if (mOverlayByUser == null) {
+            mOverlayByUser = new HashMap<>();
+        }
+        if (!mOverlayByUser.containsKey(userId)) {
+            Map<String, List<OverlayInfo>> overlaysByTarget = new HashMap<>();
+            for (String target : ResourceConstants.DEFAULT_TARGET_PACKAGES) {
+                overlaysByTarget.put(target, getOverlayInfosForTarget(target, userId));
+            }
+            mOverlayByUser.put(userId, overlaysByTarget);
+        }
+    }
+
+
     private List<OverlayInfo> getOverlayInfosForTarget(String targetPackageName, int userId) {
         return mOverlayManager.getOverlayInfosForTarget(targetPackageName, userId);
     }
 
     private void addAllEnabledOverlaysForTarget(Map<String, String> overlays, String target) {
-        for (OverlayInfo overlayInfo : getOverlayInfosForTarget(target,
-                UserHandle.myUserId())) {
+        // Can't use mOverlayByUser map as the enabled state might change
+        for (OverlayInfo overlayInfo : getOverlayInfosForTarget(target, UserHandle.myUserId())) {
             if (overlayInfo.isEnabled()) {
                 overlays.put(overlayInfo.category, overlayInfo.packageName);
             }
diff --git a/src/com/android/customization/model/theme/ThemeManager.java b/src/com/android/customization/model/theme/ThemeManager.java
index a37893b..91192dc 100644
--- a/src/com/android/customization/model/theme/ThemeManager.java
+++ b/src/com/android/customization/model/theme/ThemeManager.java
@@ -16,6 +16,7 @@
 package com.android.customization.model.theme;
 
 import static com.android.customization.model.ResourceConstants.ANDROID_PACKAGE;
+import static com.android.customization.model.ResourceConstants.DEFAULT_TARGET_PACKAGES;
 import static com.android.customization.model.ResourceConstants.SETTINGS_PACKAGE;
 import static com.android.customization.model.ResourceConstants.SYSUI_PACKAGE;
 
@@ -155,8 +156,8 @@
 
     public Map<String, String> getCurrentOverlays() {
         if (mCurrentOverlays == null) {
-            mCurrentOverlays = mOverlayManagerCompat.getEnabledOverlaysForTargets(ANDROID_PACKAGE,
-                    SYSUI_PACKAGE, SETTINGS_PACKAGE);
+            mCurrentOverlays = mOverlayManagerCompat.getEnabledOverlaysForTargets(
+                    DEFAULT_TARGET_PACKAGES);
             mCurrentOverlays.entrySet().removeIf(
                     categoryAndPackage -> !THEME_CATEGORIES.contains(categoryAndPackage.getKey()));
         }
@@ -164,6 +165,7 @@
     }
 
     public String getStoredOverlays() {
-        return Settings.Secure.getString(mActivity.getContentResolver(), ResourceConstants.THEME_SETTING);
+        return Settings.Secure.getString(mActivity.getContentResolver(),
+                ResourceConstants.THEME_SETTING);
     }
 }
diff --git a/src/com/android/customization/model/theme/CustomTheme.java b/src/com/android/customization/model/theme/custom/CustomTheme.java
similarity index 92%
rename from src/com/android/customization/model/theme/CustomTheme.java
rename to src/com/android/customization/model/theme/custom/CustomTheme.java
index 289e3e4..982378f 100644
--- a/src/com/android/customization/model/theme/CustomTheme.java
+++ b/src/com/android/customization/model/theme/custom/CustomTheme.java
@@ -13,10 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.customization.model.theme;
+package com.android.customization.model.theme.custom;
 
 import android.view.View;
 
+import com.android.customization.model.theme.ThemeBundle;
 import com.android.wallpaper.R;
 
 import java.util.Map;
diff --git a/src/com/android/customization/model/theme/custom/FontOptionsProvider.java b/src/com/android/customization/model/theme/custom/FontOptionsProvider.java
new file mode 100644
index 0000000..ef423e7
--- /dev/null
+++ b/src/com/android/customization/model/theme/custom/FontOptionsProvider.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 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.customization.model.theme.custom;
+
+import static com.android.customization.model.ResourceConstants.CONFIG_BODY_FONT_FAMILY;
+import static com.android.customization.model.ResourceConstants.CONFIG_HEADLINE_FONT_FAMILY;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.Typeface;
+import android.util.Log;
+
+import com.android.customization.model.theme.OverlayManagerCompat;
+import com.android.customization.model.theme.custom.ThemeComponentOption.FontOption;
+
+/**
+ * Implementation of {@link ThemeComponentOptionProvider} that reads {@link FontOption}s from
+ * font overlays.
+ */
+public class FontOptionsProvider extends ThemeComponentOptionProvider<FontOption> {
+
+    private static final String TAG = "FontOptionsProvider";
+
+    public FontOptionsProvider(Context context, OverlayManagerCompat manager) {
+        super(context, manager, OVERLAY_CATEGORY_FONT);
+    }
+
+    @Override
+    protected void loadOptions() {
+        for (String overlayPackage : mOverlayPackages) {
+            try {
+                Resources overlayRes = getOverlayResources(overlayPackage);
+                Typeface headlineFont = Typeface.create(
+                        getFontFamily(overlayPackage, overlayRes, CONFIG_HEADLINE_FONT_FAMILY),
+                        Typeface.NORMAL);
+                Typeface bodyFont = Typeface.create(
+                        getFontFamily(overlayPackage, overlayRes, CONFIG_BODY_FONT_FAMILY),
+                        Typeface.NORMAL);
+                PackageManager pm = mContext.getPackageManager();
+                String label = pm.getApplicationInfo(overlayPackage, 0).loadLabel(pm).toString();
+                mOptions.add(new FontOption(overlayPackage, label, headlineFont, bodyFont));
+            } catch (NameNotFoundException | NotFoundException e) {
+                Log.w(TAG, String.format("Couldn't load font overlay %s, will skip it",
+                        overlayPackage), e);
+            }
+        }
+    }
+
+    private String getFontFamily(String overlayPackage, Resources overlayRes, String configName) {
+        return overlayRes.getString(overlayRes.getIdentifier(configName, "string", overlayPackage));
+    }
+}
diff --git a/src/com/android/customization/model/theme/custom/ThemeComponentOption.java b/src/com/android/customization/model/theme/custom/ThemeComponentOption.java
new file mode 100644
index 0000000..71cb31a
--- /dev/null
+++ b/src/com/android/customization/model/theme/custom/ThemeComponentOption.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 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.customization.model.theme.custom;
+
+import android.graphics.Typeface;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.customization.model.CustomizationManager;
+import com.android.customization.model.CustomizationOption;
+
+/**
+ * Represents an option of a component of a custom Theme (for example, a possible color, or font,
+ * shape, etc).
+ * Extending classes correspond to each component's options and provide the structure to bind
+ * preview and thumbnails.
+ */
+public abstract class ThemeComponentOption implements CustomizationOption<ThemeComponentOption> {
+
+    protected final String mOverlayPackageName;
+
+    ThemeComponentOption(String packageName) {
+        mOverlayPackageName = packageName;
+    }
+
+    public String getOverlayPackageName() {
+        return mOverlayPackageName;
+    }
+
+    @Override
+    public String getTitle() {
+        return null;
+    }
+
+    public abstract void bindPreview(ViewGroup container);
+
+    public static class FontOption extends ThemeComponentOption {
+
+        private final String mLabel;
+        private final Typeface mHeadlineFont;
+        private final Typeface mBodyFont;
+
+        public FontOption(String packageName, String label, Typeface headlineFont,
+                Typeface bodyFont) {
+            super(packageName);
+            mLabel = label;
+            mHeadlineFont = headlineFont;
+            mBodyFont = bodyFont;
+        }
+
+        @Override
+        public String getTitle() {
+            return mLabel;
+        }
+
+        @Override
+        public void bindThumbnailTile(View view) {
+
+        }
+
+        @Override
+        public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
+            return false;
+        }
+
+        @Override
+        public int getLayoutResId() {
+            return 0;
+        }
+
+        @Override
+        public void bindPreview(ViewGroup container) {
+
+        }
+    }
+
+    public static class IconOption extends ThemeComponentOption {
+
+        IconOption(String packageName) {
+            super(packageName);
+        }
+
+        @Override
+        public void bindThumbnailTile(View view) {
+
+        }
+
+        @Override
+        public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
+            return false;
+        }
+
+        @Override
+        public int getLayoutResId() {
+            return 0;
+        }
+
+        @Override
+        public void bindPreview(ViewGroup container) {
+
+        }
+    }
+
+    public static class ColorOption extends ThemeComponentOption {
+
+        ColorOption(String packageName) {
+            super(packageName);
+        }
+
+        @Override
+        public void bindThumbnailTile(View view) {
+
+        }
+
+        @Override
+        public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
+            return false;
+        }
+
+        @Override
+        public int getLayoutResId() {
+            return 0;
+        }
+
+        @Override
+        public void bindPreview(ViewGroup container) {
+
+        }
+    }
+
+    public static class ShapeOption extends ThemeComponentOption {
+
+        ShapeOption(String packageName) {
+            super(packageName);
+        }
+
+        @Override
+        public void bindThumbnailTile(View view) {
+
+        }
+
+        @Override
+        public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
+            return false;
+        }
+
+        @Override
+        public int getLayoutResId() {
+            return 0;
+        }
+
+        @Override
+        public void bindPreview(ViewGroup container) {
+
+        }
+    }
+}
diff --git a/src/com/android/customization/model/theme/custom/ThemeComponentOptionProvider.java b/src/com/android/customization/model/theme/custom/ThemeComponentOptionProvider.java
new file mode 100644
index 0000000..316ee99
--- /dev/null
+++ b/src/com/android/customization/model/theme/custom/ThemeComponentOptionProvider.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 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.customization.model.theme.custom;
+
+import static com.android.customization.model.ResourceConstants.DEFAULT_TARGET_PACKAGES;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.os.UserHandle;
+
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+import com.android.customization.model.theme.OverlayManagerCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base class used to retrieve Custom Theme Component options (eg, different fonts)
+ * from the system.
+ */
+public abstract class ThemeComponentOptionProvider<T extends ThemeComponentOption> {
+
+    protected final Context mContext;
+    protected final List<String> mOverlayPackages;
+    protected List<T> mOptions;
+
+    public ThemeComponentOptionProvider(Context context, OverlayManagerCompat manager,
+            String... categories) {
+        mContext = context;
+        mOverlayPackages = new ArrayList<>();
+        for (String category : categories) {
+            mOverlayPackages.addAll(manager.getOverlayPackagesForCategory(category,
+                    UserHandle.myUserId(), DEFAULT_TARGET_PACKAGES));
+        }
+    }
+
+    /**
+     * Returns whether there are options for this component available in the current setup.
+     */
+    public boolean isAvailable() {
+        return !mOverlayPackages.isEmpty();
+    }
+
+    /**
+     * Retrieve the available options for this component.
+     * @param callback called when the themes have been retrieved (or immediately if cached)
+     * @param reload whether to reload themes if they're cached.
+     */
+    public void fetch(OptionsFetchedListener<T> callback, boolean reload) {
+        if (mOptions == null || reload) {
+            mOptions = new ArrayList<>();
+            loadOptions();
+        }
+
+        if(callback != null) {
+            callback.onOptionsLoaded(mOptions);
+        }
+    }
+
+    protected abstract void loadOptions();
+
+    protected Resources getOverlayResources(String overlayPackage) throws NameNotFoundException {
+        return mContext.getPackageManager().getResourcesForApplication(overlayPackage);
+    }
+}
diff --git a/src/com/android/customization/picker/theme/ThemeFragment.java b/src/com/android/customization/picker/theme/ThemeFragment.java
index 669f21c..4487247 100644
--- a/src/com/android/customization/picker/theme/ThemeFragment.java
+++ b/src/com/android/customization/picker/theme/ThemeFragment.java
@@ -44,7 +44,7 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.customization.model.CustomizationManager.Callback;
-import com.android.customization.model.theme.CustomTheme;
+import com.android.customization.model.theme.custom.CustomTheme;
 import com.android.customization.model.theme.ThemeBundle;
 import com.android.customization.model.theme.ThemeBundle.PreviewInfo;
 import com.android.customization.model.theme.ThemeManager;