summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKunhung Li <kunhungli@google.com>2022-02-12 16:58:06 +0800
committerKunhung Li <kunhungli@google.com>2022-02-23 07:56:56 +0000
commit53fc4d5aa3aa40a23b3819add0d0f1f6e1b6e796 (patch)
treea5fcf75c25431dcea46caf91892e14d400c7b08b /src
parentcaf59966f58db3eaf3972babf7ad2af86d0f1373 (diff)
downloadThemePicker-53fc4d5aa3aa40a23b3819add0d0f1f6e1b6e796.tar.gz
Move color picking code into AOSP
Add color picking related code, resources and libraries. Bug: 218396282 Test: manual Change-Id: I6aa244c8dac69f4663f97e19e91cfb6cf3eb0990
Diffstat (limited to 'src')
-rw-r--r--src/com/android/customization/model/ResourceConstants.java8
-rw-r--r--src/com/android/customization/model/color/ColorBundle.java350
-rw-r--r--src/com/android/customization/model/color/ColorBundlePreviewExtractor.java105
-rw-r--r--src/com/android/customization/model/color/ColorCustomizationManager.java265
-rw-r--r--src/com/android/customization/model/color/ColorOption.java223
-rw-r--r--src/com/android/customization/model/color/ColorOptionsProvider.java77
-rw-r--r--src/com/android/customization/model/color/ColorProvider.kt281
-rw-r--r--src/com/android/customization/model/color/ColorSectionController.java474
-rw-r--r--src/com/android/customization/model/color/ColorSeedOption.java244
-rw-r--r--src/com/android/customization/model/color/ColorUtils.kt68
-rw-r--r--src/com/android/customization/model/color/WallpaperColorResources.java53
-rw-r--r--src/com/android/customization/picker/color/ColorSectionView.java33
12 files changed, 2181 insertions, 0 deletions
diff --git a/src/com/android/customization/model/ResourceConstants.java b/src/com/android/customization/model/ResourceConstants.java
index 293feb86..aaee9352 100644
--- a/src/com/android/customization/model/ResourceConstants.java
+++ b/src/com/android/customization/model/ResourceConstants.java
@@ -90,6 +90,14 @@ public interface ResourceConstants {
"ic_battery_80_24dp"
};
+ /**
+ * Color bundle strings used to reference system resources.
+ */
+ String COLOR_BUNDLES_ARRAY_NAME = "color_bundles";
+ String COLOR_BUNDLE_NAME_PREFIX = "bundle_name_";
+ String COLOR_BUNDLE_MAIN_COLOR_PREFIX = "color_secondary_";
+ String COLOR_BUNDLE_STYLE_PREFIX = "color_style_";
+
ArrayList<String> sTargetPackages = new ArrayList<>();
String ACCENT_COLOR_LIGHT_NAME = "accent_device_default_light";
String ACCENT_COLOR_DARK_NAME = "accent_device_default_dark";
diff --git a/src/com/android/customization/model/color/ColorBundle.java b/src/com/android/customization/model/color/ColorBundle.java
new file mode 100644
index 00000000..dc5a367a
--- /dev/null
+++ b/src/com/android/customization/model/color/ColorBundle.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2022 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.color;
+
+import static com.android.customization.model.ResourceConstants.PATH_SIZE;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.PathShape;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.Dimension;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.graphics.PathParser;
+
+import com.android.customization.model.ResourceConstants;
+import com.android.systemui.monet.Style;
+import com.android.wallpaper.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a preset color available for the user to chose as their theming option.
+ */
+public class ColorBundle extends ColorOption {
+
+ private final PreviewInfo mPreviewInfo;
+
+ @VisibleForTesting ColorBundle(String title,
+ Map<String, String> overlayPackages, boolean isDefault, Style style, int index,
+ PreviewInfo previewInfo) {
+ super(title, overlayPackages, isDefault, style, index);
+ mPreviewInfo = previewInfo;
+ }
+
+ @Override
+ public void bindThumbnailTile(View view) {
+ Resources res = view.getContext().getResources();
+ int primaryColor = mPreviewInfo.resolvePrimaryColor(res);
+ int secondaryColor = mPreviewInfo.resolveSecondaryColor(res);
+ int padding = view.isActivated()
+ ? res.getDimensionPixelSize(R.dimen.color_seed_option_tile_padding_selected)
+ : res.getDimensionPixelSize(R.dimen.color_seed_option_tile_padding);
+
+ for (int i = 0; i < mPreviewColorIds.length; i++) {
+ ImageView colorPreviewImageView = view.findViewById(mPreviewColorIds[i]);
+ int color = i % 2 == 0 ? primaryColor : secondaryColor;
+ colorPreviewImageView.getDrawable().setColorFilter(color, PorterDuff.Mode.SRC);
+ colorPreviewImageView.setPadding(padding, padding, padding, padding);
+ }
+ view.setContentDescription(getContentDescription(view.getContext()));
+ }
+
+ @Override
+ public PreviewInfo getPreviewInfo() {
+ return mPreviewInfo;
+ }
+
+ @Override
+ public int getLayoutResId() {
+ return R.layout.color_option;
+ }
+
+ @Override
+ public String getSource() {
+ return ColorOptionsProvider.COLOR_SOURCE_PRESET;
+ }
+
+ /**
+ * The preview information of {@link ColorBundle}
+ */
+ public static class PreviewInfo implements ColorOption.PreviewInfo {
+ @ColorInt
+ public final int secondaryColorLight;
+ @ColorInt public final int secondaryColorDark;
+ // Monet system palette and accent colors
+ @ColorInt public final int primaryColorLight;
+ @ColorInt public final int primaryColorDark;
+ public final List<Drawable> icons;
+ public final Drawable shapeDrawable;
+ @Dimension
+ public final int bottomSheetCornerRadius;
+
+ @ColorInt private int mOverrideSecondaryColorLight = Color.TRANSPARENT;
+ @ColorInt private int mOverrideSecondaryColorDark = Color.TRANSPARENT;
+ @ColorInt private int mOverridePrimaryColorLight = Color.TRANSPARENT;
+ @ColorInt private int mOverridePrimaryColorDark = Color.TRANSPARENT;
+
+ private PreviewInfo(
+ int secondaryColorLight, int secondaryColorDark, int colorSystemPaletteLight,
+ int primaryColorDark, List<Drawable> icons, Drawable shapeDrawable,
+ @Dimension int cornerRadius) {
+ this.secondaryColorLight = secondaryColorLight;
+ this.secondaryColorDark = secondaryColorDark;
+ this.primaryColorLight = colorSystemPaletteLight;
+ this.primaryColorDark = primaryColorDark;
+ this.icons = icons;
+ this.shapeDrawable = shapeDrawable;
+ this.bottomSheetCornerRadius = cornerRadius;
+ }
+
+ /**
+ * Returns the accent color to be applied corresponding with the current configuration's
+ * UI mode.
+ * @return one of {@link #secondaryColorDark} or {@link #secondaryColorLight}
+ */
+ @ColorInt
+ public int resolveSecondaryColor(Resources res) {
+ boolean night = (res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
+ == Configuration.UI_MODE_NIGHT_YES;
+ if (mOverrideSecondaryColorDark != Color.TRANSPARENT
+ || mOverrideSecondaryColorLight != Color.TRANSPARENT) {
+ return night ? mOverrideSecondaryColorDark : mOverrideSecondaryColorLight;
+ }
+ return night ? secondaryColorDark : secondaryColorLight;
+ }
+
+ /**
+ * Returns the palette (main) color to be applied corresponding with the current
+ * configuration's UI mode.
+ * @return one of {@link #secondaryColorDark} or {@link #secondaryColorLight}
+ */
+ @ColorInt
+ public int resolvePrimaryColor(Resources res) {
+ boolean night = (res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
+ == Configuration.UI_MODE_NIGHT_YES;
+ if (mOverridePrimaryColorDark != Color.TRANSPARENT
+ || mOverridePrimaryColorLight != Color.TRANSPARENT) {
+ return night ? mOverridePrimaryColorDark : mOverridePrimaryColorLight;
+ }
+ return night ? primaryColorDark
+ : primaryColorLight;
+ }
+
+ /**
+ * Sets accent colors to override the ones in this bundle
+ */
+ public void setOverrideAccentColors(int overrideColorAccentLight,
+ int overrideColorAccentDark) {
+ mOverrideSecondaryColorLight = overrideColorAccentLight;
+ mOverrideSecondaryColorDark = overrideColorAccentDark;
+ }
+
+ /**
+ * Sets palette colors to override the ones in this bundle
+ */
+ public void setOverridePaletteColors(int overrideColorPaletteLight,
+ int overrideColorPaletteDark) {
+ mOverridePrimaryColorLight = overrideColorPaletteLight;
+ mOverridePrimaryColorDark = overrideColorPaletteDark;
+ }
+ }
+
+ /**
+ * The builder of ColorBundle
+ */
+ public static class Builder {
+ protected String mTitle;
+ @ColorInt private int mSecondaryColorLight = Color.TRANSPARENT;
+ @ColorInt private int mSecondaryColorDark = Color.TRANSPARENT;
+ // System and Monet colors
+ @ColorInt private int mPrimaryColorLight = Color.TRANSPARENT;
+ @ColorInt private int mPrimaryColorDark = Color.TRANSPARENT;
+ private List<Drawable> mIcons = new ArrayList<>();
+ private boolean mIsDefault;
+ private Style mStyle = Style.TONAL_SPOT;
+ private int mIndex;
+ protected Map<String, String> mPackages = new HashMap<>();
+
+ /**
+ * Builds the ColorBundle
+ * @param context {@link Context}
+ * @return new {@link ColorBundle} object
+ */
+ public ColorBundle build(Context context) {
+ if (mTitle == null) {
+ mTitle = context.getString(R.string.adaptive_color_title);
+ }
+ return new ColorBundle(mTitle, mPackages, mIsDefault, mStyle, mIndex,
+ createPreviewInfo(context));
+ }
+
+ /**
+ * Creates preview information
+ * @param context the {@link Context}
+ * @return the {@link PreviewInfo} object
+ */
+ public PreviewInfo createPreviewInfo(@NonNull Context context) {
+ ShapeDrawable shapeDrawable = null;
+ Resources system = context.getResources().getSystem();
+ String pathString = system.getString(
+ system.getIdentifier(ResourceConstants.CONFIG_ICON_MASK,
+ "string", ResourceConstants.ANDROID_PACKAGE));
+ Path path = null;
+ if (!TextUtils.isEmpty(pathString)) {
+ path = PathParser.createPathFromPathData(pathString);
+ }
+ if (path != null) {
+ PathShape shape = new PathShape(path, PATH_SIZE, PATH_SIZE);
+ shapeDrawable = new ShapeDrawable(shape);
+ shapeDrawable.setIntrinsicHeight((int) PATH_SIZE);
+ shapeDrawable.setIntrinsicWidth((int) PATH_SIZE);
+ }
+ return new PreviewInfo(mSecondaryColorLight,
+ mSecondaryColorDark, mPrimaryColorLight, mPrimaryColorDark, mIcons,
+ shapeDrawable, system.getDimensionPixelOffset(
+ system.getIdentifier(ResourceConstants.CONFIG_CORNERRADIUS,
+ "dimen", ResourceConstants.ANDROID_PACKAGE)));
+ }
+
+ public Map<String, String> getPackages() {
+ return Collections.unmodifiableMap(mPackages);
+ }
+
+ /**
+ * Gets title of this {@link ColorBundle} object
+ * @return title string
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Sets title of bundle
+ * @param title specified title
+ * @return this of {@link Builder}
+ */
+ public Builder setTitle(String title) {
+ mTitle = title;
+ return this;
+ }
+
+ /**
+ * Sets color accent (light)
+ * @param colorSecondaryLight color accent light in {@link ColorInt}
+ * @return this of {@link Builder}
+ */
+ public Builder setColorSecondaryLight(@ColorInt int colorSecondaryLight) {
+ mSecondaryColorLight = colorSecondaryLight;
+ return this;
+ }
+
+ /**
+ * Sets color accent (dark)
+ * @param colorSecondaryDark color accent dark in {@link ColorInt}
+ * @return this of {@link Builder}
+ */
+ public Builder setColorSecondaryDark(@ColorInt int colorSecondaryDark) {
+ mSecondaryColorDark = colorSecondaryDark;
+ return this;
+ }
+
+ /**
+ * Sets color system palette (light)
+ * @param colorPrimaryLight color system palette in {@link ColorInt}
+ * @return this of {@link Builder}
+ */
+ public Builder setColorPrimaryLight(@ColorInt int colorPrimaryLight) {
+ mPrimaryColorLight = colorPrimaryLight;
+ return this;
+ }
+
+ /**
+ * Sets color system palette (dark)
+ * @param colorPrimaryDark color system palette in {@link ColorInt}
+ * @return this of {@link Builder}
+ */
+ public Builder setColorPrimaryDark(@ColorInt int colorPrimaryDark) {
+ mPrimaryColorDark = colorPrimaryDark;
+ return this;
+ }
+
+ /**
+ * Sets icon for bundle
+ * @param icon icon in {@link Drawable}
+ * @return this of {@link Builder}
+ */
+ public Builder addIcon(Drawable icon) {
+ mIcons.add(icon);
+ return this;
+ }
+
+ /**
+ * Sets overlay package for bundle
+ * @param category the category of bundle
+ * @param packageName tha name of package in the category
+ * @return this of {@link Builder}
+ */
+ public Builder addOverlayPackage(String category, String packageName) {
+ mPackages.put(category, packageName);
+ return this;
+ }
+
+ /**
+ * Sets the style of this color seed
+ * @param style color style of {@link Style}
+ * @return this of {@link Builder}
+ */
+ public Builder setStyle(Style style) {
+ mStyle = style;
+ return this;
+ }
+
+ /**
+ * Sets color option index of bundle
+ * @param index color option index
+ * @return this of {@link Builder}
+ */
+ public Builder setIndex(int index) {
+ mIndex = index;
+ return this;
+ }
+
+ /**
+ * Sets as default bundle
+ * @return this of {@link Builder}
+ */
+ public Builder asDefault() {
+ mIsDefault = true;
+ return this;
+ }
+ }
+}
diff --git a/src/com/android/customization/model/color/ColorBundlePreviewExtractor.java b/src/com/android/customization/model/color/ColorBundlePreviewExtractor.java
new file mode 100644
index 00000000..b67eec88
--- /dev/null
+++ b/src/com/android/customization/model/color/ColorBundlePreviewExtractor.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright (C) 2022 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.color;
+
+import static com.android.customization.model.ResourceConstants.ANDROID_PACKAGE;
+import static com.android.customization.model.ResourceConstants.ICONS_FOR_PREVIEW;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SYSTEM_PALETTE;
+import static com.android.customization.model.color.ColorUtils.toColorString;
+
+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.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.ColorInt;
+
+import com.android.systemui.monet.ColorScheme;
+import com.android.systemui.monet.Style;
+
+/**
+ * Utility class to read all the details of a color bundle for previewing it
+ * (eg, actual color values)
+ */
+class ColorBundlePreviewExtractor {
+
+ private static final String TAG = "ColorBundlePreviewExtractor";
+
+ private final PackageManager mPackageManager;
+
+ ColorBundlePreviewExtractor(Context context) {
+ mPackageManager = context.getPackageManager();
+ }
+
+ void addSecondaryColor(ColorBundle.Builder builder, @ColorInt int color) {
+ ColorScheme darkColorScheme = new ColorScheme(color, true);
+ ColorScheme lightColorScheme = new ColorScheme(color, false);
+ int lightSecondary = lightColorScheme.getAccentColor();
+ int darkSecondary = darkColorScheme.getAccentColor();
+ builder.addOverlayPackage(OVERLAY_CATEGORY_COLOR, toColorString(color))
+ .setColorSecondaryLight(lightSecondary)
+ .setColorSecondaryDark(darkSecondary);
+ }
+
+ void addPrimaryColor(ColorBundle.Builder builder, @ColorInt int color) {
+ ColorScheme darkColorScheme = new ColorScheme(color, true);
+ ColorScheme lightColorScheme = new ColorScheme(color, false);
+ int lightPrimary = lightColorScheme.getAccentColor();
+ int darkPrimary = darkColorScheme.getAccentColor();
+ builder.addOverlayPackage(OVERLAY_CATEGORY_SYSTEM_PALETTE, toColorString(color))
+ .setColorPrimaryLight(lightPrimary)
+ .setColorPrimaryDark(darkPrimary);
+ }
+
+ void addColorStyle(ColorBundle.Builder builder, String styleName) {
+ Style s = Style.TONAL_SPOT;
+ if (!TextUtils.isEmpty(styleName)) {
+ try {
+ s = Style.valueOf(styleName);
+ } catch (IllegalArgumentException e) {
+ Log.i(TAG, "Unknown style : " + styleName + ". Will default to TONAL_SPOT.");
+ }
+ }
+ builder.setStyle(s);
+ }
+
+ void addAndroidIconOverlay(ColorBundle.Builder builder) throws NameNotFoundException {
+ addSystemDefaultIcons(builder, ICONS_FOR_PREVIEW);
+ }
+
+ void addSystemDefaultIcons(ColorBundle.Builder builder, String... previewIcons) {
+ try {
+ for (String iconName : previewIcons) {
+ builder.addIcon(loadIconPreviewDrawable(iconName));
+ }
+ } catch (NameNotFoundException | NotFoundException e) {
+ Log.w(TAG, "Didn't find android package icons, will skip preview", e);
+ }
+ }
+
+ Drawable loadIconPreviewDrawable(String drawableName)
+ throws NameNotFoundException, NotFoundException {
+ Resources packageRes = mPackageManager.getResourcesForApplication(ANDROID_PACKAGE);
+ Resources res = Resources.getSystem();
+ return res.getDrawable(packageRes.getIdentifier(drawableName, "drawable",
+ ANDROID_PACKAGE), null);
+ }
+}
diff --git a/src/com/android/customization/model/color/ColorCustomizationManager.java b/src/com/android/customization/model/color/ColorCustomizationManager.java
new file mode 100644
index 00000000..f1f1e537
--- /dev/null
+++ b/src/com/android/customization/model/color/ColorCustomizationManager.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2022 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.color;
+
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SYSTEM_PALETTE;
+import static com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_PRESET;
+import static com.android.customization.model.color.ColorOptionsProvider.OVERLAY_COLOR_BOTH;
+import static com.android.customization.model.color.ColorOptionsProvider.OVERLAY_COLOR_INDEX;
+import static com.android.customization.model.color.ColorOptionsProvider.OVERLAY_COLOR_SOURCE;
+import static com.android.customization.model.color.ColorOptionsProvider.OVERLAY_THEME_STYLE;
+
+import android.app.WallpaperColors;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.customization.model.CustomizationManager;
+import com.android.customization.model.ResourceConstants;
+import com.android.customization.model.color.ColorOptionsProvider.ColorSource;
+import com.android.customization.model.theme.OverlayManagerCompat;
+import com.android.wallpaper.R;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** The Color manager to manage Color bundle related operations. */
+public class ColorCustomizationManager implements CustomizationManager<ColorOption> {
+
+ private static final String TAG = "ColorCustomizationManager";
+ private static final ExecutorService sExecutorService = Executors.newSingleThreadExecutor();
+
+ private static final Set<String> COLOR_OVERLAY_SETTINGS = new HashSet<>();
+ static {
+ COLOR_OVERLAY_SETTINGS.add(OVERLAY_CATEGORY_SYSTEM_PALETTE);
+ COLOR_OVERLAY_SETTINGS.add(OVERLAY_CATEGORY_COLOR);
+ COLOR_OVERLAY_SETTINGS.add(OVERLAY_COLOR_SOURCE);
+ COLOR_OVERLAY_SETTINGS.add(OVERLAY_THEME_STYLE);
+ }
+
+ private static ColorCustomizationManager sColorCustomizationManager;
+
+ private final ColorOptionsProvider mProvider;
+ private final OverlayManagerCompat mOverlayManagerCompat;
+ private final ContentResolver mContentResolver;
+ private final ContentObserver mObserver;
+
+ private Map<String, String> mCurrentOverlays;
+ @ColorSource private String mCurrentSource;
+ private String mCurrentStyle;
+ private WallpaperColors mHomeWallpaperColors;
+ private WallpaperColors mLockWallpaperColors;
+
+ /** Returns the {@link ColorCustomizationManager} instance. */
+ public static ColorCustomizationManager getInstance(Context context,
+ OverlayManagerCompat overlayManagerCompat) {
+ if (sColorCustomizationManager == null) {
+ Context appContext = context.getApplicationContext();
+ sColorCustomizationManager = new ColorCustomizationManager(
+ new ColorProvider(appContext,
+ appContext.getString(R.string.themes_stub_package)),
+ appContext.getContentResolver(), overlayManagerCompat);
+ }
+ return sColorCustomizationManager;
+ }
+
+ @VisibleForTesting
+ ColorCustomizationManager(ColorOptionsProvider provider, ContentResolver contentResolver,
+ OverlayManagerCompat overlayManagerCompat) {
+ mProvider = provider;
+ mContentResolver = contentResolver;
+ mObserver = new ContentObserver(/* handler= */ null) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ super.onChange(selfChange, uri);
+ // Resets current overlays when system's theme setting is changed.
+ if (TextUtils.equals(uri.getLastPathSegment(), ResourceConstants.THEME_SETTING)) {
+ Log.i(TAG, "Resetting " + mCurrentOverlays + " to null");
+ mCurrentOverlays = null;
+ }
+ }
+ };
+ mContentResolver.registerContentObserver(
+ Settings.Secure.CONTENT_URI, /* notifyForDescendants= */ true, mObserver);
+ mOverlayManagerCompat = overlayManagerCompat;
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return mOverlayManagerCompat.isAvailable() && mProvider.isAvailable();
+ }
+
+ @Override
+ public void apply(ColorOption theme, Callback callback) {
+ applyOverlays(theme, callback);
+ }
+
+ private void applyOverlays(ColorOption colorOption, Callback callback) {
+ sExecutorService.submit(() -> {
+ String currentStoredOverlays = getStoredOverlays();
+ if (TextUtils.isEmpty(currentStoredOverlays)) {
+ currentStoredOverlays = "{}";
+ }
+ JSONObject overlaysJson = null;
+ try {
+ overlaysJson = new JSONObject(currentStoredOverlays);
+ JSONObject colorJson = colorOption.getJsonPackages(true);
+ for (String setting : COLOR_OVERLAY_SETTINGS) {
+ overlaysJson.remove(setting);
+ }
+ for (Iterator<String> it = colorJson.keys(); it.hasNext(); ) {
+ String key = it.next();
+ overlaysJson.put(key, colorJson.get(key));
+ }
+ overlaysJson.put(OVERLAY_COLOR_SOURCE, colorOption.getSource());
+ overlaysJson.put(OVERLAY_COLOR_INDEX, String.valueOf(colorOption.getIndex()));
+ overlaysJson.put(OVERLAY_THEME_STYLE,
+ String.valueOf(colorOption.getStyle().toString()));
+
+ // OVERLAY_COLOR_BOTH is only for wallpaper color case, not preset.
+ if (!COLOR_SOURCE_PRESET.equals(colorOption.getSource())) {
+ boolean isForBoth =
+ (mLockWallpaperColors == null || mLockWallpaperColors.equals(
+ mHomeWallpaperColors));
+ overlaysJson.put(OVERLAY_COLOR_BOTH, isForBoth ? "1" : "0");
+ } else {
+ overlaysJson.remove(OVERLAY_COLOR_BOTH);
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ boolean allApplied = overlaysJson != null && Settings.Secure.putString(
+ mContentResolver, ResourceConstants.THEME_SETTING, overlaysJson.toString());
+ new Handler(Looper.getMainLooper()).post(() -> {
+ if (allApplied) {
+ callback.onSuccess();
+ } else {
+ callback.onError(null);
+ }
+ });
+ });
+ }
+
+ @Override
+ public void fetchOptions(OptionsFetchedListener<ColorOption> callback, boolean reload) {
+ WallpaperColors lockWallpaperColors = mLockWallpaperColors;
+ if (lockWallpaperColors != null && mLockWallpaperColors.equals(mHomeWallpaperColors)) {
+ lockWallpaperColors = null;
+ }
+ mProvider.fetch(callback, reload, mHomeWallpaperColors, lockWallpaperColors);
+ }
+
+ /**
+ * Sets the current wallpaper colors to extract seeds from
+ */
+ public void setWallpaperColors(WallpaperColors homeColors,
+ @Nullable WallpaperColors lockColors) {
+ mHomeWallpaperColors = homeColors;
+ mLockWallpaperColors = lockColors;
+ }
+
+ /**
+ * Gets current overlays mapping
+ * @return the {@link Map} of overlays
+ */
+ public Map<String, String> getCurrentOverlays() {
+ if (mCurrentOverlays == null) {
+ parseSettings(getStoredOverlays());
+ }
+ return mCurrentOverlays;
+ }
+
+ /**
+ * @return The source of the currently applied color. One of
+ * {@link ColorOptionsProvider#COLOR_SOURCE_HOME},{@link ColorOptionsProvider#COLOR_SOURCE_LOCK}
+ * or {@link ColorOptionsProvider#COLOR_SOURCE_PRESET}.
+ */
+ @ColorSource
+ public String getCurrentColorSource() {
+ if (mCurrentSource == null) {
+ parseSettings(getStoredOverlays());
+ }
+ return mCurrentSource;
+ }
+
+ /**
+ * @return The style of the currently applied color. One of enum values in
+ * {@link com.android.systemui.monet.Style}.
+ */
+ public String getCurrentStyle() {
+ if (mCurrentStyle == null) {
+ parseSettings(getStoredOverlays());
+ }
+ return mCurrentStyle;
+ }
+
+ public String getStoredOverlays() {
+ return Settings.Secure.getString(mContentResolver, ResourceConstants.THEME_SETTING);
+ }
+
+ @VisibleForTesting
+ void parseSettings(String serializedJson) {
+ Map<String, String> allSettings = parseColorSettings(serializedJson);
+ mCurrentSource = allSettings.remove(OVERLAY_COLOR_SOURCE);
+ mCurrentStyle = allSettings.remove(OVERLAY_THEME_STYLE);
+ mCurrentOverlays = allSettings;
+ }
+
+ private Map<String, String> parseColorSettings(String serializedJsonSettings) {
+ Map<String, String> overlayPackages = new HashMap<>();
+ if (serializedJsonSettings != null) {
+ try {
+ final JSONObject jsonPackages = new JSONObject(serializedJsonSettings);
+
+ JSONArray names = jsonPackages.names();
+ if (names != null) {
+ for (int i = 0; i < names.length(); i++) {
+ String category = names.getString(i);
+ if (COLOR_OVERLAY_SETTINGS.contains(category)) {
+ try {
+ overlayPackages.put(category, jsonPackages.getString(category));
+ } catch (JSONException e) {
+ Log.e(TAG, "parseColorOverlays: " + e.getLocalizedMessage(), e);
+ }
+ }
+ }
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "parseColorOverlays: " + e.getLocalizedMessage(), e);
+ }
+ }
+ return overlayPackages;
+ }
+}
diff --git a/src/com/android/customization/model/color/ColorOption.java b/src/com/android/customization/model/color/ColorOption.java
new file mode 100644
index 00000000..c8b28c29
--- /dev/null
+++ b/src/com/android/customization/model/color/ColorOption.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2022 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.color;
+
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SYSTEM_PALETTE;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.customization.model.CustomizationManager;
+import com.android.customization.model.CustomizationOption;
+import com.android.customization.model.color.ColorOptionsProvider.ColorSource;
+import com.android.systemui.monet.Style;
+import com.android.wallpaper.R;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Represents a color choice for the user.
+ * This could be a preset color or those obtained from a wallpaper.
+ */
+public abstract class ColorOption implements CustomizationOption<ColorOption> {
+
+ private static final String TAG = "ColorOption";
+ private static final String EMPTY_JSON = "{}";
+ @VisibleForTesting
+ static final String TIMESTAMP_FIELD = "_applied_timestamp";
+
+ protected final Map<String, String> mPackagesByCategory;
+ protected final int[] mPreviewColorIds = {R.id.color_preview_0, R.id.color_preview_1,
+ R.id.color_preview_2, R.id.color_preview_3};
+ private final String mTitle;
+ private final boolean mIsDefault;
+ private final Style mStyle;
+ private final int mIndex;
+ private CharSequence mContentDescription;
+
+ protected ColorOption(String title, Map<String, String> overlayPackages, boolean isDefault,
+ Style style, int index) {
+ mTitle = title;
+ mIsDefault = isDefault;
+ mStyle = style;
+ mIndex = index;
+ mPackagesByCategory = Collections.unmodifiableMap(removeNullValues(overlayPackages));
+ }
+
+ @Override
+ public String getTitle() {
+ return mTitle;
+ }
+
+ @Override
+ public boolean isActive(CustomizationManager<ColorOption> manager) {
+ ColorCustomizationManager colorManager = (ColorCustomizationManager) manager;
+
+ String currentStyle = colorManager.getCurrentStyle();
+ if (TextUtils.isEmpty(currentStyle)) {
+ currentStyle = Style.TONAL_SPOT.toString();
+ }
+ boolean isCurrentStyle = TextUtils.equals(getStyle().toString(), currentStyle);
+
+ if (mIsDefault) {
+ String serializedOverlays = colorManager.getStoredOverlays();
+ return (TextUtils.isEmpty(serializedOverlays) || EMPTY_JSON.equals(serializedOverlays)
+ || colorManager.getCurrentOverlays().isEmpty() || !(serializedOverlays.contains(
+ OVERLAY_CATEGORY_SYSTEM_PALETTE) || serializedOverlays.contains(
+ OVERLAY_CATEGORY_COLOR))) && isCurrentStyle;
+ } else {
+ Map<String, String> currentOverlays = colorManager.getCurrentOverlays();
+ String currentSource = colorManager.getCurrentColorSource();
+ boolean isCurrentSource = TextUtils.isEmpty(currentSource) || getSource().equals(
+ currentSource);
+ return isCurrentSource && isCurrentStyle && mPackagesByCategory.equals(currentOverlays);
+ }
+ }
+
+ /**
+ * 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(ColorOption other) {
+ if (other == null) {
+ return false;
+ }
+ if (mIsDefault) {
+ return other.isDefault() || TextUtils.isEmpty(other.getSerializedPackages())
+ || EMPTY_JSON.equals(other.getSerializedPackages());
+ }
+ // Map#equals ensures keys and values are compared.
+ return mPackagesByCategory.equals(other.mPackagesByCategory);
+ }
+
+ /**
+ * Returns the {@link PreviewInfo} object for this ColorOption
+ */
+ public abstract PreviewInfo getPreviewInfo();
+
+ boolean isDefault() {
+ return mIsDefault;
+ }
+
+ public Map<String, String> getPackagesByCategory() {
+ return mPackagesByCategory;
+ }
+
+ public String getSerializedPackages() {
+ return getJsonPackages(false).toString();
+ }
+
+ public String getSerializedPackagesWithTimestamp() {
+ return getJsonPackages(true).toString();
+ }
+
+ /**
+ * Get a JSONObject representation of this color option, with the current values for each
+ * field, and optionally a {@link TIMESTAMP_FIELD} field.
+ * @param insertTimestamp whether to add a field with the current timestamp
+ * @return the JSONObject for this color option
+ */
+ public JSONObject getJsonPackages(boolean insertTimestamp) {
+ JSONObject json;
+ if (isDefault()) {
+ json = new JSONObject();
+ } else {
+ json = new JSONObject(mPackagesByCategory);
+ // Remove items with null values to avoid deserialization issues.
+ removeNullValues(json);
+ }
+ if (insertTimestamp) {
+ try {
+ json.put(TIMESTAMP_FIELD, System.currentTimeMillis());
+ } catch (JSONException e) {
+ Log.e(TAG, "Couldn't add timestamp to serialized themebundle");
+ }
+ }
+ return json;
+ }
+
+ private void removeNullValues(JSONObject json) {
+ Iterator<String> keys = json.keys();
+ Set<String> keysToRemove = new HashSet<>();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ if (json.isNull(key)) {
+ keysToRemove.add(key);
+ }
+ }
+ for (String key : keysToRemove) {
+ json.remove(key);
+ }
+ }
+
+ private Map<String, String> removeNullValues(Map<String, String> map) {
+ return map.entrySet()
+ .stream()
+ .filter(entry -> entry.getValue() != null)
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ }
+
+ protected CharSequence getContentDescription(Context context) {
+ if (mContentDescription == null) {
+ CharSequence defaultName = context.getString(R.string.default_theme_title);
+ if (isDefault()) {
+ mContentDescription = defaultName;
+ } else {
+ mContentDescription = mTitle;
+ }
+ }
+ return mContentDescription;
+ }
+
+ /**
+ * @return the source of this color option
+ */
+ @ColorSource
+ public abstract String getSource();
+
+ /**
+ * @return the style of this color option
+ */
+ public Style getStyle() {
+ return mStyle;
+ }
+
+ /**
+ * @return the index of this color option
+ */
+ public int getIndex() {
+ return mIndex;
+ }
+
+ /**
+ * The preview information of {@link ColorOption}
+ */
+ public interface PreviewInfo {
+ }
+
+}
diff --git a/src/com/android/customization/model/color/ColorOptionsProvider.java b/src/com/android/customization/model/color/ColorOptionsProvider.java
new file mode 100644
index 00000000..2803c7bd
--- /dev/null
+++ b/src/com/android/customization/model/color/ColorOptionsProvider.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2022 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.color;
+
+import android.app.WallpaperColors;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.StringDef;
+
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+
+/**
+ * Interface for a class that can retrieve Colors from the system.
+ */
+public interface ColorOptionsProvider {
+
+ /**
+ * Extra setting indicating the source of the color overlays (it can be one of
+ * COLOR_SOURCE_PRESET, COLOR_SOURCE_HOME or COLOR_SOURCE_LOCK)
+ */
+ String OVERLAY_COLOR_SOURCE = "android.theme.customization.color_source";
+
+ /**
+ * Extra setting indicating the style of the color overlays (it can be one of
+ * {@link com.android.systemui.monet.Style}).
+ */
+ String OVERLAY_THEME_STYLE = "android.theme.customization.theme_style";
+
+ /**
+ * Users selected color option, its value starts from 1 (which means first option).
+ */
+ String OVERLAY_COLOR_INDEX = "android.theme.customization.color_index";
+
+ /**
+ * Users selected color from both home and lock screen.
+ * Example value: 0 means home or lock screen, 1 means both.
+ */
+ String OVERLAY_COLOR_BOTH = "android.theme.customization.color_both";
+
+ String COLOR_SOURCE_PRESET = "preset";
+ String COLOR_SOURCE_HOME = "home_wallpaper";
+ String COLOR_SOURCE_LOCK = "lock_wallpaper";
+
+ @StringDef({COLOR_SOURCE_PRESET, COLOR_SOURCE_HOME, COLOR_SOURCE_LOCK})
+ @interface ColorSource{}
+
+
+ /**
+ * 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.
+ * @param homeWallpaperColors to get seed colors from
+ * @param lockWallpaperColors WallpaperColors from the lockscreen wallpaper to get seeds from,
+ * if different than homeWallpaperColors
+ */
+ void fetch(OptionsFetchedListener<ColorOption> callback, boolean reload,
+ @Nullable WallpaperColors homeWallpaperColors,
+ @Nullable WallpaperColors lockWallpaperColors);
+}
diff --git a/src/com/android/customization/model/color/ColorProvider.kt b/src/com/android/customization/model/color/ColorProvider.kt
new file mode 100644
index 00000000..0ff3837a
--- /dev/null
+++ b/src/com/android/customization/model/color/ColorProvider.kt
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2022 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.color
+
+import android.app.WallpaperColors
+import android.content.Context
+import android.content.res.ColorStateList
+import android.content.res.Resources
+import android.text.TextUtils
+import androidx.annotation.ColorInt
+import androidx.core.graphics.ColorUtils.setAlphaComponent
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener
+import com.android.customization.model.ResourceConstants.COLOR_BUNDLES_ARRAY_NAME
+import com.android.customization.model.ResourceConstants.COLOR_BUNDLE_MAIN_COLOR_PREFIX
+import com.android.customization.model.ResourceConstants.COLOR_BUNDLE_NAME_PREFIX
+import com.android.customization.model.ResourceConstants.COLOR_BUNDLE_STYLE_PREFIX
+import com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SYSTEM_PALETTE
+import com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR
+import com.android.customization.model.ResourcesApkProvider
+import com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_HOME
+import com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_LOCK
+import com.android.customization.model.color.ColorUtils.toColorString
+import com.android.systemui.monet.ColorScheme
+import com.android.systemui.monet.Style
+import com.android.wallpaper.compat.WallpaperManagerCompat
+import com.android.wallpaper.module.InjectorProvider
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.util.*
+
+/**
+ * Default implementation of {@link ColorOptionsProvider} that reads preset colors from
+ * a stub APK.
+ */
+class ColorProvider(context: Context, stubPackageName: String) :
+ ResourcesApkProvider(context, stubPackageName), ColorOptionsProvider {
+
+ companion object {
+ const val themeStyleEnabled = true
+ val styleSize = if (themeStyleEnabled) Style.values().size else 1
+ private const val TAG = "ColorProvider"
+ private const val MAX_SEED_COLORS = 4
+ private const val MAX_PRESET_COLORS = 4
+ private const val ALPHA_MASK = 0xFF
+ }
+
+ private val monetEnabled = ColorUtils.isMonetEnabled(context)
+ // TODO(b/202145216): Use style method to fetch the list of style.
+ private var styleList = if (themeStyleEnabled) arrayOf(
+ Style.TONAL_SPOT, Style.SPRITZ, Style.VIBRANT, Style.EXPRESSIVE
+ ) else arrayOf(Style.TONAL_SPOT)
+
+ private val scope = if (mContext is LifecycleOwner) {
+ mContext.lifecycleScope
+ } else {
+ CoroutineScope(Dispatchers.Default + SupervisorJob())
+ }
+
+ private var colorsAvailable = true
+ private var colorBundles: List<ColorOption>? = null
+ private var homeWallpaperColors: WallpaperColors? = null
+ private var lockWallpaperColors: WallpaperColors? = null
+
+
+ override fun isAvailable(): Boolean {
+ return monetEnabled && super.isAvailable() && colorsAvailable
+ }
+
+ override fun fetch(callback: OptionsFetchedListener<ColorOption>?, reload: Boolean,
+ homeWallpaperColors: WallpaperColors?,
+ lockWallpaperColors: WallpaperColors?) {
+ val wallpaperColorsChanged = this.homeWallpaperColors != homeWallpaperColors
+ || this.lockWallpaperColors != lockWallpaperColors
+ if (wallpaperColorsChanged) {
+ this.homeWallpaperColors = homeWallpaperColors
+ this.lockWallpaperColors = lockWallpaperColors
+ }
+ if(colorBundles == null || reload || wallpaperColorsChanged) {
+ scope.launch {
+ try {
+ if (colorBundles == null || reload) {
+ loadPreset()
+ }
+ if (wallpaperColorsChanged || reload) {
+ loadSeedColors(homeWallpaperColors, lockWallpaperColors)
+ }
+ } catch (e: Throwable) {
+ colorsAvailable = false
+ callback?.onError(e)
+ return@launch
+ }
+ callback?.onOptionsLoaded(colorBundles)
+ }
+ } else {
+ callback?.onOptionsLoaded(colorBundles)
+ }
+ }
+
+ private fun isLockScreenWallpaperLastApplied(): Boolean {
+ // The WallpaperId increases every time a new wallpaper is set, so the larger wallpaper id
+ // is the most recently set wallpaper
+ val manager = InjectorProvider.getInjector().getWallpaperManagerCompat(mContext)
+ return manager.getWallpaperId(WallpaperManagerCompat.FLAG_LOCK) >
+ manager.getWallpaperId(WallpaperManagerCompat.FLAG_SYSTEM)
+ }
+
+ private fun loadSeedColors(homeWallpaperColors: WallpaperColors?,
+ lockWallpaperColors: WallpaperColors?) {
+ if (homeWallpaperColors == null) return
+
+ val bundles: MutableList<ColorOption> = ArrayList()
+ val colorsPerSource = if (lockWallpaperColors == null) {
+ MAX_SEED_COLORS
+ } else {
+ MAX_SEED_COLORS / 2
+ }
+
+ if (lockWallpaperColors != null) {
+ val shouldLockColorsGoFirst = isLockScreenWallpaperLastApplied()
+ // First half of the colors
+ buildColorSeeds(
+ if (shouldLockColorsGoFirst) lockWallpaperColors else homeWallpaperColors,
+ colorsPerSource,
+ if (shouldLockColorsGoFirst) COLOR_SOURCE_LOCK else COLOR_SOURCE_HOME,
+ true,
+ bundles)
+ // Second half of the colors
+ buildColorSeeds(
+ if (shouldLockColorsGoFirst) homeWallpaperColors else lockWallpaperColors,
+ MAX_SEED_COLORS - bundles.size / styleSize,
+ if (shouldLockColorsGoFirst) COLOR_SOURCE_HOME else COLOR_SOURCE_LOCK,
+ false,
+ bundles)
+ } else {
+ buildColorSeeds(homeWallpaperColors, colorsPerSource, COLOR_SOURCE_HOME, true, bundles)
+ }
+
+ bundles.addAll(colorBundles?.filterNot{it is ColorSeedOption} ?: emptyList())
+ colorBundles = bundles
+ }
+
+ private fun buildColorSeeds(wallpaperColors: WallpaperColors, maxColors: Int, source: String,
+ containsDefault: Boolean, bundles: MutableList<ColorOption>) {
+ val seedColors = ColorScheme.getSeedColors(wallpaperColors)
+ val defaultSeed = seedColors.first()
+ buildBundle(defaultSeed, 0, containsDefault, source, bundles)
+ for ((i, colorInt) in seedColors.drop(1).take(maxColors - 1).withIndex()) {
+ buildBundle(colorInt, i + 1, false, source, bundles)
+ }
+ }
+
+ private fun buildBundle(colorInt: Int, i: Int, isDefault: Boolean, source: String,
+ bundles: MutableList<ColorOption>) {
+ // TODO(b/202145216): Measure time cost in the loop.
+ for (style in styleList) {
+ val builder = ColorSeedOption.Builder()
+ val lightColorScheme = ColorScheme(colorInt, /* darkTheme= */ false, style)
+ val darkColorScheme = ColorScheme(colorInt, /* darkTheme= */ true, style)
+ builder.setLightColors(lightColorScheme.getLightColorPreview())
+ .setDarkColors(darkColorScheme.getDarkColorPreview())
+ .addOverlayPackage(OVERLAY_CATEGORY_SYSTEM_PALETTE,
+ if (isDefault) "" else toColorString(colorInt)
+ )
+ .addOverlayPackage(OVERLAY_CATEGORY_COLOR,
+ if (isDefault) "" else toColorString(colorInt)
+ )
+ .setSource(source)
+ .setStyle(style)
+ // Color option index value starts from 1.
+ .setIndex(i + 1)
+
+ if (isDefault) builder.asDefault()
+
+ bundles.add(builder.build())
+ }
+ }
+
+ /**
+ * Returns the colors for the light theme version of the preview of a ColorScheme
+ * based on this order:
+ * |-------|
+ * | 0 | 1 |
+ * |---+---|
+ * | 2 | 3 |
+ * |-------|
+ */
+ @ColorInt
+ private fun ColorScheme.getLightColorPreview(): IntArray {
+ return intArrayOf(setAlphaComponent(this.accent1[2], ALPHA_MASK),
+ setAlphaComponent(this.accent1[2], ALPHA_MASK),
+ ColorStateList.valueOf(this.accent3[6]).withLStar(85f).colors[0],
+ setAlphaComponent(this.accent1[6], ALPHA_MASK))
+ }
+
+ /**
+ * Returns the color for the dark theme version of the preview of a ColorScheme
+ * based on this order:
+ * |-------|
+ * | 0 | 1 |
+ * |---+---|
+ * | 2 | 3 |
+ * |-------|
+ */
+ @ColorInt
+ private fun ColorScheme.getDarkColorPreview(): IntArray {
+ return intArrayOf(setAlphaComponent(this.accent1[2], ALPHA_MASK),
+ setAlphaComponent(this.accent1[2], ALPHA_MASK),
+ ColorStateList.valueOf(this.accent3[6]).withLStar(85f).colors[0],
+ setAlphaComponent(this.accent1[6], ALPHA_MASK))
+ }
+
+ private fun ColorScheme.getPresetColorPreview(seed: Int): IntArray {
+ return when(this.style) {
+ Style.FRUIT_SALAD -> intArrayOf(seed, this.accent1[2])
+ Style.TONAL_SPOT -> intArrayOf(this.accentColor, this.accentColor)
+ else -> intArrayOf(this.accent1[2], this.accent1[2])
+ }
+ }
+
+ private suspend fun loadPreset() = withContext(Dispatchers.IO) {
+ val extractor = ColorBundlePreviewExtractor(mContext)
+ val bundles: MutableList<ColorOption> = ArrayList()
+
+ val bundleNames = getItemsFromStub(COLOR_BUNDLES_ARRAY_NAME)
+ // Color option index value starts from 1.
+ var index = 1
+ val maxPresetColors = if (themeStyleEnabled) bundleNames.size else MAX_PRESET_COLORS
+ for (bundleName in bundleNames.take(maxPresetColors)) {
+ val builder = ColorBundle.Builder()
+ builder.title = getItemStringFromStub(COLOR_BUNDLE_NAME_PREFIX, bundleName)
+ builder.setIndex(index)
+ val colorFromStub = getItemColorFromStub(COLOR_BUNDLE_MAIN_COLOR_PREFIX, bundleName)
+ extractor.addPrimaryColor(builder, colorFromStub)
+ extractor.addSecondaryColor(builder, colorFromStub)
+ if (themeStyleEnabled) {
+ val styleName = try {
+ getItemStringFromStub(COLOR_BUNDLE_STYLE_PREFIX, bundleName)
+ } catch (e: Resources.NotFoundException) {
+ null
+ }
+ extractor.addColorStyle(builder, styleName)
+ val style = try {
+ if (styleName != null) Style.valueOf(styleName) else Style.TONAL_SPOT
+ } catch (e: IllegalArgumentException) {
+ Style.TONAL_SPOT
+ }
+
+ val darkColors = ColorScheme(colorFromStub, true, style)
+ .getPresetColorPreview(colorFromStub)
+ val lightColors = ColorScheme(colorFromStub, false, style)
+ .getPresetColorPreview(colorFromStub)
+ builder.setColorPrimaryDark(darkColors[0]).setColorSecondaryDark(darkColors[1])
+ builder.setColorPrimaryLight(lightColors[0]).setColorPrimaryLight(lightColors[1])
+ }
+
+ extractor.addAndroidIconOverlay(builder)
+ bundles.add(builder.build(mContext))
+ index++
+ }
+
+ colorBundles = bundles
+ }
+}
diff --git a/src/com/android/customization/model/color/ColorSectionController.java b/src/com/android/customization/model/color/ColorSectionController.java
new file mode 100644
index 00000000..c3b6825c
--- /dev/null
+++ b/src/com/android/customization/model/color/ColorSectionController.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2022 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.color;
+
+import static android.view.View.VISIBLE;
+
+import static com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_HOME;
+import static com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_LOCK;
+import static com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_PRESET;
+import static com.android.customization.widget.OptionSelectorController.CheckmarkStyle.CENTER;
+
+import android.app.Activity;
+import android.app.WallpaperColors;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.stats.style.StyleEnums;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager2.widget.ViewPager2;
+
+import com.android.customization.model.CustomizationManager;
+import com.android.customization.model.theme.OverlayManagerCompat;
+import com.android.customization.module.CustomizationInjector;
+import com.android.customization.module.ThemesUserEventLogger;
+import com.android.customization.picker.color.ColorSectionView;
+import com.android.customization.widget.OptionSelectorController;
+import com.android.wallpaper.R;
+import com.android.wallpaper.model.CustomizationSectionController;
+import com.android.wallpaper.model.WallpaperColorsViewModel;
+import com.android.wallpaper.module.InjectorProvider;
+import com.android.wallpaper.widget.PageIndicator;
+import com.android.wallpaper.widget.SeparatedTabLayout;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Color section view's controller for the logic of color customization.
+ */
+public class ColorSectionController implements CustomizationSectionController<ColorSectionView> {
+
+ private static final String TAG = "ColorSectionController";
+ private static final String KEY_COLOR_TAB_POSITION = "COLOR_TAB_POSITION";
+ private static final long MIN_COLOR_APPLY_PERIOD = 500L;
+
+ private static final int WALLPAPER_TAB_INDEX = 0;
+ private static final int PRESET_TAB_INDEX = 1;
+
+ private final ThemesUserEventLogger mEventLogger;
+ private final ColorCustomizationManager mColorManager;
+ private final WallpaperColorsViewModel mWallpaperColorsViewModel;
+ private final LifecycleOwner mLifecycleOwner;
+ private final ColorSectionAdapter mColorSectionAdapter = new ColorSectionAdapter();
+ private final List<ColorOption> mWallpaperColorOptions = new ArrayList<>();
+ private final List<ColorOption> mPresetColorOptions = new ArrayList<>();
+
+ private ViewPager2 mColorSectionViewPager;
+ private ColorOption mSelectedColor;
+ private SeparatedTabLayout mTabLayout;
+ @Nullable private WallpaperColors mHomeWallpaperColors;
+ @Nullable private WallpaperColors mLockWallpaperColors;
+ // Uses a boolean value to indicate whether wallpaper color is ready because WallpaperColors
+ // maybe be null when it's ready.
+ private boolean mHomeWallpaperColorsReady;
+ private boolean mLockWallpaperColorsReady;
+ private Optional<Integer> mTabPositionToRestore = Optional.empty();
+ private long mLastColorApplyingTime = 0L;
+ private ColorSectionView mColorSectionView;
+
+ private static int getNumPages(int optionsPerPage, int totalOptions) {
+ return (int) Math.ceil((float) totalOptions / optionsPerPage);
+ }
+
+ public ColorSectionController(Activity activity, WallpaperColorsViewModel viewModel,
+ LifecycleOwner lifecycleOwner, @Nullable Bundle savedInstanceState) {
+ CustomizationInjector injector = (CustomizationInjector) InjectorProvider.getInjector();
+ mEventLogger = (ThemesUserEventLogger) injector.getUserEventLogger(activity);
+ mColorManager = ColorCustomizationManager.getInstance(activity,
+ new OverlayManagerCompat(activity));
+ mWallpaperColorsViewModel = viewModel;
+ mLifecycleOwner = lifecycleOwner;
+
+ if (savedInstanceState != null && savedInstanceState.containsKey(KEY_COLOR_TAB_POSITION)) {
+ mTabPositionToRestore = Optional.of(savedInstanceState.getInt(KEY_COLOR_TAB_POSITION));
+ }
+ }
+
+ @Override
+ public boolean isAvailable(@Nullable Context context) {
+ return context != null && ColorUtils.isMonetEnabled(context) && mColorManager.isAvailable();
+ }
+
+ @Override
+ public ColorSectionView createView(Context context) {
+ mColorSectionView = (ColorSectionView) LayoutInflater.from(context).inflate(
+ R.layout.color_section_view, /* root= */ null);
+ mColorSectionViewPager = mColorSectionView.findViewById(R.id.color_section_view_pager);
+ mColorSectionViewPager.setAdapter(mColorSectionAdapter);
+ mColorSectionViewPager.setUserInputEnabled(false);
+ mTabLayout = mColorSectionView.findViewById(R.id.separated_tabs);
+ mColorSectionAdapter.setNumColors(context.getResources().getInteger(
+ R.integer.options_grid_num_columns));
+ // TODO(b/202145216): Use just 2 views when tapping either button on top.
+ mTabLayout.setViewPager(mColorSectionViewPager);
+
+ mWallpaperColorsViewModel.getHomeWallpaperColors().observe(mLifecycleOwner,
+ homeColors -> {
+ mHomeWallpaperColors = homeColors;
+ mHomeWallpaperColorsReady = true;
+ maybeLoadColors();
+ });
+ mWallpaperColorsViewModel.getLockWallpaperColors().observe(mLifecycleOwner,
+ lockColors -> {
+ mLockWallpaperColors = lockColors;
+ mLockWallpaperColorsReady = true;
+ maybeLoadColors();
+ });
+ return mColorSectionView;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ if (mColorSectionViewPager != null) {
+ savedInstanceState.putInt(KEY_COLOR_TAB_POSITION,
+ mColorSectionViewPager.getCurrentItem());
+ }
+ }
+
+ private void maybeLoadColors() {
+ if (mHomeWallpaperColorsReady && mLockWallpaperColorsReady) {
+ mColorManager.setWallpaperColors(mHomeWallpaperColors, mLockWallpaperColors);
+ loadColorOptions(/* reload= */ false);
+ }
+ }
+
+ private void loadColorOptions(boolean reload) {
+ mColorManager.fetchOptions(new CustomizationManager.OptionsFetchedListener<ColorOption>() {
+ @Override
+ public void onOptionsLoaded(List<ColorOption> options) {
+ mWallpaperColorOptions.clear();
+ mPresetColorOptions.clear();
+
+ for (ColorOption option : options) {
+ if (option instanceof ColorSeedOption) {
+ mWallpaperColorOptions.add(option);
+ } else if (option instanceof ColorBundle) {
+ mPresetColorOptions.add(option);
+ }
+ }
+ mSelectedColor = findActiveColorOption(mWallpaperColorOptions,
+ mPresetColorOptions);
+ mTabLayout.post(()-> setUpColorViewPager());
+ }
+
+ @Override
+ public void onError(@Nullable Throwable throwable) {
+ if (throwable != null) {
+ Log.e(TAG, "Error loading theme bundles", throwable);
+ }
+ }
+ }, reload);
+ }
+
+ private void setUpColorViewPager() {
+ mColorSectionAdapter.notifyDataSetChanged();
+
+ if (mTabLayout != null && mTabLayout.getTabCount() == 0) {
+ mTabLayout.addTab(mTabLayout.newTab().setText(R.string.wallpaper_color_tab),
+ WALLPAPER_TAB_INDEX);
+ mTabLayout.addTab(mTabLayout.newTab().setText(R.string.preset_color_tab),
+ PRESET_TAB_INDEX);
+ }
+
+ if (mWallpaperColorOptions.isEmpty()) {
+ // Select preset tab and disable wallpaper tab.
+ mTabLayout.getTabAt(WALLPAPER_TAB_INDEX).view.setEnabled(false);
+ mColorSectionViewPager.setCurrentItem(PRESET_TAB_INDEX, /* smoothScroll= */ false);
+ return;
+ }
+
+ mColorSectionViewPager.setCurrentItem(
+ mTabPositionToRestore.orElseGet(
+ () -> COLOR_SOURCE_PRESET.equals(mColorManager.getCurrentColorSource())
+ ? PRESET_TAB_INDEX
+ : WALLPAPER_TAB_INDEX),
+ /* smoothScroll= */ false);
+
+ // Disable "wallpaper colors" and "basic colors" swiping for new color style.
+ mColorSectionViewPager.setUserInputEnabled(!ColorProvider.themeStyleEnabled);
+ }
+
+ private void setupWallpaperColorPages(ViewPager2 container, int colorsPerPage,
+ PageIndicator pageIndicator) {
+ container.setAdapter(new ColorPageAdapter(mWallpaperColorOptions, /* pageEnabled= */ true,
+ colorsPerPage));
+ if (ColorProvider.themeStyleEnabled) {
+ // Update page index to show selected items.
+ int selectedIndex = mWallpaperColorOptions.indexOf(mSelectedColor);
+ if (selectedIndex >= 0 && colorsPerPage != 0) {
+ int pageIndex = selectedIndex / colorsPerPage;
+ container.setCurrentItem(pageIndex, /* smoothScroll= */ false);
+ }
+ pageIndicator.setNumPages(getNumPages(colorsPerPage, mWallpaperColorOptions.size()));
+ registerOnPageChangeCallback(container, pageIndicator);
+ }
+ }
+
+ private void setupPresetColorPages(ViewPager2 container, int colorsPerPage,
+ PageIndicator pageIndicator) {
+ container.setAdapter(new ColorPageAdapter(mPresetColorOptions, /* pageEnabled= */ true,
+ colorsPerPage));
+ if (ColorProvider.themeStyleEnabled) {
+ // Update page index to show selected items.
+ int selectedIndex = mPresetColorOptions.indexOf(mSelectedColor);
+ if (selectedIndex >= 0 && colorsPerPage != 0) {
+ int pageIndex = selectedIndex / colorsPerPage;
+ container.setCurrentItem(pageIndex, /* smoothScroll= */ false);
+ }
+ pageIndicator.setNumPages(getNumPages(colorsPerPage, mPresetColorOptions.size()));
+ registerOnPageChangeCallback(container, pageIndicator);
+ }
+ }
+
+ private void registerOnPageChangeCallback(ViewPager2 container, PageIndicator pageIndicator) {
+ container.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
+ @Override
+ public void onPageSelected(int position) {
+ super.onPageSelected(position);
+ pageIndicator.setLocation(position);
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset,
+ int positionOffsetPixels) {
+ super.onPageScrolled(position, positionOffset, positionOffsetPixels);
+ pageIndicator.setLocation(position);
+ }
+ });
+ }
+
+ private void setupColorOptions(RecyclerView container, List<ColorOption> colorOptions,
+ boolean pageEnabled, int index, int colorsPerPage) {
+ int totalSize = colorOptions.size();
+ if (totalSize == 0) {
+ return;
+ }
+
+ List<ColorOption> subOptions;
+ if (pageEnabled && ColorProvider.themeStyleEnabled) {
+ subOptions = colorOptions.subList(colorsPerPage * index,
+ Math.min(colorsPerPage * (index + 1), totalSize));
+ } else {
+ subOptions = colorOptions;
+ }
+
+ OptionSelectorController<ColorOption> adaptiveController = new OptionSelectorController<>(
+ container, subOptions, /* useGrid= */ true, CENTER);
+ adaptiveController.initOptions(mColorManager);
+ setUpColorOptionsController(adaptiveController);
+ }
+
+ private ColorOption findActiveColorOption(List<ColorOption> wallpaperColorOptions,
+ List<ColorOption> presetColorOptions) {
+ ColorOption activeColorOption = null;
+ for (ColorOption colorOption : Lists.newArrayList(
+ Iterables.concat(wallpaperColorOptions, presetColorOptions))) {
+ if (colorOption.isActive(mColorManager)) {
+ activeColorOption = colorOption;
+ break;
+ }
+ }
+ // Use the first one option by default. This should not happen as above should have an
+ // active option found.
+ if (activeColorOption == null) {
+ activeColorOption = wallpaperColorOptions.isEmpty()
+ ? presetColorOptions.get(0)
+ : wallpaperColorOptions.get(0);
+ }
+ return activeColorOption;
+ }
+
+ private void setUpColorOptionsController(
+ OptionSelectorController<ColorOption> optionSelectorController) {
+ if (mSelectedColor != null && optionSelectorController.containsOption(mSelectedColor)) {
+ optionSelectorController.setSelectedOption(mSelectedColor);
+ }
+
+ optionSelectorController.addListener(selectedOption -> {
+ ColorOption selectedColor = (ColorOption) selectedOption;
+ if (mSelectedColor.equals(selectedColor)) {
+ return;
+ }
+ mSelectedColor = (ColorOption) selectedOption;
+ // Post with delay for color option to run ripple.
+ new Handler().postDelayed(()-> applyColor(mSelectedColor), /* delayMillis= */ 100);
+ });
+ }
+
+ private void applyColor(ColorOption colorOption) {
+ if (SystemClock.elapsedRealtime() - mLastColorApplyingTime < MIN_COLOR_APPLY_PERIOD) {
+ return;
+ }
+ mLastColorApplyingTime = SystemClock.elapsedRealtime();
+ mColorManager.apply(colorOption, new CustomizationManager.Callback() {
+ @Override
+ public void onSuccess() {
+ mColorSectionView.announceForAccessibility(
+ mColorSectionView.getContext().getString(R.string.color_changed));
+ mEventLogger.logColorApplied(getColorAction(colorOption), colorOption.getIndex());
+ }
+
+ @Override
+ public void onError(@Nullable Throwable throwable) {
+ Log.w(TAG, "Apply theme with error: " + throwable);
+ }
+ });
+ }
+
+ private int getColorAction(ColorOption colorOption) {
+ int action = StyleEnums.DEFAULT_ACTION;
+ boolean isForBoth = mLockWallpaperColors == null || mLockWallpaperColors.equals(
+ mHomeWallpaperColors);
+
+ if (TextUtils.equals(colorOption.getSource(), COLOR_SOURCE_PRESET)) {
+ action = StyleEnums.COLOR_PRESET_APPLIED;
+ } else if (isForBoth) {
+ action = StyleEnums.COLOR_WALLPAPER_HOME_LOCK_APPLIED;
+ } else {
+ switch (colorOption.getSource()) {
+ case COLOR_SOURCE_HOME:
+ action = StyleEnums.COLOR_WALLPAPER_HOME_APPLIED;
+ break;
+ case COLOR_SOURCE_LOCK:
+ action = StyleEnums.COLOR_WALLPAPER_LOCK_APPLIED;
+ break;
+ }
+ }
+ return action;
+ }
+
+ private class ColorSectionAdapter extends
+ RecyclerView.Adapter<ColorSectionAdapter.ColorPageViewHolder> {
+
+ private final int mItemCounts = new int[]{WALLPAPER_TAB_INDEX, PRESET_TAB_INDEX}.length;
+ private int mNumColors;
+
+ @Override
+ public int getItemCount() {
+ return mItemCounts;
+ }
+
+ @Override
+ public void onBindViewHolder(ColorPageViewHolder viewHolder, int position) {
+ switch (position) {
+ case WALLPAPER_TAB_INDEX:
+ setupWallpaperColorPages(viewHolder.mContainer, mNumColors,
+ viewHolder.mPageIndicator);
+ break;
+ case PRESET_TAB_INDEX:
+ setupPresetColorPages(viewHolder.mContainer, mNumColors,
+ viewHolder.mPageIndicator);
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public ColorPageViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
+ return new ColorPageViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(
+ viewType, viewGroup, false));
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return R.layout.color_pages_view;
+ }
+
+ public void setNumColors(int numColors) {
+ mNumColors = numColors;
+ }
+
+ private class ColorPageViewHolder extends RecyclerView.ViewHolder {
+ private ViewPager2 mContainer;
+ private PageIndicator mPageIndicator;
+
+ ColorPageViewHolder(View itemView) {
+ super(itemView);
+ mContainer = itemView.findViewById(R.id.color_page_container);
+ mPageIndicator = itemView.findViewById(R.id.color_page_indicator);
+ if (ColorProvider.themeStyleEnabled) {
+ mPageIndicator.setVisibility(VISIBLE);
+ }
+ }
+ }
+ }
+
+ private class ColorPageAdapter extends
+ RecyclerView.Adapter<ColorPageAdapter.ColorOptionViewHolder> {
+
+ private final boolean mPageEnabled;
+ private final List<ColorOption> mColorOptions;
+ private final int mColorsPerPage;
+
+ private ColorPageAdapter(List<ColorOption> colorOptions, boolean pageEnabled,
+ int colorsPerPage) {
+ mPageEnabled = pageEnabled;
+ mColorOptions = colorOptions;
+ mColorsPerPage = colorsPerPage;
+ }
+
+ @Override
+ public int getItemCount() {
+ if (!mPageEnabled || !ColorProvider.themeStyleEnabled) {
+ return 1;
+ }
+ // Color page size.
+ return getNumPages(mColorsPerPage, mColorOptions.size());
+ }
+
+ @Override
+ public void onBindViewHolder(ColorOptionViewHolder viewHolder, int position) {
+ setupColorOptions(viewHolder.mContainer, mColorOptions, mPageEnabled, position,
+ mColorsPerPage);
+ }
+
+ @Override
+ public ColorOptionViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
+ return new ColorOptionViewHolder(
+ LayoutInflater.from(viewGroup.getContext()).inflate(viewType, viewGroup,
+ false));
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return R.layout.color_options_view;
+ }
+
+ private class ColorOptionViewHolder extends RecyclerView.ViewHolder {
+ private RecyclerView mContainer;
+
+ ColorOptionViewHolder(View itemView) {
+ super(itemView);
+ mContainer = itemView.findViewById(R.id.color_option_container);
+ }
+ }
+ }
+}
diff --git a/src/com/android/customization/model/color/ColorSeedOption.java b/src/com/android/customization/model/color/ColorSeedOption.java
new file mode 100644
index 00000000..7bddcb0b
--- /dev/null
+++ b/src/com/android/customization/model/color/ColorSeedOption.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2022 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.color;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.PorterDuff.Mode;
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.customization.model.color.ColorOptionsProvider.ColorSource;
+import com.android.systemui.monet.Style;
+import com.android.wallpaper.R;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents a seed color obtained from WallpaperColors, for the user to chose as their theming
+ * option.
+ */
+public class ColorSeedOption extends ColorOption {
+
+ private final PreviewInfo mPreviewInfo;
+ @ColorSource
+ private final String mSource;
+
+ @VisibleForTesting
+ ColorSeedOption(String title, Map<String, String> overlayPackages, boolean isDefault,
+ @ColorSource String source, Style style, int index, PreviewInfo previewInfo) {
+ super(title, overlayPackages, isDefault, style, index);
+ mSource = source;
+ mPreviewInfo = previewInfo;
+ }
+
+ @Override
+ public PreviewInfo getPreviewInfo() {
+ return mPreviewInfo;
+ }
+
+ @Override
+ public String getSource() {
+ return mSource;
+ }
+
+ @Override
+ public int getLayoutResId() {
+ return R.layout.color_option;
+ }
+
+ @Override
+ public void bindThumbnailTile(View view) {
+ Resources res = view.getContext().getResources();
+ @ColorInt int[] colors = mPreviewInfo.resolveColors(res);
+
+ int padding = view.isActivated()
+ ? res.getDimensionPixelSize(R.dimen.color_seed_option_tile_padding_selected)
+ : res.getDimensionPixelSize(R.dimen.color_seed_option_tile_padding);
+ for (int i = 0; i < mPreviewColorIds.length; i++) {
+ ImageView colorPreviewImageView = view.findViewById(mPreviewColorIds[i]);
+ colorPreviewImageView.getDrawable().setColorFilter(colors[i], Mode.SRC);
+ colorPreviewImageView.setPadding(padding, padding, padding, padding);
+ }
+
+ view.setContentDescription(getContentDescription(view.getContext()));
+ }
+
+ @Override
+ protected CharSequence getContentDescription(Context context) {
+ // Override because we want all options with the same description.
+ return context.getString(R.string.wallpaper_color_title);
+ }
+
+ /**
+ * The preview information of {@link ColorOption}
+ */
+ public static class PreviewInfo implements ColorOption.PreviewInfo {
+ @ColorInt public int[] lightColors;
+ @ColorInt public int[] darkColors;
+
+ private PreviewInfo(@ColorInt int[] lightColors, @ColorInt int[] darkColors) {
+ this.lightColors = lightColors;
+ this.darkColors = darkColors;
+ }
+
+ /**
+ * Returns the colors to be applied corresponding with the current
+ * configuration's UI mode.
+ * @return one of {@link #lightColors} or {@link #darkColors}
+ */
+ @ColorInt
+ public int[] resolveColors(Resources res) {
+ boolean night = (res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
+ == Configuration.UI_MODE_NIGHT_YES;
+ return night ? darkColors : lightColors;
+ }
+ }
+
+ /**
+ * The builder of ColorSeedOption
+ */
+ public static class Builder {
+ protected String mTitle;
+ @ColorInt
+ private int[] mLightColors;
+ @ColorInt
+ private int[] mDarkColors;
+ @ColorSource
+ private String mSource;
+ private boolean mIsDefault;
+ private Style mStyle = Style.TONAL_SPOT;
+ private int mIndex;
+ protected Map<String, String> mPackages = new HashMap<>();
+
+ /**
+ * Builds the ColorSeedOption
+ * @return new {@link ColorOption} object
+ */
+ public ColorSeedOption build() {
+ return new ColorSeedOption(mTitle, mPackages, mIsDefault, mSource, mStyle, mIndex,
+ createPreviewInfo());
+ }
+
+ /**
+ * Creates preview information
+ * @return the {@link PreviewInfo} object
+ */
+ public PreviewInfo createPreviewInfo() {
+ return new PreviewInfo(mLightColors, mDarkColors);
+ }
+
+ public Map<String, String> getPackages() {
+ return Collections.unmodifiableMap(mPackages);
+ }
+
+ /**
+ * Gets title of {@link ColorOption} object
+ * @return title string
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Sets title of bundle
+ * @param title specified title
+ * @return this of {@link ColorBundle.Builder}
+ */
+ public Builder setTitle(String title) {
+ mTitle = title;
+ return this;
+ }
+
+ /**
+ * Sets the colors for preview in light mode
+ * @param lightColors {@link ColorInt} colors for light mode
+ * @return this of {@link Builder}
+ */
+ public Builder setLightColors(@ColorInt int[] lightColors) {
+ mLightColors = lightColors;
+ return this;
+ }
+
+ /**
+ * Sets the colors for preview in light mode
+ * @param darkColors {@link ColorInt} colors for light mode
+ * @return this of {@link Builder}
+ */
+ public Builder setDarkColors(@ColorInt int[] darkColors) {
+ mDarkColors = darkColors;
+ return this;
+ }
+
+
+ /**
+ * Sets overlay package for bundle
+ * @param category the category of bundle
+ * @param packageName tha name of package in the category
+ * @return this of {@link Builder}
+ */
+ public Builder addOverlayPackage(String category, String packageName) {
+ mPackages.put(category, packageName);
+ return this;
+ }
+
+ /**
+ * Sets the source of this color seed
+ * @param source typically either {@link ColorOptionsProvider#COLOR_SOURCE_HOME} or
+ * {@link ColorOptionsProvider#COLOR_SOURCE_LOCK}
+ * @return this of {@link Builder}
+ */
+ public Builder setSource(@ColorSource String source) {
+ mSource = source;
+ return this;
+ }
+
+ /**
+ * Sets the source of this color seed
+ * @param style color style of {@link Style}
+ * @return this of {@link Builder}
+ */
+ public Builder setStyle(Style style) {
+ mStyle = style;
+ return this;
+ }
+
+ /**
+ * Sets color option index of seed
+ * @param index color option index
+ * @return this of {@link ColorBundle.Builder}
+ */
+ public Builder setIndex(int index) {
+ mIndex = index;
+ return this;
+ }
+
+ /**
+ * Sets as default bundle
+ * @return this of {@link Builder}
+ */
+ public Builder asDefault() {
+ mIsDefault = true;
+ return this;
+ }
+ }
+}
diff --git a/src/com/android/customization/model/color/ColorUtils.kt b/src/com/android/customization/model/color/ColorUtils.kt
new file mode 100644
index 00000000..f07ff319
--- /dev/null
+++ b/src/com/android/customization/model/color/ColorUtils.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 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.color
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.os.SystemProperties
+import android.util.Log
+import androidx.annotation.ColorInt
+
+/**
+ * Utility to wrap Monet's color extraction
+ */
+object ColorUtils {
+ private const val TAG = "ColorUtils"
+ private const val MONET_FLAG = "flag_monet"
+ private var sSysuiRes: Resources? = null
+ private var sFlagId = 0
+
+ /**
+ * Returns true if color extraction is enabled in systemui.
+ */
+ @JvmStatic
+ fun isMonetEnabled(context: Context): Boolean {
+ var monetEnabled = SystemProperties.getBoolean("persist.systemui.flag_monet", false)
+ if (!monetEnabled) {
+ if (sSysuiRes == null) {
+ try {
+ val pm = context.packageManager
+ val sysUIInfo = pm.getApplicationInfo("com.android.systemui",
+ PackageManager.GET_META_DATA or PackageManager.MATCH_SYSTEM_ONLY)
+ if (sysUIInfo != null) {
+ sSysuiRes = pm.getResourcesForApplication(sysUIInfo)
+ }
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.w(TAG, "Couldn't read color flag, skipping section", e)
+ }
+ }
+ if (sFlagId == 0) {
+ sFlagId = if (sSysuiRes == null) 0 else sSysuiRes!!.getIdentifier(
+ MONET_FLAG, "bool", "com.android.systemui")
+ }
+ if (sFlagId > 0) {
+ monetEnabled = sSysuiRes!!.getBoolean(sFlagId)
+ }
+ }
+ return monetEnabled
+ }
+
+ @JvmStatic
+ fun toColorString(@ColorInt color: Int): String {
+ return String.format("%06X", 0xFFFFFF and color)
+ }
+} \ No newline at end of file
diff --git a/src/com/android/customization/model/color/WallpaperColorResources.java b/src/com/android/customization/model/color/WallpaperColorResources.java
new file mode 100644
index 00000000..eb8b39be
--- /dev/null
+++ b/src/com/android/customization/model/color/WallpaperColorResources.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 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.color;
+
+import android.app.WallpaperColors;
+import android.content.Context;
+import android.util.SparseIntArray;
+import android.widget.RemoteViews.ColorResources;
+
+import com.android.systemui.monet.ColorScheme;
+
+import java.util.List;
+
+/** A class to override colors in a {@link Context} with wallpaper colors. */
+public class WallpaperColorResources {
+
+ private final SparseIntArray mColorOverlay = new SparseIntArray();
+
+ public WallpaperColorResources(WallpaperColors wallpaperColors) {
+ ColorScheme wallpaperColorScheme = new ColorScheme(wallpaperColors, /* darkTheme= */ false);
+ addOverlayColor(wallpaperColorScheme.getNeutral1(), android.R.color.system_neutral1_10);
+ addOverlayColor(wallpaperColorScheme.getNeutral2(), android.R.color.system_neutral2_10);
+ addOverlayColor(wallpaperColorScheme.getAccent1(), android.R.color.system_accent1_10);
+ addOverlayColor(wallpaperColorScheme.getAccent2(), android.R.color.system_accent2_10);
+ addOverlayColor(wallpaperColorScheme.getAccent3(), android.R.color.system_accent3_10);
+ }
+
+ /** Applies the wallpaper color resources to the {@code context}. */
+ public void apply(Context context) {
+ ColorResources.create(context, mColorOverlay).apply(context);
+ }
+
+ private void addOverlayColor(List<Integer> colors, int firstResourceColorId) {
+ int resourceColorId = firstResourceColorId;
+ for (int color : colors) {
+ mColorOverlay.put(resourceColorId, color);
+ resourceColorId++;
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/color/ColorSectionView.java b/src/com/android/customization/picker/color/ColorSectionView.java
new file mode 100644
index 00000000..b8ba2e4e
--- /dev/null
+++ b/src/com/android/customization/picker/color/ColorSectionView.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 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.picker.color;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+
+import com.android.wallpaper.picker.SectionView;
+
+/**
+ * The class inherits from {@link SectionView} as the view representing the color section of the
+ * customization picker.
+ */
+public final class ColorSectionView extends SectionView {
+ public ColorSectionView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+}