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.text.TextUtils; 23 import android.util.Log; 24 25 import androidx.annotation.ColorInt; 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.Objects; 43 import java.util.Set; 44 import java.util.stream.Collectors; 45 46 /** 47 * Represents a color choice for the user. 48 * This could be a preset color or those obtained from a wallpaper. 49 */ 50 public abstract class ColorOption implements CustomizationOption<ColorOption> { 51 52 private static final String TAG = "ColorOption"; 53 private static final String EMPTY_JSON = "{}"; 54 @VisibleForTesting 55 static final String TIMESTAMP_FIELD = "_applied_timestamp"; 56 57 protected final Map<String, String> mPackagesByCategory; 58 private final String mTitle; 59 private final boolean mIsDefault; 60 @Style.Type 61 private final Integer mStyle; 62 private final int mIndex; 63 private CharSequence mContentDescription; 64 private final @ColorInt int mSeedColor; 65 ColorOption(String title, Map<String, String> overlayPackages, boolean isDefault, int seedColor, @Style.Type Integer style, int index)66 protected ColorOption(String title, Map<String, String> overlayPackages, boolean isDefault, 67 int seedColor, @Style.Type Integer style, int index) { 68 mTitle = title; 69 mIsDefault = isDefault; 70 mSeedColor = seedColor; 71 mStyle = style; 72 mIndex = index; 73 mPackagesByCategory = Collections.unmodifiableMap(removeNullValues(overlayPackages)); 74 } 75 76 @Override getTitle()77 public String getTitle() { 78 return mTitle; 79 } 80 81 @Override isActive(CustomizationManager<ColorOption> manager)82 public boolean isActive(CustomizationManager<ColorOption> manager) { 83 ColorCustomizationManager colorManager = (ColorCustomizationManager) manager; 84 85 String currentStyle = colorManager.getCurrentStyle(); 86 if (TextUtils.isEmpty(currentStyle)) { 87 currentStyle = Style.toString(Style.TONAL_SPOT); 88 } 89 boolean isCurrentStyle = TextUtils.equals(Style.toString(getStyle()), currentStyle); 90 91 if (mIsDefault) { 92 String serializedOverlays = colorManager.getStoredOverlays(); 93 // a default color option is active if the manager has no stored overlays or current 94 // overlays, or the stored overlay does not contain either category system palette or 95 // category color 96 return (TextUtils.isEmpty(serializedOverlays) || EMPTY_JSON.equals(serializedOverlays) 97 || colorManager.getCurrentOverlays().isEmpty() || !(serializedOverlays.contains( 98 OVERLAY_CATEGORY_SYSTEM_PALETTE) || serializedOverlays.contains( 99 OVERLAY_CATEGORY_COLOR))) && isCurrentStyle; 100 } else { 101 Map<String, String> currentOverlays = colorManager.getCurrentOverlays(); 102 String currentSource = colorManager.getCurrentColorSource(); 103 boolean isCurrentSource = TextUtils.isEmpty(currentSource) || getSource().equals( 104 currentSource); 105 return isCurrentSource && isCurrentStyle && mPackagesByCategory.equals(currentOverlays); 106 } 107 } 108 getSeedColor()109 public @ColorInt int getSeedColor() { 110 return mSeedColor; 111 } 112 113 /** 114 * This is similar to #equals() but it only compares this theme's packages with the other, that 115 * is, it will return true if applying this theme has the same effect of applying the given one. 116 */ isEquivalent(ColorOption other)117 public boolean isEquivalent(ColorOption other) { 118 if (other == null) { 119 return false; 120 } 121 if (!Objects.equals(mStyle, other.getStyle())) { 122 return false; 123 } 124 String thisSerializedPackages = getSerializedPackages(); 125 if (mIsDefault || TextUtils.isEmpty(thisSerializedPackages) 126 || EMPTY_JSON.equals(thisSerializedPackages)) { 127 String otherSerializedPackages = other.getSerializedPackages(); 128 return other.isDefault() || TextUtils.isEmpty(otherSerializedPackages) 129 || EMPTY_JSON.equals(otherSerializedPackages); 130 } 131 // Map#equals ensures keys and values are compared. 132 return mPackagesByCategory.equals(other.mPackagesByCategory); 133 } 134 135 /** 136 * Returns the {@link PreviewInfo} object for this ColorOption 137 */ getPreviewInfo()138 public abstract PreviewInfo getPreviewInfo(); 139 isDefault()140 boolean isDefault() { 141 return mIsDefault; 142 } 143 getPackagesByCategory()144 public Map<String, String> getPackagesByCategory() { 145 return mPackagesByCategory; 146 } 147 getSerializedPackages()148 public String getSerializedPackages() { 149 return getJsonPackages(false).toString(); 150 } 151 getSerializedPackagesWithTimestamp()152 public String getSerializedPackagesWithTimestamp() { 153 return getJsonPackages(true).toString(); 154 } 155 156 /** 157 * Get a JSONObject representation of this color option, with the current values for each 158 * field, and optionally a {@link TIMESTAMP_FIELD} field. 159 * @param insertTimestamp whether to add a field with the current timestamp 160 * @return the JSONObject for this color option 161 */ getJsonPackages(boolean insertTimestamp)162 public JSONObject getJsonPackages(boolean insertTimestamp) { 163 JSONObject json; 164 if (isDefault()) { 165 json = new JSONObject(); 166 } else { 167 json = new JSONObject(mPackagesByCategory); 168 // Remove items with null values to avoid deserialization issues. 169 removeNullValues(json); 170 } 171 if (insertTimestamp) { 172 try { 173 json.put(TIMESTAMP_FIELD, System.currentTimeMillis()); 174 } catch (JSONException e) { 175 Log.e(TAG, "Couldn't add timestamp to serialized themebundle"); 176 } 177 } 178 return json; 179 } 180 removeNullValues(JSONObject json)181 private void removeNullValues(JSONObject json) { 182 Iterator<String> keys = json.keys(); 183 Set<String> keysToRemove = new HashSet<>(); 184 while (keys.hasNext()) { 185 String key = keys.next(); 186 if (json.isNull(key)) { 187 keysToRemove.add(key); 188 } 189 } 190 for (String key : keysToRemove) { 191 json.remove(key); 192 } 193 } 194 removeNullValues(Map<String, String> map)195 private Map<String, String> removeNullValues(Map<String, String> map) { 196 return map.entrySet() 197 .stream() 198 .filter(entry -> entry.getValue() != null) 199 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 200 } 201 202 /** */ getContentDescription(Context context)203 public CharSequence getContentDescription(Context context) { 204 if (mContentDescription == null) { 205 CharSequence defaultName = context.getString(R.string.default_theme_title); 206 if (isDefault()) { 207 mContentDescription = defaultName; 208 } else { 209 mContentDescription = mTitle; 210 } 211 } 212 return mContentDescription; 213 } 214 215 /** 216 * @return the source of this color option 217 */ 218 @ColorSource getSource()219 public abstract String getSource(); 220 221 /** 222 * @return the source of this color option for logging 223 */ 224 @ThemesUserEventLogger.ColorSource getSourceForLogging()225 public abstract int getSourceForLogging(); 226 227 /** 228 * @return the style of this color option 229 */ 230 @Style.Type getStyle()231 public Integer getStyle() { 232 return mStyle; 233 } 234 235 /** 236 * @return the style of this color option for logging 237 */ getStyleForLogging()238 public abstract int getStyleForLogging(); 239 240 /** 241 * @return the index of this color option 242 */ getIndex()243 public int getIndex() { 244 return mIndex; 245 } 246 247 /** 248 * The preview information of {@link ColorOption} 249 */ 250 public interface PreviewInfo { 251 } 252 253 } 254