/*
* Copyright (C) 2016 The CyanogenMod Project
* Portions copyright (C) 2014, T-Mobile USA, Inc.
*
* 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.res;
import android.content.ContentResolver;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.JsonReader;
import android.util.JsonToken;
import android.util.JsonWriter;
import android.util.Log;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Collections;
import java.util.Map;
/**
* The Theme Configuration allows lookup of a theme element (fonts, icon, overlay) for a given
* application. If there isn't a particular theme designated to an app, it will fallback on the
* default theme. If there isn't a default theme then it will simply fallback to holo.
*
* @hide
*/
public class ThemeConfig implements Cloneable, Parcelable, Comparable<ThemeConfig> {
public static final String TAG = ThemeConfig.class.getCanonicalName();
public static final String SYSTEM_DEFAULT = "system";
/**
* Special package name for theming the navbar separate from the rest of SystemUI
*/
public static final String SYSTEMUI_NAVBAR_PKG = "com.android.systemui.navbar";
public static final String SYSTEMUI_STATUS_BAR_PKG = "com.android.systemui";
// Key for any app which does not have a specific theme applied
private static final String KEY_DEFAULT_PKG = "default";
private static final SystemConfig mSystemConfig = new SystemConfig();
private static final SystemAppTheme mSystemAppTheme = new SystemAppTheme();
// Deprecated constants for upgrade
private static final String THEME_PACKAGE_NAME_PERSISTENCE_PROPERTY
= "persist.sys.themePackageName";
private static final String THEME_ICONPACK_PACKAGE_NAME_PERSISTENCE_PROPERTY
= "themeIconPackPkgName";
private static final String THEME_FONT_PACKAGE_NAME_PERSISTENCE_PROPERTY
= "themeFontPackPkgName";
/**
* @hide
* Serialized json structure mapping app pkgnames to their set theme.
*
* {
* "default":{
*" stylePkgName":"com.jasonevil.theme.miuiv5dark",
* "iconPkgName":"com.cyngn.hexo",
* "fontPkgName":"com.cyngn.hexo"
* }
* }
* If an app does not have a specific theme set then it will use the 'default' theme+
* example: 'default' -> overlayPkgName: 'org.blue.theme'
* 'com.android.phone' -> 'com.red.theme'
* 'com.google.vending' -> 'com.white.theme'
*/
public static final String THEME_PKG_CONFIGURATION_PERSISTENCE_PROPERTY = "themeConfig";
// Maps pkgname to theme (ex com.angry.birds -> red theme)
protected final Map<String, AppTheme> mThemes = new ArrayMap<>();
public ThemeConfig(Map<String, AppTheme> appThemes) {
mThemes.putAll(appThemes);
}
public String getOverlayPkgName() {
AppTheme theme = getDefaultTheme();
return theme.mOverlayPkgName;
}
public String getOverlayForStatusBar() {
return getOverlayPkgNameForApp(SYSTEMUI_STATUS_BAR_PKG);
}
public String getOverlayForNavBar() {
return getOverlayPkgNameForApp(SYSTEMUI_NAVBAR_PKG);
}
public String getOverlayPkgNameForApp(String appPkgName) {
AppTheme theme = getThemeFor(appPkgName);
return theme.mOverlayPkgName;
}
public String getIconPackPkgName() {
AppTheme theme = getDefaultTheme();
return theme.mIconPkgName;
}
public String getIconPackPkgNameForApp(String appPkgName) {
AppTheme theme = getThemeFor(appPkgName);
return theme.mIconPkgName;
}
public String getFontPkgName() {
AppTheme defaultTheme = getDefaultTheme();
return defaultTheme.mFontPkgName;
}
public String getFontPkgNameForApp(String appPkgName) {
AppTheme theme = getThemeFor(appPkgName);
return theme.mFontPkgName;
}
public Map<String, AppTheme> getAppThemes() {
return Collections.unmodifiableMap(mThemes);
}
private AppTheme getThemeFor(String pkgName) {
AppTheme theme = mThemes.get(pkgName);
if (theme == null) theme = getDefaultTheme();
return theme;
}
private AppTheme getDefaultTheme() {
AppTheme theme = mThemes.get(KEY_DEFAULT_PKG);
if (theme == null) theme = mSystemAppTheme;
return theme;
}
@Override
public boolean equals(Object object) {
if (object == this) {
return true;
}
if (object instanceof ThemeConfig) {
ThemeConfig o = (ThemeConfig) object;
Map<String, AppTheme> currThemes = (mThemes == null) ?
new ArrayMap<String, AppTheme>() : mThemes;
Map<String, AppTheme> newThemes = (o.mThemes == null) ?
new ArrayMap<String, AppTheme>() : o.mThemes;
return currThemes.equals(newThemes);
}
return false;
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
if (mThemes != null) {
result.append("themes:");
result.append(mThemes);
}
return result.toString();
}
@Override
public int hashCode() {
int hash = 17;
hash = 31 * hash + mThemes.hashCode();
return hash;
}
public String toJson() {
return JsonSerializer.toJson(this);
}
public static ThemeConfig fromJson(String json) {
return JsonSerializer.fromJson(json);
}
/**
* Represents the theme that the device booted into. This is used to
* simulate a "default" configuration based on the user's last known
* preference until the theme is switched at runtime.
*/
public static ThemeConfig getBootTheme(ContentResolver resolver) {
return getBootThemeForUser(resolver, UserHandle.USER_OWNER);
}
public static ThemeConfig getBootThemeForUser(ContentResolver resolver, int userHandle) {
ThemeConfig bootTheme = mSystemConfig;
try {
String json = Settings.Secure.getStringForUser(resolver,
THEME_PKG_CONFIGURATION_PERSISTENCE_PROPERTY, userHandle);
bootTheme = ThemeConfig.fromJson(json);
// Handle upgrade Case: Previously the theme configuration was in separate fields
if (bootTheme == null) {
String overlayPkgName = Settings.Secure.getStringForUser(resolver,
THEME_PACKAGE_NAME_PERSISTENCE_PROPERTY, userHandle);
String iconPackPkgName = Settings.Secure.getStringForUser(resolver,
THEME_ICONPACK_PACKAGE_NAME_PERSISTENCE_PROPERTY, userHandle);
String fontPkgName = Settings.Secure.getStringForUser(resolver,
THEME_FONT_PACKAGE_NAME_PERSISTENCE_PROPERTY, userHandle);
Builder builder = new Builder();
builder.defaultOverlay(overlayPkgName);
builder.defaultIcon(iconPackPkgName);
builder.defaultFont(fontPkgName);
bootTheme = builder.build();
}
} catch (SecurityException e) {
Log.w(TAG, "Could not get boot theme");
}
return bootTheme;
}
/**
* Represents the system framework theme, perceived by the system as there
* being no theme applied.
*/
public static ThemeConfig getSystemTheme() {
return mSystemConfig;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
String json = JsonSerializer.toJson(this);
dest.writeString(json);
}
public static final Parcelable.Creator<ThemeConfig> CREATOR =
new Parcelable.Creator<ThemeConfig>() {
public ThemeConfig createFromParcel(Parcel source) {
String json = source.readString();
ThemeConfig themeConfig = JsonSerializer.fromJson(json);
return themeConfig;
}
public ThemeConfig[] newArray(int size) {
return new ThemeConfig[size];
}
};
@Override
public int compareTo(ThemeConfig o) {
if (o == null) return -1;
int n = 0;
n = mThemes.equals(o.mThemes) ? 0 : 1;
return n;
}
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
Log.d(TAG, "clone not supported", e);
return null;
}
}
public static class AppTheme implements Cloneable, Comparable<AppTheme> {
// If any field is modified or added here be sure to change the serializer accordingly
String mOverlayPkgName;
String mIconPkgName;
String mFontPkgName;
public AppTheme(String overlayPkgName, String iconPkgName, String fontPkgName) {
mOverlayPkgName = overlayPkgName;
mIconPkgName = iconPkgName;
mFontPkgName = fontPkgName;
}
public String getIconPackPkgName() {
return mIconPkgName;
}
public String getOverlayPkgName() {
return mOverlayPkgName;
}
public String getFontPackPkgName() {
return mFontPkgName;
}
@Override
public synchronized int hashCode() {
int hash = 17;
hash = 31 * hash + (mOverlayPkgName == null ? 0 : mOverlayPkgName.hashCode());
hash = 31 * hash + (mIconPkgName == null ? 0 : mIconPkgName.hashCode());
hash = 31 * hash + (mFontPkgName == null ? 0 : mFontPkgName.hashCode());
return hash;
}
@Override
public int compareTo(AppTheme o) {
if (o == null) return -1;
int n = 0;
n = mIconPkgName.compareTo(o.mIconPkgName);
if (n != 0) return n;
n = mFontPkgName.compareTo(o.mFontPkgName);
if (n != 0) return n;
n = mOverlayPkgName.equals(o.mOverlayPkgName) ? 0 : 1;
return n;
}
@Override
public boolean equals(Object object) {
if (object == this) {
return true;
}
if (object instanceof AppTheme) {
AppTheme o = (AppTheme) object;
String currentOverlayPkgName = (mOverlayPkgName == null)? "" : mOverlayPkgName;
String newOverlayPkgName = (o.mOverlayPkgName == null)? "" : o.mOverlayPkgName;
String currentIconPkgName = (mIconPkgName == null)? "" : mIconPkgName;
String newIconPkgName = (o.mIconPkgName == null)? "" : o.mIconPkgName;
String currentFontPkgName = (mFontPkgName == null)? "" : mFontPkgName;
String newFontPkgName = (o.mFontPkgName == null)? "" : o.mFontPkgName;
return (currentIconPkgName.equals(newIconPkgName) &&
currentFontPkgName.equals(newFontPkgName) &&
currentOverlayPkgName.equals(newOverlayPkgName));
}
return false;
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
if (mOverlayPkgName != null) {
result.append("overlay:");
result.append(mOverlayPkgName);
}
if (!TextUtils.isEmpty(mIconPkgName)) {
result.append(", iconPack:");
result.append(mIconPkgName);
}
if (!TextUtils.isEmpty(mFontPkgName)) {
result.append(", fontPkg:");
result.append(mFontPkgName);
}
return result.toString();
}
}
public static class Builder {
private Map<String, String> mOverlays = new ArrayMap<>();
private Map<String, String> mIcons = new ArrayMap<>();
private Map<String, String> mFonts = new ArrayMap<>();
public Builder() {}
public Builder(ThemeConfig theme) {
for(Map.Entry<String, AppTheme> entry : theme.mThemes.entrySet()) {
String key = entry.getKey();
AppTheme appTheme = entry.getValue();
mFonts.put(key, appTheme.getFontPackPkgName());
mIcons.put(key, appTheme.getIconPackPkgName());
mOverlays.put(key, appTheme.getOverlayPkgName());
}
}
/**
* For uniquely theming a specific app. ex. "Dialer gets red theme,
* Calculator gets blue theme"
*/
public Builder defaultOverlay(String themePkgName) {
if (themePkgName != null) {
mOverlays.put(KEY_DEFAULT_PKG, themePkgName);
} else {
mOverlays.remove(KEY_DEFAULT_PKG);
}
return this;
}
public Builder defaultFont(String themePkgName) {
if (themePkgName != null) {
mFonts.put(KEY_DEFAULT_PKG, themePkgName);
} else {
mFonts.remove(KEY_DEFAULT_PKG);
}
return this;
}
public Builder defaultIcon(String themePkgName) {
if (themePkgName != null) {
mIcons.put(KEY_DEFAULT_PKG, themePkgName);
} else {
mIcons.remove(KEY_DEFAULT_PKG);
}
return this;
}
public Builder icon(String appPkgName, String themePkgName) {
if (themePkgName != null) {
mIcons.put(appPkgName, themePkgName);
} else {
mIcons.remove(appPkgName);
}
return this;
}
public Builder overlay(String appPkgName, String themePkgName) {
if (themePkgName != null) {
mOverlays.put(appPkgName, themePkgName);
} else {
mOverlays.remove(appPkgName);
}
return this;
}
public Builder font(String appPkgName, String themePkgName) {
if (themePkgName != null) {
mFonts.put(appPkgName, themePkgName);
} else {
mFonts.remove(appPkgName);
}
return this;
}
public ThemeConfig build() {
ArraySet<String> appPkgSet = new ArraySet<>();
appPkgSet.addAll(mOverlays.keySet());
appPkgSet.addAll(mIcons.keySet());
appPkgSet.addAll(mFonts.keySet());
Map<String, AppTheme> appThemes = new ArrayMap<>();
for(String appPkgName : appPkgSet) {
String icon = mIcons.get(appPkgName);
String overlay = mOverlays.get(appPkgName);
String font = mFonts.get(appPkgName);
// Remove app theme if all items are null
if (overlay == null && icon == null && font == null) {
if (appThemes.containsKey(appPkgName)) {
appThemes.remove(appPkgName);
}
} else {
AppTheme appTheme = new AppTheme(overlay, icon, font);
appThemes.put(appPkgName, appTheme);
}
}
ThemeConfig themeConfig = new ThemeConfig(appThemes);
return themeConfig;
}
}
public static class JsonSerializer {
private static final String NAME_OVERLAY_PKG = "mOverlayPkgName";
private static final String NAME_ICON_PKG = "mIconPkgName";
private static final String NAME_FONT_PKG = "mFontPkgName";
public static String toJson(ThemeConfig theme) {
String json = null;
Writer writer = null;
JsonWriter jsonWriter = null;
try {
writer = new StringWriter();
jsonWriter = new JsonWriter(writer);
writeTheme(jsonWriter, theme);
json = writer.toString();
} catch(IOException e) {
Log.e(TAG, "Could not write theme mapping", e);
} finally {
closeQuietly(writer);
closeQuietly(jsonWriter);
}
return json;
}
private static void writeTheme(JsonWriter writer, ThemeConfig theme)
throws IOException {
writer.beginObject();
for(Map.Entry<String, AppTheme> entry : theme.mThemes.entrySet()) {
String appPkgName = entry.getKey();
AppTheme appTheme = entry.getValue();
writer.name(appPkgName);
writeAppTheme(writer, appTheme);
}
writer.endObject();
}
private static void writeAppTheme(JsonWriter writer, AppTheme appTheme) throws IOException {
writer.beginObject();
writer.name(NAME_OVERLAY_PKG).value(appTheme.mOverlayPkgName);
writer.name(NAME_ICON_PKG).value(appTheme.mIconPkgName);
writer.name(NAME_FONT_PKG).value(appTheme.mFontPkgName);
writer.endObject();
}
public static ThemeConfig fromJson(String json) {
if (json == null) return null;
Map<String, AppTheme> map = new ArrayMap<>();
StringReader reader = null;
JsonReader jsonReader = null;
try {
reader = new StringReader(json);
jsonReader = new JsonReader(reader);
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String appPkgName = jsonReader.nextName();
AppTheme appTheme = readAppTheme(jsonReader);
map.put(appPkgName, appTheme);
}
jsonReader.endObject();
} catch(Exception e) {
Log.e(TAG, "Could not parse ThemeConfig from: " + json, e);
} finally {
closeQuietly(reader);
closeQuietly(jsonReader);
}
return new ThemeConfig(map);
}
private static AppTheme readAppTheme(JsonReader reader) throws IOException {
String overlay = null;
String icon = null;
String font = null;
reader.beginObject();
while(reader.hasNext()) {
String name = reader.nextName();
if (NAME_OVERLAY_PKG.equals(name) && reader.peek() != JsonToken.NULL) {
overlay = reader.nextString();
} else if (NAME_ICON_PKG.equals(name) && reader.peek() != JsonToken.NULL) {
icon = reader.nextString();
} else if (NAME_FONT_PKG.equals(name) && reader.peek() != JsonToken.NULL) {
font = reader.nextString();
} else {
reader.skipValue();
}
}
reader.endObject();
return new AppTheme(overlay, icon, font);
}
private static void closeQuietly(Reader reader) {
try {
if (reader != null) reader.close();
} catch(IOException e) {
}
}
private static void closeQuietly(JsonReader reader) {
try {
if (reader != null) reader.close();
} catch(IOException e) {
}
}
private static void closeQuietly(Writer writer) {
try {
if (writer != null) writer.close();
} catch(IOException e) {
}
}
private static void closeQuietly(JsonWriter writer) {
try {
if (writer != null) writer.close();
} catch(IOException e) {
}
}
}
public static class SystemConfig extends ThemeConfig {
public SystemConfig() {
super(new ArrayMap<String, AppTheme>());
}
}
public static class SystemAppTheme extends AppTheme {
public SystemAppTheme() {
super(SYSTEM_DEFAULT, SYSTEM_DEFAULT, SYSTEM_DEFAULT);
}
@Override
public String toString() {
return "No Theme Applied (System)";
}
}
}