diff options
author | d34d <clark@cyngn.com> | 2016-02-23 09:58:53 -0800 |
---|---|---|
committer | d34d <clark@cyngn.com> | 2016-03-04 13:59:33 -0800 |
commit | b3ea2859fd920ea68afc3ae7010b665e2dd515ea (patch) | |
tree | 973bded90ebc12bb1d4a0472c14f9adafdcfca30 /cm/lib | |
parent | 567b43017af6f51d67ee05397df665bf136cb177 (diff) | |
download | vendor_cmsdk-b3ea2859fd920ea68afc3ae7010b665e2dd515ea.zip vendor_cmsdk-b3ea2859fd920ea68afc3ae7010b665e2dd515ea.tar.gz vendor_cmsdk-b3ea2859fd920ea68afc3ae7010b665e2dd515ea.tar.bz2 |
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
Diffstat (limited to 'cm/lib')
3 files changed, 1475 insertions, 0 deletions
diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/AppsFailureReceiver.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/AppsFailureReceiver.java new file mode 100644 index 0000000..e199d2a --- /dev/null +++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/AppsFailureReceiver.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2010, T-Mobile USA, Inc. + * Copyright (C) 2015-2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.cyanogenmod.platform.internal; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.res.ThemeConfig; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; + +import org.cyanogenmod.internal.util.ThemeUtils; + +import cyanogenmod.app.CMContextConstants; +import cyanogenmod.themes.IThemeService; +import cyanogenmod.themes.ThemeChangeRequest; +import cyanogenmod.themes.ThemeChangeRequest.RequestType; + +import static cyanogenmod.content.Intent.ACTION_APP_FAILURE; + +public class AppsFailureReceiver extends BroadcastReceiver { + + private static final int FAILURES_THRESHOLD = 3; + private static final int EXPIRATION_TIME_IN_MILLISECONDS = 30000; // 30 seconds + + private int mFailuresCount = 0; + private long mStartTime = 0; + + // This function implements the following logic. + // If after a theme was applied the number of application launch failures + // at any moment was equal to FAILURES_THRESHOLD + // in less than EXPIRATION_TIME_IN_MILLISECONDS + // the default theme is applied unconditionally. + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + final long currentTime = SystemClock.uptimeMillis(); + if (ACTION_APP_FAILURE.equals(action)) { + if (currentTime - mStartTime > EXPIRATION_TIME_IN_MILLISECONDS) { + // reset both the count and the timer + mStartTime = currentTime; + mFailuresCount = 0; + } + if (mFailuresCount <= FAILURES_THRESHOLD) { + mFailuresCount++; + if (mFailuresCount == FAILURES_THRESHOLD) { + // let the theme manager take care of getting us back on the default theme + IThemeService tm = IThemeService.Stub.asInterface(ServiceManager + .getService(CMContextConstants.CM_THEME_SERVICE)); + final String themePkgName = ThemeConfig.SYSTEM_DEFAULT; + ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder(); + builder.setOverlay(themePkgName) + .setStatusBar(themePkgName) + .setNavBar(themePkgName) + .setIcons(themePkgName) + .setFont(themePkgName) + .setBootanimation(themePkgName) + .setWallpaper(themePkgName) + .setLockWallpaper(themePkgName) + .setAlarm(themePkgName) + .setNotification(themePkgName) + .setRingtone(themePkgName) + .setRequestType(RequestType.THEME_RESET); + // Since we are resetting everything to the system theme, we can have the + // theme service remove all per app themes without setting them explicitly :) + try { + tm.requestThemeChange(builder.build(), true); + postThemeResetNotification(context); + } catch (RemoteException e) { + /* ignore */ + } + } + } + } else if (ThemeUtils.ACTION_THEME_CHANGED.equals(action)) { + // reset both the count and the timer + mStartTime = currentTime; + mFailuresCount = 0; + } + } + + /** + * Posts a notification to let the user know their theme was reset + * @param context + */ + private void postThemeResetNotification(Context context) { + NotificationManager nm = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + String title = context.getString(R.string.theme_reset_notification_title); + String body = context.getString(R.string.theme_reset_notification_message); + Notification notice = new Notification.Builder(context) + .setAutoCancel(true) + .setOngoing(false) + .setContentTitle(title) + .setContentText(body) + .setStyle(new Notification.BigTextStyle().bigText(body)) + .setSmallIcon(android.R.drawable.stat_notify_error) + .setWhen(System.currentTimeMillis()) + .setCategory(Notification.CATEGORY_SYSTEM) + .setPriority(Notification.PRIORITY_MAX) + .build(); + nm.notify(R.string.theme_reset_notification_title, notice); + } +} diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/IconCacheManagerService.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/IconCacheManagerService.java new file mode 100644 index 0000000..56be660 --- /dev/null +++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/IconCacheManagerService.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyanogenmod.platform.internal; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Binder; +import android.os.FileUtils; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import com.android.server.SystemService; +import cyanogenmod.app.CMContextConstants; + +import org.cyanogenmod.internal.themes.IIconCacheManager; +import org.cyanogenmod.internal.util.ThemeUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.Arrays; +import java.util.Comparator; + +/** @hide */ +public class IconCacheManagerService extends SystemService { + private static final String TAG = IconCacheManagerService.class.getSimpleName(); + + private static final long MAX_ICON_CACHE_SIZE = 33554432L; // 32MB + private static final long PURGED_ICON_CACHE_SIZE = 25165824L; // 24 MB + + private long mIconCacheSize = 0L; + + public IconCacheManagerService(Context context) { + super(context); + } + + @Override + public void onStart() { + Log.d(TAG, "registerIconCache cmiconcache: " + this); + publishBinderService(CMContextConstants.CM_ICON_CACHE_SERVICE, mService); + } + + private void purgeIconCache() { + Log.d(TAG, "Purging icon cahe of size " + mIconCacheSize); + File cacheDir = new File(ThemeUtils.SYSTEM_THEME_ICON_CACHE_DIR); + File[] files = cacheDir.listFiles(); + Arrays.sort(files, mOldestFilesFirstComparator); + for (File f : files) { + if (!f.isDirectory()) { + final long size = f.length(); + if(f.delete()) mIconCacheSize -= size; + } + if (mIconCacheSize <= PURGED_ICON_CACHE_SIZE) break; + } + } + + private Comparator<File> mOldestFilesFirstComparator = new Comparator<File>() { + @Override + public int compare(File lhs, File rhs) { + return (int) (lhs.lastModified() - rhs.lastModified()); + } + }; + + private IBinder mService = new IIconCacheManager.Stub() { + @Override + public boolean cacheComposedIcon(Bitmap icon, String fileName) throws RemoteException { + final long token = Binder.clearCallingIdentity(); + boolean success; + FileOutputStream os; + final File cacheDir = new File(ThemeUtils.SYSTEM_THEME_ICON_CACHE_DIR); + if (cacheDir.listFiles().length == 0) { + mIconCacheSize = 0; + } + try { + File outFile = new File(cacheDir, fileName); + os = new FileOutputStream(outFile); + icon.compress(Bitmap.CompressFormat.PNG, 90, os); + os.close(); + FileUtils.setPermissions(outFile, + FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IROTH, + -1, -1); + mIconCacheSize += outFile.length(); + if (mIconCacheSize > MAX_ICON_CACHE_SIZE) { + purgeIconCache(); + } + success = true; + } catch (Exception e) { + success = false; + Log.w(TAG, "Unable to cache icon " + fileName, e); + } + Binder.restoreCallingIdentity(token); + return success; + } + + }; +} diff --git a/cm/lib/main/java/org/cyanogenmod/platform/internal/ThemeManagerService.java b/cm/lib/main/java/org/cyanogenmod/platform/internal/ThemeManagerService.java new file mode 100644 index 0000000..c78f187 --- /dev/null +++ b/cm/lib/main/java/org/cyanogenmod/platform/internal/ThemeManagerService.java @@ -0,0 +1,1246 @@ +/* + * Copyright (C) 2014-2016 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyanogenmod.platform.internal; + +import android.app.ActivityManager; +import android.app.ActivityManagerNative; +import android.app.IActivityManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.WallpaperManager; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.ThemeConfig; +import android.media.RingtoneManager; +import android.os.Binder; +import android.os.Environment; +import android.os.FileUtils; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; + +import com.android.server.SystemService; + +import cyanogenmod.app.CMContextConstants; +import cyanogenmod.providers.CMSettings; +import cyanogenmod.providers.ThemesContract.MixnMatchColumns; +import cyanogenmod.providers.ThemesContract.ThemesColumns; +import cyanogenmod.themes.IThemeChangeListener; +import cyanogenmod.themes.IThemeProcessingListener; +import cyanogenmod.themes.IThemeService; +import cyanogenmod.themes.ThemeChangeRequest; + +import org.cyanogenmod.internal.util.ImageUtils; +import org.cyanogenmod.internal.util.ThemeUtils; +import org.cyanogenmod.platform.internal.AppsFailureReceiver; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import libcore.io.IoUtils; + +import static android.content.res.ThemeConfig.SYSTEM_DEFAULT; +import static cyanogenmod.platform.Manifest.permission.ACCESS_THEME_MANAGER; +import static org.cyanogenmod.internal.util.ThemeUtils.SYSTEM_THEME_PATH; +import static org.cyanogenmod.internal.util.ThemeUtils.THEME_BOOTANIMATION_PATH; + +public class ThemeManagerService extends SystemService { + + private static final String TAG = ThemeManagerService.class.getName(); + + private static final boolean DEBUG = false; + + private static final String GOOGLE_SETUPWIZARD_PACKAGE = "com.google.android.setupwizard"; + private static final String CM_SETUPWIZARD_PACKAGE = "com.cyanogenmod.setupwizard"; + private static final String MANAGED_PROVISIONING_PACKAGE = "com.android.managedprovisioning"; + + // Defines a min and max compatible api level for themes on this system. + private static final int MIN_COMPATIBLE_VERSION = 21; + + private HandlerThread mWorker; + private ThemeWorkerHandler mHandler; + private ResourceProcessingHandler mResourceProcessingHandler; + private Context mContext; + private PackageManager mPM; + private int mProgress; + private boolean mWallpaperChangedByUs = false; + private int mCurrentUserId = UserHandle.USER_OWNER; + + private boolean mIsThemeApplying = false; + + private final RemoteCallbackList<IThemeChangeListener> mClients = new RemoteCallbackList<>(); + + private final RemoteCallbackList<IThemeProcessingListener> mProcessingListeners = + new RemoteCallbackList<>(); + + final private ArrayList<String> mThemesToProcessQueue = new ArrayList<>(); + + private long mLastThemeChangeTime = 0; + private int mLastThemeChangeRequestType; + + private class ThemeWorkerHandler extends Handler { + private static final int MESSAGE_CHANGE_THEME = 1; + private static final int MESSAGE_APPLY_DEFAULT_THEME = 2; + private static final int MESSAGE_REBUILD_RESOURCE_CACHE = 3; + + public ThemeWorkerHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_CHANGE_THEME: + final ThemeChangeRequest request = (ThemeChangeRequest) msg.obj; + doApplyTheme(request, msg.arg1 == 1); + break; + case MESSAGE_APPLY_DEFAULT_THEME: + doApplyDefaultTheme(); + break; + case MESSAGE_REBUILD_RESOURCE_CACHE: + doRebuildResourceCache(); + break; + default: + Log.w(TAG, "Unknown message " + msg.what); + break; + } + } + } + + private class ResourceProcessingHandler extends Handler { + private static final int MESSAGE_QUEUE_THEME_FOR_PROCESSING = 3; + private static final int MESSAGE_DEQUEUE_AND_PROCESS_THEME = 4; + + public ResourceProcessingHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_QUEUE_THEME_FOR_PROCESSING: + String pkgName = (String) msg.obj; + synchronized (mThemesToProcessQueue) { + if (!mThemesToProcessQueue.contains(pkgName)) { + if (DEBUG) Log.d(TAG, "Adding " + pkgName + " for processing"); + mThemesToProcessQueue.add(pkgName); + if (mThemesToProcessQueue.size() == 1) { + this.sendEmptyMessage(MESSAGE_DEQUEUE_AND_PROCESS_THEME); + } + } + } + break; + case MESSAGE_DEQUEUE_AND_PROCESS_THEME: + synchronized (mThemesToProcessQueue) { + pkgName = mThemesToProcessQueue.get(0); + } + if (pkgName != null) { + if (DEBUG) Log.d(TAG, "Processing " + pkgName); + String name; + try { + PackageInfo pi = mPM.getPackageInfo(pkgName, 0); + name = getThemeName(pi); + } catch (PackageManager.NameNotFoundException e) { + name = null; + } + + int result = mPM.processThemeResources(pkgName); + if (result < 0) { + postFailedThemeInstallNotification(name != null ? name : pkgName); + } + sendThemeResourcesCachedBroadcast(pkgName, result); + + synchronized (mThemesToProcessQueue) { + mThemesToProcessQueue.remove(0); + if (mThemesToProcessQueue.size() > 0 && + !hasMessages(MESSAGE_DEQUEUE_AND_PROCESS_THEME)) { + this.sendEmptyMessage(MESSAGE_DEQUEUE_AND_PROCESS_THEME); + } + } + postFinishedProcessing(pkgName); + } + break; + default: + Log.w(TAG, "Unknown message " + msg.what); + break; + } + } + } + + + public ThemeManagerService(Context context) { + super(context); + mContext = context; + mWorker = new HandlerThread("ThemeServiceWorker", Process.THREAD_PRIORITY_BACKGROUND); + mWorker.start(); + mHandler = new ThemeWorkerHandler(mWorker.getLooper()); + Log.i(TAG, "Spawned worker thread"); + + HandlerThread processingThread = new HandlerThread("ResourceProcessingThread", + Process.THREAD_PRIORITY_BACKGROUND); + processingThread.start(); + mResourceProcessingHandler = + new ResourceProcessingHandler(processingThread.getLooper()); + + // create the theme directories if they do not exist + ThemeUtils.createThemeDirIfNotExists(); + ThemeUtils.createFontDirIfNotExists(); + ThemeUtils.createAlarmDirIfNotExists(); + ThemeUtils.createNotificationDirIfNotExists(); + ThemeUtils.createRingtoneDirIfNotExists(); + ThemeUtils.createIconCacheDirIfNotExists(); + } + + @Override + public void onStart() { + publishBinderService(CMContextConstants.CM_THEME_SERVICE, mService); + // listen for wallpaper changes + IntentFilter filter = new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED); + mContext.registerReceiver(mWallpaperChangeReceiver, filter); + + filter = new IntentFilter(Intent.ACTION_USER_SWITCHED); + mContext.registerReceiver(mUserChangeReceiver, filter); + + mPM = mContext.getPackageManager(); + + if (!isThemeApiUpToDate()) { + Log.d(TAG, "The system has been upgraded to a theme new api, " + + "checking if currently set theme is compatible"); + removeObsoleteThemeOverlayIfExists(); + updateThemeApi(); + } + } + + @Override + public void onBootPhase(int phase) { + super.onBootPhase(phase); + if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) { + registerAppsFailureReceiver(); + processInstalledThemes(); + } + } + + private void registerAppsFailureReceiver() { + IntentFilter filter = new IntentFilter(); + filter.addAction(cyanogenmod.content.Intent.ACTION_APP_FAILURE); + filter.addAction(ThemeUtils.ACTION_THEME_CHANGED); + mContext.registerReceiver(new AppsFailureReceiver(), filter); + } + + private void removeObsoleteThemeOverlayIfExists() { + // Get the current overlay theme so we can see it it's overlay should be unapplied + final IActivityManager am = ActivityManagerNative.getDefault(); + ThemeConfig config = null; + try { + if (am != null) { + config = am.getConfiguration().themeConfig; + } else { + Log.e(TAG, "ActivityManager getDefault() " + + "returned null, cannot remove obsolete theme"); + } + } catch(RemoteException e) { + Log.e(TAG, "Failed to get the theme config ", e); + } + if (config == null) return; // No need to unapply a theme if one isn't set + + // Populate the currentTheme map for the components we care about, we'll look + // at the compatibility of each pkg below. + HashMap<String, String> currentThemeMap = new HashMap<>(); + currentThemeMap.put(ThemesColumns.MODIFIES_STATUS_BAR, config.getOverlayForStatusBar()); + currentThemeMap.put(ThemesColumns.MODIFIES_NAVIGATION_BAR, + config.getOverlayForNavBar()); + currentThemeMap.put(ThemesColumns.MODIFIES_OVERLAYS, config.getOverlayPkgName()); + + // Look at each component's theme (that we care about at least) and check compatibility + // of the pkg with the system. If it is not compatible then we will add it to a theme + // change request. + Map<String, String> defaults = ThemeUtils.getDefaultComponents(mContext); + ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder(); + for(Map.Entry<String, String> entry : currentThemeMap.entrySet()) { + String component = entry.getKey(); + String pkgName = entry.getValue(); + String defaultPkg = defaults.get(component); + + // Check that the default overlay theme is not currently set + if (defaultPkg.equals(pkgName)) { + Log.d(TAG, "Current overlay theme is same as default. " + + "Not doing anything for " + component); + continue; + } + + // No need to unapply a system theme since it is always compatible + if (ThemeConfig.SYSTEM_DEFAULT.equals(pkgName)) { + Log.d(TAG, "Current overlay theme for " + + component + " was system. no need to unapply"); + continue; + } + + if (!isThemeCompatibleWithUpgradedApi(pkgName)) { + Log.d(TAG, pkgName + "is incompatible with latest theme api for component " + + component + ", Applying " + defaultPkg); + builder.setComponent(component, pkgName); + } + } + + // Now actually unapply the incompatible themes + ThemeChangeRequest request = builder.build(); + if (!request.getThemeComponentsMap().isEmpty()) { + try { + ((IThemeService) mService).requestThemeChange(request, true); + } catch(RemoteException e) { + // This cannot happen + } + } else { + Log.d(TAG, "Current theme is compatible with the system. Not unapplying anything"); + } + } + + private boolean isThemeCompatibleWithUpgradedApi(String pkgName) { + // Note this function does not cover the case of a downgrade. That case is out of scope and + // would require predicting whether the future API levels will be compatible or not. + boolean compatible = false; + try { + PackageInfo pi = mPM.getPackageInfo(pkgName, 0); + Log.d(TAG, "Comparing theme target: " + pi.applicationInfo.targetSdkVersion + + "to " + android.os.Build.VERSION.SDK_INT); + compatible = pi.applicationInfo.targetSdkVersion >= MIN_COMPATIBLE_VERSION; + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Unable to get package info for " + pkgName, e); + } + return compatible; + } + + private boolean isThemeApiUpToDate() { + // We can't be 100% sure its an upgrade. If the field is undefined it + // could have been a factory reset. + final ContentResolver resolver = mContext.getContentResolver(); + int recordedApiLevel = android.os.Build.VERSION.SDK_INT; + try { + recordedApiLevel = CMSettings.Secure.getInt(resolver, + CMSettings.Secure.THEME_PREV_BOOT_API_LEVEL); + } catch (CMSettings.CMSettingNotFoundException e) { + recordedApiLevel = -1; + Log.d(TAG, "Previous api level not found. First time booting?"); + } + Log.d(TAG, "Prev api level was: " + recordedApiLevel + + ", api is now: " + android.os.Build.VERSION.SDK_INT); + + return recordedApiLevel == android.os.Build.VERSION.SDK_INT; + } + + private void updateThemeApi() { + final ContentResolver resolver = mContext.getContentResolver(); + boolean success = CMSettings.Secure.putInt(resolver, + CMSettings.Secure.THEME_PREV_BOOT_API_LEVEL, android.os.Build.VERSION.SDK_INT); + if (!success) { + Log.e(TAG, "Unable to store latest API level to secure settings"); + } + } + + private void doApplyTheme(ThemeChangeRequest request, boolean removePerAppTheme) { + synchronized(this) { + mProgress = 0; + } + + if (request == null || request.getNumChangesRequested() == 0) { + postFinish(true, request, 0); + return; + } + mIsThemeApplying = true; + mLastThemeChangeTime = System.currentTimeMillis(); + mLastThemeChangeRequestType = request.getReqeustType().ordinal(); + + incrementProgress(5); + + // TODO: provide progress updates that reflect the time needed for each component + final int progressIncrement = 75 / request.getNumChangesRequested(); + + if (request.getIconsThemePackageName() != null) { + updateIcons(request.getIconsThemePackageName()); + incrementProgress(progressIncrement); + } + + if (request.getWallpaperThemePackageName() != null) { + if (updateWallpaper(request.getWallpaperThemePackageName(), + request.getWallpaperId())) { + mWallpaperChangedByUs = true; + } + incrementProgress(progressIncrement); + } + + if (request.getLockWallpaperThemePackageName() != null) { + updateLockscreen(request.getLockWallpaperThemePackageName()); + incrementProgress(progressIncrement); + } + + Environment.setUserRequired(false); + if (request.getNotificationThemePackageName() != null) { + updateNotifications(request.getNotificationThemePackageName()); + incrementProgress(progressIncrement); + } + + if (request.getAlarmThemePackageName() != null) { + updateAlarms(request.getAlarmThemePackageName()); + incrementProgress(progressIncrement); + } + + if (request.getRingtoneThemePackageName() != null) { + updateRingtones(request.getRingtoneThemePackageName()); + incrementProgress(progressIncrement); + } + Environment.setUserRequired(true); + + if (request.getBootanimationThemePackageName() != null) { + updateBootAnim(request.getBootanimationThemePackageName()); + incrementProgress(progressIncrement); + } + + if (request.getFontThemePackageName() != null) { + updateFonts(request.getFontThemePackageName()); + incrementProgress(progressIncrement); + } + + if (request.getLiveLockScreenThemePackageName() != null) { + updateLiveLockScreen(request.getLiveLockScreenThemePackageName()); + incrementProgress(progressIncrement); + } + + try { + updateProvider(request, mLastThemeChangeTime); + } catch(IllegalArgumentException e) { + // Safeguard against provider not being ready yet. + Log.e(TAG, "Not updating the theme provider since it is unavailable"); + } + + if (shouldUpdateConfiguration(request)) { + updateConfiguration(request, removePerAppTheme); + } + + killLaunchers(request); + + postFinish(true, request, mLastThemeChangeTime); + mIsThemeApplying = false; + } + + private void doApplyDefaultTheme() { + final ContentResolver resolver = mContext.getContentResolver(); + final String defaultThemePkg = CMSettings.Secure.getString(resolver, + CMSettings.Secure.DEFAULT_THEME_PACKAGE); + if (!TextUtils.isEmpty(defaultThemePkg)) { + String defaultThemeComponents = CMSettings.Secure.getString(resolver, + CMSettings.Secure.DEFAULT_THEME_COMPONENTS); + List<String> components; + if (TextUtils.isEmpty(defaultThemeComponents)) { + components = ThemeUtils.getAllComponents(); + } else { + components = new ArrayList<String>( + Arrays.asList(defaultThemeComponents.split("\\|"))); + } + ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder(); + for (String component : components) { + builder.setComponent(component, defaultThemePkg); + } + try { + ((IThemeService) mService).requestThemeChange(builder.build(), true); + } catch (RemoteException e) { + Log.w(TAG, "Unable to set default theme", e); + } + } + } + + private void doRebuildResourceCache() { + FileUtils.deleteContents(new File(ThemeUtils.RESOURCE_CACHE_DIR)); + processInstalledThemes(); + } + + private void updateProvider(ThemeChangeRequest request, long updateTime) { + ContentValues values = new ContentValues(); + values.put(MixnMatchColumns.COL_UPDATE_TIME, updateTime); + Map<String, String> componentMap = request.getThemeComponentsMap(); + for (String component : componentMap.keySet()) { + values.put(MixnMatchColumns.COL_VALUE, componentMap.get(component)); + String where = MixnMatchColumns.COL_KEY + "=?"; + String[] selectionArgs = { MixnMatchColumns.componentToMixNMatchKey(component) }; + if (selectionArgs[0] == null) { + continue; // No equivalence between mixnmatch and theme + } + + // Add component ID for multiwallpaper + if (ThemesColumns.MODIFIES_LAUNCHER.equals(component)) { + values.put(MixnMatchColumns.COL_COMPONENT_ID, request.getWallpaperId()); + } + + mContext.getContentResolver().update(MixnMatchColumns.CONTENT_URI, values, where, + selectionArgs); + } + } + + private boolean updateIcons(String pkgName) { + ThemeUtils.clearIconCache(); + try { + if (pkgName.equals(SYSTEM_DEFAULT)) { + mPM.updateIconMaps(null); + } else { + mPM.updateIconMaps(pkgName); + } + } catch (Exception e) { + Log.w(TAG, "Changing icons failed", e); + return false; + } + return true; + } + + private boolean updateFonts(String pkgName) { + //Clear the font dir + FileUtils.deleteContents(new File(ThemeUtils.SYSTEM_THEME_FONT_PATH)); + + if (!pkgName.equals(SYSTEM_DEFAULT)) { + //Get Font Assets + Context themeCtx; + String[] assetList; + try { + themeCtx = mContext.createPackageContext(pkgName, Context.CONTEXT_IGNORE_SECURITY); + AssetManager assetManager = themeCtx.getAssets(); + assetList = assetManager.list("fonts"); + } catch (Exception e) { + Log.e(TAG, "There was an error getting assets for pkg " + pkgName, e); + return false; + } + if (assetList == null || assetList.length == 0) { + Log.e(TAG, "Could not find any font assets"); + return false; + } + + //Copy font assets to font dir + for(String asset : assetList) { + InputStream is = null; + OutputStream os = null; + try { + is = ThemeUtils.getInputStreamFromAsset(themeCtx, + "file:///android_asset/fonts/" + asset); + File outFile = new File(ThemeUtils.SYSTEM_THEME_FONT_PATH, asset); + FileUtils.copyToFile(is, outFile); + FileUtils.setPermissions(outFile, + FileUtils.S_IRWXU|FileUtils.S_IRGRP|FileUtils.S_IRWXO, -1, -1); + } catch (Exception e) { + Log.e(TAG, "There was an error installing the new fonts for pkg " + pkgName, e); + return false; + } finally { + IoUtils.closeQuietly(is); + IoUtils.closeQuietly(os); + } + } + } + + //Notify zygote that themes need a refresh + SystemProperties.set("sys.refresh_theme", "1"); + return true; + } + + private boolean updateBootAnim(String pkgName) { + if (SYSTEM_DEFAULT.equals(pkgName)) { + clearBootAnimation(); + return true; + } + + try { + final ApplicationInfo ai = mPM.getApplicationInfo(pkgName, 0); + applyBootAnimation(ai.sourceDir); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Changing boot animation failed", e); + return false; + } + return true; + } + + private boolean updateAlarms(String pkgName) { + return updateAudible(ThemeUtils.SYSTEM_THEME_ALARM_PATH, "alarms", + RingtoneManager.TYPE_ALARM, pkgName); + } + + private boolean updateNotifications(String pkgName) { + return updateAudible(ThemeUtils.SYSTEM_THEME_NOTIFICATION_PATH, "notifications", + RingtoneManager.TYPE_NOTIFICATION, pkgName); + } + + private boolean updateRingtones(String pkgName) { + return updateAudible(ThemeUtils.SYSTEM_THEME_RINGTONE_PATH, "ringtones", + RingtoneManager.TYPE_RINGTONE, pkgName); + } + + private boolean updateAudible(String dirPath, String assetPath, int type, String pkgName) { + //Clear the dir + ThemeUtils.clearAudibles(mContext, dirPath); + if (pkgName.equals(SYSTEM_DEFAULT)) { + if (!ThemeUtils.setDefaultAudible(mContext, type)) { + Log.e(TAG, "There was an error installing the default audio file"); + return false; + } + return true; + } + + PackageInfo pi = null; + try { + pi = mPM.getPackageInfo(pkgName, 0); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Unable to update audible " + dirPath, e); + return false; + } + + //Get theme Assets + Context themeCtx; + String[] assetList; + try { + themeCtx = mContext.createPackageContext(pkgName, Context.CONTEXT_IGNORE_SECURITY); + AssetManager assetManager = themeCtx.getAssets(); + assetList = assetManager.list(assetPath); + } catch (Exception e) { + Log.e(TAG, "There was an error getting assets for pkg " + pkgName, e); + return false; + } + if (assetList == null || assetList.length == 0) { + Log.e(TAG, "Could not find any audio assets"); + return false; + } + + // TODO: right now we just load the first file but this will need to be changed + // in the future if multiple audio files are supported. + final String asset = assetList[0]; + if (!ThemeUtils.isValidAudible(asset)) return false; + + InputStream is = null; + OutputStream os = null; + try { + is = ThemeUtils.getInputStreamFromAsset(themeCtx, "file:///android_asset/" + + assetPath + File.separator + asset); + File outFile = new File(dirPath, asset); + FileUtils.copyToFile(is, outFile); + FileUtils.setPermissions(outFile, + FileUtils.S_IRWXU|FileUtils.S_IRGRP|FileUtils.S_IRWXO,-1, -1); + ThemeUtils.setAudible(mContext, outFile, type, pi.themeInfo.name); + } catch (Exception e) { + Log.e(TAG, "There was an error installing the new audio file for pkg " + pkgName, e); + return false; + } finally { + IoUtils.closeQuietly(is); + IoUtils.closeQuietly(os); + } + return true; + } + + private boolean updateLockscreen(String pkgName) { + boolean success; + success = setCustomLockScreenWallpaper(pkgName); + + if (success) { + mContext.sendBroadcastAsUser(new Intent(Intent.ACTION_KEYGUARD_WALLPAPER_CHANGED), + UserHandle.ALL); + } + return success; + } + + private boolean setCustomLockScreenWallpaper(String pkgName) { + WallpaperManager wm = WallpaperManager.getInstance(mContext); + try { + if (SYSTEM_DEFAULT.equals(pkgName) || TextUtils.isEmpty(pkgName)) { + wm.clearKeyguardWallpaper(); + } else { + InputStream in = ImageUtils.getCroppedKeyguardStream(pkgName, mContext); + if (in != null) { + wm.setKeyguardStream(in); + IoUtils.closeQuietly(in); + } + } + } catch (Exception e) { + Log.e(TAG, "There was an error setting lockscreen wp for pkg " + pkgName, e); + return false; + } + return true; + } + + private boolean updateWallpaper(String pkgName, long id) { + WallpaperManager wm = WallpaperManager.getInstance(mContext); + if (SYSTEM_DEFAULT.equals(pkgName)) { + try { + wm.clear(); + } catch (IOException e) { + return false; + } + } else if (TextUtils.isEmpty(pkgName)) { + try { + wm.clear(false); + } catch (IOException e) { + return false; + } + } else { + InputStream in = null; + try { + in = ImageUtils.getCroppedWallpaperStream(pkgName, id, mContext); + if (in != null) + wm.setStream(in); + } catch (Exception e) { + return false; + } finally { + IoUtils.closeQuietly(in); + } + } + return true; + } + + private boolean updateLiveLockScreen(String pkgName) { + // TODO: do something meaningful here once ready + return true; + } + + private boolean updateConfiguration(ThemeChangeRequest request, + boolean removePerAppThemes) { + final IActivityManager am = ActivityManagerNative.getDefault(); + if (am != null) { + final long token = Binder.clearCallingIdentity(); + try { + Configuration config = am.getConfiguration(); + ThemeConfig.Builder themeBuilder = createBuilderFrom(config, request, null, + removePerAppThemes); + ThemeConfig newConfig = themeBuilder.build(); + + config.themeConfig = newConfig; + am.updateConfiguration(config); + } catch (RemoteException e) { + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } + return true; + } + + private boolean updateConfiguration(ThemeConfig themeConfig) { + final IActivityManager am = ActivityManagerNative.getDefault(); + if (am != null) { + final long token = Binder.clearCallingIdentity(); + try { + Configuration config = am.getConfiguration(); + + config.themeConfig = themeConfig; + am.updateConfiguration(config); + } catch (RemoteException e) { + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } + return true; + } + + private boolean shouldUpdateConfiguration(ThemeChangeRequest request) { + return request.getOverlayThemePackageName() != null || + request.getFontThemePackageName() != null || + request.getIconsThemePackageName() != null || + request.getStatusBarThemePackageName() != null || + request.getNavBarThemePackageName() != null || + request.getPerAppOverlays().size() > 0; + } + + private static ThemeConfig.Builder createBuilderFrom(Configuration config, + ThemeChangeRequest request, String pkgName, boolean removePerAppThemes) { + ThemeConfig.Builder builder = new ThemeConfig.Builder(config.themeConfig); + + if (removePerAppThemes) removePerAppThemesFromConfig(builder, config.themeConfig); + + if (request.getIconsThemePackageName() != null) { + builder.defaultIcon(pkgName == null ? request.getIconsThemePackageName() : pkgName); + } + + if (request.getOverlayThemePackageName() != null) { + builder.defaultOverlay(pkgName == null ? + request.getOverlayThemePackageName() : pkgName); + } + + if (request.getFontThemePackageName() != null) { + builder.defaultFont(pkgName == null ? request.getFontThemePackageName() : pkgName); + } + + if (request.getStatusBarThemePackageName() != null) { + builder.overlay(ThemeConfig.SYSTEMUI_STATUS_BAR_PKG, pkgName == null ? + request.getStatusBarThemePackageName() : pkgName); + } + + if (request.getNavBarThemePackageName() != null) { + builder.overlay(ThemeConfig.SYSTEMUI_NAVBAR_PKG, pkgName == null ? + request.getNavBarThemePackageName() : pkgName); + } + + // check for any per app overlays being applied + Map<String, String> appOverlays = request.getPerAppOverlays(); + for (String appPkgName : appOverlays.keySet()) { + if (appPkgName != null) { + builder.overlay(appPkgName, appOverlays.get(appPkgName)); + } + } + + return builder; + } + + private static void removePerAppThemesFromConfig(ThemeConfig.Builder builder, + ThemeConfig themeConfig) { + if (themeConfig != null) { + Map<String, ThemeConfig.AppTheme> themes = themeConfig.getAppThemes(); + for (String appPkgName : themes.keySet()) { + if (ThemeUtils.isPerAppThemeComponent(appPkgName)) { + builder.overlay(appPkgName, null); + } + } + } + } + + // Kill the current Home process, they tend to be evil and cache + // drawable references in all apps + private void killLaunchers(ThemeChangeRequest request) { + if (request.getOverlayThemePackageName() == null + && request.getIconsThemePackageName() == null) { + return; + } + + final ActivityManager am = + (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); + + Intent homeIntent = new Intent(); + homeIntent.setAction(Intent.ACTION_MAIN); + homeIntent.addCategory(Intent.CATEGORY_HOME); + + List<ResolveInfo> infos = mPM.queryIntentActivities(homeIntent, 0); + List<ResolveInfo> themeChangeInfos = mPM.queryBroadcastReceivers( + new Intent(ThemeUtils.ACTION_THEME_CHANGED), 0); + for(ResolveInfo info : infos) { + if (info.activityInfo != null && info.activityInfo.applicationInfo != null && + !isSetupActivity(info) && !handlesThemeChanges( + info.activityInfo.applicationInfo.packageName, themeChangeInfos)) { + String pkgToStop = info.activityInfo.applicationInfo.packageName; + Log.d(TAG, "Force stopping " + pkgToStop + " for theme change"); + try { + am.forceStopPackage(pkgToStop); + } catch(Exception e) { + Log.e(TAG, "Unable to force stop package, did you forget platform signature?", + e); + } + } + } + } + + private boolean isSetupActivity(ResolveInfo info) { + return GOOGLE_SETUPWIZARD_PACKAGE.equals(info.activityInfo.packageName) || + MANAGED_PROVISIONING_PACKAGE.equals(info.activityInfo.packageName) || + CM_SETUPWIZARD_PACKAGE.equals(info.activityInfo.packageName); + } + + private boolean handlesThemeChanges(String pkgName, List<ResolveInfo> infos) { + if (infos != null && infos.size() > 0) { + for (ResolveInfo info : infos) { + if (info.activityInfo.applicationInfo.packageName.equals(pkgName)) { + return true; + } + } + } + return false; + } + + private void postProgress() { + int N = mClients.beginBroadcast(); + for(int i=0; i < N; i++) { + IThemeChangeListener listener = mClients.getBroadcastItem(0); + try { + listener.onProgress(mProgress); + } catch(RemoteException e) { + Log.w(TAG, "Unable to post progress to client listener", e); + } + } + mClients.finishBroadcast(); + } + + private void postFinish(boolean isSuccess, ThemeChangeRequest request, long updateTime) { + synchronized(this) { + mProgress = 0; + } + + int N = mClients.beginBroadcast(); + for(int i=0; i < N; i++) { + IThemeChangeListener listener = mClients.getBroadcastItem(0); + try { + listener.onFinish(isSuccess); + } catch(RemoteException e) { + Log.w(TAG, "Unable to post progress to client listener", e); + } + } + mClients.finishBroadcast(); + + // if successful, broadcast that the theme changed + if (isSuccess) { + broadcastThemeChange(request, updateTime); + } + } + + private void postFinishedProcessing(String pkgName) { + int N = mProcessingListeners.beginBroadcast(); + for(int i=0; i < N; i++) { + IThemeProcessingListener listener = mProcessingListeners.getBroadcastItem(0); + try { + listener.onFinishedProcessing(pkgName); + } catch(RemoteException e) { + Log.w(TAG, "Unable to post progress to listener", e); + } + } + mProcessingListeners.finishBroadcast(); + } + + private void broadcastThemeChange(ThemeChangeRequest request, long updateTime) { + Map<String, String> componentMap = request.getThemeComponentsMap(); + if (componentMap == null || componentMap.size() == 0) return; + + final Intent intent = new Intent(ThemeUtils.ACTION_THEME_CHANGED); + ArrayList componentsArrayList = new ArrayList(componentMap.keySet()); + intent.putStringArrayListExtra(ThemeUtils.EXTRA_COMPONENTS, componentsArrayList); + intent.putExtra(ThemeUtils.EXTRA_REQUEST_TYPE, request.getReqeustType().ordinal()); + intent.putExtra(ThemeUtils.EXTRA_UPDATE_TIME, updateTime); + mContext.sendBroadcastAsUser(intent, UserHandle.ALL); + } + + private void incrementProgress(int increment) { + synchronized(this) { + mProgress += increment; + if (mProgress > 100) mProgress = 100; + } + postProgress(); + } + + private boolean applyBootAnimation(String themePath) { + boolean success = false; + try { + ZipFile zip = new ZipFile(new File(themePath)); + ZipEntry ze = zip.getEntry(THEME_BOOTANIMATION_PATH); + if (ze != null) { + clearBootAnimation(); + BufferedInputStream is = new BufferedInputStream(zip.getInputStream(ze)); + final String bootAnimationPath = SYSTEM_THEME_PATH + File.separator + + "bootanimation.zip"; + ThemeUtils.copyAndScaleBootAnimation(mContext, is, bootAnimationPath); + FileUtils.setPermissions(bootAnimationPath, + FileUtils.S_IRWXU|FileUtils.S_IRGRP|FileUtils.S_IROTH, -1, -1); + } + zip.close(); + success = true; + } catch (Exception e) { + Log.w(TAG, "Unable to load boot animation for " + themePath, e); + } + + return success; + } + + private void clearBootAnimation() { + File anim = new File(SYSTEM_THEME_PATH + File.separator + "bootanimation.zip"); + if (anim.exists()) + anim.delete(); + } + + private BroadcastReceiver mWallpaperChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!mWallpaperChangedByUs) { + // In case the mixnmatch table has a mods_launcher entry, we'll clear it + ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder(); + builder.setWallpaper(""); + updateProvider(builder.build(), System.currentTimeMillis()); + } else { + mWallpaperChangedByUs = false; + } + } + }; + + private BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + if (userHandle >= 0 && userHandle != mCurrentUserId) { + mCurrentUserId = userHandle; + ThemeConfig config = ThemeConfig.getBootThemeForUser(mContext.getContentResolver(), + userHandle); + if (DEBUG) { + Log.d(TAG, + "Changing theme for user " + userHandle + " to " + config.toString()); + } + ThemeChangeRequest request = new ThemeChangeRequest.Builder(config).build(); + try { + ((IThemeService) mService).requestThemeChange(request, true); + } catch (RemoteException e) { + Log.e(TAG, "Unable to change theme for user change", e); + } + } + } + }; + + private void processInstalledThemes() { + final String defaultTheme = getDefaultThemePackageName(mContext); + Message msg; + // Make sure the default theme is the first to get processed! + if (!ThemeConfig.SYSTEM_DEFAULT.equals(defaultTheme)) { + msg = mHandler.obtainMessage( + ResourceProcessingHandler.MESSAGE_QUEUE_THEME_FOR_PROCESSING, + 0, 0, defaultTheme); + mResourceProcessingHandler.sendMessage(msg); + } + // Iterate over all installed packages and queue up the ones that are themes or icon packs + List<PackageInfo> packages = mPM.getInstalledPackages(0); + for (PackageInfo info : packages) { + if (!defaultTheme.equals(info.packageName) && + (info.isThemeApk || info.isLegacyIconPackApk)) { + msg = mHandler.obtainMessage( + ResourceProcessingHandler.MESSAGE_QUEUE_THEME_FOR_PROCESSING, + 0, 0, info.packageName); + mResourceProcessingHandler.sendMessage(msg); + } + } + } + + private void sendThemeResourcesCachedBroadcast(String themePkgName, int resultCode) { + final Intent intent = new Intent(Intent.ACTION_THEME_RESOURCES_CACHED); + intent.putExtra(Intent.EXTRA_THEME_PACKAGE_NAME, themePkgName); + intent.putExtra(Intent.EXTRA_THEME_RESULT, resultCode); + mContext.sendBroadcastAsUser(intent, UserHandle.ALL); + } + + /** + * Posts a notification to let the user know the theme was not installed. + * @param name + */ + private void postFailedThemeInstallNotification(String name) { + NotificationManager nm = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + Notification notice = new Notification.Builder(mContext) + .setAutoCancel(true) + .setOngoing(false) + .setContentTitle( + mContext.getString(R.string.theme_install_error_title)) + .setContentText(String.format( + mContext.getString(R.string.theme_install_error_message), name)) + .setSmallIcon(android.R.drawable.stat_notify_error) + .setWhen(System.currentTimeMillis()) + .build(); + nm.notify(name.hashCode(), notice); + } + + private String getThemeName(PackageInfo pi) { + if (pi.themeInfo != null) { + return pi.themeInfo.name; + } else if (pi.isLegacyIconPackApk) { + return pi.applicationInfo.name; + } + + return null; + } + + /** + * Get the default theme package name + * Historically this was done using {@link ThemeUtils#getDefaultThemePackageName(Context)} but + * the setting that is queried in that method uses the AOSP settings provider but the setting + * is now in CMSettings. Since {@link ThemeUtils} is in the core framework we cannot access + * CMSettings. + * @param context + * @return Default theme package name + */ + private static String getDefaultThemePackageName(Context context) { + final String defaultThemePkg = CMSettings.Secure.getString(context.getContentResolver(), + CMSettings.Secure.DEFAULT_THEME_PACKAGE); + if (!TextUtils.isEmpty(defaultThemePkg)) { + PackageManager pm = context.getPackageManager(); + try { + if (pm.getPackageInfo(defaultThemePkg, 0) != null) { + return defaultThemePkg; + } + } catch (PackageManager.NameNotFoundException e) { + // doesn't exist so system will be default + Log.w(TAG, "Default theme " + defaultThemePkg + " not found", e); + } + } + + return SYSTEM_DEFAULT; + } + + private final IBinder mService = new IThemeService.Stub() { + @Override + public void requestThemeChangeUpdates(IThemeChangeListener listener) + throws RemoteException { + enforcePermission(); + mClients.register(listener); + } + + @Override + public void removeUpdates(IThemeChangeListener listener) throws RemoteException { + enforcePermission(); + mClients.unregister(listener); + } + + @Override + public void requestThemeChange(ThemeChangeRequest request, boolean removePerAppThemes) + throws RemoteException { + enforcePermission(); + Message msg; + + /** + * Since the ThemeService handles compiling theme resource we need to make sure that any + * of the components we are trying to apply are either already processed or put to the + * front of the queue and handled before the theme change takes place. + * + * TODO: create a callback that can be sent to any ThemeChangeListeners to notify them + * that the theme will be applied once the processing is done. + */ + synchronized (mThemesToProcessQueue) { + Map<String, String> componentMap = request.getThemeComponentsMap(); + for (Object key : componentMap.keySet()) { + if (ThemesColumns.MODIFIES_OVERLAYS.equals(key) || + ThemesColumns.MODIFIES_NAVIGATION_BAR.equals(key) || + ThemesColumns.MODIFIES_STATUS_BAR.equals(key) || + ThemesColumns.MODIFIES_ICONS.equals(key)) { + String pkgName = componentMap.get(key); + if (mThemesToProcessQueue.indexOf(pkgName) > 0) { + mThemesToProcessQueue.remove(pkgName); + mThemesToProcessQueue.add(0, pkgName); + // We want to make sure these resources are taken care of first so + // send the dequeue message and place it in the front of the queue + msg = mResourceProcessingHandler.obtainMessage( + ResourceProcessingHandler.MESSAGE_DEQUEUE_AND_PROCESS_THEME); + mResourceProcessingHandler.sendMessageAtFrontOfQueue(msg); + } + } + } + } + msg = Message.obtain(); + msg.what = ThemeWorkerHandler.MESSAGE_CHANGE_THEME; + msg.obj = request; + msg.arg1 = removePerAppThemes ? 1 : 0; + mHandler.sendMessage(msg); + } + + @Override + public void applyDefaultTheme() { + enforcePermission(); + Message msg = Message.obtain(); + msg.what = ThemeWorkerHandler.MESSAGE_APPLY_DEFAULT_THEME; + mHandler.sendMessage(msg); + } + + @Override + public boolean isThemeApplying() throws RemoteException { + enforcePermission(); + return mIsThemeApplying; + } + + @Override + public int getProgress() throws RemoteException { + enforcePermission(); + synchronized(this) { + return mProgress; + } + } + + @Override + public boolean processThemeResources(String themePkgName) throws RemoteException { + enforcePermission(); + try { + mPM.getPackageInfo(themePkgName, 0); + } catch (PackageManager.NameNotFoundException e) { + // Package doesn't exist so nothing to process + return false; + } + // Obtain a message and send it to the handler to process this theme + Message msg = mResourceProcessingHandler.obtainMessage( + ResourceProcessingHandler.MESSAGE_QUEUE_THEME_FOR_PROCESSING, 0, 0, + themePkgName); + mResourceProcessingHandler.sendMessage(msg); + return true; + } + + @Override + public boolean isThemeBeingProcessed(String themePkgName) throws RemoteException { + enforcePermission(); + synchronized (mThemesToProcessQueue) { + return mThemesToProcessQueue.contains(themePkgName); + } + } + + @Override + public void registerThemeProcessingListener(IThemeProcessingListener listener) + throws RemoteException { + enforcePermission(); + mProcessingListeners.register(listener); + } + + @Override + public void unregisterThemeProcessingListener(IThemeProcessingListener listener) + throws RemoteException { + enforcePermission(); + mProcessingListeners.unregister(listener); + } + + @Override + public void rebuildResourceCache() throws RemoteException { + enforcePermission(); + mHandler.sendEmptyMessage(ThemeWorkerHandler.MESSAGE_REBUILD_RESOURCE_CACHE); + } + + @Override + public long getLastThemeChangeTime() { + return mLastThemeChangeTime; + } + + @Override + public int getLastThemeChangeRequestType() { + return mLastThemeChangeRequestType; + } + + private void enforcePermission() { + mContext.enforceCallingOrSelfPermission(ACCESS_THEME_MANAGER, null); + } + }; +} |