diff options
Diffstat (limited to 'src')
16 files changed, 2518 insertions, 468 deletions
diff --git a/src/org/cyanogenmod/themes/provider/AppReceiver.java b/src/org/cyanogenmod/themes/provider/AppReceiver.java index c856abd..af9688b 100644 --- a/src/org/cyanogenmod/themes/provider/AppReceiver.java +++ b/src/org/cyanogenmod/themes/provider/AppReceiver.java @@ -39,9 +39,10 @@ public class AppReceiver extends BroadcastReceiver { final String action = intent.getAction(); try { if (Intent.ACTION_PACKAGE_ADDED.equals(action) && !isReplacing) { - if (!isThemeBeingProcessed(context, pkgName)) { - ThemePackageHelper.insertPackage(context, pkgName); - } else { + final boolean themeProcessing = isThemeBeingProcessed(context, pkgName); + ThemePackageHelper.insertPackage(context, pkgName, !themeProcessing); + + if (themeProcessing) { // store this package name so we know it's being processed and it can be // added to the DB when ACTION_THEME_RESOURCES_CACHED is received PreferenceUtils.addThemeBeingProcessed(context, pkgName); @@ -49,17 +50,18 @@ public class AppReceiver extends BroadcastReceiver { } else if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) { ThemePackageHelper.removePackage(context, pkgName); } else if (Intent.ACTION_PACKAGE_REPLACED.equals(action)) { - if (!isThemeBeingProcessed(context, pkgName)) { - if (themeExistsInProvider(context, pkgName)) { - ThemePackageHelper.updatePackage(context, pkgName); - } else { - // Edge case where app was not a theme in previous install - ThemePackageHelper.insertPackage(context, pkgName); - } + final boolean themeProcessing = isThemeBeingProcessed(context, pkgName); + if (themeExistsInProvider(context, pkgName)) { + ThemePackageHelper.updatePackage(context, pkgName); } else { - // store this package name so we know it's being processed and it can be - // added to the DB when ACTION_THEME_RESOURCES_CACHED is received - PreferenceUtils.addThemeBeingProcessed(context, pkgName); + // Edge case where app was not a theme in previous install + ThemePackageHelper.insertPackage(context, pkgName, !themeProcessing); + + if (themeProcessing) { + // store this package name so we know it's being processed and it can be + // added or updated when ACTION_THEME_RESOURCES_CACHED is received + PreferenceUtils.addThemeBeingProcessed(context, pkgName); + } } } else if (Intent.ACTION_THEME_RESOURCES_CACHED.equals(action)) { final String themePkgName = intent.getStringExtra(Intent.EXTRA_THEME_PACKAGE_NAME); @@ -73,7 +75,7 @@ public class AppReceiver extends BroadcastReceiver { ThemePackageHelper.updatePackage(context, themePkgName); } else { // Edge case where app was not a theme in previous install - ThemePackageHelper.insertPackage(context, themePkgName); + ThemePackageHelper.insertPackage(context, themePkgName, true); } } } diff --git a/src/org/cyanogenmod/themes/provider/BitmapUtils.java b/src/org/cyanogenmod/themes/provider/BitmapUtils.java deleted file mode 100644 index 4a49068..0000000 --- a/src/org/cyanogenmod/themes/provider/BitmapUtils.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2014 The CyanogenMod Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.cyanogenmod.themes.provider; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.util.Log; - -import java.io.Closeable; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; - -public class BitmapUtils { - private static final String TAG = "BitmapUtils"; - private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5; - - /** - * Returns the bitmap from the given uri loaded using the given options. - * Returns null on failure. - */ - public static Bitmap loadBitmap(Context context, InputStream is, BitmapFactory.Options o) { - try { - return BitmapFactory.decodeStream(is, null, o); - } finally { - closeSilently(is); - } - } - - /** - * Loads a bitmap that has been downsampled using sampleSize from a given url. - */ - public static Bitmap loadDownsampledBitmap(Context context, InputStream is, int sampleSize) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inMutable = true; - options.inSampleSize = sampleSize; - return loadBitmap(context, is, options); - } - - /** - * Loads a bitmap that is downsampled by at least the input sample size. In - * low-memory situations, the bitmap may be downsampled further. - */ - public static Bitmap loadBitmapWithBackouts(Context context, InputStream is, int sampleSize) { - boolean noBitmap = true; - int num_tries = 0; - if (sampleSize <= 0) { - sampleSize = 1; - } - Bitmap bmap = null; - while (noBitmap) { - try { - // Try to decode, downsample if low-memory. - bmap = loadDownsampledBitmap(context, is, sampleSize); - noBitmap = false; - } catch (java.lang.OutOfMemoryError e) { - // Try with more downsampling before failing for good. - if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) { - throw e; - } - bmap = null; - System.gc(); - sampleSize *= 2; - } - } - return bmap; - } - - public static void closeSilently(Closeable c) { - if (c == null) return; - try { - c.close(); - } catch (IOException t) { - Log.w(TAG, "close fail ", t); - } - } -} diff --git a/src/org/cyanogenmod/themes/provider/CopyImageService.java b/src/org/cyanogenmod/themes/provider/CopyImageService.java deleted file mode 100644 index cbf7be2..0000000 --- a/src/org/cyanogenmod/themes/provider/CopyImageService.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (C) 2014 The CyanogenMod Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.cyanogenmod.themes.provider; - -import android.app.IntentService; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.AssetManager; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Environment; -import android.provider.ThemesContract.ThemesColumns; -import android.util.Log; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; - -/* - * Copies images from the theme APK to the local provider's cache - */ -public class CopyImageService extends IntentService { - public static final String ACTION_INSERT = "org.cyanogenmod.themes.provider.action.insert"; - public static final String ACTION_DELETE = "org.cyanogenmod.themes.provider.action.delete"; - public static final String EXTRA_PKG_NAME = "extra_pkg_name"; - - private static final String TAG = CopyImageService.class.getName(); - private static final String IMAGES_PATH = - "/data/org.cyanogenmod.themes.provider/files/images/"; - private static final String WALLPAPER_PATH = - "/data/org.cyanogenmod.themes.provider/files/wallpapers/"; - - private static final String WALLPAPER_PREVIEW = "images/wallpaper_preview"; - private static final String LOCKSCREEN_PREVIEW = "images/lockscreen_preview"; - private static final String STYLES_PREVIEW = "images/styles_preview"; - - private static final String EXT_JPG = ".jpg"; - private static final String EXT_PNG = ".png"; - - public CopyImageService() { - super(CopyImageService.class.getName()); - } - - @Override - protected void onHandleIntent(Intent intent) { - if (intent.getExtras() == null || intent.getExtras().getString(EXTRA_PKG_NAME) == null) { - Log.e(TAG, "No package name or extras provided"); - return; - } - - String pkgName = intent.getExtras().getString(EXTRA_PKG_NAME); - if (ACTION_INSERT.equals(intent.getAction())) { - createPreviewImages(this, pkgName); - insertPreviewValuesIntoDb(pkgName); - } else if (ACTION_DELETE.equals(intent.getAction())) { - deletePreviewImages(pkgName); - } - } - - public static void createPreviewImages(Context context, String pkgName) { - // Presently this is just mocked up. IE We expect the theme APK to - // provide the bitmap. - Context themeContext = null; - try { - themeContext = context.createPackageContext(pkgName, - Context.CONTEXT_IGNORE_SECURITY); - } catch (NameNotFoundException e) { - Log.e(TAG, "Error getting themeContext", e); - return; - } - - // This is for testing only. We copy some assets from APK and put into - // internal storage - AssetManager assetManager = themeContext.getAssets(); - try { - InputStream homescreen = getPreviewAsset(assetManager, WALLPAPER_PREVIEW); - - InputStream lockscreen = getPreviewAsset(assetManager, LOCKSCREEN_PREVIEW); - - File dataDir = context.getFilesDir(); // Environment.getDataDirectory(); - File imgDir = new File(dataDir, "images"); - File wpDir = new File(dataDir, "wallpapers"); - imgDir.mkdir(); - wpDir.mkdir(); - - File homescreenOut = new File(imgDir, pkgName + ".homescreen.jpg"); - File lockscreenOut = new File(imgDir, pkgName + ".lockscreen.jpg"); - - FileOutputStream out = new FileOutputStream(homescreenOut); - Bitmap bmp = BitmapUtils.loadBitmapWithBackouts(context, homescreen, 1); - bmp.compress(Bitmap.CompressFormat.JPEG, 90, out); - out.close(); - - out = new FileOutputStream(lockscreenOut); - bmp = BitmapUtils.loadBitmapWithBackouts(context, lockscreen, 1); - bmp.compress(Bitmap.CompressFormat.JPEG, 90, out); - out.close(); - } catch (IOException e) { - Log.e(TAG, "ThemesOpenHelper could not copy preview image"); - } - - //Copy Style preview - try { - InputStream stylepreview = getPreviewAsset(assetManager, STYLES_PREVIEW); - File dataDir = context.getFilesDir(); - File imgDir = new File(dataDir, "images"); - imgDir.mkdir(); - - File styleOut = new File(imgDir, pkgName + ".stylepreview.jpg"); - - FileOutputStream out = new FileOutputStream(styleOut); - Bitmap bmp = BitmapUtils.loadBitmapWithBackouts(context, stylepreview, 1); - bmp.compress(Bitmap.CompressFormat.JPEG, 90, out); - out.close(); - } catch (IOException e) { - Log.e(TAG, "ThemesOpenHelper could not copy style image data"); - } - } - - private void insertPreviewValuesIntoDb(String pkgName) { - String homescreen = getHomeScreenPreviewPath(pkgName); - String lockscreen = getLockScreenPreviewPath(pkgName); - String stylePreview = getStylesPreviewPath(pkgName); - String wallpaper = getWallpaperPreviewPath(pkgName); - - Uri hsUri = Uri.parse(homescreen); - Uri lsUri = Uri.parse(lockscreen); - Uri wpUri = Uri.parse(wallpaper); - Uri styleUri = Uri.parse(stylePreview); - - String where = ThemesColumns.PKG_NAME + "=?"; - String[] selectionArgs = { pkgName }; - - ContentValues values = new ContentValues(); - values.put(ThemesColumns.HOMESCREEN_URI, hsUri.toString()); - values.put(ThemesColumns.LOCKSCREEN_URI, lsUri.toString()); - values.put(ThemesColumns.STYLE_URI, styleUri.toString()); - values.put(ThemesColumns.WALLPAPER_URI, "file:///android_asset/wallpapers/wallpaper1.jpg"); - - getContentResolver().update(ThemesColumns.CONTENT_URI, values, - where, selectionArgs); - } - - private void deletePreviewImages(String pkgName) { - File home = new File(getHomeScreenPreviewPath(pkgName)); - home.delete(); - - File lockscreen = new File(getLockScreenPreviewPath(pkgName)); - lockscreen.delete(); - - File style = new File(getStylesPreviewPath(pkgName)); - style.delete(); - - File wallpaper = new File(getWallpaperPreviewPath(pkgName)); - wallpaper.delete(); - } - - private static String getHomeScreenPreviewPath(String pkgName) { - return Environment.getDataDirectory().getPath() - + IMAGES_PATH + pkgName - + ".homescreen.jpg"; - } - - private static String getLockScreenPreviewPath(String pkgName) { - return Environment.getDataDirectory().getPath() - + IMAGES_PATH + pkgName - + ".lockscreen.jpg"; - } - - private static String getStylesPreviewPath(String pkgName) { - return Environment.getDataDirectory().getPath() - + IMAGES_PATH + pkgName - + ".stylepreview.jpg"; - } - - private static String getWallpaperPreviewPath(String pkgName) { - return ContentResolver.SCHEME_FILE + "://" - + Environment.getDataDirectory().getPath() - + WALLPAPER_PATH + pkgName - + ".wallpaper1.jpg"; - } - - private static InputStream getPreviewAsset(AssetManager am, String preview) throws IOException { - InputStream is = null; - try { - is = am.open(preview + EXT_JPG); - } catch (IOException e) { - // we'll try and fallback to PNG - } - if (is == null) is = am.open(preview + EXT_PNG); - - return is; - } -} diff --git a/src/org/cyanogenmod/themes/provider/PreviewGenerationService.java b/src/org/cyanogenmod/themes/provider/PreviewGenerationService.java new file mode 100644 index 0000000..0db60f6 --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/PreviewGenerationService.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.cyanogenmod.themes.provider; + +import android.app.IntentService; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.ThemeConfig; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.os.Bundle; +import android.provider.ThemesContract.ThemesColumns; +import android.provider.ThemesContract.PreviewColumns; +import android.util.Log; +import org.cyanogenmod.themes.provider.util.BootAnimationPreviewGenerator; +import org.cyanogenmod.themes.provider.util.IconPreviewGenerator; +import org.cyanogenmod.themes.provider.util.IconPreviewGenerator.IconItems; +import org.cyanogenmod.themes.provider.util.StylePreviewGenerator; +import org.cyanogenmod.themes.provider.util.StylePreviewGenerator.StyleItems; +import org.cyanogenmod.themes.provider.util.SystemUiPreviewGenerator; +import org.cyanogenmod.themes.provider.util.SystemUiPreviewGenerator.SystemUiItems; +import org.cyanogenmod.themes.provider.util.WallpaperPreviewGenerator; +import org.cyanogenmod.themes.provider.util.WallpaperPreviewGenerator.WallpaperItems; + +import java.io.ByteArrayOutputStream; + +/* + * Copies images from the theme APK to the local provider's cache + */ +public class PreviewGenerationService extends IntentService { + public static final String ACTION_INSERT = "org.cyanogenmod.themes.provider.action.insert"; + public static final String ACTION_UPDATE = "org.cyanogenmod.themes.provider.action.update"; + public static final String EXTRA_PKG_NAME = "extra_pkg_name"; + public static final String EXTRA_HAS_SYSTEMUI = "extra_has_system_ui"; + public static final String EXTRA_HAS_ICONS = "extra_has_icons"; + public static final String EXTRA_HAS_WALLPAPER = "extra_has_wallpaper"; + public static final String EXTRA_HAS_STYLES = "extra_has_styles"; + public static final String EXTRA_HAS_BOOTANIMATION = "extra_has_bootanimation"; + + private static final String TAG = PreviewGenerationService.class.getName(); + + public PreviewGenerationService() { + super(PreviewGenerationService.class.getName()); + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent.getExtras() == null || intent.getExtras().getString(EXTRA_PKG_NAME) == null) { + Log.e(TAG, "No package name or extras provided"); + return; + } + + final Bundle extras = intent.getExtras(); + String pkgName = extras.getString(EXTRA_PKG_NAME); + boolean hasSystemUi = extras.getBoolean(EXTRA_HAS_SYSTEMUI, false); + boolean hasIcons = extras.getBoolean(EXTRA_HAS_ICONS, false); + boolean hasWallpaper = extras.getBoolean(EXTRA_HAS_WALLPAPER, false); + boolean hasStyles = extras.getBoolean(EXTRA_HAS_STYLES, false); + boolean hasBootanimation = extras.getBoolean(EXTRA_HAS_BOOTANIMATION, false); + boolean isSystemTheme = ThemeConfig.SYSTEM_DEFAULT.equals(pkgName); + final String action = intent.getAction(); + if (ACTION_INSERT.equals(action) || ACTION_UPDATE.equals(action)) { + PackageInfo info = null; + try { + if (!isSystemTheme ) { + info = getPackageManager().getPackageInfo(pkgName, 0); + } + } catch (NameNotFoundException e) { + Log.e(TAG, "Unable to get package info for " + pkgName, e); + } + if (isSystemTheme || info != null) { + SystemUiItems items = null; + try { + items = !hasSystemUi ? null : + new SystemUiPreviewGenerator(this).generateSystemUiItems(pkgName); + } catch (Exception e) { + Log.e(TAG, "Unable to create statusbar previews for " + pkgName, e); + } + + IconItems iconItems = null; + if (hasIcons) { + try { + iconItems = new IconPreviewGenerator(this).generateIconItems(pkgName); + } catch (Exception e) { + Log.e(TAG, "Unable to create icon previews for " + pkgName, e); + } + } + + WallpaperItems wallpaperItems = null; + if (hasWallpaper) { + try { + wallpaperItems = new WallpaperPreviewGenerator(this) + .generateWallpaperPreviews(info); + } catch (Exception e) { + Log.e(TAG, "Unable to create wallpaper previews for " + pkgName, e); + } + } + + StyleItems styleItems = null; + if (hasStyles) { + try { + styleItems = new StylePreviewGenerator(this).generateStylePreviews(pkgName); + } catch (Exception e) { + Log.e(TAG, "Unable to create style previews for " + pkgName, e); + } + } + + Bitmap bootAnim = null; + if (hasBootanimation) { + try { + bootAnim = new BootAnimationPreviewGenerator(this) + .generateBootAnimationPreview(pkgName); + } catch (Exception e) { + Log.e(TAG, "Unable to create boot animation preview for " + pkgName, e); + } + } + insertPreviewItemsIntoDb(pkgName, items, iconItems, wallpaperItems, styleItems, + bootAnim); + } + } + } + + private void insertPreviewItemsIntoDb(String pkgName, SystemUiItems items, IconItems icons, + WallpaperItems wallpaperItems, StyleItems styleItems, + Bitmap bootAnim) { + String[] projection = {ThemesColumns._ID}; + String selection = ThemesColumns.PKG_NAME + "=?"; + String[] selectionArgs = { pkgName }; + + final ContentResolver resolver = getContentResolver(); + Cursor cursor = resolver.query(ThemesColumns.CONTENT_URI, projection, selection, + selectionArgs, null); + + if (cursor != null) { + cursor.moveToFirst(); + int id = cursor.getInt(cursor.getColumnIndexOrThrow(ThemesColumns._ID)); + cursor.close(); + + ContentValues values = new ContentValues(); + values.put(PreviewColumns.THEME_ID, id); + if (items != null) { + values.put(PreviewColumns.STATUSBAR_BACKGROUND, + getBitmapBlobPng(items.statusbarBackground)); + values.put(PreviewColumns.STATUSBAR_BLUETOOTH_ICON, + getBitmapBlobPng(items.bluetoothIcon)); + values.put(PreviewColumns.STATUSBAR_WIFI_ICON, + getBitmapBlobPng(items.wifiIcon)); + values.put(PreviewColumns.STATUSBAR_SIGNAL_ICON, + getBitmapBlobPng(items.signalIcon)); + values.put(PreviewColumns.STATUSBAR_BATTERY_PORTRAIT, + getBitmapBlobPng(items.batteryPortrait)); + values.put(PreviewColumns.STATUSBAR_BATTERY_LANDSCAPE, + getBitmapBlobPng(items.batteryLandscape)); + values.put(PreviewColumns.STATUSBAR_BATTERY_CIRCLE, + getBitmapBlobPng(items.batteryCircle)); + values.put(PreviewColumns.STATUSBAR_CLOCK_TEXT_COLOR, items.clockColor); + values.put(PreviewColumns.STATUSBAR_WIFI_COMBO_MARGIN_END, items.wifiMarginEnd); + values.put(PreviewColumns.NAVBAR_BACKGROUND, + getBitmapBlobPng(items.navbarBackground)); + values.put(PreviewColumns.NAVBAR_BACK_BUTTON, + getBitmapBlobPng(items.navbarBack)); + values.put(PreviewColumns.NAVBAR_HOME_BUTTON, + getBitmapBlobPng(items.navbarHome)); + values.put(PreviewColumns.NAVBAR_RECENT_BUTTON, + getBitmapBlobPng(items.navbarRecent)); + } + if (icons != null) { + values.put(PreviewColumns.ICON_PREVIEW_1, getBitmapBlobPng(icons.icon1)); + values.put(PreviewColumns.ICON_PREVIEW_2, getBitmapBlobPng(icons.icon2)); + values.put(PreviewColumns.ICON_PREVIEW_3, getBitmapBlobPng(icons.icon3)); + } + if (wallpaperItems != null) { + values.put(PreviewColumns.WALLPAPER_PREVIEW, + getBitmapBlobJpg(wallpaperItems.wpPreview)); + values.put(PreviewColumns.WALLPAPER_THUMBNAIL, + getBitmapBlobPng(wallpaperItems.wpThumbnail)); + values.put(PreviewColumns.LOCK_WALLPAPER_PREVIEW, + getBitmapBlobJpg(wallpaperItems.lsPreview)); + values.put(PreviewColumns.LOCK_WALLPAPER_THUMBNAIL, + getBitmapBlobPng(wallpaperItems.lsThumbnail)); + } + if (styleItems != null) { + values.put(PreviewColumns.STYLE_THUMBNAIL, getBitmapBlobPng(styleItems.thumbnail)); + values.put(PreviewColumns.STYLE_PREVIEW, getBitmapBlobPng(styleItems.preview)); + } + if (bootAnim != null) { + values.put(PreviewColumns.BOOTANIMATION_THUMBNAIL, getBitmapBlobPng(bootAnim)); + } + + selection = PreviewColumns.THEME_ID + "=?"; + selectionArgs = new String[] { String.valueOf(id) }; + // Try an update first, if that returns 0 then we need to insert these values + if (resolver.update( + PreviewColumns.CONTENT_URI, values, selection, selectionArgs) == 0) { + resolver.insert(PreviewColumns.CONTENT_URI, values); + } + } + } + + private static byte[] getBitmapBlobPng(Bitmap bmp) { + return getBitmapBlob(bmp, CompressFormat.PNG, 100); + } + + private static byte[] getBitmapBlobJpg(Bitmap bmp) { + return getBitmapBlob(bmp, CompressFormat.JPEG, 80); + } + + private static byte[] getBitmapBlob(Bitmap bmp, CompressFormat format, int quality) { + if (bmp == null) return null; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + bmp.compress(format, quality, out); + return out.toByteArray(); + } +} diff --git a/src/org/cyanogenmod/themes/provider/ThemePackageHelper.java b/src/org/cyanogenmod/themes/provider/ThemePackageHelper.java index feb4647..f26e5e7 100644 --- a/src/org/cyanogenmod/themes/provider/ThemePackageHelper.java +++ b/src/org/cyanogenmod/themes/provider/ThemePackageHelper.java @@ -17,7 +17,6 @@ package org.cyanogenmod.themes.provider; import android.content.ContentValues; import android.content.Context; -import android.content.pm.LegacyThemeInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; @@ -31,7 +30,6 @@ import android.database.Cursor; import android.provider.ThemesContract; import android.provider.ThemesContract.MixnMatchColumns; import android.provider.ThemesContract.ThemesColumns; -import android.text.TextUtils; import android.util.Log; import java.io.IOException; @@ -41,7 +39,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import static android.content.res.ThemeConfig.HOLO_DEFAULT; +import static android.content.res.ThemeConfig.SYSTEM_DEFAULT; /** * Helper class to populate the provider with info from the theme. @@ -61,31 +59,33 @@ public class ThemePackageHelper { sComponentToFolderName.put(ThemesColumns.MODIFIES_ALARMS, "alarms"); sComponentToFolderName.put(ThemesColumns.MODIFIES_NOTIFICATIONS, "notifications"); sComponentToFolderName.put(ThemesColumns.MODIFIES_RINGTONES, "ringtones"); + sComponentToFolderName.put(ThemesColumns.MODIFIES_STATUS_BAR, + "overlays/com.android.systemui"); + sComponentToFolderName.put(ThemesColumns.MODIFIES_NAVIGATION_BAR, + "overlays/com.android.systemui"); } - public static boolean insertPackage(Context context, String pkgName) + public static boolean insertPackage(Context context, String pkgName, boolean processPreviews) throws NameNotFoundException { PackageInfo pi = context.getPackageManager().getPackageInfo(pkgName, 0); if (pi == null) return false; Map<String, Boolean> capabilities = getCapabilities(context, pkgName); - if (pi.themeInfos != null && pi.themeInfos.length > 0) { - insertPackageInternal(context, pi, capabilities); - } else if (pi.legacyThemeInfos != null && pi.legacyThemeInfos.length > 0) { - insertLegacyPackageInternal(context, pi, capabilities); + if (pi.themeInfo != null) { + insertPackageInternal(context, pi, capabilities, processPreviews); } else if (pi.isLegacyIconPackApk){ // We must be here because it is a legacy icon pack capabilities = new HashMap<String, Boolean>(); capabilities.put(ThemesColumns.MODIFIES_ICONS, true); - insertLegacyIconPackInternal(context, pi, capabilities); + insertLegacyIconPackInternal(context, pi, capabilities,processPreviews); } return true; } private static void insertPackageInternal(Context context, PackageInfo pi, - Map<String, Boolean> capabilities) { - ThemeInfo info = pi.themeInfos[0]; + Map<String, Boolean> capabilities, boolean processPreviews) { + ThemeInfo info = pi.themeInfo; boolean isPresentableTheme = ThemePackageHelper.isPresentableTheme(capabilities); ContentValues values = new ContentValues(); @@ -94,30 +94,13 @@ public class ThemePackageHelper { values.put(ThemesColumns.AUTHOR, info.author); values.put(ThemesColumns.DATE_CREATED, System.currentTimeMillis()); values.put(ThemesColumns.PRESENT_AS_THEME, isPresentableTheme); - values.put(ThemesColumns.IS_LEGACY_THEME, pi.isLegacyThemeApk); - values.put(ThemesColumns.IS_DEFAULT_THEME, - ThemeUtils.getDefaultThemePackageName(context).equals(pi.packageName) ? 1 : 0); - values.put(ThemesColumns.LAST_UPDATE_TIME, pi.lastUpdateTime); - - // Insert theme capabilities - insertCapabilities(capabilities, values); - - context.getContentResolver().insert(ThemesColumns.CONTENT_URI, values); - } - - private static void insertLegacyPackageInternal(Context context, PackageInfo pi, - Map<String, Boolean> capabilities) { - LegacyThemeInfo info = pi.legacyThemeInfos[0]; - ContentValues values = new ContentValues(); - values.put(ThemesColumns.PKG_NAME, pi.packageName); - values.put(ThemesColumns.TITLE, info.name); - values.put(ThemesColumns.AUTHOR, info.author); - values.put(ThemesColumns.DATE_CREATED, System.currentTimeMillis()); - values.put(ThemesColumns.PRESENT_AS_THEME, 1); - values.put(ThemesColumns.IS_LEGACY_THEME, pi.isLegacyThemeApk); + values.put(ThemesColumns.IS_LEGACY_THEME, false); values.put(ThemesColumns.IS_DEFAULT_THEME, ThemeUtils.getDefaultThemePackageName(context).equals(pi.packageName) ? 1 : 0); values.put(ThemesColumns.LAST_UPDATE_TIME, pi.lastUpdateTime); + values.put(ThemesColumns.INSTALL_TIME, pi.firstInstallTime); + values.put(ThemesProvider.KEY_PROCESS_PREVIEWS, processPreviews); + values.put(ThemesColumns.TARGET_API, pi.applicationInfo.targetSdkVersion); // Insert theme capabilities insertCapabilities(capabilities, values); @@ -126,7 +109,7 @@ public class ThemePackageHelper { } private static void insertLegacyIconPackInternal(Context context, PackageInfo pi, - Map<String, Boolean> capabilities) { + Map<String, Boolean> capabilities, boolean processPreviews) { PackageManager pm = context.getPackageManager(); CharSequence labelName = pm.getApplicationLabel(pi.applicationInfo); if (labelName == null) labelName = context.getString(R.string.unknown_app_name); @@ -137,7 +120,10 @@ public class ThemePackageHelper { values.put(ThemesColumns.AUTHOR, ""); values.put(ThemesColumns.DATE_CREATED, System.currentTimeMillis()); values.put(ThemesColumns.LAST_UPDATE_TIME, pi.lastUpdateTime); + values.put(ThemesColumns.INSTALL_TIME, pi.firstInstallTime); values.put(ThemesColumns.IS_LEGACY_ICONPACK, 1); + values.put(ThemesProvider.KEY_PROCESS_PREVIEWS, processPreviews); + values.put(ThemesColumns.TARGET_API, pi.applicationInfo.targetSdkVersion); // Insert theme capabilities insertCapabilities(capabilities, values); @@ -146,15 +132,13 @@ public class ThemePackageHelper { } public static void updatePackage(Context context, String pkgName) throws NameNotFoundException { - if (HOLO_DEFAULT.equals(pkgName)) { - updateHoloPackageInternal(context); + if (SYSTEM_DEFAULT.equals(pkgName)) { + updateSystemPackageInternal(context); } else { PackageInfo pi = context.getPackageManager().getPackageInfo(pkgName, 0); Map<String, Boolean> capabilities = getCapabilities(context, pkgName); - if (pi.themeInfos != null && pi.themeInfos.length > 0) { + if (pi.themeInfo != null) { updatePackageInternal(context, pi, capabilities); - } else if (pi.legacyThemeInfos != null && pi.legacyThemeInfos.length > 0) { - updateLegacyPackageInternal(context, pi, capabilities); } else if (pi.isLegacyIconPackApk) { updateLegacyIconPackInternal(context, pi, capabilities); } @@ -166,7 +150,7 @@ public class ThemePackageHelper { private static void updatePackageInternal(Context context, PackageInfo pi, Map<String, Boolean> capabilities) { - ThemeInfo info = pi.themeInfos[0]; + ThemeInfo info = pi.themeInfo; boolean isPresentableTheme = ThemePackageHelper.isPresentableTheme(capabilities); ContentValues values = new ContentValues(); values.put(ThemesColumns.PKG_NAME, pi.packageName); @@ -174,44 +158,26 @@ public class ThemePackageHelper { values.put(ThemesColumns.AUTHOR, info.author); values.put(ThemesColumns.DATE_CREATED, System.currentTimeMillis()); values.put(ThemesColumns.PRESENT_AS_THEME, isPresentableTheme); - values.put(ThemesColumns.IS_LEGACY_THEME, pi.isLegacyThemeApk); + values.put(ThemesColumns.IS_LEGACY_THEME, false); values.put(ThemesColumns.IS_DEFAULT_THEME, ThemeUtils.getDefaultThemePackageName(context).equals(pi.packageName) ? 1 : 0); values.put(ThemesColumns.LAST_UPDATE_TIME, pi.lastUpdateTime); + values.put(ThemesColumns.INSTALL_TIME, pi.firstInstallTime); - String where = ThemesColumns.PKG_NAME + "=?"; - String[] args = { pi.packageName }; - context.getContentResolver().update(ThemesColumns.CONTENT_URI, values, where, args); - } + // Insert theme capabilities + insertCapabilities(capabilities, values); - private static void updateHoloPackageInternal(Context context) { - ContentValues values = new ContentValues(); - values.put(ThemesColumns.IS_DEFAULT_THEME, - HOLO_DEFAULT == ThemeUtils.getDefaultThemePackageName(context) ? 1 : 0); String where = ThemesColumns.PKG_NAME + "=?"; - String[] args = { HOLO_DEFAULT }; + String[] args = { pi.packageName }; context.getContentResolver().update(ThemesColumns.CONTENT_URI, values, where, args); } - private static void updateLegacyPackageInternal(Context context, PackageInfo pi, - Map<String, Boolean> capabilities) { - LegacyThemeInfo info = pi.legacyThemeInfos[0]; + private static void updateSystemPackageInternal(Context context) { ContentValues values = new ContentValues(); - values.put(ThemesColumns.PKG_NAME, pi.packageName); - values.put(ThemesColumns.TITLE, info.name); - values.put(ThemesColumns.AUTHOR, info.author); - values.put(ThemesColumns.DATE_CREATED, System.currentTimeMillis()); - values.put(ThemesColumns.PRESENT_AS_THEME, 1); - values.put(ThemesColumns.IS_LEGACY_THEME, pi.isLegacyThemeApk); values.put(ThemesColumns.IS_DEFAULT_THEME, - ThemeUtils.getDefaultThemePackageName(context).equals(pi.packageName) ? 1 : 0); - values.put(ThemesColumns.LAST_UPDATE_TIME, pi.lastUpdateTime); - - // Insert theme capabilities - insertCapabilities(capabilities, values); - + SYSTEM_DEFAULT == ThemeUtils.getDefaultThemePackageName(context) ? 1 : 0); String where = ThemesColumns.PKG_NAME + "=?"; - String[] args = { pi.packageName }; + String[] args = { SYSTEM_DEFAULT }; context.getContentResolver().update(ThemesColumns.CONTENT_URI, values, where, args); } @@ -227,6 +193,7 @@ public class ThemePackageHelper { values.put(ThemesColumns.TITLE, labelName.toString()); values.put(ThemesColumns.DATE_CREATED, System.currentTimeMillis()); values.put(ThemesColumns.LAST_UPDATE_TIME, pi.lastUpdateTime); + values.put(ThemesColumns.INSTALL_TIME, pi.firstInstallTime); String where = ThemesColumns.PKG_NAME + "=?"; String[] args = { pi.packageName }; @@ -288,8 +255,7 @@ public class ThemePackageHelper { for (Map.Entry<String, String> entry : sComponentToFolderName.entrySet()) { String component = entry.getKey(); String folderName = entry.getValue(); - boolean hasComponent = pi.isLegacyThemeApk ? hasThemeComponentLegacy(pi, component) - : hasThemeComponent(themeContext, folderName); + boolean hasComponent = hasThemeComponent(themeContext, folderName); implementMap.put(component, hasComponent); } return implementMap; @@ -304,29 +270,7 @@ public class ThemePackageHelper { } } - private static boolean hasThemeComponentLegacy(PackageInfo pi, String component) { - if (ThemesColumns.MODIFIES_OVERLAYS.equals(component)) { - return true; - } else if (ThemesColumns.MODIFIES_LAUNCHER.equals(component)) { - if (pi.legacyThemeInfos != null && pi.legacyThemeInfos.length > 0 - && pi.legacyThemeInfos[0].wallpaperResourceId != 0) { - return true; - } - } else if (ThemesColumns.MODIFIES_RINGTONES.equals(component)) { - if (pi.legacyThemeInfos != null && pi.legacyThemeInfos.length > 0 - && !TextUtils.isEmpty(pi.legacyThemeInfos[0].ringtoneFileName)) { - return true; - } - } else if (ThemesColumns.MODIFIES_NOTIFICATIONS.equals(component)) { - if (pi.legacyThemeInfos != null && pi.legacyThemeInfos.length > 0 - && !TextUtils.isEmpty(pi.legacyThemeInfos[0].notificationFileName)) { - return true; - } - } - return false; - } - - private static boolean hasThemeComponent(Context themeContext, String component) { + public static boolean hasThemeComponent(Context themeContext, String component) { boolean found = false; AssetManager assetManager = themeContext.getAssets(); try { @@ -344,11 +288,10 @@ public class ThemePackageHelper { // which implements 2+ components. Such themes can be shown to the user // under the "themes" category. public static boolean isPresentableTheme(Map<String, Boolean> implementMap) { - int count = 0; - for (Boolean isImplemented : implementMap.values()) { - count += isImplemented ? 1 : 0; - } - return count >= 2; + return implementMap != null && + implementMap.containsKey(ThemesColumns.MODIFIES_LAUNCHER) && + implementMap.containsKey(ThemesColumns.MODIFIES_ICONS) && + implementMap.containsKey(ThemesColumns.MODIFIES_OVERLAYS); } private static void reapplyInstalledComponentsForTheme(Context context, String pkgName) { diff --git a/src/org/cyanogenmod/themes/provider/ThemesOpenHelper.java b/src/org/cyanogenmod/themes/provider/ThemesOpenHelper.java index 458deae..33a8336 100644 --- a/src/org/cyanogenmod/themes/provider/ThemesOpenHelper.java +++ b/src/org/cyanogenmod/themes/provider/ThemesOpenHelper.java @@ -17,22 +17,27 @@ package org.cyanogenmod.themes.provider; import android.content.ContentValues; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.content.pm.ThemeUtils; import android.content.res.ThemeConfig; +import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.provider.ThemesContract; import android.provider.ThemesContract.ThemesColumns; import android.provider.ThemesContract.MixnMatchColumns; +import android.provider.ThemesContract.PreviewColumns; import android.util.Log; public class ThemesOpenHelper extends SQLiteOpenHelper { private static final String TAG = ThemesOpenHelper.class.getName(); - private static final int DATABASE_VERSION = 5; + private static final int DATABASE_VERSION = 11; private static final String DATABASE_NAME = "themes.db"; - private static final String DEFAULT_PKG_NAME = ThemeConfig.HOLO_DEFAULT; + private static final String DEFAULT_PKG_NAME = ThemeConfig.SYSTEM_DEFAULT; private Context mContext; @@ -45,9 +50,11 @@ public class ThemesOpenHelper extends SQLiteOpenHelper { public void onCreate(SQLiteDatabase db) { db.execSQL(ThemesTable.THEMES_TABLE_CREATE); db.execSQL(MixnMatchTable.MIXNMATCH_TABLE_CREATE); + db.execSQL(PreviewsTable.PREVIEWS_TABLE_CREATE); - ThemesTable.insertHoloDefaults(db, mContext); + ThemesTable.insertSystemDefaults(db, mContext); MixnMatchTable.insertDefaults(db); + PreviewsTable.insertDefaults(mContext); } @Override @@ -70,6 +77,30 @@ public class ThemesOpenHelper extends SQLiteOpenHelper { upgradeToVersion5(db); oldVersion = 5; } + if (oldVersion == 5) { + upgradeToVersion6(db); + oldVersion = 6; + } + if (oldVersion == 6) { + upgradeToVersion7(db); + oldVersion = 7; + } + if (oldVersion == 7) { + upgradeToVersion8(db); + oldVersion = 8; + } + if (oldVersion == 8) { + upgradeToVersion9(db); + oldVersion = 9; + } + if (oldVersion == 9) { + upgradeToVersion10(db); + oldVersion = 10; + } + if (oldVersion == 10) { + upgradeToVersion11(db); + oldVersion = 11; + } if (oldVersion != DATABASE_VERSION) { Log.e(TAG, "Recreating db because unknown database version: " + oldVersion); dropTables(db); @@ -111,12 +142,12 @@ public class ThemesOpenHelper extends SQLiteOpenHelper { db.execSQL(addIsDefault); // change default package name to holo - String changeDefaultToHolo = String.format("UPDATE %s SET %s='%s' WHERE" + + String changeDefaultToSystem = String.format("UPDATE %s SET %s='%s' WHERE" + " %s='%s'", ThemesTable.TABLE_NAME, ThemesColumns.PKG_NAME, DEFAULT_PKG_NAME, ThemesColumns.PKG_NAME, "default"); - db.execSQL(changeDefaultToHolo); + db.execSQL(changeDefaultToSystem); - if (isHoloDefault(mContext)) { + if (isSystemDefault(mContext)) { // flag holo as default if String makeHoloDefault = String.format("UPDATE %s SET %s=%d WHERE" + " %s='%s'", ThemesTable.TABLE_NAME, ThemesColumns.IS_DEFAULT_THEME, 1, @@ -130,9 +161,204 @@ public class ThemesOpenHelper extends SQLiteOpenHelper { MixnMatchColumns.COL_VALUE, "default")); } + private void upgradeToVersion6(SQLiteDatabase db) { + db.execSQL(PreviewsTable.PREVIEWS_TABLE_CREATE); + + // remove (Default) from Holo's title + db.execSQL(String.format("UPDATE %s SET %s='%s' WHERE %s='%s'", ThemesTable.TABLE_NAME, + ThemesColumns.TITLE, "Holo", ThemesColumns.PKG_NAME, "holo")); + + // we need to update any existing themes + final String[] projection = { ThemesColumns.PKG_NAME, ThemesColumns.MODIFIES_STATUS_BAR, + ThemesColumns.MODIFIES_ICONS, ThemesColumns.MODIFIES_OVERLAYS, + ThemesColumns.MODIFIES_LAUNCHER, ThemesColumns.MODIFIES_BOOT_ANIM }; + final String selection = ThemesColumns.MODIFIES_OVERLAYS + "=?"; + final String[] selectionArgs = { "1" }; + final Cursor c = db.query(ThemesTable.TABLE_NAME, projection, selection, selectionArgs, + null, null, null); + if (c != null) { + while(c.moveToNext()) { + Intent intent = new Intent(mContext, PreviewGenerationService.class); + intent.setAction(PreviewGenerationService.ACTION_INSERT); + intent.putExtra(PreviewGenerationService.EXTRA_PKG_NAME, c.getString(0)); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_SYSTEMUI, c.getInt(1) == 1); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_ICONS, c.getInt(2) == 1); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_STYLES, c.getInt(3) == 1); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_WALLPAPER, c.getInt(4) == 1); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_BOOTANIMATION, c.getInt(5) == 1); + mContext.startService(intent); + } + c.close(); + } + } + + private void upgradeToVersion7(SQLiteDatabase db) { + String addStatusBar = String.format("ALTER TABLE %s ADD COLUMN %s INTEGER", + ThemesTable.TABLE_NAME, ThemesColumns.MODIFIES_STATUS_BAR); + String addNavBar = String.format("ALTER TABLE %s ADD COLUMN %s INTEGER", + ThemesTable.TABLE_NAME, ThemesColumns.MODIFIES_NAVIGATION_BAR); + db.execSQL(addStatusBar); + db.execSQL(addNavBar); + + // we need to update any existing themes + final String[] projection = { ThemesColumns.PKG_NAME, ThemesColumns.IS_LEGACY_THEME }; + final String selection = ThemesColumns.MODIFIES_OVERLAYS + "=? OR " + + ThemesColumns.IS_LEGACY_THEME + "=?"; + final String[] selectionArgs = { "1", "1" }; + final Cursor c = db.query(ThemesTable.TABLE_NAME, projection, selection, selectionArgs, + null, null, null); + if (c != null) { + while(c.moveToNext()) { + final String pkgName = c.getString(0); + final boolean isLegacyTheme = c.getInt(1) == 1; + boolean hasSystemUi = false; + if (DEFAULT_PKG_NAME.equals(pkgName) || isLegacyTheme) { + hasSystemUi = true; + } else { + try { + Context themeContext = mContext.createPackageContext(pkgName, 0); + hasSystemUi = ThemePackageHelper.hasThemeComponent(themeContext, + ThemePackageHelper.sComponentToFolderName.get( + ThemesColumns.MODIFIES_STATUS_BAR)); + } catch (PackageManager.NameNotFoundException e) { + // default to false + } + } + if (hasSystemUi) { + db.execSQL(String.format("UPDATE %S SET %s='1', %s='1' WHERE %s='%s'", + ThemesTable.TABLE_NAME, ThemesColumns.MODIFIES_STATUS_BAR, + ThemesColumns.MODIFIES_NAVIGATION_BAR, ThemesColumns.PKG_NAME, + pkgName)); + Intent intent = new Intent(mContext, PreviewGenerationService.class); + intent.setAction(PreviewGenerationService.ACTION_INSERT); + intent.putExtra(PreviewGenerationService.EXTRA_PKG_NAME, pkgName); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_SYSTEMUI, true); + mContext.startService(intent); + } + } + c.close(); + } + } + + private void upgradeToVersion8(SQLiteDatabase db) { + String addNavBar = String.format("ALTER TABLE %s ADD COLUMN %s BLOB", + PreviewsTable.TABLE_NAME, PreviewColumns.NAVBAR_BACKGROUND); + db.execSQL(addNavBar); + + // we need to update any existing themes with the new NAVBAR_BACKGROUND + final String[] projection = { ThemesColumns.PKG_NAME, ThemesColumns.IS_LEGACY_THEME }; + final String selection = ThemesColumns.MODIFIES_OVERLAYS + "=? OR " + + ThemesColumns.IS_LEGACY_THEME + "=?"; + final String[] selectionArgs = { "1", "1" }; + final Cursor c = db.query(ThemesTable.TABLE_NAME, projection, selection, selectionArgs, + null, null, null); + if (c != null) { + while(c.moveToNext()) { + final String pkgName = c.getString(0); + final boolean isLegacyTheme = c.getInt(1) == 1; + boolean hasSystemUi = false; + if (DEFAULT_PKG_NAME.equals(pkgName) || isLegacyTheme) { + hasSystemUi = true; + } else { + try { + Context themeContext = mContext.createPackageContext(pkgName, 0); + hasSystemUi = ThemePackageHelper.hasThemeComponent(themeContext, + ThemePackageHelper.sComponentToFolderName.get( + ThemesColumns.MODIFIES_STATUS_BAR)); + } catch (PackageManager.NameNotFoundException e) { + // default to false + } + } + if (hasSystemUi) { + Intent intent = new Intent(mContext, PreviewGenerationService.class); + intent.setAction(PreviewGenerationService.ACTION_INSERT); + intent.putExtra(PreviewGenerationService.EXTRA_PKG_NAME, pkgName); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_SYSTEMUI, true); + mContext.startService(intent); + } + } + c.close(); + } + } + + private void upgradeToVersion9(SQLiteDatabase db) { + String addNavBar = String.format("ALTER TABLE %s ADD COLUMN %s INTEGER DEFAULT 0", + ThemesTable.TABLE_NAME, ThemesColumns.INSTALL_TIME); + db.execSQL(addNavBar); + + // we need to update any existing themes with their install time + final String[] projection = { ThemesColumns.PKG_NAME }; + final Cursor c = db.query(ThemesTable.TABLE_NAME, projection, null, null, null, null, null); + if (c != null) { + while(c.moveToNext()) { + final String pkgName = c.getString(0); + try { + PackageInfo pi = mContext.getPackageManager().getPackageInfo(pkgName, 0); + db.execSQL(String.format("UPDATE %s SET %s='%d' WHERE %s='%s'", + ThemesTable.TABLE_NAME, ThemesColumns.INSTALL_TIME, pi.firstInstallTime, + ThemesColumns.PKG_NAME, pkgName)); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Unable to update install time for " + pkgName, e); + } + } + c.close(); + } + } + + private void upgradeToVersion10(SQLiteDatabase db) { + // add API entries + String sql = String.format("ALTER TABLE %s ADD COLUMN %s TEXT", + ThemesTable.TABLE_NAME, ThemesColumns.TARGET_API); + db.execSQL(sql); + + // we need to update any existing themes with their install time + final String[] projection = { ThemesColumns.PKG_NAME }; + final Cursor c = db.query(ThemesTable.TABLE_NAME, projection, null, null, null, null, null); + if (c != null) { + while(c.moveToNext()) { + final String pkgName = c.getString(0); + int targetSdk = -1; + if (DEFAULT_PKG_NAME.equals(pkgName)) { + // 0 is a special value used for the system theme, not to be confused with the + // default theme which may not be the same as the system theme. + targetSdk = 0; + } else { + try { + PackageInfo pi = mContext.getPackageManager().getPackageInfo(pkgName, 0); + targetSdk = pi.applicationInfo.targetSdkVersion; + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Unable to update target sdk for " + pkgName, e); + } + } + if (targetSdk != -1) { + db.execSQL(String.format("UPDATE %s SET %s='%d' WHERE %s='%s'", ThemesTable + .TABLE_NAME, ThemesColumns.TARGET_API, + targetSdk, ThemesColumns.PKG_NAME, pkgName)); + } + } + c.close(); + } + } + + private void upgradeToVersion11(SQLiteDatabase db) { + // Update holo theme to be called "system" + final String NEW_THEME_TITLE = "System"; + final String PREV_SYSTEM_PKG_NAME = "holo"; + String holoToSystem = String.format("UPDATE TABLE %s " + + "SET title=%s, pkg_name=%s " + + "WHERE %s='%s'", + ThemesTable.TABLE_NAME, + NEW_THEME_TITLE, + DEFAULT_PKG_NAME, + ThemesColumns.PKG_NAME, PREV_SYSTEM_PKG_NAME); + db.execSQL(holoToSystem); + + } + private void dropTables(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS " + ThemesTable.TABLE_NAME); db.execSQL("DROP TABLE IF EXISTS " + MixnMatchTable.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + PreviewsTable.TABLE_NAME); } public static class ThemesTable { @@ -164,26 +390,25 @@ public class ThemesOpenHelper extends SQLiteOpenHelper { ThemesColumns.MODIFIES_NOTIFICATIONS + " INTEGER DEFAULT 0, " + ThemesColumns.MODIFIES_ALARMS + " INTEGER DEFAULT 0, " + ThemesColumns.MODIFIES_OVERLAYS + " INTEGER DEFAULT 0, " + + ThemesColumns.MODIFIES_STATUS_BAR + " INTEGER DEFAULT 0, " + + ThemesColumns.MODIFIES_NAVIGATION_BAR + " INTEGER DEFAULT 0, " + ThemesColumns.PRESENT_AS_THEME + " INTEGER DEFAULT 0, " + - ThemesColumns.IS_LEGACY_THEME + " INTEGER DEFAULT 0," + - ThemesColumns.IS_DEFAULT_THEME + " INTEGER DEFAULT 0," + - ThemesColumns.IS_LEGACY_ICONPACK + " INTEGER DEFAULT 0," + - ThemesColumns.LAST_UPDATE_TIME + " INTEGER DEFAULT 0" + + ThemesColumns.IS_LEGACY_THEME + " INTEGER DEFAULT 0, " + + ThemesColumns.IS_DEFAULT_THEME + " INTEGER DEFAULT 0, " + + ThemesColumns.IS_LEGACY_ICONPACK + " INTEGER DEFAULT 0, " + + ThemesColumns.LAST_UPDATE_TIME + " INTEGER DEFAULT 0, " + + ThemesColumns.INSTALL_TIME + " INTEGER DEFAULT 0, " + + ThemesColumns.TARGET_API + " INTEGER DEFAULT 0" + ")"; - public static void insertHoloDefaults(SQLiteDatabase db, Context context) { - int isDefault = isHoloDefault(context) ? 1 : 0; + public static void insertSystemDefaults(SQLiteDatabase db, Context context) { + int isDefault = isSystemDefault(context) ? 1 : 0; ContentValues values = new ContentValues(); - values.put(ThemesColumns.TITLE, "Holo (Default)"); + values.put(ThemesColumns.TITLE, "System"); values.put(ThemesColumns.PKG_NAME, DEFAULT_PKG_NAME); values.put(ThemesColumns.PRIMARY_COLOR, 0xff33b5e5); values.put(ThemesColumns.SECONDARY_COLOR, 0xff000000); values.put(ThemesColumns.AUTHOR, "Android"); - values.put(ThemesColumns.BOOT_ANIM_URI, "file:///android_asset/default_holo_theme/holo_boot_anim.jpg"); - values.put(ThemesColumns.HOMESCREEN_URI, "file:///android_asset/default_holo_theme/holo_homescreen.png"); - values.put(ThemesColumns.LOCKSCREEN_URI, "file:///android_asset/default_holo_theme/holo_lockscreen.png"); - values.put(ThemesColumns.STYLE_URI, "file:///android_asset/default_holo_theme/style.jpg"); - values.put(ThemesColumns.WALLPAPER_URI, "file:///android_asset/default_holo_theme/blueice_modcircle.jpg"); values.put(ThemesColumns.MODIFIES_ALARMS, 1); values.put(ThemesColumns.MODIFIES_BOOT_ANIM, 1); values.put(ThemesColumns.MODIFIES_FONTS, 1); @@ -192,6 +417,8 @@ public class ThemesOpenHelper extends SQLiteOpenHelper { values.put(ThemesColumns.MODIFIES_LOCKSCREEN, 1); values.put(ThemesColumns.MODIFIES_NOTIFICATIONS, 1); values.put(ThemesColumns.MODIFIES_RINGTONES, 1); + values.put(ThemesColumns.MODIFIES_STATUS_BAR, 1); + values.put(ThemesColumns.MODIFIES_NAVIGATION_BAR, 1); values.put(ThemesColumns.PRESENT_AS_THEME, 1); values.put(ThemesColumns.IS_LEGACY_THEME, 0); values.put(ThemesColumns.IS_DEFAULT_THEME, isDefault); @@ -219,9 +446,80 @@ public class ThemesOpenHelper extends SQLiteOpenHelper { } } - private static boolean isHoloDefault(Context context) { + public static class PreviewsTable { + protected static final String TABLE_NAME = "previews"; + private static final String PREVIEWS_TABLE_CREATE = + "CREATE TABLE " + TABLE_NAME + " (" + + PreviewColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + PreviewColumns.THEME_ID + " INTEGER, " + + PreviewColumns.STATUSBAR_BACKGROUND + " BLOB, " + + PreviewColumns.STATUSBAR_BLUETOOTH_ICON + " BLOB, " + + PreviewColumns.STATUSBAR_WIFI_ICON + " BLOB, " + + PreviewColumns.STATUSBAR_SIGNAL_ICON + " BLOB, " + + PreviewColumns.STATUSBAR_BATTERY_PORTRAIT + " BLOB, " + + PreviewColumns.STATUSBAR_BATTERY_LANDSCAPE + " BLOB, " + + PreviewColumns.STATUSBAR_BATTERY_CIRCLE + " BLOB, " + + PreviewColumns.STATUSBAR_CLOCK_TEXT_COLOR + " INTEGER, " + + PreviewColumns.STATUSBAR_WIFI_COMBO_MARGIN_END + " INTEGER, " + + PreviewColumns.NAVBAR_BACKGROUND + " BLOB, " + + PreviewColumns.NAVBAR_BACK_BUTTON + " BLOB, " + + PreviewColumns.NAVBAR_HOME_BUTTON + " BLOB, " + + PreviewColumns.NAVBAR_RECENT_BUTTON + " BLOB, " + + PreviewColumns.ICON_PREVIEW_1 + " BLOB, " + + PreviewColumns.ICON_PREVIEW_2 + " BLOB, " + + PreviewColumns.ICON_PREVIEW_3 + " BLOB, " + + PreviewColumns.ICON_PREVIEW_4 + " BLOB, " + + PreviewColumns.STYLE_PREVIEW + " BLOB, " + + PreviewColumns.STYLE_THUMBNAIL + " BLOB, " + + PreviewColumns.WALLPAPER_PREVIEW + " BLOB, " + + PreviewColumns.WALLPAPER_THUMBNAIL + " BLOB, " + + PreviewColumns.LOCK_WALLPAPER_PREVIEW + " BLOB, " + + PreviewColumns.LOCK_WALLPAPER_THUMBNAIL + " BLOB, " + + PreviewColumns.BOOTANIMATION_THUMBNAIL + " BLOB, " + + "FOREIGN KEY (" + PreviewColumns.THEME_ID + ") REFERENCES " + + ThemesTable.TABLE_NAME + "(" + ThemesColumns._ID + ")" + + ")"; + + public static final String[] STATUS_BAR_PREVIEW_COLUMNS = { + PreviewColumns.STATUSBAR_BACKGROUND, + PreviewColumns.STATUSBAR_BLUETOOTH_ICON, + PreviewColumns.STATUSBAR_WIFI_ICON, + PreviewColumns.STATUSBAR_SIGNAL_ICON, + PreviewColumns.STATUSBAR_BATTERY_PORTRAIT, + PreviewColumns.STATUSBAR_BATTERY_LANDSCAPE, + PreviewColumns.STATUSBAR_BATTERY_CIRCLE, + PreviewColumns.STATUSBAR_WIFI_COMBO_MARGIN_END, + PreviewColumns.STATUSBAR_CLOCK_TEXT_COLOR + }; + public static final String[] NAVIGATION_BAR_PREVIEW_COLUMNS = { + PreviewColumns.NAVBAR_BACK_BUTTON, + PreviewColumns.NAVBAR_HOME_BUTTON, + PreviewColumns.NAVBAR_RECENT_BUTTON, + PreviewColumns.NAVBAR_BACKGROUND + }; + public static final String[] ICON_PREVIEW_COLUMNS = { + PreviewColumns.ICON_PREVIEW_1, + PreviewColumns.ICON_PREVIEW_2, + PreviewColumns.ICON_PREVIEW_3, + PreviewColumns.ICON_PREVIEW_4 + }; + + public static void insertDefaults(Context context) { + Intent intent = new Intent(context, PreviewGenerationService.class); + intent.setAction(PreviewGenerationService.ACTION_INSERT); + intent.putExtra(PreviewGenerationService.EXTRA_PKG_NAME, DEFAULT_PKG_NAME); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_SYSTEMUI, true); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_ICONS, true); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_STYLES, true); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_WALLPAPER, true); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_BOOTANIMATION, true); + context.startService(intent); + } + } + + private static boolean isSystemDefault(Context context) { // == is okay since we are checking if what is returned is the same constant string value - return ThemeConfig.HOLO_DEFAULT == ThemeUtils.getDefaultThemePackageName(context); + return ThemeConfig.SYSTEM_DEFAULT == ThemeUtils.getDefaultThemePackageName(context); } } diff --git a/src/org/cyanogenmod/themes/provider/ThemesProvider.java b/src/org/cyanogenmod/themes/provider/ThemesProvider.java index 24c79bf..b64eafe 100644 --- a/src/org/cyanogenmod/themes/provider/ThemesProvider.java +++ b/src/org/cyanogenmod/themes/provider/ThemesProvider.java @@ -35,10 +35,13 @@ import android.os.Handler; import android.preference.PreferenceManager; import android.provider.ThemesContract; import android.provider.ThemesContract.MixnMatchColumns; +import android.provider.ThemesContract.PreviewColumns; import android.provider.ThemesContract.ThemesColumns; +import android.text.TextUtils; import android.util.Log; import org.cyanogenmod.themes.provider.ThemesOpenHelper.MixnMatchTable; +import org.cyanogenmod.themes.provider.ThemesOpenHelper.PreviewsTable; import org.cyanogenmod.themes.provider.ThemesOpenHelper.ThemesTable; import java.util.ArrayList; @@ -48,7 +51,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import static android.content.res.ThemeConfig.HOLO_DEFAULT; +import static android.content.res.ThemeConfig.SYSTEM_DEFAULT; public class ThemesProvider extends ContentProvider { private static final String TAG = ThemesProvider.class.getSimpleName(); @@ -57,9 +60,14 @@ public class ThemesProvider extends ContentProvider { private static final int MIXNMATCH_KEY = 2; private static final int THEMES = 3; private static final int THEMES_ID = 4; + private static final int PREVIEWS = 5; + private static final int PREVIEWS_ID = 6; + private static final int APPLIED_PREVIEWS = 7; private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + public static final String KEY_PROCESS_PREVIEWS = "process_previews"; + private final Handler mHandler = new Handler(); private ThemesOpenHelper mDatabase; @@ -68,6 +76,9 @@ public class ThemesProvider extends ContentProvider { sUriMatcher.addURI(ThemesContract.AUTHORITY, "mixnmatch/*", MIXNMATCH_KEY); sUriMatcher.addURI(ThemesContract.AUTHORITY, "themes/", THEMES); sUriMatcher.addURI(ThemesContract.AUTHORITY, "themes/#", THEMES_ID); + sUriMatcher.addURI(ThemesContract.AUTHORITY, "previews/", PREVIEWS); + sUriMatcher.addURI(ThemesContract.AUTHORITY, "previews/#", PREVIEWS_ID); + sUriMatcher.addURI(ThemesContract.AUTHORITY, "applied_previews/", APPLIED_PREVIEWS); } public static void setActiveTheme(Context context, String pkgName) { @@ -90,17 +101,14 @@ public class ThemesProvider extends ContentProvider { case THEMES: SQLiteDatabase sqlDB = mDatabase.getWritableDatabase(); - // Determine the pkg name and delete preview images - String[] columns = new String[] {ThemesColumns.PKG_NAME}; + // Get the theme's _id and delete preview images + String[] columns = new String[] { ThemesColumns._ID }; Cursor c = sqlDB.query(ThemesTable.TABLE_NAME, columns, selection, selectionArgs, null, null, null); if (c == null) return 0; if (c.moveToFirst()) { - String pkgName = c.getString(0); - Intent intent = new Intent(getContext(), CopyImageService.class); - intent.setAction(CopyImageService.ACTION_DELETE); - intent.putExtra(CopyImageService.EXTRA_PKG_NAME, pkgName); - getContext().startService(intent); + sqlDB.delete(PreviewsTable.TABLE_NAME, + PreviewColumns.THEME_ID + "=" + c.getInt(0), null); } c.close(); @@ -125,6 +133,10 @@ public class ThemesProvider extends ContentProvider { return "vnd.android.cursor.dir/mixnmatch"; case MIXNMATCH_KEY: return "vnd.android.cursor.item/mixnmatch"; + case PREVIEWS: + return "vnd.android.cursor.dir/previews"; + case PREVIEWS_ID: + return "vnd.android.cursor.item/previews"; default: return null; } @@ -137,17 +149,41 @@ public class ThemesProvider extends ContentProvider { long id = 0; switch (uriType) { case THEMES: + boolean processPreviews = true; + if (values.containsKey(KEY_PROCESS_PREVIEWS)) { + processPreviews = values.getAsBoolean(KEY_PROCESS_PREVIEWS); + values.remove(KEY_PROCESS_PREVIEWS); + } id = sqlDB.insert(ThemesOpenHelper.ThemesTable.TABLE_NAME, null, values); - Intent intent = new Intent(getContext(), CopyImageService.class); - intent.setAction(CopyImageService.ACTION_INSERT); - intent.putExtra(CopyImageService.EXTRA_PKG_NAME, - values.getAsString(ThemesColumns.PKG_NAME)); - getContext().startService(intent); + if (processPreviews) { + Intent intent = new Intent(getContext(), PreviewGenerationService.class); + intent.setAction(PreviewGenerationService.ACTION_INSERT); + intent.putExtra(PreviewGenerationService.EXTRA_PKG_NAME, + values.getAsString(ThemesColumns.PKG_NAME)); + Boolean hasSystemUi = values.getAsBoolean(ThemesColumns.MODIFIES_STATUS_BAR); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_SYSTEMUI, + hasSystemUi != null && hasSystemUi); + Boolean hasIcons = values.getAsBoolean(ThemesColumns.MODIFIES_ICONS); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_ICONS, + hasIcons != null && hasIcons); + Boolean hasStyles = values.getAsBoolean(ThemesColumns.MODIFIES_OVERLAYS); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_STYLES, + hasStyles != null && hasStyles); + Boolean hasWallpaper = values.getAsBoolean(ThemesColumns.MODIFIES_LAUNCHER); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_WALLPAPER, + hasWallpaper != null && hasWallpaper); + Boolean hasBootAni = values.getAsBoolean(ThemesColumns.MODIFIES_BOOT_ANIM); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_BOOTANIMATION, + hasBootAni != null && hasBootAni); + getContext().startService(intent); + } break; case MIXNMATCH: throw new UnsupportedOperationException("Cannot insert rows into MixNMatch table"); + case PREVIEWS: + id = sqlDB.insert(ThemesOpenHelper.PreviewsTable.TABLE_NAME, null, values); + break; default: - throw new IllegalArgumentException("Unknown URI: " + uri); } getContext().getContentResolver().notifyChange(uri, null); return Uri.parse(MixnMatchColumns.CONTENT_URI + "/" + id); @@ -195,6 +231,15 @@ public class ThemesProvider extends ContentProvider { queryBuilder.setTables(THEMES_MIXNMATCH_INNER_JOIN); queryBuilder.appendWhere(MixnMatchColumns.COL_KEY + "=" + uri.getLastPathSegment()); break; + case PREVIEWS: + queryBuilder.setTables(THEMES_PREVIEWS_INNER_JOIN); + break; + case PREVIEWS_ID: + queryBuilder.setTables(THEMES_PREVIEWS_INNER_JOIN); + queryBuilder.appendWhere(PreviewColumns._ID + "=" + uri.getLastPathSegment()); + break; + case APPLIED_PREVIEWS: + return getAppliedPreviews(db); default: return null; } @@ -212,6 +257,10 @@ public class ThemesProvider extends ContentProvider { + " INNER JOIN " + ThemesTable.TABLE_NAME + " ON (" + MixnMatchColumns.COL_VALUE + " = " + ThemesColumns.PKG_NAME + ")"; + private static final String THEMES_PREVIEWS_INNER_JOIN = PreviewsTable.TABLE_NAME + + " INNER JOIN " + ThemesTable.TABLE_NAME + " ON (" + PreviewColumns.THEME_ID + + " = " + ThemesTable.TABLE_NAME + "." + ThemesColumns._ID + ")"; + @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { @@ -220,36 +269,120 @@ public class ThemesProvider extends ContentProvider { switch (sUriMatcher.match(uri)) { case THEMES: - rowsUpdated = sqlDB.update(ThemesTable.TABLE_NAME, values, selection, selectionArgs); - if (updateNotTriggeredByContentProvider(values)) { - Intent intent = new Intent(getContext(), CopyImageService.class); - intent.putExtra(CopyImageService.EXTRA_PKG_NAME, - values.getAsString(ThemesColumns.PKG_NAME)); - getContext().startService(intent); - getContext().getContentResolver().notifyChange(uri, null); - } - return rowsUpdated; case THEMES_ID: + String pkgName = values.getAsString(ThemesColumns.PKG_NAME); + final boolean updatePreviews = getShouldUpdatePreviews(sqlDB, pkgName); rowsUpdated = sqlDB.update(ThemesTable.TABLE_NAME, values, selection, selectionArgs); - if (updateNotTriggeredByContentProvider(values)) { - Intent intent = new Intent(getContext(), CopyImageService.class); - intent.putExtra(CopyImageService.EXTRA_PKG_NAME, + if (updateNotTriggeredByContentProvider(values) && updatePreviews) { + Intent intent = new Intent(getContext(), PreviewGenerationService.class); + intent.setAction(PreviewGenerationService.ACTION_UPDATE); + intent.putExtra(PreviewGenerationService.EXTRA_PKG_NAME, values.getAsString(ThemesColumns.PKG_NAME)); + Boolean hasSystemUi = values.getAsBoolean(ThemesColumns.MODIFIES_STATUS_BAR); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_SYSTEMUI, + hasSystemUi != null && hasSystemUi); + Boolean hasIcons = values.getAsBoolean(ThemesColumns.MODIFIES_ICONS); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_ICONS, + hasIcons != null && hasIcons); + Boolean hasStyles = values.getAsBoolean(ThemesColumns.MODIFIES_OVERLAYS); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_STYLES, + hasStyles != null && hasStyles); + Boolean hasWallpaper = values.getAsBoolean(ThemesColumns.MODIFIES_LAUNCHER); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_WALLPAPER, + hasWallpaper != null && hasWallpaper); + Boolean hasBootAni = values.getAsBoolean(ThemesColumns.MODIFIES_BOOT_ANIM); + intent.putExtra(PreviewGenerationService.EXTRA_HAS_BOOTANIMATION, + hasBootAni != null && hasBootAni); getContext().startService(intent); - getContext().getContentResolver().notifyChange(uri, null); } getContext().getContentResolver().notifyChange(uri, null); - return rowsUpdated; + break; case MIXNMATCH: rowsUpdated = sqlDB.update(MixnMatchTable.TABLE_NAME, values, selection, selectionArgs); getContext().getContentResolver().notifyChange(uri, null); + break; case MIXNMATCH_KEY: // Don't support right now. Any need? + break; + case PREVIEWS: + rowsUpdated = sqlDB.update(PreviewsTable.TABLE_NAME, values, selection, selectionArgs); + getContext().getContentResolver().notifyChange(uri, null); + break; } return rowsUpdated; } /** + * Queries the currently applied components and creates a SQLite statement consisting + * of a series of (SELECT ...) statements + * @param db Readable database + * @return + */ + private Cursor getAppliedPreviews(SQLiteDatabase db) { + Cursor c = db.query(MixnMatchTable.TABLE_NAME, null, null, null, null, null, null); + if (c != null) { + StringBuilder sb = new StringBuilder("SELECT * FROM "); + String delimeter = ""; + while (c.moveToNext()) { + String key = c.getString(0); + String pkgName = c.getString(1); + String component = key != null ? MixnMatchColumns.mixNMatchKeyToComponent(key) : + null; + if (component != null && pkgName != null) { + // We need to get the theme's id using its package name + String[] columns = { ThemesColumns._ID }; + Cursor current = db.query(ThemesTable.TABLE_NAME, columns, + ThemesColumns.PKG_NAME + "='" + pkgName + "'", null, null, null, null); + int id = -1; + if (current != null) { + if (current.moveToFirst()) id = current.getInt(0); + current.close(); + } + if (id >= 0) { + if (ThemesColumns.MODIFIES_STATUS_BAR.equals(component)) { + sb.append(delimeter).append("(SELECT "); + sb.append(TextUtils.join(",", + PreviewsTable.STATUS_BAR_PREVIEW_COLUMNS)); + sb.append(String.format(" FROM previews WHERE %s=%d)", + PreviewColumns.THEME_ID, id)); + delimeter = ","; + } else if (ThemesColumns.MODIFIES_ICONS.equals(component)) { + sb.append(delimeter).append("(SELECT "); + sb.append(TextUtils.join(",", PreviewsTable.ICON_PREVIEW_COLUMNS)); + sb.append(String.format(" FROM previews WHERE %s=%d)", + PreviewColumns.THEME_ID, id)); + delimeter = ","; + } else if (ThemesColumns.MODIFIES_LAUNCHER.equals(component)) { + sb.append(delimeter).append("(SELECT "); + sb.append(String.format("%s", PreviewColumns.WALLPAPER_PREVIEW)); + sb.append(String.format(" FROM previews WHERE %s=%d)", + PreviewColumns.THEME_ID, id)); + delimeter = ","; + } else if (ThemesColumns.MODIFIES_NAVIGATION_BAR.equals(component)) { + sb.append(delimeter).append("(SELECT "); + sb.append(TextUtils.join(",", + PreviewsTable.NAVIGATION_BAR_PREVIEW_COLUMNS)); + sb.append(String.format(" FROM previews WHERE %s=%d)", + PreviewColumns.THEME_ID, id)); + delimeter = ","; + } else if (ThemesColumns.MODIFIES_OVERLAYS.equals(component)) { + sb.append(delimeter).append("(SELECT "); + sb.append(PreviewColumns.STYLE_PREVIEW); + sb.append(String.format(" FROM previews WHERE %s=%d)", + PreviewColumns.THEME_ID, id)); + delimeter = ","; + } + } + } + } + c.close(); + sb.append(";"); + return db.rawQuery(sb.toString(), null); + } + return null; + } + + /** * When there is an insert or update to a theme, an async service will kick off to update * several of the preview image columns. Since this service also calls a 2nd update on the * content resolver, we need to break the loop so that we don't kick off the service again. @@ -261,6 +394,31 @@ public class ThemesProvider extends ContentProvider { .containsKey(ThemesColumns.STYLE_URI)); } + private boolean getShouldUpdatePreviews(SQLiteDatabase db, String pkgName) { + if (pkgName != null) { + long lastUpdateTime = 0; + String[] columns = {ThemesColumns.LAST_UPDATE_TIME}; + String selection = ThemesColumns.PKG_NAME + "=?"; + String[] selectionArgs = {pkgName}; + Cursor c = + db.query(ThemesTable.TABLE_NAME, columns, selection, selectionArgs, null, null, + null); + if (c != null) { + c.moveToFirst(); + lastUpdateTime = c.getInt(0); + c.close(); + } + + try { + PackageInfo pi = getContext().getPackageManager().getPackageInfo(pkgName, 0); + return lastUpdateTime < pi.lastUpdateTime; + } catch (NameNotFoundException e) { + Log.w(TAG, "Unable to retrieve PackageInfo for " + pkgName, e); + } + } + return false; + } + /** * This class has been modified from its original source. Original Source: ThemesProvider.java * See https://github.com/tmobile/themes-platform-vendor-tmobile-providers-ThemeManager @@ -303,7 +461,7 @@ public class ThemesProvider extends ContentProvider { List<PackageInfo> themePackages = new ArrayList<PackageInfo>(); Map<String, PackageInfo> pmThemes = new HashMap<String, PackageInfo>(); for (PackageInfo info : packages) { - if (info.isThemeApk || info.isLegacyThemeApk || info.isLegacyIconPackApk) { + if (info.isThemeApk || info.isLegacyIconPackApk) { themePackages.add(info); pmThemes.put(info.packageName, info); } @@ -326,10 +484,10 @@ public class ThemesProvider extends ContentProvider { String pkgName = current.getString(pkgNameIdx); boolean isDefault = current.getInt(isDefaultIdx) == 1; - // Ignore holo theme - if (pkgName.equals(HOLO_DEFAULT)) { - if (defaultThemePkg.equals(HOLO_DEFAULT) != isDefault) { - updateList.add(HOLO_DEFAULT); + // Ignore system theme + if (pkgName.equals(SYSTEM_DEFAULT)) { + if (defaultThemePkg.equals(SYSTEM_DEFAULT) != isDefault) { + updateList.add(SYSTEM_DEFAULT); } continue; } @@ -374,7 +532,7 @@ public class ThemesProvider extends ContentProvider { } ThemeManager mService = (ThemeManager) getContext().getSystemService( Context.THEME_SERVICE); - mService.requestThemeChange(HOLO_DEFAULT, moveToDefault); + mService.requestThemeChange(SYSTEM_DEFAULT, moveToDefault); // Update the database after we revert to default deleteThemes(deleteList); @@ -395,7 +553,7 @@ public class ThemesProvider extends ContentProvider { private void insertThemes(Collection<PackageInfo> themesToInsert) { for (PackageInfo themeInfo : themesToInsert) { try { - ThemePackageHelper.insertPackage(getContext(), themeInfo.packageName); + ThemePackageHelper.insertPackage(getContext(), themeInfo.packageName, true); } catch (NameNotFoundException e) { Log.e(TAG, "Unable to insert theme " + themeInfo.packageName, e); } @@ -412,5 +570,4 @@ public class ThemesProvider extends ContentProvider { } } } - } diff --git a/src/org/cyanogenmod/themes/provider/util/BitmapUtils.java b/src/org/cyanogenmod/themes/provider/util/BitmapUtils.java new file mode 100644 index 0000000..002970b --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/util/BitmapUtils.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.cyanogenmod.themes.provider.util; + +import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Rect; +import android.net.Uri; +import android.util.Log; +import android.util.TypedValue; + +import java.io.Closeable; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public class BitmapUtils { + private static final String TAG = "BitmapUtils"; + + /** + * Returns the bitmap from the given uri loaded using the given options. + * Returns null on failure. + */ + public static Bitmap loadBitmap(Context context, InputStream is, BitmapFactory.Options o) { + try { + return BitmapFactory.decodeStream(is, null, o); + } finally { + closeSilently(is); + } + } + + public static Bitmap decodeFile(String path, int reqWidth, int reqHeight) { + FileInputStream fis = null; + try { + fis = new FileInputStream(path); + return decodeStream(fis, reqWidth, reqHeight); + } catch (FileNotFoundException e) { + Log.e(TAG, "Unable to open resource in path" + path, e); + } finally { + closeSilently(fis); + } + return null; + } + + public static Bitmap decodeStream(InputStream is, int reqWidth, int reqHeight) { + BitmapFactory.Options opts = new BitmapFactory.Options(); + + // Determine insample size + opts.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, opts); + opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight); + + // Decode the bitmap, regionally if necessary + Bitmap bitmap = null; + opts.inJustDecodeBounds = false; + Rect rect = getCropRectIfNecessary(opts, reqWidth, reqHeight); + try { + if (rect != null) { + BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); + bitmap = decoder.decodeRegion(rect, opts); + } else { + bitmap = BitmapFactory.decodeStream(is, null, opts); + } + } catch (IOException e) { + Log.e(TAG, "Unable to decode bitmap from stream", e); + } + return bitmap; + } + + public static Bitmap decodeResource(Resources res, int resId, int reqWidth, int reqHeight) { + BitmapFactory.Options opts = new BitmapFactory.Options(); + + // Determine insample size + opts.inJustDecodeBounds = true; + BitmapFactory.decodeResource(res, resId, opts); + opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight); + + // Decode the bitmap, regionally if necessary + Bitmap bitmap = null; + opts.inJustDecodeBounds = false; + Rect rect = getCropRectIfNecessary(opts, reqWidth, reqHeight); + + InputStream stream = null; + try { + if (rect != null) { + stream = res.openRawResource(resId, new TypedValue()); + if (stream == null) return null; + BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(stream, false); + bitmap = decoder.decodeRegion(rect, opts); + } else { + bitmap = BitmapFactory.decodeResource(res, resId, opts); + } + } catch (IOException e) { + Log.e(TAG, "Unable to open resource " + resId, e); + } finally { + closeSilently(stream); + } + return bitmap; + } + + + public static Bitmap decodeByteArray(byte[] buffer, int reqWidth, int reqHeight) { + BitmapFactory.Options opts = new BitmapFactory.Options(); + + // Determine insample size + opts.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(buffer, 0, buffer.length, opts); + opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight); + + // Decode the bitmap, regionally if necessary + Bitmap bitmap = null; + opts.inJustDecodeBounds = false; + Rect rect = getCropRectIfNecessary(opts, reqWidth, reqHeight); + try { + if (rect != null) { + BitmapRegionDecoder decoder = + BitmapRegionDecoder.newInstance(buffer, 0, buffer.length, false); + bitmap = decoder.decodeRegion(rect, opts); + } else { + bitmap = BitmapFactory.decodeByteArray(buffer, 0, buffer.length, opts); + } + } catch (IOException e) { + Log.e(TAG, "Unable to decode bitmap from stream", e); + } + return bitmap; + } + + public static Bitmap getBitmapFromAsset(Context ctx, String path, int reqWidth, int reqHeight) { + if (ctx == null || path == null) + return null; + + Bitmap bitmap = null; + try { + AssetManager assets = ctx.getAssets(); + InputStream is = assets.open(path); + bitmap = decodeStream(is, reqWidth, reqHeight); + } catch (IOException e) { + e.printStackTrace(); + } + return bitmap; + } + + + /** + * For excessively large images with an awkward ratio we + * will want to crop them + * @return + */ + public static Rect getCropRectIfNecessary( + BitmapFactory.Options options,int reqWidth, int reqHeight) { + final int width = options.outWidth; + final int height = options.outHeight; + Rect rect = new Rect(0, 0, width, height); + // Determine downsampled size + int targetWidth = reqWidth * options.inSampleSize; + int targetHeight = reqHeight * options.inSampleSize; + + if (targetHeight < height) { + rect.top = (height - targetHeight) / 2; + rect.bottom = rect.top + targetHeight; + } + if (targetWidth < width) { + rect.left = (width - targetWidth) / 2; + rect.right = rect.left + targetWidth; + } + return rect; + } + + public static int calculateInSampleSize( + BitmapFactory.Options options, int reqWidth, int reqHeight) { + return calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight); + } + + // Modified from original source: + // http://developer.android.com/training/displaying-bitmaps/load-bitmap.html + public static int calculateInSampleSize( + int decodeWidth, int decodeHeight, int reqWidth, int reqHeight) { + // Raw height and width of image + int inSampleSize = 1; + + if (decodeHeight > reqHeight || decodeWidth > reqWidth) { + final int halfHeight = decodeHeight / 2; + final int halfWidth = decodeWidth / 2; + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while ((halfHeight / inSampleSize) > reqHeight && + (halfWidth / inSampleSize) > reqWidth) { + inSampleSize *= 2; + } + } + + return inSampleSize; + } + + public static void closeSilently(Closeable c) { + if (c == null) return; + try { + c.close(); + } catch (IOException t) { + Log.w(TAG, "close fail ", t); + } + } +} diff --git a/src/org/cyanogenmod/themes/provider/util/BootAnimationPreviewGenerator.java b/src/org/cyanogenmod/themes/provider/util/BootAnimationPreviewGenerator.java new file mode 100644 index 0000000..61771a7 --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/util/BootAnimationPreviewGenerator.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.cyanogenmod.themes.provider.util; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.ThemeConfig; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.view.View; +import android.widget.ImageView; +import org.cyanogenmod.themes.provider.R; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class BootAnimationPreviewGenerator { + public static final String SYSTEM_BOOT_ANI_PATH = "/system/media/bootanimation.zip"; + public static final String THEME_BOOT_ANI_PATH = "bootanimation/bootanimation.zip"; + + private Context mContext; + + public BootAnimationPreviewGenerator(Context context) { + mContext = context; + } + + public Bitmap generateBootAnimationPreview(String pkgName) + throws IOException, PackageManager.NameNotFoundException { + ZipInputStream zis; + String previewName; + if (ThemeConfig.SYSTEM_DEFAULT.equals(pkgName)) { + previewName = getPreviewFrameEntryName(new FileInputStream(SYSTEM_BOOT_ANI_PATH)); + zis = new ZipInputStream(new FileInputStream(SYSTEM_BOOT_ANI_PATH)); + } else { + final Context themeCtx = mContext.createPackageContext(pkgName, 0); + previewName = getPreviewFrameEntryName(themeCtx.getAssets().open(THEME_BOOT_ANI_PATH)); + zis = new ZipInputStream(themeCtx.getAssets().open(THEME_BOOT_ANI_PATH)); + } + ZipEntry ze; + Bitmap bmp = null; + while ((ze = zis.getNextEntry()) != null) { + final String entryName = ze.getName(); + if (entryName.equals(previewName)) { + bmp = BitmapFactory.decodeStream(zis); + break; + } + } + zis.close(); + + View v = View.inflate(mContext, R.layout.bootanimation_preview, null); + ImageView iv = (ImageView) v.findViewById(R.id.preview); + iv.setImageBitmap(bmp); + return LayoutRenderUtils.renderViewToBitmap(v); + } + + private String getPreviewFrameEntryName(InputStream is) throws IOException { + ZipInputStream zis = (is instanceof ZipInputStream) ? (ZipInputStream) is + : new ZipInputStream(new BufferedInputStream(is)); + ZipEntry ze; + String previewName = null; + long max = 0; + while ((ze = zis.getNextEntry()) != null) { + final String entryName = ze.getName(); + if (entryName.contains("/") + && (entryName.endsWith(".png") || entryName.endsWith(".jpg"))) { + if(ze.getSize() > max) { + previewName = entryName; + max = ze.getSize(); + } + } + } + zis.close(); + + return previewName; + } +} diff --git a/src/org/cyanogenmod/themes/provider/util/IconPreviewGenerator.java b/src/org/cyanogenmod/themes/provider/util/IconPreviewGenerator.java new file mode 100644 index 0000000..b1857d8 --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/util/IconPreviewGenerator.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.cyanogenmod.themes.provider.util; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; + +public class IconPreviewGenerator { + private static final ComponentName COMPONENT_DIALER = + new ComponentName("com.android.dialer", "com.android.dialer.DialtactsActivity"); + private static final ComponentName COMPONENT_MESSAGING = + new ComponentName("com.android.mms", "com.android.mms.ui.ConversationList"); + private static final ComponentName COMPONENT_CAMERANEXT = + new ComponentName("com.cyngn.cameranext", "com.android.camera.CameraLauncher"); + private static final ComponentName COMPONENT_CAMERA = + new ComponentName("com.android.camera2", "com.android.camera.CameraLauncher"); + private static final ComponentName COMPONENT_BROWSER = + new ComponentName("com.android.browser", "com.android.browser.BrowserActivity"); + private static final ComponentName COMPONENT_SETTINGS = + new ComponentName("com.android.settings", "com.android.settings.Settings"); + private static final ComponentName COMPONENT_CALENDAR = + new ComponentName("com.android.calendar", "com.android.calendar.AllInOneActivity"); + private static final ComponentName COMPONENT_GALERY = + new ComponentName("com.android.gallery3d", "com.android.gallery3d.app.GalleryActivity"); + private static final String CAMERA_NEXT_PACKAGE = "com.cyngn.cameranext"; + + private ComponentName[] mIconComponents; + + private Context mContext; + + public IconPreviewGenerator(Context context) { + mContext = context; + } + + public IconItems generateIconItems(String pkgName) { + IconItems items = new IconItems(); + IconPreviewHelper helper = new IconPreviewHelper(mContext, pkgName); + + final ComponentName[] components = getIconComponents(mContext); + BitmapDrawable drawable; + drawable = (BitmapDrawable) helper.getIcon(components[0]); + items.icon1 = drawable.getBitmap(); + drawable = (BitmapDrawable) helper.getIcon(components[1]); + items.icon2 = drawable.getBitmap(); + drawable = (BitmapDrawable) helper.getIcon(components[2]); + items.icon3 = drawable.getBitmap(); + return items; + } + + private ComponentName[] getIconComponents(Context context) { + if (mIconComponents == null || mIconComponents.length == 0) { + mIconComponents = new ComponentName[]{COMPONENT_DIALER, COMPONENT_MESSAGING, + COMPONENT_CAMERA, COMPONENT_BROWSER}; + + PackageManager pm = context.getPackageManager(); + + // if device does not have telephony replace dialer and mms + if (!pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { + mIconComponents[0] = COMPONENT_CALENDAR; + mIconComponents[1] = COMPONENT_GALERY; + } + + if (!pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)) { + mIconComponents[2] = COMPONENT_SETTINGS; + } else { + // decide on which camera icon to use + try { + if (pm.getPackageInfo(CAMERA_NEXT_PACKAGE, 0) != null) { + mIconComponents[2] = COMPONENT_CAMERANEXT; + } + } catch (PackageManager.NameNotFoundException e) { + // default to COMPONENT_CAMERA + } + } + + } + + return mIconComponents; + } + public class IconItems { + public Bitmap icon1; + public Bitmap icon2; + public Bitmap icon3; + } +} diff --git a/src/org/cyanogenmod/themes/provider/util/IconPreviewHelper.java b/src/org/cyanogenmod/themes/provider/util/IconPreviewHelper.java new file mode 100644 index 0000000..484136c --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/util/IconPreviewHelper.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.cyanogenmod.themes.provider.util; + +import android.app.ActivityManager; +import android.app.ComposedIconInfo; +import android.app.IconPackHelper; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageItemInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.SparseArray; + +/** + * This class handles all the logic to build a preview icon + * If the system currently has a theme applied we do NOT + * want this code to be impacted by it. So code in this + * class creates special "no theme attached" resource objects + * to retrieve objects from. + */ +public class IconPreviewHelper { + private static final String TAG = IconPreviewHelper.class.getSimpleName(); + private final static float ICON_SCALE_FACTOR = 1.3f; //Arbitrary. Looks good + + private Context mContext; + private DisplayMetrics mDisplayMetrics; + private Configuration mConfiguration; + private int mIconDpi = 0; + private String mThemePkgName; + private IconPackHelper mIconPackHelper; + private int mIconSize; + + /** + * @param themePkgName - The package name of the theme we wish to preview + */ + public IconPreviewHelper(Context context, String themePkgName) { + mContext = context; + mDisplayMetrics = context.getResources().getDisplayMetrics(); + mConfiguration = context.getResources().getConfiguration(); + ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + mIconDpi = (int) (am.getLauncherLargeIconDensity() * ICON_SCALE_FACTOR); + mThemePkgName = themePkgName; + mIconPackHelper = new IconPackHelper(mContext); + try { + mIconPackHelper.loadIconPack(mThemePkgName); + } catch (NameNotFoundException e) {} + mIconSize = (int) (am.getLauncherLargeIconSize() * ICON_SCALE_FACTOR); + } + + /** + * Returns the actual label name for a given component + * If the activity does not have a label it will return app's label + * If neither has a label returns empty string + */ + public String getLabel(ComponentName component) { + String label = ""; + try { + PackageManager pm = mContext.getPackageManager(); + ApplicationInfo appInfo = pm.getApplicationInfo(component.getPackageName(), 0); + ActivityInfo activityInfo = pm.getActivityInfo(component, 0); + + AssetManager assets = new AssetManager(); + assets.addAssetPath(appInfo.publicSourceDir); + Resources res = new Resources(assets, mDisplayMetrics, mConfiguration); + + if (activityInfo.labelRes != 0) { + label = res.getString(activityInfo.labelRes); + } else if (appInfo.labelRes != 0) { + label = res.getString(appInfo.labelRes); + } + } catch(NameNotFoundException exception) { + Log.e(TAG, "unable to find pkg for " + component.toString()); + } + return label; + } + + /** + * Returns the icon for the given component regardless of the system's + * currently applied theme. If the preview theme does not support the icon, then + * return the system default icon. + */ + public Drawable getIcon(ComponentName component) { + String packageName = component.getPackageName(); + String activityName = component.getClassName(); + Drawable icon = getThemedIcon(packageName, activityName); + if (icon == null) { + icon = getDefaultIcon(packageName, activityName); + } + if (icon != null) { + icon.setBounds(0, 0, mIconSize, mIconSize); + } + return icon; + } + + private Drawable getThemedIcon(String pkgName, String activityName) { + Drawable drawable = null; + ActivityInfo info = new ActivityInfo(); + info.packageName = pkgName; + info.name = activityName; + drawable = mIconPackHelper.getDrawableForActivityWithDensity(info, mIconDpi); + + return drawable; + } + + /** + * Returns the default icon. This can be the normal icon associated with the app or a composed + * icon if the icon pack supports background, mask, and/or foreground. + * @param pkgName + * @param activityName + * @return + */ + private Drawable getDefaultIcon(String pkgName, String activityName) { + Drawable drawable = null; + ComponentName component = new ComponentName(pkgName, activityName); + PackageManager pm = mContext.getPackageManager(); + Resources res = null; + try { + ActivityInfo info = pm.getActivityInfo(component, 0); + ApplicationInfo appInfo = pm.getApplicationInfo(pkgName, 0); + + res = pm.getThemedResourcesForApplication(pkgName, mThemePkgName); + + final int iconId = info.icon != 0 ? info.icon : appInfo.icon; + info.themedIcon = 0; + setupComposedIcon(res, info, iconId); + drawable = getFullResIcon(res, iconId); + } catch (NameNotFoundException e2) { + Log.w(TAG, "Unable to get the icon for " + pkgName + " using default"); + } + drawable = (drawable != null) ? + getComposedIcon(res, drawable) : getFullResDefaultActivityIcon(); + return drawable; + } + + private Drawable getComposedIcon(Resources res, Drawable baseIcon) { + ComposedIconInfo iconInfo = mIconPackHelper.getComposedIconInfo(); + if (res != null && iconInfo != null && (iconInfo.iconBacks != null || + iconInfo.iconMask != 0 || iconInfo.iconUpon != 0)) { + return IconPackHelper.IconCustomizer.getComposedIconDrawable(baseIcon, res, iconInfo); + } + return baseIcon; + } + + private void setupComposedIcon(Resources res, ActivityInfo info, int iconId) { + ComposedIconInfo iconInfo = mIconPackHelper.getComposedIconInfo(); + + res.setComposedIconInfo(iconInfo); + + SparseArray<PackageItemInfo> icons = new SparseArray<PackageItemInfo>(1); + info.themedIcon = 0; + icons.put(iconId, info); + res.setIconResources(icons); + } + + private Drawable getFullResIcon(Resources resources, int iconId) { + Drawable d; + try { + d = resources.getDrawableForDensity(iconId, mIconDpi, null, false); + } catch (Resources.NotFoundException e) { + d = null; + } + return (d != null) ? d : getFullResDefaultActivityIcon(); + } + + private Drawable getFullResDefaultActivityIcon() { + return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon); + } +} diff --git a/src/org/cyanogenmod/themes/provider/util/LayoutRenderUtils.java b/src/org/cyanogenmod/themes/provider/util/LayoutRenderUtils.java new file mode 100644 index 0000000..c174d8e --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/util/LayoutRenderUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.cyanogenmod.themes.provider.util; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.view.View; +import android.widget.FrameLayout; + +public class LayoutRenderUtils { + + public static Bitmap renderViewToBitmap(View view) { + // Provide it with a layout params. It should necessarily be wrapping the + // content as we not really going to have a parent for it. + view.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT)); + + // Pre-measure the view so that height and width don't remain null. + view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); + + // Assign a size and position to the view and all of its descendants + view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); + + // Create the bitmap + Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), + view.getMeasuredHeight(), + Bitmap.Config.ARGB_8888); + // Create a canvas with the specified bitmap to draw into + Canvas c = new Canvas(bitmap); + + // Render this view to the given Canvas + view.draw(c); + return bitmap; + } +} diff --git a/src/org/cyanogenmod/themes/provider/util/StylePreviewGenerator.java b/src/org/cyanogenmod/themes/provider/util/StylePreviewGenerator.java new file mode 100644 index 0000000..eebcb36 --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/util/StylePreviewGenerator.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.cyanogenmod.themes.provider.util; + +import android.content.Context; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.Bitmap; +import android.os.UserHandle; +import android.view.View; + +import org.cyanogenmod.themes.provider.R; + +public class StylePreviewGenerator { + private Context mContext; + + public StylePreviewGenerator(Context context) { + mContext = context; + } + + public StyleItems generateStylePreviews(String pkgName) throws NameNotFoundException { + StyleItems items = new StyleItems(); + Context themeContext = mContext.createPackageContextAsUser("android", pkgName, 0, + UserHandle.CURRENT); + View v = View.inflate(themeContext, R.layout.controls_thumbnail, null); + items.thumbnail = LayoutRenderUtils.renderViewToBitmap(v); + + v = View.inflate(themeContext, R.layout.controls_preview, null); + items.preview = LayoutRenderUtils.renderViewToBitmap(v); + return items; + } + + public class StyleItems { + public Bitmap thumbnail; + public Bitmap preview; + } +} diff --git a/src/org/cyanogenmod/themes/provider/util/SystemUiPreviewGenerator.java b/src/org/cyanogenmod/themes/provider/util/SystemUiPreviewGenerator.java new file mode 100644 index 0000000..d53ad2f --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/util/SystemUiPreviewGenerator.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.cyanogenmod.themes.provider.util; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.VectorDrawable; +import android.widget.FrameLayout; +import org.cyanogenmod.themes.provider.view.BatteryMeterView; + +import org.cyanogenmod.themes.provider.R; + +public class SystemUiPreviewGenerator { + public static final String SYSTEMUI_PACKAGE = "com.android.systemui"; + private static final String WIFI_DRAWABLE = "stat_sys_wifi_signal_3_fully"; + private static final String SIGNAL_DRAWABLE = "stat_sys_signal_3_fully"; + private static final String BLUETOOTH_DRAWABLE = "stat_sys_data_bluetooth_connected"; + private static final String STATUS_BAR_BATTERY_WIDTH = "status_bar_battery_width"; + private static final String STATUS_BAR_BATTERY_HEIGHT = "status_bar_battery_height"; + private static final String STATUS_BAR_CLOCK_COLOR = "status_bar_clock_color"; + private static final String SYSTEM_BAR_BACKGROUND = "system_bar_background"; + private static final String STATUS_BAR_BACKGROUND_OPAQUE = "status_bar_background_opaque"; + private static final String NAVIGATION_BAR_BACKGROUND_OPAQUE + = "navigation_bar_background_opaque"; + private static final String IC_SYSBAR_BACK = "ic_sysbar_back"; + private static final String IC_SYSBAR_HOME = "ic_sysbar_home"; + private static final String IC_SYSBAR_RECENT = "ic_sysbar_recent"; + private static final String STATUS_BAR_ICON_SIZE = "status_bar_icon_size"; + + private Context mContext; + + public SystemUiPreviewGenerator(Context context) { + mContext = context; + } + + /** + * Loads the necessary resources for the given theme. + * @param pkgName Package name for the theme to use + * @return + * @throws NameNotFoundException + */ + public SystemUiItems generateSystemUiItems(String pkgName) throws NameNotFoundException { + PackageManager pm = mContext.getPackageManager(); + Resources res = pm.getThemedResourcesForApplication(SYSTEMUI_PACKAGE, pkgName); + int iconSize = res.getDimensionPixelSize(res.getIdentifier(STATUS_BAR_ICON_SIZE, "dimen", + SYSTEMUI_PACKAGE)); + + SystemUiItems items = new SystemUiItems(); + Drawable d = res.getDrawable(res.getIdentifier(BLUETOOTH_DRAWABLE, "drawable", + SYSTEMUI_PACKAGE)); + items.bluetoothIcon = renderDrawableToBitmap(d, iconSize); + + d = res.getDrawable(res.getIdentifier(WIFI_DRAWABLE, "drawable", SYSTEMUI_PACKAGE)); + items.wifiIcon = renderDrawableToBitmap(d, iconSize); + + d = res.getDrawable(res.getIdentifier(SIGNAL_DRAWABLE, "drawable", SYSTEMUI_PACKAGE)); + items.signalIcon = renderDrawableToBitmap(d, iconSize); + + items.clockColor = res.getColor(res.getIdentifier(STATUS_BAR_CLOCK_COLOR, "color", + SYSTEMUI_PACKAGE)); + // wifi margin no longer used in systemui + items.wifiMarginEnd = 0; + generateBatteryPreviews(res, items); + generateBackgroundPreviews(res, items); + + items.navbarBack = BitmapFactory.decodeResource(res, res.getIdentifier(IC_SYSBAR_BACK, + "drawable", SYSTEMUI_PACKAGE)); + items.navbarHome = BitmapFactory.decodeResource(res, res.getIdentifier(IC_SYSBAR_HOME, + "drawable", SYSTEMUI_PACKAGE)); + items.navbarRecent = BitmapFactory.decodeResource(res, res.getIdentifier(IC_SYSBAR_RECENT, + "drawable", SYSTEMUI_PACKAGE)); + + return items; + } + + private Bitmap renderDrawableToBitmap(Drawable d, int iconSize) { + if (d instanceof VectorDrawable) { + return renderVectorDrawableToBitmap((VectorDrawable) d, iconSize); + } else if (d instanceof BitmapDrawable) { + return ((BitmapDrawable) d).getBitmap(); + } + + return null; + } + + private Bitmap renderVectorDrawableToBitmap(VectorDrawable d, int iconSize) { + Bitmap bmp = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bmp); + d.setBounds(0, 0, iconSize, iconSize); + d.draw(canvas); + + return bmp; + } + + /** + * Generates the various battery types using the provided resources. + * @param res + * @param items + */ + private void generateBatteryPreviews(Resources res, SystemUiItems items) { + FrameLayout view = new FrameLayout(mContext); + BatteryMeterView battery = new BatteryMeterView(mContext, res); + view.addView(battery, + new FrameLayout.LayoutParams(mContext.getResources().getDimensionPixelSize( + R.dimen.status_bar_battery_width), + mContext.getResources().getDimensionPixelSize( + R.dimen.status_bar_battery_height))); + battery.setMode(BatteryMeterView.BatteryMeterMode.BATTERY_METER_ICON_PORTRAIT); + items.batteryPortrait = LayoutRenderUtils.renderViewToBitmap(view); + battery.setMode(BatteryMeterView.BatteryMeterMode.BATTERY_METER_ICON_LANDSCAPE); + items.batteryLandscape = LayoutRenderUtils.renderViewToBitmap(view); + battery.setMode(BatteryMeterView.BatteryMeterMode.BATTERY_METER_CIRCLE); + items.batteryCircle = LayoutRenderUtils.renderViewToBitmap(view); + } + + private void generateBackgroundPreviews(Resources res, SystemUiItems items) { + int width = Math.max(res.getDisplayMetrics().widthPixels, + res.getDisplayMetrics().heightPixels); + int height = mContext.getResources().getDimensionPixelSize( + R.dimen.status_bar_battery_height); + Drawable defaultBackground = res.getDrawable( + res.getIdentifier(SYSTEM_BAR_BACKGROUND, "drawable", SYSTEMUI_PACKAGE)); + defaultBackground.setBounds(0, 0, width, height); + + Drawable statusbarBackground = null; + int resId = res.getIdentifier(STATUS_BAR_BACKGROUND_OPAQUE, "color", SYSTEMUI_PACKAGE); + if (resId != 0) { + statusbarBackground = new ColorDrawable(res.getColor(resId)); + statusbarBackground.setBounds(0, 0, width, height); + } + items.statusbarBackground = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(items.statusbarBackground); + if (statusbarBackground != null) { + statusbarBackground.draw(canvas); + } else { + defaultBackground.draw(canvas); + } + + Drawable navbarBackground = null; + resId = res.getIdentifier(NAVIGATION_BAR_BACKGROUND_OPAQUE, "color", SYSTEMUI_PACKAGE); + if (resId != 0) { + navbarBackground = new ColorDrawable(res.getColor(resId)); + navbarBackground.setBounds(0, 0, width, height); + } + items.navbarBackground = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + canvas.setBitmap(items.navbarBackground); + if (navbarBackground != null) { + navbarBackground.draw(canvas); + } else { + defaultBackground.draw(canvas); + } + } + + public class SystemUiItems { + /** + * Status bar items + */ + public Bitmap bluetoothIcon; + public Bitmap wifiIcon; + public Bitmap signalIcon; + public Bitmap batteryPortrait; + public Bitmap batteryLandscape; + public Bitmap batteryCircle; + public Bitmap statusbarBackground; + public int clockColor; + public int wifiMarginEnd; + + /** + * Navigation bar items + */ + public Bitmap navbarBackground; + public Bitmap navbarBack; + public Bitmap navbarHome; + public Bitmap navbarRecent; + } +} diff --git a/src/org/cyanogenmod/themes/provider/util/WallpaperPreviewGenerator.java b/src/org/cyanogenmod/themes/provider/util/WallpaperPreviewGenerator.java new file mode 100644 index 0000000..01b888a --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/util/WallpaperPreviewGenerator.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.cyanogenmod.themes.provider.util; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ThemeUtils; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.content.res.ThemeConfig; +import android.graphics.Bitmap; + +import org.cyanogenmod.themes.provider.R; + +import java.io.File; +import java.io.IOException; + +public class WallpaperPreviewGenerator { + private static final String WALLPAPER_ASSET_PATH = "wallpapers"; + private static final String LOCKSCREEN_ASSET_PATH = "lockscreen"; + private Context mContext; + private int mPreviewSize; + private int mThumbnailSize; + + public WallpaperPreviewGenerator(Context context) { + mContext = context; + final Resources res = context.getResources(); + mPreviewSize = res.getDimensionPixelSize(R.dimen.wallpaper_preview_size); + mThumbnailSize = res.getDimensionPixelSize(R.dimen.wallpaper_thumbnail_size); + } + + public WallpaperItems generateWallpaperPreviews(PackageInfo themeInfo) + throws NameNotFoundException, IOException { + WallpaperItems items = new WallpaperItems(); + if (themeInfo == null) { + Resources res = mContext.getPackageManager().getThemedResourcesForApplication("android", + ThemeConfig.SYSTEM_DEFAULT); + items.wpPreview = items.lsPreview = BitmapUtils.decodeResource(res, + com.android.internal.R.drawable.default_wallpaper, mPreviewSize, mPreviewSize); + } else { + final Context themeContext = mContext.createPackageContext(themeInfo.packageName, 0); + final AssetManager assets = themeContext.getAssets(); + String path = ThemeUtils.getWallpaperPath(assets); + if (path != null) { + items.wpPreview = BitmapUtils.getBitmapFromAsset(themeContext, path, + mPreviewSize, mPreviewSize); + } + path = ThemeUtils.getLockscreenWallpaperPath(assets); + if (path != null) { + items.lsPreview = BitmapUtils.getBitmapFromAsset(themeContext, path, + mPreviewSize, mPreviewSize); + } + } + if (items.wpPreview != null) { + items.wpThumbnail = Bitmap.createScaledBitmap(items.wpPreview, mThumbnailSize, + mThumbnailSize, true); + } + if (items.lsPreview != null) { + items.lsThumbnail = Bitmap.createScaledBitmap(items.lsPreview, mThumbnailSize, + mThumbnailSize, true); + } + return items; + } + + public class WallpaperItems { + // Wallpaper items + public Bitmap wpThumbnail; + public Bitmap wpPreview; + + // Lockscreen wallpaper items + public Bitmap lsThumbnail; + public Bitmap lsPreview; + } +} diff --git a/src/org/cyanogenmod/themes/provider/view/BatteryMeterView.java b/src/org/cyanogenmod/themes/provider/view/BatteryMeterView.java new file mode 100755 index 0000000..4b772fa --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/view/BatteryMeterView.java @@ -0,0 +1,733 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.cyanogenmod.themes.provider.view; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.os.Handler; +import android.provider.Settings; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.View; + +import static org.cyanogenmod.themes.provider.util.SystemUiPreviewGenerator.SYSTEMUI_PACKAGE; + +public class BatteryMeterView extends View { + private static final String TAG = BatteryMeterView.class.getSimpleName(); + private static final boolean ENABLE_PERCENT = true; + + private static final String BATTERYMETER_COLOR_LEVELS = "batterymeter_color_levels"; + private static final String BATTERYMETER_COLOR_VALUES = "batterymeter_color_values"; + private static final String BATTERYMETER_FRAME_COLOR = "batterymeter_frame_color"; + private static final String BATTERYMETER_BOLT_COLOR = "batterymeter_bolt_color"; + private static final String BATTERYMETER_BOLT_POINTS = "batterymeter_bolt_points"; + private static final String STATUS_BAR_CLOCK_COLOR = "status_bar_clock_color"; + + public static enum BatteryMeterMode { + BATTERY_METER_GONE, + BATTERY_METER_ICON_PORTRAIT, + BATTERY_METER_ICON_LANDSCAPE, + BATTERY_METER_CIRCLE, + BATTERY_METER_TEXT + } + + private final Runnable mInvalidate = new Runnable() { + public void run() { + invalidateIfVisible(); + } + }; + + private final Handler mHandler; + + protected BatteryMeterMode mMeterMode = null; + protected boolean mShowPercent = false; + protected boolean mAttached; + + private int mHeight; + private int mWidth; + + final int[] mColors; + final int[] mBoltPoints; + final int mFrameColor; + final int mBoltColor; + final int mTextColor; + + private BatteryMeterDrawable mBatteryMeterDrawable; + private final Object mLock = new Object(); + + private Resources mResources; + + public BatteryMeterView(Context context, Resources res) { + this(context, null, 0, res); + } + + public BatteryMeterView(Context context, AttributeSet attrs, Resources res) { + this(context, attrs, 0, res); + } + + public BatteryMeterView(Context context, AttributeSet attrs, int defStyle, Resources res) { + super(context, attrs, defStyle); + mHandler = new Handler(); + + TypedArray levels = res.obtainTypedArray(res.getIdentifier(BATTERYMETER_COLOR_LEVELS, + "array", SYSTEMUI_PACKAGE)); + TypedArray colors = res.obtainTypedArray(res.getIdentifier(BATTERYMETER_COLOR_VALUES, + "array", SYSTEMUI_PACKAGE)); + TypedArray boltPoints = res.obtainTypedArray(res.getIdentifier(BATTERYMETER_BOLT_POINTS, + "array", SYSTEMUI_PACKAGE)); + + mResources = res; + + int N = levels.length(); + mColors = new int[2*N]; + for (int i=0; i<N; i++) { + mColors[2*i] = levels.getInt(i, 0); + mColors[2*i+1] = colors.getColor(i, 0); + } + levels.recycle(); + colors.recycle(); + + N = boltPoints.length(); + mBoltPoints = new int[N]; + for (int i = 0; i < N; i++) { + mBoltPoints[i] = boltPoints.getInt(i, 0); + } + boltPoints.recycle(); + + mShowPercent = ENABLE_PERCENT && 0 != Settings.System.getInt( + context.getContentResolver(), "status_bar_show_battery_percent", 0); + + mMeterMode = BatteryMeterMode.BATTERY_METER_CIRCLE; + mBatteryMeterDrawable = createBatteryMeterDrawable(mMeterMode); + + mFrameColor = res.getColor(res.getIdentifier(BATTERYMETER_FRAME_COLOR, "color", + SYSTEMUI_PACKAGE)); + mBoltColor = res.getColor(res.getIdentifier(BATTERYMETER_BOLT_COLOR, "color", + SYSTEMUI_PACKAGE)); + mTextColor = res.getColor(res.getIdentifier(STATUS_BAR_CLOCK_COLOR, "color", + SYSTEMUI_PACKAGE)); + + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + + protected BatteryMeterDrawable createBatteryMeterDrawable(BatteryMeterMode mode) { + Resources res = mResources; + switch (mode) { + case BATTERY_METER_CIRCLE: + return new CircleBatteryMeterDrawable(res); + + case BATTERY_METER_TEXT: + return new TextBatteryMeterDrawable(res); + + case BATTERY_METER_ICON_LANDSCAPE: + return new NormalBatteryMeterDrawable(res, true); + + default: + return new NormalBatteryMeterDrawable(res, false); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + if (mMeterMode == BatteryMeterMode.BATTERY_METER_CIRCLE) { + height += (CircleBatteryMeterDrawable.STROKE_WITH / 3); + width = height; + } else if (mMeterMode == BatteryMeterMode.BATTERY_METER_TEXT) { + width = (int)((TextBatteryMeterDrawable) mBatteryMeterDrawable).calculateMeasureWidth(); + onSizeChanged(width, height, 0, 0); // Force a size changed event + } else if (mMeterMode.compareTo(BatteryMeterMode.BATTERY_METER_ICON_LANDSCAPE) == 0) { + width = (int)(height * 1.2f); + } + setMeasuredDimension(width, height); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mHeight = h; + mWidth = w; + synchronized (mLock) { + if (mBatteryMeterDrawable != null) { + mBatteryMeterDrawable.onSizeChanged(w, h, oldw, oldh); + } + } + } + + protected void invalidateIfVisible() { + if (getVisibility() == View.VISIBLE && mAttached) { + if (mAttached) { + postInvalidate(); + } else { + invalidate(); + } + } + } + + public int getColorForLevel(int percent) { + int thresh, color = 0; + for (int i=0; i<mColors.length; i+=2) { + thresh = mColors[i]; + color = mColors[i+1]; + if (percent <= thresh) return color; + } + return color; + } + + public void setShowPercent(boolean show) { + if (ENABLE_PERCENT) { + mShowPercent = show; + invalidateIfVisible(); + } + } + + public void setMode(BatteryMeterMode mode) { + if (mMeterMode == mode) { + return; + } + + mMeterMode = mode; + if (mode == BatteryMeterMode.BATTERY_METER_GONE) { + setVisibility(View.GONE); + synchronized (mLock) { + mBatteryMeterDrawable = null; + } + } else { + synchronized (mLock) { + if (mBatteryMeterDrawable != null) { + mBatteryMeterDrawable.onDispose(); + } + mBatteryMeterDrawable = createBatteryMeterDrawable(mode); + } + if (mMeterMode == BatteryMeterMode.BATTERY_METER_ICON_PORTRAIT || + mMeterMode == BatteryMeterMode.BATTERY_METER_ICON_LANDSCAPE) { + ((NormalBatteryMeterDrawable)mBatteryMeterDrawable).loadBoltPoints( + mContext.getResources()); + } + setVisibility(View.VISIBLE); + postInvalidate(); + requestLayout(); + } + } + + @Override + public void draw(Canvas c) { + synchronized (mLock) { + if (mBatteryMeterDrawable != null) { + mBatteryMeterDrawable.onDraw(c); + } + } + } + + protected interface BatteryMeterDrawable { + void onDraw(Canvas c); + void onSizeChanged(int w, int h, int oldw, int oldh); + void onDispose(); + } + + protected class NormalBatteryMeterDrawable implements BatteryMeterDrawable { + + public static final boolean SINGLE_DIGIT_PERCENT = false; + + public static final int FULL = 96; + public static final int EMPTY = 4; + + public static final float SUBPIXEL = 0.4f; // inset rects for softer edges + + private boolean mDisposed; + + protected final boolean mHorizontal; + + private Paint mFramePaint, mBatteryPaint, mWarningTextPaint, mTextPaint, mBoltPaint; + private int mButtonHeight; + private float mTextHeight; + + private final float[] mBoltPoints; + private final Path mBoltPath = new Path(); + + private final RectF mFrame = new RectF(); + private final RectF mButtonFrame = new RectF(); + private final RectF mClipFrame = new RectF(); + private final RectF mBoltFrame = new RectF(); + + public NormalBatteryMeterDrawable(Resources res, boolean horizontal) { + super(); + mHorizontal = horizontal; + mDisposed = false; + + mFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mFramePaint.setColor(mFrameColor); + mFramePaint.setDither(true); + mFramePaint.setStrokeWidth(0); + mFramePaint.setStyle(Paint.Style.FILL_AND_STROKE); + mFramePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP)); + + mBatteryPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mBatteryPaint.setDither(true); + mBatteryPaint.setStrokeWidth(0); + mBatteryPaint.setStyle(Paint.Style.FILL_AND_STROKE); + + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setColor(mBoltColor); + Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD); + mTextPaint.setTypeface(font); + mTextPaint.setTextAlign(Paint.Align.CENTER); + + mWarningTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mWarningTextPaint.setColor(mColors[1]); + font = Typeface.create("sans-serif", Typeface.BOLD); + mWarningTextPaint.setTypeface(font); + mWarningTextPaint.setTextAlign(Paint.Align.CENTER); + + mBoltPaint = new Paint(); + mBoltPaint.setAntiAlias(true); + mBoltPaint.setColor(mFrameColor); + mBoltPoints = loadBoltPoints(res); + } + + @Override + public void onDraw(Canvas c) { + if (mDisposed) return; + + final int level = 50; + + float drawFrac = (float) level / 100f; + final int pt = getPaddingTop() + (mHorizontal ? (int)(mHeight * 0.20f) : 0); + final int pl = getPaddingLeft(); + final int pr = getPaddingRight(); + final int pb = getPaddingBottom(); + int height = mHeight - pt - pb; + int width = mWidth - pl - pr; + + mButtonHeight = (int) ((mHorizontal ? width : height) * 0.12f); + + mFrame.set(0, 0, width, height); + mFrame.offset(pl, pt); + + if (mHorizontal) { + mButtonFrame.set( + /*cover frame border of intersecting area*/ + width - (mButtonHeight + 5) - mFrame.left, + mFrame.top + height * 0.25f, + mFrame.right, + mFrame.bottom - height * 0.25f); + + mButtonFrame.top += SUBPIXEL; + mButtonFrame.bottom -= SUBPIXEL; + mButtonFrame.right -= SUBPIXEL; + } else { + mButtonFrame.set( + mFrame.left + width * 0.25f, + mFrame.top, + mFrame.right - width * 0.25f, + mFrame.top + mButtonHeight + 5 /*cover frame border of intersecting area*/); + + mButtonFrame.top += SUBPIXEL; + mButtonFrame.left += SUBPIXEL; + mButtonFrame.right -= SUBPIXEL; + } + + if (mHorizontal) { + mFrame.right -= mButtonHeight; + } else { + mFrame.top += mButtonHeight; + } + mFrame.left += SUBPIXEL; + mFrame.top += SUBPIXEL; + mFrame.right -= SUBPIXEL; + mFrame.bottom -= SUBPIXEL; + + // first, draw the battery shape + c.drawRect(mFrame, mFramePaint); + + // fill 'er up + final int color = getColorForLevel(level); + mBatteryPaint.setColor(color); + + if (level >= FULL) { + drawFrac = 1f; + } else if (level <= EMPTY) { + drawFrac = 0f; + } + + c.drawRect(mButtonFrame, drawFrac == 1f ? mBatteryPaint : mFramePaint); + + mClipFrame.set(mFrame); + if (mHorizontal) { + mClipFrame.right -= (mFrame.width() * (1f - drawFrac)); + } else { + mClipFrame.top += (mFrame.height() * (1f - drawFrac)); + } + + c.save(Canvas.CLIP_SAVE_FLAG); + c.clipRect(mClipFrame); + c.drawRect(mFrame, mBatteryPaint); + c.restore(); + + // draw the bolt + final float bl = (int)(mFrame.left + mFrame.width() / (mHorizontal ? 9f : 4.5f)); + final float bt = (int)(mFrame.top + mFrame.height() / (mHorizontal ? 4.5f : 6f)); + final float br = (int)(mFrame.right - mFrame.width() / (mHorizontal ? 6f : 7f)); + final float bb = (int)(mFrame.bottom - mFrame.height() / (mHorizontal ? 7f : 10f)); + if (mBoltFrame.left != bl || mBoltFrame.top != bt + || mBoltFrame.right != br || mBoltFrame.bottom != bb) { + mBoltFrame.set(bl, bt, br, bb); + mBoltPath.reset(); + mBoltPath.moveTo( + mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), + mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); + for (int i = 2; i < mBoltPoints.length; i += 2) { + mBoltPath.lineTo( + mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(), + mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height()); + } + mBoltPath.lineTo( + mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), + mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); + } + c.drawPath(mBoltPath, mBoltPaint); + if (mShowPercent) { + final float nofull = mHorizontal ? 0.75f : 0.6f; + final float single = mHorizontal ? 0.86f : 0.75f; + mTextPaint.setTextSize(height * + (SINGLE_DIGIT_PERCENT ? single + : nofull)); + mTextHeight = -mTextPaint.getFontMetrics().ascent; + + final String str = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level); + final float x = mWidth * 0.5f; + final float y = pt + (height + mTextHeight) * 0.47f; + + c.drawText(str, x, y, mTextPaint); + } + } + + @Override + public void onDispose() { + mHandler.removeCallbacks(mInvalidate); + mDisposed = true; + } + + @Override + public void onSizeChanged(int w, int h, int oldw, int oldh) { + mWarningTextPaint.setTextSize(h * 0.75f); + } + + private float[] loadBoltPoints(Resources res) { + final int[] pts = BatteryMeterView.this.mBoltPoints; + int maxX = 0, maxY = 0; + for (int i = 0; i < pts.length; i += 2) { + maxX = Math.max(maxX, pts[i]); + maxY = Math.max(maxY, pts[i + 1]); + } + final float[] ptsF = new float[pts.length]; + for (int i = 0; i < pts.length; i += 2) { + ptsF[i] = (float)pts[i] / maxX; + ptsF[i + 1] = (float)pts[i + 1] / maxY; + } + return ptsF; + } + } + + protected class CircleBatteryMeterDrawable implements BatteryMeterDrawable { + + public static final float STROKE_WITH = 6.5f; + + private boolean mDisposed; + + private int mAnimOffset; + private boolean mIsAnimating; // stores charge-animation status to reliably + //remove callbacks + + private int mCircleSize; // draw size of circle + private RectF mRectLeft; // contains the precalculated rect used in drawArc(), + // derived from mCircleSize + private float mTextX, mTextY; // precalculated position for drawText() to appear centered + + private Paint mTextPaint; + private Paint mFrontPaint; + private Paint mBackPaint; + private Paint mBoltPaint; + + private final RectF mBoltFrame = new RectF(); + private final float[] mBoltPoints; + private final Path mBoltPath = new Path(); + + public CircleBatteryMeterDrawable(Resources res) { + super(); + mDisposed = false; + + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setColor(mTextColor); + Typeface font = Typeface.create("sans-serif-condensed", Typeface.NORMAL); + mTextPaint.setTypeface(font); + mTextPaint.setTextAlign(Paint.Align.CENTER); + + mFrontPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mFrontPaint.setStrokeCap(Paint.Cap.BUTT); + mFrontPaint.setDither(true); + mFrontPaint.setStrokeWidth(0); + mFrontPaint.setStyle(Paint.Style.STROKE); + + mBackPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mBackPaint.setColor(mFrameColor); + mBackPaint.setStrokeCap(Paint.Cap.BUTT); + mBackPaint.setDither(true); + mBackPaint.setStrokeWidth(0); + mBackPaint.setStyle(Paint.Style.STROKE); + mBackPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR)); + + mBoltPaint = new Paint(); + mBoltPaint.setAntiAlias(true); + mBoltPaint.setColor(getColorForLevel(50)); + mBoltPoints = loadBoltPoints(res); + } + + @Override + public void onDraw(Canvas c) { + if (mDisposed) return; + + if (mRectLeft == null) { + initSizeBasedStuff(); + } + + drawCircle(c, mTextX, mRectLeft); + } + + @Override + public void onDispose() { + mHandler.removeCallbacks(mInvalidate); + mDisposed = true; + } + + @Override + public void onSizeChanged(int w, int h, int oldw, int oldh) { + initSizeBasedStuff(); + } + + private float[] loadBoltPoints(Resources res) { + final int[] pts = BatteryMeterView.this.mBoltPoints; + int maxX = 0, maxY = 0; + for (int i = 0; i < pts.length; i += 2) { + maxX = Math.max(maxX, pts[i]); + maxY = Math.max(maxY, pts[i + 1]); + } + final float[] ptsF = new float[pts.length]; + for (int i = 0; i < pts.length; i += 2) { + ptsF[i] = (float)pts[i] / maxX; + ptsF[i + 1] = (float)pts[i + 1] / maxY; + } + return ptsF; + } + + private void drawCircle(Canvas canvas, float textX, RectF drawRect) { + int level = 50; + Paint paint; + + paint = mFrontPaint; + paint.setColor(getColorForLevel(level)); + + // draw thin gray ring first + canvas.drawArc(drawRect, 270, 360, false, mBackPaint); + // draw colored arc representing charge level + canvas.drawArc(drawRect, 270 + mAnimOffset, 3.6f * level, false, paint); + // if chosen by options, draw percentage text in the middle + // always skip percentage when 100, so layout doesnt break + + // draw the bolt + final float bl = (int)(drawRect.left + drawRect.width() / 3.2f); + final float bt = (int)(drawRect.top + drawRect.height() / 4f); + final float br = (int)(drawRect.right - drawRect.width() / 5.2f); + final float bb = (int)(drawRect.bottom - drawRect.height() / 8f); + if (mBoltFrame.left != bl || mBoltFrame.top != bt + || mBoltFrame.right != br || mBoltFrame.bottom != bb) { + mBoltFrame.set(bl, bt, br, bb); + mBoltPath.reset(); + mBoltPath.moveTo( + mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), + mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); + for (int i = 2; i < mBoltPoints.length; i += 2) { + mBoltPath.lineTo( + mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(), + mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height()); + } + mBoltPath.lineTo( + mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), + mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); + } + canvas.drawPath(mBoltPath, mBoltPaint); + } + + /** + * initializes all size dependent variables + * sets stroke width and text size of all involved paints + * YES! i think the method name is appropriate + */ + private void initSizeBasedStuff() { + mCircleSize = Math.min(getMeasuredWidth(), getMeasuredHeight()); + mTextPaint.setTextSize(mCircleSize / 2f); + + float strokeWidth = mCircleSize / STROKE_WITH; + mFrontPaint.setStrokeWidth(strokeWidth); + mBackPaint.setStrokeWidth(strokeWidth); + + // calculate rectangle for drawArc calls + int pLeft = getPaddingLeft(); + mRectLeft = new RectF(pLeft + strokeWidth / 2.0f, 0 + strokeWidth / 2.0f, mCircleSize + - strokeWidth / 2.0f + pLeft, mCircleSize - strokeWidth / 2.0f); + + // calculate Y position for text + Rect bounds = new Rect(); + mTextPaint.getTextBounds("99", 0, "99".length(), bounds); + mTextX = mCircleSize / 2.0f + getPaddingLeft(); + // the +1dp at end of formula balances out rounding issues.works out on all resolutions + mTextY = mCircleSize / 2.0f + (bounds.bottom - bounds.top) / 2.0f + - strokeWidth / 2.0f + getResources().getDisplayMetrics().density; + } + } + + protected class TextBatteryMeterDrawable implements BatteryMeterDrawable { + + private static final boolean DRAW_LEVEL = false; + + public static final int FULL = 96; + public static final int EMPTY = 4; + + private boolean mDisposed; + + private float mTextX; + private float mTextY; + + private boolean mOldPlugged = false; + private int mOldLevel = -1; + + private boolean mIsAnimating; + private int mAnimOffset; + + private Paint mBackPaint; + private Paint mFrontPaint; + + public TextBatteryMeterDrawable(Resources res) { + super(); + mDisposed = false; + mIsAnimating = false; + + DisplayMetrics dm = res.getDisplayMetrics(); + + mBackPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mBackPaint.setTextAlign(Paint.Align.RIGHT); + mBackPaint.setColor(mFrameColor); + mBackPaint.setTextSize(16.0f * dm.density); + + mFrontPaint = new Paint(mBackPaint); + } + + @Override + public void onDraw(Canvas c) { + if (mDisposed) return; + + int level = 50; + boolean plugged = true; + + if (mOldLevel != level || mOldPlugged != plugged) { + mOldLevel = level; + mOldPlugged = plugged; + + postInvalidate(); + requestLayout(); + return; + } + + mFrontPaint.setColor(getColorForLevel(level)); + + // Is plugged? Then use the animation status + drawWithLevel(c, mAnimOffset, getLevel(level)); + } + + private void drawWithLevel(Canvas c, int level, String levelTxt) { + Rect bounds = getBounds(level); + + // Draw the background + c.drawText(levelTxt, mTextX, mTextY, mBackPaint); + + // Draw the foreground + c.save(); + c.clipRect(0.0f, mTextY - ((level * bounds.height()) / 100.0f), mTextX, mTextY); + c.drawText(levelTxt, mTextX, mTextY, mFrontPaint); + c.restore(); + } + + private void drawWithoutLevel(Canvas c, String levelTxt) { + // We need to draw the overlay back paint to get the proper color + c.drawText(levelTxt, mTextX, mTextY, mBackPaint); + c.drawText(levelTxt, mTextX, mTextY, mFrontPaint); + } + + @Override + public void onDispose() { + mHandler.removeCallbacks(mInvalidate); + mDisposed = true; + } + + @Override + public void onSizeChanged(int w, int h, int oldw, int oldh) { + Rect bounds = getBounds(50); + float onedp = mContext.getResources().getDisplayMetrics().density * 0.5f; + float height = h - getPaddingBottom() - getPaddingTop(); + + mTextX = w; + mTextY = h - getPaddingBottom() - (height / 2 - bounds.height() /2) + onedp; + } + + protected float calculateMeasureWidth() { + Rect bounds = getBounds(50); + float onedp = mContext.getResources().getDisplayMetrics().density; + return bounds.width() + getPaddingStart() + getPaddingEnd() + onedp; + } + + private Rect getBounds(int level) { + Rect bounds = new Rect(); + String levelTxt = getLevel(level); + mBackPaint.getTextBounds(levelTxt, 0, levelTxt.length(), bounds); + return bounds; + } + + private String getLevel(int level) { + if (level == -1) { + return String.format("?", level); + } + return String.format("%s%%", level); + } + + private void resetChargeAnimation() { + if (mIsAnimating) { + mIsAnimating = false; + mAnimOffset = 0; + mHandler.removeCallbacks(mInvalidate); + } + } + } +} |