[ThemePicker 5/N] Create a ThemeProvider

Create a Provider that can read the themes available in the
system from a stub apk.
For now we only read Font and Color overlays (the rest
coming in a follow up CL).

Bug: 120559294

Change-Id: I78f1a31073ff219befc7b0d37227afb38c746380
diff --git a/res/values/override.xml b/res/values/override.xml
new file mode 100644
index 0000000..b40eec8
--- /dev/null
+++ b/res/values/override.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+     Copyright (C) 2018 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.
+-->
+<resources>
+    <string name="themes_stub_package" translatable="false"/>
+</resources>
\ No newline at end of file
diff --git a/src/com/android/customization/model/CustomizationManager.java b/src/com/android/customization/model/CustomizationManager.java
new file mode 100644
index 0000000..2a783a8
--- /dev/null
+++ b/src/com/android/customization/model/CustomizationManager.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+import java.util.List;
+
+/**
+ * Interface for a class that handles a "Customization" (eg, "Themes", "Clockfaces", etc)
+ * @param <T> the type of {@link CustomizationOption} that this Manager class provides.
+ */
+public interface CustomizationManager<T extends CustomizationOption> {
+
+    /**
+     * Listener interface for fetching CustomizationOptions
+     */
+    interface OptionsFetchedListener<T extends CustomizationOption> {
+        /**
+         * Called when the options have been retrieved.
+         */
+        void onOptionsLoaded(List<T> options);
+    }
+
+    /**
+     * Returns whether this customization is available in the system.
+     */
+    boolean isAvailable();
+
+    /**
+     * Applies the given option into the system.
+     */
+    void apply(T option);
+
+    /**
+     * Loads the available options for the type of Customization managed by this class, calling the
+     * given callback when done.
+     */
+    void fetchOptions(OptionsFetchedListener<T> callback);
+}
diff --git a/src/com/android/customization/model/CustomizationOption.java b/src/com/android/customization/model/CustomizationOption.java
index 8c668da..c89d12e 100644
--- a/src/com/android/customization/model/CustomizationOption.java
+++ b/src/com/android/customization/model/CustomizationOption.java
@@ -17,13 +17,13 @@
 
 import android.view.View;
 
-import com.android.wallpaper.R;
-
 import androidx.annotation.LayoutRes;
 
