aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/cm_current.txt128
-rw-r--r--cm/lib/main/java/org/cyanogenmod/platform/internal/AppsFailureReceiver.java120
-rw-r--r--cm/lib/main/java/org/cyanogenmod/platform/internal/IconCacheManagerService.java109
-rw-r--r--cm/lib/main/java/org/cyanogenmod/platform/internal/ThemeManagerService.java1246
-rw-r--r--cm/res/AndroidManifest.xml21
-rw-r--r--cm/res/res/values/strings.xml25
-rw-r--r--cm/res/res/values/symbols.xml9
-rw-r--r--src/java/cyanogenmod/app/CMContextConstants.java14
-rw-r--r--src/java/cyanogenmod/content/Intent.java46
-rw-r--r--src/java/cyanogenmod/providers/ThemesContract.java717
-rw-r--r--src/java/cyanogenmod/themes/IThemeChangeListener.aidl23
-rw-r--r--src/java/cyanogenmod/themes/IThemeProcessingListener.aidl22
-rw-r--r--src/java/cyanogenmod/themes/IThemeService.aidl44
-rw-r--r--src/java/cyanogenmod/themes/ThemeChangeRequest.aidl19
-rw-r--r--src/java/cyanogenmod/themes/ThemeChangeRequest.java329
-rw-r--r--src/java/cyanogenmod/themes/ThemeManager.java383
-rw-r--r--src/java/org/cyanogenmod/internal/themes/IIconCacheManager.aidl24
-rw-r--r--src/java/org/cyanogenmod/internal/util/ImageUtils.java332
-rw-r--r--src/java/org/cyanogenmod/internal/util/ThemeUtils.java687
-rw-r--r--system-api/cm_system-current.txt127
20 files changed, 4425 insertions, 0 deletions
diff --git a/api/cm_current.txt b/api/cm_current.txt
index cced235..f078de0 100644
--- a/api/cm_current.txt
+++ b/api/cm_current.txt
@@ -371,8 +371,13 @@ package cyanogenmod.content {
ctor public Intent();
field public static final java.lang.String ACTION_PROTECTED = "cyanogenmod.intent.action.PACKAGE_PROTECTED";
field public static final java.lang.String ACTION_PROTECTED_CHANGED = "cyanogenmod.intent.action.PROTECTED_COMPONENT_UPDATE";
+ field public static final java.lang.String ACTION_THEME_INSTALLED = "cyanogenmod.intent.action.THEME_INSTALLED";
+ field public static final java.lang.String ACTION_THEME_REMOVED = "cyanogenmod.intent.action.THEME_REMOVED";
+ field public static final java.lang.String ACTION_THEME_UPDATED = "cyanogenmod.intent.action.THEME_UPDATED";
+ field public static final java.lang.String CATEGORY_THEME_PACKAGE_INSTALLED_STATE_CHANGE = "cyanogenmod.intent.category.THEME_PACKAGE_INSTALL_STATE_CHANGE";
field public static final java.lang.String EXTRA_PROTECTED_COMPONENTS = "cyanogenmod.intent.extra.PACKAGE_PROTECTED_COMPONENTS";
field public static final java.lang.String EXTRA_PROTECTED_STATE = "cyanogenmod.intent.extra.PACKAGE_PROTECTED_STATE";
+ field public static final java.lang.String URI_SCHEME_PACKAGE = "package";
}
}
@@ -556,6 +561,7 @@ package cyanogenmod.platform {
public static final class Manifest.permission {
ctor public Manifest.permission();
field public static final java.lang.String ACCESS_APP_SUGGESTIONS = "cyanogenmod.permission.ACCESS_APP_SUGGESTIONS";
+ field public static final java.lang.String ACCESS_THEME_MANAGER = "cyanogenmod.permission.ACCESS_THEME_MANAGER";
field public static final java.lang.String HARDWARE_ABSTRACTION_ACCESS = "cyanogenmod.permission.HARDWARE_ABSTRACTION_ACCESS";
field public static final java.lang.String MANAGE_ALARMS = "cyanogenmod.permission.MANAGE_ALARMS";
field public static final java.lang.String MANAGE_PERSISTENT_STORAGE = "cyanogenmod.permission.MANAGE_PERSISTENT_STORAGE";
@@ -567,10 +573,12 @@ package cyanogenmod.platform {
field public static final java.lang.String PUBLISH_CUSTOM_TILE = "cyanogenmod.permission.PUBLISH_CUSTOM_TILE";
field public static final java.lang.String READ_ALARMS = "cyanogenmod.permission.READ_ALARMS";
field public static final java.lang.String READ_MSIM_PHONE_STATE = "cyanogenmod.permission.READ_MSIM_PHONE_STATE";
+ field public static final java.lang.String READ_THEMES = "cyanogenmod.permission.READ_THEMES";
field public static final java.lang.String THIRD_PARTY_KEYGUARD = "android.permission.THIRD_PARTY_KEYGUARD";
field public static final java.lang.String WRITE_ALARMS = "cyanogenmod.permission.WRITE_ALARMS";
field public static final java.lang.String WRITE_SECURE_SETTINGS = "cyanogenmod.permission.WRITE_SECURE_SETTINGS";
field public static final java.lang.String WRITE_SETTINGS = "cyanogenmod.permission.WRITE_SETTINGS";
+ field public static final java.lang.String WRITE_THEMES = "cyanogenmod.permission.WRITE_THEMES";
}
public final class R {
@@ -878,6 +886,126 @@ package cyanogenmod.providers {
field public static final java.lang.String ZEN_PRIORITY_ALLOW_LIGHTS = "zen_priority_allow_lights";
}
+ public class ThemesContract {
+ ctor public ThemesContract();
+ field public static final java.lang.String AUTHORITY = "com.cyanogenmod.themes";
+ field public static final android.net.Uri AUTHORITY_URI;
+ }
+
+ public static class ThemesContract.MixnMatchColumns {
+ ctor public ThemesContract.MixnMatchColumns();
+ method public static java.lang.String componentToImageColName(java.lang.String);
+ method public static java.lang.String componentToMixNMatchKey(java.lang.String);
+ method public static java.lang.String mixNMatchKeyToComponent(java.lang.String);
+ field public static final java.lang.String COL_COMPONENT_ID = "component_id";
+ field public static final java.lang.String COL_KEY = "key";
+ field public static final java.lang.String COL_PREV_VALUE = "previous_value";
+ field public static final java.lang.String COL_UPDATE_TIME = "update_time";
+ field public static final java.lang.String COL_VALUE = "value";
+ field public static final android.net.Uri CONTENT_URI;
+ field public static final java.lang.String KEY_ALARM = "mixnmatch_alarm";
+ field public static final java.lang.String KEY_BOOT_ANIM = "mixnmatch_boot_anim";
+ field public static final java.lang.String KEY_FONT = "mixnmatch_font";
+ field public static final java.lang.String KEY_HOMESCREEN = "mixnmatch_homescreen";
+ field public static final java.lang.String KEY_ICONS = "mixnmatch_icons";
+ field public static final java.lang.String KEY_LIVE_LOCK_SCREEN = "mixnmatch_live_lock_screen";
+ field public static final java.lang.String KEY_LOCKSCREEN = "mixnmatch_lockscreen";
+ field public static final java.lang.String KEY_NAVIGATION_BAR = "mixnmatch_navigation_bar";
+ field public static final java.lang.String KEY_NOTIFICATIONS = "mixnmatch_notifications";
+ field public static final java.lang.String KEY_OVERLAYS = "mixnmatch_overlays";
+ field public static final java.lang.String KEY_RINGTONE = "mixnmatch_ringtone";
+ field public static final java.lang.String KEY_STATUS_BAR = "mixnmatch_status_bar";
+ field public static final java.lang.String[] ROWS;
+ }
+
+ public static class ThemesContract.PreviewColumns {
+ ctor public ThemesContract.PreviewColumns();
+ field public static final android.net.Uri APPLIED_URI;
+ field public static final java.lang.String BOOTANIMATION_THUMBNAIL = "bootanimation_thumbnail";
+ field public static final java.lang.String COL_KEY = "key";
+ field public static final java.lang.String COL_VALUE = "value";
+ field public static final android.net.Uri COMPONENTS_URI;
+ field public static final java.lang.String COMPONENT_ID = "component_id";
+ field public static final android.net.Uri CONTENT_URI;
+ field public static final java.lang.String ICON_PREVIEW_1 = "icon_preview_1";
+ field public static final java.lang.String ICON_PREVIEW_2 = "icon_preview_2";
+ field public static final java.lang.String ICON_PREVIEW_3 = "icon_preview_3";
+ field public static final java.lang.String LIVE_LOCK_SCREEN_PREVIEW = "live_lock_screen_preview";
+ field public static final java.lang.String LIVE_LOCK_SCREEN_THUMBNAIL = "live_lock_screen_thumbnail";
+ field public static final java.lang.String LOCK_WALLPAPER_PREVIEW = "lock_wallpaper_preview";
+ field public static final java.lang.String LOCK_WALLPAPER_THUMBNAIL = "lock_wallpaper_thumbnail";
+ field public static final java.lang.String NAVBAR_BACKGROUND = "navbar_background";
+ field public static final java.lang.String NAVBAR_BACK_BUTTON = "navbar_back_button";
+ field public static final java.lang.String NAVBAR_HOME_BUTTON = "navbar_home_button";
+ field public static final java.lang.String NAVBAR_RECENT_BUTTON = "navbar_recent_button";
+ field public static final java.lang.String STATUSBAR_BACKGROUND = "statusbar_background";
+ field public static final java.lang.String STATUSBAR_BATTERY_CIRCLE = "statusbar_battery_circle";
+ field public static final java.lang.String STATUSBAR_BATTERY_LANDSCAPE = "statusbar_battery_landscape";
+ field public static final java.lang.String STATUSBAR_BATTERY_PORTRAIT = "statusbar_battery_portrait";
+ field public static final java.lang.String STATUSBAR_BLUETOOTH_ICON = "statusbar_bluetooth_icon";
+ field public static final java.lang.String STATUSBAR_CLOCK_TEXT_COLOR = "statusbar_clock_text_color";
+ field public static final java.lang.String STATUSBAR_SIGNAL_ICON = "statusbar_signal_icon";
+ field public static final java.lang.String STATUSBAR_WIFI_COMBO_MARGIN_END = "wifi_combo_margin_end";
+ field public static final java.lang.String STATUSBAR_WIFI_ICON = "statusbar_wifi_icon";
+ field public static final java.lang.String STYLE_PREVIEW = "style_preview";
+ field public static final java.lang.String STYLE_THUMBNAIL = "style_thumbnail";
+ field public static final java.lang.String THEME_ID = "theme_id";
+ field public static final java.lang.String[] VALID_KEYS;
+ field public static final java.lang.String WALLPAPER_FULL = "wallpaper_full";
+ field public static final java.lang.String WALLPAPER_PREVIEW = "wallpaper_preview";
+ field public static final java.lang.String WALLPAPER_THUMBNAIL = "wallpaper_thumbnail";
+ field public static final java.lang.String _ID = "_id";
+ }
+
+ public static class ThemesContract.ThemesColumns {
+ ctor public ThemesContract.ThemesColumns();
+ field public static final java.lang.String AUTHOR = "author";
+ field public static final java.lang.String BOOT_ANIM_URI = "bootanim_uri";
+ field public static final android.net.Uri CONTENT_URI;
+ field public static final java.lang.String DATE_CREATED = "created";
+ field public static final java.lang.String FONT_URI = "font_uri";
+ field public static final java.lang.String HOMESCREEN_URI = "homescreen_uri";
+ field public static final java.lang.String ICON_URI = "icon_uri";
+ field public static final java.lang.String INSTALL_STATE = "install_state";
+ field public static final java.lang.String INSTALL_TIME = "install_time";
+ field public static final java.lang.String IS_DEFAULT_THEME = "is_default_theme";
+ field public static final java.lang.String IS_LEGACY_ICONPACK = "is_legacy_iconpack";
+ field public static final java.lang.String IS_LEGACY_THEME = "is_legacy_theme";
+ field public static final java.lang.String LAST_UPDATE_TIME = "updateTime";
+ field public static final java.lang.String LOCKSCREEN_URI = "lockscreen_uri";
+ field public static final java.lang.String MODIFIES_ALARMS = "mods_alarms";
+ field public static final java.lang.String MODIFIES_BOOT_ANIM = "mods_bootanim";
+ field public static final java.lang.String MODIFIES_FONTS = "mods_fonts";
+ field public static final java.lang.String MODIFIES_ICONS = "mods_icons";
+ field public static final java.lang.String MODIFIES_LAUNCHER = "mods_homescreen";
+ field public static final java.lang.String MODIFIES_LIVE_LOCK_SCREEN = "mods_live_lock_screen";
+ field public static final java.lang.String MODIFIES_LOCKSCREEN = "mods_lockscreen";
+ field public static final java.lang.String MODIFIES_NAVIGATION_BAR = "mods_navigation_bar";
+ field public static final java.lang.String MODIFIES_NOTIFICATIONS = "mods_notifications";
+ field public static final java.lang.String MODIFIES_OVERLAYS = "mods_overlays";
+ field public static final java.lang.String MODIFIES_RINGTONES = "mods_ringtones";
+ field public static final java.lang.String MODIFIES_STATUS_BAR = "mods_status_bar";
+ field public static final java.lang.String OVERLAYS_URI = "overlays_uri";
+ field public static final java.lang.String PKG_NAME = "pkg_name";
+ field public static final java.lang.String PRESENT_AS_THEME = "present_as_theme";
+ field public static final java.lang.String PRIMARY_COLOR = "primary_color";
+ field public static final java.lang.String SECONDARY_COLOR = "secondary_color";
+ field public static final java.lang.String STATUSBAR_URI = "status_uri";
+ field public static final java.lang.String STYLE_URI = "style_uri";
+ field public static final java.lang.String TARGET_API = "target_api";
+ field public static final java.lang.String TITLE = "title";
+ field public static final java.lang.String WALLPAPER_URI = "wallpaper_uri";
+ field public static final java.lang.String _ID = "_id";
+ }
+
+ public static class ThemesContract.ThemesColumns.InstallState {
+ ctor public ThemesContract.ThemesColumns.InstallState();
+ field public static final int INSTALLED = 3; // 0x3
+ field public static final int INSTALLING = 1; // 0x1
+ field public static final int UNKNOWN = 0; // 0x0
+ field public static final int UPDATING = 2; // 0x2
+ }
+
}
package cyanogenmod.util {
diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/AppsFailureReceiver.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/AppsFailureReceiver.java
new file mode 100644
index 0000000..e199d2a
--- /dev/null
+++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/AppsFailureReceiver.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2010, T-Mobile USA, Inc.
+ * Copyright (C) 2015-2016 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 org.cyanogenmod.platform.internal;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.ThemeConfig;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+
+import org.cyanogenmod.internal.util.ThemeUtils;
+
+import cyanogenmod.app.CMContextConstants;
+import cyanogenmod.themes.IThemeService;
+import cyanogenmod.themes.ThemeChangeRequest;
+import cyanogenmod.themes.ThemeChangeRequest.RequestType;
+
+import static cyanogenmod.content.Intent.ACTION_APP_FAILURE;
+
+public class AppsFailureReceiver extends BroadcastReceiver {
+
+ private static final int FAILURES_THRESHOLD = 3;
+ private static final int EXPIRATION_TIME_IN_MILLISECONDS = 30000; // 30 seconds
+
+ private int mFailuresCount = 0;
+ private long mStartTime = 0;
+
+ // This function implements the following logic.
+ // If after a theme was applied the number of application launch failures
+ // at any moment was equal to FAILURES_THRESHOLD
+ // in less than EXPIRATION_TIME_IN_MILLISECONDS
+ // the default theme is applied unconditionally.
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ final long currentTime = SystemClock.uptimeMillis();
+ if (ACTION_APP_FAILURE.equals(action)) {
+ if (currentTime - mStartTime > EXPIRATION_TIME_IN_MILLISECONDS) {
+ // reset both the count and the timer
+ mStartTime = currentTime;
+ mFailuresCount = 0;
+ }
+ if (mFailuresCount <= FAILURES_THRESHOLD) {
+ mFailuresCount++;
+ if (mFailuresCount == FAILURES_THRESHOLD) {
+ // let the theme manager take care of getting us back on the default theme
+ IThemeService tm = IThemeService.Stub.asInterface(ServiceManager
+ .getService(CMContextConstants.CM_THEME_SERVICE));
+ final String themePkgName = ThemeConfig.SYSTEM_DEFAULT;
+ ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder();
+ builder.setOverlay(themePkgName)
+ .setStatusBar(themePkgName)
+ .setNavBar(themePkgName)
+ .setIcons(themePkgName)
+ .setFont(themePkgName)
+ .setBootanimation(themePkgName)
+ .setWallpaper(themePkgName)
+ .setLockWallpaper(themePkgName)
+ .setAlarm(themePkgName)
+ .setNotification(themePkgName)
+ .setRingtone(themePkgName)
+ .setRequestType(RequestType.THEME_RESET);
+ // Since we are resetting everything to the system theme, we can have the
+ // theme service remove all per app themes without setting them explicitly :)
+ try {
+ tm.requestThemeChange(builder.build(), true);
+ postThemeResetNotification(context);
+ } catch (RemoteException e) {
+ /* ignore */
+ }
+ }
+ }
+ } else if (ThemeUtils.ACTION_THEME_CHANGED.equals(action)) {
+ // reset both the count and the timer
+ mStartTime = currentTime;
+ mFailuresCount = 0;
+ }
+ }
+
+ /**
+ * Posts a notification to let the user know their theme was reset
+ * @param context
+ */
+ private void postThemeResetNotification(Context context) {
+ NotificationManager nm =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ String title = context.getString(R.string.theme_reset_notification_title);
+ String body = context.getString(R.string.theme_reset_notification_message);
+ Notification notice = new Notification.Builder(context)
+ .setAutoCancel(true)
+ .setOngoing(false)
+ .setContentTitle(title)
+ .setContentText(body)
+ .setStyle(new Notification.BigTextStyle().bigText(body))
+ .setSmallIcon(android.R.drawable.stat_notify_error)
+ .setWhen(System.currentTimeMillis())
+ .setCategory(Notification.CATEGORY_SYSTEM)
+ .setPriority(Notification.PRIORITY_MAX)
+ .build();
+ nm.notify(R.string.theme_reset_notification_title, notice);
+ }
+}
diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/IconCacheManagerService.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/IconCacheManagerService.java
new file mode 100644
index 0000000..56be660
--- /dev/null
+++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/IconCacheManagerService.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016 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 org.cyanogenmod.platform.internal;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Binder;
+import android.os.FileUtils;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import com.android.server.SystemService;
+import cyanogenmod.app.CMContextConstants;
+
+import org.cyanogenmod.internal.themes.IIconCacheManager;
+import org.cyanogenmod.internal.util.ThemeUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.Arrays;
+import java.util.Comparator;
+
+/** @hide */
+public class IconCacheManagerService extends SystemService {
+ private static final String TAG = IconCacheManagerService.class.getSimpleName();
+
+ private static final long MAX_ICON_CACHE_SIZE = 33554432L; // 32MB
+ private static final long PURGED_ICON_CACHE_SIZE = 25165824L; // 24 MB
+
+ private long mIconCacheSize = 0L;
+
+ public IconCacheManagerService(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {
+ Log.d(TAG, "registerIconCache cmiconcache: " + this);
+ publishBinderService(CMContextConstants.CM_ICON_CACHE_SERVICE, mService);
+ }
+
+ private void purgeIconCache() {
+ Log.d(TAG, "Purging icon cahe of size " + mIconCacheSize);
+ File cacheDir = new File(ThemeUtils.SYSTEM_THEME_ICON_CACHE_DIR);
+ File[] files = cacheDir.listFiles();
+ Arrays.sort(files, mOldestFilesFirstComparator);
+ for (File f : files) {
+ if (!f.isDirectory()) {
+ final long size = f.length();
+ if(f.delete()) mIconCacheSize -= size;
+ }
+ if (mIconCacheSize <= PURGED_ICON_CACHE_SIZE) break;
+ }
+ }
+
+ private Comparator<File> mOldestFilesFirstComparator = new Comparator<File>() {
+ @Override
+ public int compare(File lhs, File rhs) {
+ return (int) (lhs.lastModified() - rhs.lastModified());
+ }
+ };
+
+ private IBinder mService = new IIconCacheManager.Stub() {
+ @Override
+ public boolean cacheComposedIcon(Bitmap icon, String fileName) throws RemoteException {
+ final long token = Binder.clearCallingIdentity();
+ boolean success;
+ FileOutputStream os;
+ final File cacheDir = new File(ThemeUtils.SYSTEM_THEME_ICON_CACHE_DIR);
+ if (cacheDir.listFiles().length == 0) {
+ mIconCacheSize = 0;
+ }
+ try {
+ File outFile = new File(cacheDir, fileName);
+ os = new FileOutputStream(outFile);
+ icon.compress(Bitmap.CompressFormat.PNG, 90, os);
+ os.close();
+ FileUtils.setPermissions(outFile,
+ FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IROTH,
+ -1, -1);
+ mIconCacheSize += outFile.length();
+ if (mIconCacheSize > MAX_ICON_CACHE_SIZE) {
+ purgeIconCache();
+ }
+ success = true;
+ } catch (Exception e) {
+ success = false;
+ Log.w(TAG, "Unable to cache icon " + fileName, e);
+ }
+ Binder.restoreCallingIdentity(token);
+ return success;
+ }
+
+ };
+}
diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/ThemeManagerService.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/ThemeManagerService.java
new file mode 100644
index 0000000..c78f187
--- /dev/null
+++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/ThemeManagerService.java
@@ -0,0 +1,1246 @@
+/*
+ * Copyright (C) 2014-2016 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 org.cyanogenmod.platform.internal;
+
+import android.app.ActivityManager;
+import android.app.ActivityManagerNative;
+import android.app.IActivityManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.WallpaperManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.ThemeConfig;
+import android.media.RingtoneManager;
+import android.os.Binder;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.server.SystemService;
+
+import cyanogenmod.app.CMContextConstants;
+import cyanogenmod.providers.CMSettings;
+import cyanogenmod.providers.ThemesContract.MixnMatchColumns;
+import cyanogenmod.providers.ThemesContract.ThemesColumns;
+import cyanogenmod.themes.IThemeChangeListener;
+import cyanogenmod.themes.IThemeProcessingListener;
+import cyanogenmod.themes.IThemeService;
+import cyanogenmod.themes.ThemeChangeRequest;
+
+import org.cyanogenmod.internal.util.ImageUtils;
+import org.cyanogenmod.internal.util.ThemeUtils;
+import org.cyanogenmod.platform.internal.AppsFailureReceiver;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import libcore.io.IoUtils;
+
+import static android.content.res.ThemeConfig.SYSTEM_DEFAULT;
+import static cyanogenmod.platform.Manifest.permission.ACCESS_THEME_MANAGER;
+import static org.cyanogenmod.internal.util.ThemeUtils.SYSTEM_THEME_PATH;
+import static org.cyanogenmod.internal.util.ThemeUtils.THEME_BOOTANIMATION_PATH;
+
+public class ThemeManagerService extends SystemService {
+
+ private static final String TAG = ThemeManagerService.class.getName();
+
+ private static final boolean DEBUG = false;
+
+ private static final String GOOGLE_SETUPWIZARD_PACKAGE = "com.google.android.setupwizard";
+ private static final String CM_SETUPWIZARD_PACKAGE = "com.cyanogenmod.setupwizard";
+ private static final String MANAGED_PROVISIONING_PACKAGE = "com.android.managedprovisioning";
+
+ // Defines a min and max compatible api level for themes on this system.
+ private static final int MIN_COMPATIBLE_VERSION = 21;
+
+ private HandlerThread mWorker;
+ private ThemeWorkerHandler mHandler;
+ private ResourceProcessingHandler mResourceProcessingHandler;
+ private Context mContext;
+ private PackageManager mPM;
+ private int mProgress;
+ private boolean mWallpaperChangedByUs = false;
+ private int mCurrentUserId = UserHandle.USER_OWNER;
+
+ private boolean mIsThemeApplying = false;
+
+ private final RemoteCallbackList<IThemeChangeListener> mClients = new RemoteCallbackList<>();
+
+ private final RemoteCallbackList<IThemeProcessingListener> mProcessingListeners =
+ new RemoteCallbackList<>();
+
+ final private ArrayList<String> mThemesToProcessQueue = new ArrayList<>();
+
+ private long mLastThemeChangeTime = 0;
+ private int mLastThemeChangeRequestType;
+
+ private class ThemeWorkerHandler extends Handler {
+ private static final int MESSAGE_CHANGE_THEME = 1;
+ private static final int MESSAGE_APPLY_DEFAULT_THEME = 2;
+ private static final int MESSAGE_REBUILD_RESOURCE_CACHE = 3;
+
+ public ThemeWorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_CHANGE_THEME:
+ final ThemeChangeRequest request = (ThemeChangeRequest) msg.obj;
+ doApplyTheme(request, msg.arg1 == 1);
+ break;
+ case MESSAGE_APPLY_DEFAULT_THEME:
+ doApplyDefaultTheme();
+ break;
+ case MESSAGE_REBUILD_RESOURCE_CACHE:
+ doRebuildResourceCache();
+ break;
+ default:
+ Log.w(TAG, "Unknown message " + msg.what);
+ break;
+ }
+ }
+ }
+
+ private class ResourceProcessingHandler extends Handler {
+ private static final int MESSAGE_QUEUE_THEME_FOR_PROCESSING = 3;
+ private static final int MESSAGE_DEQUEUE_AND_PROCESS_THEME = 4;
+
+ public ResourceProcessingHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_QUEUE_THEME_FOR_PROCESSING:
+ String pkgName = (String) msg.obj;
+ synchronized (mThemesToProcessQueue) {
+ if (!mThemesToProcessQueue.contains(pkgName)) {
+ if (DEBUG) Log.d(TAG, "Adding " + pkgName + " for processing");
+ mThemesToProcessQueue.add(pkgName);
+ if (mThemesToProcessQueue.size() == 1) {
+ this.sendEmptyMessage(MESSAGE_DEQUEUE_AND_PROCESS_THEME);
+ }
+ }
+ }
+ break;
+ case MESSAGE_DEQUEUE_AND_PROCESS_THEME:
+ synchronized (mThemesToProcessQueue) {
+ pkgName = mThemesToProcessQueue.get(0);
+ }
+ if (pkgName != null) {
+ if (DEBUG) Log.d(TAG, "Processing " + pkgName);
+ String name;
+ try {
+ PackageInfo pi = mPM.getPackageInfo(pkgName, 0);
+ name = getThemeName(pi);
+ } catch (PackageManager.NameNotFoundException e) {
+ name = null;
+ }
+
+ int result = mPM.processThemeResources(pkgName);
+ if (result < 0) {
+ postFailedThemeInstallNotification(name != null ? name : pkgName);
+ }
+ sendThemeResourcesCachedBroadcast(pkgName, result);
+
+ synchronized (mThemesToProcessQueue) {
+ mThemesToProcessQueue.remove(0);
+ if (mThemesToProcessQueue.size() > 0 &&
+ !hasMessages(MESSAGE_DEQUEUE_AND_PROCESS_THEME)) {
+ this.sendEmptyMessage(MESSAGE_DEQUEUE_AND_PROCESS_THEME);
+ }
+ }
+ postFinishedProcessing(pkgName);
+ }
+ break;
+ default:
+ Log.w(TAG, "Unknown message " + msg.what);
+ break;
+ }
+ }
+ }
+
+
+ public ThemeManagerService(Context context) {
+ super(context);
+ mContext = context;
+ mWorker = new HandlerThread("ThemeServiceWorker", Process.THREAD_PRIORITY_BACKGROUND);
+ mWorker.start();
+ mHandler = new ThemeWorkerHandler(mWorker.getLooper());
+ Log.i(TAG, "Spawned worker thread");
+
+ HandlerThread processingThread = new HandlerThread("ResourceProcessingThread",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ processingThread.start();
+ mResourceProcessingHandler =
+ new ResourceProcessingHandler(processingThread.getLooper());
+
+ // create the theme directories if they do not exist
+ ThemeUtils.createThemeDirIfNotExists();
+ ThemeUtils.createFontDirIfNotExists();
+ ThemeUtils.createAlarmDirIfNotExists();
+ ThemeUtils.createNotificationDirIfNotExists();
+ ThemeUtils.createRingtoneDirIfNotExists();
+ ThemeUtils.createIconCacheDirIfNotExists();
+ }
+
+ @Override
+ public void onStart() {
+ publishBinderService(CMContextConstants.CM_THEME_SERVICE, mService);
+ // listen for wallpaper changes
+ IntentFilter filter = new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED);
+ mContext.registerReceiver(mWallpaperChangeReceiver, filter);
+
+ filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
+ mContext.registerReceiver(mUserChangeReceiver, filter);
+
+ mPM = mContext.getPackageManager();
+
+ if (!isThemeApiUpToDate()) {
+ Log.d(TAG, "The system has been upgraded to a theme new api, " +
+ "checking if currently set theme is compatible");
+ removeObsoleteThemeOverlayIfExists();
+ updateThemeApi();
+ }
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ super.onBootPhase(phase);
+ if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
+ registerAppsFailureReceiver();
+ processInstalledThemes();
+ }
+ }
+
+ private void registerAppsFailureReceiver() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(cyanogenmod.content.Intent.ACTION_APP_FAILURE);
+ filter.addAction(ThemeUtils.ACTION_THEME_CHANGED);
+ mContext.registerReceiver(new AppsFailureReceiver(), filter);
+ }
+
+ private void removeObsoleteThemeOverlayIfExists() {
+ // Get the current overlay theme so we can see it it's overlay should be unapplied
+ final IActivityManager am = ActivityManagerNative.getDefault();
+ ThemeConfig config = null;
+ try {
+ if (am != null) {
+ config = am.getConfiguration().themeConfig;
+ } else {
+ Log.e(TAG, "ActivityManager getDefault() " +
+ "returned null, cannot remove obsolete theme");
+ }
+ } catch(RemoteException e) {
+ Log.e(TAG, "Failed to get the theme config ", e);
+ }
+ if (config == null) return; // No need to unapply a theme if one isn't set
+
+ // Populate the currentTheme map for the components we care about, we'll look
+ // at the compatibility of each pkg below.
+ HashMap<String, String> currentThemeMap = new HashMap<>();
+ currentThemeMap.put(ThemesColumns.MODIFIES_STATUS_BAR, config.getOverlayForStatusBar());
+ currentThemeMap.put(ThemesColumns.MODIFIES_NAVIGATION_BAR,
+ config.getOverlayForNavBar());
+ currentThemeMap.put(ThemesColumns.MODIFIES_OVERLAYS, config.getOverlayPkgName());
+
+ // Look at each component's theme (that we care about at least) and check compatibility
+ // of the pkg with the system. If it is not compatible then we will add it to a theme
+ // change request.
+ Map<String, String> defaults = ThemeUtils.getDefaultComponents(mContext);
+ ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder();
+ for(Map.Entry<String, String> entry : currentThemeMap.entrySet()) {
+ String component = entry.getKey();
+ String pkgName = entry.getValue();
+ String defaultPkg = defaults.get(component);
+
+ // Check that the default overlay theme is not currently set
+ if (defaultPkg.equals(pkgName)) {
+ Log.d(TAG, "Current overlay theme is same as default. " +
+ "Not doing anything for " + component);
+ continue;
+ }
+
+ // No need to unapply a system theme since it is always compatible
+ if (ThemeConfig.SYSTEM_DEFAULT.equals(pkgName)) {
+ Log.d(TAG, "Current overlay theme for "
+ + component + " was system. no need to unapply");
+ continue;
+ }
+
+ if (!isThemeCompatibleWithUpgradedApi(pkgName)) {
+ Log.d(TAG, pkgName + "is incompatible with latest theme api for component " +
+ component + ", Applying " + defaultPkg);
+ builder.setComponent(component, pkgName);
+ }
+ }
+
+ // Now actually unapply the incompatible themes
+ ThemeChangeRequest request = builder.build();
+ if (!request.getThemeComponentsMap().isEmpty()) {
+ try {
+ ((IThemeService) mService).requestThemeChange(request, true);
+ } catch(RemoteException e) {
+ // This cannot happen
+ }
+ } else {
+ Log.d(TAG, "Current theme is compatible with the system. Not unapplying anything");
+ }
+ }
+
+ private boolean isThemeCompatibleWithUpgradedApi(String pkgName) {
+ // Note this function does not cover the case of a downgrade. That case is out of scope and
+ // would require predicting whether the future API levels will be compatible or not.
+ boolean compatible = false;
+ try {
+ PackageInfo pi = mPM.getPackageInfo(pkgName, 0);
+ Log.d(TAG, "Comparing theme target: " + pi.applicationInfo.targetSdkVersion +
+ "to " + android.os.Build.VERSION.SDK_INT);
+ compatible = pi.applicationInfo.targetSdkVersion >= MIN_COMPATIBLE_VERSION;
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Unable to get package info for " + pkgName, e);
+ }
+ return compatible;
+ }
+
+ private boolean isThemeApiUpToDate() {
+ // We can't be 100% sure its an upgrade. If the field is undefined it
+ // could have been a factory reset.
+ final ContentResolver resolver = mContext.getContentResolver();
+ int recordedApiLevel = android.os.Build.VERSION.SDK_INT;
+ try {
+ recordedApiLevel = CMSettings.Secure.getInt(resolver,
+ CMSettings.Secure.THEME_PREV_BOOT_API_LEVEL);
+ } catch (CMSettings.CMSettingNotFoundException e) {
+ recordedApiLevel = -1;
+ Log.d(TAG, "Previous api level not found. First time booting?");
+ }
+ Log.d(TAG, "Prev api level was: " + recordedApiLevel
+ + ", api is now: " + android.os.Build.VERSION.SDK_INT);
+
+ return recordedApiLevel == android.os.Build.VERSION.SDK_INT;
+ }
+
+ private void updateThemeApi() {
+ final ContentResolver resolver = mContext.getContentResolver();
+ boolean success = CMSettings.Secure.putInt(resolver,
+ CMSettings.Secure.THEME_PREV_BOOT_API_LEVEL, android.os.Build.VERSION.SDK_INT);
+ if (!success) {
+ Log.e(TAG, "Unable to store latest API level to secure settings");
+ }
+ }
+
+ private void doApplyTheme(ThemeChangeRequest request, boolean removePerAppTheme) {
+ synchronized(this) {
+ mProgress = 0;
+ }
+
+ if (request == null || request.getNumChangesRequested() == 0) {
+ postFinish(true, request, 0);
+ return;
+ }
+ mIsThemeApplying = true;
+ mLastThemeChangeTime = System.currentTimeMillis();
+ mLastThemeChangeRequestType = request.getReqeustType().ordinal();
+
+ incrementProgress(5);
+
+ // TODO: provide progress updates that reflect the time needed for each component
+ final int progressIncrement = 75 / request.getNumChangesRequested();
+
+ if (request.getIconsThemePackageName() != null) {
+ updateIcons(request.getIconsThemePackageName());
+ incrementProgress(progressIncrement);
+ }
+
+ if (request.getWallpaperThemePackageName() != null) {
+ if (updateWallpaper(request.getWallpaperThemePackageName(),
+ request.getWallpaperId())) {
+ mWallpaperChangedByUs = true;
+ }
+ incrementProgress(progressIncrement);
+ }
+
+ if (request.getLockWallpaperThemePackageName() != null) {
+ updateLockscreen(request.getLockWallpaperThemePackageName());
+ incrementProgress(progressIncrement);
+ }
+
+ Environment.setUserRequired(false);
+ if (request.getNotificationThemePackageName() != null) {
+ updateNotifications(request.getNotificationThemePackageName());
+ incrementProgress(progressIncrement);
+ }
+
+ if (request.getAlarmThemePackageName() != null) {
+ updateAlarms(request.getAlarmThemePackageName());
+ incrementProgress(progressIncrement);
+ }
+
+ if (request.getRingtoneThemePackageName() != null) {
+ updateRingtones(request.getRingtoneThemePackageName());
+ incrementProgress(progressIncrement);
+ }
+ Environment.setUserRequired(true);
+
+ if (request.getBootanimationThemePackageName() != null) {
+ updateBootAnim(request.getBootanimationThemePackageName());
+ incrementProgress(progressIncrement);
+ }
+
+ if (request.getFontThemePackageName() != null) {
+ updateFonts(request.getFontThemePackageName());
+ incrementProgress(progressIncrement);
+ }
+
+ if (request.getLiveLockScreenThemePackageName() != null) {
+ updateLiveLockScreen(request.getLiveLockScreenThemePackageName());
+ incrementProgress(progressIncrement);
+ }
+
+ try {
+ updateProvider(request, mLastThemeChangeTime);
+ } catch(IllegalArgumentException e) {
+ // Safeguard against provider not being ready yet.
+ Log.e(TAG, "Not updating the theme provider since it is unavailable");
+ }
+
+ if (shouldUpdateConfiguration(request)) {
+ updateConfiguration(request, removePerAppTheme);
+ }
+
+ killLaunchers(request);
+
+ postFinish(true, request, mLastThemeChangeTime);
+ mIsThemeApplying = false;
+ }
+
+ private void doApplyDefaultTheme() {
+ final ContentResolver resolver = mContext.getContentResolver();
+ final String defaultThemePkg = CMSettings.Secure.getString(resolver,
+ CMSettings.Secure.DEFAULT_THEME_PACKAGE);
+ if (!TextUtils.isEmpty(defaultThemePkg)) {
+ String defaultThemeComponents = CMSettings.Secure.getString(resolver,
+ CMSettings.Secure.DEFAULT_THEME_COMPONENTS);
+ List<String> components;
+ if (TextUtils.isEmpty(defaultThemeComponents)) {
+ components = ThemeUtils.getAllComponents();
+ } else {
+ components = new ArrayList<String>(
+ Arrays.asList(defaultThemeComponents.split("\\|")));
+ }
+ ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder();
+ for (String component : components) {
+ builder.setComponent(component, defaultThemePkg);
+ }
+ try {
+ ((IThemeService) mService).requestThemeChange(builder.build(), true);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to set default theme", e);
+ }
+ }
+ }
+
+ private void doRebuildResourceCache() {
+ FileUtils.deleteContents(new File(ThemeUtils.RESOURCE_CACHE_DIR));
+ processInstalledThemes();
+ }
+
+ private void updateProvider(ThemeChangeRequest request, long updateTime) {
+ ContentValues values = new ContentValues();
+ values.put(MixnMatchColumns.COL_UPDATE_TIME, updateTime);
+ Map<String, String> componentMap = request.getThemeComponentsMap();
+ for (String component : componentMap.keySet()) {
+ values.put(MixnMatchColumns.COL_VALUE, componentMap.get(component));
+ String where = MixnMatchColumns.COL_KEY + "=?";
+ String[] selectionArgs = { MixnMatchColumns.componentToMixNMatchKey(component) };
+ if (selectionArgs[0] == null) {
+ continue; // No equivalence between mixnmatch and theme
+ }
+
+ // Add component ID for multiwallpaper
+ if (ThemesColumns.MODIFIES_LAUNCHER.equals(component)) {
+ values.put(MixnMatchColumns.COL_COMPONENT_ID, request.getWallpaperId());
+ }
+
+ mContext.getContentResolver().update(MixnMatchColumns.CONTENT_URI, values, where,
+ selectionArgs);
+ }
+ }
+
+ private boolean updateIcons(String pkgName) {
+ ThemeUtils.clearIconCache();
+ try {
+ if (pkgName.equals(SYSTEM_DEFAULT)) {
+ mPM.updateIconMaps(null);
+ } else {
+ mPM.updateIconMaps(pkgName);
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Changing icons failed", e);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean updateFonts(String pkgName) {
+ //Clear the font dir
+ FileUtils.deleteContents(new File(ThemeUtils.SYSTEM_THEME_FONT_PATH));
+
+ if (!pkgName.equals(SYSTEM_DEFAULT)) {
+ //Get Font Assets
+ Context themeCtx;
+ String[] assetList;
+ try {
+ themeCtx = mContext.createPackageContext(pkgName, Context.CONTEXT_IGNORE_SECURITY);
+ AssetManager assetManager = themeCtx.getAssets();
+ assetList = assetManager.list("fonts");
+ } catch (Exception e) {
+ Log.e(TAG, "There was an error getting assets for pkg " + pkgName, e);
+ return false;
+ }
+ if (assetList == null || assetList.length == 0) {
+ Log.e(TAG, "Could not find any font assets");
+ return false;
+ }
+
+ //Copy font assets to font dir
+ for(String asset : assetList) {
+ InputStream is = null;
+ OutputStream os = null;
+ try {
+ is = ThemeUtils.getInputStreamFromAsset(themeCtx,
+ "file:///android_asset/fonts/" + asset);
+ File outFile = new File(ThemeUtils.SYSTEM_THEME_FONT_PATH, asset);
+ FileUtils.copyToFile(is, outFile);
+ FileUtils.setPermissions(outFile,
+ FileUtils.S_IRWXU|FileUtils.S_IRGRP|FileUtils.S_IRWXO, -1, -1);
+ } catch (Exception e) {
+ Log.e(TAG, "There was an error installing the new fonts for pkg " + pkgName, e);
+ return false;
+ } finally {
+ IoUtils.closeQuietly(is);
+ IoUtils.closeQuietly(os);
+ }
+ }
+ }
+
+ //Notify zygote that themes need a refresh
+ SystemProperties.set("sys.refresh_theme", "1");
+ return true;
+ }
+
+ private boolean updateBootAnim(String pkgName) {
+ if (SYSTEM_DEFAULT.equals(pkgName)) {
+ clearBootAnimation();
+ return true;
+ }
+
+ try {
+ final ApplicationInfo ai = mPM.getApplicationInfo(pkgName, 0);
+ applyBootAnimation(ai.sourceDir);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Changing boot animation failed", e);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean updateAlarms(String pkgName) {
+ return updateAudible(ThemeUtils.SYSTEM_THEME_ALARM_PATH, "alarms",
+ RingtoneManager.TYPE_ALARM, pkgName);
+ }
+
+ private boolean updateNotifications(String pkgName) {
+ return updateAudible(ThemeUtils.SYSTEM_THEME_NOTIFICATION_PATH, "notifications",
+ RingtoneManager.TYPE_NOTIFICATION, pkgName);
+ }
+
+ private boolean updateRingtones(String pkgName) {
+ return updateAudible(ThemeUtils.SYSTEM_THEME_RINGTONE_PATH, "ringtones",
+ RingtoneManager.TYPE_RINGTONE, pkgName);
+ }
+
+ private boolean updateAudible(String dirPath, String assetPath, int type, String pkgName) {
+ //Clear the dir
+ ThemeUtils.clearAudibles(mContext, dirPath);
+ if (pkgName.equals(SYSTEM_DEFAULT)) {
+ if (!ThemeUtils.setDefaultAudible(mContext, type)) {
+ Log.e(TAG, "There was an error installing the default audio file");
+ return false;
+ }
+ return true;
+ }
+
+ PackageInfo pi = null;
+ try {
+ pi = mPM.getPackageInfo(pkgName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Unable to update audible " + dirPath, e);
+ return false;
+ }
+
+ //Get theme Assets
+ Context themeCtx;
+ String[] assetList;
+ try {
+ themeCtx = mContext.createPackageContext(pkgName, Context.CONTEXT_IGNORE_SECURITY);
+ AssetManager assetManager = themeCtx.getAssets();
+ assetList = assetManager.list(assetPath);
+ } catch (Exception e) {
+ Log.e(TAG, "There was an error getting assets for pkg " + pkgName, e);
+ return false;
+ }
+ if (assetList == null || assetList.length == 0) {
+ Log.e(TAG, "Could not find any audio assets");
+ return false;
+ }
+
+ // TODO: right now we just load the first file but this will need to be changed
+ // in the future if multiple audio files are supported.
+ final String asset = assetList[0];
+ if (!ThemeUtils.isValidAudible(asset)) return false;
+
+ InputStream is = null;
+ OutputStream os = null;
+ try {
+ is = ThemeUtils.getInputStreamFromAsset(themeCtx, "file:///android_asset/"
+ + assetPath + File.separator + asset);
+ File outFile = new File(dirPath, asset);
+ FileUtils.copyToFile(is, outFile);
+ FileUtils.setPermissions(outFile,
+ FileUtils.S_IRWXU|FileUtils.S_IRGRP|FileUtils.S_IRWXO,-1, -1);
+ ThemeUtils.setAudible(mContext, outFile, type, pi.themeInfo.name);
+ } catch (Exception e) {
+ Log.e(TAG, "There was an error installing the new audio file for pkg " + pkgName, e);
+ return false;
+ } finally {
+ IoUtils.closeQuietly(is);
+ IoUtils.closeQuietly(os);
+ }
+ return true;
+ }
+
+ private boolean updateLockscreen(String pkgName) {
+ boolean success;
+ success = setCustomLockScreenWallpaper(pkgName);
+
+ if (success) {
+ mContext.sendBroadcastAsUser(new Intent(Intent.ACTION_KEYGUARD_WALLPAPER_CHANGED),
+ UserHandle.ALL);
+ }
+ return success;
+ }
+
+ private boolean setCustomLockScreenWallpaper(String pkgName) {
+ WallpaperManager wm = WallpaperManager.getInstance(mContext);
+ try {
+ if (SYSTEM_DEFAULT.equals(pkgName) || TextUtils.isEmpty(pkgName)) {
+ wm.clearKeyguardWallpaper();
+ } else {
+ InputStream in = ImageUtils.getCroppedKeyguardStream(pkgName, mContext);
+ if (in != null) {
+ wm.setKeyguardStream(in);
+ IoUtils.closeQuietly(in);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "There was an error setting lockscreen wp for pkg " + pkgName, e);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean updateWallpaper(String pkgName, long id) {
+ WallpaperManager wm = WallpaperManager.getInstance(mContext);
+ if (SYSTEM_DEFAULT.equals(pkgName)) {
+ try {
+ wm.clear();
+ } catch (IOException e) {
+ return false;
+ }
+ } else if (TextUtils.isEmpty(pkgName)) {
+ try {
+ wm.clear(false);
+ } catch (IOException e) {
+ return false;
+ }
+ } else {
+ InputStream in = null;
+ try {
+ in = ImageUtils.getCroppedWallpaperStream(pkgName, id, mContext);
+ if (in != null)
+ wm.setStream(in);
+ } catch (Exception e) {
+ return false;
+ } finally {
+ IoUtils.closeQuietly(in);
+ }
+ }
+ return true;
+ }
+
+ private boolean updateLiveLockScreen(String pkgName) {
+ // TODO: do something meaningful here once ready
+ return true;
+ }
+
+ private boolean updateConfiguration(ThemeChangeRequest request,
+ boolean removePerAppThemes) {
+ final IActivityManager am = ActivityManagerNative.getDefault();
+ if (am != null) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Configuration config = am.getConfiguration();
+ ThemeConfig.Builder themeBuilder = createBuilderFrom(config, request, null,
+ removePerAppThemes);
+ ThemeConfig newConfig = themeBuilder.build();
+
+ config.themeConfig = newConfig;
+ am.updateConfiguration(config);
+ } catch (RemoteException e) {
+ return false;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ return true;
+ }
+
+ private boolean updateConfiguration(ThemeConfig themeConfig) {
+ final IActivityManager am = ActivityManagerNative.getDefault();
+ if (am != null) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Configuration config = am.getConfiguration();
+
+ config.themeConfig = themeConfig;
+ am.updateConfiguration(config);
+ } catch (RemoteException e) {
+ return false;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ return true;
+ }
+
+ private boolean shouldUpdateConfiguration(ThemeChangeRequest request) {
+ return request.getOverlayThemePackageName() != null ||
+ request.getFontThemePackageName() != null ||
+ request.getIconsThemePackageName() != null ||
+ request.getStatusBarThemePackageName() != null ||
+ request.getNavBarThemePackageName() != null ||
+ request.getPerAppOverlays().size() > 0;
+ }
+
+ private static ThemeConfig.Builder createBuilderFrom(Configuration config,
+ ThemeChangeRequest request, String pkgName, boolean removePerAppThemes) {
+ ThemeConfig.Builder builder = new ThemeConfig.Builder(config.themeConfig);
+
+ if (removePerAppThemes) removePerAppThemesFromConfig(builder, config.themeConfig);
+
+ if (request.getIconsThemePackageName() != null) {
+ builder.defaultIcon(pkgName == null ? request.getIconsThemePackageName() : pkgName);
+ }
+
+ if (request.getOverlayThemePackageName() != null) {
+ builder.defaultOverlay(pkgName == null ?
+ request.getOverlayThemePackageName() : pkgName);
+ }
+
+ if (request.getFontThemePackageName() != null) {
+ builder.defaultFont(pkgName == null ? request.getFontThemePackageName() : pkgName);
+ }
+
+ if (request.getStatusBarThemePackageName() != null) {
+ builder.overlay(ThemeConfig.SYSTEMUI_STATUS_BAR_PKG, pkgName == null ?
+ request.getStatusBarThemePackageName() : pkgName);
+ }
+
+ if (request.getNavBarThemePackageName() != null) {
+ builder.overlay(ThemeConfig.SYSTEMUI_NAVBAR_PKG, pkgName == null ?
+ request.getNavBarThemePackageName() : pkgName);
+ }
+
+ // check for any per app overlays being applied
+ Map<String, String> appOverlays = request.getPerAppOverlays();
+ for (String appPkgName : appOverlays.keySet()) {
+ if (appPkgName != null) {
+ builder.overlay(appPkgName, appOverlays.get(appPkgName));
+ }
+ }
+
+ return builder;
+ }
+
+ private static void removePerAppThemesFromConfig(ThemeConfig.Builder builder,
+ ThemeConfig themeConfig) {
+ if (themeConfig != null) {
+ Map<String, ThemeConfig.AppTheme> themes = themeConfig.getAppThemes();
+ for (String appPkgName : themes.keySet()) {
+ if (ThemeUtils.isPerAppThemeComponent(appPkgName)) {
+ builder.overlay(appPkgName, null);
+ }
+ }
+ }
+ }
+
+ // Kill the current Home process, they tend to be evil and cache
+ // drawable references in all apps
+ private void killLaunchers(ThemeChangeRequest request) {
+ if (request.getOverlayThemePackageName() == null
+ && request.getIconsThemePackageName() == null) {
+ return;
+ }
+
+ final ActivityManager am =
+ (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+
+ Intent homeIntent = new Intent();
+ homeIntent.setAction(Intent.ACTION_MAIN);
+ homeIntent.addCategory(Intent.CATEGORY_HOME);
+
+ List<ResolveInfo> infos = mPM.queryIntentActivities(homeIntent, 0);
+ List<ResolveInfo> themeChangeInfos = mPM.queryBroadcastReceivers(
+ new Intent(ThemeUtils.ACTION_THEME_CHANGED), 0);
+ for(ResolveInfo info : infos) {
+ if (info.activityInfo != null && info.activityInfo.applicationInfo != null &&
+ !isSetupActivity(info) && !handlesThemeChanges(
+ info.activityInfo.applicationInfo.packageName, themeChangeInfos)) {
+ String pkgToStop = info.activityInfo.applicationInfo.packageName;
+ Log.d(TAG, "Force stopping " + pkgToStop + " for theme change");
+ try {
+ am.forceStopPackage(pkgToStop);
+ } catch(Exception e) {
+ Log.e(TAG, "Unable to force stop package, did you forget platform signature?",
+ e);
+ }
+ }
+ }
+ }
+
+ private boolean isSetupActivity(ResolveInfo info) {
+ return GOOGLE_SETUPWIZARD_PACKAGE.equals(info.activityInfo.packageName) ||
+ MANAGED_PROVISIONING_PACKAGE.equals(info.activityInfo.packageName) ||
+ CM_SETUPWIZARD_PACKAGE.equals(info.activityInfo.packageName);
+ }
+
+ private boolean handlesThemeChanges(String pkgName, List<ResolveInfo> infos) {
+ if (infos != null && infos.size() > 0) {
+ for (ResolveInfo info : infos) {
+ if (info.activityInfo.applicationInfo.packageName.equals(pkgName)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private void postProgress() {
+ int N = mClients.beginBroadcast();
+ for(int i=0; i < N; i++) {
+ IThemeChangeListener listener = mClients.getBroadcastItem(0);
+ try {
+ listener.onProgress(mProgress);
+ } catch(RemoteException e) {
+ Log.w(TAG, "Unable to post progress to client listener", e);
+ }
+ }
+ mClients.finishBroadcast();
+ }
+
+ private void postFinish(boolean isSuccess, ThemeChangeRequest request, long updateTime) {
+ synchronized(this) {
+ mProgress = 0;
+ }
+
+ int N = mClients.beginBroadcast();
+ for(int i=0; i < N; i++) {
+ IThemeChangeListener listener = mClients.getBroadcastItem(0);
+ try {
+ listener.onFinish(isSuccess);
+ } catch(RemoteException e) {
+ Log.w(TAG, "Unable to post progress to client listener", e);
+ }
+ }
+ mClients.finishBroadcast();
+
+ // if successful, broadcast that the theme changed
+ if (isSuccess) {
+ broadcastThemeChange(request, updateTime);
+ }
+ }
+
+ private void postFinishedProcessing(String pkgName) {
+ int N = mProcessingListeners.beginBroadcast();
+ for(int i=0; i < N; i++) {
+ IThemeProcessingListener listener = mProcessingListeners.getBroadcastItem(0);
+ try {
+ listener.onFinishedProcessing(pkgName);
+ } catch(RemoteException e) {
+ Log.w(TAG, "Unable to post progress to listener", e);
+ }
+ }
+ mProcessingListeners.finishBroadcast();
+ }
+
+ private void broadcastThemeChange(ThemeChangeRequest request, long updateTime) {
+ Map<String, String> componentMap = request.getThemeComponentsMap();
+ if (componentMap == null || componentMap.size() == 0) return;
+
+ final Intent intent = new Intent(ThemeUtils.ACTION_THEME_CHANGED);
+ ArrayList componentsArrayList = new ArrayList(componentMap.keySet());
+ intent.putStringArrayListExtra(ThemeUtils.EXTRA_COMPONENTS, componentsArrayList);
+ intent.putExtra(ThemeUtils.EXTRA_REQUEST_TYPE, request.getReqeustType().ordinal());
+ intent.putExtra(ThemeUtils.EXTRA_UPDATE_TIME, updateTime);
+ mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ private void incrementProgress(int increment) {
+ synchronized(this) {
+ mProgress += increment;
+ if (mProgress > 100) mProgress = 100;
+ }
+ postProgress();
+ }
+
+ private boolean applyBootAnimation(String themePath) {
+ boolean success = false;
+ try {
+ ZipFile zip = new ZipFile(new File(themePath));
+ ZipEntry ze = zip.getEntry(THEME_BOOTANIMATION_PATH);
+ if (ze != null) {
+ clearBootAnimation();
+ BufferedInputStream is = new BufferedInputStream(zip.getInputStream(ze));
+ final String bootAnimationPath = SYSTEM_THEME_PATH + File.separator
+ + "bootanimation.zip";
+ ThemeUtils.copyAndScaleBootAnimation(mContext, is, bootAnimationPath);
+ FileUtils.setPermissions(bootAnimationPath,
+ FileUtils.S_IRWXU|FileUtils.S_IRGRP|FileUtils.S_IROTH, -1, -1);
+ }
+ zip.close();
+ success = true;
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to load boot animation for " + themePath, e);
+ }
+
+ return success;
+ }
+
+ private void clearBootAnimation() {
+ File anim = new File(SYSTEM_THEME_PATH + File.separator + "bootanimation.zip");
+ if (anim.exists())
+ anim.delete();
+ }
+
+ private BroadcastReceiver mWallpaperChangeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!mWallpaperChangedByUs) {
+ // In case the mixnmatch table has a mods_launcher entry, we'll clear it
+ ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder();
+ builder.setWallpaper("");
+ updateProvider(builder.build(), System.currentTimeMillis());
+ } else {
+ mWallpaperChangedByUs = false;
+ }
+ }
+ };
+
+ private BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ if (userHandle >= 0 && userHandle != mCurrentUserId) {
+ mCurrentUserId = userHandle;
+ ThemeConfig config = ThemeConfig.getBootThemeForUser(mContext.getContentResolver(),
+ userHandle);
+ if (DEBUG) {
+ Log.d(TAG,
+ "Changing theme for user " + userHandle + " to " + config.toString());
+ }
+ ThemeChangeRequest request = new ThemeChangeRequest.Builder(config).build();
+ try {
+ ((IThemeService) mService).requestThemeChange(request, true);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to change theme for user change", e);
+ }
+ }
+ }
+ };
+
+ private void processInstalledThemes() {
+ final String defaultTheme = getDefaultThemePackageName(mContext);
+ Message msg;
+ // Make sure the default theme is the first to get processed!
+ if (!ThemeConfig.SYSTEM_DEFAULT.equals(defaultTheme)) {
+ msg = mHandler.obtainMessage(
+ ResourceProcessingHandler.MESSAGE_QUEUE_THEME_FOR_PROCESSING,
+ 0, 0, defaultTheme);
+ mResourceProcessingHandler.sendMessage(msg);
+ }
+ // Iterate over all installed packages and queue up the ones that are themes or icon packs
+ List<PackageInfo> packages = mPM.getInstalledPackages(0);
+ for (PackageInfo info : packages) {
+ if (!defaultTheme.equals(info.packageName) &&
+ (info.isThemeApk || info.isLegacyIconPackApk)) {
+ msg = mHandler.obtainMessage(
+ ResourceProcessingHandler.MESSAGE_QUEUE_THEME_FOR_PROCESSING,
+ 0, 0, info.packageName);
+ mResourceProcessingHandler.sendMessage(msg);
+ }
+ }
+ }
+
+ private void sendThemeResourcesCachedBroadcast(String themePkgName, int resultCode) {
+ final Intent intent = new Intent(Intent.ACTION_THEME_RESOURCES_CACHED);
+ intent.putExtra(Intent.EXTRA_THEME_PACKAGE_NAME, themePkgName);
+ intent.putExtra(Intent.EXTRA_THEME_RESULT, resultCode);
+ mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ /**
+ * Posts a notification to let the user know the theme was not installed.
+ * @param name
+ */
+ private void postFailedThemeInstallNotification(String name) {
+ NotificationManager nm =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ Notification notice = new Notification.Builder(mContext)
+ .setAutoCancel(true)
+ .setOngoing(false)
+ .setContentTitle(
+ mContext.getString(R.string.theme_install_error_title))
+ .setContentText(String.format(
+ mContext.getString(R.string.theme_install_error_message), name))
+ .setSmallIcon(android.R.drawable.stat_notify_error)
+ .setWhen(System.currentTimeMillis())
+ .build();
+ nm.notify(name.hashCode(), notice);
+ }
+
+ private String getThemeName(PackageInfo pi) {
+ if (pi.themeInfo != null) {
+ return pi.themeInfo.name;
+ } else if (pi.isLegacyIconPackApk) {
+ return pi.applicationInfo.name;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the default theme package name
+ * Historically this was done using {@link ThemeUtils#getDefaultThemePackageName(Context)} but
+ * the setting that is queried in that method uses the AOSP settings provider but the setting
+ * is now in CMSettings. Since {@link ThemeUtils} is in the core framework we cannot access
+ * CMSettings.
+ * @param context
+ * @return Default theme package name
+ */
+ private static String getDefaultThemePackageName(Context context) {
+ final String defaultThemePkg = CMSettings.Secure.getString(context.getContentResolver(),
+ CMSettings.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 final IBinder mService = new IThemeService.Stub() {
+ @Override
+ public void requestThemeChangeUpdates(IThemeChangeListener listener)
+ throws RemoteException {
+ enforcePermission();
+ mClients.register(listener);
+ }
+
+ @Override
+ public void removeUpdates(IThemeChangeListener listener) throws RemoteException {
+ enforcePermission();
+ mClients.unregister(listener);
+ }
+
+ @Override
+ public void requestThemeChange(ThemeChangeRequest request, boolean removePerAppThemes)
+ throws RemoteException {
+ enforcePermission();
+ Message msg;
+
+ /**
+ * Since the ThemeService handles compiling theme resource we need to make sure that any
+ * of the components we are trying to apply are either already processed or put to the
+ * front of the queue and handled before the theme change takes place.
+ *
+ * TODO: create a callback that can be sent to any ThemeChangeListeners to notify them
+ * that the theme will be applied once the processing is done.
+ */
+ synchronized (mThemesToProcessQueue) {
+ Map<String, String> componentMap = request.getThemeComponentsMap();
+ for (Object key : componentMap.keySet()) {
+ if (ThemesColumns.MODIFIES_OVERLAYS.equals(key) ||
+ ThemesColumns.MODIFIES_NAVIGATION_BAR.equals(key) ||
+ ThemesColumns.MODIFIES_STATUS_BAR.equals(key) ||
+ ThemesColumns.MODIFIES_ICONS.equals(key)) {
+ String pkgName = componentMap.get(key);
+ if (mThemesToProcessQueue.indexOf(pkgName) > 0) {
+ mThemesToProcessQueue.remove(pkgName);
+ mThemesToProcessQueue.add(0, pkgName);
+ // We want to make sure these resources are taken care of first so
+ // send the dequeue message and place it in the front of the queue
+ msg = mResourceProcessingHandler.obtainMessage(
+ ResourceProcessingHandler.MESSAGE_DEQUEUE_AND_PROCESS_THEME);
+ mResourceProcessingHandler.sendMessageAtFrontOfQueue(msg);
+ }
+ }
+ }
+ }
+ msg = Message.obtain();
+ msg.what = ThemeWorkerHandler.MESSAGE_CHANGE_THEME;
+ msg.obj = request;
+ msg.arg1 = removePerAppThemes ? 1 : 0;
+ mHandler.sendMessage(msg);
+ }
+
+ @Override
+ public void applyDefaultTheme() {
+ enforcePermission();
+ Message msg = Message.obtain();
+ msg.what = ThemeWorkerHandler.MESSAGE_APPLY_DEFAULT_THEME;
+ mHandler.sendMessage(msg);
+ }
+
+ @Override
+ public boolean isThemeApplying() throws RemoteException {
+ enforcePermission();
+ return mIsThemeApplying;
+ }
+
+ @Override
+ public int getProgress() throws RemoteException {
+ enforcePermission();
+ synchronized(this) {
+ return mProgress;
+ }
+ }
+
+ @Override
+ public boolean processThemeResources(String themePkgName) throws RemoteException {
+ enforcePermission();
+ try {
+ mPM.getPackageInfo(themePkgName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Package doesn't exist so nothing to process
+ return false;
+ }
+ // Obtain a message and send it to the handler to process this theme
+ Message msg = mResourceProcessingHandler.obtainMessage(
+ ResourceProcessingHandler.MESSAGE_QUEUE_THEME_FOR_PROCESSING, 0, 0,
+ themePkgName);
+ mResourceProcessingHandler.sendMessage(msg);
+ return true;
+ }
+
+ @Override
+ public boolean isThemeBeingProcessed(String themePkgName) throws RemoteException {
+ enforcePermission();
+ synchronized (mThemesToProcessQueue) {
+ return mThemesToProcessQueue.contains(themePkgName);
+ }
+ }
+
+ @Override
+ public void registerThemeProcessingListener(IThemeProcessingListener listener)
+ throws RemoteException {
+ enforcePermission();
+ mProcessingListeners.register(listener);
+ }
+
+ @Override
+ public void unregisterThemeProcessingListener(IThemeProcessingListener listener)
+ throws RemoteException {
+ enforcePermission();
+ mProcessingListeners.unregister(listener);
+ }
+
+ @Override
+ public void rebuildResourceCache() throws RemoteException {
+ enforcePermission();
+ mHandler.sendEmptyMessage(ThemeWorkerHandler.MESSAGE_REBUILD_RESOURCE_CACHE);
+ }
+
+ @Override
+ public long getLastThemeChangeTime() {
+ return mLastThemeChangeTime;
+ }
+
+ @Override
+ public int getLastThemeChangeRequestType() {
+ return mLastThemeChangeRequestType;
+ }
+
+ private void enforcePermission() {
+ mContext.enforceCallingOrSelfPermission(ACCESS_THEME_MANAGER, null);
+ }
+ };
+}
diff --git a/cm/res/AndroidManifest.xml b/cm/res/AndroidManifest.xml
index b301559..b5e6f74 100644
--- a/cm/res/AndroidManifest.xml
+++ b/cm/res/AndroidManifest.xml
@@ -144,6 +144,27 @@
android:description="@string/permdesc_thirdPartyKeyguard"
android:protectionLevel="normal" />
+ <!-- Allows an application to use ThemeManager -->
+ <permission android:name="cyanogenmod.permission.ACCESS_THEME_MANAGER"
+ android:label="@string/permlab_accessThemeService"
+ android:description="@string/permdesc_accessThemeService"
+ android:protectionLevel="normal" />
+
+ <!-- Allows an application to read the current theme configuration and
+ get information about the various themes currently installed -->
+ <permission android:name="cyanogenmod.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 -->
+ <permission android:name="cyanogenmod.permission.WRITE_THEMES"
+ android:label="@string/permlab_writeThemes"
+ android:description="@string/permdesc_writeThemesDesc"
+ android:protectionLevel="normal" />
+
<application android:process="system"
android:persistent="true"
android:hasCode="false"
diff --git a/cm/res/res/values/strings.xml b/cm/res/res/values/strings.xml
index a18e11f..6feeb3f 100644
--- a/cm/res/res/values/strings.xml
+++ b/cm/res/res/values/strings.xml
@@ -136,4 +136,29 @@
<string name="touchscreen_gesture_extras">Extras</string>
<string name="touchscreen_gesture_haptic_feedback">Haptic feedback</string>
<string name="touchscreen_gesture_haptic_feedback_summary">Vibrate when a gesture got detected</string>
+
+ <!-- Theme installation error notification -->
+ <string name="theme_install_error_title">Failed to install theme</string>
+ <string name="theme_install_error_message"><xliff:g id="theme">%1$s</xliff:g> failed to install</string>
+
+ <!-- Theme reset notification -->
+ <string name="theme_reset_notification_title">Theme reset</string>
+ <string name="theme_reset_notification_message">System theme restored due to multiple app crashes.</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>
+
</resources>
diff --git a/cm/res/res/values/symbols.xml b/cm/res/res/values/symbols.xml
index 4a0d8a4..8973e7c 100644
--- a/cm/res/res/values/symbols.xml
+++ b/cm/res/res/values/symbols.xml
@@ -93,4 +93,13 @@
<java-symbol type="string" name="touchscreen_gesture_extras" />
<java-symbol type="string" name="touchscreen_gesture_haptic_feedback" />
<java-symbol type="string" name="touchscreen_gesture_haptic_feedback_summary" />
+
+ <!-- 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_message" />
+
</resources>
diff --git a/src/java/cyanogenmod/app/CMContextConstants.java b/src/java/cyanogenmod/app/CMContextConstants.java
index 6c5e39b..a1da29c 100644
--- a/src/java/cyanogenmod/app/CMContextConstants.java
+++ b/src/java/cyanogenmod/app/CMContextConstants.java
@@ -97,4 +97,18 @@ public final class CMContextConstants {
* @hide
*/
public static final String CM_PERFORMANCE_SERVICE = "cmperformance";
+
+ /**
+ * Controls changing and applying themes
+ *
+ * @hide
+ */
+ public static final String CM_THEME_SERVICE = "cmthemes";
+
+ /**
+ * Manages composed icons
+ *
+ * @hide
+ */
+ public static final String CM_ICON_CACHE_SERVICE = "cmiconcache";
}
diff --git a/src/java/cyanogenmod/content/Intent.java b/src/java/cyanogenmod/content/Intent.java
index 5a1f612..8b7c106 100644
--- a/src/java/cyanogenmod/content/Intent.java
+++ b/src/java/cyanogenmod/content/Intent.java
@@ -88,4 +88,50 @@ public class Intent {
public static final String ACTION_INITIALIZE_CM_HARDWARE =
"cyanogenmod.intent.action.INITIALIZE_CM_HARDWARE";
+ /**
+ * Broadcast Action: Indicate that an unrecoverable error happened during app launch.
+ * Could indicate that curently applied theme is malicious.
+ * @hide
+ */
+ public static final String ACTION_APP_FAILURE = "cyanogenmod.intent.action.APP_FAILURE";
+
+ /**
+ * Used to indicate that a theme package has been installed or un-installed.
+ */
+ public static final String CATEGORY_THEME_PACKAGE_INSTALLED_STATE_CHANGE =
+ "cyanogenmod.intent.category.THEME_PACKAGE_INSTALL_STATE_CHANGE";
+
+ /**
+ * Action sent from the provider when a theme has been fully installed. Fully installed
+ * means that the apk was installed by PackageManager and the theme resources were
+ * processed and cached by {@link org.cyanogenmod.platform.internal.ThemeManagerService}
+ * Requires the {@link cyanogenmod.platform.Manifest.permission#READ_THEMES} permission to
+ * receive this broadcast.
+ */
+ public static final String ACTION_THEME_INSTALLED =
+ "cyanogenmod.intent.action.THEME_INSTALLED";
+
+ /**
+ * Action sent from the provider when a theme has been updated.
+ * Requires the {@link cyanogenmod.platform.Manifest.permission#READ_THEMES} permission to
+ * receive this broadcast.
+ */
+ public static final String ACTION_THEME_UPDATED =
+ "cyanogenmod.intent.action.THEME_UPDATED";
+
+ /**
+ * Action sent from the provider when a theme has been removed.
+ * Requires the {@link cyanogenmod.platform.Manifest.permission#READ_THEMES} permission to
+ * receive this broadcast.
+ */
+ public static final String ACTION_THEME_REMOVED =
+ "cyanogenmod.intent.action.THEME_REMOVED";
+
+ /**
+ * Uri scheme used to broadcast the theme's package name when broadcasting
+ * {@link Intent#ACTION_THEME_INSTALLED} or
+ * {@link Intent#ACTION_THEME_REMOVED}
+ */
+ public static final String URI_SCHEME_PACKAGE = "package";
+
}
diff --git a/src/java/cyanogenmod/providers/ThemesContract.java b/src/java/cyanogenmod/providers/ThemesContract.java
new file mode 100644
index 0000000..4cdfeb9
--- /dev/null
+++ b/src/java/cyanogenmod/providers/ThemesContract.java
@@ -0,0 +1,717 @@
+/*
+ * Copyright (C) 2016 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 cyanogenmod.providers;
+
+import android.net.Uri;
+
+/**
+ * <p>
+ * The contract between the themes provider and applications. Contains
+ * definitions for the supported URIs and columns.
+ * </p>
+ */
+public class ThemesContract {
+ /** The authority for the themes provider */
+ public static final String AUTHORITY = "com.cyanogenmod.themes";
+ /** A content:// style uri to the authority for the themes provider */
+ 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";
+
+ /**
+ * 1 if theme has a live lock screen
+ * <P>Type: INTEGER</P>
+ * <P>Default: 0</P>
+ */
+ public static final String MODIFIES_LIVE_LOCK_SCREEN = "mods_live_lock_screen";
+
+ /**
+ * 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";
+
+ /**
+ * The install state of the theme.
+ * Can be one of the following:
+ * {@link InstallState#UNKNOWN}
+ * {@link InstallState#INSTALLING}
+ * {@link InstallState#UPDATING}
+ * {@link InstallState#INSTALLED}
+ * <P>Type: INTEGER</P>
+ * <P>Default: 0</P>
+ */
+ public static final String INSTALL_STATE = "install_state";
+
+ public static class InstallState {
+ public static final int UNKNOWN = 0;
+ public static final int INSTALLING = 1;
+ public static final int UPDATING = 2;
+ public static final int INSTALLED = 3;
+ }
+ }
+
+ /**
+ * 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";
+
+ /**
+ * The package name that corresponds to where this component was applied from previously
+ * <P>Type: String</P>
+ */
+ public static final String COL_PREV_VALUE = "previous_value";
+
+ /**
+ * Time when this entry was last updated
+ * <P>Type: INTEGER</P>
+ */
+ public static final String COL_UPDATE_TIME = "update_time";
+
+ /*
+ * The unique ID for the component within a theme.
+ * Always 0 unless multiples of a component exist.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String COL_COMPONENT_ID = "component_id";
+
+ /**
+ * 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 KEY_LIVE_LOCK_SCREEN = "mixnmatch_live_lock_screen";
+
+ 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,
+ KEY_LIVE_LOCK_SCREEN
+ };
+
+ /**
+ * 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");
+ } else if (component.equals(MixnMatchColumns.KEY_LIVE_LOCK_SCREEN)) {
+ throw new IllegalArgumentException(
+ "Live lock screen 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;
+ } else if (component.equals(ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN)) {
+ return MixnMatchColumns.KEY_LIVE_LOCK_SCREEN;
+ }
+ 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;
+ } else if (mixnmatchKey.equals(MixnMatchColumns.KEY_LIVE_LOCK_SCREEN)) {
+ return ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN;
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Table containing cached preview files for a given theme
+ */
+ public static class PreviewColumns {
+ /**
+ * Uri for retrieving the previews table.
+ * Querying the themes provider using this URI will return a cursor with a key and value
+ * columns, and a row for each component.
+ */
+ 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");
+
+ /**
+ * Uri for retrieving the default previews for the theme.
+ * Querying the themes provider using this URI will return a cursor with a single row
+ * containing all the previews for the default components of the current theme.
+ */
+ public static final Uri COMPONENTS_URI = Uri.withAppendedPath(AUTHORITY_URI,
+ "components_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";
+
+ /**
+ * The unique ID for the component within a theme.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String COMPONENT_ID = "component_id";
+
+ /**
+ * The unique key for a row. See the Valid key constants section below
+ * 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
+ */
+
+ /**
+ * Cached image of the themed status bar background.
+ * <P>Type: String (file path)</P>
+ */
+ public static final String STATUSBAR_BACKGROUND = "statusbar_background";
+
+ /**
+ * Cached image of the themed bluetooth status icon.
+ * <P>Type: String (file path)</P>
+ */
+ public static final String STATUSBAR_BLUETOOTH_ICON = "statusbar_bluetooth_icon";
+
+ /**
+ * Cached image of the themed wifi status icon.
+ * <P>Type: String (file path)</P>
+ */
+ public static final String STATUSBAR_WIFI_ICON = "statusbar_wifi_icon";
+
+ /**
+ * Cached image of the themed cellular signal status icon.
+ * <P>Type: String (file path)</P>
+ */
+ public static final String STATUSBAR_SIGNAL_ICON = "statusbar_signal_icon";
+
+ /**
+ * Cached image of the themed battery using portrait style.
+ * <P>Type: String (file path)</P>
+ */
+ public static final String STATUSBAR_BATTERY_PORTRAIT = "statusbar_battery_portrait";
+
+ /**
+ * Cached image of the themed battery using landscape style.
+ * <P>Type: String (file path)</P>
+ */
+ public static final String STATUSBAR_BATTERY_LANDSCAPE = "statusbar_battery_landscape";
+
+ /**
+ * Cached image of the themed battery using circle style.
+ * <P>Type: String (file path)</P>
+ */
+ public static final String STATUSBAR_BATTERY_CIRCLE = "statusbar_battery_circle";
+
+ /**
+ * 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";
+
+ /**
+ * 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";
+
+ /**
+ * Cached image of the themed navigation bar background.
+ * <P>Type: String (file path)</P>
+ */
+ public static final String NAVBAR_BACKGROUND = "navbar_background";
+
+ /**
+ * Cached image of the themed back button.
+ * <P>Type: String (file path)</P>
+ */
+ public static final String NAVBAR_BACK_BUTTON = "navbar_back_button";
+
+ /**
+ * Cached image of the themed home button.
+ * <P>Type: String (file path)</P>
+ */
+ public static final String NAVBAR_HOME_BUTTON = "navbar_home_button";
+
+ /**
+ * Cached image of the themed recents button.
+ * <P>Type: String (file path)</P>
+ */
+ public static final String NAVBAR_RECENT_BUTTON = "navbar_recent_button";
+
+ /**
+ * Cached image of the 1/3 icons
+ * <P>Type: String (file path)</P>
+ */
+ public static final String ICON_PREVIEW_1 = "icon_preview_1";
+
+ /**
+ * Cached image of the 2/3 icons
+ * <P>Type: String (file path)</P>
+ */
+ public static final String ICON_PREVIEW_2 = "icon_preview_2";
+
+ /**
+ * Cached image of the 3/3 icons
+ * <P>Type: String (file path)</P>
+ */
+ public static final String ICON_PREVIEW_3 = "icon_preview_3";
+
+ /**
+ * Full path to the theme's wallpaper asset.
+ * <P>Type: String (file path)</P>
+ */
+ public static final String WALLPAPER_FULL = "wallpaper_full";
+
+ /**
+ * Cached preview of the theme's wallpaper which is larger than the thumbnail
+ * but smaller than the full sized wallpaper.
+ * <P>Type: String (file path)</P>
+ */
+ public static final String WALLPAPER_PREVIEW = "wallpaper_preview";
+
+ /**
+ * Cached thumbnail of the theme's wallpaper
+ * <P>Type: String (file path)</P>
+ */
+ public static final String WALLPAPER_THUMBNAIL = "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: String (file path)</P>
+ */
+ public static final String LOCK_WALLPAPER_PREVIEW = "lock_wallpaper_preview";
+
+ /**
+ * Cached thumbnail of the theme's lockscreen wallpaper
+ * <P>Type: String (file path)</P>
+ */
+ public static final String LOCK_WALLPAPER_THUMBNAIL = "lock_wallpaper_thumbnail";
+
+ /**
+ * Cached preview of UI controls representing the theme's style
+ * <P>Type: String (file path)</P>
+ */
+ public static final String STYLE_PREVIEW = "style_preview";
+
+ /**
+ * Cached thumbnail preview of UI controls representing the theme's style
+ * <P>Type: String (file path)</P>
+ */
+ public static final String STYLE_THUMBNAIL = "style_thumbnail";
+
+ /**
+ * Cached thumbnail of the theme's boot animation
+ * <P>Type: String (file path)</P>
+ */
+ public static final String BOOTANIMATION_THUMBNAIL = "bootanimation_thumbnail";
+
+ /**
+ * Cached preview of live lock screen
+ * <P>Type: String (file path)</P>
+ */
+ public static final String LIVE_LOCK_SCREEN_PREVIEW = "live_lock_screen_preview";
+
+ /**
+ * Cached thumbnail preview of live lock screen
+ * <P>Type: String (file path)</P>
+ */
+ public static final String LIVE_LOCK_SCREEN_THUMBNAIL = "live_lock_screen_thumbnail";
+
+ public static final String[] VALID_KEYS = {
+ STATUSBAR_BACKGROUND,
+ STATUSBAR_BLUETOOTH_ICON,
+ STATUSBAR_WIFI_ICON,
+ STATUSBAR_SIGNAL_ICON,
+ STATUSBAR_BATTERY_PORTRAIT,
+ STATUSBAR_BATTERY_LANDSCAPE,
+ STATUSBAR_BATTERY_CIRCLE,
+ STATUSBAR_CLOCK_TEXT_COLOR,
+ STATUSBAR_WIFI_COMBO_MARGIN_END,
+ NAVBAR_BACKGROUND,
+ NAVBAR_BACK_BUTTON,
+ NAVBAR_HOME_BUTTON,
+ NAVBAR_RECENT_BUTTON,
+ ICON_PREVIEW_1,
+ ICON_PREVIEW_2,
+ ICON_PREVIEW_3,
+ WALLPAPER_FULL,
+ WALLPAPER_PREVIEW,
+ WALLPAPER_THUMBNAIL,
+ LOCK_WALLPAPER_PREVIEW,
+ LOCK_WALLPAPER_THUMBNAIL,
+ STYLE_PREVIEW,
+ STYLE_THUMBNAIL,
+ BOOTANIMATION_THUMBNAIL,
+ LIVE_LOCK_SCREEN_PREVIEW,
+ LIVE_LOCK_SCREEN_THUMBNAIL,
+ };
+ }
+}
diff --git a/src/java/cyanogenmod/themes/IThemeChangeListener.aidl b/src/java/cyanogenmod/themes/IThemeChangeListener.aidl
new file mode 100644
index 0000000..0700eb6
--- /dev/null
+++ b/src/java/cyanogenmod/themes/IThemeChangeListener.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2014-2016 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 cyanogenmod.themes;
+
+/** {@hide} */
+oneway interface IThemeChangeListener {
+ void onProgress(int progress);
+ void onFinish(boolean isSuccess);
+}
diff --git a/src/java/cyanogenmod/themes/IThemeProcessingListener.aidl b/src/java/cyanogenmod/themes/IThemeProcessingListener.aidl
new file mode 100644
index 0000000..648e1a9
--- /dev/null
+++ b/src/java/cyanogenmod/themes/IThemeProcessingListener.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2014-2016 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 cyanogenmod.themes;
+
+/** {@hide} */
+oneway interface IThemeProcessingListener {
+ void onFinishedProcessing(String pkgName);
+}
diff --git a/src/java/cyanogenmod/themes/IThemeService.aidl b/src/java/cyanogenmod/themes/IThemeService.aidl
new file mode 100644
index 0000000..fa186e9
--- /dev/null
+++ b/src/java/cyanogenmod/themes/IThemeService.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2014-2016 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 cyanogenmod.themes;
+
+import cyanogenmod.themes.IThemeChangeListener;
+import cyanogenmod.themes.IThemeProcessingListener;
+import cyanogenmod.themes.ThemeChangeRequest;
+
+import java.util.Map;
+
+/** {@hide} */
+interface IThemeService {
+ oneway void requestThemeChangeUpdates(in IThemeChangeListener listener);
+ oneway void removeUpdates(in IThemeChangeListener listener);
+
+ oneway void requestThemeChange(in ThemeChangeRequest request, boolean removePerAppThemes);
+ oneway void applyDefaultTheme();
+ boolean isThemeApplying();
+ int getProgress();
+
+ boolean processThemeResources(String themePkgName);
+ boolean isThemeBeingProcessed(String themePkgName);
+ oneway void registerThemeProcessingListener(in IThemeProcessingListener listener);
+ oneway void unregisterThemeProcessingListener(in IThemeProcessingListener listener);
+
+ oneway void rebuildResourceCache();
+
+ long getLastThemeChangeTime();
+ int getLastThemeChangeRequestType();
+}
diff --git a/src/java/cyanogenmod/themes/ThemeChangeRequest.aidl b/src/java/cyanogenmod/themes/ThemeChangeRequest.aidl
new file mode 100644
index 0000000..e1d9e4f
--- /dev/null
+++ b/src/java/cyanogenmod/themes/ThemeChangeRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2015-2016 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 cyanogenmod.themes;
+
+parcelable ThemeChangeRequest;
diff --git a/src/java/cyanogenmod/themes/ThemeChangeRequest.java b/src/java/cyanogenmod/themes/ThemeChangeRequest.java
new file mode 100644
index 0000000..5eb497e
--- /dev/null
+++ b/src/java/cyanogenmod/themes/ThemeChangeRequest.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2015-2016 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 cyanogenmod.themes;
+
+import android.content.pm.ThemeUtils;
+import android.content.res.ThemeConfig;
+import android.os.Parcel;
+import android.os.Parcelable;
+import cyanogenmod.os.Build;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static cyanogenmod.providers.ThemesContract.ThemesColumns.*;
+
+public final class ThemeChangeRequest implements Parcelable {
+ public static final int DEFAULT_WALLPAPER_ID = -1;
+
+ private final Map<String, String> mThemeComponents = new HashMap<>();
+ private final Map<String, String> mPerAppOverlays = new HashMap<>();
+ private RequestType mRequestType;
+ private long mWallpaperId = -1;
+
+ public String getOverlayThemePackageName() {
+ return getThemePackageNameForComponent(MODIFIES_OVERLAYS);
+ }
+
+ public String getStatusBarThemePackageName() {
+ return getThemePackageNameForComponent(MODIFIES_STATUS_BAR);
+ }
+
+ public String getNavBarThemePackageName() {
+ return getThemePackageNameForComponent(MODIFIES_NAVIGATION_BAR);
+ }
+
+ public String getFontThemePackageName() {
+ return getThemePackageNameForComponent(MODIFIES_FONTS);
+ }
+
+ public String getIconsThemePackageName() {
+ return getThemePackageNameForComponent(MODIFIES_ICONS);
+ }
+
+ public String getBootanimationThemePackageName() {
+ return getThemePackageNameForComponent(MODIFIES_BOOT_ANIM);
+ }
+
+ public String getWallpaperThemePackageName() {
+ return getThemePackageNameForComponent(MODIFIES_LAUNCHER);
+ }
+
+ public String getLockWallpaperThemePackageName() {
+ return getThemePackageNameForComponent(MODIFIES_LOCKSCREEN);
+ }
+
+ public String getAlarmThemePackageName() {
+ return getThemePackageNameForComponent(MODIFIES_ALARMS);
+ }
+
+ public String getNotificationThemePackageName() {
+ return getThemePackageNameForComponent(MODIFIES_NOTIFICATIONS);
+ }
+
+ public String getRingtoneThemePackageName() {
+ return getThemePackageNameForComponent(MODIFIES_RINGTONES);
+ }
+
+ public String getLiveLockScreenThemePackageName() {
+ return getThemePackageNameForComponent(MODIFIES_LIVE_LOCK_SCREEN);
+ }
+
+ public final Map<String, String> getThemeComponentsMap() {
+ return Collections.unmodifiableMap(mThemeComponents);
+ }
+
+ public long getWallpaperId() {
+ return mWallpaperId;
+ }
+
+ /**
+ * Get the mapping for per app themes
+ * @return A mapping of apps and the theme to apply for each one. or null if none set.
+ */
+ public final Map<String, String> getPerAppOverlays() {
+ return Collections.unmodifiableMap(mPerAppOverlays);
+ }
+
+ public int getNumChangesRequested() {
+ return mThemeComponents.size() + mPerAppOverlays.size();
+ }
+
+ public RequestType getReqeustType() {
+ return mRequestType;
+ }
+
+ private String getThemePackageNameForComponent(String componentName) {
+ return mThemeComponents.get(componentName);
+ }
+
+ private ThemeChangeRequest(Map<String, String> components, Map<String, String> perAppThemes,
+ RequestType requestType, long wallpaperId) {
+ if (components != null) {
+ mThemeComponents.putAll(components);
+ }
+ if (perAppThemes != null) {
+ mPerAppOverlays.putAll(perAppThemes);
+ }
+ mRequestType = requestType;
+ mWallpaperId = wallpaperId;
+ }
+
+ private ThemeChangeRequest(Parcel source) {
+ // Read parcelable version, make sure to define explicit changes
+ // within {@link Build.PARCELABLE_VERSION);
+ int version = source.readInt();
+ int size = source.readInt();
+ int start = source.dataPosition();
+
+ int numComponents = source.readInt();
+ for (int i = 0; i < numComponents; i++) {
+ mThemeComponents.put(source.readString(), source.readString());
+ }
+
+ numComponents = source.readInt();
+ for (int i = 0 ; i < numComponents; i++) {
+ mPerAppOverlays.put(source.readString(), source.readString());
+ }
+ mRequestType = RequestType.values()[source.readInt()];
+ mWallpaperId = source.readLong();
+ source.setDataPosition(start + size);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ // Write parcelable version, make sure to define explicit changes
+ // within {@link Build.PARCELABLE_VERSION);
+ dest.writeInt(Build.PARCELABLE_VERSION);
+ int sizePos = dest.dataPosition();
+ // Inject a placeholder that will store the parcel size from this point on
+ // (not including the size itself).
+ dest.writeInt(0);
+ int dataStartPos = dest.dataPosition();
+
+ dest.writeInt(mThemeComponents.size());
+ for (String component : mThemeComponents.keySet()) {
+ dest.writeString(component);
+ dest.writeString(mThemeComponents.get(component));
+ }
+ dest.writeInt((mPerAppOverlays.size()));
+ for (String appPkgName : mPerAppOverlays.keySet()) {
+ dest.writeString(appPkgName);
+ dest.writeString(mPerAppOverlays.get(appPkgName));
+ }
+ dest.writeInt(mRequestType.ordinal());
+ dest.writeLong(mWallpaperId);
+
+ // Go back and write size
+ int size = dest.dataPosition() - dataStartPos;
+ dest.setDataPosition(sizePos);
+ dest.writeInt(size);
+ dest.setDataPosition(dataStartPos + size);
+ }
+
+ public static final Parcelable.Creator<ThemeChangeRequest> CREATOR =
+ new Parcelable.Creator<ThemeChangeRequest>() {
+ @Override
+ public ThemeChangeRequest createFromParcel(Parcel source) {
+ return new ThemeChangeRequest(source);
+ }
+
+ @Override
+ public ThemeChangeRequest[] newArray(int size) {
+ return new ThemeChangeRequest[size];
+ }
+ };
+
+ public enum RequestType {
+ USER_REQUEST,
+ USER_REQUEST_MIXNMATCH,
+ THEME_UPDATED,
+ THEME_REMOVED,
+ THEME_RESET
+ }
+
+ public static class Builder {
+ Map<String, String> mThemeComponents = new HashMap<>();
+ Map<String, String> mPerAppOverlays = new HashMap<>();
+ RequestType mRequestType = RequestType.USER_REQUEST;
+ long mWallpaperId;
+
+ public Builder() {}
+
+ public Builder(ThemeConfig themeConfig) {
+ if (themeConfig != null) {
+ buildChangeRequestFromThemeConfig(themeConfig);
+ }
+ }
+
+ public Builder setOverlay(String pkgName) {
+ return setComponent(MODIFIES_OVERLAYS, pkgName);
+ }
+
+ public Builder setStatusBar(String pkgName) {
+ return setComponent(MODIFIES_STATUS_BAR, pkgName);
+ }
+
+ public Builder setNavBar(String pkgName) {
+ return setComponent(MODIFIES_NAVIGATION_BAR, pkgName);
+ }
+
+ public Builder setFont(String pkgName) {
+ return setComponent(MODIFIES_FONTS, pkgName);
+ }
+
+ public Builder setIcons(String pkgName) {
+ return setComponent(MODIFIES_ICONS, pkgName);
+ }
+
+ public Builder setBootanimation(String pkgName) {
+ return setComponent(MODIFIES_BOOT_ANIM, pkgName);
+ }
+
+ public Builder setWallpaper(String pkgName) {
+ return setComponent(MODIFIES_LAUNCHER, pkgName);
+ }
+
+ // Used in the case that more than one wallpaper exists for a given pkg name
+ public Builder setWallpaperId(long id) {
+ mWallpaperId = id;
+ return this;
+ }
+
+ public Builder setLockWallpaper(String pkgName) {
+ return setComponent(MODIFIES_LOCKSCREEN, pkgName);
+ }
+
+ public Builder setAlarm(String pkgName) {
+ return setComponent(MODIFIES_ALARMS, pkgName);
+ }
+
+ public Builder setNotification(String pkgName) {
+ return setComponent(MODIFIES_NOTIFICATIONS, pkgName);
+ }
+
+ public Builder setRingtone(String pkgName) {
+ return setComponent(MODIFIES_RINGTONES, pkgName);
+ }
+
+ public Builder setLiveLockScreen(String pkgName) {
+ return setComponent(MODIFIES_LIVE_LOCK_SCREEN, pkgName);
+ }
+
+ public Builder setComponent(String component, String pkgName) {
+ if (pkgName != null) {
+ mThemeComponents.put(component, pkgName);
+ } else {
+ mThemeComponents.remove(component);
+ }
+ return this;
+ }
+
+ public Builder setAppOverlay(String appPkgName, String themePkgName) {
+ if (appPkgName != null) {
+ if (themePkgName != null) {
+ mPerAppOverlays.put(appPkgName, themePkgName);
+ } else {
+ mPerAppOverlays.remove(appPkgName);
+ }
+ }
+
+ return this;
+ }
+
+ public Builder setRequestType(RequestType requestType) {
+ mRequestType = requestType != null ? requestType : RequestType.USER_REQUEST;
+ return this;
+ }
+
+ public ThemeChangeRequest build() {
+ return new ThemeChangeRequest(mThemeComponents, mPerAppOverlays,
+ mRequestType, mWallpaperId);
+ }
+
+ private void buildChangeRequestFromThemeConfig(ThemeConfig themeConfig) {
+ if (themeConfig.getFontPkgName() != null) {
+ this.setFont(themeConfig.getFontPkgName());
+ }
+ if (themeConfig.getIconPackPkgName() != null) {
+ this.setIcons(themeConfig.getIconPackPkgName());
+ }
+ if (themeConfig.getOverlayPkgName() != null) {
+ this.setOverlay(themeConfig.getOverlayPkgName());
+ }
+ if (themeConfig.getOverlayForStatusBar() != null) {
+ this.setStatusBar(themeConfig.getOverlayForStatusBar());
+ }
+ if (themeConfig.getOverlayForNavBar() != null) {
+ this.setNavBar(themeConfig.getOverlayForNavBar());
+ }
+
+ // Check if there are any per-app overlays using this theme
+ final Map<String, ThemeConfig.AppTheme> themes = themeConfig.getAppThemes();
+ for (String appPkgName : themes.keySet()) {
+ if (ThemeUtils.isPerAppThemeComponent(appPkgName)) {
+ this.setAppOverlay(appPkgName, themes.get(appPkgName).getOverlayPkgName());
+ }
+ }
+ }
+ }
+}
diff --git a/src/java/cyanogenmod/themes/ThemeManager.java b/src/java/cyanogenmod/themes/ThemeManager.java
new file mode 100644
index 0000000..4c575ae
--- /dev/null
+++ b/src/java/cyanogenmod/themes/ThemeManager.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2014-2016 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 cyanogenmod.themes;
+
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.ArraySet;
+import android.util.Log;
+
+import cyanogenmod.app.CMContextConstants;
+import cyanogenmod.themes.ThemeChangeRequest.RequestType;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Manages changing and applying of themes.
+ * <p>Get an instance of this class by calling blah blah blah</p>
+ */
+public class ThemeManager {
+ private static final String TAG = ThemeManager.class.getName();
+ private static IThemeService sService;
+ private static ThemeManager sInstance;
+ private static Handler mHandler;
+
+ private Set<ThemeChangeListener> mChangeListeners = new ArraySet<>();
+
+ private Set<ThemeProcessingListener> mProcessingListeners = new ArraySet<>();
+
+ private ThemeManager() {
+ mHandler = new Handler(Looper.getMainLooper());
+ sService = getService();
+ }
+
+ public static ThemeManager getInstance() {
+ if (sInstance == null) {
+ sInstance = new ThemeManager();
+ }
+
+ return sInstance;
+ }
+
+ private static IThemeService getService() {
+ if (sService != null) {
+ return sService;
+ }
+ IBinder b = ServiceManager.getService(CMContextConstants.CM_THEME_SERVICE);
+ if (b != null) {
+ sService = IThemeService.Stub.asInterface(b);
+ return sService;
+ }
+ return null;
+ }
+
+ 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<>();
+ 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<>();
+ 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<>();
+ 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);
+ }
+ }
+ }
+ }
+ });
+ }
+ };
+
+
+ /**
+ * @deprecated Use {@link ThemeManager#registerThemeChangeListener(ThemeChangeListener)} instead
+ */
+ public void addClient(ThemeChangeListener listener) {
+ registerThemeChangeListener(listener);
+ }
+
+ /**
+ * @deprecated Use {@link ThemeManager#unregisterThemeChangeListener(ThemeChangeListener)}
+ * instead
+ */
+ public void removeClient(ThemeChangeListener listener) {
+ unregisterThemeChangeListener(listener);
+ }
+
+ /**
+ * @deprecated Use {@link ThemeManager#unregisterThemeChangeListener(ThemeChangeListener)}
+ * instead
+ */
+ public void onClientPaused(ThemeChangeListener listener) {
+ unregisterThemeChangeListener(listener);
+ }
+
+ /**
+ * @deprecated Use {@link ThemeManager#registerThemeChangeListener(ThemeChangeListener)} instead
+ */
+ public void onClientResumed(ThemeChangeListener listener) {
+ registerThemeChangeListener(listener);
+ }
+
+ /**
+ * @deprecated Use {@link ThemeManager#unregisterThemeChangeListener(ThemeChangeListener)}
+ * instead
+ */
+ public void onClientDestroyed(ThemeChangeListener listener) {
+ unregisterThemeChangeListener(listener);
+ }
+
+ /**
+ * Register a {@link ThemeChangeListener} to be notified when a theme is done being processed.
+ * @param listener {@link ThemeChangeListener} to register
+ */
+ public void registerThemeChangeListener(ThemeChangeListener listener) {
+ synchronized (mChangeListeners) {
+ if (mChangeListeners.contains(listener)) {
+ throw new IllegalArgumentException("Listener already registered");
+ }
+ if (mChangeListeners.size() == 0) {
+ try {
+ sService.requestThemeChangeUpdates(mThemeChangeListener);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to register listener", e);
+ }
+ }
+ mChangeListeners.add(listener);
+ }
+ }
+
+ /**
+ * Unregister a {@link ThemeChangeListener}
+ * @param listener {@link ThemeChangeListener} to unregister
+ */
+ public void unregisterThemeChangeListener(ThemeChangeListener listener) {
+ synchronized (mChangeListeners) {
+ mChangeListeners.remove(listener);
+ if (mChangeListeners.size() == 0) {
+ try {
+ sService.removeUpdates(mThemeChangeListener);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to unregister listener", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Register a {@link ThemeProcessingListener} to be notified when a theme is done being
+ * processed.
+ * @param listener {@link ThemeProcessingListener} to register
+ */
+ public void registerProcessingListener(ThemeProcessingListener listener) {
+ synchronized (mProcessingListeners) {
+ if (mProcessingListeners.contains(listener)) {
+ throw new IllegalArgumentException("Listener already registered");
+ }
+ if (mProcessingListeners.size() == 0) {
+ try {
+ sService.registerThemeProcessingListener(mThemeProcessingListener);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to register listener", e);
+ }
+ }
+ mProcessingListeners.add(listener);
+ }
+ }
+
+ /**
+ * Unregister a {@link ThemeProcessingListener}.
+ * @param listener {@link ThemeProcessingListener} to unregister
+ */
+ public void unregisterProcessingListener(ThemeChangeListener listener) {
+ synchronized (mProcessingListeners) {
+ mProcessingListeners.remove(listener);
+ if (mProcessingListeners.size() == 0) {
+ try {
+ sService.unregisterThemeProcessingListener(mThemeProcessingListener);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to unregister listener", e);
+ }
+ }
+ }
+ }
+
+ public void requestThemeChange(String pkgName, List<String> components) {
+ requestThemeChange(pkgName, components, true);
+ }
+
+ public void requestThemeChange(String pkgName, List<String> components,
+ boolean removePerAppThemes) {
+ Map<String, String> componentMap = new HashMap<>(components.size());
+ for (String component : components) {
+ componentMap.put(component, pkgName);
+ }
+ requestThemeChange(componentMap, removePerAppThemes);
+ }
+
+ public void requestThemeChange(Map<String, String> componentMap) {
+ requestThemeChange(componentMap, true);
+ }
+
+ public void requestThemeChange(Map<String, String> componentMap, boolean removePerAppThemes) {
+ ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder();
+ for (String component : componentMap.keySet()) {
+ builder.setComponent(component, componentMap.get(component));
+ }
+
+ requestThemeChange(builder.build(), removePerAppThemes);
+ }
+
+ public void requestThemeChange(ThemeChangeRequest request, boolean removePerAppThemes) {
+ try {
+ sService.requestThemeChange(request, removePerAppThemes);
+ } catch (RemoteException e) {
+ logThemeServiceException(e);
+ }
+ }
+
+ public void applyDefaultTheme() {
+ try {
+ sService.applyDefaultTheme();
+ } catch (RemoteException e) {
+ logThemeServiceException(e);
+ }
+ }
+
+ public boolean isThemeApplying() {
+ try {
+ return sService.isThemeApplying();
+ } catch (RemoteException e) {
+ logThemeServiceException(e);
+ }
+
+ return false;
+ }
+
+ public boolean isThemeBeingProcessed(String themePkgName) {
+ try {
+ return sService.isThemeBeingProcessed(themePkgName);
+ } catch (RemoteException e) {
+ logThemeServiceException(e);
+ }
+ return false;
+ }
+
+ public int getProgress() {
+ try {
+ return sService.getProgress();
+ } catch (RemoteException e) {
+ logThemeServiceException(e);
+ }
+ return -1;
+ }
+
+ public boolean processThemeResources(String themePkgName) {
+ try {
+ return sService.processThemeResources(themePkgName);
+ } catch (RemoteException e) {
+ logThemeServiceException(e);
+ }
+ return false;
+ }
+
+ public long getLastThemeChangeTime() {
+ try {
+ return sService.getLastThemeChangeTime();
+ } catch (RemoteException e) {
+ logThemeServiceException(e);
+ }
+ return 0;
+ }
+
+ public ThemeChangeRequest.RequestType getLastThemeChangeRequestType() {
+ try {
+ int type = sService.getLastThemeChangeRequestType();
+ return (type >= 0 && type < RequestType.values().length)
+ ? RequestType.values()[type]
+ : null;
+ } catch (RemoteException e) {
+ logThemeServiceException(e);
+ }
+
+ return null;
+ }
+
+ 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/src/java/org/cyanogenmod/internal/themes/IIconCacheManager.aidl b/src/java/org/cyanogenmod/internal/themes/IIconCacheManager.aidl
new file mode 100644
index 0000000..c69e082
--- /dev/null
+++ b/src/java/org/cyanogenmod/internal/themes/IIconCacheManager.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016 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 org.cyanogenmod.internal.themes;
+
+import android.graphics.Bitmap;
+
+/** @hide */
+interface IIconCacheManager {
+ boolean cacheComposedIcon(in Bitmap icon, String path);
+}
diff --git a/src/java/org/cyanogenmod/internal/util/ImageUtils.java b/src/java/org/cyanogenmod/internal/util/ImageUtils.java
new file mode 100644
index 0000000..c67c23c
--- /dev/null
+++ b/src/java/org/cyanogenmod/internal/util/ImageUtils.java
@@ -0,0 +1,332 @@
+/*
+ * 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 org.cyanogenmod.internal.util;
+
+import android.app.WallpaperManager;
+import android.content.Context;
+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.text.TextUtils;
+import android.util.Log;
+import android.webkit.URLUtil;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+import cyanogenmod.providers.ThemesContract.PreviewColumns;
+import cyanogenmod.providers.ThemesContract.ThemesColumns;
+
+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, long wallpaperId,
+ 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, wallpaperId, 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, wallpaperId, 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, long componentId,
+ Context context) {
+ String wpPath;
+ if (TextUtils.isEmpty(pkgName) || context == null) {
+ return null;
+ }
+
+ InputStream inputStream = null;
+ String selection = 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();
+ wpPath = queryWpPathFromComponentId(context, pkgName, componentId);
+ if (wpPath == null) 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;
+ }
+
+ private static String queryWpPathFromComponentId(Context context, String pkgName,
+ long componentId) {
+ String wpPath = null;
+ String[] projection = new String[] { PreviewColumns.COL_VALUE };
+ String selection = ThemesColumns.PKG_NAME + "=? AND " +
+ PreviewColumns.COMPONENT_ID + "=? AND " +
+ PreviewColumns.COL_KEY + "=?";
+ String[] selectionArgs = new String[] {
+ pkgName,
+ Long.toString(componentId),
+ PreviewColumns.WALLPAPER_FULL
+ };
+
+ Cursor c = context.getContentResolver()
+ .query(PreviewColumns.COMPONENTS_URI,
+ projection, selection, selectionArgs, null);
+ if (c != null) {
+ try {
+ if (c.moveToFirst()) {
+ int valIdx = c.getColumnIndex(PreviewColumns.COL_VALUE);
+ wpPath = c.getString(valIdx);
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Could not get wallpaper path", e);
+ } finally {
+ c.close();
+ }
+ }
+ return wpPath;
+ }
+}
+
diff --git a/src/java/org/cyanogenmod/internal/util/ThemeUtils.java b/src/java/org/cyanogenmod/internal/util/ThemeUtils.java
new file mode 100644
index 0000000..ef51ced
--- /dev/null
+++ b/src/java/org/cyanogenmod/internal/util/ThemeUtils.java
@@ -0,0 +1,687 @@
+/*
+ * Copyright (C) 2016 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 org.cyanogenmod.internal.util;
+
+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.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageParser;
+import android.content.res.AssetManager;
+import android.content.res.ThemeConfig;
+import android.database.Cursor;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.FileUtils;
+import android.os.SystemProperties;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+
+import android.view.WindowManager;
+import cyanogenmod.providers.CMSettings;
+import cyanogenmod.providers.ThemesContract.ThemesColumns;
+
+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.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+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.class.getSimpleName();
+
+ // Package name for any app which does not have a specific theme applied
+ private static final String DEFAULT_PKG = "default";
+
+ private static final Set<String> SUPPORTED_THEME_COMPONENTS = new ArraySet<>();
+
+ static {
+ SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_ALARMS);
+ SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_BOOT_ANIM);
+ SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_FONTS);
+ SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_ICONS);
+ SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_LAUNCHER);
+ SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN);
+ SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_LOCKSCREEN);
+ SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_NAVIGATION_BAR);
+ SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_NOTIFICATIONS);
+ SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_OVERLAYS);
+ SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_RINGTONES);
+ SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_STATUS_BAR);
+ }
+
+ // Constants for theme change broadcast
+ public static final String ACTION_THEME_CHANGED = "org.cyanogenmod.intent.action.THEME_CHANGED";
+ public static final String EXTRA_COMPONENTS = "components";
+ public static final String EXTRA_REQUEST_TYPE = "request_type";
+ public static final String EXTRA_UPDATE_TIME = "update_time";
+
+ // path to asset lockscreen and wallpapers directory
+ public static final String LOCKSCREEN_WALLPAPER_PATH = "lockscreen";
+ public static final String WALLPAPER_PATH = "wallpapers";
+
+ // 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 int SYSTEM_TARGET_API = 0;
+
+ /* Path to cached theme resources */
+ public static final String RESOURCE_CACHE_DIR = "/data/resource-cache/";
+
+ /* 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 IDMAP_SUFFIX = "@idmap";
+ public static final String COMMON_RES_TARGET = "common";
+
+ public static final String ICON_HASH_FILENAME = "hash";
+
+ public static final String FONT_XML = "fonts.xml";
+
+ public static String getDefaultThemePackageName(Context context) {
+ final String defaultThemePkg = CMSettings.Secure.getString(context.getContentResolver(),
+ CMSettings.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;
+ }
+
+ /**
+ * Returns a mutable list of all theme components
+ * @return
+ */
+ public static List<String> getAllComponents() {
+ List<String> components = new ArrayList<>(SUPPORTED_THEME_COMPONENTS.size());
+ components.addAll(SUPPORTED_THEME_COMPONENTS);
+ 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 selection = ThemesColumns.PKG_NAME + "= ?";
+ String[] selectionArgs = new String[]{ pkgName };
+ Cursor c = context.getContentResolver().query(ThemesColumns.CONTENT_URI,
+ null, selection, selectionArgs, null);
+
+ if (c != null) {
+ if (c.moveToFirst()) {
+ List<String> allComponents = getAllComponents();
+ for (String component : allComponents) {
+ int index = c.getColumnIndex(component);
+ if (c.getInt(index) == 1) {
+ supportedComponents.add(component);
+ }
+ }
+ }
+ c.close();
+ }
+ 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 (!DEFAULT_PKG.equals(defaultThemePkg)) {
+ defaultComponents = getSupportedComponents(context, defaultThemePkg);
+ }
+
+ Map<String, String> componentMap = new HashMap<>(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;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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 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);
+ }
+
+ public static void clearIconCache() {
+ FileUtils.deleteContents(new File(SYSTEM_THEME_ICON_CACHE_DIR));
+ }
+
+ 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_WALLPAPER_PATH);
+ String asset = getFirstNonEmptyAsset(assets);
+ if (asset == null) return null;
+ return LOCKSCREEN_WALLPAPER_PATH + File.separator + asset;
+ }
+
+ public static String getWallpaperPath(AssetManager assetManager) throws IOException {
+ String[] assets = assetManager.list(WALLPAPER_PATH);
+ String asset = getFirstNonEmptyAsset(assets);
+ if (asset == null) return null;
+ return WALLPAPER_PATH + File.separator + asset;
+ }
+
+ public static List<String> getWallpaperPathList(AssetManager assetManager)
+ throws IOException {
+ List<String> wallpaperList = new ArrayList<String>();
+ String[] assets = assetManager.list(WALLPAPER_PATH);
+ for (String asset : assets) {
+ if (!TextUtils.isEmpty(asset)) {
+ wallpaperList.add(WALLPAPER_PATH + File.separator + asset);
+ }
+ }
+ return wallpaperList;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Creates a theme'd context using the overlay applied to SystemUI
+ * @param context Base context
+ * @return Themed context
+ */
+ public static Context createUiContext(final Context context) {
+ try {
+ Context uiContext = context.createPackageContext("com.android.systemui",
+ Context.CONTEXT_RESTRICTED);
+ return new ThemedUiContext(uiContext, context.getApplicationContext());
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+
+ return null;
+ }
+
+ /**
+ * 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 InputStream getInputStreamFromAsset(Context ctx, String path) throws IOException {
+ if (ctx == null || path == null) return null;
+
+ InputStream is;
+ String ASSET_BASE = "file:///android_asset/";
+ path = path.substring(ASSET_BASE.length());
+ AssetManager assets = ctx.getAssets();
+ is = assets.open(path);
+ return is;
+ }
+
+ /**
+ * Convenience method to determine if a theme component is a per app theme and not a standard
+ * component.
+ * @param component
+ * @return
+ */
+ public static boolean isPerAppThemeComponent(String component) {
+ return !(DEFAULT_PKG.equals(component)
+ || ThemeConfig.SYSTEMUI_STATUS_BAR_PKG.equals(component)
+ || ThemeConfig.SYSTEMUI_NAVBAR_PKG.equals(component));
+ }
+
+ /**
+ * 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.
+ * @param assets
+ * @return
+ */
+ private static String getFirstNonEmptyAsset(String[] assets) {
+ if (assets == null) return null;
+ String filename = null;
+ for(String asset : assets) {
+ if (!TextUtils.isEmpty(asset)) {
+ filename = asset;
+ break;
+ }
+ }
+ return filename;
+ }
+
+ 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);
+ }
+ }
+ }
+
+ private static class ThemedUiContext extends ContextWrapper {
+ private Context mAppContext;
+
+ public ThemedUiContext(Context context, Context appContext) {
+ super(context);
+ mAppContext = appContext;
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return mAppContext;
+ }
+
+ @Override
+ public String getPackageName() {
+ return mAppContext.getPackageName();
+ }
+ }
+}
diff --git a/system-api/cm_system-current.txt b/system-api/cm_system-current.txt
index cced235..0d8efa4 100644
--- a/system-api/cm_system-current.txt
+++ b/system-api/cm_system-current.txt
@@ -371,8 +371,13 @@ package cyanogenmod.content {
ctor public Intent();
field public static final java.lang.String ACTION_PROTECTED = "cyanogenmod.intent.action.PACKAGE_PROTECTED";
field public static final java.lang.String ACTION_PROTECTED_CHANGED = "cyanogenmod.intent.action.PROTECTED_COMPONENT_UPDATE";
+ field public static final java.lang.String ACTION_THEME_INSTALLED = "cyanogenmod.intent.action.THEME_INSTALLED";
+ field public static final java.lang.String ACTION_THEME_REMOVED = "cyanogenmod.intent.action.THEME_REMOVED";
+ field public static final java.lang.String ACTION_THEME_UPDATED = "cyanogenmod.intent.action.THEME_UPDATED";
+ field public static final java.lang.String CATEGORY_THEME_PACKAGE_INSTALLED_STATE_CHANGE = "cyanogenmod.intent.category.THEME_PACKAGE_INSTALL_STATE_CHANGE";
field public static final java.lang.String EXTRA_PROTECTED_COMPONENTS = "cyanogenmod.intent.extra.PACKAGE_PROTECTED_COMPONENTS";
field public static final java.lang.String EXTRA_PROTECTED_STATE = "cyanogenmod.intent.extra.PACKAGE_PROTECTED_STATE";
+ field public static final java.lang.String URI_SCHEME_PACKAGE = "package";
}
}
@@ -556,6 +561,7 @@ package cyanogenmod.platform {
public static final class Manifest.permission {
ctor public Manifest.permission();
field public static final java.lang.String ACCESS_APP_SUGGESTIONS = "cyanogenmod.permission.ACCESS_APP_SUGGESTIONS";
+ field public static final java.lang.String ACCESS_THEME_MANAGER = "cyanogenmod.permission.ACCESS_THEME_MANAGER";
field public static final java.lang.String HARDWARE_ABSTRACTION_ACCESS = "cyanogenmod.permission.HARDWARE_ABSTRACTION_ACCESS";
field public static final java.lang.String MANAGE_ALARMS = "cyanogenmod.permission.MANAGE_ALARMS";
field public static final java.lang.String MANAGE_PERSISTENT_STORAGE = "cyanogenmod.permission.MANAGE_PERSISTENT_STORAGE";
@@ -567,6 +573,7 @@ package cyanogenmod.platform {
field public static final java.lang.String PUBLISH_CUSTOM_TILE = "cyanogenmod.permission.PUBLISH_CUSTOM_TILE";
field public static final java.lang.String READ_ALARMS = "cyanogenmod.permission.READ_ALARMS";
field public static final java.lang.String READ_MSIM_PHONE_STATE = "cyanogenmod.permission.READ_MSIM_PHONE_STATE";
+ field public static final java.lang.String READ_THEMES = "cyanogenmod.permission.READ_THEMES";
field public static final java.lang.String THIRD_PARTY_KEYGUARD = "android.permission.THIRD_PARTY_KEYGUARD";
field public static final java.lang.String WRITE_ALARMS = "cyanogenmod.permission.WRITE_ALARMS";
field public static final java.lang.String WRITE_SECURE_SETTINGS = "cyanogenmod.permission.WRITE_SECURE_SETTINGS";
@@ -878,6 +885,126 @@ package cyanogenmod.providers {
field public static final java.lang.String ZEN_PRIORITY_ALLOW_LIGHTS = "zen_priority_allow_lights";
}
+ public class ThemesContract {
+ ctor public ThemesContract();
+ field public static final java.lang.String AUTHORITY = "com.cyanogenmod.themes";
+ field public static final android.net.Uri AUTHORITY_URI;
+ }
+
+ public static class ThemesContract.MixnMatchColumns {
+ ctor public ThemesContract.MixnMatchColumns();
+ method public static java.lang.String componentToImageColName(java.lang.String);
+ method public static java.lang.String componentToMixNMatchKey(java.lang.String);
+ method public static java.lang.String mixNMatchKeyToComponent(java.lang.String);
+ field public static final java.lang.String COL_COMPONENT_ID = "component_id";
+ field public static final java.lang.String COL_KEY = "key";
+ field public static final java.lang.String COL_PREV_VALUE = "previous_value";
+ field public static final java.lang.String COL_UPDATE_TIME = "update_time";
+ field public static final java.lang.String COL_VALUE = "value";
+ field public static final android.net.Uri CONTENT_URI;
+ field public static final java.lang.String KEY_ALARM = "mixnmatch_alarm";
+ field public static final java.lang.String KEY_BOOT_ANIM = "mixnmatch_boot_anim";
+ field public static final java.lang.String KEY_FONT = "mixnmatch_font";
+ field public static final java.lang.String KEY_HOMESCREEN = "mixnmatch_homescreen";
+ field public static final java.lang.String KEY_ICONS = "mixnmatch_icons";
+ field public static final java.lang.String KEY_LIVE_LOCK_SCREEN = "mixnmatch_live_lock_screen";
+ field public static final java.lang.String KEY_LOCKSCREEN = "mixnmatch_lockscreen";
+ field public static final java.lang.String KEY_NAVIGATION_BAR = "mixnmatch_navigation_bar";
+ field public static final java.lang.String KEY_NOTIFICATIONS = "mixnmatch_notifications";
+ field public static final java.lang.String KEY_OVERLAYS = "mixnmatch_overlays";
+ field public static final java.lang.String KEY_RINGTONE = "mixnmatch_ringtone";
+ field public static final java.lang.String KEY_STATUS_BAR = "mixnmatch_status_bar";
+ field public static final java.lang.String[] ROWS;
+ }
+
+ public static class ThemesContract.PreviewColumns {
+ ctor public ThemesContract.PreviewColumns();
+ field public static final android.net.Uri APPLIED_URI;
+ field public static final java.lang.String BOOTANIMATION_THUMBNAIL = "bootanimation_thumbnail";
+ field public static final java.lang.String COL_KEY = "key";
+ field public static final java.lang.String COL_VALUE = "value";
+ field public static final android.net.Uri COMPONENTS_URI;
+ field public static final java.lang.String COMPONENT_ID = "component_id";
+ field public static final android.net.Uri CONTENT_URI;
+ field public static final java.lang.String ICON_PREVIEW_1 = "icon_preview_1";
+ field public static final java.lang.String ICON_PREVIEW_2 = "icon_preview_2";
+ field public static final java.lang.String ICON_PREVIEW_3 = "icon_preview_3";
+ field public static final java.lang.String LIVE_LOCK_SCREEN_PREVIEW = "live_lock_screen_preview";
+ field public static final java.lang.String LIVE_LOCK_SCREEN_THUMBNAIL = "live_lock_screen_thumbnail";
+ field public static final java.lang.String LOCK_WALLPAPER_PREVIEW = "lock_wallpaper_preview";
+ field public static final java.lang.String LOCK_WALLPAPER_THUMBNAIL = "lock_wallpaper_thumbnail";
+ field public static final java.lang.String NAVBAR_BACKGROUND = "navbar_background";
+ field public static final java.lang.String NAVBAR_BACK_BUTTON = "navbar_back_button";
+ field public static final java.lang.String NAVBAR_HOME_BUTTON = "navbar_home_button";
+ field public static final java.lang.String NAVBAR_RECENT_BUTTON = "navbar_recent_button";
+ field public static final java.lang.String STATUSBAR_BACKGROUND = "statusbar_background";
+ field public static final java.lang.String STATUSBAR_BATTERY_CIRCLE = "statusbar_battery_circle";
+ field public static final java.lang.String STATUSBAR_BATTERY_LANDSCAPE = "statusbar_battery_landscape";
+ field public static final java.lang.String STATUSBAR_BATTERY_PORTRAIT = "statusbar_battery_portrait";
+ field public static final java.lang.String STATUSBAR_BLUETOOTH_ICON = "statusbar_bluetooth_icon";
+ field public static final java.lang.String STATUSBAR_CLOCK_TEXT_COLOR = "statusbar_clock_text_color";
+ field public static final java.lang.String STATUSBAR_SIGNAL_ICON = "statusbar_signal_icon";
+ field public static final java.lang.String STATUSBAR_WIFI_COMBO_MARGIN_END = "wifi_combo_margin_end";
+ field public static final java.lang.String STATUSBAR_WIFI_ICON = "statusbar_wifi_icon";
+ field public static final java.lang.String STYLE_PREVIEW = "style_preview";
+ field public static final java.lang.String STYLE_THUMBNAIL = "style_thumbnail";
+ field public static final java.lang.String THEME_ID = "theme_id";
+ field public static final java.lang.String[] VALID_KEYS;
+ field public static final java.lang.String WALLPAPER_FULL = "wallpaper_full";
+ field public static final java.lang.String WALLPAPER_PREVIEW = "wallpaper_preview";
+ field public static final java.lang.String WALLPAPER_THUMBNAIL = "wallpaper_thumbnail";
+ field public static final java.lang.String _ID = "_id";
+ }
+
+ public static class ThemesContract.ThemesColumns {
+ ctor public ThemesContract.ThemesColumns();
+ field public static final java.lang.String AUTHOR = "author";
+ field public static final java.lang.String BOOT_ANIM_URI = "bootanim_uri";
+ field public static final android.net.Uri CONTENT_URI;
+ field public static final java.lang.String DATE_CREATED = "created";
+ field public static final java.lang.String FONT_URI = "font_uri";
+ field public static final java.lang.String HOMESCREEN_URI = "homescreen_uri";
+ field public static final java.lang.String ICON_URI = "icon_uri";
+ field public static final java.lang.String INSTALL_STATE = "install_state";
+ field public static final java.lang.String INSTALL_TIME = "install_time";
+ field public static final java.lang.String IS_DEFAULT_THEME = "is_default_theme";
+ field public static final java.lang.String IS_LEGACY_ICONPACK = "is_legacy_iconpack";
+ field public static final java.lang.String IS_LEGACY_THEME = "is_legacy_theme";
+ field public static final java.lang.String LAST_UPDATE_TIME = "updateTime";
+ field public static final java.lang.String LOCKSCREEN_URI = "lockscreen_uri";
+ field public static final java.lang.String MODIFIES_ALARMS = "mods_alarms";
+ field public static final java.lang.String MODIFIES_BOOT_ANIM = "mods_bootanim";
+ field public static final java.lang.String MODIFIES_FONTS = "mods_fonts";
+ field public static final java.lang.String MODIFIES_ICONS = "mods_icons";
+ field public static final java.lang.String MODIFIES_LAUNCHER = "mods_homescreen";
+ field public static final java.lang.String MODIFIES_LIVE_LOCK_SCREEN = "mods_live_lock_screen";
+ field public static final java.lang.String MODIFIES_LOCKSCREEN = "mods_lockscreen";
+ field public static final java.lang.String MODIFIES_NAVIGATION_BAR = "mods_navigation_bar";
+ field public static final java.lang.String MODIFIES_NOTIFICATIONS = "mods_notifications";
+ field public static final java.lang.String MODIFIES_OVERLAYS = "mods_overlays";
+ field public static final java.lang.String MODIFIES_RINGTONES = "mods_ringtones";
+ field public static final java.lang.String MODIFIES_STATUS_BAR = "mods_status_bar";
+ field public static final java.lang.String OVERLAYS_URI = "overlays_uri";
+ field public static final java.lang.String PKG_NAME = "pkg_name";
+ field public static final java.lang.String PRESENT_AS_THEME = "present_as_theme";
+ field public static final java.lang.String PRIMARY_COLOR = "primary_color";
+ field public static final java.lang.String SECONDARY_COLOR = "secondary_color";
+ field public static final java.lang.String STATUSBAR_URI = "status_uri";
+ field public static final java.lang.String STYLE_URI = "style_uri";
+ field public static final java.lang.String TARGET_API = "target_api";
+ field public static final java.lang.String TITLE = "title";
+ field public static final java.lang.String WALLPAPER_URI = "wallpaper_uri";
+ field public static final java.lang.String _ID = "_id";
+ }
+
+ public static class ThemesContract.ThemesColumns.InstallState {
+ ctor public ThemesContract.ThemesColumns.InstallState();
+ field public static final int INSTALLED = 3; // 0x3
+ field public static final int INSTALLING = 1; // 0x1
+ field public static final int UNKNOWN = 0; // 0x0
+ field public static final int UPDATING = 2; // 0x2
+ }
+
}
package cyanogenmod.util {