From 53fc4d5aa3aa40a23b3819add0d0f1f6e1b6e796 Mon Sep 17 00:00:00 2001 From: Kunhung Li Date: Sat, 12 Feb 2022 16:58:06 +0800 Subject: Move color picking code into AOSP Add color picking related code, resources and libraries. Bug: 218396282 Test: manual Change-Id: I6aa244c8dac69f4663f97e19e91cfb6cf3eb0990 --- .../customization/model/ResourceConstants.java | 8 + .../customization/model/color/ColorBundle.java | 350 +++++++++++++++ .../model/color/ColorBundlePreviewExtractor.java | 105 +++++ .../model/color/ColorCustomizationManager.java | 265 ++++++++++++ .../customization/model/color/ColorOption.java | 223 ++++++++++ .../model/color/ColorOptionsProvider.java | 77 ++++ .../customization/model/color/ColorProvider.kt | 281 ++++++++++++ .../model/color/ColorSectionController.java | 474 +++++++++++++++++++++ .../customization/model/color/ColorSeedOption.java | 244 +++++++++++ .../customization/model/color/ColorUtils.kt | 68 +++ .../model/color/WallpaperColorResources.java | 53 +++ .../picker/color/ColorSectionView.java | 33 ++ 12 files changed, 2181 insertions(+) create mode 100644 src/com/android/customization/model/color/ColorBundle.java create mode 100644 src/com/android/customization/model/color/ColorBundlePreviewExtractor.java create mode 100644 src/com/android/customization/model/color/ColorCustomizationManager.java create mode 100644 src/com/android/customization/model/color/ColorOption.java create mode 100644 src/com/android/customization/model/color/ColorOptionsProvider.java create mode 100644 src/com/android/customization/model/color/ColorProvider.kt create mode 100644 src/com/android/customization/model/color/ColorSectionController.java create mode 100644 src/com/android/customization/model/color/ColorSeedOption.java create mode 100644 src/com/android/customization/model/color/ColorUtils.kt create mode 100644 src/com/android/customization/model/color/WallpaperColorResources.java create mode 100644 src/com/android/customization/picker/color/ColorSectionView.java (limited to 'src') 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 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 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 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 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 mIcons = new ArrayList<>(); + private boolean mIsDefault; + private Style mStyle = Style.TONAL_SPOT; + private int mIndex; + protected Map 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 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 { + + private static final String TAG = "ColorCustomizationManager"; + private static final ExecutorService sExecutorService = Executors.newSingleThreadExecutor(); + + private static final Set 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 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 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 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 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 allSettings = parseColorSettings(serializedJson); + mCurrentSource = allSettings.remove(OVERLAY_COLOR_SOURCE); + mCurrentStyle = allSettings.remove(OVERLAY_THEME_STYLE); + mCurrentOverlays = allSettings; + } + + private Map parseColorSettings(String serializedJsonSettings) { + Map 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 { + + private static final String TAG = "ColorOption"; + private static final String EMPTY_JSON = "{}"; + @VisibleForTesting + static final String TIMESTAMP_FIELD = "_applied_timestamp"; + + protected final Map 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 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 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 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 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 keys = json.keys(); + Set 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 removeNullValues(Map 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 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? = 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?, 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 = 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) { + 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) { + // 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 = 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 { + + 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 mWallpaperColorOptions = new ArrayList<>(); + private final List 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 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() { + @Override + public void onOptionsLoaded(List 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 colorOptions, + boolean pageEnabled, int index, int colorsPerPage) { + int totalSize = colorOptions.size(); + if (totalSize == 0) { + return; + } + + List subOptions; + if (pageEnabled && ColorProvider.themeStyleEnabled) { + subOptions = colorOptions.subList(colorsPerPage * index, + Math.min(colorsPerPage * (index + 1), totalSize)); + } else { + subOptions = colorOptions; + } + + OptionSelectorController adaptiveController = new OptionSelectorController<>( + container, subOptions, /* useGrid= */ true, CENTER); + adaptiveController.initOptions(mColorManager); + setUpColorOptionsController(adaptiveController); + } + + private ColorOption findActiveColorOption(List wallpaperColorOptions, + List 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 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 { + + 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 { + + private final boolean mPageEnabled; + private final List mColorOptions; + private final int mColorsPerPage; + + private ColorPageAdapter(List 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 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 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 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 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); + } +} -- cgit v1.2.3