+import com.android.wallpaper.R;
+
 
 /**
- * Represents an option of customization (eg, a Theme, a Clock face, a Grid size)
+ * Represents an option of customization (eg, a ThemeBundle, a Clock face, a Grid size)
  */
 public interface CustomizationOption {
 
diff --git a/src/com/android/customization/model/theme/DefaultThemeProvider.java b/src/com/android/customization/model/theme/DefaultThemeProvider.java
new file mode 100644
index 0000000..25c7c72
--- /dev/null
+++ b/src/com/android/customization/model/theme/DefaultThemeProvider.java
@@ -0,0 +1,163 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+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.text.TextUtils;
+import android.util.Log;
+
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+import com.android.customization.model.theme.ThemeBundle.Builder;
+import com.android.wallpaper.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Default implementation of {@link ThemeBundleProvider} that reads Themes' overlays from a stub APK.
+ */
+public class DefaultThemeProvider implements ThemeBundleProvider {
+
+    private static final String TAG = "DefaultThemeProvider";
+
+    private static final String THEMES_ARRAY = "themes";
+    private static final String TITLE_PREFIX = "theme_title_";
+    private static final String FONT_PREFIX = "theme_overlay_font_";
+    private static final String COLOR_PREFIX = "theme_overlay_color_";
+
+    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 final Context mContext;
+    private final String mStubPackageName;
+    private Resources mStubApkResources;
+    private List<ThemeBundle> mThemes;
+
+    public DefaultThemeProvider(Context context) {
+        mContext = context;
+        mStubPackageName = mContext.getString(R.string.themes_stub_package);
+        init();
+    }
+
+    private void init() {
+        mStubApkResources = null;
+        try {
+            PackageManager pm = mContext.getPackageManager();
+            ApplicationInfo stubAppInfo = pm.getApplicationInfo(
+                    mStubPackageName, PackageManager.GET_META_DATA);
+            if (stubAppInfo != null) {
+                mStubApkResources = pm.getResourcesForApplication(stubAppInfo);
+            }
+        } catch (NameNotFoundException e) {
+            Log.w(TAG, "Themes stub APK not found.");
+        }
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return mStubApkResources != null;
+    }
+
+    @Override
+    public void fetch(OptionsFetchedListener<ThemeBundle> callback, boolean reload) {
+        if (mThemes == null || reload) {
+            mThemes = new ArrayList<>();
+            readThemesFromStub();
+        }
+
+        if(callback != null) {
+            callback.onOptionsLoaded(mThemes);
+        }
+    }
+
+    private void readThemesFromStub() {
+        int themesListResId = mStubApkResources.getIdentifier(THEMES_ARRAY, "array",
+                mStubPackageName);
+        String[] themeNames = mStubApkResources.getStringArray(themesListResId);
+
+        for (String themeName : themeNames) {
+            ThemeBundle.Builder builder = new Builder();
+            try {
+                builder.setTitle(mStubApkResources.getString(
+                        mStubApkResources.getIdentifier(TITLE_PREFIX + themeName,
+                                "string", mStubPackageName)));
+
+                String fontOverlayPackage = getOverlayPackage(FONT_PREFIX, themeName);
+
+                if (!TextUtils.isEmpty(fontOverlayPackage)) {
+                    builder.setFontOverlayPackage(fontOverlayPackage)
+                            .setBodyFontFamily(loadTypeface(CONFIG_BODY_FONT_FAMILY,
+                                    fontOverlayPackage))
+                            .setHeadlineFontFamily(loadTypeface(CONFIG_HEADLINE_FONT_FAMILY,
+                                    fontOverlayPackage));
+                }
+
+                String colorOverlayPackage = getOverlayPackage(COLOR_PREFIX, themeName);
+
+                if (!TextUtils.isEmpty(colorOverlayPackage)) {
+                    builder.setColorPackage(colorOverlayPackage)
+                            .setColorAccentLight(loadColor(ACCENT_COLOR_LIGHT_NAME,
+                                    colorOverlayPackage))
+                            .setColorAccentDark(loadColor(ACCENT_COLOR_DARK_NAME,
+                                    colorOverlayPackage));
+                }
+
+                //TODO (santie) read the other overlays
+
+                mThemes.add(builder.build());
+            } catch (NameNotFoundException | NotFoundException e) {
+                Log.w(TAG, String.format("Couldn't load part of theme %s, will skip it", themeName),
+                        e);
+            }
+        }
+    }
+
+    private String getOverlayPackage(String prefix, String themeName) {
+        int overlayPackageResId = mStubApkResources.getIdentifier(prefix + themeName,
+                "string", mStubPackageName);
+        return mStubApkResources.getString(overlayPackageResId);
+    }
+
+    private Typeface loadTypeface(String configName, String fontOverlayPackage)
+            throws NameNotFoundException, NotFoundException {
+
+        // TODO(santie): check for font being present in system
+
+        Resources overlayRes = mContext.getPackageManager()
+                .getResourcesForApplication(fontOverlayPackage);
+
+        String fontFamily = overlayRes.getString(overlayRes.getIdentifier(configName,
+                "string", fontOverlayPackage));
+        return Typeface.create(fontFamily, Typeface.NORMAL);
+    }
+
+    private int loadColor(String colorName, String colorPackage)
+            throws NameNotFoundException, NotFoundException {
+
+        Resources overlayRes = mContext.getPackageManager()
+                .getResourcesForApplication(colorPackage);
+        return overlayRes.getColor(overlayRes.getIdentifier(colorName, "color", colorPackage),
+                null);
+    }
+}
diff --git a/src/com/android/customization/model/theme/ThemeBundle.java b/src/com/android/customization/model/theme/ThemeBundle.java
new file mode 100644
index 0000000..874e90d
--- /dev/null
+++ b/src/com/android/customization/model/theme/ThemeBundle.java
@@ -0,0 +1,149 @@
+/*
+ * 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;
+
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.CustomizationOption;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a Theme component available in the system as a "persona" bundle.
+ * Note that in this context a Theme is not related to Android's Styles, but it's rather an
+ * abstraction representing a series of overlays to be applied to the system.
+ */
+public class ThemeBundle implements CustomizationOption {
+
+    private final String mTitle;
+    private final PreviewInfo mPreviewInfo;
+
+    private ThemeBundle(String title, PreviewInfo previewInfo) {
+        mTitle = title;
+        mPreviewInfo = previewInfo;
+    }
+
+    @Override
+    public String getTitle() {
+        return mTitle;
+    }
+
+    @Override
+    public void bindThumbnailTile(View view) {
+        //TODO(santie): implement
+    }
+
+    @Override
+    public boolean isCurrentlySet() {
+        return false;
+    }
+
+    @Override
+    public int getLayoutResId() {
+        return 0; //TODO(santie)
+    }
+
+    public PreviewInfo getPreviewInfo() {
+        return mPreviewInfo;
+    }
+
+    public static class PreviewInfo {
+        public final Typeface bodyFontFamily;
+        public final Typeface headlineFontFamily;
+        @ColorInt public final int colorAccentLight;
+        @ColorInt public final int colorAccentDark;
+        public final List<Drawable> icons;
+        public final String shapePath;
+        @DrawableRes public final int wallpaperDrawableRes;
+
+        public PreviewInfo(Typeface bodyFontFamily, Typeface headlineFontFamily,
+                int colorAccentLight,
+                int colorAccentDark, List<Drawable> icons, String shapePath,
+                int wallpaperDrawableRes) {
+            this.bodyFontFamily = bodyFontFamily;
+            this.headlineFontFamily = headlineFontFamily;
+            this.colorAccentLight = colorAccentLight;
+            this.colorAccentDark = colorAccentDark;
+            this.icons = icons;
+            this.shapePath = shapePath;
+            this.wallpaperDrawableRes = wallpaperDrawableRes;
+        }
+    }
+
+    public static class Builder {
+        private String mTitle;
+        private Typeface mBodyFontFamily;
+        private Typeface mHeadlineFontFamily;
+        @ColorInt private int mColorAccentLight;
+        @ColorInt private int mColorAccentDark;
+        private List<Drawable> mIcons = new ArrayList<>();
+        private String mShapePath;
+        @DrawableRes private int mWallpaperDrawableResId;
+
+        private String mFontOverlayPackage;
+        private String mColorsPackage;
+        private List<String> mIconPackages = new ArrayList<>();
+        private String mShapePackage;
+
+        public ThemeBundle build() {
+            return new ThemeBundle(mTitle,
+                    new PreviewInfo(mBodyFontFamily, mHeadlineFontFamily, mColorAccentLight,
+                            mColorAccentDark, mIcons, mShapePath, mWallpaperDrawableResId));
+        }
+
+        public Builder setTitle(String title) {
+            mTitle = title;
+            return this;
+        }
+
+        public Builder setFontOverlayPackage(String packageName) {
+            mFontOverlayPackage = packageName;
+            return this;
+        }
+
+        public Builder setBodyFontFamily(@Nullable Typeface bodyFontFamily) {
+            mBodyFontFamily = bodyFontFamily;
+            return this;
+        }
+
+        public Builder setHeadlineFontFamily(@Nullable Typeface headlineFontFamily) {
+            mHeadlineFontFamily = headlineFontFamily;
+            return this;
+        }
+
+        public Builder setColorPackage(String packageName) {
+            mColorsPackage = packageName;
+            return this;
+        }
+
+        public Builder setColorAccentLight(@ColorInt int colorAccentLight) {
+            mColorAccentLight = colorAccentLight;
+            return this;
+        }
+
+        public Builder setColorAccentDark(@ColorInt int colorAccentDark) {
+            mColorAccentDark = colorAccentDark;
+            return this;
+        }
+    }
+}
diff --git a/src/com/android/customization/model/theme/ThemeBundleProvider.java b/src/com/android/customization/model/theme/ThemeBundleProvider.java
new file mode 100644
index 0000000..dbcebef
--- /dev/null
+++ b/src/com/android/customization/model/theme/ThemeBundleProvider.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+
+/**
+ * Interface for a class that can retrieve Themes from the system.
+ */
+public interface ThemeBundleProvider {
+
+    /**
+     * Returns whether themes are available in the current setup.
+     */
+    boolean isAvailable();
+
+    /**
+     * Retrieve the available themes.
+     * @param callback called when the themes have been retrieved (or immediately if cached)
+     * @param reload whether to reload themes if they're cached.
+     */
+    void fetch(OptionsFetchedListener<ThemeBundle> callback, boolean reload);
+
+}
diff --git a/src/com/android/customization/model/theme/ThemeManager.java b/src/com/android/customization/model/theme/ThemeManager.java
new file mode 100644
index 0000000..a07d808
--- /dev/null
+++ b/src/com/android/customization/model/theme/ThemeManager.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+import com.android.customization.model.CustomizationManager;
+
+public class ThemeManager implements CustomizationManager<ThemeBundle> {
+
+    private final ThemeBundleProvider mProvider;
+
+    public ThemeManager(ThemeBundleProvider provider) {
+        mProvider = provider;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return mProvider.isAvailable();
+    }
+
+    @Override
+    public void apply(ThemeBundle theme) {
+        // TODO(santie) implement
+    }
+
+    @Override
+    public void fetchOptions(OptionsFetchedListener<ThemeBundle> callback) {
+        mProvider.fetch(callback, false);
+    }
+}
diff --git a/src/com/android/customization/picker/CustomizationPickerActivity.java b/src/com/android/customization/picker/CustomizationPickerActivity.java
index 5b19a7d..e2dee20 100644
--- a/src/com/android/customization/picker/CustomizationPickerActivity.java
+++ b/src/com/android/customization/picker/CustomizationPickerActivity.java
@@ -180,7 +180,7 @@
     }
 
     /**
-     * Represents a section of the Picker (eg "Theme", "Clock", etc).
+     * Represents a section of the Picker (eg "ThemeBundle", "Clock", etc).
      * There should be a concrete subclass per available section, providing the corresponding
      * Fragment to be displayed when switching to each section.
      */
