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.VisibleForTesting; 26 27 import com.android.customization.model.CustomizationManager; 28 import com.android.customization.model.CustomizationOption; 29 import com.android.customization.model.color.ColorOptionsProvider.ColorSource; 30 import com.android.systemui.monet.Style; 31 import com.android.wallpaper.R; 32 33 import org.json.JSONException; 34 import org.json.JSONObject; 35 36 import java.util.Collections; 37 import java.util.HashSet; 38 import java.util.Iterator; 39 import java.util.Map; 40 import java.util.Set; 41 import java.util.stream.Collectors; 42 43 /** 44 * Represents a color choice for the user. 45 * This could be a preset color or those obtained from a wallpaper. 46 */ 47 public abstract class ColorOption implements CustomizationOption<ColorOption> { 48 49 private static final String TAG = "ColorOption"; 50 private static final String EMPTY_JSON = "{}"; 51 @VisibleForTesting 52 static final String TIMESTAMP_FIELD = "_applied_timestamp"; 53 54 protected final Map<String, String> mPackagesByCategory; 55 protected final int[] mPreviewColorIds = {R.id.color_preview_0, R.id.color_preview_1, 56 R.id.color_preview_2, R.id.color_preview_3}; 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 return (TextUtils.isEmpty(serializedOverlays) || EMPTY_JSON.equals(serializedOverlays) 90 || colorManager.getCurrentOverlays().isEmpty() || !(serializedOverlays.contains( 91 OVERLAY_CATEGORY_SYSTEM_PALETTE) || serializedOverlays.contains( 92 OVERLAY_CATEGORY_COLOR))) && isCurrentStyle; 93 } else { 94 Map<String, String> currentOverlays = colorManager.getCurrentOverlays(); 95 String currentSource = colorManager.getCurrentColorSource(); 96 boolean isCurrentSource = TextUtils.isEmpty(currentSource) || getSource().equals( 97 currentSource); 98 return isCurrentSource && isCurrentStyle && mPackagesByCategory.equals(currentOverlays); 99 } 100 } 101 102 /** 103 * This is similar to #equals() but it only compares this theme's packages with the other, that 104 * is, it will return true if applying this theme has the same effect of applying the given one. 105 */ isEquivalent(ColorOption other)106 public boolean isEquivalent(ColorOption other) { 107 if (other == null) { 108 return false; 109 } 110 if (mStyle != other.getStyle()) { 111 return false; 112 } 113 String thisSerializedPackages = getSerializedPackages(); 114 if (mIsDefault || TextUtils.isEmpty(thisSerializedPackages) 115 || EMPTY_JSON.equals(thisSerializedPackages)) { 116 String otherSerializedPackages = other.getSerializedPackages(); 117 return other.isDefault() || TextUtils.isEmpty(otherSerializedPackages) 118 || EMPTY_JSON.equals(otherSerializedPackages); 119 } 120 // Map#equals ensures keys and values are compared. 121 return mPackagesByCategory.equals(other.mPackagesByCategory); 122 } 123 124 /** 125 * Returns the {@link PreviewInfo} object for this ColorOption 126 */ getPreviewInfo()127 public abstract PreviewInfo getPreviewInfo(); 128 isDefault()129 boolean isDefault() { 130 return mIsDefault; 131 } 132 getPackagesByCategory()133 public Map<String, String> getPackagesByCategory() { 134 return mPackagesByCategory; 135 } 136 getSerializedPackages()137 public String getSerializedPackages() { 138 return getJsonPackages(false).toString(); 139 } 140 getSerializedPackagesWithTimestamp()141 public String getSerializedPackagesWithTimestamp() { 142 return getJsonPackages(true).toString(); 143 } 144 145 /** 146 * Get a JSONObject representation of this color option, with the current values for each 147 * field, and optionally a {@link TIMESTAMP_FIELD} field. 148 * @param insertTimestamp whether to add a field with the current timestamp 149 * @return the JSONObject for this color option 150 */ getJsonPackages(boolean insertTimestamp)151 public JSONObject getJsonPackages(boolean insertTimestamp) { 152 JSONObject json; 153 if (isDefault()) { 154 json = new JSONObject(); 155 } else { 156 json = new JSONObject(mPackagesByCategory); 157 // Remove items with null values to avoid deserialization issues. 158 removeNullValues(json); 159 } 160 if (insertTimestamp) { 161 try { 162 json.put(TIMESTAMP_FIELD, System.currentTimeMillis()); 163 } catch (JSONException e) { 164 Log.e(TAG, "Couldn't add timestamp to serialized themebundle"); 165 } 166 } 167 return json; 168 } 169 removeNullValues(JSONObject json)170 private void removeNullValues(JSONObject json) { 171 Iterator<String> keys = json.keys(); 172 Set<String> keysToRemove = new HashSet<>(); 173 while (keys.hasNext()) { 174 String key = keys.next(); 175 if (json.isNull(key)) { 176 keysToRemove.add(key); 177 } 178 } 179 for (String key : keysToRemove) { 180 json.remove(key); 181 } 182 } 183 removeNullValues(Map<String, String> map)184 private Map<String, String> removeNullValues(Map<String, String> map) { 185 return map.entrySet() 186 .stream() 187 .filter(entry -> entry.getValue() != null) 188 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 189 } 190 191 /** */ getContentDescription(Context context)192 public CharSequence getContentDescription(Context context) { 193 if (mContentDescription == null) { 194 CharSequence defaultName = context.getString(R.string.default_theme_title); 195 if (isDefault()) { 196 mContentDescription = defaultName; 197 } else { 198 mContentDescription = mTitle; 199 } 200 } 201 return mContentDescription; 202 } 203 204 /** 205 * @return the source of this color option 206 */ 207 @ColorSource getSource()208 public abstract String getSource(); 209 210 /** 211 * @return the style of this color option 212 */ getStyle()213 public Style getStyle() { 214 return mStyle; 215 } 216 217 /** 218 * @return the index of this color option 219 */ getIndex()220 public int getIndex() { 221 return mIndex; 222 } 223 224 /** 225 * The preview information of {@link ColorOption} 226 */ 227 public interface PreviewInfo { 228 } 229 230 } 231