diff options
Diffstat (limited to 'core/java/android/content')
25 files changed, 2748 insertions, 28 deletions
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 4c7dd10..cd3df9b 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3035,6 +3035,16 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a + * {@link android.content.res.ThemeManager} for accessing theme service. + * + * @see #getSystemService + * @see android.content.res.ThemeManager + * @hide + */ + public static final String THEME_SERVICE = "themes"; + + /** + * Use with {@link #getSystemService} to retrieve a * {@link android.nfc.NfcManager} for using NFC. * * @see #getSystemService @@ -3744,6 +3754,26 @@ public abstract class Context { int flags) throws PackageManager.NameNotFoundException; /** + * Similar to {@link #createPackageContext(String, int)}, but with a + * different {@link UserHandle}. For example, {@link #getContentResolver()} + * will open any {@link Uri} as the given user. A theme package can be + * specified which will be used when adding resources to this context + * + * @hide + */ + public abstract Context createPackageContextAsUser( + String packageName, String themePackageName, int flags, UserHandle user) + throws PackageManager.NameNotFoundException; + + /** + * Creates a context given an {@link android.content.pm.ApplicationInfo}. + * + * @hide + */ + public abstract Context createApplicationContext(ApplicationInfo application, + String themePackageName, int flags) throws PackageManager.NameNotFoundException; + + /** * Get the userId associated with this context * @return user id * diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 8359edf..5f57d73 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -757,7 +757,20 @@ public class ContextWrapper extends Context { @Override public Context createApplicationContext(ApplicationInfo application, int flags) throws PackageManager.NameNotFoundException { - return mBase.createApplicationContext(application, flags); + return createApplicationContext(application, null, flags); + } + + /** @hide */ + public Context createApplicationContext(ApplicationInfo application, + String themePackageName, int flags) throws PackageManager.NameNotFoundException { + return mBase.createApplicationContext(application, themePackageName, flags); + } + + /** @hide */ + @Override + public Context createPackageContextAsUser(String packageName, String themePackageName, + int flags, UserHandle user) throws PackageManager.NameNotFoundException { + return mBase.createPackageContextAsUser(packageName, themePackageName, flags, user); } /** @hide */ diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 9e742e5..2340a5e 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2006 The Android Open Source Project + * This code has been modified. Portions copyright (C) 2010, 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. @@ -2841,6 +2842,19 @@ public class Intent implements Parcelable, Cloneable { "android.intent.action.QUICK_CLOCK"; /** + * Broadcast Action: Indicate that unrecoverable error happened during app launch. + * Could indicate that curently applied theme is malicious. + * @hide + */ + public static final String ACTION_APP_LAUNCH_FAILURE = "com.tmobile.intent.action.APP_LAUNCH_FAILURE"; + + /** + * Broadcast Action: Request to reset the unrecoverable errors count to 0. + * @hide + */ + public static final String ACTION_APP_LAUNCH_FAILURE_RESET = "com.tmobile.intent.action.APP_LAUNCH_FAILURE_RESET"; + + /** * Activity Action: Shows the brightness setting dialog. * @hide */ @@ -2936,6 +2950,19 @@ public class Intent implements Parcelable, Cloneable { public static final String ACTION_CREATE_DOCUMENT = "android.intent.action.CREATE_DOCUMENT"; /** + * Broadcast Action: A theme's resources were cached. Includes two extra fields, + * {@link #EXTRA_THEME_PACKAGE_NAME}, containing the package name of the theme that was + * processed, and {@link #EXTRA_THEME_RESULT}, containing the result code. + * + * <p class="note">This is a protected intent that can only be sent + * by the system.</p> + * + * @hide + */ + public static final String ACTION_THEME_RESOURCES_CACHED = + "android.intent.action.THEME_RESOURCES_CACHED"; + + /** * Activity Action: Allow the user to pick a directory subtree. When * invoked, the system will display the various {@link DocumentsProvider} * instances installed on the device, letting the user navigate through @@ -3203,6 +3230,14 @@ public class Intent implements Parcelable, Cloneable { @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_CAR_MODE = "android.intent.category.CAR_MODE"; + /** + * Used to indicate that a theme package has been installed or un-installed. + * + * @hide + */ + public static final String CATEGORY_THEME_PACKAGE_INSTALLED_STATE_CHANGE = + "com.tmobile.intent.category.THEME_PACKAGE_INSTALL_STATE_CHANGE"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Application launch intent categories (see addCategory()). @@ -3813,6 +3848,26 @@ public class Intent implements Parcelable, Cloneable { public static final String EXTRA_SIM_ACTIVATION_RESPONSE = "android.intent.extra.SIM_ACTIVATION_RESPONSE"; + /** + * Extra for {@link #ACTION_THEME_RESOURCES_CACHED} that provides the return value + * from processThemeResources. A value of 0 indicates a successful caching of resources. + * Error results are: + * {@link android.content.pm.PackageManager#INSTALL_FAILED_THEME_AAPT_ERROR} + * {@link android.content.pm.PackageManager#INSTALL_FAILED_THEME_IDMAP_ERROR} + * {@link android.content.pm.PackageManager#INSTALL_FAILED_THEME_UNKNOWN_ERROR} + * + * @hide + */ + public static final String EXTRA_THEME_RESULT = "android.intent.extra.RESULT"; + + /** + * Extra for {@link #ACTION_THEME_RESOURCES_CACHED} that provides the package name of the + * theme that was processed. + * + * @hide + */ + public static final String EXTRA_THEME_PACKAGE_NAME = "android.intent.extra.PACKAGE_NAME"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Intent flags (see mFlags variable). diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 43cc63b..f319a88 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1,6 +1,7 @@ /* * Copyright (C) 2007 The Android Open Source Project - * + * This code has been modified. Portions copyright (C) 2010, 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 @@ -493,6 +494,10 @@ public class ActivityInfo extends ComponentInfo */ public static final int CONFIG_ORIENTATION = 0x0080; /** + * @hide + */ + public static final int CONFIG_THEME_RESOURCE = 0x008000; + /** * Bit in {@link #configChanges} that indicates that the activity * can itself handle changes to the screen layout. Set from the * {@link android.R.attr#configChanges} attribute. diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index d3d4443..2af7db9 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2007 The Android Open Source Project + * This code has been modified. Portions copyright (C) 2010, 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. @@ -668,6 +669,12 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { */ public boolean protect = false; + /* + * Is given application theme agnostic, i.e. behaves properly when default theme is changed. + * @hide + */ + public boolean isThemeable = false; + public void dump(Printer pw, String prefix) { super.dumpFront(pw, prefix); if (className != null) { @@ -801,6 +808,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { backupAgentName = orig.backupAgentName; fullBackupContent = orig.fullBackupContent; protect = orig.protect; + isThemeable = orig.isThemeable; } @@ -856,6 +864,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeInt(uiOptions); dest.writeInt(fullBackupContent); dest.writeInt(protect ? 1 : 0); + dest.writeInt(isThemeable ? 1 : 0); } public static final Parcelable.Creator<ApplicationInfo> CREATOR @@ -910,6 +919,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { uiOptions = source.readInt(); fullBackupContent = source.readInt(); protect = source.readInt() != 0; + isThemeable = source.readInt() != 0; } /** diff --git a/core/java/android/content/pm/BaseThemeInfo.java b/core/java/android/content/pm/BaseThemeInfo.java new file mode 100644 index 0000000..8ece42d --- /dev/null +++ b/core/java/android/content/pm/BaseThemeInfo.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2010, 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.pm; + +import android.os.Parcelable; +import android.os.Parcel; +import android.util.Log; +import android.util.AttributeSet; +import android.content.res.Resources; + +/** + * @hide + */ +public class BaseThemeInfo implements Parcelable { + /** + * The theme id, which does not change when the theme is modified. + * Specifies an Android UI Style using style name. + * + * @see themeId attribute + * + */ + public String themeId; + + /** + * The name of the theme (as displayed by UI). + * + * @see name attribute + * + */ + public String name; + + /** + * The author name of the theme package. + * + * @see author attribute + * + */ + public String author; + + /* + * Describe the kinds of special objects contained in this Parcelable's + * marshalled representation. + * + * @return a bitmask indicating the set of special object types marshalled + * by the Parcelable. + * + * @see android.os.Parcelable#describeContents() + */ + public int describeContents() { + return 0; + } + + /* + * Flatten this object in to a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}. + * + * @see android.os.Parcelable#writeToParcel(android.os.Parcel, int) + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(themeId); + dest.writeString(name); + dest.writeString(author); + } + + /** @hide */ + public static final Parcelable.Creator<BaseThemeInfo> CREATOR + = new Parcelable.Creator<BaseThemeInfo>() { + public BaseThemeInfo createFromParcel(Parcel source) { + return new BaseThemeInfo(source); + } + + public BaseThemeInfo[] newArray(int size) { + return new BaseThemeInfo[size]; + } + }; + + /** @hide */ + public final String getResolvedString(Resources res, AttributeSet attrs, int index) { + int resId = attrs.getAttributeResourceValue(index, 0); + if (resId !=0 ) { + return res.getString(resId); + } + return attrs.getAttributeValue(index); + } + + protected BaseThemeInfo() { + } + + protected BaseThemeInfo(Parcel source) { + themeId = source.readString(); + name = source.readString(); + author = source.readString(); + } +} diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index a0bd10c..e9ff946 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -17,6 +17,7 @@ package android.content.pm; +import android.app.ComposedIconInfo; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; @@ -511,4 +512,9 @@ interface IPackageManager { /** Protected Apps */ void setComponentProtectedSetting(in ComponentName componentName, in boolean newState, int userId); + + /** Themes */ + void updateIconMapping(String pkgName); + ComposedIconInfo getComposedIconInfo(); + int processThemeResources(String themePkgName); } diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index 9e6c6b5..0de867e 100644 --- a/core/java/android/content/pm/PackageInfo.java +++ b/core/java/android/content/pm/PackageInfo.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2007 The Android Open Source Project + * This code has been modified. Portions copyright (C) 2010, 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. @@ -16,6 +17,11 @@ package android.content.pm; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + import android.os.Parcel; import android.os.Parcelable; @@ -254,6 +260,34 @@ public class PackageInfo implements Parcelable { /** @hide */ public boolean coreApp; + // Is Theme Apk + /** + * {@hide} + */ + public boolean isThemeApk = false; + + /** + * {@hide} + */ + public boolean hasIconPack = false; + + /** + * {@hide} + */ + public ArrayList<String> mOverlayTargets; + + // Is Legacy Icon Apk + /** + * {@hide} + */ + public boolean isLegacyIconPackApk = false; + + // ThemeInfo + /** + * {@hide} + */ + public ThemeInfo themeInfo; + /** @hide */ public boolean requiredForAllUsers; @@ -323,6 +357,13 @@ public class PackageInfo implements Parcelable { dest.writeString(restrictedAccountType); dest.writeString(requiredAccountType); dest.writeString(overlayTarget); + + /* Theme-specific. */ + dest.writeInt((isThemeApk) ? 1 : 0); + dest.writeStringList(mOverlayTargets); + dest.writeParcelable(themeInfo, parcelableFlags); + dest.writeInt(hasIconPack ? 1 : 0); + dest.writeInt((isLegacyIconPackApk) ? 1 : 0); } public static final Parcelable.Creator<PackageInfo> CREATOR @@ -372,5 +413,12 @@ public class PackageInfo implements Parcelable { restrictedAccountType = source.readString(); requiredAccountType = source.readString(); overlayTarget = source.readString(); + + /* Theme-specific. */ + isThemeApk = (source.readInt() != 0); + mOverlayTargets = source.createStringArrayList(); + themeInfo = source.readParcelable(null); + hasIconPack = source.readInt() == 1; + isLegacyIconPackApk = source.readInt() == 1; } } diff --git a/core/java/android/content/pm/PackageInfoLite.java b/core/java/android/content/pm/PackageInfoLite.java index 1efe082..d4f33fb 100644 --- a/core/java/android/content/pm/PackageInfoLite.java +++ b/core/java/android/content/pm/PackageInfoLite.java @@ -62,6 +62,7 @@ public class PackageInfoLite implements Parcelable { */ public int recommendedInstallLocation; public int installLocation; + public boolean isTheme; public VerifierInfo[] verifiers; @@ -87,6 +88,7 @@ public class PackageInfoLite implements Parcelable { dest.writeInt(recommendedInstallLocation); dest.writeInt(installLocation); dest.writeInt(multiArch ? 1 : 0); + dest.writeInt(isTheme ? 1 : 0); if (verifiers == null || verifiers.length == 0) { dest.writeInt(0); @@ -116,6 +118,7 @@ public class PackageInfoLite implements Parcelable { recommendedInstallLocation = source.readInt(); installLocation = source.readInt(); multiArch = (source.readInt() != 0); + isTheme = source.readInt() == 1 ? true : false; final int verifiersLength = source.readInt(); if (verifiersLength == 0) { diff --git a/core/java/android/content/pm/PackageItemInfo.java b/core/java/android/content/pm/PackageItemInfo.java index 22a899c..366deb4 100644 --- a/core/java/android/content/pm/PackageItemInfo.java +++ b/core/java/android/content/pm/PackageItemInfo.java @@ -66,7 +66,14 @@ public class PackageItemInfo { * component's icon. From the "icon" attribute or, if not set, 0. */ public int icon; - + + /** + * A drawable resource identifier in the icon pack's resources + * If there isn't an icon pack or not set, then 0. + * @hide + */ + public int themedIcon; + /** * A drawable resource identifier (in the package's resources) of this * component's banner. From the "banner" attribute or, if not set, 0. @@ -110,6 +117,7 @@ public class PackageItemInfo { logo = orig.logo; metaData = orig.metaData; showUserIcon = orig.showUserIcon; + themedIcon = orig.themedIcon; } /** @@ -309,8 +317,9 @@ public class PackageItemInfo { dest.writeBundle(metaData); dest.writeInt(banner); dest.writeInt(showUserIcon); + dest.writeInt(themedIcon); } - + protected PackageItemInfo(Parcel source) { name = source.readString(); packageName = source.readString(); @@ -322,6 +331,7 @@ public class PackageItemInfo { metaData = source.readBundle(); banner = source.readInt(); showUserIcon = source.readInt(); + themedIcon = source.readInt(); } /** diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 53587fd..7b924fa 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -830,6 +830,38 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_ABORTED = -115; /** + * Used by themes + * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the system failed to install the theme because aapt could not compile the app + * @hide + */ + @SystemApi + public static final int INSTALL_FAILED_THEME_AAPT_ERROR = -400; + + /** + * Used by themes + * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the system failed to install the theme because idmap failed + * apps. + * @hide + */ + @SystemApi + public static final int INSTALL_FAILED_THEME_IDMAP_ERROR = -401; + + /** + * Used by themes + * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the system failed to install the theme for an unknown reason + * apps. + * @hide + */ + @SystemApi + public static final int INSTALL_FAILED_THEME_UNKNOWN_ERROR = -402; + + /** * Flag parameter for {@link #deletePackage} to indicate that you don't want to delete the * package's data directory. * @@ -3525,6 +3557,18 @@ public abstract class PackageManager { public abstract Resources getResourcesForApplicationAsUser(String appPackageName, int userId) throws NameNotFoundException; + /** @hide */ + public abstract Resources getThemedResourcesForApplication(ApplicationInfo app, + String themePkgName) throws NameNotFoundException; + + /** @hide */ + public abstract Resources getThemedResourcesForApplication(String appPackageName, + String themePkgName) throws NameNotFoundException; + + /** @hide */ + public abstract Resources getThemedResourcesForApplicationAsUser(String appPackageName, + String themePkgName, int userId) throws NameNotFoundException; + /** * Retrieve overall information about an application package defined * in a package archive file @@ -4729,4 +4773,22 @@ public abstract class PackageManager { } } } + + /** + * Updates the theme icon res id for the new theme + * @hide + */ + public abstract void updateIconMaps(String pkgName); + + /** + * Used to compile theme resources for a given theme + * @param themePkgName + * @return A value of 0 indicates success. Possible errors returned are: + * {@link android.content.pm.PackageManager#INSTALL_FAILED_THEME_AAPT_ERROR}, + * {@link android.content.pm.PackageManager#INSTALL_FAILED_THEME_IDMAP_ERROR}, or + * {@link android.content.pm.PackageManager#INSTALL_FAILED_THEME_UNKNOWN_ERROR} + * + * @hide + */ + public abstract int processThemeResources(String themePkgName); } diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 0edf9c1..b5fcfe9 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2007 The Android Open Source Project + * This code has been modified. Portions copyright (C) 2010, 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. @@ -61,6 +62,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; @@ -77,12 +79,17 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.jar.StrictJarFile; import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; /** * Parser for package files (APKs) on disk. This supports apps packaged either @@ -112,6 +119,17 @@ public class PackageParser { /** File name in an APK for the Android manifest. */ private static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml"; + /** Path to overlay directory in a theme APK */ + private static final String OVERLAY_PATH = "assets/overlays/"; + /** Path to icon directory in a theme APK */ + private static final String ICON_PATH = "assets/icons/"; + + private static final String PACKAGE_REDIRECTIONS_XML = "res/xml/redirections.xml"; + + private static final String TAG_PACKAGE_REDIRECTIONS = "package-redirections"; + private static final String TAG_RESOURCE_REDIRECTIONS = "resource-redirections"; + private static final String TAG_ITEM = "item"; + private static final String ATTRIBUTE_ITEM_NAME = "name"; /** Path prefix for apps on expanded storage */ private static final String MNT_EXPAND = "/mnt/expand/"; @@ -251,6 +269,7 @@ public class PackageParser { public final int versionCode; public final int installLocation; public final VerifierInfo[] verifiers; + public boolean isTheme; /** Names of any split APKs, ordered by parsed splitName */ public final String[] splitNames; @@ -276,6 +295,7 @@ public class PackageParser { public final boolean multiArch; public final boolean extractNativeLibs; + public PackageLite(String codePath, ApkLite baseApk, String[] splitNames, String[] splitCodePaths, int[] splitRevisionCodes) { this.packageName = baseApk.packageName; @@ -291,6 +311,7 @@ public class PackageParser { this.coreApp = baseApk.coreApp; this.multiArch = baseApk.multiArch; this.extractNativeLibs = baseApk.extractNativeLibs; + this.isTheme = baseApk.isTheme; } public List<String> getAllCodePaths() { @@ -318,11 +339,12 @@ public class PackageParser { public final boolean coreApp; public final boolean multiArch; public final boolean extractNativeLibs; + public final boolean isTheme; public ApkLite(String codePath, String packageName, String splitName, int versionCode, int revisionCode, int installLocation, List<VerifierInfo> verifiers, Signature[] signatures, boolean coreApp, boolean multiArch, - boolean extractNativeLibs) { + boolean extractNativeLibs, boolean isTheme) { this.codePath = codePath; this.packageName = packageName; this.splitName = splitName; @@ -334,6 +356,7 @@ public class PackageParser { this.coreApp = coreApp; this.multiArch = multiArch; this.extractNativeLibs = extractNativeLibs; + this.isTheme = isTheme; } } @@ -424,6 +447,14 @@ public class PackageParser { pi.versionName = p.mVersionName; pi.sharedUserId = p.mSharedUserId; pi.sharedUserLabel = p.mSharedUserLabel; + pi.isThemeApk = p.mIsThemeApk; + pi.hasIconPack = p.hasIconPack; + pi.isLegacyIconPackApk = p.mIsLegacyIconPackApk; + + if (pi.isThemeApk) { + pi.mOverlayTargets = p.mOverlayTargets; + pi.themeInfo = p.mThemeInfo; + } pi.applicationInfo = generateApplicationInfo(p, flags, state, userId); pi.installLocation = p.installLocation; pi.coreApp = p.coreApp; @@ -897,6 +928,18 @@ public class PackageParser { pkg.baseCodePath = apkPath; pkg.mSignatures = null; + // If the pkg is a theme, we need to know what themes it overlays + // and determine if it has an icon pack + if (pkg.mIsThemeApk) { + //Determine existance of Overlays + ArrayList<String> overlayTargets = scanPackageOverlays(apkFile); + for(String overlay : overlayTargets) { + pkg.mOverlayTargets.add(overlay); + } + + pkg.hasIconPack = packageHasIconPack(apkFile); + } + return pkg; } catch (PackageParserException e) { @@ -1019,6 +1062,51 @@ public class PackageParser { return pkg; } + + private ArrayList<String> scanPackageOverlays(File originalFile) { + Set<String> overlayTargets = new HashSet<String>(); + + try { + final ZipFile privateZip = new ZipFile(originalFile.getPath()); + final Enumeration<? extends ZipEntry> privateZipEntries = privateZip.entries(); + while (privateZipEntries.hasMoreElements()) { + final ZipEntry zipEntry = privateZipEntries.nextElement(); + final String zipEntryName = zipEntry.getName(); + + if (zipEntryName.startsWith(OVERLAY_PATH) && zipEntryName.length() > 16) { + String[] subdirs = zipEntryName.split("/"); + overlayTargets.add(subdirs[2]); + } + } + } catch(Exception e) { + e.printStackTrace(); + overlayTargets.clear(); + } + + ArrayList<String> overlays = new ArrayList<String>(); + overlays.addAll(overlayTargets); + return overlays; + } + + private boolean packageHasIconPack(File originalFile) { + try { + final ZipFile privateZip = new ZipFile(originalFile.getPath()); + final Enumeration<? extends ZipEntry> privateZipEntries = privateZip.entries(); + while (privateZipEntries.hasMoreElements()) { + final ZipEntry zipEntry = privateZipEntries.nextElement(); + final String zipEntryName = zipEntry.getName(); + + if (zipEntryName.startsWith(ICON_PATH) && + zipEntryName.length() > ICON_PATH.length()) { + return true; + } + } + } catch(Exception e) { + Log.e(TAG, "Could not read zip entries while checking if apk has icon pack", e); + } + return false; + } + /** * Gathers the {@link ManifestDigest} for {@code pkg} if it exists in the * APK. If it successfully scanned the package and found the @@ -1299,6 +1387,9 @@ public class PackageParser { // Only search the tree when the tag is directly below <manifest> int type; final int searchDepth = parser.getDepth() + 1; + // Search for category and actions inside <intent-filter> + final int iconPackSearchDepth = parser.getDepth() + 4; + boolean isTheme = false; final List<VerifierInfo> verifiers = new ArrayList<VerifierInfo>(); while ((type = parser.next()) != XmlPullParser.END_DOCUMENT @@ -1325,11 +1416,53 @@ public class PackageParser { } } } + + if (parser.getDepth() == searchDepth && "meta-data".equals(parser.getName())) { + for (int i=0; i < parser.getAttributeCount(); i++) { + if ("name".equals(parser.getAttributeName(i)) && + ThemeInfo.META_TAG_NAME.equals(parser.getAttributeValue(i))) { + isTheme = true; + installLocation = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY; + break; + } + } + } + + if (parser.getDepth() == searchDepth && "theme".equals(parser.getName())) { + isTheme = true; + installLocation = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY; + } + + if (parser.getDepth() == iconPackSearchDepth && isLegacyIconPack(parser)) { + isTheme = true; + installLocation = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY; + } } return new ApkLite(codePath, packageSplit.first, packageSplit.second, versionCode, revisionCode, installLocation, verifiers, signatures, coreApp, multiArch, - extractNativeLibs); + extractNativeLibs, isTheme); + } + + private static boolean isLegacyIconPack(XmlPullParser parser) { + boolean isAction = "action".equals(parser.getName()); + boolean isCategory = "category".equals(parser.getName()); + String[] items = isAction ? ThemeUtils.sSupportedActions + : (isCategory ? ThemeUtils.sSupportedCategories : null); + + if (items != null) { + for (int i = 0; i < parser.getAttributeCount(); i++) { + if ("name".equals(parser.getAttributeName(i))) { + final String value = parser.getAttributeValue(i); + for (String item : items) { + if (item.equals(value)) { + return true; + } + } + } + } + } + return false; } /** @@ -1381,6 +1514,8 @@ public class PackageParser { } final Package pkg = new Package(pkgName); + Bundle metaDataBundle = new Bundle(); + boolean foundApp = false; TypedArray sa = res.obtainAttributes(attrs, @@ -1793,6 +1928,11 @@ public class PackageParser { XmlUtils.skipCurrentTag(parser); continue; + } else if (parser.getName().equals("meta-data")) { + if ((metaDataBundle=parseMetaData(res, parser, attrs, metaDataBundle, + outError)) == null) { + return null; + } } else if (RIGID_PARSER) { outError[0] = "Bad element under <manifest>: " + parser.getName(); @@ -1881,6 +2021,17 @@ public class PackageParser { >= android.os.Build.VERSION_CODES.DONUT)) { pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES; } + if (pkg.mIsThemeApk || pkg.mIsLegacyIconPackApk) { + pkg.applicationInfo.isThemeable = false; + } + + //Is this pkg a theme? + if (metaDataBundle.containsKey(ThemeInfo.META_TAG_NAME)) { + pkg.mIsThemeApk = true; + pkg.mTrustedOverlay = true; + pkg.mOverlayPriority = 1; + pkg.mThemeInfo = new ThemeInfo(metaDataBundle); + } return pkg; } @@ -2412,6 +2563,9 @@ public class PackageParser { final ApplicationInfo ai = owner.applicationInfo; final String pkgName = owner.applicationInfo.packageName; + // assume that this package is themeable unless explicitly set to false. + ai.isThemeable = true; + TypedArray sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifestApplication); @@ -3241,6 +3395,26 @@ public class PackageParser { if (!parseIntent(res, parser, attrs, true, true, intent, outError)) { return null; } + + // Check if package is a legacy icon pack + if (!owner.mIsLegacyIconPackApk) { + for(String action : ThemeUtils.sSupportedActions) { + if (intent.hasAction(action)) { + owner.mIsLegacyIconPackApk = true; + break; + } + + } + } + if (!owner.mIsLegacyIconPackApk) { + for(String category : ThemeUtils.sSupportedCategories) { + if (intent.hasCategory(category)) { + owner.mIsLegacyIconPackApk = true; + break; + } + } + } + if (intent.countActions() == 0) { Slog.w(TAG, "No actions in intent filter at " + mArchiveSourcePath + " " @@ -4348,6 +4522,17 @@ public class PackageParser { // For use by package manager to keep track of when a package was last used. public long mLastPackageUsageTimeInMills; + // Is Theme Apk + public boolean mIsThemeApk = false; + public final ArrayList<String> mOverlayTargets = new ArrayList<String>(0); + public Map<String, Map<String, String>> mPackageRedirections + = new HashMap<String, Map<String, String>>(); + + // Theme info + public ThemeInfo mThemeInfo = null; + + // Legacy icon pack + public boolean mIsLegacyIconPackApk = false; // // User set enabled state. // public int mSetEnabled = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; @@ -4390,6 +4575,8 @@ public class PackageParser { public int mOverlayPriority; public boolean mTrustedOverlay; + public boolean hasIconPack; + /** * Data used to feed the KeySetManagerService */ diff --git a/core/java/android/content/pm/ThemeInfo.aidl b/core/java/android/content/pm/ThemeInfo.aidl new file mode 100644 index 0000000..acbc85e --- /dev/null +++ b/core/java/android/content/pm/ThemeInfo.aidl @@ -0,0 +1,3 @@ +package android.content.pm; + +parcelable ThemeInfo; diff --git a/core/java/android/content/pm/ThemeInfo.java b/core/java/android/content/pm/ThemeInfo.java new file mode 100644 index 0000000..ab798db --- /dev/null +++ b/core/java/android/content/pm/ThemeInfo.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010, 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.pm; + +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParser; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.content.res.Resources; + +/** + * Overall information about "theme" package. This corresponds + * to the information collected from AndroidManifest.xml + * + * Below is an example of the manifest: + * + * <meta-data android:name="org.cyanogenmod.theme.name" android:value="Foobar's Theme"/> + * <meta-data android:name="org.cyanogenmod.theme.author" android:value="Mr.Foo" /> + * + * @hide + */ +public final class ThemeInfo extends BaseThemeInfo { + + public static final String META_TAG_NAME = "org.cyanogenmod.theme.name"; + public static final String META_TAG_AUTHOR = "org.cyanogenmod.theme.author"; + + public ThemeInfo(Bundle bundle) { + super(); + name = bundle.getString(META_TAG_NAME); + themeId = name; + author = bundle.getString(META_TAG_AUTHOR); + } + + public static final Parcelable.Creator<ThemeInfo> CREATOR + = new Parcelable.Creator<ThemeInfo>() { + public ThemeInfo createFromParcel(Parcel source) { + return new ThemeInfo(source); + } + + public ThemeInfo[] newArray(int size) { + return new ThemeInfo[size]; + } + }; + + private ThemeInfo(Parcel source) { + super(source); + } +} diff --git a/core/java/android/content/pm/ThemeUtils.java b/core/java/android/content/pm/ThemeUtils.java new file mode 100644 index 0000000..7cb2216 --- /dev/null +++ b/core/java/android/content/pm/ThemeUtils.java @@ -0,0 +1,733 @@ +/* + * Copyright (C) 2014 The CyanogenMod 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.pm; + +import android.Manifest; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.IntentFilter; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.ThemeConfig; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.FileUtils; +import android.os.SystemProperties; +import android.provider.MediaStore; +import android.provider.Settings; +import android.provider.ThemesContract; +import android.provider.ThemesContract.ThemesColumns; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.WindowManager; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import static android.content.res.ThemeConfig.SYSTEM_DEFAULT; + +/** + * @hide + */ +public class ThemeUtils { + private static final String TAG = "ThemeUtils"; + + /* Path inside a theme APK to the overlay folder */ + public static final String OVERLAY_PATH = "assets/overlays/"; + public static final String ICONS_PATH = "assets/icons/"; + public static final String COMMON_RES_PATH = "assets/overlays/common/"; + public static final String FONT_XML = "fonts.xml"; + public static final String RESTABLE_EXTENSION = ".arsc"; + public static final String IDMAP_PREFIX = "/data/resource-cache/"; + public static final String IDMAP_SUFFIX = "@idmap"; + public static final String COMMON_RES_SUFFIX = ".common"; + public static final String COMMON_RES_TARGET = "common"; + public static final String ICON_HASH_FILENAME = "hash"; + + // path to external theme resources, i.e. bootanimation.zip + public static final String SYSTEM_THEME_PATH = "/data/system/theme"; + public static final String SYSTEM_THEME_FONT_PATH = SYSTEM_THEME_PATH + File.separator + "fonts"; + public static final String SYSTEM_THEME_RINGTONE_PATH = SYSTEM_THEME_PATH + + File.separator + "ringtones"; + public static final String SYSTEM_THEME_NOTIFICATION_PATH = SYSTEM_THEME_PATH + + File.separator + "notifications"; + public static final String SYSTEM_THEME_ALARM_PATH = SYSTEM_THEME_PATH + + File.separator + "alarms"; + public static final String SYSTEM_THEME_ICON_CACHE_DIR = SYSTEM_THEME_PATH + + File.separator + "icons"; + // internal path to bootanimation.zip inside theme apk + public static final String THEME_BOOTANIMATION_PATH = "assets/bootanimation/bootanimation.zip"; + + public static final String SYSTEM_MEDIA_PATH = "/system/media/audio"; + public static final String SYSTEM_ALARMS_PATH = SYSTEM_MEDIA_PATH + File.separator + + "alarms"; + public static final String SYSTEM_RINGTONES_PATH = SYSTEM_MEDIA_PATH + File.separator + + "ringtones"; + public static final String SYSTEM_NOTIFICATIONS_PATH = SYSTEM_MEDIA_PATH + File.separator + + "notifications"; + + private static final String MEDIA_CONTENT_URI = "content://media/internal/audio/media"; + + public static final String ACTION_THEME_CHANGED = "org.cyanogenmod.intent.action.THEME_CHANGED"; + + public static final String CATEGORY_THEME_COMPONENT_PREFIX = "org.cyanogenmod.intent.category."; + + public static final int SYSTEM_TARGET_API = 0; + + private static final String SETTINGS_DB = + "/data/data/com.android.providers.settings/databases/settings.db"; + private static final String SETTINGS_SECURE_TABLE = "secure"; + + // Actions in manifests which identify legacy icon packs + public static final String[] sSupportedActions = new String[] { + "org.adw.launcher.THEMES", + "com.gau.go.launcherex.theme" + }; + + // Categories in manifests which identify legacy icon packs + public static final String[] sSupportedCategories = new String[] { + "com.fede.launcher.THEME_ICONPACK", + "com.anddoes.launcher.THEME", + "com.teslacoilsw.launcher.THEME" + }; + + + /* + * Retrieve the path to a resource table (ie resource.arsc) + * Themes have a resources.arsc for every overlay package targeted. These are compiled + * at install time and stored in the data partition. + * + */ + public static String getResTablePath(String targetPkgName, PackageInfo overlayPkg) { + return getResTablePath(targetPkgName, overlayPkg.applicationInfo.publicSourceDir); + } + + public static String getResTablePath(String targetPkgName, PackageParser.Package overlayPkg) { + return getResTablePath(targetPkgName, overlayPkg.applicationInfo.publicSourceDir); + } + + public static String getResTablePath(String targetPkgName, String overlayApkPath) { + String restablePath = getResDir(targetPkgName, overlayApkPath) + "/resources.arsc"; + return restablePath; + } + + /* + * Retrieve the path to the directory where resource table (ie resource.arsc) resides + * Themes have a resources.arsc for every overlay package targeted. These are compiled + * at install time and stored in the data partition. + * + */ + public static String getResDir(String targetPkgName, PackageInfo overlayPkg) { + return getResDir(targetPkgName, overlayPkg.applicationInfo.publicSourceDir); + } + + public static String getResDir(String targetPkgName, PackageParser.Package overlayPkg) { + return getResDir(targetPkgName, overlayPkg.applicationInfo.publicSourceDir); + } + + public static String getResDir(String targetPkgName, String overlayApkPath) { + String restableName = overlayApkPath.replaceAll("/", "@") + "@" + targetPkgName; + if (restableName.startsWith("@")) restableName = restableName.substring(1); + return IDMAP_PREFIX + restableName; + } + + public static String getIconPackDir(String pkgName) { + return IDMAP_PREFIX + pkgName; + } + + public static String getIconHashFile(String pkgName) { + return getIconPackDir(pkgName) + File.separator + ICON_HASH_FILENAME; + } + + public static String getIconPackApkPath(String pkgName) { + return getIconPackDir(pkgName) + "/resources.apk"; + } + + public static String getIconPackResPath(String pkgName) { + return getIconPackDir(pkgName) + "/resources.arsc"; + } + + public static String getOverlayPathToTarget(String targetPkgName) { + StringBuilder sb = new StringBuilder(); + sb.append(OVERLAY_PATH); + sb.append(targetPkgName); + sb.append('/'); + return sb.toString(); + } + + public static String getCommonPackageName(String themePackageName) { + if (TextUtils.isEmpty(themePackageName)) return null; + + return COMMON_RES_TARGET; + } + + public static void createCacheDirIfNotExists() throws IOException { + File file = new File(IDMAP_PREFIX); + if (!file.exists() && !file.mkdir()) { + throw new IOException("Could not create dir: " + file.toString()); + } + FileUtils.setPermissions(file, FileUtils.S_IRWXU + | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH, -1, -1); + } + + public static void createResourcesDirIfNotExists(String targetPkgName, String overlayApkPath) + throws IOException { + File file = new File(getResDir(targetPkgName, overlayApkPath)); + if (!file.exists() && !file.mkdir()) { + throw new IOException("Could not create dir: " + file.toString()); + } + FileUtils.setPermissions(file, FileUtils.S_IRWXU + | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH, -1, -1); + } + + public static void createIconDirIfNotExists(String pkgName) throws IOException { + File file = new File(getIconPackDir(pkgName)); + if (!file.exists() && !file.mkdir()) { + throw new IOException("Could not create dir: " + file.toString()); + } + FileUtils.setPermissions(file, FileUtils.S_IRWXU + | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH, -1, -1); + } + + private static boolean dirExists(String dirPath) { + final File dir = new File(dirPath); + return dir.exists() && dir.isDirectory(); + } + + private static void createDirIfNotExists(String dirPath) { + if (!dirExists(dirPath)) { + File dir = new File(dirPath); + if (dir.mkdir()) { + FileUtils.setPermissions(dir, FileUtils.S_IRWXU | + FileUtils.S_IRWXG| FileUtils.S_IROTH | FileUtils.S_IXOTH, -1, -1); + } + } + } + + /** + * Create SYSTEM_THEME_PATH directory if it does not exist + */ + public static void createThemeDirIfNotExists() { + createDirIfNotExists(SYSTEM_THEME_PATH); + } + + /** + * Create SYSTEM_FONT_PATH directory if it does not exist + */ + public static void createFontDirIfNotExists() { + createDirIfNotExists(SYSTEM_THEME_FONT_PATH); + } + + /** + * Create SYSTEM_THEME_RINGTONE_PATH directory if it does not exist + */ + public static void createRingtoneDirIfNotExists() { + createDirIfNotExists(SYSTEM_THEME_RINGTONE_PATH); + } + + /** + * Create SYSTEM_THEME_NOTIFICATION_PATH directory if it does not exist + */ + public static void createNotificationDirIfNotExists() { + createDirIfNotExists(SYSTEM_THEME_NOTIFICATION_PATH); + } + + /** + * Create SYSTEM_THEME_ALARM_PATH directory if it does not exist + */ + public static void createAlarmDirIfNotExists() { + createDirIfNotExists(SYSTEM_THEME_ALARM_PATH); + } + + /** + * Create SYSTEM_THEME_ICON_CACHE_DIR directory if it does not exist + */ + public static void createIconCacheDirIfNotExists() { + createDirIfNotExists(SYSTEM_THEME_ICON_CACHE_DIR); + } + + public static void clearIconCache() { + deleteFilesInDir(SYSTEM_THEME_ICON_CACHE_DIR); + } + + //Note: will not delete populated subdirs + public static void deleteFilesInDir(String dirPath) { + File fontDir = new File(dirPath); + File[] files = fontDir.listFiles(); + if (files != null) { + for(File file : fontDir.listFiles()) { + file.delete(); + } + } + } + + public static InputStream getInputStreamFromAsset(Context ctx, String path) throws IOException { + if (ctx == null || path == null) + return null; + InputStream is = null; + String ASSET_BASE = "file:///android_asset/"; + path = path.substring(ASSET_BASE.length()); + AssetManager assets = ctx.getAssets(); + is = assets.open(path); + return is; + } + + public static void closeQuietly(InputStream stream) { + if (stream == null) + return; + try { + stream.close(); + } catch (IOException e) { + } + } + + public static void closeQuietly(OutputStream stream) { + if (stream == null) + return; + try { + stream.close(); + } catch (IOException e) { + } + } + + /** + * Scale the boot animation to better fit the device by editing the desc.txt found + * in the bootanimation.zip + * @param context Context to use for getting an instance of the WindowManager + * @param input InputStream of the original bootanimation.zip + * @param dst Path to store the newly created bootanimation.zip + * @throws IOException + */ + public static void copyAndScaleBootAnimation(Context context, InputStream input, String dst) + throws IOException { + final OutputStream os = new FileOutputStream(dst); + final ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(os)); + final ZipInputStream bootAni = new ZipInputStream(new BufferedInputStream(input)); + ZipEntry ze; + + zos.setMethod(ZipOutputStream.STORED); + final byte[] bytes = new byte[4096]; + int len; + while ((ze = bootAni.getNextEntry()) != null) { + ZipEntry entry = new ZipEntry(ze.getName()); + entry.setMethod(ZipEntry.STORED); + entry.setCrc(ze.getCrc()); + entry.setSize(ze.getSize()); + entry.setCompressedSize(ze.getSize()); + if (!ze.getName().equals("desc.txt")) { + // just copy this entry straight over into the output zip + zos.putNextEntry(entry); + while ((len = bootAni.read(bytes)) > 0) { + zos.write(bytes, 0, len); + } + } else { + String line; + BufferedReader reader = new BufferedReader(new InputStreamReader(bootAni)); + final String[] info = reader.readLine().split(" "); + + int scaledWidth; + int scaledHeight; + WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics dm = new DisplayMetrics(); + wm.getDefaultDisplay().getRealMetrics(dm); + // just in case the device is in landscape orientation we will + // swap the values since most (if not all) animations are portrait + if (dm.widthPixels > dm.heightPixels) { + scaledWidth = dm.heightPixels; + scaledHeight = dm.widthPixels; + } else { + scaledWidth = dm.widthPixels; + scaledHeight = dm.heightPixels; + } + + int width = Integer.parseInt(info[0]); + int height = Integer.parseInt(info[1]); + + if (width == height) + scaledHeight = scaledWidth; + else { + // adjust scaledHeight to retain original aspect ratio + float scale = (float)scaledWidth / (float)width; + int newHeight = (int)((float)height * scale); + if (newHeight < scaledHeight) + scaledHeight = newHeight; + } + + CRC32 crc32 = new CRC32(); + int size = 0; + ByteBuffer buffer = ByteBuffer.wrap(bytes); + line = String.format("%d %d %s\n", scaledWidth, scaledHeight, info[2]); + buffer.put(line.getBytes()); + size += line.getBytes().length; + crc32.update(line.getBytes()); + while ((line = reader.readLine()) != null) { + line = String.format("%s\n", line); + buffer.put(line.getBytes()); + size += line.getBytes().length; + crc32.update(line.getBytes()); + } + entry.setCrc(crc32.getValue()); + entry.setSize(size); + entry.setCompressedSize(size); + zos.putNextEntry(entry); + zos.write(buffer.array(), 0, size); + } + zos.closeEntry(); + } + zos.close(); + } + + public static boolean isValidAudible(String fileName) { + return (fileName != null && + (fileName.endsWith(".mp3") || fileName.endsWith(".ogg"))); + } + + public static boolean setAudible(Context context, File ringtone, int type, String name) { + final String path = ringtone.getAbsolutePath(); + final String mimeType = name.endsWith(".ogg") ? "audio/ogg" : "audio/mp3"; + ContentValues values = new ContentValues(); + values.put(MediaStore.MediaColumns.DATA, path); + values.put(MediaStore.MediaColumns.TITLE, name); + values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); + values.put(MediaStore.MediaColumns.SIZE, ringtone.length()); + values.put(MediaStore.Audio.Media.IS_RINGTONE, type == RingtoneManager.TYPE_RINGTONE); + values.put(MediaStore.Audio.Media.IS_NOTIFICATION, + type == RingtoneManager.TYPE_NOTIFICATION); + values.put(MediaStore.Audio.Media.IS_ALARM, type == RingtoneManager.TYPE_ALARM); + values.put(MediaStore.Audio.Media.IS_MUSIC, false); + + Uri uri = MediaStore.Audio.Media.getContentUriForPath(path); + Uri newUri = null; + Cursor c = context.getContentResolver().query(uri, + new String[] {MediaStore.MediaColumns._ID}, + MediaStore.MediaColumns.DATA + "='" + path + "'", + null, null); + if (c != null && c.getCount() > 0) { + c.moveToFirst(); + long id = c.getLong(0); + c.close(); + newUri = Uri.withAppendedPath(Uri.parse(MEDIA_CONTENT_URI), "" + id); + context.getContentResolver().update(uri, values, + MediaStore.MediaColumns._ID + "=" + id, null); + } + if (newUri == null) + newUri = context.getContentResolver().insert(uri, values); + try { + RingtoneManager.setActualDefaultRingtoneUri(context, type, newUri); + } catch (Exception e) { + return false; + } + return true; + } + + public static boolean setDefaultAudible(Context context, int type) { + final String audiblePath = getDefaultAudiblePath(type); + if (audiblePath != null) { + Uri uri = MediaStore.Audio.Media.getContentUriForPath(audiblePath); + Cursor c = context.getContentResolver().query(uri, + new String[] {MediaStore.MediaColumns._ID}, + MediaStore.MediaColumns.DATA + "='" + audiblePath + "'", + null, null); + if (c != null && c.getCount() > 0) { + c.moveToFirst(); + long id = c.getLong(0); + c.close(); + uri = Uri.withAppendedPath( + Uri.parse(MEDIA_CONTENT_URI), "" + id); + } + if (uri != null) + RingtoneManager.setActualDefaultRingtoneUri(context, type, uri); + } else { + return false; + } + return true; + } + + public static String getDefaultAudiblePath(int type) { + final String name; + final String path; + switch (type) { + case RingtoneManager.TYPE_ALARM: + name = SystemProperties.get("ro.config.alarm_alert", null); + path = name != null ? SYSTEM_ALARMS_PATH + File.separator + name : null; + break; + case RingtoneManager.TYPE_NOTIFICATION: + name = SystemProperties.get("ro.config.notification_sound", null); + path = name != null ? SYSTEM_NOTIFICATIONS_PATH + File.separator + name : null; + break; + case RingtoneManager.TYPE_RINGTONE: + name = SystemProperties.get("ro.config.ringtone", null); + path = name != null ? SYSTEM_RINGTONES_PATH + File.separator + name : null; + break; + default: + path = null; + break; + } + return path; + } + + public static void clearAudibles(Context context, String audiblePath) { + final File audibleDir = new File(audiblePath); + if (audibleDir.exists()) { + String[] files = audibleDir.list(); + final ContentResolver resolver = context.getContentResolver(); + for (String s : files) { + final String filePath = audiblePath + File.separator + s; + Uri uri = MediaStore.Audio.Media.getContentUriForPath(filePath); + resolver.delete(uri, MediaStore.MediaColumns.DATA + "=\"" + + filePath + "\"", null); + (new File(filePath)).delete(); + } + } + } + + public static Context createUiContext(final Context context) { + try { + Context uiContext = context.createPackageContext("com.android.systemui", + Context.CONTEXT_RESTRICTED); + return new ThemedUiContext(uiContext, context.getPackageName()); + } catch (PackageManager.NameNotFoundException e) { + } + + return null; + } + + public static void registerThemeChangeReceiver(final Context context, + final BroadcastReceiver receiver) { + IntentFilter filter = new IntentFilter(ACTION_THEME_CHANGED); + + context.registerReceiver(receiver, filter); + } + + public static String getLockscreenWallpaperPath(AssetManager assetManager) throws IOException { + String[] assets = assetManager.list("lockscreen"); + String asset = getFirstNonEmptyAsset(assets); + if (asset == null) return null; + return "lockscreen/" + asset; + } + + public static String getWallpaperPath(AssetManager assetManager) throws IOException { + String[] assets = assetManager.list("wallpapers"); + String asset = getFirstNonEmptyAsset(assets); + if (asset == null) return null; + return "wallpapers/" + asset; + } + + // Returns the first non-empty asset name. Empty assets can occur if the APK is built + // with folders included as zip entries in the APK. Searching for files inside "folderName" via + // assetManager.list("folderName") can cause these entries to be included as empty strings. + private static String getFirstNonEmptyAsset(String[] assets) { + if (assets == null) return null; + String filename = null; + for(String asset : assets) { + if (!asset.isEmpty()) { + filename = asset; + break; + } + } + return filename; + } + + public static String getDefaultThemePackageName(Context context) { + final String defaultThemePkg = Settings.Secure.getString(context.getContentResolver(), + Settings.Secure.DEFAULT_THEME_PACKAGE); + if (!TextUtils.isEmpty(defaultThemePkg)) { + PackageManager pm = context.getPackageManager(); + try { + if (pm.getPackageInfo(defaultThemePkg, 0) != null) { + return defaultThemePkg; + } + } catch (PackageManager.NameNotFoundException e) { + // doesn't exist so system will be default + Log.w(TAG, "Default theme " + defaultThemePkg + " not found", e); + } + } + + return SYSTEM_DEFAULT; + } + + private static class ThemedUiContext extends ContextWrapper { + private String mPackageName; + + public ThemedUiContext(Context context, String packageName) { + super(context); + mPackageName = packageName; + } + + @Override + public String getPackageName() { + return mPackageName; + } + } + + // Returns a mutable list of all theme components + public static List<String> getAllComponents() { + List<String> components = new ArrayList<String>(9); + components.add(ThemesColumns.MODIFIES_FONTS); + components.add(ThemesColumns.MODIFIES_LAUNCHER); + components.add(ThemesColumns.MODIFIES_ALARMS); + components.add(ThemesColumns.MODIFIES_BOOT_ANIM); + components.add(ThemesColumns.MODIFIES_ICONS); + components.add(ThemesColumns.MODIFIES_LOCKSCREEN); + components.add(ThemesColumns.MODIFIES_NOTIFICATIONS); + components.add(ThemesColumns.MODIFIES_OVERLAYS); + components.add(ThemesColumns.MODIFIES_RINGTONES); + components.add(ThemesColumns.MODIFIES_STATUS_BAR); + components.add(ThemesColumns.MODIFIES_NAVIGATION_BAR); + return components; + } + + /** + * Returns a mutable list of all the theme components supported by a given package + * NOTE: This queries the themes content provider. If there isn't a provider installed + * or if it is too early in the boot process this method will not work. + */ + public static List<String> getSupportedComponents(Context context, String pkgName) { + List<String> supportedComponents = new ArrayList<String>(); + + String selection = ThemesContract.ThemesColumns.PKG_NAME + "= ?"; + String[] selectionArgs = new String[]{ pkgName }; + Cursor c = context.getContentResolver().query(ThemesContract.ThemesColumns.CONTENT_URI, + null, selection, selectionArgs, null); + + if (c != null && c.moveToFirst()) { + List<String> allComponents = getAllComponents(); + for(String component : allComponents) { + int index = c.getColumnIndex(component); + if (c.getInt(index) == 1) { + supportedComponents.add(component); + } + } + } + return supportedComponents; + } + + /** + * Get the components from the default theme. If the default theme is not SYSTEM then any + * components that are not in the default theme will come from SYSTEM to create a complete + * component map. + * @param context + * @return + */ + public static Map<String, String> getDefaultComponents(Context context) { + String defaultThemePkg = getDefaultThemePackageName(context); + List<String> defaultComponents = null; + List<String> systemComponents = getSupportedComponents(context, SYSTEM_DEFAULT); + if (!SYSTEM_DEFAULT.equals(defaultThemePkg)) { + defaultComponents = getSupportedComponents(context, defaultThemePkg); + } + + Map<String, String> componentMap = new HashMap<String, String>(systemComponents.size()); + if (defaultComponents != null) { + for (String component : defaultComponents) { + componentMap.put(component, defaultThemePkg); + } + } + for (String component : systemComponents) { + if (!componentMap.containsKey(component)) { + componentMap.put(component, SYSTEM_DEFAULT); + } + } + + return componentMap; + } + + /** + * Takes an existing component map and adds any missing components from the default + * map of components. + * @param context + * @param componentMap An existing component map + */ + public static void completeComponentMap(Context context, + Map<String, String> componentMap) { + if (componentMap == null) return; + + Map<String, String> defaultComponents = getDefaultComponents(context); + for (String component : defaultComponents.keySet()) { + if (!componentMap.containsKey(component)) { + componentMap.put(component, defaultComponents.get(component)); + } + } + } + + /** + * Get the boot theme by accessing the settings.db directly instead of using a content resolver. + * Only use this when the system is starting up and the settings content provider is not ready. + * + * Note: This method will only succeed if the system is calling this since normal apps will not + * be able to access the settings db path. + * + * @return The boot theme or null if unable to read the database or get the entry for theme + * config + */ + public static ThemeConfig getBootThemeDirty() { + ThemeConfig config = null; + SQLiteDatabase db = null; + try { + db = SQLiteDatabase.openDatabase(SETTINGS_DB, null, + SQLiteDatabase.OPEN_READONLY); + if (db != null) { + String selection = "name=?"; + String[] selectionArgs = + { Configuration.THEME_PKG_CONFIGURATION_PERSISTENCE_PROPERTY }; + String[] columns = {"value"}; + Cursor c = db.query(SETTINGS_SECURE_TABLE, columns, selection, selectionArgs, + null, null, null); + if (c != null) { + if (c.getCount() > 0) { + c.moveToFirst(); + String json = c.getString(0); + if (json != null) { + config = ThemeConfig.fromJson(json); + } + } + c.close(); + } + } + } catch (Exception e) { + Log.w(TAG, "Unable to open " + SETTINGS_DB, e); + } finally { + if (db != null) { + db.close(); + } + } + + return config; + } +} diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index 8d96f5c..f663c50 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -24,6 +24,7 @@ import android.util.TypedValue; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.HashMap; /** @@ -77,6 +78,16 @@ public final class AssetManager implements AutoCloseable { private boolean mOpen = true; private HashMap<Long, RuntimeException> mRefStacks; + private String mAppName; + + private boolean mThemeSupport; + private String mThemePackageName; + private String mIconPackageName; + private String mCommonResPackageName; + private ArrayList<Integer> mThemeCookies = new ArrayList<Integer>(2); + private int mIconPackCookie; + private int mCommonResCookie; + /** * Create a new AssetManager containing only the basic system assets. * Applications will not generally use this method, instead retrieving the @@ -252,6 +263,12 @@ public final class AssetManager implements AutoCloseable { } } + /*package*/ final void recreateStringBlocks() { + synchronized (this) { + makeStringBlocks(sSystem.mStringBlocks); + } + } + /*package*/ final void makeStringBlocks(StringBlock[] seed) { final int seedNum = (seed != null) ? seed.length : 0; final int num = getStringBlockCount(); @@ -628,9 +645,11 @@ public final class AssetManager implements AutoCloseable { * {@hide} */ - public final int addOverlayPath(String idmapPath) { + public final int addOverlayPath(String idmapPath, String resApkPath, String targetPkgPath, + String prefixPath) { synchronized (this) { - int res = addOverlayPathNative(idmapPath); + int res = addOverlayPathNative(idmapPath, resApkPath, targetPkgPath, + prefixPath); makeStringBlocks(mStringBlocks); return res; } @@ -641,7 +660,59 @@ public final class AssetManager implements AutoCloseable { * * {@hide} */ - public native final int addOverlayPathNative(String idmapPath); + private native final int addOverlayPathNative(String idmapPath, + String resApkPath, String targetPkgPath, String prefixPath); + + /** + * Add a set of common assets. + * + * {@hide} + */ + public final int addCommonOverlayPath(String idmapPath, + String resApkPath, String prefixPath) { + synchronized (this) { + return addCommonOverlayPathNative(idmapPath, resApkPath, prefixPath); + } + } + + private native final int addCommonOverlayPathNative(String idmapPath, + String resApkPath, String prefixPath); + + /** + * Add a set of assets as an icon pack. A pkgIdOverride value will change the package's id from + * what is in the resource table to a new value. Manage this carefully, if icon pack has more + * than one package then that next package's id will use pkgIdOverride+1. + * + * Icon packs are different from overlays as they have a different pkg id and + * do not use idmap so no targetPkg is required + * + * {@hide} + */ + public final int addIconPath(String idmapPath, String resApkPath, + String prefixPath, int pkgIdOverride) { + synchronized (this) { + return addIconPathNative(idmapPath, resApkPath, prefixPath, pkgIdOverride); + } + } + + private native final int addIconPathNative(String idmapPath, + String resApkPath, String prefixPath, int pkgIdOverride); + + /** + * Delete a set of overlay assets from the asset manager. Not for use by + * applications. Returns true if succeeded or false on failure. + * + * Also works for icon packs + * + * {@hide} + */ + public final boolean removeOverlayPath(String packageName, int cookie) { + synchronized (this) { + return removeOverlayPathNative(packageName, cookie); + } + } + + private native final boolean removeOverlayPathNative(String packageName, int cookie); /** * Add multiple sets of assets to the asset manager at once. See @@ -664,6 +735,126 @@ public final class AssetManager implements AutoCloseable { } /** + * Sets a flag indicating that this AssetManager should have themes + * attached, according to the initial request to create it by the + * ApplicationContext. + * + * {@hide} + */ + public final void setThemeSupport(boolean themeSupport) { + mThemeSupport = themeSupport; + } + + /** + * Should this AssetManager have themes attached, according to the initial + * request to create it by the ApplicationContext? + * + * {@hide} + */ + public final boolean hasThemeSupport() { + return mThemeSupport; + } + + /** + * Get package name of current icon pack (may return null). + * {@hide} + */ + public String getIconPackageName() { + return mIconPackageName; + } + + /** + * Sets icon package name + * {@hide} + */ + public void setIconPackageName(String packageName) { + mIconPackageName = packageName; + } + + /** + * Get package name of current common resources (may return null). + * {@hide} + */ + public String getCommonResPackageName() { + return mCommonResPackageName; + } + + /** + * Sets common resources package name + * {@hide} + */ + public void setCommonResPackageName(String packageName) { + mCommonResPackageName = packageName; + } + + /** + * Get package name of current theme (may return null). + * {@hide} + */ + public String getThemePackageName() { + return mThemePackageName; + } + + /** + * Sets package name and highest level style id for current theme (null, 0 is allowed). + * {@hide} + */ + public void setThemePackageName(String packageName) { + mThemePackageName = packageName; + } + + /** + * Get asset cookie for current theme (may return 0). + * {@hide} + */ + public ArrayList<Integer> getThemeCookies() { + return mThemeCookies; + } + + /** {@hide} */ + public void setIconPackCookie(int cookie) { + mIconPackCookie = cookie; + } + + /** {@hide} */ + public int getIconPackCookie() { + return mIconPackCookie; + } + + /** {@hide} */ + public void setCommonResCookie(int cookie) { + mCommonResCookie = cookie; + } + + /** {@hide} */ + public int getCommonResCookie() { + return mCommonResCookie; + } + + /** + * Sets asset cookie for current theme (0 if not a themed asset manager). + * {@hide} + */ + public void addThemeCookie(int cookie) { + mThemeCookies.add(cookie); + } + + /** {@hide} */ + public String getAppName() { + return mAppName; + } + + /** {@hide} */ + public void setAppName(String pkgName) { + mAppName = pkgName; + } + + /** {@hide} */ + public boolean hasThemedAssets() { + return mThemeCookies.size() > 0; + } + + /** * Determine whether the state in this asset manager is up-to-date with * the files on the filesystem. If false is returned, you need to * instantiate a new AssetManager class to see the new data. @@ -800,6 +991,26 @@ public final class AssetManager implements AutoCloseable { /*package*/ native final int[] getStyleAttributes(int themeRes); private native final void init(boolean isSystem); + /** + * {@hide} + */ + public native final int getBasePackageCount(); + + /** + * {@hide} + */ + public native final String getBasePackageName(int index); + + /** + * {@hide} + */ + public native final String getBaseResourcePackageName(int index); + + /** + * {@hide} + */ + public native final int getBasePackageId(int index); + private native final void destroy(); private final void incRefsLocked(long id) { diff --git a/core/java/android/content/res/CompatibilityInfo.java b/core/java/android/content/res/CompatibilityInfo.java index da35ee9..47d5d05 100644 --- a/core/java/android/content/res/CompatibilityInfo.java +++ b/core/java/android/content/res/CompatibilityInfo.java @@ -92,9 +92,15 @@ public class CompatibilityInfo implements Parcelable { */ public final float applicationInvertedScale; + /** + * Whether the application supports third-party theming. + */ + public final boolean isThemeable; + public CompatibilityInfo(ApplicationInfo appInfo, int screenLayout, int sw, boolean forceCompat) { int compatFlags = 0; + isThemeable = appInfo.isThemeable; if (appInfo.requiresSmallestWidthDp != 0 || appInfo.compatibleWidthLimitDp != 0 || appInfo.largestWidthLimitDp != 0) { @@ -242,17 +248,19 @@ public class CompatibilityInfo implements Parcelable { } private CompatibilityInfo(int compFlags, - int dens, float scale, float invertedScale) { + int dens, float scale, float invertedScale, boolean isThemeable) { mCompatibilityFlags = compFlags; applicationDensity = dens; applicationScale = scale; applicationInvertedScale = invertedScale; + this.isThemeable = isThemeable; } private CompatibilityInfo() { this(NEVER_NEEDS_COMPAT, DisplayMetrics.DENSITY_DEVICE, 1.0f, - 1.0f); + 1.0f, + true); } /** @@ -526,6 +534,7 @@ public class CompatibilityInfo implements Parcelable { if (applicationDensity != oc.applicationDensity) return false; if (applicationScale != oc.applicationScale) return false; if (applicationInvertedScale != oc.applicationInvertedScale) return false; + if (isThemeable != oc.isThemeable) return false; return true; } catch (ClassCastException e) { return false; @@ -563,6 +572,7 @@ public class CompatibilityInfo implements Parcelable { result = 31 * result + applicationDensity; result = 31 * result + Float.floatToIntBits(applicationScale); result = 31 * result + Float.floatToIntBits(applicationInvertedScale); + result = 31 * result + (isThemeable ? 1 : 0); return result; } @@ -577,6 +587,7 @@ public class CompatibilityInfo implements Parcelable { dest.writeInt(applicationDensity); dest.writeFloat(applicationScale); dest.writeFloat(applicationInvertedScale); + dest.writeInt(isThemeable ? 1 : 0); } public static final Parcelable.Creator<CompatibilityInfo> CREATOR @@ -597,5 +608,6 @@ public class CompatibilityInfo implements Parcelable { applicationDensity = source.readInt(); applicationScale = source.readFloat(); applicationInvertedScale = source.readFloat(); + isThemeable = source.readInt() == 1 ? true : false; } } diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index fd60476..f077d4d 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2008 The Android Open Source Project + * This code has been modified. Portions copyright (C) 2010, 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. @@ -82,6 +83,11 @@ public final class Configuration implements Parcelable, Comparable<Configuration public Locale locale; /** + * @hide + */ + public ThemeConfig themeConfig; + + /** * Locale should persist on setting. This is hidden because it is really * questionable whether this is the right way to expose the functionality. * @hide @@ -441,7 +447,47 @@ public final class Configuration implements Parcelable, Comparable<Configuration public static final int ORIENTATION_LANDSCAPE = 2; /** @deprecated Not currently supported or used. */ @Deprecated public static final int ORIENTATION_SQUARE = 3; - + + /** + * @hide + * @deprecated + */ + public static final String THEME_PACKAGE_NAME_PERSISTENCE_PROPERTY + = "persist.sys.themePackageName"; + + /** + * @hide + * @deprecated + */ + public static final String THEME_ICONPACK_PACKAGE_NAME_PERSISTENCE_PROPERTY + = "themeIconPackPkgName"; + + /** + * @hide + * @deprecated + */ + public 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"; + /** * Overall orientation of the screen. May be one of * {@link #ORIENTATION_LANDSCAPE}, {@link #ORIENTATION_PORTRAIT}. @@ -673,8 +719,11 @@ public final class Configuration implements Parcelable, Comparable<Configuration compatScreenHeightDp = o.compatScreenHeightDp; compatSmallestScreenWidthDp = o.compatSmallestScreenWidthDp; seq = o.seq; + if (o.themeConfig != null) { + themeConfig = (ThemeConfig) o.themeConfig.clone(); + } } - + public String toString() { StringBuilder sb = new StringBuilder(128); sb.append("{"); @@ -809,6 +858,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration sb.append(" s."); sb.append(seq); } + sb.append(" themeResource="); + sb.append(themeConfig); sb.append('}'); return sb.toString(); } @@ -835,6 +886,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration smallestScreenWidthDp = compatSmallestScreenWidthDp = SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; densityDpi = DENSITY_DPI_UNDEFINED; seq = 0; + themeConfig = null; } /** {@hide} */ @@ -977,7 +1029,13 @@ public final class Configuration implements Parcelable, Comparable<Configuration if (delta.seq != 0) { seq = delta.seq; } - + + if (delta.themeConfig != null + && (themeConfig == null || !themeConfig.equals(delta.themeConfig))) { + changed |= ActivityInfo.CONFIG_THEME_RESOURCE; + themeConfig = (ThemeConfig)delta.themeConfig.clone(); + } + return changed; } @@ -1087,7 +1145,10 @@ public final class Configuration implements Parcelable, Comparable<Configuration && densityDpi != delta.densityDpi) { changed |= ActivityInfo.CONFIG_DENSITY; } - + if (delta.themeConfig != null && + (themeConfig == null || !themeConfig.equals(delta.themeConfig))) { + changed |= ActivityInfo.CONFIG_THEME_RESOURCE; + } return changed; } @@ -1103,7 +1164,9 @@ public final class Configuration implements Parcelable, Comparable<Configuration * @return Return true if the resource needs to be loaded, else false. */ public static boolean needNewResources(int configChanges, int interestingChanges) { - return (configChanges & (interestingChanges|ActivityInfo.CONFIG_FONT_SCALE)) != 0; + return (configChanges & (interestingChanges | + ActivityInfo.CONFIG_FONT_SCALE | + ActivityInfo.CONFIG_THEME_RESOURCE)) != 0; } /** @@ -1176,6 +1239,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration dest.writeInt(compatScreenHeightDp); dest.writeInt(compatSmallestScreenWidthDp); dest.writeInt(seq); + dest.writeParcelable(themeConfig, flags); } public void readFromParcel(Parcel source) { @@ -1204,6 +1268,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration compatScreenHeightDp = source.readInt(); compatSmallestScreenWidthDp = source.readInt(); seq = source.readInt(); + themeConfig = source.readParcelable(ThemeConfig.class.getClassLoader()); } public static final Parcelable.Creator<Configuration> CREATOR @@ -1271,7 +1336,12 @@ public final class Configuration implements Parcelable, Comparable<Configuration n = this.smallestScreenWidthDp - that.smallestScreenWidthDp; if (n != 0) return n; n = this.densityDpi - that.densityDpi; - //if (n != 0) return n; + if (n != 0) return n; + if (this.themeConfig == null) { + if (that.themeConfig != null) return 1; + } else { + n = this.themeConfig.compareTo(that.themeConfig); + } return n; } @@ -1308,6 +1378,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration result = 31 * result + screenHeightDp; result = 31 * result + smallestScreenWidthDp; result = 31 * result + densityDpi; + result = 31 * result + (this.themeConfig != null ? + this.themeConfig.hashCode() : 0); return result; } diff --git a/core/java/android/content/res/IThemeChangeListener.aidl b/core/java/android/content/res/IThemeChangeListener.aidl new file mode 100644 index 0000000..a2e2abd --- /dev/null +++ b/core/java/android/content/res/IThemeChangeListener.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2014 The CyanogenMod 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.res; + +/** {@hide} */ +oneway interface IThemeChangeListener { + void onProgress(int progress); + void onFinish(boolean isSuccess); +} diff --git a/core/java/android/content/res/IThemeProcessingListener.aidl b/core/java/android/content/res/IThemeProcessingListener.aidl new file mode 100644 index 0000000..2e1c16e --- /dev/null +++ b/core/java/android/content/res/IThemeProcessingListener.aidl @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2014 The CyanogenMod 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.res; + +/** {@hide} */ +oneway interface IThemeProcessingListener { + void onFinishedProcessing(String pkgName); +} diff --git a/core/java/android/content/res/IThemeService.aidl b/core/java/android/content/res/IThemeService.aidl new file mode 100644 index 0000000..e8bb5c4 --- /dev/null +++ b/core/java/android/content/res/IThemeService.aidl @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014 The CyanogenMod 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.res; + +import android.content.res.IThemeChangeListener; +import android.content.res.IThemeProcessingListener; +import android.graphics.Bitmap; + +import java.util.Map; + +/** {@hide} */ +interface IThemeService { + void requestThemeChangeUpdates(in IThemeChangeListener listener); + void removeUpdates(in IThemeChangeListener listener); + + void requestThemeChange(in Map componentMap); + void applyDefaultTheme(); + boolean isThemeApplying(); + int getProgress(); + + boolean cacheComposedIcon(in Bitmap icon, String path); + + boolean processThemeResources(String themePkgName); + boolean isThemeBeingProcessed(String themePkgName); + void registerThemeProcessingListener(in IThemeProcessingListener listener); + void unregisterThemeProcessingListener(in IThemeProcessingListener listener); +} diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 731903c..f6a966b 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -21,6 +21,9 @@ import android.annotation.ColorInt; import android.annotation.StyleRes; import android.annotation.StyleableRes; import com.android.internal.util.GrowingArrayUtils; +import android.app.ComposedIconInfo; +import android.app.IconPackHelper; +import android.app.IconPackHelper.IconCustomizer; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; @@ -45,6 +48,7 @@ import android.annotation.RawRes; import android.annotation.StringRes; import android.annotation.XmlRes; import android.content.pm.ActivityInfo; +import android.content.pm.PackageItemInfo; import android.graphics.Movie; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; @@ -59,6 +63,7 @@ import android.util.Log; import android.util.LongSparseArray; import android.util.Pools.SynchronizedPool; import android.util.Slog; +import android.util.SparseArray; import android.util.TypedValue; import android.view.ViewDebug; import android.view.ViewHierarchyEncoder; @@ -108,6 +113,20 @@ public class Resources { private static final int ID_OTHER = 0x01000004; + // Package IDs for themes. Aapt will compile the res table with this id. + /** @hide */ + public static final int THEME_FRAMEWORK_PKG_ID = 0x60; + /** @hide */ + public static final int THEME_APP_PKG_ID = 0x61; + /** @hide */ + public static final int THEME_ICON_PKG_ID = 0x62; + /** + * The common resource pkg id needs to be less than the THEME_FRAMEWORK_PKG_ID + * otherwise aapt will complain and fail + * @hide + */ + public static final int THEME_COMMON_PKG_ID = THEME_FRAMEWORK_PKG_ID - 1; + private static final Object sSync = new Object(); // Information about preloaded resources. Note that they are not @@ -158,6 +177,9 @@ public class Resources { private CompatibilityInfo mCompatibilityInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; + private SparseArray<PackageItemInfo> mIcons; + private ComposedIconInfo mComposedIconInfo; + static { sPreloadedDrawables = new LongSparseArray[2]; sPreloadedDrawables[0] = new LongSparseArray<>(); @@ -268,7 +290,7 @@ public class Resources { mCompatibilityInfo = compatInfo; } updateConfiguration(config, metrics); - assets.ensureStringBlocks(); + assets.recreateStringBlocks(); } /** @@ -793,6 +815,19 @@ public class Resources { */ @Nullable public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException { + return getDrawable(id, theme, true); + } + + /** @hide */ + @Nullable + public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme, boolean supportComposedIcons) + throws NotFoundException { + //Check if an icon is themed + PackageItemInfo info = mIcons != null ? mIcons.get(id) : null; + if (info != null && info.themedIcon != 0) { + id = info.themedIcon; + } + TypedValue value; synchronized (mAccessLock) { value = mTmpValue; @@ -801,9 +836,24 @@ public class Resources { } else { mTmpValue = null; } - getValue(id, value, true); + getValue(id, value, true, supportComposedIcons); + } + Drawable res = null; + try { + res = loadDrawable(value, id, theme); + } catch (NotFoundException e) { + // The below statement will be true if we were trying to load a composed icon. + // Since we received a NotFoundException, try to load the original if this + // condition is true, otherwise throw the original exception. + if (supportComposedIcons && mComposedIconInfo != null && info != null && + info.themedIcon == 0) { + Log.e(TAG, "Failed to retrieve composed icon.", e); + getValue(id, value, true, false); + res = loadDrawable(value, id, theme); + } else { + throw e; + } } - final Drawable res = loadDrawable(value, id, theme); synchronized (mAccessLock) { if (mTmpValue == null) { mTmpValue = value; @@ -860,6 +910,19 @@ public class Resources { */ @Nullable public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) { + return getDrawableForDensity(id, density, theme, true); + } + + /** @hide */ + @Nullable + public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme, + boolean supportComposedIcons) { + //Check if an icon was themed + PackageItemInfo info = mIcons != null ? mIcons.get(id) : null; + if (info != null && info.themedIcon != 0) { + id = info.themedIcon; + } + TypedValue value; synchronized (mAccessLock) { value = mTmpValue; @@ -868,7 +931,7 @@ public class Resources { } else { mTmpValue = null; } - getValueForDensity(id, density, value, true); + getValueForDensity(id, density, value, true, supportComposedIcons); /* * Pretend the requested density is actually the display density. If @@ -1344,8 +1407,24 @@ public class Resources { */ public void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException { + getValue(id, outValue, resolveRefs, true); + } + + /** @hide */ + public void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs, + boolean supportComposedIcons) throws NotFoundException { + //Check if an icon was themed + PackageItemInfo info = mIcons != null ? mIcons.get(id) : null; + if (info != null && info.themedIcon != 0) { + id = info.themedIcon; + } boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs); if (found) { + if (supportComposedIcons && IconPackHelper.shouldComposeIcon(mComposedIconInfo) + && info != null && info.themedIcon == 0) { + Drawable dr = loadDrawable(outValue, id, null); + IconCustomizer.getValue(this, id, outValue, dr); + } return; } throw new NotFoundException("Resource ID #0x" @@ -1367,8 +1446,45 @@ public class Resources { */ public void getValueForDensity(@AnyRes int id, int density, TypedValue outValue, boolean resolveRefs) throws NotFoundException { + getValueForDensity(id, density, outValue, resolveRefs, true); + } + + /** @hide */ + public void getValueForDensity(@AnyRes int id, int density, TypedValue outValue, + boolean resolveRefs, boolean supportComposedIcons) throws NotFoundException { + //Check if an icon was themed + PackageItemInfo info = mIcons != null ? mIcons.get(id) : null; + if (info != null && info.themedIcon != 0) { + id = info.themedIcon; + } + boolean found = mAssets.getResourceValue(id, density, outValue, resolveRefs); if (found) { + if (supportComposedIcons && IconPackHelper.shouldComposeIcon(mComposedIconInfo) && + info != null && info.themedIcon == 0) { + int tmpDensity = outValue.density; + /* + * Pretend the requested density is actually the display density. If + * the drawable returned is not the requested density, then force it + * to be scaled later by dividing its density by the ratio of + * requested density to actual device density. Drawables that have + * undefined density or no density don't need to be handled here. + */ + if (outValue.density > 0 && outValue.density != TypedValue.DENSITY_NONE) { + if (outValue.density == density) { + outValue.density = mMetrics.densityDpi; + } else { + outValue.density = (outValue.density * mMetrics.densityDpi) / density; + } + } + Drawable dr = loadDrawable(outValue, id, null); + + // Return to original density. If we do not do this then + // the caller will get the wrong density for the given id and perform + // more of its own scaling in loadDrawable + outValue.density = tmpDensity; + IconCustomizer.getValue(this, id, outValue, dr); + } return; } throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)); @@ -2083,7 +2199,15 @@ public class Resources { mTmpConfig.setLayoutDirection(mTmpConfig.locale); } configChanges = mConfiguration.updateFrom(mTmpConfig); - configChanges = ActivityInfo.activityInfoConfigToNative(configChanges); + + /* This is ugly, but modifying the activityInfoConfigToNative + * adapter would be messier */ + if ((configChanges & ActivityInfo.CONFIG_THEME_RESOURCE) != 0) { + configChanges = ActivityInfo.activityInfoConfigToNative(configChanges); + configChanges |= ActivityInfo.CONFIG_THEME_RESOURCE; + } else { + configChanges = ActivityInfo.activityInfoConfigToNative(configChanges); + } } return configChanges; } @@ -2526,9 +2650,10 @@ public class Resources { // attributes. final ConstantState cs; if (isColorDrawable) { - cs = sPreloadedColorDrawables.get(key); + cs = mAssets.hasThemedAssets() ? null : sPreloadedColorDrawables.get(key); } else { - cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key); + cs = mAssets.hasThemedAssets() ? null : + sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key); } Drawable dr; @@ -2666,7 +2791,7 @@ public class Resources { if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { final android.content.res.ConstantState<ColorStateList> factory = - sPreloadedColorStateLists.get(key); + mAssets.hasThemedAssets() ? null : sPreloadedColorStateLists.get(key); if (factory != null) { return factory.newInstance(); } @@ -2690,7 +2815,7 @@ public class Resources { } final android.content.res.ConstantState<ColorStateList> factory = - sPreloadedColorStateLists.get(key); + mAssets.hasThemedAssets() ? null : sPreloadedColorStateLists.get(key); if (factory != null) { csl = factory.newInstance(this, theme); } @@ -2845,6 +2970,28 @@ public class Resources { return theme.obtainStyledAttributes(set, attrs, 0, 0); } + /** @hide */ + public void setIconResources(SparseArray<PackageItemInfo> icons) { + mIcons = icons; + } + + /** @hide */ + public void setComposedIconInfo(ComposedIconInfo iconInfo) { + mComposedIconInfo = iconInfo; + } + + /** @hide */ + public ComposedIconInfo getComposedIconInfo() { + return mComposedIconInfo; + } + + /** @hide */ + public final void updateStringCache() { + synchronized (mAccessLock) { + mAssets.recreateStringBlocks(); + } + } + private Resources() { mAssets = AssetManager.getSystem(); // NOTE: Intentionally leaving this uninitialized (all values set diff --git a/core/java/android/content/res/ResourcesKey.java b/core/java/android/content/res/ResourcesKey.java index 2620571..f2ed758 100644 --- a/core/java/android/content/res/ResourcesKey.java +++ b/core/java/android/content/res/ResourcesKey.java @@ -24,6 +24,7 @@ import java.util.Objects; public final class ResourcesKey { private final String mResDir; private final float mScale; + private final boolean mIsThemeable; private final int mHash; public final int mDisplayId; @@ -31,18 +32,20 @@ public final class ResourcesKey { public final Configuration mOverrideConfiguration; public ResourcesKey(String resDir, int displayId, Configuration overrideConfiguration, - float scale) { + float scale, boolean isThemeable) { mResDir = resDir; mDisplayId = displayId; mOverrideConfiguration = overrideConfiguration != null ? overrideConfiguration : Configuration.EMPTY; mScale = scale; + mIsThemeable = isThemeable; int hash = 17; hash = 31 * hash + (mResDir == null ? 0 : mResDir.hashCode()); hash = 31 * hash + mDisplayId; hash = 31 * hash + mOverrideConfiguration.hashCode(); hash = 31 * hash + Float.floatToIntBits(mScale); + hash = 31 * hash + (mIsThemeable ? 1 : 0); mHash = hash; } @@ -74,7 +77,7 @@ public final class ResourcesKey { if (mScale != peer.mScale) { return false; } - return true; + return mIsThemeable == peer.mIsThemeable; } @Override diff --git a/core/java/android/content/res/ThemeConfig.java b/core/java/android/content/res/ThemeConfig.java new file mode 100644 index 0000000..1b1837d --- /dev/null +++ b/core/java/android/content/res/ThemeConfig.java @@ -0,0 +1,553 @@ +/* + * Copyright (C) 2014 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.provider.Settings; +import android.text.TextUtils; +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.HashMap; +import java.util.HashSet; +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(); + + // Maps pkgname to theme (ex com.angry.birds -> red theme) + protected final Map<String, AppTheme> mThemes = new HashMap<String, AppTheme>(); + + 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; + } + + 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 HashMap<String, AppTheme>() : mThemes; + Map<String, AppTheme> newThemes = (o.mThemes == null) ? + new HashMap<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(); + } + + 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) { + ThemeConfig bootTheme = mSystemConfig; + try { + String json = Settings.Secure.getString(resolver, + Configuration.THEME_PKG_CONFIGURATION_PERSISTENCE_PROPERTY); + bootTheme = ThemeConfig.fromJson(json); + + // Handle upgrade Case: Previously the theme configuration was in separate fields + if (bootTheme == null) { + String overlayPkgName = Settings.Secure.getString(resolver, + Configuration.THEME_PACKAGE_NAME_PERSISTENCE_PROPERTY); + String iconPackPkgName = Settings.Secure.getString(resolver, + Configuration.THEME_ICONPACK_PACKAGE_NAME_PERSISTENCE_PROPERTY); + String fontPkgName = Settings.Secure.getString(resolver, + Configuration.THEME_FONT_PACKAGE_NAME_PERSISTENCE_PROPERTY); + + Builder builder = new Builder(); + builder.defaultOverlay(overlayPkgName); + builder.defaultIcon(iconPackPkgName); + builder.defaultFont(fontPkgName); + bootTheme = builder.build(); + } + } catch (SecurityException e) { + Log.e(TAG, "Could not get boot theme", e); + } + 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(); + return JsonSerializer.fromJson(json); + } + + 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 : mIconPkgName.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 HashMap<String, String> mOverlays = new HashMap<String, String>(); + private HashMap<String, String> mIcons = new HashMap<String, String>(); + private HashMap<String, String> mFonts = new HashMap<String, String>(); + + 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() { + HashSet<String> appPkgSet = new HashSet<String>(); + appPkgSet.addAll(mOverlays.keySet()); + appPkgSet.addAll(mIcons.keySet()); + appPkgSet.addAll(mFonts.keySet()); + + HashMap<String, AppTheme> appThemes = new HashMap<String, AppTheme>(); + for(String appPkgName : appPkgSet) { + String icon = mIcons.get(appPkgName); + String overlay = mOverlays.get(appPkgName); + String font = mFonts.get(appPkgName); + + AppTheme appTheme = new AppTheme(overlay, icon, font); + appThemes.put(appPkgName, appTheme); + } + return new ThemeConfig(appThemes); + } + } + + + 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; + HashMap<String, AppTheme> map = new HashMap<String, AppTheme>(); + 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 HashMap<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 (Holo)"; + } + } +} diff --git a/core/java/android/content/res/ThemeManager.java b/core/java/android/content/res/ThemeManager.java new file mode 100644 index 0000000..a9d2fcc --- /dev/null +++ b/core/java/android/content/res/ThemeManager.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2014 The CyanogenMod 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.res; + +import android.content.Context; +import android.content.pm.ThemeUtils; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * {@hide} + */ +public class ThemeManager { + private static final String TAG = ThemeManager.class.getName(); + private Context mContext; + private IThemeService mService; + private Handler mHandler; + + private Set<ThemeChangeListener> mChangeListeners = + new HashSet<ThemeChangeListener>(); + + private Set<ThemeProcessingListener> mProcessingListeners = + new HashSet<ThemeProcessingListener>(); + + public ThemeManager(Context context, IThemeService service) { + mContext = context; + mService = service; + mHandler = new Handler(Looper.getMainLooper()); + } + + private final IThemeChangeListener mThemeChangeListener = new IThemeChangeListener.Stub() { + @Override + public void onProgress(final int progress) throws RemoteException { + mHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mChangeListeners) { + List<ThemeChangeListener> listenersToRemove = new ArrayList + <ThemeChangeListener>(); + for (ThemeChangeListener listener : mChangeListeners) { + try { + listener.onProgress(progress); + } catch (Throwable e) { + Log.w(TAG, "Unable to update theme change progress", e); + listenersToRemove.add(listener); + } + } + if (listenersToRemove.size() > 0) { + for (ThemeChangeListener listener : listenersToRemove) { + mChangeListeners.remove(listener); + } + } + } + } + }); + } + + @Override + public void onFinish(final boolean isSuccess) throws RemoteException { + mHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mChangeListeners) { + List<ThemeChangeListener> listenersToRemove = new ArrayList + <ThemeChangeListener>(); + for (ThemeChangeListener listener : mChangeListeners) { + try { + listener.onFinish(isSuccess); + } catch (Throwable e) { + Log.w(TAG, "Unable to update theme change listener", e); + listenersToRemove.add(listener); + } + } + if (listenersToRemove.size() > 0) { + for (ThemeChangeListener listener : listenersToRemove) { + mChangeListeners.remove(listener); + } + } + } + } + }); + } + }; + + private final IThemeProcessingListener mThemeProcessingListener = + new IThemeProcessingListener.Stub() { + @Override + public void onFinishedProcessing(final String pkgName) throws RemoteException { + mHandler.post(new Runnable() { + @Override + public void run() { + synchronized (mProcessingListeners) { + List<ThemeProcessingListener> listenersToRemove = new ArrayList + <ThemeProcessingListener>(); + for (ThemeProcessingListener listener : mProcessingListeners) { + try { + listener.onFinishedProcessing(pkgName); + } catch (Throwable e) { + Log.w(TAG, "Unable to update theme change progress", e); + listenersToRemove.add(listener); + } + } + if (listenersToRemove.size() > 0) { + for (ThemeProcessingListener listener : listenersToRemove) { + mProcessingListeners.remove(listener); + } + } + } + } + }); + } + }; + + + public void addClient(ThemeChangeListener listener) { + synchronized (mChangeListeners) { + if (mChangeListeners.contains(listener)) { + throw new IllegalArgumentException("Client was already added "); + } + if (mChangeListeners.size() == 0) { + try { + mService.requestThemeChangeUpdates(mThemeChangeListener); + } catch (RemoteException e) { + Log.w(TAG, "Unable to register listener", e); + } + } + mChangeListeners.add(listener); + } + } + + public void removeClient(ThemeChangeListener listener) { + synchronized (mChangeListeners) { + mChangeListeners.remove(listener); + if (mChangeListeners.size() == 0) { + try { + mService.removeUpdates(mThemeChangeListener); + } catch (RemoteException e) { + Log.w(TAG, "Unable to remove listener", e); + } + } + } + } + + public void onClientPaused(ThemeChangeListener listener) { + removeClient(listener); + } + + public void onClientResumed(ThemeChangeListener listener) { + addClient(listener); + } + + public void onClientDestroyed(ThemeChangeListener listener) { + removeClient(listener); + } + + /** + * Register a ThemeProcessingListener to be notified when a theme is done being processed. + * @param listener ThemeChangeListener to register + */ + public void registerProcessingListener(ThemeProcessingListener listener) { + synchronized (mProcessingListeners) { + if (mProcessingListeners.contains(listener)) { + throw new IllegalArgumentException("Listener was already added "); + } + if (mProcessingListeners.size() == 0) { + try { + mService.registerThemeProcessingListener(mThemeProcessingListener); + } catch (RemoteException e) { + Log.w(TAG, "Unable to register listener", e); + } + } + mProcessingListeners.add(listener); + } + } + + /** + * Unregister a ThemeChangeListener. + * @param listener ThemeChangeListener to unregister + */ + public void unregisterProcessingListener(ThemeChangeListener listener) { + synchronized (mProcessingListeners) { + mProcessingListeners.remove(listener); + if (mProcessingListeners.size() == 0) { + try { + mService.unregisterThemeProcessingListener(mThemeProcessingListener); + } catch (RemoteException e) { + Log.w(TAG, "Unable to remove listener", e); + } + } + } + } + + /** + * Convenience method. Applies the entire theme. + */ + public void requestThemeChange(String pkgName) { + //List<String> components = ThemeUtils.getSupportedComponents(mContext, pkgName); + //requestThemeChange(pkgName, components); + } + + public void requestThemeChange(String pkgName, List<String> components) { + Map<String, String> componentMap = new HashMap<String, String>(components.size()); + for (String component : components) { + componentMap.put(component, pkgName); + } + requestThemeChange(componentMap); + } + + public void requestThemeChange(Map<String, String> componentMap) { + try { + mService.requestThemeChange(componentMap); + } catch (RemoteException e) { + logThemeServiceException(e); + } + } + + public void applyDefaultTheme() { + try { + mService.applyDefaultTheme(); + } catch (RemoteException e) { + logThemeServiceException(e); + } + } + + public boolean isThemeApplying() { + try { + return mService.isThemeApplying(); + } catch (RemoteException e) { + logThemeServiceException(e); + } + + return false; + } + + public boolean isThemeBeingProcessed(String themePkgName) { + try { + return mService.isThemeBeingProcessed(themePkgName); + } catch (RemoteException e) { + logThemeServiceException(e); + } + return false; + } + + public int getProgress() { + try { + return mService.getProgress(); + } catch (RemoteException e) { + logThemeServiceException(e); + } + return -1; + } + + public boolean processThemeResources(String themePkgName) { + try { + return mService.processThemeResources(themePkgName); + } catch (RemoteException e) { + logThemeServiceException(e); + } + return false; + } + + private void logThemeServiceException(Exception e) { + Log.w(TAG, "Unable to access ThemeService", e); + } + + public interface ThemeChangeListener { + void onProgress(int progress); + void onFinish(boolean isSuccess); + } + + public interface ThemeProcessingListener { + void onFinishedProcessing(String pkgName); + } +} + |