diff --git a/src/com/android/customization/picker/theme/ThemeFragment.java b/src/com/android/customization/picker/theme/ThemeFragment.java
index ec04b17..5f0610c 100644
--- a/src/com/android/customization/picker/theme/ThemeFragment.java
+++ b/src/com/android/customization/picker/theme/ThemeFragment.java
@@ -27,7 +27,7 @@
 import com.android.wallpaper.picker.ToolbarFragment;
 
 /**
- * Fragment that contains the main UI for selecting and applying a Theme.
+ * Fragment that contains the main UI for selecting and applying a ThemeBundle.
  */
 public class ThemeFragment extends ToolbarFragment {
 
diff --git a/src/com/android/customization/widget/OptionSelectorController.java b/src/com/android/customization/widget/OptionSelectorController.java
index 91dbabb..6ffa0e6 100644
--- a/src/com/android/customization/widget/OptionSelectorController.java
+++ b/src/com/android/customization/widget/OptionSelectorController.java
@@ -20,6 +20,10 @@
 import android.view.ViewGroup;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
 import com.android.customization.model.CustomizationOption;
 import com.android.wallpaper.R;
 
@@ -27,10 +31,6 @@
 import java.util.List;
 import java.util.Set;
 
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
 /**
  * Simple controller for a RecyclerView-based widget to hold the options for each customization
  * section (eg, thumbnails for themes, clocks, grid sizes).
@@ -50,13 +50,14 @@
     }
 
     private final RecyclerView mContainer;
-    private final List<CustomizationOption> mOptions;
+    private final List<? extends CustomizationOption> mOptions;
 
     private final Set<OptionSelectedListener> mListeners = new HashSet<>();
     private RecyclerView.Adapter<TileViewHolder> mAdapter;
     private CustomizationOption mSelectedOption;
 
-    public OptionSelectorController(RecyclerView container, List<CustomizationOption> options) {
+    public OptionSelectorController(RecyclerView container,
+            List<? extends CustomizationOption> options) {
         mContainer = container;
         mOptions = options;
     }