1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.customization.model.color; 17 18 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR; 19 import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SYSTEM_PALETTE; 20 21 import android.content.Context; 22 import android.graphics.Color; 23 import android.text.TextUtils; 24 import android.util.Log; 25 26 import androidx.annotation.VisibleForTesting; 27 28 import com.android.customization.model.CustomizationManager; 29 import com.android.customization.model.CustomizationOption; 30 import com.android.customization.model.color.ColorOptionsProvider.ColorSource; 31 import com.android.customization.module.logging.ThemesUserEventLogger; 32 import com.android.systemui.monet.Style; 33 import com.android.themepicker.R; 34 35 import org.json.JSONException; 36 import org.json.JSONObject; 37 38 import java.util.Collections; 39 import java.util.HashSet; 40 import java.util.Iterator; 41 import java.util.Map; 42 import java.util.Set; 43 import java.util.stream.Collectors; 44 45 /** 46 * Represents a color choice for the user. 47 * This could be a preset color or those obtained from a wallpaper. 48 */ 49 public abstract class ColorOption implements CustomizationOption<ColorOption> { 50 51 private static final String TAG = "ColorOption"; 52 private static final String EMPTY_JSON = "{}"; 53 @VisibleForTesting 54 static final String TIMESTAMP_FIELD = "_applied_timestamp"; 55 56 protected final Map<String, String> mPackagesByCategory; 57 private final String mTitle; 58 private final boolean mIsDefault; 59 private final Style mStyle; 60 private final int mIndex; 61 private CharSequence mContentDescription; 62 ColorOption(String title, Map<String, String> overlayPackages, boolean isDefault, Style style, int index)63 protected ColorOption(String title, Map<String, String> overlayPackages, boolean isDefault, 64 Style style, int index) { 65 mTitle = title; 66 mIsDefault = isDefault; 67 mStyle = style; 68 mIndex = index; 69 mPackagesByCategory = Collections.unmodifiableMap(removeNullValues(overlayPackages)); 70 } 71 72 @Override getTitle()73 public String getTitle() { 74 return mTitle; 75 } 76 77 @Override isActive(CustomizationManager<ColorOption> manager)78 public boolean isActive(CustomizationManager<ColorOption> manager) { 79 ColorCustomizationManager colorManager = (ColorCustomizationManager) manager; 80 81 String currentStyle = colorManager.getCurrentStyle(); 82 if (TextUtils.isEmpty(currentStyle)) { 83 currentStyle = Style.TONAL_SPOT.toString(); 84 } 85 boolean isCurrentStyle = TextUtils.equals(getStyle().toString(), currentStyle); 86 87 if (mIsDefault) { 88 String serializedOverlays = colorManager.getStoredOverlays(); 89 // a default color option is active if the manager has no stored overlays or current 90 // overlays, or the stored overlay does not contain either category system palette or 91 // category color 92 return (TextUtils.isEmpty(serializedOverlays) || EMPTY_JSON.equals(serializedOverlays) 93 || colorManager.getCurrentOverlays().isEmpty() || !(serializedOverlays.contains( 94 OVERLAY_CATEGORY_SYSTEM_PALETTE) || serializedOverlays.contains( 95 OVERLAY_CATEGORY_COLOR))) && isCurrentStyle; 96 } else { 97 Map<String, String> currentOverlays = colorManager.getCurrentOverlays(); 98 String currentSource = colorManager.getCurrentColorSource(); 99 boolean isCurrentSource = TextUtils.isEmpty(currentSource) || getSource().equals( 100 currentSource); 101 return isCurrentSource && isCurrentStyle && mPackagesByCategory.equals(currentOverlays); 102 } 103 } 104 105 /** 106 * Gets the seed color from the overlay packages for logging. 107 * 108 * @return an int representing the seed color, or NULL_SEED_COLOR 109 */ getSeedColorForLogging()110 public int getSeedColorForLogging() { 111 String seedColor = mPackagesByCategory.get(OVERLAY_CATEGORY_SYSTEM_PALETTE); 112 if (seedColor == null || seedColor.isEmpty()) { 113 return ThemesUserEventLogger.NULL_SEED_COLOR; 114 } 115 if (!seedColor.startsWith("#")) { 116 seedColor = "#" + seedColor; 117 } 118 return Color.parseColor(seedColor); 119 } 120 121 /** 122 * This is similar to #equals() but it only compares this theme's packages with the other, that 123 * is, it will return true if applying this theme has the same effect of applying the given one. 124 */ isEquivalent(ColorOption other)125 public boolean isEquivalent(ColorOption other) { 126 if (other == null) { 127 return false; 128 } 129 if (mStyle != other.getStyle()) { 130 return false; 131 } 132 String thisSerializedPackages = getSerializedPackages(); 133 if (mIsDefault || TextUtils.isEmpty(thisSerializedPackages) 134 || EMPTY_JSON.equals(thisSerializedPackages)) { 135 String otherSerializedPackages = other.getSerializedPackages(); 136 return other.isDefault() || TextUtils.isEmpty(otherSerializedPackages) 137 || EMPTY_JSON.equals(otherSerializedPackages); 138 } 139 // Map#equals ensures keys and values are compared. 140 return mPackagesByCategory.equals(other.mPackagesByCategory); 141 } 142 143 /** 144 * Returns the {@link PreviewInfo} object for this ColorOption 145 */ getPreviewInfo()146 public abstract PreviewInfo getPreviewInfo(); 147 isDefault()148 boolean isDefault() { 149 return mIsDefault; 150 } 151 getPackagesByCategory()152 public Map<String, String> getPackagesByCategory() { 153 return mPackagesByCategory; 154 } 155 getSerializedPackages()156 public String getSerializedPackages() { 157 return getJsonPackages(false).toString(); 158 } 159 getSerializedPackagesWithTimestamp()160 public String getSerializedPackagesWithTimestamp() { 161 return getJsonPackages(true).toString(); 162 } 163 164 /** 165 * Get a JSONObject representation of this color option, with the current values for each 166 * field, and optionally a {@link TIMESTAMP_FIELD} field. 167 * @param insertTimestamp whether to add a field with the current timestamp 168 * @return the JSONObject for this color option 169 */ getJsonPackages(boolean insertTimestamp)170 public JSONObject getJsonPackages(boolean insertTimestamp) { 171 JSONObject json; 172 if (isDefault()) { 173 json = new JSONObject(); 174 } else { 175 json = new JSONObject(mPackagesByCategory); 176 // Remove items with null values to avoid deserialization issues. 177 removeNullValues(json); 178 } 179 if (insertTimestamp) { 180 try { 181 json.put(TIMESTAMP_FIELD, System.currentTimeMillis()); 182 } catch (JSONException e) { 183 Log.e(TAG, "Couldn't add timestamp to serialized themebundle"); 184 } 185 } 186 return json; 187 } 188 removeNullValues(JSONObject json)189 private void removeNullValues(JSONObject json) { 190 Iterator<String> keys = json.keys(); 191 Set<String> keysToRemove = new HashSet<>(); 192 while (keys.hasNext()) { 193 String key = keys.next(); 194 if (json.isNull(key)) { 195 keysToRemove.add(key); 196 } 197 } 198 for (String key : keysToRemove) { 199 json.remove(key); 200 } 201 } 202 removeNullValues(Map<String, String> map)203 private Map<String, String> removeNullValues(Map<String, String> map) { 204 return map.entrySet() 205 .stream() 206 .filter(entry -> entry.getValue() != null) 207 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 208 } 209 210 /** */ getContentDescription(Context context)211 public CharSequence getContentDescription(Context context) { 212 if (mContentDescription == null) { 213 CharSequence defaultName = context.getString(R.string.default_theme_title); 214 if (isDefault()) { 215 mContentDescription = defaultName; 216 } else { 217 mContentDescription = mTitle; 218 } 219 } 220 return mContentDescription; 221 } 222 223 /** 224 * @return the source of this color option 225 */ 226 @ColorSource getSource()227 public abstract String getSource(); 228 229 /** 230 * @return the source of this color option for logging 231 */ 232 @ThemesUserEventLogger.ColorSource getSourceForLogging()233 public abstract int getSourceForLogging(); 234 235 /** 236 * @return the style of this color option 237 */ getStyle()238 public Style getStyle() { 239 return mStyle; 240 } 241 242 /** 243 * @return the style of this color option for logging 244 */ getStyleForLogging()245 public abstract int getStyleForLogging(); 246 247 /** 248 * @return the index of this color option 249 */ getIndex()250 public int getIndex() { 251 return mIndex; 252 } 253 254 /** 255 * The preview information of {@link ColorOption} 256 */ 257 public interface PreviewInfo { 258 } 259 260 } 261