diff options
Diffstat (limited to 'core')
53 files changed, 5749 insertions, 69 deletions
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 37e8aa4939c..01ea22918d0 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -38,6 +38,7 @@ import android.content.pm.ConfigurationInfo; import android.content.pm.IPackageDataObserver; import android.content.pm.PackageManager; import android.content.pm.UserInfo; +import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Color; @@ -56,6 +57,7 @@ import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Size; import android.util.Slog; + import org.xmlpull.v1.XmlSerializer; import java.io.FileDescriptor; @@ -2206,6 +2208,16 @@ public class ActivityManager { return null; } } + /** + * @hide + */ + public Configuration getConfiguration() { + try { + return ActivityManagerNative.getDefault().getConfiguration(); + } catch (RemoteException e) { + return null; + } + } /** * Returns a list of application processes that are running on the device. @@ -2786,4 +2798,17 @@ public class ActivityManager { } } } + + /** + * @throws SecurityException Throws SecurityException if the caller does + * not hold the {@link android.Manifest.permission#CHANGE_CONFIGURATION} permission. + * + * @hide + */ + public void updateConfiguration(Configuration values) throws SecurityException { + try { + ActivityManagerNative.getDefault().updateConfiguration(values); + } catch (RemoteException e) { + } + } } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index dd49009281f..3ee7621ec62 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -43,6 +43,7 @@ import android.database.sqlite.SQLiteDebug; import android.database.sqlite.SQLiteDebug.DbStats; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Typeface; import android.hardware.display.DisplayManagerGlobal; import android.net.IConnectivityManager; import android.net.Proxy; @@ -71,6 +72,7 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; +import android.text.TextUtils; import android.util.AndroidRuntimeException; import android.util.ArrayMap; import android.util.DisplayMetrics; @@ -83,6 +85,7 @@ import android.util.Slog; import android.util.SuperNotCalledException; import android.view.Display; import android.view.HardwareRenderer; +import android.view.InflateException; import android.view.View; import android.view.ViewDebug; import android.view.ViewManager; @@ -1621,9 +1624,19 @@ public final class ActivityThread { */ Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, - LoadedApk pkgInfo) { + LoadedApk pkgInfo, Context context, String pkgName) { return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs, - displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo(), null); + displayId, pkgName, overrideConfiguration, pkgInfo.getCompatibilityInfo(), null, + context); + } + + /** + * Creates the top level resources for the given package. + */ + Resources getTopLevelThemedResources(String resDir, int displayId, LoadedApk pkgInfo, + String pkgName, String themePkgName) { + return mResourcesManager.getTopLevelThemedResources(resDir, displayId, pkgName, + themePkgName, pkgInfo.getCompatibilityInfo(), null); } final Handler getHandler() { @@ -1731,8 +1744,7 @@ public final class ActivityThread { ref = mResourcePackages.get(aInfo.packageName); } LoadedApk packageInfo = ref != null ? ref.get() : null; - if (packageInfo == null || (packageInfo.mResources != null - && !packageInfo.mResources.getAssets().isUpToDate())) { + if (packageInfo == null) { if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package " : "Loading resource-only package ") + aInfo.packageName + " (in " + (mBoundApplication != null @@ -1750,6 +1762,10 @@ public final class ActivityThread { new WeakReference<LoadedApk>(packageInfo)); } } + if (packageInfo.mResources != null + && !packageInfo.mResources.getAssets().isUpToDate()) { + packageInfo.mResources = null; + } return packageInfo; } } @@ -4088,8 +4104,10 @@ public final class ActivityThread { if (configDiff != 0) { // Ask text layout engine to free its caches if there is a locale change boolean hasLocaleConfigChange = ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0); - if (hasLocaleConfigChange) { + boolean hasThemeConfigChange = ((configDiff & ActivityInfo.CONFIG_THEME_RESOURCE) != 0); + if (hasLocaleConfigChange || hasThemeConfigChange) { Canvas.freeTextLayoutCaches(); + Typeface.recreateDefaults(); if (DEBUG_CONFIGURATION) Slog.v(TAG, "Cleared TextLayout Caches"); } } diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 93cc521e3dc..05f915c153f 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -18,6 +18,7 @@ package android.app; import android.content.ComponentName; import android.content.ContentResolver; +import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; @@ -925,12 +926,13 @@ final class ApplicationPackageManager extends PackageManager { if (app.packageName.equals("system")) { return mContext.mMainThread.getSystemContext().getResources(); } + final boolean sameUid = (app.uid == Process.myUid()); Resources r = mContext.mMainThread.getTopLevelResources( sameUid ? app.sourceDir : app.publicSourceDir, sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs, app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY, - null, mContext.mPackageInfo); + null, mContext.mPackageInfo, mContext, app.packageName); if (r != null) { return r; } @@ -965,6 +967,48 @@ final class ApplicationPackageManager extends PackageManager { throw new NameNotFoundException("Package " + appPackageName + " doesn't exist"); } + /** @hide */ + @Override public Resources getThemedResourcesForApplication( + ApplicationInfo app, String themePkgName) throws NameNotFoundException { + if (app.packageName.equals("system")) { + return mContext.mMainThread.getSystemContext().getResources(); + } + + Resources r = mContext.mMainThread.getTopLevelThemedResources( + app.uid == Process.myUid() ? app.sourceDir : app.publicSourceDir, + Display.DEFAULT_DISPLAY, mContext.mPackageInfo, app.packageName, themePkgName); + if (r != null) { + return r; + } + throw new NameNotFoundException("Unable to open " + app.publicSourceDir); + } + + /** @hide */ + @Override public Resources getThemedResourcesForApplication( + String appPackageName, String themePkgName) throws NameNotFoundException { + return getThemedResourcesForApplication( + getApplicationInfo(appPackageName, 0), themePkgName); + } + + /** @hide */ + @Override + public Resources getThemedResourcesForApplicationAsUser(String appPackageName, + String themePackageName, int userId) throws NameNotFoundException { + if (userId < 0) { + throw new IllegalArgumentException( + "Call does not support special user #" + userId); + } + try { + ApplicationInfo ai = mPM.getApplicationInfo(appPackageName, 0, userId); + if (ai != null) { + return getThemedResourcesForApplication(ai, themePackageName); + } + } catch (RemoteException e) { + throw new RuntimeException("Package manager has died", e); + } + throw new NameNotFoundException("Package " + appPackageName + " doesn't exist"); + } + int mCachedSafeMode = -1; @Override public boolean isSafeMode() { try { @@ -1770,4 +1814,30 @@ final class ApplicationPackageManager extends PackageManager { = new ArrayMap<ResourceName, WeakReference<Drawable.ConstantState>>(); private static ArrayMap<ResourceName, WeakReference<CharSequence>> sStringCache = new ArrayMap<ResourceName, WeakReference<CharSequence>>(); + + /** + * @hide + */ + @Override + public void updateIconMaps(String pkgName) { + try { + mPM.updateIconMapping(pkgName); + } catch (RemoteException re) { + Log.e(TAG, "Failed to update icon maps", re); + } + } + + /** + * @hide + */ + @Override + public int processThemeResources(String themePkgName) { + try { + return mPM.processThemeResources(themePkgName); + } catch (RemoteException e) { + Log.e(TAG, "Unable to process theme resources for " + themePkgName, e); + } + + return 0; + } } diff --git a/core/java/android/app/ComposedIconInfo.aidl b/core/java/android/app/ComposedIconInfo.aidl new file mode 100644 index 00000000000..8a1bab52d8c --- /dev/null +++ b/core/java/android/app/ComposedIconInfo.aidl @@ -0,0 +1,19 @@ +/* + * 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.app; + +/** @hide */ +parcelable ComposedIconInfo; diff --git a/core/java/android/app/ComposedIconInfo.java b/core/java/android/app/ComposedIconInfo.java new file mode 100644 index 00000000000..7fab85241b1 --- /dev/null +++ b/core/java/android/app/ComposedIconInfo.java @@ -0,0 +1,96 @@ +/* + * 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.app; + +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +public class ComposedIconInfo implements Parcelable { + public int iconUpon, iconMask; + public int[] iconBacks; + public float iconScale; + public int iconDensity; + public int iconSize; + public float[] colorFilter; + + public ComposedIconInfo() { + super(); + } + + private ComposedIconInfo(Parcel source) { + iconScale = source.readFloat(); + iconDensity = source.readInt(); + iconSize = source.readInt(); + int backCount = source.readInt(); + if (backCount > 0) { + iconBacks = new int[backCount]; + for (int i = 0; i < backCount; i++) { + iconBacks[i] = source.readInt(); + } + } + iconMask = source.readInt(); + iconUpon = source.readInt(); + int colorFilterSize = source.readInt(); + if (colorFilterSize > 0) { + colorFilter = new float[colorFilterSize]; + for (int i = 0; i < colorFilterSize; i++) { + colorFilter[i] = source.readFloat(); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeFloat(iconScale); + dest.writeInt(iconDensity); + dest.writeInt(iconSize); + dest.writeInt(iconBacks != null ? iconBacks.length : 0); + if (iconBacks != null) { + for (int resId : iconBacks) { + dest.writeInt(resId); + } + } + dest.writeInt(iconMask); + dest.writeInt(iconUpon); + if (colorFilter != null) { + dest.writeInt(colorFilter.length); + for (float val : colorFilter) { + dest.writeFloat(val); + } + } else { + dest.writeInt(0); + } + } + + public static final Creator<ComposedIconInfo> CREATOR + = new Creator<ComposedIconInfo>() { + @Override + public ComposedIconInfo createFromParcel(Parcel source) { + return new ComposedIconInfo(source); + } + + @Override + public ComposedIconInfo[] newArray(int size) { + return new ComposedIconInfo[0]; + } + }; +} diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index d607f81adec..3539b7ffe7c 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -19,6 +19,8 @@ package android.app; import android.app.usage.IUsageStatsManager; import android.app.usage.UsageStatsManager; import android.appwidget.AppWidgetManager; +import android.content.res.IThemeService; +import android.content.res.ThemeManager; import android.os.Build; import android.service.persistentdata.IPersistentDataBlockService; @@ -769,6 +771,14 @@ class ContextImpl extends Context { return new ProfileManager (outerContext, ctx.mMainThread.getHandler()); } }); + + registerService(THEME_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(THEME_SERVICE); + IThemeService service = IThemeService.Stub.asInterface(b); + return new ThemeManager(ctx.getOuterContext(), + service); + }}); } static ContextImpl getImpl(Context context) { @@ -2100,13 +2110,19 @@ class ContextImpl extends Context { @Override public Context createApplicationContext(ApplicationInfo application, int flags) throws NameNotFoundException { + return createApplicationContext(application, null, flags); + } + + @Override + public Context createApplicationContext(ApplicationInfo application, String themePackageName, + int flags) throws NameNotFoundException { LoadedApk pi = mMainThread.getPackageInfo(application, mResources.getCompatibilityInfo(), flags | CONTEXT_REGISTER_PACKAGE); if (pi != null) { final boolean restricted = (flags & CONTEXT_RESTRICTED) == CONTEXT_RESTRICTED; ContextImpl c = new ContextImpl(this, mMainThread, pi, mActivityToken, new UserHandle(UserHandle.getUserId(application.uid)), restricted, - mDisplay, mOverrideConfiguration); + mDisplay, mOverrideConfiguration, themePackageName); if (c.mResources != null) { return c; } @@ -2119,24 +2135,30 @@ class ContextImpl extends Context { @Override public Context createPackageContext(String packageName, int flags) throws NameNotFoundException { - return createPackageContextAsUser(packageName, flags, + return createPackageContextAsUser(packageName, null, flags, mUser != null ? mUser : Process.myUserHandle()); } @Override public Context createPackageContextAsUser(String packageName, int flags, UserHandle user) throws NameNotFoundException { + return createPackageContextAsUser(packageName, null, flags, user); + } + + @Override + public Context createPackageContextAsUser(String packageName, String themePackageName, + int flags, UserHandle user) throws NameNotFoundException { final boolean restricted = (flags & CONTEXT_RESTRICTED) == CONTEXT_RESTRICTED; if (packageName.equals("system") || packageName.equals("android")) { return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken, - user, restricted, mDisplay, mOverrideConfiguration); + user, restricted, mDisplay, mOverrideConfiguration, themePackageName); } LoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(), flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier()); if (pi != null) { ContextImpl c = new ContextImpl(this, mMainThread, pi, mActivityToken, - user, restricted, mDisplay, mOverrideConfiguration); + user, restricted, mDisplay, mOverrideConfiguration, themePackageName); if (c.mResources != null) { return c; } @@ -2208,7 +2230,7 @@ class ContextImpl extends Context { static ContextImpl createSystemContext(ActivityThread mainThread) { LoadedApk packageInfo = new LoadedApk(mainThread); ContextImpl context = new ContextImpl(null, mainThread, - packageInfo, null, null, false, null, null); + packageInfo, null, null, false, null, null, null); context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(), context.mResourcesManager.getDisplayMetricsLocked(Display.DEFAULT_DISPLAY)); return context; @@ -2217,7 +2239,7 @@ class ContextImpl extends Context { static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) { if (packageInfo == null) throw new IllegalArgumentException("packageInfo"); return new ContextImpl(null, mainThread, - packageInfo, null, null, false, null, null); + packageInfo, null, null, false, null, null, null); } static ContextImpl createActivityContext(ActivityThread mainThread, @@ -2225,12 +2247,19 @@ class ContextImpl extends Context { if (packageInfo == null) throw new IllegalArgumentException("packageInfo"); if (activityToken == null) throw new IllegalArgumentException("activityInfo"); return new ContextImpl(null, mainThread, - packageInfo, activityToken, null, false, null, null); + packageInfo, activityToken, null, false, null, null, null); } private ContextImpl(ContextImpl container, ActivityThread mainThread, LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted, Display display, Configuration overrideConfiguration) { + this(container, mainThread, packageInfo, activityToken, user, restricted, display, + overrideConfiguration, null); + } + + private ContextImpl(ContextImpl container, ActivityThread mainThread, + LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted, + Display display, Configuration overrideConfiguration, String themePackageName) { mOuterContext = this; mMainThread = mainThread; @@ -2260,15 +2289,19 @@ class ContextImpl extends Context { Resources resources = packageInfo.getResources(mainThread); if (resources != null) { - if (activityToken != null + if (activityToken != null || themePackageName != null || displayId != Display.DEFAULT_DISPLAY || overrideConfiguration != null || (compatInfo != null && compatInfo.applicationScale != resources.getCompatibilityInfo().applicationScale)) { - resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(), - packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(), + resources = themePackageName == null ? mResourcesManager.getTopLevelResources( + packageInfo.getResDir(), packageInfo.getSplitResDirs(), + packageInfo.getOverlayDirs(), packageInfo.getApplicationInfo().sharedLibraryFiles, displayId, - overrideConfiguration, compatInfo, activityToken); + packageInfo.getAppDir(), overrideConfiguration, compatInfo, activityToken, + mOuterContext) : + mResourcesManager.getTopLevelThemedResources(packageInfo.getResDir(), displayId, + packageInfo.getPackageName(), themePackageName, compatInfo ,activityToken); } } mResources = resources; diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl index 3b5900b4a5d..3117c2d4388 100644 --- a/core/java/android/app/IWallpaperManager.aidl +++ b/core/java/android/app/IWallpaperManager.aidl @@ -30,6 +30,12 @@ interface IWallpaperManager { * Set the wallpaper. */ ParcelFileDescriptor setWallpaper(String name); + + /** + * Set the keyguard wallpaper. + * @hide + */ + ParcelFileDescriptor setKeyguardWallpaper(String name); /** * Set the live wallpaper. @@ -41,6 +47,13 @@ interface IWallpaperManager { */ ParcelFileDescriptor getWallpaper(IWallpaperManagerCallback cb, out Bundle outParams); + + /** + * Get the keyguard wallpaper. + * @hide + */ + ParcelFileDescriptor getKeyguardWallpaper(IWallpaperManagerCallback cb, + out Bundle outParams); /** * Get information about a live wallpaper. @@ -52,6 +65,12 @@ interface IWallpaperManager { */ void clearWallpaper(); + /* + * Clear the keyguard wallpaper. + * @hide + */ + void clearKeyguardWallpaper(); + /** * Return whether there is a wallpaper set with the given name. */ diff --git a/core/java/android/app/IWallpaperManagerCallback.aidl b/core/java/android/app/IWallpaperManagerCallback.aidl index 991b2bc924b..b217318291d 100644 --- a/core/java/android/app/IWallpaperManagerCallback.aidl +++ b/core/java/android/app/IWallpaperManagerCallback.aidl @@ -28,4 +28,9 @@ oneway interface IWallpaperManagerCallback { * Called when the wallpaper has changed */ void onWallpaperChanged(); + + /** + * Called when the keygaurd wallpaper has changed + */ + void onKeyguardWallpaperChanged(); } diff --git a/core/java/android/app/IconPackHelper.java b/core/java/android/app/IconPackHelper.java new file mode 100644 index 00000000000..057633f89ee --- /dev/null +++ b/core/java/android/app/IconPackHelper.java @@ -0,0 +1,848 @@ +/* + * 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.app; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import android.content.pm.PackageInfo; +import android.content.res.IThemeService; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.PaintDrawable; +import android.graphics.drawable.VectorDrawable; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; +import android.util.TypedValue; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ThemeUtils; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.DisplayMetrics; + +/** @hide */ +public class IconPackHelper { + private static final String TAG = IconPackHelper.class.getSimpleName(); + private static final String ICON_MASK_TAG = "iconmask"; + private static final String ICON_BACK_TAG = "iconback"; + private static final String ICON_UPON_TAG = "iconupon"; + private static final String ICON_SCALE_TAG = "scale"; + private static final String ICON_BACK_FORMAT = "iconback%d"; + + private static final ComponentName ICON_BACK_COMPONENT; + private static final ComponentName ICON_MASK_COMPONENT; + private static final ComponentName ICON_UPON_COMPONENT; + private static final ComponentName ICON_SCALE_COMPONENT; + + private static final float DEFAULT_SCALE = 1.0f; + private static final int COMPOSED_ICON_COOKIE = 128; + + private final Context mContext; + private Map<ComponentName, String> mIconPackResourceMap; + private String mLoadedIconPackName; + private Resources mLoadedIconPackResource; + private ComposedIconInfo mComposedIconInfo; + private int mIconBackCount = 0; + private ColorFilterUtils.Builder mFilterBuilder; + + static { + ICON_BACK_COMPONENT = new ComponentName(ICON_BACK_TAG, ""); + ICON_MASK_COMPONENT = new ComponentName(ICON_MASK_TAG, ""); + ICON_UPON_COMPONENT = new ComponentName(ICON_UPON_TAG, ""); + ICON_SCALE_COMPONENT = new ComponentName(ICON_SCALE_TAG, ""); + } + + public IconPackHelper(Context context) { + mContext = context; + mIconPackResourceMap = new HashMap<ComponentName, String>(); + ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + mComposedIconInfo = new ComposedIconInfo(); + mComposedIconInfo.iconSize = am.getLauncherLargeIconSize(); + mComposedIconInfo.iconDensity = am.getLauncherLargeIconDensity(); + mFilterBuilder = new ColorFilterUtils.Builder(); + } + + private void loadResourcesFromXmlParser(XmlPullParser parser, + Map<ComponentName, String> iconPackResources) + throws XmlPullParserException, IOException { + mIconBackCount = 0; + int eventType = parser.getEventType(); + do { + + if (eventType != XmlPullParser.START_TAG) { + continue; + } + + if (parseComposedIconComponent(parser, iconPackResources)) { + continue; + } + + if (ColorFilterUtils.parseIconFilter(parser, mFilterBuilder)) { + continue; + } + + if (parser.getName().equalsIgnoreCase(ICON_SCALE_TAG)) { + String factor = parser.getAttributeValue(null, "factor"); + if (factor == null) { + if (parser.getAttributeCount() == 1) { + factor = parser.getAttributeValue(0); + } + } + iconPackResources.put(ICON_SCALE_COMPONENT, factor); + continue; + } + + if (!parser.getName().equalsIgnoreCase("item")) { + continue; + } + + String component = parser.getAttributeValue(null, "component"); + String drawable = parser.getAttributeValue(null, "drawable"); + + // Validate component/drawable exist + if (TextUtils.isEmpty(component) || TextUtils.isEmpty(drawable)) { + continue; + } + + // Validate format/length of component + if (!component.startsWith("ComponentInfo{") || !component.endsWith("}") + || component.length() < 16 || drawable.length() == 0) { + continue; + } + + // Sanitize stored value + component = component.substring(14, component.length() - 1).toLowerCase(); + + ComponentName name = null; + if (!component.contains("/")) { + // Package icon reference + name = new ComponentName(component.toLowerCase(), ""); + } else { + name = ComponentName.unflattenFromString(component); + } + + if (name != null) { + iconPackResources.put(name, drawable); + } + } while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT); + } + + private boolean isComposedIconComponent(String tag) { + return tag.equalsIgnoreCase(ICON_MASK_TAG) || + tag.equalsIgnoreCase(ICON_BACK_TAG) || + tag.equalsIgnoreCase(ICON_UPON_TAG); + } + + private boolean parseComposedIconComponent(XmlPullParser parser, + Map<ComponentName, String> iconPackResources) { + String icon; + String tag = parser.getName(); + if (!isComposedIconComponent(tag)) { + return false; + } + + if (parser.getAttributeCount() >= 1) { + if (tag.equalsIgnoreCase(ICON_BACK_TAG)) { + mIconBackCount = parser.getAttributeCount(); + for (int i = 0; i < mIconBackCount; i++) { + tag = String.format(ICON_BACK_FORMAT, i); + icon = parser.getAttributeValue(i); + iconPackResources.put(new ComponentName(tag, ""), icon); + } + } else { + icon = parser.getAttributeValue(0); + iconPackResources.put(new ComponentName(tag, ""), + icon); + } + return true; + } + + return false; + } + + public void loadIconPack(String packageName) throws NameNotFoundException { + if (packageName == null) { + mLoadedIconPackResource = null; + mLoadedIconPackName = null; + mComposedIconInfo.iconBacks = null; + mComposedIconInfo.iconMask = mComposedIconInfo.iconUpon = 0; + mComposedIconInfo.iconScale = 0; + mComposedIconInfo.colorFilter = null; + } else { + mIconBackCount = 0; + Resources res = createIconResource(mContext, packageName); + mIconPackResourceMap = getIconResMapFromXml(res, packageName); + mLoadedIconPackResource = res; + mLoadedIconPackName = packageName; + loadComposedIconComponents(); + ColorMatrix cm = mFilterBuilder.build(); + if (cm != null) { + mComposedIconInfo.colorFilter = cm.getArray().clone(); + } + } + } + + public ComposedIconInfo getComposedIconInfo() { + return mComposedIconInfo; + } + + private void loadComposedIconComponents() { + mComposedIconInfo.iconMask = getResourceIdForName(ICON_MASK_COMPONENT); + mComposedIconInfo.iconUpon = getResourceIdForName(ICON_UPON_COMPONENT); + + // Take care of loading iconback which can have multiple images + if (mIconBackCount > 0) { + mComposedIconInfo.iconBacks = new int[mIconBackCount]; + for (int i = 0; i < mIconBackCount; i++) { + mComposedIconInfo.iconBacks[i] = + getResourceIdForName( + new ComponentName(String.format(ICON_BACK_FORMAT, i), "")); + } + } + + // Get the icon scale from this pack + String scale = mIconPackResourceMap.get(ICON_SCALE_COMPONENT); + if (scale != null) { + try { + mComposedIconInfo.iconScale = Float.valueOf(scale); + } catch (NumberFormatException e) { + mComposedIconInfo.iconScale = DEFAULT_SCALE; + } + } else { + mComposedIconInfo.iconScale = DEFAULT_SCALE; + } + } + + private int getResourceIdForName(ComponentName component) { + String item = mIconPackResourceMap.get(component); + if (!TextUtils.isEmpty(item)) { + return getResourceIdForDrawable(item); + } + return 0; + } + + public static Resources createIconResource(Context context, String packageName) + throws NameNotFoundException { + PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); + String themeApk = info.applicationInfo.publicSourceDir; + + String prefixPath; + String iconApkPath; + String iconResPath; + if (info.isLegacyIconPackApk) { + iconResPath = ""; + iconApkPath = ""; + prefixPath = ""; + } else { + prefixPath = ThemeUtils.ICONS_PATH; //path inside APK + iconApkPath = ThemeUtils.getIconPackApkPath(packageName); + iconResPath = ThemeUtils.getIconPackResPath(packageName); + } + + AssetManager assets = new AssetManager(); + assets.addIconPath(themeApk, iconApkPath, + prefixPath, Resources.THEME_ICON_PKG_ID); + + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + Configuration config = context.getResources().getConfiguration(); + Resources res = new Resources(assets, dm, config); + return res; + } + + public Map<ComponentName, String> getIconResMapFromXml(Resources res, String packageName) { + XmlPullParser parser = null; + InputStream inputStream = null; + Map<ComponentName, String> iconPackResources = new HashMap<ComponentName, String>(); + + try { + inputStream = res.getAssets().open("appfilter.xml"); + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + parser = factory.newPullParser(); + parser.setInput(inputStream, "UTF-8"); + } catch (Exception e) { + // Catch any exception since we want to fall back to parsing the xml/ + // resource in all cases + int resId = res.getIdentifier("appfilter", "xml", packageName); + if (resId != 0) { + parser = res.getXml(resId); + } + } + + if (parser != null) { + try { + loadResourcesFromXmlParser(parser, iconPackResources); + return iconPackResources; + } catch (XmlPullParserException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + // Cleanup resources + if (parser instanceof XmlResourceParser) { + ((XmlResourceParser) parser).close(); + } else if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + } + } + } + } + + // Application uses a different theme format (most likely launcher pro) + int arrayId = res.getIdentifier("theme_iconpack", "array", packageName); + if (arrayId == 0) { + arrayId = res.getIdentifier("icon_pack", "array", packageName); + } + + if (arrayId != 0) { + String[] iconPack = res.getStringArray(arrayId); + ComponentName compName = null; + for (String entry : iconPack) { + + if (TextUtils.isEmpty(entry)) { + continue; + } + + String icon = entry; + entry = entry.replaceAll("_", "."); + + compName = new ComponentName(entry.toLowerCase(), ""); + iconPackResources.put(compName, icon); + + int activityIndex = entry.lastIndexOf("."); + if (activityIndex <= 0 || activityIndex == entry.length() - 1) { + continue; + } + + String iconPackage = entry.substring(0, activityIndex); + if (TextUtils.isEmpty(iconPackage)) { + continue; + } + + String iconActivity = entry.substring(activityIndex + 1); + if (TextUtils.isEmpty(iconActivity)) { + continue; + } + + // Store entries as lower case to ensure match + iconPackage = iconPackage.toLowerCase(); + iconActivity = iconActivity.toLowerCase(); + + iconActivity = iconPackage + "." + iconActivity; + compName = new ComponentName(iconPackage, iconActivity); + iconPackResources.put(compName, icon); + } + } + return iconPackResources; + } + + boolean isIconPackLoaded() { + return mLoadedIconPackResource != null && + mLoadedIconPackName != null && + mIconPackResourceMap != null; + } + + private int getResourceIdForDrawable(String resource) { + int resId = + mLoadedIconPackResource.getIdentifier(resource, "drawable",mLoadedIconPackName); + return resId; + } + + public int getResourceIdForActivityIcon(ActivityInfo info) { + if (!isIconPackLoaded()) { + return 0; + } + ComponentName compName = new ComponentName(info.packageName.toLowerCase(), + info.name.toLowerCase()); + String drawable = mIconPackResourceMap.get(compName); + if (drawable != null) { + int resId = getResourceIdForDrawable(drawable); + if (resId != 0) return resId; + } + + // Icon pack doesn't have an icon for the activity, fallback to package icon + compName = new ComponentName(info.packageName.toLowerCase(), ""); + drawable = mIconPackResourceMap.get(compName); + if (drawable == null) { + return 0; + } + return getResourceIdForDrawable(drawable); + } + + public int getResourceIdForApp(String pkgName) { + ActivityInfo info = new ActivityInfo(); + info.packageName = pkgName; + info.name = ""; + return getResourceIdForActivityIcon(info); + } + + public Drawable getDrawableForActivity(ActivityInfo info) { + int id = getResourceIdForActivityIcon(info); + if (id == 0) return null; + return mLoadedIconPackResource.getDrawable(id, null, false); + } + + public Drawable getDrawableForActivityWithDensity(ActivityInfo info, int density) { + int id = getResourceIdForActivityIcon(info); + if (id == 0) return null; + return mLoadedIconPackResource.getDrawableForDensity(id, density, null, false); + } + + public static boolean shouldComposeIcon(ComposedIconInfo iconInfo) { + return iconInfo != null && + (iconInfo.iconBacks != null || iconInfo.iconMask != 0 || + iconInfo.iconUpon != 0 || iconInfo.colorFilter != null); + } + + public static class IconCustomizer { + private static final Random sRandom = new Random(); + private static final IThemeService sThemeService; + + static { + sThemeService = IThemeService.Stub.asInterface( + ServiceManager.getService(Context.THEME_SERVICE)); + } + + public static Drawable getComposedIconDrawable(Drawable icon, Context context, + ComposedIconInfo iconInfo) { + final Resources res = context.getResources(); + return getComposedIconDrawable(icon, res, iconInfo); + } + + public static Drawable getComposedIconDrawable(Drawable icon, Resources res, + ComposedIconInfo iconInfo) { + if (iconInfo == null) return icon; + int back = 0; + if (iconInfo.iconBacks != null && iconInfo.iconBacks.length > 0) { + back = iconInfo.iconBacks[sRandom.nextInt(iconInfo.iconBacks.length)]; + } + Bitmap bmp = createIconBitmap(icon, res, back, iconInfo.iconMask, iconInfo.iconUpon, + iconInfo.iconScale, iconInfo.iconSize, iconInfo.colorFilter); + return bmp != null ? new BitmapDrawable(res, bmp): null; + } + + public static void getValue(Resources res, int resId, TypedValue outValue, + Drawable baseIcon) { + final String pkgName = res.getAssets().getAppName(); + TypedValue tempValue = new TypedValue(); + tempValue.setTo(outValue); + outValue.assetCookie = COMPOSED_ICON_COOKIE; + outValue.data = resId & (COMPOSED_ICON_COOKIE << 24 | 0x00ffffff); + outValue.string = getCachedIconPath(pkgName, resId, outValue.density); + + if (!(new File(outValue.string.toString()).exists())) { + // compose the icon and cache it + final ComposedIconInfo iconInfo = res.getComposedIconInfo(); + int back = 0; + if (iconInfo.iconBacks != null && iconInfo.iconBacks.length > 0) { + back = iconInfo.iconBacks[(outValue.string.hashCode() & 0x7fffffff) + % iconInfo.iconBacks.length]; + } + Bitmap bmp = createIconBitmap(baseIcon, res, back, iconInfo.iconMask, + iconInfo.iconUpon, iconInfo.iconScale, iconInfo.iconSize, + iconInfo.colorFilter); + if (!cacheComposedIcon(bmp, getCachedIconName(pkgName, resId, outValue.density))) { + Log.w(TAG, "Unable to cache icon " + outValue.string); + // restore the original TypedValue + outValue.setTo(tempValue); + } + } + } + + private static Bitmap createIconBitmap(Drawable icon, Resources res, int iconBack, + int iconMask, int iconUpon, float scale, int iconSize, float[] colorFilter) { + if (iconSize <= 0) return null; + + final Canvas canvas = new Canvas(); + canvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.ANTI_ALIAS_FLAG, + Paint.FILTER_BITMAP_FLAG)); + + int width = 0, height = 0; + if (icon instanceof PaintDrawable) { + PaintDrawable painter = (PaintDrawable) icon; + painter.setIntrinsicWidth(iconSize); + painter.setIntrinsicHeight(iconSize); + + // A PaintDrawable does not have an exact size + width = iconSize; + height = iconSize; + } else if (icon instanceof BitmapDrawable) { + // Ensure the bitmap has a density. + BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; + Bitmap bitmap = bitmapDrawable.getBitmap(); + if (bitmap.getDensity() == Bitmap.DENSITY_NONE) { + bitmapDrawable.setTargetDensity(res.getDisplayMetrics()); + } + canvas.setDensity(bitmap.getDensity()); + + // If the original size of the icon isn't greater + // than twice the size of recommended large icons + // respect the original size of the icon + // otherwise enormous icons can easily create + // OOM situations. + if ((bitmap.getWidth() < (iconSize * 2)) + && (bitmap.getHeight() < (iconSize * 2))) { + width = bitmap.getWidth(); + height = bitmap.getHeight(); + } else { + width = iconSize; + height = iconSize; + } + } else if (icon instanceof VectorDrawable) { + width = height = iconSize; + } + + if (width <= 0 || height <= 0) return null; + + Bitmap bitmap = Bitmap.createBitmap(width, height, + Bitmap.Config.ARGB_8888); + canvas.setBitmap(bitmap); + + // Scale the original + Rect oldBounds = new Rect(); + oldBounds.set(icon.getBounds()); + icon.setBounds(0, 0, width, height); + canvas.save(); + canvas.scale(scale, scale, width / 2, height / 2); + if (colorFilter != null) { + Paint p = null; + if (icon instanceof BitmapDrawable) { + p = ((BitmapDrawable) icon).getPaint(); + } else if (icon instanceof PaintDrawable) { + p = ((PaintDrawable) icon).getPaint(); + } + if (p != null) p.setColorFilter(new ColorMatrixColorFilter(colorFilter)); + } + icon.draw(canvas); + canvas.restore(); + + // Mask off the original if iconMask is not null + if (iconMask != 0) { + Drawable mask = res.getDrawable(iconMask); + if (mask != null) { + mask.setBounds(icon.getBounds()); + ((BitmapDrawable) mask).getPaint().setXfermode( + new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + mask.draw(canvas); + } + } + // Draw the iconBacks if not null and then the original (scaled and masked) icon on top + if (iconBack != 0) { + Drawable back = res.getDrawable(iconBack); + if (back != null) { + back.setBounds(icon.getBounds()); + ((BitmapDrawable) back).getPaint().setXfermode( + new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); + back.draw(canvas); + } + } + // Finally draw the foreground if one was supplied + if (iconUpon != 0) { + Drawable upon = res.getDrawable(iconUpon); + if (upon != null) { + upon.setBounds(icon.getBounds()); + upon.draw(canvas); + } + } + icon.setBounds(oldBounds); + bitmap.setDensity(canvas.getDensity()); + + return bitmap; + } + + private static boolean cacheComposedIcon(Bitmap bmp, String path) { + try { + return sThemeService.cacheComposedIcon(bmp, path); + } catch (RemoteException e) { + Log.e(TAG, "Unable to cache icon.", e); + } + + return false; + } + + private static String getCachedIconPath(String pkgName, int resId, int density) { + return String.format("%s/%s", ThemeUtils.SYSTEM_THEME_ICON_CACHE_DIR, + getCachedIconName(pkgName, resId, density)); + } + + private static String getCachedIconName(String pkgName, int resId, int density) { + return String.format("%s_%08x_%d.png", pkgName, resId, density); + } + } + + public static class ColorFilterUtils { + private static final String TAG_FILTER = "filter"; + private static final String FILTER_HUE = "hue"; + private static final String FILTER_SATURATION = "saturation"; + private static final String FILTER_INVERT = "invert"; + private static final String FILTER_BRIGHTNESS = "brightness"; + private static final String FILTER_CONTRAST = "contrast"; + private static final String FILTER_ALPHA = "alpha"; + private static final String FILTER_TINT = "tint"; + + private static final int MIN_HUE = -180; + private static final int MAX_HUE = 180; + private static final int MIN_SATURATION = 0; + private static final int MAX_SATURATION = 200; + private static final int MIN_BRIGHTNESS = 0; + private static final int MAX_BRIGHTNESS = 200; + private static final int MIN_CONTRAST = -100; + private static final int MAX_CONTRAST = 100; + private static final int MIN_ALPHA = 0; + private static final int MAX_ALPHA = 100; + + public static boolean parseIconFilter(XmlPullParser parser, Builder builder) + throws IOException, XmlPullParserException { + String tag = parser.getName(); + if (!TAG_FILTER.equals(tag)) return false; + + int attrCount = parser.getAttributeCount(); + String attrName; + String attr = null; + int intValue; + while (attrCount-- > 0) { + attrName = parser.getAttributeName(attrCount); + if (attrName.equals("name")) { + attr = parser.getAttributeValue(attrCount); + } + } + String content = parser.nextText(); + if (attr != null && content != null && content.length() > 0) { + content = content.trim(); + if (FILTER_HUE.equalsIgnoreCase(attr)) { + intValue = clampValue(getInt(content, 0),MIN_HUE, MAX_HUE); + builder.hue(intValue); + } else if (FILTER_SATURATION.equalsIgnoreCase(attr)) { + intValue = clampValue(getInt(content, 100), + MIN_SATURATION, MAX_SATURATION); + builder.saturate(intValue); + } else if (FILTER_INVERT.equalsIgnoreCase(attr)) { + if ("true".equalsIgnoreCase(content)) { + builder.invertColors(); + } + } else if (FILTER_BRIGHTNESS.equalsIgnoreCase(attr)) { + intValue = clampValue(getInt(content, 100), + MIN_BRIGHTNESS, MAX_BRIGHTNESS); + builder.brightness(intValue); + } else if (FILTER_CONTRAST.equalsIgnoreCase(attr)) { + intValue = clampValue(getInt(content, 0), + MIN_CONTRAST, MAX_CONTRAST); + builder.contrast(intValue); + } else if (FILTER_ALPHA.equalsIgnoreCase(attr)) { + intValue = clampValue(getInt(content, 100), MIN_ALPHA, MAX_ALPHA); + builder.alpha(intValue); + } else if (FILTER_TINT.equalsIgnoreCase(attr)) { + try { + intValue = Color.parseColor(content); + builder.tint(intValue); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Cannot apply tint, invalid argument: " + content); + } + } + } + return true; + } + + private static int getInt(String value, int defaultValue) { + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + private static int clampValue(int value, int min, int max) { + return Math.min(max, Math.max(min, value)); + } + + /** + * See the following links for reference + * http://groups.google.com/group/android-developers/browse_thread/thread/9e215c83c3819953 + * http://gskinner.com/blog/archives/2007/12/colormatrix_cla.html + * @param value + */ + public static ColorMatrix adjustHue(float value) { + ColorMatrix cm = new ColorMatrix(); + value = value / 180 * (float) Math.PI; + if (value != 0) { + float cosVal = (float) Math.cos(value); + float sinVal = (float) Math.sin(value); + float lumR = 0.213f; + float lumG = 0.715f; + float lumB = 0.072f; + float[] mat = new float[]{ + lumR + cosVal * (1 - lumR) + sinVal * (-lumR), + lumG + cosVal * (-lumG) + sinVal * (-lumG), + lumB + cosVal * (-lumB) + sinVal * (1 - lumB), 0, 0, + lumR + cosVal * (-lumR) + sinVal * (0.143f), + lumG + cosVal * (1 - lumG) + sinVal * (0.140f), + lumB + cosVal * (-lumB) + sinVal * (-0.283f), 0, 0, + lumR + cosVal * (-lumR) + sinVal * (-(1 - lumR)), + lumG + cosVal * (-lumG) + sinVal * (lumG), + lumB + cosVal * (1 - lumB) + sinVal * (lumB), 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 0, 0, 1}; + cm.set(mat); + } + return cm; + } + + public static ColorMatrix adjustSaturation(float saturation) { + saturation = saturation / 100; + ColorMatrix cm = new ColorMatrix(); + cm.setSaturation(saturation); + + return cm; + } + + public static ColorMatrix invertColors() { + float[] matrix = { + -1, 0, 0, 0, 255, //red + 0, -1, 0, 0, 255, //green + 0, 0, -1, 0, 255, //blue + 0, 0, 0, 1, 0 //alpha + }; + + return new ColorMatrix(matrix); + } + + public static ColorMatrix adjustBrightness(float brightness) { + brightness = brightness / 100; + ColorMatrix cm = new ColorMatrix(); + cm.setScale(brightness, brightness, brightness, 1); + + return cm; + } + + public static ColorMatrix adjustContrast(float contrast) { + contrast = contrast / 100 + 1; + float o = (-0.5f * contrast + 0.5f) * 255; + float[] matrix = { + contrast, 0, 0, 0, o, //red + 0, contrast, 0, 0, o, //green + 0, 0, contrast, 0, o, //blue + 0, 0, 0, 1, 0 //alpha + }; + + return new ColorMatrix(matrix); + } + + public static ColorMatrix adjustAlpha(float alpha) { + alpha = alpha / 100; + ColorMatrix cm = new ColorMatrix(); + cm.setScale(1, 1, 1, alpha); + + return cm; + } + + public static ColorMatrix applyTint(int color) { + float alpha = Color.alpha(color) / 255f; + float red = Color.red(color) * alpha; + float green = Color.green(color) * alpha; + float blue = Color.blue(color) * alpha; + + float[] matrix = { + 1, 0, 0, 0, red, //red + 0, 1, 0, 0, green, //green + 0, 0, 1, 0, blue, //blue + 0, 0, 0, 1, 0 //alpha + }; + + return new ColorMatrix(matrix); + } + + public static class Builder { + private List<ColorMatrix> mMatrixList; + + public Builder() { + mMatrixList = new ArrayList<ColorMatrix>(); + } + + public Builder hue(float value) { + mMatrixList.add(adjustHue(value)); + return this; + } + + public Builder saturate(float saturation) { + mMatrixList.add(adjustSaturation(saturation)); + return this; + } + + public Builder brightness(float brightness) { + mMatrixList.add(adjustBrightness(brightness)); + return this; + } + + public Builder contrast(float contrast) { + mMatrixList.add(adjustContrast(contrast)); + return this; + } + + public Builder alpha(float alpha) { + mMatrixList.add(adjustAlpha(alpha)); + return this; + } + + public Builder invertColors() { + mMatrixList.add(ColorFilterUtils.invertColors()); + return this; + } + + public Builder tint(int color) { + mMatrixList.add(applyTint(color)); + return this; + } + + public ColorMatrix build() { + if (mMatrixList == null || mMatrixList.size() == 0) return null; + + ColorMatrix colorMatrix = new ColorMatrix(); + for (ColorMatrix cm : mMatrixList) { + colorMatrix.postConcat(cm); + } + return colorMatrix; + } + } + } +} diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index aa98e973855..c8aa72053eb 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -531,7 +531,8 @@ public final class LoadedApk { public Resources getResources(ActivityThread mainThread) { if (mResources == null) { mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs, - mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this); + mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this, + mainThread.getSystemContext(), mPackageName); } return mResources; } diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index 1691d8e28f5..26f9887458e 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -18,21 +18,36 @@ package android.app; import static android.app.ActivityThread.DEBUG_CONFIGURATION; +import android.content.Context; import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageItemInfo; +import android.content.pm.PackageManager; +import android.content.pm.ThemeUtils; import android.content.res.AssetManager; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; +import android.content.res.ThemeConfig; import android.content.res.Resources; import android.content.res.ResourcesKey; import android.hardware.display.DisplayManagerGlobal; import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.DisplayMetrics; +import android.util.Log; import android.util.Slog; +import android.util.SparseArray; import android.view.Display; import android.view.DisplayAdjustments; import java.lang.ref.WeakReference; +import java.util.List; import java.util.Locale; /** @hide */ @@ -49,6 +64,7 @@ public class ResourcesManager { = new ArrayMap<DisplayAdjustments, DisplayMetrics>(); CompatibilityInfo mResCompatibilityInfo; + static IPackageManager sPackageManager; Configuration mResConfiguration; final Configuration mTmpConfig = new Configuration(); @@ -150,10 +166,13 @@ public class ResourcesManager { * @param token the application token for determining stack bounds. */ public Resources getTopLevelResources(String resDir, String[] splitResDirs, - String[] overlayDirs, String[] libDirs, int displayId, - Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) { + String[] overlayDirs, String[] libDirs, int displayId, String packageName, + Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token, + Context context) { final float scale = compatInfo.applicationScale; - ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token); + final boolean isThemeable = compatInfo.isThemeable; + ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, + isThemeable, token); Resources r; synchronized (this) { // Resources is app scale dependent. @@ -178,6 +197,8 @@ public class ResourcesManager { //} AssetManager assets = new AssetManager(); + assets.setAppName(packageName); + assets.setThemeSupport(compatInfo.isThemeable); // resDir can be null if the 'android' package is creating a new Resources object. // This is fine, since each AssetManager automatically loads the 'android' package // already. @@ -197,7 +218,7 @@ public class ResourcesManager { if (overlayDirs != null) { for (String idmapPath : overlayDirs) { - assets.addOverlayPath(idmapPath); + assets.addOverlayPath(idmapPath, null, null, null, null); } } @@ -226,7 +247,29 @@ public class ResourcesManager { } else { config = getConfiguration(); } + + boolean iconsAttached = false; + /* Attach theme information to the resulting AssetManager when appropriate. */ + if (compatInfo.isThemeable && config != null && !context.getPackageManager().isSafeMode()) { + if (config.themeConfig == null) { + try { + config.themeConfig = ThemeConfig.getBootTheme(context.getContentResolver()); + } catch (Exception e) { + Slog.d(TAG, "ThemeConfig.getBootTheme failed, falling back to system theme", e); + config.themeConfig = ThemeConfig.getSystemTheme(); + } + } + + if (config.themeConfig != null) { + attachThemeAssets(assets, config.themeConfig); + attachCommonAssets(assets, config.themeConfig); + iconsAttached = attachIconAssets(assets, config.themeConfig); + } + } + r = new Resources(assets, dm, config, compatInfo, token); + if (iconsAttached) setActivityIcons(r); + if (false) { Slog.i(TAG, "Created app resources " + resDir + " " + r + ": " + r.getConfiguration() + " appScale=" @@ -249,6 +292,115 @@ public class ResourcesManager { } } + /** + * Creates the top level Resources for applications with the given compatibility info. + * + * @param resDir the resource directory. + * @param compatInfo the compability info. Must not be null. + * @param token the application token for determining stack bounds. + * + * @hide + */ + public Resources getTopLevelThemedResources(String resDir, int displayId, + String packageName, + String themePackageName, + CompatibilityInfo compatInfo, IBinder token) { + Resources r; + + AssetManager assets = new AssetManager(); + assets.setAppName(packageName); + assets.setThemeSupport(true); + if (assets.addAssetPath(resDir) == 0) { + return null; + } + + //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics); + DisplayMetrics dm = getDisplayMetricsLocked(displayId); + Configuration config; + boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); + if (!isDefaultDisplay) { + config = new Configuration(getConfiguration()); + applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config); + } else { + config = getConfiguration(); + } + + /* Attach theme information to the resulting AssetManager when appropriate. */ + ThemeConfig.Builder builder = new ThemeConfig.Builder(); + builder.defaultOverlay(themePackageName); + builder.defaultIcon(themePackageName); + builder.defaultFont(themePackageName); + + ThemeConfig themeConfig = builder.build(); + attachThemeAssets(assets, themeConfig); + attachCommonAssets(assets, themeConfig); + attachIconAssets(assets, themeConfig); + + r = new Resources(assets, dm, config, compatInfo, token); + setActivityIcons(r); + + return r; + } + + /** + * Creates a map between an activity & app's icon ids to its component info. This map + * is then stored in the resource object. + * When resource.getDrawable(id) is called it will check this mapping and replace + * the id with the themed resource id if one is available + * @param context + * @param pkgName + * @param r + */ + private void setActivityIcons(Resources r) { + SparseArray<PackageItemInfo> iconResources = new SparseArray<PackageItemInfo>(); + String pkgName = r.getAssets().getAppName(); + PackageInfo pkgInfo = null; + ApplicationInfo appInfo = null; + + try { + pkgInfo = getPackageManager().getPackageInfo(pkgName, PackageManager.GET_ACTIVITIES, + UserHandle.getCallingUserId()); + } catch (RemoteException e1) { + Log.e(TAG, "Unable to get pkg " + pkgName, e1); + return; + } + + final ThemeConfig themeConfig = r.getConfiguration().themeConfig; + if (pkgName != null && themeConfig != null && + pkgName.equals(themeConfig.getIconPackPkgName())) { + return; + } + + //Map application icon + if (pkgInfo != null && pkgInfo.applicationInfo != null) { + appInfo = pkgInfo.applicationInfo; + if (appInfo.themedIcon != 0 || iconResources.get(appInfo.icon) == null) { + iconResources.put(appInfo.icon, appInfo); + } + } + + //Map activity icons. + if (pkgInfo != null && pkgInfo.activities != null) { + for (ActivityInfo ai : pkgInfo.activities) { + if (ai.icon != 0 && (ai.themedIcon != 0 || iconResources.get(ai.icon) == null)) { + iconResources.put(ai.icon, ai); + } else if (appInfo != null && appInfo.icon != 0 && + (ai.themedIcon != 0 || iconResources.get(appInfo.icon) == null)) { + iconResources.put(appInfo.icon, ai); + } + } + } + + r.setIconResources(iconResources); + final IPackageManager pm = getPackageManager(); + try { + ComposedIconInfo iconInfo = pm.getComposedIconInfo(); + r.setComposedIconInfo(iconInfo); + } catch (Exception e) { + Log.wtf(TAG, "Failed to retrieve ComposedIconInfo", e); + } + } + public final boolean applyConfigurationToResourcesLocked(Configuration config, CompatibilityInfo compat) { if (mResConfiguration == null) { @@ -293,6 +445,22 @@ public class ResourcesManager { boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); DisplayMetrics dm = defaultDisplayMetrics; final boolean hasOverrideConfiguration = key.hasOverrideConfiguration(); + boolean themeChanged = (changes & ActivityInfo.CONFIG_THEME_RESOURCE) != 0; + if (themeChanged) { + AssetManager am = r.getAssets(); + if (am.hasThemeSupport()) { + r.setIconResources(null); + r.setComposedIconInfo(null); + detachThemeAssets(am); + if (config.themeConfig != null) { + attachThemeAssets(am, config.themeConfig); + attachCommonAssets(am, config.themeConfig); + if (attachIconAssets(am, config.themeConfig)) { + setActivityIcons(r); + } + } + } + } if (!isDefaultDisplay || hasOverrideConfiguration) { if (tmpConfig == null) { tmpConfig = new Configuration(); @@ -309,6 +477,9 @@ public class ResourcesManager { } else { r.updateConfiguration(config, dm, compat); } + if (themeChanged) { + r.updateStringCache(); + } //Slog.i(TAG, "Updated app resources " + v.getKey() // + " " + r + ": " + r.getConfiguration()); } else { @@ -320,4 +491,234 @@ public class ResourcesManager { return changes != 0; } + public static IPackageManager getPackageManager() { + if (sPackageManager != null) { + return sPackageManager; + } + IBinder b = ServiceManager.getService("package"); + sPackageManager = IPackageManager.Stub.asInterface(b); + return sPackageManager; + } + + + /** + * Attach the necessary theme asset paths and meta information to convert an + * AssetManager to being globally "theme-aware". + * + * @param assets + * @param theme + * @return true if the AssetManager is now theme-aware; false otherwise. + * This can fail, for example, if the theme package has been been + * removed and the theme manager has yet to revert formally back to + * the framework default. + */ + private boolean attachThemeAssets(AssetManager assets, ThemeConfig theme) { + PackageInfo piTheme = null; + PackageInfo piTarget = null; + PackageInfo piAndroid = null; + + // Some apps run in process of another app (eg keyguard/systemUI) so we must get the + // package name from the res tables. The 0th base package name will be the android group. + // The 1st base package name will be the app group if one is attached. Check if it is there + // first or else the system will crash! + String basePackageName = null; + String resourcePackageName = null; + int count = assets.getBasePackageCount(); + if (count > 1) { + basePackageName = assets.getBasePackageName(1); + resourcePackageName = assets.getBaseResourcePackageName(1); + } else if (count == 1) { + basePackageName = assets.getBasePackageName(0); + } else { + return false; + } + + try { + piTheme = getPackageManager().getPackageInfo( + theme.getOverlayPkgNameForApp(basePackageName), 0, + UserHandle.getCallingUserId()); + piTarget = getPackageManager().getPackageInfo( + basePackageName, 0, UserHandle.getCallingUserId()); + + // Handle special case where a system app (ex trebuchet) may have had its pkg name + // renamed during an upgrade. basePackageName would be the manifest value which will + // fail on getPackageInfo(). resource pkg is assumed to have the original name + if (piTarget == null && resourcePackageName != null) { + piTarget = getPackageManager().getPackageInfo(resourcePackageName, + 0, UserHandle.getCallingUserId()); + } + piAndroid = getPackageManager().getPackageInfo("android", 0, + UserHandle.getCallingUserId()); + } catch (RemoteException e) { + } + + if (piTheme == null || piTheme.applicationInfo == null || + piTarget == null || piTarget.applicationInfo == null || + piAndroid == null || piAndroid.applicationInfo == null || + piTheme.mOverlayTargets == null) { + return false; + } + + String themePackageName = basePackageName; + String themePath = piTheme.applicationInfo.publicSourceDir; + if (!piTarget.isThemeApk && piTheme.mOverlayTargets.contains(basePackageName)) { + String targetPackagePath = piTarget.applicationInfo.sourceDir; + String prefixPath = ThemeUtils.getOverlayPathToTarget(basePackageName); + + String resCachePath = ThemeUtils.getTargetCacheDir(piTarget.packageName, piTheme); + String resApkPath = resCachePath + "/resources.apk"; + String idmapPath = ThemeUtils.getIdmapPath(piTarget.packageName, piTheme.packageName); + int cookie = assets.addOverlayPath(idmapPath, themePath, resApkPath, + targetPackagePath, prefixPath); + + if (cookie != 0) { + assets.setThemePackageName(basePackageName); + assets.addThemeCookie(cookie); + } + } + + if (!piTarget.isThemeApk && piTheme.mOverlayTargets.contains("android")) { + String resCachePath= ThemeUtils.getTargetCacheDir(piAndroid.packageName, piTheme); + String prefixPath = ThemeUtils.getOverlayPathToTarget(piAndroid.packageName); + String targetPackagePath = piAndroid.applicationInfo.publicSourceDir; + String resApkPath = resCachePath + "/resources.apk"; + String idmapPath = ThemeUtils.getIdmapPath("android", piTheme.packageName); + int cookie = assets.addOverlayPath(idmapPath, themePath, + resApkPath, targetPackagePath, prefixPath); + if (cookie != 0) { + assets.setThemePackageName(themePackageName); + assets.addThemeCookie(cookie); + } + } + + return true; + } + + /** + * Attach the necessary icon asset paths. Icon assets should be in a different + * namespace than the standard 0x7F. + * + * @param assets + * @param theme + * @return true if succes, false otherwise + */ + private boolean attachIconAssets(AssetManager assets, ThemeConfig theme) { + PackageInfo piIcon = null; + try { + piIcon = getPackageManager().getPackageInfo(theme.getIconPackPkgName(), 0, + UserHandle.getCallingUserId()); + } catch (RemoteException e) { + } + + if (piIcon == null || piIcon.applicationInfo == null) { + return false; + } + + String iconPkg = theme.getIconPackPkgName(); + if (iconPkg != null && !iconPkg.isEmpty()) { + String themeIconPath = piIcon.applicationInfo.publicSourceDir; + String prefixPath = ThemeUtils.ICONS_PATH; + String iconDir = ThemeUtils.getIconPackDir(iconPkg); + String resTablePath = iconDir + "/resources.arsc"; + String resApkPath = iconDir + "/resources.apk"; + + // Legacy Icon packs have everything in their APK + if (piIcon.isLegacyIconPackApk) { + prefixPath = ""; + resApkPath = ""; + resTablePath = ""; + } + + int cookie = assets.addIconPath(themeIconPath, resApkPath, prefixPath, + Resources.THEME_ICON_PKG_ID); + if (cookie != 0) { + assets.setIconPackCookie(cookie); + assets.setIconPackageName(iconPkg); + } + } + + return true; + } + + /** + * Attach the necessary common asset paths. Common assets should be in a different + * namespace than the standard 0x7F. + * + * @param assets + * @param theme + * @return true if succes, false otherwise + */ + private boolean attachCommonAssets(AssetManager assets, ThemeConfig theme) { + // Some apps run in process of another app (eg keyguard/systemUI) so we must get the + // package name from the res tables. The 0th base package name will be the android group. + // The 1st base package name will be the app group if one is attached. Check if it is there + // first or else the system will crash! + String basePackageName; + int count = assets.getBasePackageCount(); + if (count > 1) { + basePackageName = assets.getBasePackageName(1); + } else if (count == 1) { + basePackageName = assets.getBasePackageName(0); + } else { + return false; + } + + PackageInfo piTheme = null; + try { + piTheme = getPackageManager().getPackageInfo( + theme.getOverlayPkgNameForApp(basePackageName), 0, + UserHandle.getCallingUserId()); + } catch (RemoteException e) { + } + + if (piTheme == null || piTheme.applicationInfo == null) { + return false; + } + + String themePackageName = + ThemeUtils.getCommonPackageName(piTheme.applicationInfo.packageName); + if (themePackageName != null && !themePackageName.isEmpty()) { + String themePath = piTheme.applicationInfo.publicSourceDir; + String prefixPath = ThemeUtils.COMMON_RES_PATH; + String resCachePath = + ThemeUtils.getTargetCacheDir(ThemeUtils.COMMON_RES_TARGET, piTheme); + String resApkPath = resCachePath + "/resources.apk"; + int cookie = assets.addCommonOverlayPath(themePath, resApkPath, + prefixPath); + if (cookie != 0) { + assets.setCommonResCookie(cookie); + assets.setCommonResPackageName(themePackageName); + } + } + + return true; + } + + private void detachThemeAssets(AssetManager assets) { + String themePackageName = assets.getThemePackageName(); + String iconPackageName = assets.getIconPackageName(); + String commonResPackageName = assets.getCommonResPackageName(); + + //Remove Icon pack if it exists + if (!TextUtils.isEmpty(iconPackageName) && assets.getIconPackCookie() > 0) { + assets.removeOverlayPath(iconPackageName, assets.getIconPackCookie()); + assets.setIconPackageName(null); + assets.setIconPackCookie(0); + } + //Remove common resources if it exists + if (!TextUtils.isEmpty(commonResPackageName) && assets.getCommonResCookie() > 0) { + assets.removeOverlayPath(commonResPackageName, assets.getCommonResCookie()); + assets.setCommonResPackageName(null); + assets.setCommonResCookie(0); + } + final List<Integer> themeCookies = assets.getThemeCookies(); + if (!TextUtils.isEmpty(themePackageName) && !themeCookies.isEmpty()) { + // remove overlays in reverse order + for (int i = themeCookies.size() - 1; i >= 0; i--) { + assets.removeOverlayPath(themePackageName, themeCookies.get(i)); + } + } + assets.getThemeCookies().clear(); + assets.setThemePackageName(null); + } } diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 8bfe6d3859a..1e52d86ff76 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -32,6 +32,7 @@ import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; +import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; @@ -226,9 +227,10 @@ public class WallpaperManager { private IWallpaperManager mService; private Bitmap mWallpaper; private Bitmap mDefaultWallpaper; + private Bitmap mKeyguardWallpaper; private static final int MSG_CLEAR_WALLPAPER = 1; - + Globals(Looper looper) { IBinder b = ServiceManager.getService(Context.WALLPAPER_SERVICE); mService = IWallpaperManager.Stub.asInterface(b); @@ -246,6 +248,12 @@ public class WallpaperManager { } } + public void onKeyguardWallpaperChanged() { + synchronized (this) { + mKeyguardWallpaper = null; + } + } + public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault) { synchronized (this) { if (mWallpaper != null) { @@ -272,6 +280,23 @@ public class WallpaperManager { } } + /** + * @hide + */ + public Bitmap peekKeyguardWallpaperBitmap(Context context) { + synchronized (this) { + if (mKeyguardWallpaper != null) { + return mKeyguardWallpaper; + } + try { + mKeyguardWallpaper = getCurrentKeyguardWallpaperLocked(context); + } catch (OutOfMemoryError e) { + Log.w(TAG, "No memory load current keyguard wallpaper", e); + } + return mKeyguardWallpaper; + } + } + public void forgetLoadedWallpaper() { synchronized (this) { mWallpaper = null; @@ -308,7 +333,33 @@ public class WallpaperManager { } return null; } - + + private Bitmap getCurrentKeyguardWallpaperLocked(Context context) { + try { + Bundle params = new Bundle(); + ParcelFileDescriptor fd = mService.getKeyguardWallpaper(this, params); + if (fd != null) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + Bitmap bm = BitmapFactory.decodeFileDescriptor( + fd.getFileDescriptor(), null, options); + return bm; + } catch (OutOfMemoryError e) { + Log.w(TAG, "Can't decode file", e); + } finally { + try { + fd.close(); + } catch (IOException e) { + // Ignore + } + } + } + } catch (RemoteException e) { + // Ignore + } + return null; + } + private Bitmap getDefaultWallpaperLocked(Context context) { InputStream is = openDefaultWallpaper(context); if (is != null) { @@ -327,6 +378,18 @@ public class WallpaperManager { } return null; } + + /** @hide */ + public void clearKeyguardWallpaper() { + synchronized (this) { + try { + mService.clearKeyguardWallpaper(); + } catch (RemoteException e) { + // ignore + } + mKeyguardWallpaper = null; + } + } } private static final Object sSync = new Object[0]; @@ -586,6 +649,15 @@ public class WallpaperManager { return null; } + /** @hide */ + public Drawable getFastKeyguardDrawable() { + Bitmap bm = sGlobals.peekKeyguardWallpaperBitmap(mContext); + if (bm != null) { + return new FastBitmapDrawable(bm); + } + return null; + } + /** * Like {@link #getFastDrawable()}, but if there is no wallpaper set, * a null pointer is returned. @@ -611,6 +683,13 @@ public class WallpaperManager { } /** + * @hide + */ + public Bitmap getKeyguardBitmap() { + return sGlobals.peekKeyguardWallpaperBitmap(mContext); + } + + /** * Remove all internal references to the last loaded wallpaper. Useful * for apps that want to reduce memory usage when they only temporarily * need to have the wallpaper. After calling, the next request for the @@ -771,6 +850,35 @@ public class WallpaperManager { } /** + * @param bitmap + * @throws IOException + * @hide + */ + public void setKeyguardBitmap(Bitmap bitmap) throws IOException { + if (sGlobals.mService == null) { + Log.w(TAG, "WallpaperService not running"); + return; + } + try { + ParcelFileDescriptor fd = sGlobals.mService.setKeyguardWallpaper(null); + if (fd == null) { + return; + } + FileOutputStream fos = null; + try { + fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); + bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos); + } finally { + if (fos != null) { + fos.close(); + } + } + } catch (RemoteException e) { + // Ignore + } + } + + /** * Change the current system wallpaper to a specific byte stream. The * give InputStream is copied into persistent storage and will now be * used as the wallpaper. Currently it must be either a JPEG or PNG @@ -809,6 +917,33 @@ public class WallpaperManager { } } + /** + * @hide + */ + public void setKeyguardStream(InputStream data) throws IOException { + if (sGlobals.mService == null) { + Log.w(TAG, "WallpaperService not running"); + return; + } + try { + ParcelFileDescriptor fd = sGlobals.mService.setKeyguardWallpaper(null); + if (fd == null) { + return; + } + FileOutputStream fos = null; + try { + fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd); + setWallpaper(data, fos); + } finally { + if (fos != null) { + fos.close(); + } + } + } catch (RemoteException e) { + // Ignore + } + } + private void setWallpaper(InputStream data, FileOutputStream fos) throws IOException { byte[] buffer = new byte[32768]; @@ -1029,7 +1164,29 @@ public class WallpaperManager { mWallpaperXStep = xStep; mWallpaperYStep = yStep; } - + + /** @hide */ + public int getLastWallpaperX() { + try { + return WindowManagerGlobal.getWindowSession().getLastWallpaperX(); + } catch (RemoteException e) { + // Ignore. + } + + return -1; + } + + /** @hide */ + public int getLastWallpaperY() { + try { + return WindowManagerGlobal.getWindowSession().getLastWallpaperY(); + } catch (RemoteException e) { + // Ignore. + } + + return -1; + } + /** * Send an arbitrary command to the current active wallpaper. * @@ -1086,7 +1243,25 @@ public class WallpaperManager { * wallpaper. */ public void clear() throws IOException { - setStream(openDefaultWallpaper(mContext)); + clear(true); + } + + /** @hide */ + public void clear(boolean setToDefault) throws IOException { + if (setToDefault) { + setStream(openDefaultWallpaper(mContext)); + } else { + Bitmap blackBmp = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); + blackBmp.setPixel(0, 0, mContext.getResources().getColor(android.R.color.black)); + setBitmap(blackBmp); + } + } + + /** + * @hide + */ + public void clearKeyguardWallpaper() { + sGlobals.clearKeyguardWallpaper(); } /** diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 57b6df14679..9831d915107 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -2706,6 +2706,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 @@ -3368,6 +3378,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 ad7c350aa24..837208619d7 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -668,7 +668,20 @@ public class ContextWrapper extends Context { /** @hide */ 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 02c7395cd42..21fe8d60cdb 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. @@ -1847,6 +1848,15 @@ public class Intent implements Parcelable, Cloneable { */ @Deprecated @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_WALLPAPER_CHANGED = "android.intent.action.WALLPAPER_CHANGED"; + + /** + * Broadcast Action: The current keyguard wallpaper configuration + * has changed and should be re-read. + * {@hide} + */ + public static final String ACTION_KEYGUARD_WALLPAPER_CHANGED = + "android.intent.action.KEYGUARD_WALLPAPER_CHANGED"; + /** * Broadcast Action: The current device {@link android.content.res.Configuration} * (orientation, locale, etc) has changed. When such a change happens, the @@ -2611,6 +2621,21 @@ 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_FAILURE = + "com.tmobile.intent.action.APP_FAILURE"; + + /** + * Broadcast Action: Request to reset the unrecoverable errors count to 0. + * @hide + */ + public static final String ACTION_APP_FAILURE_RESET = + "com.tmobile.intent.action.APP_FAILURE_RESET"; + + /** * Activity Action: Shows the brightness setting dialog. * @hide */ @@ -2706,6 +2731,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 @@ -2923,6 +2961,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()). @@ -3432,6 +3478,26 @@ public class Intent implements Parcelable, Cloneable { /** {@hide} */ public static final String EXTRA_REASON = "android.intent.extra.REASON"; + /** + * 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 641f843ea2d..1ff75531a14 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 @@ -492,6 +493,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 76d71c17845..5e342c9c5f0 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. @@ -588,6 +589,12 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { public int installLocation = PackageInfo.INSTALL_LOCATION_UNSPECIFIED; /** + * Is given application theme agnostic, i.e. behaves properly when default theme is changed. + * {@hide} + */ + public boolean isThemeable = false; + + /** * When true, indicates that any one component within this application is * protected. * @hide @@ -717,6 +724,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { uiOptions = orig.uiOptions; backupAgentName = orig.backupAgentName; protect = orig.protect; + isThemeable = orig.isThemeable; } @@ -768,6 +776,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeInt(descriptionRes); dest.writeInt(uiOptions); dest.writeInt(protect ? 1 : 0); + dest.writeInt(isThemeable ? 1 : 0); } public static final Parcelable.Creator<ApplicationInfo> CREATOR @@ -818,6 +827,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { descriptionRes = source.readInt(); uiOptions = 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 00000000000..8ece42d1933 --- /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 5700e44341f..a24ccad7b90 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; @@ -459,4 +460,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 b66bd017275..ed6d734d5a4 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; @@ -237,6 +243,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; @@ -301,6 +335,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 @@ -346,5 +387,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 e336c5f752a..3622df4d174 100644 --- a/core/java/android/content/pm/PackageInfoLite.java +++ b/core/java/android/content/pm/PackageInfoLite.java @@ -52,6 +52,7 @@ public class PackageInfoLite implements Parcelable { */ public int recommendedInstallLocation; public int installLocation; + public boolean isTheme; public VerifierInfo[] verifiers; @@ -74,6 +75,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); @@ -100,6 +102,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 cacdf8e3625..cbd6198607b 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; } /** @@ -292,8 +300,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(); @@ -305,6 +314,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 c37322a1a5a..cbdf40b9598 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -782,6 +782,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. * @@ -3121,6 +3153,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 @@ -4123,4 +4167,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 afb439247fe..23bf2789ca9 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. @@ -58,6 +59,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; @@ -74,13 +76,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 @@ -110,6 +116,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"; /** @hide */ public static class NewPermissionInfo { @@ -246,6 +263,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; @@ -265,6 +283,7 @@ public class PackageParser { public final boolean coreApp; public final boolean multiArch; + public PackageLite(String codePath, ApkLite baseApk, String[] splitNames, String[] splitCodePaths) { this.packageName = baseApk.packageName; @@ -277,6 +296,7 @@ public class PackageParser { this.splitCodePaths = splitCodePaths; this.coreApp = baseApk.coreApp; this.multiArch = baseApk.multiArch; + this.isTheme = baseApk.isTheme; } public List<String> getAllCodePaths() { @@ -302,10 +322,11 @@ public class PackageParser { public final Signature[] signatures; public final boolean coreApp; public final boolean multiArch; + public final boolean isTheme; public ApkLite(String codePath, String packageName, String splitName, int versionCode, int installLocation, List<VerifierInfo> verifiers, Signature[] signatures, - boolean coreApp, boolean multiArch) { + boolean coreApp, boolean multiArch, boolean isTheme) { this.codePath = codePath; this.packageName = packageName; this.splitName = splitName; @@ -315,6 +336,7 @@ public class PackageParser { this.signatures = signatures; this.coreApp = coreApp; this.multiArch = multiArch; + this.isTheme = isTheme; } } @@ -413,6 +435,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; @@ -874,6 +904,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) { @@ -996,6 +1038,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 @@ -1275,6 +1362,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 @@ -1299,10 +1389,52 @@ 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, - installLocation, verifiers, signatures, coreApp, multiArch); + installLocation, verifiers, signatures, coreApp, multiArch, 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; } /** @@ -1354,6 +1486,8 @@ public class PackageParser { } final Package pkg = new Package(pkgName); + Bundle metaDataBundle = new Bundle(); + boolean foundApp = false; TypedArray sa = res.obtainAttributes(attrs, @@ -1759,6 +1893,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(); @@ -1849,6 +1988,9 @@ 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; + } /* * b/8528162: Ignore the <uses-permission android:required> attribute if @@ -1861,6 +2003,14 @@ public class PackageParser { } } + //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; } @@ -2389,6 +2539,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); @@ -3175,6 +3328,26 @@ public class PackageParser { if (!parseIntent(res, parser, attrs, 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 + " " @@ -4246,6 +4419,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; @@ -4291,6 +4475,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 00000000000..acbc85e9c8b --- /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 00000000000..ab798db7b00 --- /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 00000000000..38391d481b1 --- /dev/null +++ b/core/java/android/content/pm/ThemeUtils.java @@ -0,0 +1,718 @@ +/* + * 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.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.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 RESOURCE_CACHE_DIR = "/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" + }; + + + /** + * Get the root path of the resource cache for the given theme + * @param themePkgName + * @return Root resource cache path for the given theme + */ + public static String getOverlayResourceCacheDir(String themePkgName) { + return RESOURCE_CACHE_DIR + themePkgName; + } + + /** + * Get the path of the resource cache for the given target and theme + * @param targetPkgName + * @param themePkg + * @return Path to the resource cache for this target and theme + */ + public static String getTargetCacheDir(String targetPkgName, PackageInfo themePkg) { + return getTargetCacheDir(targetPkgName, themePkg.packageName); + } + + public static String getTargetCacheDir(String targetPkgName, PackageParser.Package themePkg) { + return getTargetCacheDir(targetPkgName, themePkg.packageName); + } + + public static String getTargetCacheDir(String targetPkgName, String themePkgName) { + return getOverlayResourceCacheDir(themePkgName) + File.separator + targetPkgName; + } + + /** + * Get the path to the icons for the given theme + * @param pkgName + * @return + */ + public static String getIconPackDir(String pkgName) { + return getOverlayResourceCacheDir(pkgName) + File.separator + "icons"; + } + + 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 getIdmapPath(String targetPkgName, String overlayPkgName) { + return getTargetCacheDir(targetPkgName, overlayPkgName) + File.separator + "idmap"; + } + + 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(RESOURCE_CACHE_DIR); + 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 overlayPkgName) + throws IOException { + createDirIfNotExists(getOverlayResourceCacheDir(overlayPkgName)); + File file = new File(getTargetCacheDir(targetPkgName, overlayPkgName)); + 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 { + createDirIfNotExists(getOverlayResourceCacheDir(pkgName)); + 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() { + FileUtils.deleteContents(new File(SYSTEM_THEME_ICON_CACHE_DIR)); + } + + 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 e413ed29be8..74a906ffb4e 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -21,9 +21,11 @@ import android.util.Log; import android.util.SparseArray; import android.util.TypedValue; +import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.HashMap; /** @@ -77,6 +79,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 +264,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(); @@ -629,17 +647,72 @@ public final class AssetManager implements AutoCloseable { * * {@hide} */ - public final int addOverlayPath(String idmapPath) { + // TODO: change the signature of this method to match addOverlayPathNative + public final int addOverlayPath(String idmapPath, String themeApkPath, + String resApkPath, String targetPkgPath, String prefixPath) { synchronized (this) { - int res = addOverlayPathNative(idmapPath); - if (mStringBlocks != null) { - makeStringBlocks(mStringBlocks); + return addOverlayPathNative(idmapPath, themeApkPath, resApkPath, targetPkgPath, + prefixPath); + } + } + + private native final int addOverlayPathNative(String idmapPath, String themeApkPath, + String resApkPath, String targetPkgPath, String prefixPath); + + /** + * Add a set of common assets. + * + * {@hide} + */ + public final int addCommonOverlayPath(String themeApkPath, + String resApkPath, String prefixPath) { + synchronized (this) { + if ((new File(themeApkPath).exists()) && (new File(resApkPath).exists())) { + return addCommonOverlayPathNative(themeApkPath, resApkPath, prefixPath); } - return res; + + return 0; } } - private native final int addOverlayPathNative(String idmapPath); + private native final int addCommonOverlayPathNative(String themeApkPath, + 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 @@ -662,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. @@ -788,6 +981,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 da35ee92267..47d5d0551ee 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 27bbb242f57..121c1375b55 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 @@ -412,7 +418,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}. @@ -644,8 +690,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("{"); @@ -780,6 +829,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(); } @@ -806,6 +857,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} */ @@ -948,7 +1000,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; } @@ -1058,7 +1116,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; } @@ -1074,7 +1135,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; } /** @@ -1147,6 +1210,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) { @@ -1175,6 +1239,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 @@ -1242,7 +1307,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; } @@ -1279,6 +1349,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 00000000000..a2e2abd6def --- /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 00000000000..2e1c16e53bf --- /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 00000000000..e8bb5c4329e --- /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 7f276c24dc7..cc8a6049298 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -16,6 +16,9 @@ package android.content.res; +import android.app.ComposedIconInfo; +import android.app.IconPackHelper; +import android.app.IconPackHelper.IconCustomizer; import android.util.Pools.SynchronizedPool; import android.view.ViewDebug; import com.android.internal.util.XmlUtils; @@ -25,6 +28,7 @@ import org.xmlpull.v1.XmlPullParserException; import android.annotation.Nullable; import android.content.pm.ActivityInfo; +import android.content.pm.PackageItemInfo; import android.graphics.Movie; import android.graphics.drawable.Drawable; import android.graphics.drawable.ColorDrawable; @@ -38,6 +42,7 @@ import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.util.Slog; +import android.util.SparseArray; import android.util.TypedValue; import android.util.LongSparseArray; @@ -86,6 +91,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 @@ -136,6 +155,9 @@ public class Resources { @SuppressWarnings("unused") private WeakReference<IBinder> mToken; + private SparseArray<PackageItemInfo> mIcons; + private ComposedIconInfo mComposedIconInfo; + static { sPreloadedDrawables = new LongSparseArray[2]; sPreloadedDrawables[0] = new LongSparseArray<ConstantState>(); @@ -230,7 +252,7 @@ public class Resources { } mToken = new WeakReference<IBinder>(token); updateConfiguration(config, metrics); - assets.ensureStringBlocks(); + assets.recreateStringBlocks(); } /** @@ -745,6 +767,18 @@ public class Resources { * not exist. */ public Drawable getDrawable(int id, @Nullable Theme theme) throws NotFoundException { + return getDrawable(id, theme, true); + } + + /** @hide */ + public Drawable getDrawable(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; @@ -753,9 +787,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; @@ -808,6 +857,18 @@ public class Resources { * not exist. */ public Drawable getDrawableForDensity(int id, int density, @Nullable Theme theme) { + return getDrawableForDensity(id, density, theme, true); + } + + /** @hide */ + public Drawable getDrawableForDensity(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; @@ -816,7 +877,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 @@ -1226,8 +1287,24 @@ public class Resources { */ public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException { + getValue(id, outValue, resolveRefs, true); + } + + /** @hide */ + public void getValue(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" @@ -1249,8 +1326,44 @@ public class Resources { */ public void getValueForDensity(int id, int density, TypedValue outValue, boolean resolveRefs) throws NotFoundException { + getValueForDensity(id, density, outValue, resolveRefs, true); + } + + /** @hide */ + public void getValueForDensity(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)); @@ -1776,7 +1889,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); + } } if (mConfiguration.locale == null) { mConfiguration.locale = Locale.getDefault(); @@ -1848,6 +1969,19 @@ public class Resources { private void clearDrawableCacheLocked( LongSparseArray<WeakReference<ConstantState>> cache, int configChanges) { + /* + * Quick test to find out if the config change that occurred should + * trigger a full cache wipe. + */ + if (Configuration.needNewResources(configChanges, 0)) { + if (DEBUG_CONFIG) { + Log.d(TAG, "Clear drawable cache from config changes: 0x" + + Integer.toHexString(configChanges)); + } + cache.clear(); + return; + } + if (DEBUG_CONFIG) { Log.d(TAG, "Cleaning up drawables config changes: 0x" + Integer.toHexString(configChanges)); @@ -2278,6 +2412,13 @@ public class Resources { return true; } + /** @hide */ + public final void updateStringCache() { + synchronized (mAccessLock) { + mAssets.recreateStringBlocks(); + } + } + /*package*/ Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException { if (TRACE_FOR_PRELOAD) { // Log only framework resources @@ -2316,9 +2457,10 @@ public class Resources { // themeable 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); } final Drawable dr; @@ -2496,7 +2638,7 @@ public class Resources { if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { - csl = sPreloadedColorStateLists.get(key); + csl = mAssets.hasThemedAssets() ? null : sPreloadedColorStateLists.get(key); if (csl != null) { return csl; } @@ -2517,7 +2659,7 @@ public class Resources { return csl; } - csl = sPreloadedColorStateLists.get(key); + csl = mAssets.hasThemedAssets() ? null : sPreloadedColorStateLists.get(key); if (csl != null) { return csl; } @@ -2664,6 +2806,21 @@ public class Resources { } } + /** @hide */ + public void setIconResources(SparseArray<PackageItemInfo> icons) { + mIcons = icons; + } + + /** @hide */ + public void setComposedIconInfo(ComposedIconInfo iconInfo) { + mComposedIconInfo = iconInfo; + } + + /** @hide */ + public ComposedIconInfo getComposedIconInfo() { + return mComposedIconInfo; + } + 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 e0f1b3a3f07..e3ab161e754 100644 --- a/core/java/android/content/res/ResourcesKey.java +++ b/core/java/android/content/res/ResourcesKey.java @@ -22,6 +22,7 @@ import android.os.IBinder; public final class ResourcesKey { final String mResDir; final float mScale; + final private boolean mIsThemeable; private final int mHash; private final IBinder mToken; @@ -29,13 +30,14 @@ public final class ResourcesKey { public final Configuration mOverrideConfiguration = new Configuration(); public ResourcesKey(String resDir, int displayId, Configuration overrideConfiguration, - float scale, IBinder token) { + float scale, boolean isThemeable, IBinder token) { mResDir = resDir; mDisplayId = displayId; if (overrideConfiguration != null) { mOverrideConfiguration.setTo(overrideConfiguration); } mScale = scale; + mIsThemeable = isThemeable; mToken = token; int hash = 17; @@ -44,6 +46,7 @@ public final class ResourcesKey { hash = 31 * hash + (mOverrideConfiguration != null ? mOverrideConfiguration.hashCode() : 0); hash = 31 * hash + Float.floatToIntBits(mScale); + hash = 31 * hash + (mIsThemeable ? 1 : 0); mHash = hash; } @@ -83,7 +86,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 00000000000..1b1837d7420 --- /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 00000000000..a9d2fcc942a --- /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); + } +} + diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 3ada9bb002e..855ae8c19ec 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -486,11 +486,12 @@ public class Process { String abi, String instructionSet, String appDataDir, + boolean refreshTheme, String[] zygoteArgs) { try { return startViaZygote(processClass, niceName, uid, gid, gids, debugFlags, mountExternal, targetSdkVersion, seInfo, - abi, instructionSet, appDataDir, zygoteArgs); + abi, instructionSet, appDataDir, refreshTheme, zygoteArgs); } catch (ZygoteStartFailedEx ex) { Log.e(LOG_TAG, "Starting VM process through Zygote failed"); @@ -609,6 +610,7 @@ public class Process { String abi, String instructionSet, String appDataDir, + boolean refreshTheme, String[] extraArgs) throws ZygoteStartFailedEx { synchronized(Process.class) { @@ -639,6 +641,9 @@ public class Process { } else if (mountExternal == Zygote.MOUNT_EXTERNAL_MULTIUSER_ALL) { argsForZygote.add("--mount-external-multiuser-all"); } + if (refreshTheme) { + argsForZygote.add("--refresh_theme"); + } argsForZygote.add("--target-sdk-version=" + targetSdkVersion); //TODO optionally enable debuger diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 91053d52549..1b8db12721a 100755 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5216,6 +5216,35 @@ public final class Settings { public static final String PROTECTED_COMPONENTS = "protected_components"; /** + * Default theme to use. If empty, use holo. + * @hide + */ + public static final String DEFAULT_THEME_PACKAGE = "default_theme_package"; + + /** + * A '|' delimited list of theme components to apply from the default theme on first boot. + * Components can be one or more of the "mods_XXXXXXX" found in + * {@link ThemesContract$ThemesColumns}. Leaving this field blank assumes all components + * will be applied. + * + * ex: mods_icons|mods_overlays|mods_homescreen + * + * @hide + */ + public static final String DEFAULT_THEME_COMPONENTS = "default_theme_components"; + + /** + * This will be set to the system's current theme API version when ThemeService starts. + * It is useful for when an upgrade from one version of CM to another occurs. + * For example, after a user upgrades from CM11 to CM12, the value of this field + * might be 19. ThemeService would then change the value to 21. This is useful + * when an API change breaks a theme. Themeservice can identify old themes and + * unapply them from the system. + * @hide + */ + public static final String THEME_PREV_BOOT_API_LEVEL = "theme_prev_boot_api_level"; + + /** * This are the settings to be backed up. * * NOTE: Settings are backed up and restored in the order they appear diff --git a/core/java/android/provider/ThemesContract.java b/core/java/android/provider/ThemesContract.java new file mode 100644 index 00000000000..33fb09df512 --- /dev/null +++ b/core/java/android/provider/ThemesContract.java @@ -0,0 +1,565 @@ +package android.provider; + +import android.net.Uri; + +/** + * @hide + */ +public class ThemesContract { + public static final String AUTHORITY = "com.cyanogenmod.themes"; + public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); + + public static class ThemesColumns { + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "themes"); + + /** + * The unique ID for a row. + * <P>Type: INTEGER (long)</P> + */ + public static final String _ID = "_id"; + + /** + * The user visible title. + * <P>Type: TEXT</P> + */ + public static final String TITLE = "title"; + + /** + * Unique text to identify the apk pkg. ie "com.foo.bar" + * <P>Type: TEXT</P> + */ + public static final String PKG_NAME = "pkg_name"; + + /** + * A 32 bit RRGGBB color representative of the themes color scheme + * <P>Type: INTEGER</P> + */ + public static final String PRIMARY_COLOR = "primary_color"; + + /** + * A 2nd 32 bit RRGGBB color representative of the themes color scheme + * <P>Type: INTEGER</P> + */ + public static final String SECONDARY_COLOR = "secondary_color"; + + /** + * Name of the author of the theme + * <P>Type: TEXT</P> + */ + public static final String AUTHOR = "author"; + + /** + * The time that this row was created on its originating client (msecs + * since the epoch). + * <P>Type: INTEGER</P> + */ + public static final String DATE_CREATED = "created"; + + /** + * URI to an image that shows the homescreen with the theme applied + * since the epoch). + * <P>Type: TEXT</P> + */ + public static final String HOMESCREEN_URI = "homescreen_uri"; + + /** + * URI to an image that shows the lockscreen with theme applied + * <P>Type: TEXT</P> + */ + public static final String LOCKSCREEN_URI = "lockscreen_uri"; + + /** + * URI to an image that shows the style (aka skin) with theme applied + * <P>Type: TEXT</P> + */ + public static final String STYLE_URI = "style_uri"; + + /** + * TODO: Figure structure for actual animation instead of static + * URI to an image of the boot_anim. + * <P>Type: TEXT</P> + */ + public static final String BOOT_ANIM_URI = "bootanim_uri"; + + /** + * URI to an image of the status bar for this theme. + * <P>Type: TEXT</P> + */ + public static final String STATUSBAR_URI = "status_uri"; + + /** + * URI to an image of the fonts in this theme. + * <P>Type: TEXT</P> + */ + public static final String FONT_URI = "font_uri"; + + /** + * URI to an image of the fonts in this theme. + * <P>Type: TEXT</P> + */ + public static final String ICON_URI = "icon_uri"; + + /** + * URI to an image of the fonts in this theme. + * <P>Type: TEXT</P> + */ + public static final String OVERLAYS_URI = "overlays_uri"; + + /** + * 1 if theme modifies the launcher/homescreen else 0 + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String MODIFIES_LAUNCHER = "mods_homescreen"; + + /** + * 1 if theme modifies the lockscreen else 0 + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String MODIFIES_LOCKSCREEN = "mods_lockscreen"; + + /** + * 1 if theme modifies icons else 0 + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String MODIFIES_ICONS = "mods_icons"; + + /** + * 1 if theme modifies fonts + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String MODIFIES_FONTS = "mods_fonts"; + + /** + * 1 if theme modifies boot animation + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String MODIFIES_BOOT_ANIM = "mods_bootanim"; + + /** + * 1 if theme modifies notifications + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String MODIFIES_NOTIFICATIONS = "mods_notifications"; + + /** + * 1 if theme modifies alarm sounds + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String MODIFIES_ALARMS = "mods_alarms"; + + /** + * 1 if theme modifies ringtones + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String MODIFIES_RINGTONES = "mods_ringtones"; + + /** + * 1 if theme has overlays + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String MODIFIES_OVERLAYS = "mods_overlays"; + + /** + * 1 if theme has an overlay for SystemUI/StatusBar + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String MODIFIES_STATUS_BAR = "mods_status_bar"; + + /** + * 1 if theme has an overlay for SystemUI/NavBar + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String MODIFIES_NAVIGATION_BAR = "mods_navigation_bar"; + + /** + * URI to the theme's wallpaper. We should support multiple wallpaper + * but for now we will just have 1. + * <P>Type: TEXT</P> + */ + public static final String WALLPAPER_URI = "wallpaper_uri"; + + /** + * 1 if this row should actually be presented as a theme to the user. + * For example if a "theme" only modifies one component (ex icons) then + * we do not present it to the user under the themes table. + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String PRESENT_AS_THEME = "present_as_theme"; + + /** + * 1 if this theme is a legacy theme. + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String IS_LEGACY_THEME = "is_legacy_theme"; + + /** + * 1 if this theme is the system default theme. + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String IS_DEFAULT_THEME = "is_default_theme"; + + /** + * 1 if this theme is a legacy iconpack. A legacy icon pack is an APK that was written + * for Trebuchet or a 3rd party launcher. + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String IS_LEGACY_ICONPACK = "is_legacy_iconpack"; + + /** + * install/update time in millisecs. When the row is inserted this column + * is populated by the PackageInfo. It is used for syncing to PM + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String LAST_UPDATE_TIME = "updateTime"; + + /** + * install time in millisecs. When the row is inserted this column + * is populated by the PackageInfo. + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String INSTALL_TIME = "install_time"; + + /** + * The target API this theme supports + * is populated by the PackageInfo. + * <P>Type: INTEGER</P> + * <P>Default: 0</P> + */ + public static final String TARGET_API = "target_api"; + } + + /** + * Key-value table which assigns a component (ex wallpaper) to a theme's package + */ + public static class MixnMatchColumns { + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "mixnmatch"); + + /** + * The unique key for a row. See the KEY_* constants + * for valid examples + * <P>Type: TEXT</P> + */ + public static final String COL_KEY = "key"; + + /** + * The package name that corresponds to a given component. + * <P>Type: String</P> + */ + public static final String COL_VALUE = "value"; + + /** + * Valid keys + */ + public static final String KEY_HOMESCREEN = "mixnmatch_homescreen"; + public static final String KEY_LOCKSCREEN = "mixnmatch_lockscreen"; + public static final String KEY_ICONS = "mixnmatch_icons"; + public static final String KEY_STATUS_BAR = "mixnmatch_status_bar"; + public static final String KEY_BOOT_ANIM = "mixnmatch_boot_anim"; + public static final String KEY_FONT = "mixnmatch_font"; + public static final String KEY_ALARM = "mixnmatch_alarm"; + public static final String KEY_NOTIFICATIONS = "mixnmatch_notifications"; + public static final String KEY_RINGTONE = "mixnmatch_ringtone"; + public static final String KEY_OVERLAYS = "mixnmatch_overlays"; + public static final String KEY_NAVIGATION_BAR = "mixnmatch_navigation_bar"; + + public static final String[] ROWS = { KEY_HOMESCREEN, + KEY_LOCKSCREEN, + KEY_ICONS, + KEY_STATUS_BAR, + KEY_BOOT_ANIM, + KEY_FONT, + KEY_NOTIFICATIONS, + KEY_RINGTONE, + KEY_ALARM, + KEY_OVERLAYS, + KEY_NAVIGATION_BAR + }; + + /** + * For a given key value in the MixNMatch table, return the column + * associated with it in the Themes Table. This is useful for URI based + * elements like wallpaper where the caller wishes to determine the + * wallpaper URI. + */ + public static String componentToImageColName(String component) { + if (component.equals(MixnMatchColumns.KEY_HOMESCREEN)) { + return ThemesColumns.HOMESCREEN_URI; + } else if (component.equals(MixnMatchColumns.KEY_LOCKSCREEN)) { + return ThemesColumns.LOCKSCREEN_URI; + } else if (component.equals(MixnMatchColumns.KEY_BOOT_ANIM)) { + return ThemesColumns.BOOT_ANIM_URI; + } else if (component.equals(MixnMatchColumns.KEY_FONT)) { + return ThemesColumns.FONT_URI; + } else if (component.equals(MixnMatchColumns.KEY_ICONS)) { + return ThemesColumns.ICON_URI; + } else if (component.equals(MixnMatchColumns.KEY_STATUS_BAR)) { + return ThemesColumns.STATUSBAR_URI; + } else if (component.equals(MixnMatchColumns.KEY_NOTIFICATIONS)) { + throw new IllegalArgumentException("Notifications mixnmatch component does not have a related column"); + } else if (component.equals(MixnMatchColumns.KEY_RINGTONE)) { + throw new IllegalArgumentException("Ringtone mixnmatch component does not have a related column"); + } else if (component.equals(MixnMatchColumns.KEY_OVERLAYS)) { + return ThemesColumns.OVERLAYS_URI; + } else if (component.equals(MixnMatchColumns.KEY_STATUS_BAR)) { + throw new IllegalArgumentException( + "Status bar mixnmatch component does not have a related column"); + } else if (component.equals(MixnMatchColumns.KEY_NAVIGATION_BAR)) { + throw new IllegalArgumentException( + "Navigation bar mixnmatch component does not have a related column"); + } + return null; + } + + /** + * A component in the themes table (IE "mods_wallpaper") has an + * equivalent key in mixnmatch table + */ + public static String componentToMixNMatchKey(String component) { + if (component.equals(ThemesColumns.MODIFIES_LAUNCHER)) { + return MixnMatchColumns.KEY_HOMESCREEN; + } else if (component.equals(ThemesColumns.MODIFIES_ICONS)) { + return MixnMatchColumns.KEY_ICONS; + } else if (component.equals(ThemesColumns.MODIFIES_LOCKSCREEN)) { + return MixnMatchColumns.KEY_LOCKSCREEN; + } else if (component.equals(ThemesColumns.MODIFIES_FONTS)) { + return MixnMatchColumns.KEY_FONT; + } else if (component.equals(ThemesColumns.MODIFIES_BOOT_ANIM)) { + return MixnMatchColumns.KEY_BOOT_ANIM; + } else if (component.equals(ThemesColumns.MODIFIES_ALARMS)) { + return MixnMatchColumns.KEY_ALARM; + } else if (component.equals(ThemesColumns.MODIFIES_NOTIFICATIONS)) { + return MixnMatchColumns.KEY_NOTIFICATIONS; + } else if (component.equals(ThemesColumns.MODIFIES_RINGTONES)) { + return MixnMatchColumns.KEY_RINGTONE; + } else if (component.equals(ThemesColumns.MODIFIES_OVERLAYS)) { + return MixnMatchColumns.KEY_OVERLAYS; + } else if (component.equals(ThemesColumns.MODIFIES_STATUS_BAR)) { + return MixnMatchColumns.KEY_STATUS_BAR; + } else if (component.equals(ThemesColumns.MODIFIES_NAVIGATION_BAR)) { + return MixnMatchColumns.KEY_NAVIGATION_BAR; + } + return null; + } + + /** + * A mixnmatch key in has an + * equivalent value in the themes table + */ + public static String mixNMatchKeyToComponent(String mixnmatchKey) { + if (mixnmatchKey.equals(MixnMatchColumns.KEY_HOMESCREEN)) { + return ThemesColumns.MODIFIES_LAUNCHER; + } else if (mixnmatchKey.equals(MixnMatchColumns.KEY_ICONS)) { + return ThemesColumns.MODIFIES_ICONS; + } else if (mixnmatchKey.equals(MixnMatchColumns.KEY_LOCKSCREEN)) { + return ThemesColumns.MODIFIES_LOCKSCREEN; + } else if (mixnmatchKey.equals(MixnMatchColumns.KEY_FONT)) { + return ThemesColumns.MODIFIES_FONTS; + } else if (mixnmatchKey.equals(MixnMatchColumns.KEY_BOOT_ANIM)) { + return ThemesColumns.MODIFIES_BOOT_ANIM; + } else if (mixnmatchKey.equals(MixnMatchColumns.KEY_ALARM)) { + return ThemesColumns.MODIFIES_ALARMS; + } else if (mixnmatchKey.equals(MixnMatchColumns.KEY_NOTIFICATIONS)) { + return ThemesColumns.MODIFIES_NOTIFICATIONS; + } else if (mixnmatchKey.equals(MixnMatchColumns.KEY_RINGTONE)) { + return ThemesColumns.MODIFIES_RINGTONES; + } else if (mixnmatchKey.equals(MixnMatchColumns.KEY_OVERLAYS)) { + return ThemesColumns.MODIFIES_OVERLAYS; + } else if (mixnmatchKey.equals(MixnMatchColumns.KEY_STATUS_BAR)) { + return ThemesColumns.MODIFIES_STATUS_BAR; + } else if (mixnmatchKey.equals(MixnMatchColumns.KEY_NAVIGATION_BAR)) { + return ThemesColumns.MODIFIES_NAVIGATION_BAR; + } + return null; + } + } + + /** + * Table containing cached preview blobs for a given theme + */ + public static class PreviewColumns { + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "previews"); + + /** + * Uri for retrieving the previews for the currently applied components. + * Querying the themes provider using this URI will return a cursor with a single row + * containing all the previews for the components that are currently applied. + */ + public static final Uri APPLIED_URI = Uri.withAppendedPath(AUTHORITY_URI, + "applied_previews"); + + /** + * The unique ID for a row. + * <P>Type: INTEGER (long)</P> + */ + public static final String _ID = "_id"; + + /** + * The unique ID for the theme these previews belong to. + * <P>Type: INTEGER (long)</P> + */ + public static final String THEME_ID = "theme_id"; + + /** + * Cached image of the themed status bar background. + * <P>Type: BLOB (bitmap)</P> + */ + public static final String STATUSBAR_BACKGROUND = "statusbar_background"; + + /** + * Cached image of the themed bluetooth status icon. + * <P>Type: BLOB (bitmap)</P> + */ + public static final String STATUSBAR_BLUETOOTH_ICON = "statusbar_bluetooth_icon"; + + /** + * Cached image of the themed wifi status icon. + * <P>Type: BLOB (bitmap)</P> + */ + public static final String STATUSBAR_WIFI_ICON = "statusbar_wifi_icon"; + + /** + * Cached image of the themed cellular signal status icon. + * <P>Type: BLOB (bitmap)</P> + */ + public static final String STATUSBAR_SIGNAL_ICON = "statusbar_signal_icon"; + + /** + * Cached image of the themed battery using portrait style. + * <P>Type: BLOB (bitmap)</P> + */ + public static final String STATUSBAR_BATTERY_PORTRAIT = "statusbar_battery_portrait"; + + /** + * Cached image of the themed battery using landscape style. + * <P>Type: BLOB (bitmap)</P> + */ + public static final String STATUSBAR_BATTERY_LANDSCAPE = "statusbar_battery_landscape"; + + /** + * Cached image of the themed battery using circle style. + * <P>Type: BLOB (bitmap)</P> + */ + public static final String STATUSBAR_BATTERY_CIRCLE = "statusbar_battery_circle"; + + /** + * The themed margin value between the wifi and rssi signal icons. + * <P>Type: INTEGER (int)</P> + */ + public static final String STATUSBAR_WIFI_COMBO_MARGIN_END = "wifi_combo_margin_end"; + + /** + * The themed color used for clock text in the status bar. + * <P>Type: INTEGER (int)</P> + */ + public static final String STATUSBAR_CLOCK_TEXT_COLOR = "statusbar_clock_text_color"; + + /** + * Cached image of the themed navigation bar background. + * <P>Type: BLOB (bitmap)</P> + */ + public static final String NAVBAR_BACKGROUND = "navbar_background"; + + /** + * Cached image of the themed back button. + * <P>Type: BLOB (bitmap)</P> + */ + public static final String NAVBAR_BACK_BUTTON = "navbar_back_button"; + + /** + * Cached image of the themed home button. + * <P>Type: BLOB (bitmap)</P> + */ + public static final String NAVBAR_HOME_BUTTON = "navbar_home_button"; + + /** + * Cached image of the themed recents button. + * <P>Type: BLOB (bitmap)</P> + */ + public static final String NAVBAR_RECENT_BUTTON = "navbar_recent_button"; + + /** + * Cached image of the 1/4 icons + * <P>Type: BLOB (bitmap)</P> + */ + public static final String ICON_PREVIEW_1 = "icon_preview_1"; + + /** + * Cached image of the 2/4 icons + * <P>Type: BLOB (bitmap)</P> + */ + public static final String ICON_PREVIEW_2 = "icon_preview_2"; + + /** + * Cached image of the 3/4 icons + * <P>Type: BLOB (bitmap)</P> + */ + public static final String ICON_PREVIEW_3 = "icon_preview_3"; + + /** + * Cached image of the 4/4 icons + * <P>Type: BLOB (bitmap)</P> + */ + public static final String ICON_PREVIEW_4 = "icon_preview_4"; + + /** + * Cached preview of UI controls representing the theme's style + * <P>Type: BLOB (bitmap)</P> + */ + public static final String STYLE_PREVIEW = "style_preview"; + + /** + * Cached thumbnail preview of UI controls representing the theme's style + * <P>Type: BLOB (bitmap)</P> + */ + public static final String STYLE_THUMBNAIL = "style_thumbnail"; + + /** + * Cached thumbnail of the theme's boot animation + * <P>Type: BLOB (bitmap)</P> + */ + public static final String BOOTANIMATION_THUMBNAIL = "bootanimation_thumbnail"; + + /** + * Cached thumbnail of the theme's wallpaper + * <P>Type: BLOB (bitmap)</P> + */ + public static final String WALLPAPER_THUMBNAIL = "wallpaper_thumbnail"; + + /** + * Cached preview of the theme's wallpaper which is larger than the thumbnail + * but smaller than the full sized wallpaper. + * <P>Type: BLOB (bitmap)</P> + */ + public static final String WALLPAPER_PREVIEW = "wallpaper_preview"; + + /** + * Cached thumbnail of the theme's lockscreen wallpaper + * <P>Type: BLOB (bitmap)</P> + */ + public static final String LOCK_WALLPAPER_THUMBNAIL = "lock_wallpaper_thumbnail"; + + /** + * Cached preview of the theme's lockscreen wallpaper which is larger than the thumbnail + * but smaller than the full sized lockscreen wallpaper. + * <P>Type: BLOB (bitmap)</P> + */ + public static final String LOCK_WALLPAPER_PREVIEW = "lock_wallpaper_preview"; + } +} diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 6aa86c7a0be..d5afdf8f421 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -125,7 +125,7 @@ interface IWindowManager boolean inKeyguardRestrictedInputMode(); void dismissKeyguard(); void keyguardGoingAway(boolean disableWindowAnimations, - boolean keyguardGoingToNotificationShade); + boolean keyguardGoingToNotificationShade, boolean keyguardShowingMedia); void closeSystemDialogs(String reason); @@ -223,6 +223,16 @@ interface IWindowManager int maxHeight, boolean force565); /** + * Get the current x offset for the wallpaper + */ + int getLastWallpaperX(); + + /** + * Get the current y offset for the wallpaper + */ + int getLastWallpaperY(); + + /** * Called by the status bar to notify Views of changes to System UI visiblity. */ oneway void statusBarVisibilityChanged(int visibility); diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 037ed281be1..789f8ad8b64 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -182,6 +182,16 @@ interface IWindowSession { */ void setWallpaperDisplayOffset(IBinder windowToken, int x, int y); + /** + * Get the current x offset for the wallpaper + */ + int getLastWallpaperX(); + + /** + * Get the current y offset for the wallpaper + */ + int getLastWallpaperY(); + Bundle sendWallpaperCommand(IBinder window, String action, int x, int y, int z, in Bundle extras, boolean sync); diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 673f075e6c1..a1e0dc775fd 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -758,7 +758,8 @@ public interface WindowManagerPolicy { * Create and return an animation to let the wallpaper disappear after being shown on a force * hiding window. */ - public Animation createForceHideWallpaperExitAnimation(boolean goingToNotificationShade); + public Animation createForceHideWallpaperExitAnimation(boolean goingToNotificationShade, + boolean keyguardShowingMedia); /** * Called from the input reader thread before a key is enqueued. diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 7cb3c378ace..2c0fca1bc9e 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -2516,6 +2516,12 @@ public class RemoteViews implements Parcelable, Filter { /** @hide */ public View apply(Context context, ViewGroup parent, OnClickHandler handler) { + return apply(context, parent, handler, null); + } + + /** @hide */ + public View apply(Context context, ViewGroup parent, OnClickHandler handler, + String themePackageName) { RemoteViews rvToApply = getRemoteViewsToApply(context); View result; @@ -2523,7 +2529,7 @@ public class RemoteViews implements Parcelable, Filter { // user. So build a context that loads resources from that user but // still returns the current users userId so settings like data / time formats // are loaded without requiring cross user persmissions. - final Context contextForResources = getContextForResources(context); + final Context contextForResources = getContextForResources(context, themePackageName); Context inflationContext = new ContextWrapper(context) { @Override public Resources getResources() { @@ -2589,7 +2595,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private Context getContextForResources(Context context) { + private Context getContextForResources(Context context, String themePackageName) { if (mApplication != null) { if (context.getUserId() == UserHandle.getUserId(mApplication.uid) && context.getPackageName().equals(mApplication.packageName)) { diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index 2ef8a20f7d9..272788d362f 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -16,6 +16,7 @@ package com.android.internal.os; +import android.graphics.Typeface; import android.net.Credentials; import android.net.LocalSocket; import android.os.Process; @@ -214,6 +215,10 @@ class ZygoteConnection { ZygoteInit.setCloseOnExec(serverPipeFd, true); } + if (parsedArgs.refreshTheme) { + Typeface.recreateDefaults(); + } + /** * In order to avoid leaking descriptors to the Zygote child, * the native code must close the two Zygote socket descriptors @@ -410,6 +415,9 @@ class ZygoteConnection { */ String appDataDir; + /** from --refresh_theme */ + boolean refreshTheme; + /** * Constructs instance and parses args * @param args zygote command-line args @@ -569,6 +577,8 @@ class ZygoteConnection { instructionSet = arg.substring(arg.indexOf('=') + 1); } else if (arg.startsWith("--app-data-dir=")) { appDataDir = arg.substring(arg.indexOf('=') + 1); + } else if (arg.equals("--refresh_theme")) { + refreshTheme = true; } else { break; } diff --git a/core/java/com/android/internal/util/cm/ImageUtils.java b/core/java/com/android/internal/util/cm/ImageUtils.java new file mode 100644 index 00000000000..d7803840fe9 --- /dev/null +++ b/core/java/com/android/internal/util/cm/ImageUtils.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2013-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 com.android.internal.util.cm; + +import android.app.WallpaperManager; +import android.content.Context; +import android.content.pm.ThemeUtils; +import android.content.res.AssetManager; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Point; +import android.net.Uri; +import android.provider.ThemesContract; +import android.provider.ThemesContract.ThemesColumns; +import android.text.TextUtils; +import android.util.Log; +import android.webkit.URLUtil; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +import libcore.io.IoUtils; + +public class ImageUtils { + private static final String TAG = ImageUtils.class.getSimpleName(); + + private static final String ASSET_URI_PREFIX = "file:///android_asset/"; + private static final int DEFAULT_IMG_QUALITY = 100; + + /** + * Gets the Width and Height of the image + * + * @param inputStream The input stream of the image + * + * @return A point structure that holds the Width and Height (x and y)/*" + */ + public static Point getImageDimension(InputStream inputStream) { + if (inputStream == null) { + throw new IllegalArgumentException("'inputStream' cannot be null!"); + } + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(inputStream, null, options); + Point point = new Point(options.outWidth,options.outHeight); + return point; + } + + /** + * Crops the input image and returns a new InputStream of the cropped area + * + * @param inputStream The input stream of the image + * @param imageWidth Width of the input image + * @param imageHeight Height of the input image + * @param inputStream Desired Width + * @param inputStream Desired Width + * + * @return a new InputStream of the cropped area/*" + */ + public static InputStream cropImage(InputStream inputStream, int imageWidth, int imageHeight, + int outWidth, int outHeight) throws IllegalArgumentException { + if (inputStream == null){ + throw new IllegalArgumentException("inputStream cannot be null"); + } + + if (imageWidth <= 0 || imageHeight <= 0) { + throw new IllegalArgumentException( + String.format("imageWidth and imageHeight must be > 0: imageWidth=%d" + + " imageHeight=%d", imageWidth, imageHeight)); + } + + if (outWidth <= 0 || outHeight <= 0) { + throw new IllegalArgumentException( + String.format("outWidth and outHeight must be > 0: outWidth=%d" + + " outHeight=%d", imageWidth, outHeight)); + } + + int scaleDownSampleSize = Math.min(imageWidth / outWidth, imageHeight / outHeight); + if (scaleDownSampleSize > 0) { + imageWidth /= scaleDownSampleSize; + imageHeight /= scaleDownSampleSize; + } else { + float ratio = (float) outWidth / outHeight; + if (imageWidth < imageHeight * ratio) { + outWidth = imageWidth; + outHeight = (int) (outWidth / ratio); + } else { + outHeight = imageHeight; + outWidth = (int) (outHeight * ratio); + } + } + int left = (imageWidth - outWidth) / 2; + int top = (imageHeight - outHeight) / 2; + InputStream compressed = null; + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + if (scaleDownSampleSize > 1) { + options.inSampleSize = scaleDownSampleSize; + } + Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options); + if (bitmap == null) { + return null; + } + Bitmap cropped = Bitmap.createBitmap(bitmap, left, top, outWidth, outHeight); + ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048); + if (cropped.compress(Bitmap.CompressFormat.PNG, DEFAULT_IMG_QUALITY, tmpOut)) { + byte[] outByteArray = tmpOut.toByteArray(); + compressed = new ByteArrayInputStream(outByteArray); + } + } catch (Exception e) { + Log.e(TAG, "Exception " + e); + } + return compressed; + } + + /** + * Crops the lock screen image and returns a new InputStream of the cropped area + * + * @param pkgName Name of the theme package + * @param context The context + * + * @return a new InputStream of the cropped image/*" + */ + public static InputStream getCroppedKeyguardStream(String pkgName, Context context) + throws IllegalArgumentException { + if (TextUtils.isEmpty(pkgName)) { + throw new IllegalArgumentException("'pkgName' cannot be null or empty!"); + } + if (context == null) { + throw new IllegalArgumentException("'context' cannot be null!"); + } + + InputStream cropped = null; + InputStream stream = null; + try { + stream = getOriginalKeyguardStream(pkgName, context); + if (stream == null) { + return null; + } + Point point = getImageDimension(stream); + IoUtils.closeQuietly(stream); + if (point == null || point.x == 0 || point.y == 0) { + return null; + } + WallpaperManager wm = WallpaperManager.getInstance(context); + int outWidth = wm.getDesiredMinimumWidth(); + int outHeight = wm.getDesiredMinimumHeight(); + stream = getOriginalKeyguardStream(pkgName, context); + if (stream == null) { + return null; + } + cropped = cropImage(stream, point.x, point.y, outWidth, outHeight); + } catch (Exception e) { + Log.e(TAG, "Exception " + e); + } finally { + IoUtils.closeQuietly(stream); + } + return cropped; + } + + /** + * Crops the wallpaper image and returns a new InputStream of the cropped area + * + * @param pkgName Name of the theme package + * @param context The context + * + * @return a new InputStream of the cropped image/*" + */ + public static InputStream getCroppedWallpaperStream(String pkgName, Context context) { + if (TextUtils.isEmpty(pkgName)) { + throw new IllegalArgumentException("'pkgName' cannot be null or empty!"); + } + if (context == null) { + throw new IllegalArgumentException("'context' cannot be null!"); + } + + InputStream cropped = null; + InputStream stream = null; + try { + stream = getOriginalWallpaperStream(pkgName, context); + if (stream == null) { + return null; + } + Point point = getImageDimension(stream); + IoUtils.closeQuietly(stream); + if (point == null || point.x == 0 || point.y == 0) { + return null; + } + WallpaperManager wm = WallpaperManager.getInstance(context); + int outWidth = wm.getDesiredMinimumWidth(); + int outHeight = wm.getDesiredMinimumHeight(); + stream = getOriginalWallpaperStream(pkgName, context); + if (stream == null) { + return null; + } + cropped = cropImage(stream, point.x, point.y, outWidth, outHeight); + } catch (Exception e) { + Log.e(TAG, "Exception " + e); + } finally { + IoUtils.closeQuietly(stream); + } + return cropped; + } + + private static InputStream getOriginalKeyguardStream(String pkgName, Context context) { + if (TextUtils.isEmpty(pkgName) || context == null) { + return null; + } + + InputStream inputStream = null; + try { + //Get input WP stream from the theme + Context themeCtx = context.createPackageContext(pkgName, + Context.CONTEXT_IGNORE_SECURITY); + AssetManager assetManager = themeCtx.getAssets(); + String wpPath = ThemeUtils.getLockscreenWallpaperPath(assetManager); + if (wpPath == null) { + Log.w(TAG, "Not setting lockscreen wp because wallpaper file was not found."); + } else { + inputStream = ThemeUtils.getInputStreamFromAsset(themeCtx, + ASSET_URI_PREFIX + wpPath); + } + } catch (Exception e) { + Log.e(TAG, "There was an error setting lockscreen wp for pkg " + pkgName, e); + } + return inputStream; + } + + private static InputStream getOriginalWallpaperStream(String pkgName, Context context) { + if (TextUtils.isEmpty(pkgName) || context == null) { + return null; + } + + InputStream inputStream = null; + String selection = ThemesContract.ThemesColumns.PKG_NAME + "= ?"; + String[] selectionArgs = {pkgName}; + Cursor c = context.getContentResolver().query(ThemesColumns.CONTENT_URI, + null, selection, + selectionArgs, null); + if (c == null || c.getCount() < 1) { + if (c != null) c.close(); + return null; + } else { + c.moveToFirst(); + } + + try { + Context themeContext = context.createPackageContext(pkgName, + Context.CONTEXT_IGNORE_SECURITY); + boolean isLegacyTheme = c.getInt( + c.getColumnIndex(ThemesColumns.IS_LEGACY_THEME)) == 1; + String wallpaper = c.getString( + c.getColumnIndex(ThemesColumns.WALLPAPER_URI)); + if (wallpaper != null) { + if (URLUtil.isAssetUrl(wallpaper)) { + inputStream = ThemeUtils.getInputStreamFromAsset(themeContext, wallpaper); + } else { + inputStream = context.getContentResolver().openInputStream( + Uri.parse(wallpaper)); + } + } else { + // try and get the wallpaper directly from the apk if the URI was null + Context themeCtx = context.createPackageContext(pkgName, + Context.CONTEXT_IGNORE_SECURITY); + AssetManager assetManager = themeCtx.getAssets(); + String wpPath = ThemeUtils.getWallpaperPath(assetManager); + if (wpPath == null) { + Log.e(TAG, "Not setting wp because wallpaper file was not found."); + } else { + inputStream = ThemeUtils.getInputStreamFromAsset(themeCtx, + ASSET_URI_PREFIX + wpPath); + } + } + } catch (Exception e) { + Log.e(TAG, "getWallpaperStream: " + e); + } finally { + c.close(); + } + + return inputStream; + } +} + diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp index 0dd35ea8e76..386718b233d 100644 --- a/core/jni/android_util_AssetManager.cpp +++ b/core/jni/android_util_AssetManager.cpp @@ -518,24 +518,138 @@ static jint android_content_AssetManager_addAssetPath(JNIEnv* env, jobject clazz } static jint android_content_AssetManager_addOverlayPath(JNIEnv* env, jobject clazz, - jstring idmapPath) + jstring idmapPath, + jstring packagePath, + jstring resApkPath, jstring targetPkgPath, + jstring prefixPath) { ScopedUtfChars idmapPath8(env, idmapPath); if (idmapPath8.c_str() == NULL) { return 0; } + ScopedUtfChars packagePath8(env, packagePath); + if (packagePath8.c_str() == NULL) { + return 0; + } + + ScopedUtfChars resApkPath8(env, resApkPath); + if (resApkPath8.c_str() == NULL) { + return 0; + } + + ScopedUtfChars prefixPath8(env, prefixPath); + if (prefixPath8.c_str() == NULL) { + return 0; + } + + ScopedUtfChars targetPkgPath8(env, targetPkgPath); + if (targetPkgPath8.c_str() == NULL) { + return 0; + } + + AssetManager* am = assetManagerForJavaObject(env, clazz); + if (am == NULL) { + return 0; + } + + int32_t cookie; + bool res = am->addOverlayPath( + String8(idmapPath8.c_str()), + String8(packagePath8.c_str()), + &cookie, + String8(resApkPath8.c_str()), + String8(targetPkgPath8.c_str()), + String8(prefixPath8.c_str())); + + return (res) ? (jint)cookie : 0; +} + +static jint android_content_AssetManager_addCommonOverlayPath(JNIEnv* env, jobject clazz, + jstring packagePath, + jstring resApkPath, jstring prefixPath) +{ + ScopedUtfChars packagePath8(env, packagePath); + if (packagePath8.c_str() == NULL) { + return 0; + } + + ScopedUtfChars resApkPath8(env, resApkPath); + if (resApkPath8.c_str() == NULL) { + return 0; + } + + ScopedUtfChars prefixPath8(env, prefixPath); + if (prefixPath8.c_str() == NULL) { + return 0; + } + + AssetManager* am = assetManagerForJavaObject(env, clazz); + if (am == NULL) { + return 0; + } + + int32_t cookie; + bool res = am->addCommonOverlayPath(String8(packagePath8.c_str()), &cookie, + String8(resApkPath8.c_str()), + String8(prefixPath8.c_str())); + + return (res) ? (jint)cookie : 0; +} + +static jint android_content_AssetManager_addIconPath(JNIEnv* env, jobject clazz, + jstring packagePath, + jstring resApkPath, jstring prefixPath, + jint pkgIdOverride) +{ + ScopedUtfChars packagePath8(env, packagePath); + if (packagePath8.c_str() == NULL) { + return 0; + } + + ScopedUtfChars resApkPath8(env, resApkPath); + if (resApkPath8.c_str() == NULL) { + return 0; + } + + ScopedUtfChars prefixPath8(env, prefixPath); + if (prefixPath8.c_str() == NULL) { + return 0; + } + AssetManager* am = assetManagerForJavaObject(env, clazz); if (am == NULL) { return 0; } int32_t cookie; - bool res = am->addOverlayPath(String8(idmapPath8.c_str()), &cookie); + bool res = am->addIconPath(String8(packagePath8.c_str()), &cookie, + String8(resApkPath8.c_str()), + String8(prefixPath8.c_str()), pkgIdOverride); return (res) ? (jint)cookie : 0; } +static jboolean android_content_AssetManager_removeOverlayPath(JNIEnv* env, jobject clazz, + jstring packageName, jint cookie) +{ + if (packageName == NULL) { + jniThrowException(env, "java/lang/NullPointerException", "packageName"); + return JNI_FALSE; + } + + AssetManager* am = assetManagerForJavaObject(env, clazz); + if (am == NULL) { + return JNI_FALSE; + } + + const char* name8 = env->GetStringUTFChars(packageName, NULL); + bool res = am->removeOverlayPath(String8(name8), cookie); + env->ReleaseStringUTFChars(packageName, name8); + + return res; +} + static jboolean android_content_AssetManager_isUpToDate(JNIEnv* env, jobject clazz) { AssetManager* am = assetManagerForJavaObject(env, clazz); @@ -1077,7 +1191,7 @@ static jboolean android_content_AssetManager_resolveAttrs(JNIEnv* env, jobject c const ResTable::bag_entry* defStyleEnt = NULL; uint32_t defStyleTypeSetFlags = 0; ssize_t bagOff = defStyleRes != 0 - ? res.getBagLocked(defStyleRes, &defStyleEnt, &defStyleTypeSetFlags) : -1; + ? res.getBagLocked(defStyleRes, &defStyleEnt, &defStyleTypeSetFlags, true) : -1; defStyleTypeSetFlags |= defStyleBagTypeSetFlags; const ResTable::bag_entry* endDefStyleEnt = defStyleEnt + (bagOff >= 0 ? bagOff : 0);; @@ -1286,7 +1400,7 @@ static jboolean android_content_AssetManager_applyStyle(JNIEnv* env, jobject cla const ResTable::bag_entry* defStyleEnt = NULL; uint32_t defStyleTypeSetFlags = 0; ssize_t bagOff = defStyleRes != 0 - ? res.getBagLocked(defStyleRes, &defStyleEnt, &defStyleTypeSetFlags) : -1; + ? res.getBagLocked(defStyleRes, &defStyleEnt, &defStyleTypeSetFlags, true) : -1; defStyleTypeSetFlags |= defStyleBagTypeSetFlags; const ResTable::bag_entry* endDefStyleEnt = defStyleEnt + (bagOff >= 0 ? bagOff : 0); @@ -1294,7 +1408,7 @@ static jboolean android_content_AssetManager_applyStyle(JNIEnv* env, jobject cla // Retrieve the style class bag, if requested. const ResTable::bag_entry* styleEnt = NULL; uint32_t styleTypeSetFlags = 0; - bagOff = style != 0 ? res.getBagLocked(style, &styleEnt, &styleTypeSetFlags) : -1; + bagOff = style != 0 ? res.getBagLocked(style, &styleEnt, &styleTypeSetFlags, true) : -1; styleTypeSetFlags |= styleBagTypeSetFlags; const ResTable::bag_entry* endStyleEnt = styleEnt + (bagOff >= 0 ? bagOff : 0); @@ -1631,7 +1745,7 @@ static jint android_content_AssetManager_retrieveArray(JNIEnv* env, jobject claz const ResTable::bag_entry* arrayEnt = NULL; uint32_t arrayTypeSetFlags = 0; - ssize_t bagOff = res.getBagLocked(id, &arrayEnt, &arrayTypeSetFlags); + ssize_t bagOff = res.getBagLocked(id, &arrayEnt, &arrayTypeSetFlags, true); const ResTable::bag_entry* endArrayEnt = arrayEnt + (bagOff >= 0 ? bagOff : 0); @@ -1968,6 +2082,50 @@ static jint android_content_AssetManager_getGlobalAssetManagerCount(JNIEnv* env, return AssetManager::getGlobalCount(); } +static jint android_content_AssetManager_getBasePackageCount(JNIEnv* env, jobject clazz) +{ + AssetManager* am = assetManagerForJavaObject(env, clazz); + if (am == NULL) { + return JNI_FALSE; + } + + return am->getResources().getBasePackageCount(); +} + +static jstring android_content_AssetManager_getBasePackageName(JNIEnv* env, jobject clazz, + jint index) +{ + AssetManager* am = assetManagerForJavaObject(env, clazz); + if (am == NULL) { + return JNI_FALSE; + } + + String16 packageName(am->getBasePackageName(index)); + return env->NewString((const jchar*)packageName.string(), packageName.size()); +} + +static jstring android_content_AssetManager_getBaseResourcePackageName(JNIEnv* env, jobject clazz, + jint index) +{ + AssetManager* am = assetManagerForJavaObject(env, clazz); + if (am == NULL) { + return JNI_FALSE; + } + + String16 packageName(am->getResources().getBasePackageName(index)); + return env->NewString((const jchar*)packageName.string(), packageName.size()); +} + +static jint android_content_AssetManager_getBasePackageId(JNIEnv* env, jobject clazz, jint index) +{ + AssetManager* am = assetManagerForJavaObject(env, clazz); + if (am == NULL) { + return JNI_FALSE; + } + + return am->getResources().getBasePackageId(index); +} + // ---------------------------------------------------------------------------- /* @@ -2001,8 +2159,22 @@ static JNINativeMethod gAssetManagerMethods[] = { (void*) android_content_AssetManager_getAssetRemainingLength }, { "addAssetPathNative", "(Ljava/lang/String;)I", (void*) android_content_AssetManager_addAssetPath }, - { "addOverlayPathNative", "(Ljava/lang/String;)I", + { "addOverlayPathNative", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", (void*) android_content_AssetManager_addOverlayPath }, + { "addCommonOverlayPathNative", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", + (void*) android_content_AssetManager_addCommonOverlayPath }, + { "addIconPathNative", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)I", + (void*) android_content_AssetManager_addIconPath }, + { "removeOverlayPathNative", "(Ljava/lang/String;I)Z", + (void*) android_content_AssetManager_removeOverlayPath }, + { "getBasePackageCount", "()I", + (void*) android_content_AssetManager_getBasePackageCount }, + { "getBasePackageName", "(I)Ljava/lang/String;", + (void*) android_content_AssetManager_getBasePackageName }, + { "getBaseResourcePackageName", "(I)Ljava/lang/String;", + (void*) android_content_AssetManager_getBaseResourcePackageName }, + { "getBasePackageId", "(I)I", + (void*) android_content_AssetManager_getBasePackageId }, { "isUpToDate", "()Z", (void*) android_content_AssetManager_isUpToDate }, diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 9069fbb8d15..ce22eccd649 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -291,6 +291,8 @@ <protected-broadcast android:name="android.nfc.handover.intent.action.TRANSFER_PROGRESS" /> <protected-broadcast android:name="android.nfc.handover.intent.action.TRANSFER_DONE" /> + <protected-broadcast android:name="org.cyanogenmod.intent.action.THEME_CHANGED" /> + <protected-broadcast android:name="android.intent.action.THEME_RESOURCES_CACHED" /> <!-- ====================================== --> <!-- Permissions for things that cost money --> @@ -1539,6 +1541,14 @@ android:label="@string/permlab_setWallpaper" android:description="@string/permdesc_setWallpaper" /> + <!-- Allows applications to set the keyguard wallpaper + @hide --> + <permission android:name="android.permission.SET_KEYGUARD_WALLPAPER" + android:permissionGroup="android.permission-group.WALLPAPER" + android:protectionLevel="normal" + android:label="@string/permlab_setKeyguardWallpaper" + android:description="@string/permdesc_setKeyguardWallpaper" /> + <!-- Allows applications to set the wallpaper hints --> <permission android:name="android.permission.SET_WALLPAPER_HINTS" android:permissionGroup="android.permission-group.WALLPAPER" @@ -2877,6 +2887,30 @@ android:label="@string/permlab_changePhoneBlacklist" android:description="@string/permdesc_changePhoneBlacklist" /> + <!-- Allows an application to use the Theme Service + @hide --> + <permission android:name="android.permission.ACCESS_THEME_MANAGER" + android:label="@string/permlab_accessThemeService" + android:description="@string/permdesc_accessThemeService" + android:protectionLevel="signature" /> + + <!-- Allows an application to read the current theme configuration and + get information about the various themes currently installed + @hide --> + <permission android:name="android.permission.READ_THEMES" + android:label="@string/permlab_readThemes" + android:description="@string/permdesc_readThemesDesc" + android:protectionLevel="normal" /> + + <!-- Allows an application to write the current theme configuration and + write information about the various themes currently installed. + Changing themes should be done through the service ACCESS_THEME_MANAGER + @hide --> + <permission android:name="android.permission.WRITE_THEMES" + android:label="@string/permlab_writeThemes" + android:description="@string/permdesc_writeThemesDesc" + android:protectionLevel="system|signature" /> + <!-- The system process is explicitly the only one allowed to launch the confirmation UI for full backup/restore --> <uses-permission android:name="android.permission.CONFIRM_FULL_BACKUP"/> @@ -3074,6 +3108,18 @@ </intent-filter> </receiver> + <receiver android:name="com.android.server.AppsFailureReceiver" > + <intent-filter> + <action android:name="com.tmobile.intent.action.APP_FAILURE" /> + <action android:name="com.tmobile.intent.action.APP_FAILURE_RESET" /> + <action android:name="android.intent.action.PACKAGE_ADDED" /> + <action android:name="android.intent.action.PACKAGE_REMOVED" /> + <action android:name="org.cyanogenmod.intent.action.THEME_CHANGED" /> + <category android:name="com.tmobile.intent.category.THEME_PACKAGE_INSTALL_STATE_CHANGE" /> + <data android:scheme="package" /> + </intent-filter> + </receiver> + <service android:name="com.android.internal.os.storage.ExternalStorageFormatter" android:permission="android.permission.MASTER_CLEAR" android:exported="true" /> diff --git a/core/res/res/anim/lock_screen_wallpaper_exit_noop.xml b/core/res/res/anim/lock_screen_wallpaper_exit_noop.xml new file mode 100644 index 00000000000..4cc5c70aae3 --- /dev/null +++ b/core/res/res/anim/lock_screen_wallpaper_exit_noop.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:shareInterpolator="false" android:startOffset="100"> + <alpha + android:fromAlpha="0.0" android:toAlpha="0.0" + android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" + android:interpolator="@interpolator/fast_out_linear_in" + android:duration="150"/> + + <!-- Empty animation so the animation has same duration as lock_screen_behind_enter animation + --> + <translate android:fromYDelta="0" android:toYDelta="0" + android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" + android:interpolator="@interpolator/linear" + android:duration="300" /> +</set>
\ No newline at end of file diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 10c2518ec19..73887a49d7e 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -745,6 +745,8 @@ <flag name="smallestScreenSize" value="0x0800" /> <!-- The layout direction has changed. For example going from LTR to RTL. --> <flag name="layoutDirection" value="0x2000" /> + <!-- Theme has changed --> + <flag name="themeChange" value="0x8000" /> <!-- The font scaling factor has changed, that is the user has selected a new global font size. --> <flag name="fontScale" value="0x40000000" /> diff --git a/core/res/res/values/cm_strings.xml b/core/res/res/values/cm_strings.xml index 9ba01c5b90a..a2d16c0fa65 100755 --- a/core/res/res/values/cm_strings.xml +++ b/core/res/res/values/cm_strings.xml @@ -49,6 +49,23 @@ <!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. --> <string name="permgroupdesc_security">Permissions related to device security information.</string> + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_accessThemeService">access theme service</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_accessThemeService">Allows an app to access the theme service. Should never be needed for normal apps.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_readThemes">read your theme info</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_readThemesDesc">Allows the app to read your themes and + determine which theme you have applied.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_writeThemes">modify your themes</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_writeThemesDesc">Allows the app to insert new themes and + modify which theme you have applied.</string> + <!-- [CHAR LIMIT=NONE] Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permlab_readPhoneBlacklist">read phone blacklist</string> <!-- [CHAR LIMIT=NONE] Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> @@ -58,4 +75,14 @@ <string name="permlab_changePhoneBlacklist">change phone blacklist</string> <!-- [CHAR LIMIT=NONE] Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permdesc_changePhoneBlacklist">Allows an app to change the phone numbers that are blocked for incoming calls or messages.</string> + <!-- Title of an application permission, listed so the user can choose whether they want the application to do this. --> + <string name="permlab_setKeyguardWallpaper">set keyguard wallpaper</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_setKeyguardWallpaper">Allows an app to change the lock screen wallpaper.</string> + + <!-- Theme installation error notification --> + <string name="theme_install_error_title">Failed to install theme</string> + <string name="theme_install_error_message">%s failed to install</string> + <string name="theme_reset_notification_title">Theme reset</string> + <string name="theme_reset_notification_body">System theme restored due to multiple app crashes.</string> </resources> diff --git a/core/res/res/values/cm_symbols.xml b/core/res/res/values/cm_symbols.xml new file mode 100644 index 00000000000..1e5c2003cd4 --- /dev/null +++ b/core/res/res/values/cm_symbols.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* Copyright 2012, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> +<resources> + <!-- We don't want to publish private symbols in android.R as part of the + SDK. Instead, put them here. --> + <private-symbols package="com.android.internal" /> + + <!-- Theme install failure notification --> + <java-symbol type="string" name="theme_install_error_title" /> + <java-symbol type="string" name="theme_install_error_message" /> + + <!-- Theme reset notification --> + <java-symbol type="string" name="theme_reset_notification_title" /> + <java-symbol type="string" name="theme_reset_notification_body" /> +</resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index f135a75a2cf..348874b6935 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1870,6 +1870,7 @@ <java-symbol type="anim" name="lock_screen_behind_enter_wallpaper" /> <java-symbol type="anim" name="lock_screen_behind_enter_fade_in" /> <java-symbol type="anim" name="lock_screen_wallpaper_exit" /> + <java-symbol type="anim" name="lock_screen_wallpaper_exit_noop" /> <java-symbol type="bool" name="config_alwaysUseCdmaRssi" /> <java-symbol type="dimen" name="status_bar_icon_size" /> |