From b3ea2859fd920ea68afc3ae7010b665e2dd515ea Mon Sep 17 00:00:00 2001 From: d34d Date: Tue, 23 Feb 2016 09:58:53 -0800 Subject: [PATCH] Themes: Refactor themes to CMSDK [2/6] First attempt at moving as much as possible out of F/B and into cmsdk Change-Id: I9e53d1c32e01e88fc3918663dabe0001df922bc2 TICKET: CYNGNOS-2126 --- api/cm_current.txt | 128 ++ .../internal/AppsFailureReceiver.java | 120 ++ .../internal/IconCacheManagerService.java | 109 ++ .../internal/ThemeManagerService.java | 1246 +++++++++++++++++ cm/res/AndroidManifest.xml | 21 + cm/res/res/values/strings.xml | 25 + cm/res/res/values/symbols.xml | 9 + .../cyanogenmod/app/CMContextConstants.java | 14 + src/java/cyanogenmod/content/Intent.java | 46 + .../cyanogenmod/providers/ThemesContract.java | 717 ++++++++++ .../themes/IThemeChangeListener.aidl | 23 + .../themes/IThemeProcessingListener.aidl | 22 + .../cyanogenmod/themes/IThemeService.aidl | 44 + .../themes/ThemeChangeRequest.aidl | 19 + .../themes/ThemeChangeRequest.java | 329 +++++ src/java/cyanogenmod/themes/ThemeManager.java | 383 +++++ .../internal/themes/IIconCacheManager.aidl | 24 + .../cyanogenmod/internal/util/ImageUtils.java | 332 +++++ .../cyanogenmod/internal/util/ThemeUtils.java | 687 +++++++++ system-api/cm_system-current.txt | 127 ++ 20 files changed, 4425 insertions(+) create mode 100644 cm/lib/main/java/org/cyanogenmod/platform/internal/AppsFailureReceiver.java create mode 100644 cm/lib/main/java/org/cyanogenmod/platform/internal/IconCacheManagerService.java create mode 100644 cm/lib/main/java/org/cyanogenmod/platform/internal/ThemeManagerService.java create mode 100644 src/java/cyanogenmod/providers/ThemesContract.java create mode 100644 src/java/cyanogenmod/themes/IThemeChangeListener.aidl create mode 100644 src/java/cyanogenmod/themes/IThemeProcessingListener.aidl create mode 100644 src/java/cyanogenmod/themes/IThemeService.aidl create mode 100644 src/java/cyanogenmod/themes/ThemeChangeRequest.aidl create mode 100644 src/java/cyanogenmod/themes/ThemeChangeRequest.java create mode 100644 src/java/cyanogenmod/themes/ThemeManager.java create mode 100644 src/java/org/cyanogenmod/internal/themes/IIconCacheManager.aidl create mode 100644 src/java/org/cyanogenmod/internal/util/ImageUtils.java create mode 100644 src/java/org/cyanogenmod/internal/util/ThemeUtils.java 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 mOldestFilesFirstComparator = new Comparator() { + @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 mClients = new RemoteCallbackList<>(); + + private final RemoteCallbackList mProcessingListeners = + new RemoteCallbackList<>(); + + final private ArrayList 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 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 defaults = ThemeUtils.getDefaultComponents(mContext); + ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder(); + for(Map.Entry 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 components; + if (TextUtils.isEmpty(defaultThemeComponents)) { + components = ThemeUtils.getAllComponents(); + } else { + components = new ArrayList( + 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 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 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 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 infos = mPM.queryIntentActivities(homeIntent, 0); + List 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 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 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 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 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" /> + + + + + + + + + Extras Haptic feedback Vibrate when a gesture got detected + + + Failed to install theme + %1$s failed to install + + + Theme reset + System theme restored due to multiple app crashes. + + + access theme service + + Allows an app to access the theme service. Should never be needed for normal apps. + + + read your theme info + + Allows the app to read your themes and + determine which theme you have applied. + + + modify your themes + + Allows the app to insert new themes and modify which theme you have applied. + 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 @@ + + + + + + + + + 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; + +/** + *

+ * The contract between the themes provider and applications. Contains + * definitions for the supported URIs and columns. + *

+ */ +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. + *

Type: INTEGER (long)

+ */ + public static final String _ID = "_id"; + + /** + * The user visible title. + *

Type: TEXT

+ */ + public static final String TITLE = "title"; + + /** + * Unique text to identify the apk pkg. ie "com.foo.bar" + *

Type: TEXT

+ */ + public static final String PKG_NAME = "pkg_name"; + + /** + * A 32 bit RRGGBB color representative of the themes color scheme + *

Type: INTEGER

+ */ + public static final String PRIMARY_COLOR = "primary_color"; + + /** + * A 2nd 32 bit RRGGBB color representative of the themes color scheme + *

Type: INTEGER

+ */ + public static final String SECONDARY_COLOR = "secondary_color"; + + /** + * Name of the author of the theme + *

Type: TEXT

+ */ + public static final String AUTHOR = "author"; + + /** + * The time that this row was created on its originating client (msecs + * since the epoch). + *

Type: INTEGER

+ */ + public static final String DATE_CREATED = "created"; + + /** + * URI to an image that shows the homescreen with the theme applied + * since the epoch). + *

Type: TEXT

+ */ + public static final String HOMESCREEN_URI = "homescreen_uri"; + + /** + * URI to an image that shows the lockscreen with theme applied + *

Type: TEXT

+ */ + public static final String LOCKSCREEN_URI = "lockscreen_uri"; + + /** + * URI to an image that shows the style (aka skin) with theme applied + *

Type: TEXT

+ */ + 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. + *

Type: TEXT

+ */ + public static final String BOOT_ANIM_URI = "bootanim_uri"; + + /** + * URI to an image of the status bar for this theme. + *

Type: TEXT

+ */ + public static final String STATUSBAR_URI = "status_uri"; + + /** + * URI to an image of the fonts in this theme. + *

Type: TEXT

+ */ + public static final String FONT_URI = "font_uri"; + + /** + * URI to an image of the fonts in this theme. + *

Type: TEXT

+ */ + public static final String ICON_URI = "icon_uri"; + + /** + * URI to an image of the fonts in this theme. + *

Type: TEXT

+ */ + public static final String OVERLAYS_URI = "overlays_uri"; + + /** + * 1 if theme modifies the launcher/homescreen else 0 + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String MODIFIES_LAUNCHER = "mods_homescreen"; + + /** + * 1 if theme modifies the lockscreen else 0 + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String MODIFIES_LOCKSCREEN = "mods_lockscreen"; + + /** + * 1 if theme modifies icons else 0 + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String MODIFIES_ICONS = "mods_icons"; + + /** + * 1 if theme modifies fonts + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String MODIFIES_FONTS = "mods_fonts"; + + /** + * 1 if theme modifies boot animation + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String MODIFIES_BOOT_ANIM = "mods_bootanim"; + + /** + * 1 if theme modifies notifications + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String MODIFIES_NOTIFICATIONS = "mods_notifications"; + + /** + * 1 if theme modifies alarm sounds + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String MODIFIES_ALARMS = "mods_alarms"; + + /** + * 1 if theme modifies ringtones + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String MODIFIES_RINGTONES = "mods_ringtones"; + + /** + * 1 if theme has overlays + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String MODIFIES_OVERLAYS = "mods_overlays"; + + /** + * 1 if theme has an overlay for SystemUI/StatusBar + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String MODIFIES_STATUS_BAR = "mods_status_bar"; + + /** + * 1 if theme has an overlay for SystemUI/NavBar + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String MODIFIES_NAVIGATION_BAR = "mods_navigation_bar"; + + /** + * 1 if theme has a live lock screen + *

Type: INTEGER

+ *

Default: 0

+ */ + 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. + *

Type: TEXT

+ */ + 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. + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String PRESENT_AS_THEME = "present_as_theme"; + + /** + * 1 if this theme is a legacy theme. + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String IS_LEGACY_THEME = "is_legacy_theme"; + + /** + * 1 if this theme is the system default theme. + *

Type: INTEGER

+ *

Default: 0

+ */ + 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. + *

Type: INTEGER

+ *

Default: 0

+ */ + 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 + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String LAST_UPDATE_TIME = "updateTime"; + + /** + * install time in millisecs. When the row is inserted this column + * is populated by the PackageInfo. + *

Type: INTEGER

+ *

Default: 0

+ */ + public static final String INSTALL_TIME = "install_time"; + + /** + * The target API this theme supports + * is populated by the PackageInfo. + *

Type: INTEGER

+ *

Default: 0

+ */ + 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} + *

Type: INTEGER

+ *

Default: 0

+ */ + 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 + *

Type: TEXT

+ */ + public static final String COL_KEY = "key"; + + /** + * The package name that corresponds to a given component. + *

Type: String

+ */ + public static final String COL_VALUE = "value"; + + /** + * The package name that corresponds to where this component was applied from previously + *

Type: String

+ */ + public static final String COL_PREV_VALUE = "previous_value"; + + /** + * Time when this entry was last updated + *

Type: INTEGER

+ */ + 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. + *

Type: INTEGER (long)

+ */ + 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. + *

Type: INTEGER (long)

+ */ + public static final String _ID = "_id"; + + /** + * The unique ID for the theme these previews belong to. + *

Type: INTEGER (long)

+ */ + public static final String THEME_ID = "theme_id"; + + /** + * The unique ID for the component within a theme. + *

Type: INTEGER (long)

+ */ + public static final String COMPONENT_ID = "component_id"; + + /** + * The unique key for a row. See the Valid key constants section below + * for valid examples + *

Type: TEXT

+ */ + public static final String COL_KEY = "key"; + + /** + * The package name that corresponds to a given component. + *

Type: String

+ */ + public static final String COL_VALUE = "value"; + + /** + * Valid keys + */ + + /** + * Cached image of the themed status bar background. + *

Type: String (file path)

+ */ + public static final String STATUSBAR_BACKGROUND = "statusbar_background"; + + /** + * Cached image of the themed bluetooth status icon. + *

Type: String (file path)

+ */ + public static final String STATUSBAR_BLUETOOTH_ICON = "statusbar_bluetooth_icon"; + + /** + * Cached image of the themed wifi status icon. + *

Type: String (file path)

+ */ + public static final String STATUSBAR_WIFI_ICON = "statusbar_wifi_icon"; + + /** + * Cached image of the themed cellular signal status icon. + *

Type: String (file path)

+ */ + public static final String STATUSBAR_SIGNAL_ICON = "statusbar_signal_icon"; + + /** + * Cached image of the themed battery using portrait style. + *

Type: String (file path)

+ */ + public static final String STATUSBAR_BATTERY_PORTRAIT = "statusbar_battery_portrait"; + + /** + * Cached image of the themed battery using landscape style. + *

Type: String (file path)

+ */ + public static final String STATUSBAR_BATTERY_LANDSCAPE = "statusbar_battery_landscape"; + + /** + * Cached image of the themed battery using circle style. + *

Type: String (file path)

+ */ + public static final String STATUSBAR_BATTERY_CIRCLE = "statusbar_battery_circle"; + + /** + * The themed color used for clock text in the status bar. + *

Type: INTEGER (int)

+ */ + public static final String STATUSBAR_CLOCK_TEXT_COLOR = "statusbar_clock_text_color"; + + /** + * The themed margin value between the wifi and rssi signal icons. + *

Type: INTEGER (int)

+ */ + public static final String STATUSBAR_WIFI_COMBO_MARGIN_END = "wifi_combo_margin_end"; + + /** + * Cached image of the themed navigation bar background. + *

Type: String (file path)

+ */ + public static final String NAVBAR_BACKGROUND = "navbar_background"; + + /** + * Cached image of the themed back button. + *

Type: String (file path)

+ */ + public static final String NAVBAR_BACK_BUTTON = "navbar_back_button"; + + /** + * Cached image of the themed home button. + *

Type: String (file path)

+ */ + public static final String NAVBAR_HOME_BUTTON = "navbar_home_button"; + + /** + * Cached image of the themed recents button. + *

Type: String (file path)

+ */ + public static final String NAVBAR_RECENT_BUTTON = "navbar_recent_button"; + + /** + * Cached image of the 1/3 icons + *

Type: String (file path)

+ */ + public static final String ICON_PREVIEW_1 = "icon_preview_1"; + + /** + * Cached image of the 2/3 icons + *

Type: String (file path)

+ */ + public static final String ICON_PREVIEW_2 = "icon_preview_2"; + + /** + * Cached image of the 3/3 icons + *

Type: String (file path)

+ */ + public static final String ICON_PREVIEW_3 = "icon_preview_3"; + + /** + * Full path to the theme's wallpaper asset. + *

Type: String (file path)

+ */ + 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. + *

Type: String (file path)

+ */ + public static final String WALLPAPER_PREVIEW = "wallpaper_preview"; + + /** + * Cached thumbnail of the theme's wallpaper + *

Type: String (file path)

+ */ + 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. + *

Type: String (file path)

+ */ + public static final String LOCK_WALLPAPER_PREVIEW = "lock_wallpaper_preview"; + + /** + * Cached thumbnail of the theme's lockscreen wallpaper + *

Type: String (file path)

+ */ + public static final String LOCK_WALLPAPER_THUMBNAIL = "lock_wallpaper_thumbnail"; + + /** + * Cached preview of UI controls representing the theme's style + *

Type: String (file path)

+ */ + public static final String STYLE_PREVIEW = "style_preview"; + + /** + * Cached thumbnail preview of UI controls representing the theme's style + *

Type: String (file path)

+ */ + public static final String STYLE_THUMBNAIL = "style_thumbnail"; + + /** + * Cached thumbnail of the theme's boot animation + *

Type: String (file path)

+ */ + public static final String BOOTANIMATION_THUMBNAIL = "bootanimation_thumbnail"; + + /** + * Cached preview of live lock screen + *

Type: String (file path)

+ */ + public static final String LIVE_LOCK_SCREEN_PREVIEW = "live_lock_screen_preview"; + + /** + * Cached thumbnail preview of live lock screen + *

Type: String (file path)

+ */ + 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 mThemeComponents = new HashMap<>(); + private final Map 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 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 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 components, Map 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 CREATOR = + new Parcelable.Creator() { + @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 mThemeComponents = new HashMap<>(); + Map 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 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. + *

Get an instance of this class by calling blah blah blah

+ */ +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 mChangeListeners = new ArraySet<>(); + + private Set 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 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 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 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 components) { + requestThemeChange(pkgName, components, true); + } + + public void requestThemeChange(String pkgName, List components, + boolean removePerAppThemes) { + Map componentMap = new HashMap<>(components.size()); + for (String component : components) { + componentMap.put(component, pkgName); + } + requestThemeChange(componentMap, removePerAppThemes); + } + + public void requestThemeChange(Map componentMap) { + requestThemeChange(componentMap, true); + } + + public void requestThemeChange(Map 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 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 getAllComponents() { + List 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 getSupportedComponents(Context context, String pkgName) { + List 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 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 getDefaultComponents(Context context) { + String defaultThemePkg = getDefaultThemePackageName(context); + List defaultComponents = null; + List systemComponents = getSupportedComponents(context, SYSTEM_DEFAULT); + if (!DEFAULT_PKG.equals(defaultThemePkg)) { + defaultComponents = getSupportedComponents(context, defaultThemePkg); + } + + Map 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 getWallpaperPathList(AssetManager assetManager) + throws IOException { + List wallpaperList = new ArrayList(); + 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 {