[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;
}