diff options
22 files changed, 2502 insertions, 0 deletions
diff --git a/core/java/android/content/theming/FieldColor.java b/core/java/android/content/theming/FieldColor.java new file mode 100644 index 000000000000..a06a54f362b5 --- /dev/null +++ b/core/java/android/content/theming/FieldColor.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2025 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 android.content.theming; + +import android.annotation.ColorInt; +import android.annotation.FlaggedApi; +import android.graphics.Color; + +import androidx.annotation.Nullable; + +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.regex.Pattern; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldColor extends ThemeSettingsField<Integer, String> { + private static final Pattern COLOR_PATTERN = Pattern.compile("[0-9a-fA-F]{6,8}"); + + public FieldColor( + String key, + BiConsumer<ThemeSettingsUpdater, Integer> setter, + Function<ThemeSettings, Integer> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + @Override + @ColorInt + @Nullable + public Integer parse(String primitive) { + if (primitive == null) { + return null; + } + if (!COLOR_PATTERN.matcher(primitive).matches()) { + return null; + } + + try { + return Color.valueOf(Color.parseColor("#" + primitive)).toArgb(); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public String serialize(@ColorInt Integer value) { + return Integer.toHexString(value); + } + + @Override + public boolean validate(Integer value) { + return !value.equals(Color.TRANSPARENT); + } + + @Override + public Class<Integer> getFieldType() { + return Integer.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } +} diff --git a/core/java/android/content/theming/FieldColorBoth.java b/core/java/android/content/theming/FieldColorBoth.java new file mode 100644 index 000000000000..e4a9f7f716d8 --- /dev/null +++ b/core/java/android/content/theming/FieldColorBoth.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2025 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 android.content.theming; + +import android.annotation.FlaggedApi; + +import androidx.annotation.Nullable; + +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldColorBoth extends ThemeSettingsField<Boolean, String> { + public FieldColorBoth( + String key, + BiConsumer<ThemeSettingsUpdater, Boolean> setter, + Function<ThemeSettings, Boolean> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + @Override + @Nullable + public Boolean parse(String primitive) { + return switch (primitive) { + case "1" -> true; + case "0" -> false; + default -> null; + }; + } + + @Override + public String serialize(Boolean typedValue) { + if (typedValue) return "1"; + return "0"; + } + + @Override + public boolean validate(Boolean value) { + Objects.requireNonNull(value); + return true; + } + + @Override + public Class<Boolean> getFieldType() { + return Boolean.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } +} diff --git a/core/java/android/content/theming/FieldColorIndex.java b/core/java/android/content/theming/FieldColorIndex.java new file mode 100644 index 000000000000..683568a42318 --- /dev/null +++ b/core/java/android/content/theming/FieldColorIndex.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2025 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 android.content.theming; + +import android.annotation.FlaggedApi; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldColorIndex extends ThemeSettingsField<Integer, String> { + public FieldColorIndex( + String key, + BiConsumer<ThemeSettingsUpdater, Integer> setter, + Function<ThemeSettings, Integer> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + @Override + public Integer parse(String primitive) { + try { + return Integer.parseInt(primitive); + } catch (NumberFormatException e) { + return null; + } + } + + @Override + public String serialize(Integer typedValue) { + return typedValue.toString(); + } + + @Override + public boolean validate(Integer value) { + return value >= -1; + } + + @Override + public Class<Integer> getFieldType() { + return Integer.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } +} diff --git a/core/java/android/content/theming/FieldColorSource.java b/core/java/android/content/theming/FieldColorSource.java new file mode 100644 index 000000000000..1ff3aa64fda5 --- /dev/null +++ b/core/java/android/content/theming/FieldColorSource.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 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 android.content.theming; + +import android.annotation.FlaggedApi; +import android.annotation.StringDef; + +import androidx.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldColorSource extends ThemeSettingsField<String, String> { + public FieldColorSource( + String key, + BiConsumer<ThemeSettingsUpdater, String> setter, + Function<ThemeSettings, String> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + @Override + @Nullable + @Type + public String parse(String primitive) { + return primitive; + } + + @Override + public String serialize(@Type String typedValue) { + return typedValue; + } + + @Override + public boolean validate(String value) { + return switch (value) { + case "preset", "home_wallpaper", "lock_wallpaper" -> true; + default -> false; + }; + } + + @Override + public Class<String> getFieldType() { + return String.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } + + + @StringDef({"preset", "home_wallpaper", "lock_wallpaper"}) + @Retention(RetentionPolicy.SOURCE) + @interface Type { + } +} diff --git a/core/java/android/content/theming/FieldThemeStyle.java b/core/java/android/content/theming/FieldThemeStyle.java new file mode 100644 index 000000000000..b433e5b96ec3 --- /dev/null +++ b/core/java/android/content/theming/FieldThemeStyle.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 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 android.content.theming; + +import android.annotation.FlaggedApi; +import android.annotation.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** @hide */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class FieldThemeStyle extends ThemeSettingsField<Integer, String> { + public FieldThemeStyle( + String key, + BiConsumer<ThemeSettingsUpdater, Integer> setter, + Function<ThemeSettings, Integer> getter, + ThemeSettings defaults + ) { + super(key, setter, getter, defaults); + } + + private static final @ThemeStyle.Type List<Integer> sValidStyles = Arrays.asList( + ThemeStyle.EXPRESSIVE, + ThemeStyle.SPRITZ, + ThemeStyle.TONAL_SPOT, ThemeStyle.FRUIT_SALAD, ThemeStyle.RAINBOW, + ThemeStyle.VIBRANT, + ThemeStyle.MONOCHROMATIC); + + @Override + public String serialize(@ThemeStyle.Type Integer typedValue) { + return ThemeStyle.toString(typedValue); + } + + @Override + public boolean validate(Integer value) { + return sValidStyles.contains(value); + } + + @Override + @Nullable + @ThemeStyle.Type + public Integer parse(String primitive) { + try { + return ThemeStyle.valueOf(primitive); + } catch (Exception e) { + return null; + } + } + + @Override + public Class<Integer> getFieldType() { + return Integer.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } +} diff --git a/core/java/android/content/theming/ThemeSettings.java b/core/java/android/content/theming/ThemeSettings.java new file mode 100644 index 000000000000..e94c1fef5382 --- /dev/null +++ b/core/java/android/content/theming/ThemeSettings.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2025 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 android.content.theming; + +import android.annotation.ColorInt; +import android.annotation.FlaggedApi; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +/** + * Represents the theme settings for the system. + * This class holds various properties related to theming, such as color indices, palettes, + * accent colors, color sources, theme styles, and color combinations. + * + * @hide + */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public final class ThemeSettings implements Parcelable { + private final int mColorIndex; + private final int mSystemPalette; + private final int mAccentColor; + @NonNull + private final String mColorSource; + private final int mThemeStyle; + private final boolean mColorBoth; + + /** + * Constructs a new ThemeSettings object. + * + * @param colorIndex The color index. + * @param systemPalette The system palette color. + * @param accentColor The accent color. + * @param colorSource The color source. + * @param themeStyle The theme style. + * @param colorBoth The color combination. + */ + + public ThemeSettings(int colorIndex, @ColorInt int systemPalette, + @ColorInt int accentColor, @NonNull String colorSource, int themeStyle, + boolean colorBoth) { + + this.mAccentColor = accentColor; + this.mColorBoth = colorBoth; + this.mColorIndex = colorIndex; + this.mColorSource = colorSource; + this.mSystemPalette = systemPalette; + this.mThemeStyle = themeStyle; + } + + /** + * Constructs a ThemeSettings object from a Parcel. + * + * @param in The Parcel to read from. + */ + ThemeSettings(Parcel in) { + this.mAccentColor = in.readInt(); + this.mColorBoth = in.readBoolean(); + this.mColorIndex = in.readInt(); + this.mColorSource = Objects.requireNonNullElse(in.readString8(), "s"); + this.mSystemPalette = in.readInt(); + this.mThemeStyle = in.readInt(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mAccentColor); + dest.writeBoolean(mColorBoth); + dest.writeInt(mColorIndex); + dest.writeString8(mColorSource); + dest.writeInt(mSystemPalette); + dest.writeInt(mThemeStyle); + } + + /** + * Gets the color index. + * + * @return The color index. + */ + public Integer colorIndex() { + return mColorIndex; + } + + /** + * Gets the system palette color. + * + * @return The system palette color. + */ + @ColorInt + public Integer systemPalette() { + return mSystemPalette; + } + + /** + * Gets the accent color. + * + * @return The accent color. + */ + @ColorInt + public Integer accentColor() { + return mAccentColor; + } + + /** + * Gets the color source. + * + * @return The color source. + */ + @FieldColorSource.Type + public String colorSource() { + return mColorSource; + } + + /** + * Gets the theme style. + * + * @return The theme style. + */ + @ThemeStyle.Type + public Integer themeStyle() { + return mThemeStyle; + } + + /** + * Gets the color combination. + * + * @return The color combination. + */ + public Boolean colorBoth() { + return mColorBoth; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + return obj instanceof ThemeSettings other + && mColorIndex == other.mColorIndex + && mSystemPalette == other.mSystemPalette + && mAccentColor == other.mAccentColor + && mColorSource.equals(other.mColorSource) + && mThemeStyle == other.mThemeStyle + && mColorBoth == other.mColorBoth; + } + + @Override + public int hashCode() { + return Objects.hash(mColorIndex, mSystemPalette, mAccentColor, mColorSource, mThemeStyle, + mColorBoth); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Creator for Parcelable interface. + */ + public static final Creator<ThemeSettings> CREATOR = new Creator<>() { + @Override + public ThemeSettings createFromParcel(Parcel in) { + return new ThemeSettings(in); + } + + @Override + public ThemeSettings[] newArray(int size) { + return new ThemeSettings[size]; + } + }; + + /** + * Creates a new {@link ThemeSettingsUpdater} instance for updating the {@link ThemeSettings} + * through the API. + * + * @return A new {@link ThemeSettingsUpdater} instance. + */ + public static ThemeSettingsUpdater updater() { + return new ThemeSettingsUpdater(); + } +} diff --git a/core/java/android/content/theming/ThemeSettingsField.java b/core/java/android/content/theming/ThemeSettingsField.java new file mode 100644 index 000000000000..1696df4ad0f6 --- /dev/null +++ b/core/java/android/content/theming/ThemeSettingsField.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2025 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 android.content.theming; + + +import android.annotation.FlaggedApi; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.internal.util.Preconditions; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Represents a field within {@link ThemeSettings}, providing methods for parsing, serializing, + * managing default values, and validating the field's value. + * <p> + * This class is designed to be extended by concrete classes that represent specific fields within + * {@link ThemeSettings}. Each subclass should define the following methods, where T is the type of + * the field's value and J is the type of the field's value stored in JSON: + * <ul> + * <li>{@link #parse(Object)} to parse a JSON representation into the field's value type.</li> + * <li>{@link #serialize(Object)} to serialize the field's value into a JSON representation.</li> + * <li>{@link #validate(Object)} to validate the field's value.</li> + * <li>{@link #getFieldType()} to return the type of the field's value.</li> + * <li>{@link #getJsonType()} to return the type of the field's value stored in JSON.</li> + * </ul> + * <p> + * The {@link #fromJSON(JSONObject, ThemeSettingsUpdater)} and + * {@link #toJSON(ThemeSettings, JSONObject)} + * methods handle the extraction and serialization of the field's value to and from JSON objects + * respectively. The {@link #fallbackParse(Object, Object)} method is used to parse a string + * representation of the field's value, falling back to a default value if parsing fails. + * + * @param <T> The type of the field's value. + * @param <J> The type of the JSON property. + * @hide + */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public abstract class ThemeSettingsField<T, J> { + private static final String TAG = ThemeSettingsField.class.getSimpleName(); + + private static final String KEY_PREFIX = "android.theme.customization."; + public static final String OVERLAY_CATEGORY_ACCENT_COLOR = KEY_PREFIX + "accent_color"; + public static final String OVERLAY_CATEGORY_SYSTEM_PALETTE = KEY_PREFIX + "system_palette"; + public static final String OVERLAY_CATEGORY_THEME_STYLE = KEY_PREFIX + "theme_style"; + public static final String OVERLAY_COLOR_SOURCE = KEY_PREFIX + "color_source"; + public static final String OVERLAY_COLOR_INDEX = KEY_PREFIX + "color_index"; + public static final String OVERLAY_COLOR_BOTH = KEY_PREFIX + "color_both"; + + + /** + * Returns an array of all available {@link ThemeSettingsField} instances. + * + * @param defaults The default {@link ThemeSettings} object to use for default values. + * @return An array of {@link ThemeSettingsField} instances. + */ + public static ThemeSettingsField<?, ?>[] getFields(ThemeSettings defaults) { + return new ThemeSettingsField[]{ + new FieldColorIndex( + OVERLAY_COLOR_INDEX, + ThemeSettingsUpdater::colorIndex, + ThemeSettings::colorIndex, + defaults), + new FieldColor( + OVERLAY_CATEGORY_SYSTEM_PALETTE, + ThemeSettingsUpdater::systemPalette, + ThemeSettings::systemPalette, + defaults), + new FieldColor( + OVERLAY_CATEGORY_ACCENT_COLOR, + ThemeSettingsUpdater::accentColor, + ThemeSettings::accentColor, + defaults), + new FieldColorSource( + OVERLAY_COLOR_SOURCE, + ThemeSettingsUpdater::colorSource, + ThemeSettings::colorSource, + defaults), + new FieldThemeStyle( + OVERLAY_CATEGORY_THEME_STYLE, + ThemeSettingsUpdater::themeStyle, + ThemeSettings::themeStyle, + defaults), + new FieldColorBoth( + OVERLAY_COLOR_BOTH, + ThemeSettingsUpdater::colorBoth, + ThemeSettings::colorBoth, + defaults) + }; + } + + public final String key; + private final BiConsumer<ThemeSettingsUpdater, T> mSetter; + private final Function<ThemeSettings, T> mGetter; + private final ThemeSettings mDefaults; + + /** + * Creates a new {@link ThemeSettingsField}. + * + * @param key The key to identify the field in JSON objects. + * @param setter The setter to update the field's value in a {@link ThemeSettingsUpdater}. + * @param getter The getter to retrieve the field's value from a {@link ThemeSettings} + * object. + * @param defaults The default {@link ThemeSettings} object to provide default values. + */ + + public ThemeSettingsField( + String key, + BiConsumer<ThemeSettingsUpdater, T> setter, + Function<ThemeSettings, T> getter, + ThemeSettings defaults + ) { + this.key = key; + mSetter = setter; + mGetter = getter; + mDefaults = defaults; + } + + /** + * Attempts to parse a JSON primitive representation of the field's value. If parsing fails, it + * defaults to the field's default value. + * + * @param primitive The string representation to parse. + */ + private T fallbackParse(Object primitive, T fallbackValue) { + if (primitive == null) { + Log.w(TAG, "Error, field `" + key + "` was not found, defaulting to " + fallbackValue); + return fallbackValue; + } + + if (!getJsonType().isInstance(primitive)) { + Log.w(TAG, "Error, field `" + key + "` expected to be of type `" + + getJsonType().getSimpleName() + + "`, got `" + primitive.getClass().getSimpleName() + "`, defaulting to " + + fallbackValue); + return fallbackValue; + } + + // skips parsing if destination json type is already the same as field type + T parsedValue = getFieldType() == getJsonType() ? (T) primitive : parse((J) primitive); + + if (parsedValue == null) { + Log.w(TAG, "Error parsing JSON field `" + key + "` , defaulting to " + fallbackValue); + return fallbackValue; + } + + if (!validate(parsedValue)) { + Log.w(TAG, + "Error validating JSON field `" + key + "` , defaulting to " + fallbackValue); + return fallbackValue; + } + + if (parsedValue.getClass() != getFieldType()) { + Log.w(TAG, "Error: JSON field `" + key + "` expected to be of type `" + + getFieldType().getSimpleName() + + "`, defaulting to " + fallbackValue); + return fallbackValue; + } + + return parsedValue; + } + + + /** + * Extracts the field's value from a JSON object and sets it in a + * {@link ThemeSettingsUpdater}. + * + * @param source The JSON object containing the field's value. + */ + public void fromJSON(JSONObject source, ThemeSettingsUpdater updater) { + Object primitiveStr = source.opt(key); + T typedValue = fallbackParse(primitiveStr, getDefaultValue()); + mSetter.accept(updater, typedValue); + } + + /** + * Serializes the field's value from a {@link ThemeSettings} object into a JSON object. + * + * @param source The {@link ThemeSettings} object from which to retrieve the field's + * value. + * @param destination The JSON object to which the field's value will be added. + */ + public void toJSON(ThemeSettings source, JSONObject destination) { + T value = mGetter.apply(source); + Preconditions.checkState(value.getClass() == getFieldType()); + + J serialized; + if (validate(value)) { + serialized = serialize(value); + } else { + T fallbackValue = getDefaultValue(); + serialized = serialize(fallbackValue); + Log.w(TAG, "Invalid value `" + value + "` for key `" + key + "`, defaulting to '" + + fallbackValue); + } + + try { + destination.put(key, serialized); + } catch (JSONException e) { + Log.d(TAG, + "Error writing JSON primitive, skipping field " + key + ", " + e.getMessage()); + } + } + + + /** + * Returns the default value of the field. + * + * @return The default value. + */ + @VisibleForTesting + @NonNull + public T getDefaultValue() { + return mGetter.apply(mDefaults); + } + + /** + * Parses a string representation into the field's value type. + * + * @param primitive The string representation to parse. + * @return The parsed value, or null if parsing fails. + */ + @VisibleForTesting + @Nullable + public abstract T parse(J primitive); + + /** + * Serializes the field's value into a primitive type suitable for JSON. + * + * @param value The value to serialize. + * @return The serialized value. + */ + @VisibleForTesting + public abstract J serialize(T value); + + /** + * Validates the field's value. + * This method can be overridden to perform custom validation logic and MUST NOT validate for + * nullity. + * + * @param value The value to validate. + * @return {@code true} if the value is valid, {@code false} otherwise. + */ + @VisibleForTesting + public abstract boolean validate(T value); + + /** + * Returns the type of the field's value. + * + * @return The type of the field's value. + */ + @VisibleForTesting + public abstract Class<T> getFieldType(); + + /** + * Returns the type of the field's value stored in JSON. + * + * <p>This method is used to determine the expected type of the field's value when it is + * stored in a JSON object. + * + * @return The type of the field's value stored in JSON. + */ + @VisibleForTesting + public abstract Class<J> getJsonType(); +} diff --git a/core/java/android/content/theming/ThemeSettingsUpdater.java b/core/java/android/content/theming/ThemeSettingsUpdater.java new file mode 100644 index 000000000000..acd7d356db69 --- /dev/null +++ b/core/java/android/content/theming/ThemeSettingsUpdater.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2025 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 android.content.theming; + + +import android.annotation.ColorInt; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.VisibleForTesting; + +import java.util.Objects; + +/** + * Updater class for constructing {@link ThemeSettings} objects. + * This class provides a fluent interface for setting the various properties of the theme + * settings. + * + * @hide + */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +public class ThemeSettingsUpdater implements Parcelable { + @ColorInt + private Integer mAccentColor; + private Boolean mColorBoth; + private Integer mColorIndex; + private String mColorSource; + @ColorInt + private Integer mSystemPalette; + private Integer mThemeStyle; + + ThemeSettingsUpdater(Integer colorIndex, @ColorInt Integer systemPalette, + @ColorInt Integer accentColor, @FieldColorSource.Type String colorSource, + @ThemeStyle.Type Integer themeStyle, Boolean colorBoth) { + this.mAccentColor = accentColor; + this.mColorBoth = colorBoth; + this.mColorIndex = colorIndex; + this.mColorSource = colorSource; + this.mSystemPalette = systemPalette; + this.mThemeStyle = themeStyle; + } + + ThemeSettingsUpdater() { + } + + // only reading basic JVM types for nullability + @SuppressLint("ParcelClassLoader") + protected ThemeSettingsUpdater(Parcel in) { + mAccentColor = (Integer) in.readValue(null); + mColorBoth = (Boolean) in.readValue(null); + mColorIndex = (Integer) in.readValue(null); + mColorSource = (String) in.readValue(null); + mSystemPalette = (Integer) in.readValue(null); + mThemeStyle = (Integer) in.readValue(null); + } + + // using read/writeValue for nullability support + @SuppressWarnings("AndroidFrameworkEfficientParcelable") + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeValue(mAccentColor); + dest.writeValue(mColorBoth); + dest.writeValue(mColorIndex); + dest.writeValue(mColorSource); + dest.writeValue(mSystemPalette); + dest.writeValue(mThemeStyle); + } + + /** + * Sets the color index. + * + * @param colorIndex The color index to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater colorIndex(int colorIndex) { + this.mColorIndex = colorIndex; + return this; + } + + /** + * Returns the color index. + * + * @return The color index. + */ + @VisibleForTesting + public Integer getColorIndex() { + return mColorIndex; + } + + /** + * Sets the system palette color. + * + * @param systemPalette The system palette color to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater systemPalette(@ColorInt int systemPalette) { + this.mSystemPalette = systemPalette; + return this; + } + + /** + * Returns the system palette color. + * + * @return The system palette color. + */ + @VisibleForTesting + public Integer getSystemPalette() { + return mSystemPalette; + } + + /** + * Sets the accent color. + * + * @param accentColor The accent color to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater accentColor(@ColorInt int accentColor) { + this.mAccentColor = accentColor; + return this; + } + + /** + * Returns the accent color. + * + * @return The accent color. + */ + @VisibleForTesting + public Integer getAccentColor() { + return mAccentColor; + } + + /** + * Sets the color source. + * + * @param colorSource The color source to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater colorSource(@NonNull @FieldColorSource.Type String colorSource) { + this.mColorSource = colorSource; + return this; + } + + /** + * Returns the theme style. + * + * @return The theme style. + */ + @VisibleForTesting + public Integer getThemeStyle() { + return mThemeStyle; + } + + /** + * Sets the theme style. + * + * @param themeStyle The theme style to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater themeStyle(@ThemeStyle.Type int themeStyle) { + this.mThemeStyle = themeStyle; + return this; + } + + /** + * Returns the color source. + * + * @return The color source. + */ + @VisibleForTesting + public String getColorSource() { + return mColorSource; + } + + /** + * Sets the color combination. + * + * @param colorBoth The color combination to set. + * @return This {@link ThemeSettingsUpdater} instance. + */ + public ThemeSettingsUpdater colorBoth(boolean colorBoth) { + this.mColorBoth = colorBoth; + return this; + } + + /** + * Returns the color combination. + * + * @return The color combination. + */ + @VisibleForTesting + public Boolean getColorBoth() { + return mColorBoth; + } + + /** + * Constructs a new {@link ThemeSettings} object with the current builder settings. + * + * @return A new {@link ThemeSettings} object. + */ + public ThemeSettings toThemeSettings(@NonNull ThemeSettings defaults) { + return new ThemeSettings( + Objects.requireNonNullElse(mColorIndex, defaults.colorIndex()), + Objects.requireNonNullElse(mSystemPalette, defaults.systemPalette()), + Objects.requireNonNullElse(mAccentColor, defaults.accentColor()), + Objects.requireNonNullElse(mColorSource, defaults.colorSource()), + Objects.requireNonNullElse(mThemeStyle, defaults.themeStyle()), + Objects.requireNonNullElse(mColorBoth, defaults.colorBoth())); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<ThemeSettingsUpdater> CREATOR = + new Creator<>() { + @Override + public ThemeSettingsUpdater createFromParcel(Parcel in) { + return new ThemeSettingsUpdater(in); + } + + @Override + public ThemeSettingsUpdater[] newArray(int size) { + return new ThemeSettingsUpdater[size]; + } + }; +} diff --git a/core/java/android/content/theming/ThemeStyle.java b/core/java/android/content/theming/ThemeStyle.java new file mode 100644 index 000000000000..607896405020 --- /dev/null +++ b/core/java/android/content/theming/ThemeStyle.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2025 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 android.content.theming; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A class defining the different styles available for theming. + * This class replaces the previous enum implementation for improved performance and compatibility. + * + * @hide + */ +public final class ThemeStyle { + + private ThemeStyle() { + } + + /** + * @hide + */ + @IntDef({ + SPRITZ, + TONAL_SPOT, + VIBRANT, + EXPRESSIVE, + RAINBOW, + FRUIT_SALAD, + CONTENT, + MONOCHROMATIC, + CLOCK, + CLOCK_VIBRANT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Type { + } + + /** + * Represents the SPRITZ style. + */ + public static final int SPRITZ = 0; + /** + * Represents the TONAL_SPOT style. + */ + public static final int TONAL_SPOT = 1; + /** + * Represents the VIBRANT style. + */ + public static final int VIBRANT = 2; + /** + * Represents the EXPRESSIVE style. + */ + public static final int EXPRESSIVE = 3; + /** + * Represents the RAINBOW style. + */ + public static final int RAINBOW = 4; + /** + * Represents the FRUIT_SALAD style. + */ + public static final int FRUIT_SALAD = 5; + /** + * Represents the CONTENT style. + */ + public static final int CONTENT = 6; + /** + * Represents the MONOCHROMATIC style. + */ + public static final int MONOCHROMATIC = 7; + /** + * Represents the CLOCK style. + */ + public static final int CLOCK = 8; + /** + * Represents the CLOCK_VIBRANT style. + */ + public static final int CLOCK_VIBRANT = 9; + + + /** + * Returns the string representation of the given style. + * + * @param style The style value. + * @return The string representation of the style. + * @throws IllegalArgumentException if the style value is invalid. + */ + @NonNull + public static String toString(@Nullable @Type Integer style) { + // Throw an exception if style is null + if (style == null) { + throw new IllegalArgumentException("Invalid style value: null"); + } + + return switch (style) { + case SPRITZ -> "SPRITZ"; + case TONAL_SPOT -> "TONAL_SPOT"; + case VIBRANT -> "VIBRANT"; + case EXPRESSIVE -> "EXPRESSIVE"; + case RAINBOW -> "RAINBOW"; + case FRUIT_SALAD -> "FRUIT_SALAD"; + case CONTENT -> "CONTENT"; + case MONOCHROMATIC -> "MONOCHROMATIC"; + case CLOCK -> "CLOCK"; + case CLOCK_VIBRANT -> "CLOCK_VIBRANT"; + default -> throw new IllegalArgumentException("Invalid style value: " + style); + }; + } + + /** + * Returns the style value corresponding to the given style name. + * + * @param styleName The name of the style. + * @return The style value. + * @throws IllegalArgumentException if the style name is invalid. + */ + public static @Type int valueOf(@Nullable String styleName) { + return switch (styleName) { + case "SPRITZ" -> SPRITZ; + case "TONAL_SPOT" -> TONAL_SPOT; + case "VIBRANT" -> VIBRANT; + case "EXPRESSIVE" -> EXPRESSIVE; + case "RAINBOW" -> RAINBOW; + case "FRUIT_SALAD" -> FRUIT_SALAD; + case "CONTENT" -> CONTENT; + case "MONOCHROMATIC" -> MONOCHROMATIC; + case "CLOCK" -> CLOCK; + case "CLOCK_VIBRANT" -> CLOCK_VIBRANT; + default -> throw new IllegalArgumentException("Invalid style name: " + styleName); + }; + } + + /** + * Returns the name of the given style. This method is equivalent to {@link #toString(int)}. + * + * @param style The style value. + * @return The name of the style. + */ + @NonNull + public static String name(@Type int style) { + return toString(style); + } + + /** + * Returns an array containing all the style values. + * + * @return An array of all style values. + */ + public static int[] values() { + return new int[]{ + SPRITZ, + TONAL_SPOT, + VIBRANT, + EXPRESSIVE, + RAINBOW, + FRUIT_SALAD, + CONTENT, + MONOCHROMATIC, + CLOCK, + CLOCK_VIBRANT + }; + } +} diff --git a/services/core/java/com/android/server/theming/ThemeSettingsManager.java b/services/core/java/com/android/server/theming/ThemeSettingsManager.java new file mode 100644 index 000000000000..94094a6f9603 --- /dev/null +++ b/services/core/java/com/android/server/theming/ThemeSettingsManager.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2025 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.server.theming; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.content.ContentResolver; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsField; +import android.content.theming.ThemeSettingsUpdater; +import android.provider.Settings; +import android.telecom.Log; + +import com.android.internal.util.Preconditions; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Iterator; + +/** + * Manages the loading and saving of theme settings. This class handles the persistence of theme + * settings to and from the system settings. It utilizes a collection of {@link ThemeSettingsField} + * objects to represent individual theme setting fields. + * + * @hide + */ +@FlaggedApi(android.server.Flags.FLAG_ENABLE_THEME_SERVICE) +class ThemeSettingsManager { + private static final String TAG = ThemeSettingsManager.class.getSimpleName(); + static final String TIMESTAMP_FIELD = "_applied_timestamp"; + private final ThemeSettingsField<?, ?>[] mFields; + private final ThemeSettings mDefaults; + + /** + * Constructs a new {@code ThemeSettingsManager} with the specified default settings. + * + * @param defaults The default theme settings to use. + */ + ThemeSettingsManager(ThemeSettings defaults) { + mDefaults = defaults; + mFields = ThemeSettingsField.getFields(defaults); + } + + /** + * Loads the theme settings for the specified user. + * + * @param userId The ID of the user. + * @param contentResolver The content resolver to use. + * @return The loaded {@link ThemeSettings}. + */ + @NonNull + ThemeSettings loadSettings(@UserIdInt int userId, ContentResolver contentResolver) { + String jsonString = Settings.Secure.getStringForUser(contentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, userId); + + JSONObject userSettings; + + try { + userSettings = new JSONObject(jsonString == null ? "" : jsonString); + } catch (JSONException e) { + userSettings = new JSONObject(); + } + + ThemeSettingsUpdater updater = ThemeSettings.updater(); + + for (ThemeSettingsField<?, ?> field : mFields) { + field.fromJSON(userSettings, updater); + } + + return updater.toThemeSettings(mDefaults); + } + + /** + * Saves the specified theme settings for the given user. + * + * @param userId The ID of the user. + * @param contentResolver The content resolver to use. + * @param newSettings The {@link ThemeSettings} to save. + */ + void replaceSettings(@UserIdInt int userId, ContentResolver contentResolver, + ThemeSettings newSettings) throws RuntimeException { + Preconditions.checkArgument(newSettings != null, "Impossible to write empty settings"); + + JSONObject jsonSettings = new JSONObject(); + + + for (ThemeSettingsField<?, ?> field : mFields) { + field.toJSON(newSettings, jsonSettings); + } + + // user defined timestamp should be ignored. Storing new timestamp. + try { + jsonSettings.put(TIMESTAMP_FIELD, System.currentTimeMillis()); + } catch (JSONException e) { + Log.w(TAG, "Error saving timestamp: " + e.getMessage()); + } + + String jsonString = jsonSettings.toString(); + + Settings.Secure.putStringForUser(contentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, jsonString, userId); + } + + /** + * Saves the specified theme settings for the given user, while preserving unrelated existing + * properties. + * + * @param userId The ID of the user. + * @param contentResolver The content resolver to use. + * @param newSettings The {@link ThemeSettings} to save. + */ + void updateSettings(@UserIdInt int userId, ContentResolver contentResolver, + ThemeSettings newSettings) throws JSONException, RuntimeException { + Preconditions.checkArgument(newSettings != null, "Impossible to write empty settings"); + + String existingJsonString = Settings.Secure.getStringForUser(contentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, userId); + + JSONObject existingJson; + try { + existingJson = new JSONObject(existingJsonString == null ? "{}" : existingJsonString); + } catch (JSONException e) { + existingJson = new JSONObject(); + } + + JSONObject newJson = new JSONObject(); + for (ThemeSettingsField<?, ?> field : mFields) { + field.toJSON(newSettings, newJson); + } + + // user defined timestamp should be ignored. Storing new timestamp. + try { + newJson.put(TIMESTAMP_FIELD, System.currentTimeMillis()); + } catch (JSONException e) { + Log.w(TAG, "Error saving timestamp: " + e.getMessage()); + } + + // Merge the new settings with the existing settings + Iterator<String> keys = newJson.keys(); + while (keys.hasNext()) { + String key = keys.next(); + existingJson.put(key, newJson.get(key)); + } + + String mergedJsonString = existingJson.toString(); + + Settings.Secure.putStringForUser(contentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, mergedJsonString, userId); + } +} diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig index 7a6bd75e5893..f864b6b8c768 100644 --- a/services/java/com/android/server/flags.aconfig +++ b/services/java/com/android/server/flags.aconfig @@ -31,6 +31,13 @@ flag { } flag { + namespace: "system_performance" + name: "enable_theme_service" + description: "Switches from SystemUi's ThemeOverlayController to Server's ThemeService." + bug: "333694176" +} + +flag { name: "allow_removing_vpn_service" namespace: "wear_frameworks" description: "Allow removing VpnManagerService" diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 64e6d323bdfd..d3c3178f3513 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -347,6 +347,17 @@ test_module_config { include_filters: ["com.android.server.om."], } +test_module_config { + name: "FrameworksServicesTests_theme", + base: "FrameworksServicesTests", + test_suites: [ + "device-tests", + "automotive-tests", + ], + + include_filters: ["com.android.server.theming."], +} + // Used by contexthub TEST_MAPPING test_module_config { name: "FrameworksServicesTests_contexthub_presubmit", diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorBothTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorBothTests.java new file mode 100644 index 000000000000..38cbcf37f88c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorBothTests.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldColorBoth; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import com.google.common.truth.Truth; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FieldColorBothTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + private FieldColorBoth mFieldColorBoth; + + @Before + public void setup() { + mFieldColorBoth = new FieldColorBoth("colorBoth", ThemeSettingsUpdater::colorBoth, + ThemeSettings::colorBoth, DEFAULTS); + } + + @Test + public void parse_validColorBoth_returnsTrue() { + Boolean parsedValue = mFieldColorBoth.parse("1"); + assertThat(parsedValue).isTrue(); + } + + @Test + public void parse_validColorBoth_returnsFalse() { + Boolean parsedValue = mFieldColorBoth.parse("0"); + assertThat(parsedValue).isFalse(); + } + + @Test + public void parse_invalidColorBoth_returnsNull() { + Boolean parsedValue = mFieldColorBoth.parse("invalid"); + assertThat(parsedValue).isNull(); + } + + @Test + public void serialize_true_returnsTrueString() { + String serializedValue = mFieldColorBoth.serialize(true); + assertThat(serializedValue).isEqualTo("1"); + } + + @Test + public void serialize_false_returnsFalseString() { + String serializedValue = mFieldColorBoth.serialize(false); + assertThat(serializedValue).isEqualTo("0"); + } + + @Test + public void validate_true_returnsTrue() { + assertThat(mFieldColorBoth.validate(true)).isTrue(); + } + + @Test + public void validate_false_returnsTrue() { + assertThat(mFieldColorBoth.validate(false)).isTrue(); + } + + @Test + public void getFieldType_returnsBooleanClass() { + Truth.assertThat(mFieldColorBoth.getFieldType()).isEqualTo(Boolean.class); + } + + @Test + public void getJsonType_returnsStringClass() { + Truth.assertThat(mFieldColorBoth.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + Truth.assertThat(mFieldColorBoth.getDefaultValue()).isEqualTo(DEFAULTS.colorBoth()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorIndexTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorIndexTests.java new file mode 100644 index 000000000000..32df3684a81d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorIndexTests.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2025 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldColorIndex; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FieldColorIndexTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + + private FieldColorIndex mFieldColorIndex; + + @Before + public void setup() { + mFieldColorIndex = new FieldColorIndex("colorIndex", ThemeSettingsUpdater::colorIndex, + ThemeSettings::colorIndex, DEFAULTS); + } + + @Test + public void parse_validColorIndex_returnsCorrectInteger() { + Integer parsedValue = mFieldColorIndex.parse("10"); + assertThat(parsedValue).isEqualTo(10); + } + + @Test + public void parse_negativeColorIndex_returnsCorrectInteger() { + Integer parsedValue = mFieldColorIndex.parse("-1"); + assertThat(parsedValue).isEqualTo(-1); + } + + @Test + public void parse_invalidColorIndex_returnsNull() { + Integer parsedValue = mFieldColorIndex.parse("invalid"); + assertThat(parsedValue).isNull(); + } + + @Test + public void serialize_validColorIndex_returnsCorrectString() { + String serializedValue = mFieldColorIndex.serialize(15); + assertThat(serializedValue).isEqualTo("15"); + } + + @Test + public void serialize_negativeColorIndex_returnsCorrectString() { + String serializedValue = mFieldColorIndex.serialize(-1); + assertThat(serializedValue).isEqualTo("-1"); + } + + @Test + public void validate_validColorIndex_returnsTrue() { + assertThat(mFieldColorIndex.validate(5)).isTrue(); + } + + @Test + public void validate_negativeColorIndex_returnsTrue() { + assertThat(mFieldColorIndex.validate(-1)).isTrue(); + } + + @Test + public void validate_invalidColorIndex_returnsFalse() { + assertThat(mFieldColorIndex.validate(-2)).isFalse(); + } + + @Test + public void getFieldType_returnsIntegerClass() { + assertThat(mFieldColorIndex.getFieldType()).isEqualTo(Integer.class); + } + + @Test + public void getJsonType_returnsStringClass() { + assertThat(mFieldColorIndex.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + assertThat(mFieldColorIndex.getDefaultValue()).isEqualTo(DEFAULTS.colorIndex()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorSourceTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorSourceTests.java new file mode 100644 index 000000000000..06edfa862d9c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorSourceTests.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2025 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldColorSource; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import com.google.common.truth.Truth; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + + +@RunWith(JUnit4.class) +public class FieldColorSourceTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + private FieldColorSource mFieldColorSource; + + @Before + public void setup() { + mFieldColorSource = new FieldColorSource("colorSource", ThemeSettingsUpdater::colorSource, + ThemeSettings::colorSource, DEFAULTS); + } + + @Test + public void parse_validColorSource_returnsSameString() { + String validColorSource = "home_wallpaper"; + String parsedValue = mFieldColorSource.parse(validColorSource); + assertThat(parsedValue).isEqualTo(validColorSource); + } + + @Test + public void serialize_validColorSource_returnsSameString() { + String validColorSource = "lock_wallpaper"; + String serializedValue = mFieldColorSource.serialize(validColorSource); + assertThat(serializedValue).isEqualTo(validColorSource); + } + + @Test + public void validate_preset_returnsTrue() { + assertThat(mFieldColorSource.validate("preset")).isTrue(); + } + + @Test + public void validate_homeWallpaper_returnsTrue() { + assertThat(mFieldColorSource.validate("home_wallpaper")).isTrue(); + } + + @Test + public void validate_lockWallpaper_returnsTrue() { + assertThat(mFieldColorSource.validate("lock_wallpaper")).isTrue(); + } + + @Test + public void validate_invalidColorSource_returnsFalse() { + assertThat(mFieldColorSource.validate("invalid")).isFalse(); + } + + @Test + public void getFieldType_returnsStringClass() { + Truth.assertThat(mFieldColorSource.getFieldType()).isEqualTo(String.class); + } + + @Test + public void getJsonType_returnsStringClass() { + Truth.assertThat(mFieldColorSource.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + Truth.assertThat(mFieldColorSource.getDefaultValue()).isEqualTo(DEFAULTS.colorSource()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldColorTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldColorTests.java new file mode 100644 index 000000000000..54c4b29a5063 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldColorTests.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2025 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldColor; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import com.google.common.truth.Truth; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FieldColorTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + + private FieldColor mFieldColor; + + @Before + public void setup() { + // Default to blue + mFieldColor = new FieldColor("accentColor", ThemeSettingsUpdater::accentColor, + ThemeSettings::accentColor, DEFAULTS); + } + + @Test + public void parse_validColor_returnsCorrectColor() { + Integer parsedValue = mFieldColor.parse("FF0000FF"); + assertThat(parsedValue).isEqualTo(0xFF0000FF); + } @Test + public void parse_validColorLowercase_returnsCorrectColor() { + Integer parsedValue = mFieldColor.parse("ff0000ff"); + assertThat(parsedValue).isEqualTo(0xFF0000FF); + } + + @Test + public void parse_validColorNoAlpha_returnsCorrectColor() { + Integer parsedValue = mFieldColor.parse("0000ff"); + assertThat(parsedValue).isEqualTo(0xFF0000FF); + } + + + @Test + public void parse_invalidColor_returnsNull() { + Integer parsedValue = mFieldColor.parse("invalid"); + assertThat(parsedValue).isNull(); + } + + @Test + public void parse_nullColor_returnsNull() { + Integer parsedValue = mFieldColor.parse(null); + assertThat(parsedValue).isNull(); + } + + @Test + public void serialize_validColor_returnsCorrectString() { + String serializedValue = mFieldColor.serialize(0xFFFF0000); // Red + assertThat(serializedValue).isEqualTo("ffff0000"); + } + + @Test + public void serialize_zeroColor_returnsZeroString() { + String serializedValue = mFieldColor.serialize(0); + assertThat(serializedValue).isEqualTo("0"); + } + + @Test + public void validate_validColor_returnsTrue() { + assertThat(mFieldColor.validate(0xFF00FF00)).isTrue(); // Green + } + + @Test + public void getFieldType_returnsIntegerClass() { + Truth.assertThat(mFieldColor.getFieldType()).isEqualTo(Integer.class); + } + + @Test + public void getJsonType_returnsStringClass() { + Truth.assertThat(mFieldColor.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + Truth.assertThat(mFieldColor.getDefaultValue()).isEqualTo(DEFAULTS.accentColor()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/FieldThemeStyleTests.java b/services/tests/servicestests/src/com/android/server/theming/FieldThemeStyleTests.java new file mode 100644 index 000000000000..09d71292fcf6 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/FieldThemeStyleTests.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2025 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.FieldThemeStyle; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FieldThemeStyleTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + + private FieldThemeStyle mFieldThemeStyle; + + @Before + public void setup() { + mFieldThemeStyle = new FieldThemeStyle("themeStyle", ThemeSettingsUpdater::themeStyle, + ThemeSettings::themeStyle, DEFAULTS); + } + + @Test + public void parse_validThemeStyle_returnsCorrectStyle() { + Integer parsedValue = mFieldThemeStyle.parse("EXPRESSIVE"); + assertThat(parsedValue).isEqualTo(ThemeStyle.EXPRESSIVE); + } + + @Test + public void parse_invalidThemeStyle_returnsNull() { + Integer parsedValue = mFieldThemeStyle.parse("INVALID"); + assertThat(parsedValue).isNull(); + } + + @Test + public void serialize_validThemeStyle_returnsCorrectString() { + String serializedValue = mFieldThemeStyle.serialize(ThemeStyle.SPRITZ); + assertThat(serializedValue).isEqualTo("SPRITZ"); + } + + @Test + public void validate_validThemeStyle_returnsTrue() { + assertThat(mFieldThemeStyle.validate(ThemeStyle.TONAL_SPOT)).isTrue(); + } + + @Test + public void validate_invalidThemeStyle_returnsFalse() { + assertThat(mFieldThemeStyle.validate(-1)).isFalse(); + } + + @Test + public void getFieldType_returnsIntegerClass() { + assertThat(mFieldThemeStyle.getFieldType()).isEqualTo(Integer.class); + } + + @Test + public void getJsonType_returnsStringClass() { + assertThat(mFieldThemeStyle.getJsonType()).isEqualTo(String.class); + } + + @Test + public void get_returnsDefaultValue() { + assertThat(mFieldThemeStyle.getDefaultValue()).isEqualTo(DEFAULTS.themeStyle()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/theming/TEST_MAPPING new file mode 100644 index 000000000000..d8d73444f6ce --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "FrameworksServicesTests_theme" + } + ] +}
\ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsFieldTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsFieldTests.java new file mode 100644 index 000000000000..0dc267a8059f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsFieldTests.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2025 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsField; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +@RunWith(JUnit4.class) +public class ThemeSettingsFieldTests { + static final ThemeSettings DEFAULTS = new ThemeSettings(1, 0xFF123456, 0xFF654321, + "home_wallpaper", ThemeStyle.VIBRANT, true); + private ThemeSettingsUpdater mUpdater; + + @Before + public void setup() { + mUpdater = ThemeSettings.updater(); + } + + @Test + public void testFromJSON_validValue_setsValue() throws Exception { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + json.put("testKey", "5"); + + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(5); + } + + @Test + public void testFromJSON_nullValue_setsDefault() throws Exception { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + json.put("testKey", + JSONObject.NULL); // Using JSONObject.NULL is how you should indicate null in JSON + + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(DEFAULTS.colorIndex()); + } + + @Test + public void testFromJSON_invalidValue_setsDefault() throws Exception { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + json.put("testKey", "abc"); // Invalid value + + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(DEFAULTS.colorIndex()); + } + + @Test + public void testToJSON_validValue_writesValue() throws JSONException { + TestThemeSettingsFieldInteger field = getSampleField(); + ThemeSettings settings = new ThemeSettings(10, 0xFF123456, 0xFF654321, "home_wallpaper", + 0, true); + JSONObject json = new JSONObject(); + + field.toJSON(settings, json); + + assertThat(json.getString("testKey")).isEqualTo("10"); + } + + @Test + public void testDefaultValue_returnsGetDefault() { + TestThemeSettingsFieldInteger field = getSampleField(); + + assertThat(field.getDefaultValue()).isEqualTo(DEFAULTS.colorIndex()); + } + + @Test + public void test_String_validValue_returnsParsedValue() throws JSONException { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + json.put("testKey", "123"); + + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(123); + } + + @Test + public void test_String_invalidValue_returnsDefaultValue() throws JSONException { + TestThemeSettingsFieldInteger field = getSampleField(); + + JSONObject json = new JSONObject(); + // values < 0 are invalid + json.put("testKey", "-123"); + field.fromJSON(json, mUpdater); + + assertThat(mUpdater.getColorIndex()).isEqualTo(DEFAULTS.colorIndex()); + } + + private TestThemeSettingsFieldInteger getSampleField() { + return new TestThemeSettingsFieldInteger("testKey", ThemeSettingsUpdater::colorIndex, + ThemeSettings::colorIndex, DEFAULTS); + } + + + // Helper class for testing + private static class TestThemeSettingsFieldInteger extends ThemeSettingsField<Integer, String> { + TestThemeSettingsFieldInteger(String key, BiConsumer<ThemeSettingsUpdater, Integer> setter, + Function<ThemeSettings, Integer> getter, ThemeSettings defaults) { + super(key, setter, getter, defaults); + } + + @Override + public Integer parse(String primitive) { + try { + return Integer.parseInt(primitive); + } catch (NumberFormatException e) { + return null; + } + } + + @Override + public String serialize(Integer value) throws RuntimeException { + return value.toString(); + } + + @Override + public boolean validate(Integer value) { + return value > 0; + } + + @Override + public Class<Integer> getFieldType() { + return Integer.class; + } + + @Override + public Class<String> getJsonType() { + return String.class; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsManagerTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsManagerTests.java new file mode 100644 index 000000000000..44f8c73dec84 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsManagerTests.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2025 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.server.theming; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ContentResolver; +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeStyle; +import android.provider.Settings; +import android.testing.TestableContext; + +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ThemeSettingsManagerTests { + private final int mUserId = 0; + public static final ThemeSettings DEFAULTS = new ThemeSettings( + /* colorIndex= */ 1, + /* systemPalette= */ 0xFF123456, + /* accentColor= */ 0xFF654321, + /* colorSource= */ "home_wallpaper", + /* themeStyle= */ ThemeStyle.VIBRANT, + /* colorBoth= */ true); + + @Rule + public final TestableContext mContext = new TestableContext( + getInstrumentation().getTargetContext(), null); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + private ContentResolver mContentResolver; + + + @Before + public void setup() { + mContentResolver = mContext.getContentResolver(); + } + + @Test + public void loadSettings_emptyJSON_returnsDefault() { + Settings.Secure.putStringForUser(mContentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, "{}", mUserId); + + ThemeSettingsManager manager = new ThemeSettingsManager(DEFAULTS); + ThemeSettings settings = manager.loadSettings(mUserId, mContentResolver); + + assertThat(settings.colorIndex()).isEqualTo(DEFAULTS.colorIndex()); + assertThat(settings.systemPalette()).isEqualTo(DEFAULTS.systemPalette()); + assertThat(settings.accentColor()).isEqualTo(DEFAULTS.accentColor()); + assertThat(settings.colorSource()).isEqualTo(DEFAULTS.colorSource()); + assertThat(settings.themeStyle()).isEqualTo(DEFAULTS.themeStyle()); + assertThat(settings.colorBoth()).isEqualTo(DEFAULTS.colorBoth()); + } + + @Test + public void replaceSettings_writesSettingsToProvider() throws Exception { + + ThemeSettingsManager manager = new ThemeSettingsManager(DEFAULTS); + + ThemeSettings newSettings = new ThemeSettings(3, 0xFF112233, 0xFF332211, "preset", + ThemeStyle.MONOCHROMATIC, false); + manager.replaceSettings(mUserId, mContentResolver, newSettings); + + String settingsString = Settings.Secure.getStringForUser(mContentResolver, + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, mUserId); + JSONObject settingsJson = new JSONObject(settingsString); + assertThat(settingsJson.getString("android.theme.customization.color_index")).isEqualTo( + "3"); + assertThat(settingsJson.getString("android.theme.customization.system_palette")) + .isEqualTo("ff112233"); + assertThat(settingsJson.getString("android.theme.customization.accent_color")) + .isEqualTo("ff332211"); + assertThat(settingsJson.getString("android.theme.customization.color_source")) + .isEqualTo("preset"); + assertThat(settingsJson.getString("android.theme.customization.theme_style")) + .isEqualTo("MONOCHROMATIC"); + assertThat(settingsJson.getString("android.theme.customization.color_both")).isEqualTo("0"); + } + + @Test + public void updatesSettings_writesSettingsToProvider() throws Exception { + ThemeSettingsManager manager = new ThemeSettingsManager(DEFAULTS); + + ThemeSettings newSettings = new ThemeSettings(3, 0xFF112233, 0xFF332211, "preset", + ThemeStyle.MONOCHROMATIC, false); + manager.updateSettings(mUserId, mContentResolver, newSettings); + + ThemeSettings loadedSettings = manager.loadSettings(mUserId, mContentResolver); + assertThat(loadedSettings.equals(newSettings)).isTrue(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsTests.java new file mode 100644 index 000000000000..c417a4b571cb --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsTests.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2025 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertNull; + +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; +import android.os.Parcel; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ThemeSettingsTests { + public static final ThemeSettings DEFAULTS = new ThemeSettings( + /* colorIndex= */ 1, + /* systemPalette= */ 0xFF123456, + /* accentColor= */ 0xFF654321, + /* colorSource= */ "home_wallpaper", + /* themeStyle= */ ThemeStyle.VIBRANT, + /* colorBoth= */ true); + + /** + * Test that the updater correctly sets all fields when they are provided. + */ + @Test + public void testUpdater_allFieldsSet() { + ThemeSettingsUpdater updater = ThemeSettings.updater() + .colorIndex(2) + .systemPalette(0xFFFF0000) + .accentColor(0xFF00FF00) + .colorSource("preset") + .themeStyle(ThemeStyle.MONOCHROMATIC) + .colorBoth(false); + + ThemeSettings settings = updater.toThemeSettings(DEFAULTS); + + assertThat(settings.colorIndex()).isEqualTo(2); + assertThat(settings.systemPalette()).isEqualTo(0xFFFF0000); + assertThat(settings.accentColor()).isEqualTo(0xFF00FF00); + assertThat(settings.colorSource()).isEqualTo("preset"); + assertThat(settings.themeStyle()).isEqualTo(ThemeStyle.MONOCHROMATIC); + assertThat(settings.colorBoth()).isEqualTo(false); + } + + /** + * Test that the updater uses null values when no fields are explicitly set. + */ + @Test + public void testUpdater_noFieldsSet() { + ThemeSettingsUpdater updater = ThemeSettings.updater(); + + assertNull(updater.getColorIndex()); + assertNull(updater.getSystemPalette()); + assertNull(updater.getAccentColor()); + assertNull(updater.getColorSource()); + assertNull(updater.getThemeStyle()); + assertNull(updater.getColorBoth()); + } + + /** + * Test that the ThemeSettings object can be correctly parceled and restored. + */ + @Test + public void testParcel_roundTrip() { + ThemeSettingsUpdater updater = ThemeSettings.updater() + .colorIndex(2) + .systemPalette(0xFFFF0000) + .accentColor(0xFF00FF00) + .colorSource("preset") + .themeStyle(ThemeStyle.MONOCHROMATIC) + .colorBoth(false); + + ThemeSettings settings = updater.toThemeSettings(DEFAULTS); + + Parcel parcel = Parcel.obtain(); + settings.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ThemeSettings fromParcel = ThemeSettings.CREATOR.createFromParcel(parcel); + + assertThat(settings.colorIndex()).isEqualTo(fromParcel.colorIndex()); + assertThat(settings.systemPalette()).isEqualTo(fromParcel.systemPalette()); + assertThat(settings.accentColor()).isEqualTo(fromParcel.accentColor()); + assertThat(settings.colorSource()).isEqualTo(fromParcel.colorSource()); + assertThat(settings.themeStyle()).isEqualTo(fromParcel.themeStyle()); + assertThat(settings.colorBoth()).isEqualTo(fromParcel.colorBoth()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsUpdaterTests.java b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsUpdaterTests.java new file mode 100644 index 000000000000..7ce32da7b713 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/theming/ThemeSettingsUpdaterTests.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2025 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.server.theming; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.theming.ThemeSettings; +import android.content.theming.ThemeSettingsUpdater; +import android.content.theming.ThemeStyle; +import android.os.Parcel; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ThemeSettingsUpdaterTests { + public static final ThemeSettings DEFAULTS = new ThemeSettings( + /* colorIndex= */ 1, + /* systemPalette= */ 0xFF123456, + /* accentColor= */ 0xFF654321, + /* colorSource= */ "home_wallpaper", + /* themeStyle= */ ThemeStyle.VIBRANT, + /* colorBoth= */ true); + private ThemeSettingsUpdater mUpdater; + + @Before + public void setUp() { + mUpdater = ThemeSettings.updater(); + } + + @Test + public void testSetAndGetColorIndex() { + mUpdater.colorIndex(5); + assertThat(mUpdater.getColorIndex()).isEqualTo(5); + } + + @Test + public void testSetAndGetSystemPalette() { + mUpdater.systemPalette(0xFFABCDEF); + assertThat(mUpdater.getSystemPalette()).isEqualTo(0xFFABCDEF); + } + + @Test + public void testSetAndGetAccentColor() { + mUpdater.accentColor(0xFFFEDCBA); + assertThat(mUpdater.getAccentColor()).isEqualTo(0xFFFEDCBA); + } + + @Test + public void testSetAndGetColorSource() { + mUpdater.colorSource("lock_wallpaper"); + assertThat(mUpdater.getColorSource()).isEqualTo("lock_wallpaper"); + } + + @Test + public void testSetAndGetThemeStyle() { + mUpdater.themeStyle(ThemeStyle.EXPRESSIVE); + assertThat(mUpdater.getThemeStyle()).isEqualTo(ThemeStyle.EXPRESSIVE); + } + + @Test + public void testSetAndGetColorBoth() { + mUpdater.colorBoth(false); + assertThat(mUpdater.getColorBoth()).isFalse(); + } + + + @Test + public void testToThemeSettings_allFieldsSet() { + mUpdater.colorIndex(5) + .systemPalette(0xFFABCDEF) + .accentColor(0xFFFEDCBA) + .colorSource("lock_wallpaper") + .themeStyle(ThemeStyle.EXPRESSIVE) + .colorBoth(false); + ThemeSettings settings = mUpdater.toThemeSettings(DEFAULTS); + + assertThat(settings.colorIndex()).isEqualTo(5); + assertThat(settings.systemPalette()).isEqualTo(0xFFABCDEF); + assertThat(settings.accentColor()).isEqualTo(0xFFFEDCBA); + assertThat(settings.colorSource()).isEqualTo("lock_wallpaper"); + assertThat(settings.themeStyle()).isEqualTo(ThemeStyle.EXPRESSIVE); + assertThat(settings.colorBoth()).isFalse(); + } + + @Test + public void testToThemeSettings_someFieldsNotSet_usesDefaults() { + mUpdater.colorIndex(5) + .systemPalette(0xFFABCDEF); + + ThemeSettings settings = mUpdater.toThemeSettings(DEFAULTS); + + assertThat(settings.colorIndex()).isEqualTo(5); + assertThat(settings.systemPalette()).isEqualTo(0xFFABCDEF); + assertThat(settings.accentColor()).isEqualTo(DEFAULTS.accentColor()); + assertThat(settings.colorSource()).isEqualTo(DEFAULTS.colorSource()); + assertThat(settings.themeStyle()).isEqualTo(DEFAULTS.themeStyle()); + assertThat(settings.colorBoth()).isEqualTo(DEFAULTS.colorBoth()); + } + + @Test + public void testParcel_roundTrip_allFieldsSet() { + mUpdater.colorIndex(5) + .systemPalette(0xFFABCDEF) + .accentColor(0xFFFEDCBA) + .colorSource("lock_wallpaper") + .themeStyle(ThemeStyle.EXPRESSIVE) + .colorBoth(false); + + Parcel parcel = Parcel.obtain(); + mUpdater.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ThemeSettingsUpdater fromParcel = ThemeSettingsUpdater.CREATOR.createFromParcel(parcel); + + assertThat(fromParcel.getColorIndex()).isEqualTo(5); + assertThat(fromParcel.getSystemPalette()).isEqualTo(0xFFABCDEF); + assertThat(fromParcel.getAccentColor()).isEqualTo(0xFFFEDCBA); + assertThat(fromParcel.getColorSource()).isEqualTo("lock_wallpaper"); + assertThat(fromParcel.getThemeStyle()).isEqualTo(ThemeStyle.EXPRESSIVE); + assertThat(fromParcel.getColorBoth()).isFalse(); + } + + @Test + public void testParcel_roundTrip_noFieldsSet() { + Parcel parcel = Parcel.obtain(); + mUpdater.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ThemeSettingsUpdater fromParcel = ThemeSettingsUpdater.CREATOR.createFromParcel(parcel); + + assertThat(fromParcel.getColorIndex()).isNull(); + assertThat(fromParcel.getSystemPalette()).isNull(); + assertThat(fromParcel.getAccentColor()).isNull(); + assertThat(fromParcel.getColorSource()).isNull(); + assertThat(fromParcel.getThemeStyle()).isNull(); + assertThat(fromParcel.getColorBoth()).isNull(); + } +} |