From d0f415ae26b2e35508adacc920c8ea4ae809ccce Mon Sep 17 00:00:00 2001 From: Andy Mast Date: Thu, 1 May 2014 14:41:43 -0700 Subject: Theme Chooser: Initial Contribution [2/2] Introduces a new theme chooser UI for the new theme engine. The new theme chooser allows the user to mix'n'match theme components (styles, boot anim, icons, fonts, wallpapers, sounds). Contributors: Adrian Foulk - UX Lead Andrew Mast - Software Engineer Clark Scheff - Software Engineer RJ Oakes - QA Engineer Special thanks to T-Mobile for open sourcing the original theme engine. Change-Id: Ifdcc0655ae4125ba3287c5c82fbe852840b3625d --- .../cyanogenmod/themes/provider/AppReceiver.java | 46 +++ .../themes/provider/CopyImageService.java | 161 +++++++++ .../themes/provider/ThemePackageHelper.java | 273 +++++++++++++++ .../themes/provider/ThemesOpenHelper.java | 160 +++++++++ .../themes/provider/ThemesProvider.java | 389 +++++++++++++++++++++ 5 files changed, 1029 insertions(+) create mode 100644 src/org/cyanogenmod/themes/provider/AppReceiver.java create mode 100644 src/org/cyanogenmod/themes/provider/CopyImageService.java create mode 100644 src/org/cyanogenmod/themes/provider/ThemePackageHelper.java create mode 100644 src/org/cyanogenmod/themes/provider/ThemesOpenHelper.java create mode 100644 src/org/cyanogenmod/themes/provider/ThemesProvider.java (limited to 'src') diff --git a/src/org/cyanogenmod/themes/provider/AppReceiver.java b/src/org/cyanogenmod/themes/provider/AppReceiver.java new file mode 100644 index 0000000..1a0835e --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/AppReceiver.java @@ -0,0 +1,46 @@ +/* + * 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.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.util.Log; + +public class AppReceiver extends BroadcastReceiver { + public final static String TAG = AppReceiver.class.getName(); + + @Override + public void onReceive(Context context, Intent intent) { + Uri uri = intent.getData(); + String pkgName = uri != null ? uri.getSchemeSpecificPart() : null; + boolean isReplacing = intent.getExtras().getBoolean(Intent.EXTRA_REPLACING, false); + + try { + if (intent.getAction().equals(Intent.ACTION_PACKAGE_ADDED) && !isReplacing) { + ThemePackageHelper.insertPackage(context, pkgName); + } else if (intent.getAction().equals(Intent.ACTION_PACKAGE_FULLY_REMOVED)) { + ThemePackageHelper.removePackage(context, pkgName); + } else if (intent.getAction().equals(Intent.ACTION_PACKAGE_REPLACED)) { + ThemePackageHelper.updatePackage(context, pkgName); + } + } catch(NameNotFoundException e) { + Log.e(TAG, "Unable to add package to theme's provider ", e); + } + } +} diff --git a/src/org/cyanogenmod/themes/provider/CopyImageService.java b/src/org/cyanogenmod/themes/provider/CopyImageService.java new file mode 100644 index 0000000..33c6954 --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/CopyImageService.java @@ -0,0 +1,161 @@ +/* + * 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.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 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/"; + + public CopyImageService() { + super(CopyImageService.class.getName()); + } + + @Override + protected void onHandleIntent(Intent intent) { + + if (intent.getExtras() == null) + return; + + String pkgName = intent.getExtras().getString(EXTRA_PKG_NAME); + + if (pkgName != null) { + generate(this, pkgName); + } + + String homescreen = Environment.getDataDirectory().getPath() + + IMAGES_PATH + pkgName + + ".homescreen.png"; + String lockscreen = Environment.getDataDirectory().getPath() + + IMAGES_PATH + pkgName + + ".lockscreen.png"; + String stylePreview = Environment.getDataDirectory().getPath() + + IMAGES_PATH + pkgName + + ".stylepreview.jpg"; + String wallpaper = ContentResolver.SCHEME_FILE + "://" + Environment.getDataDirectory().getPath() + + WALLPAPER_PATH + pkgName + + ".wallpaper1.jpg"; + 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); + } + + public static void generate(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) { + e.printStackTrace(); + } + + // This is for testing only. We copy some assets from APK and put into + // internal storage + AssetManager assetManager = themeContext.getAssets(); + try { + InputStream homescreen = assetManager + .open("images/icons_wallpaper_straight.png"); + InputStream lockscreen = assetManager + .open("images/lockscreen_portrait.png"); + + 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.png"); + File lockscreenOut = new File(imgDir, pkgName + ".lockscreen.png"); + + FileOutputStream out = new FileOutputStream(homescreenOut); + byte[] buffer = new byte[4096]; + int count = 0; + while ((count = homescreen.read(buffer)) != -1) { + out.write(buffer, 0, count); + } + out.close(); + + out = new FileOutputStream(lockscreenOut); + while ((count = lockscreen.read(buffer)) != -1) { + out.write(buffer, 0, count); + } + out.close(); + } catch (IOException e) { + Log.e(TAG, "ThemesOpenHelper could not copy test image data"); + } + + //Copy Style preview + try { + InputStream stylepreview = assetManager + .open("images/style.jpg"); + File dataDir = context.getFilesDir(); + File imgDir = new File(dataDir, "images"); + imgDir.mkdir(); + + File styleOut = new File(imgDir, pkgName + ".stylepreview.jpg"); + + byte[] buffer = new byte[4096]; + int count = 0; + FileOutputStream out = new FileOutputStream(styleOut); + while ((count = stylepreview.read(buffer)) != -1) { + out.write(buffer, 0, count); + } + out.close(); + } catch (IOException e) { + Log.e(TAG, "ThemesOpenHelper could not copy style image data"); + } + } +} diff --git a/src/org/cyanogenmod/themes/provider/ThemePackageHelper.java b/src/org/cyanogenmod/themes/provider/ThemePackageHelper.java new file mode 100644 index 0000000..73900a4 --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/ThemePackageHelper.java @@ -0,0 +1,273 @@ +/* + * 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.ContentValues; +import android.content.Context; +import android.content.pm.LegacyThemeInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ThemeInfo; +import android.content.res.AssetManager; +import android.content.res.ThemeManager; +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; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Helper class to populate the provider with info from the theme. + */ +public class ThemePackageHelper { + public final static String TAG = ThemePackageHelper.class.getName(); + + // Maps the theme component to its folder name in assets. + public static HashMap sComponentToFolderName = new HashMap(); + static { + sComponentToFolderName.put(ThemesColumns.MODIFIES_OVERLAYS, "overlays"); + sComponentToFolderName.put(ThemesColumns.MODIFIES_BOOT_ANIM, "bootanimation"); + sComponentToFolderName.put(ThemesColumns.MODIFIES_FONTS, "fonts"); + sComponentToFolderName.put(ThemesColumns.MODIFIES_ICONS, "icons"); + sComponentToFolderName.put(ThemesColumns.MODIFIES_LAUNCHER, "wallpapers"); + sComponentToFolderName.put(ThemesColumns.MODIFIES_LOCKSCREEN, "lockscreen"); + sComponentToFolderName.put(ThemesColumns.MODIFIES_ALARMS, "alarms"); + sComponentToFolderName.put(ThemesColumns.MODIFIES_NOTIFICATIONS, "notifications"); + sComponentToFolderName.put(ThemesColumns.MODIFIES_RINGTONES, "ringtones"); + } + + public static boolean insertPackage(Context context, String pkgName) + throws NameNotFoundException { + PackageInfo pi = context.getPackageManager().getPackageInfo(pkgName, 0); + if (pi == null) + return false; + + Map 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); + } + return true; + } + + private static void insertPackageInternal(Context context, PackageInfo pi, + Map capabilities) { + ThemeInfo info = pi.themeInfos[0]; + boolean isPresentableTheme = ThemePackageHelper.isPresentableTheme(capabilities); + + 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, isPresentableTheme); + values.put(ThemesColumns.IS_LEGACY_THEME, pi.isLegacyThemeApk); + values.put(ThemesColumns.LAST_UPDATE_TIME, pi.lastUpdateTime); + + // Insert theme capabilities + for (Map.Entry entry : capabilities.entrySet()) { + String component = entry.getKey(); + Boolean isImplemented = entry.getValue(); + values.put(component, isImplemented); + } + + context.getContentResolver().insert(ThemesColumns.CONTENT_URI, values); + } + + private static void insertLegacyPackageInternal(Context context, PackageInfo pi, + Map 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.LAST_UPDATE_TIME, pi.lastUpdateTime); + + // Insert theme capabilities + for (Map.Entry entry : capabilities.entrySet()) { + String component = entry.getKey(); + Boolean isImplemented = ThemesColumns.MODIFIES_OVERLAYS.equals(component) ? Boolean.TRUE + : entry.getValue(); + values.put(component, isImplemented); + } + + context.getContentResolver().insert(ThemesColumns.CONTENT_URI, values); + } + + public static void updatePackage(Context context, String pkgName) throws NameNotFoundException { + PackageInfo pi = context.getPackageManager().getPackageInfo(pkgName, 0); + Map capabilities = getCapabilities(context, pkgName); + if (pi.themeInfos != null && pi.themeInfos.length > 0) { + updatePackageInternal(context, pi, capabilities); + } else if (pi.legacyThemeInfos != null && pi.legacyThemeInfos.length > 0) { + updateLegacyPackageInternal(context, pi, capabilities); + } + } + + private static void updatePackageInternal(Context context, PackageInfo pi, + Map capabilities) { + ThemeInfo info = pi.themeInfos[0]; + boolean isPresentableTheme = ThemePackageHelper.isPresentableTheme(capabilities); + 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, isPresentableTheme); + values.put(ThemesColumns.IS_LEGACY_THEME, pi.isLegacyThemeApk); + values.put(ThemesColumns.LAST_UPDATE_TIME, pi.lastUpdateTime); + + String where = ThemesColumns.PKG_NAME + "=?"; + String[] args = { pi.packageName }; + context.getContentResolver().update(ThemesColumns.CONTENT_URI, values, where, args); + } + + private static void updateLegacyPackageInternal(Context context, PackageInfo pi, + Map 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.LAST_UPDATE_TIME, pi.lastUpdateTime); + + String where = ThemesColumns.PKG_NAME + "=?"; + String[] args = { pi.packageName }; + context.getContentResolver().update(ThemesColumns.CONTENT_URI, values, where, args); + } + + public static void removePackage(Context context, String pkgToRemove) { + // Check currently applied components (fonts, wallpapers etc) and verify the theme is still + // installed if it is not installed, we need to set the component back to the default theme + List moveToDefault = new LinkedList(); // components to move back to default + Cursor mixnmatch = context.getContentResolver().query(MixnMatchColumns.CONTENT_URI, null, + null, null, null); + while (mixnmatch.moveToNext()) { + String mixnmatchKey = mixnmatch.getString(mixnmatch + .getColumnIndex(MixnMatchColumns.COL_KEY)); + String component = ThemesContract.MixnMatchColumns + .mixNMatchKeyToComponent(mixnmatchKey); + String pkg = mixnmatch.getString(mixnmatch.getColumnIndex(MixnMatchColumns.COL_VALUE)); + if (pkgToRemove.equals(pkg)) { + moveToDefault.add(component); + } + } + ThemeManager manager = (ThemeManager) context.getSystemService(Context.THEME_SERVICE); + manager.requestThemeChange("default", moveToDefault); + + // Delete the theme from the db + String selection = ThemesColumns.PKG_NAME + "= ?"; + String[] selectionArgs = { pkgToRemove }; + context.getContentResolver().delete(ThemesColumns.CONTENT_URI, selection, selectionArgs); + } + + /** + * Returns a map of components with value of true if the APK themes that component or false + * otherwise. Example of a theme that handles fonts but not ringtones: (MODIFIES_FONTS -> true, + * MODIFIES_RINGTONES -> false) + */ + public static Map getCapabilities(Context context, String pkgName) { + PackageInfo pi = null; + try { + pi = context.getPackageManager().getPackageInfo(pkgName, 0); + } catch (Exception e) { + Log.e(TAG, "Error getting pi during insert", e); + return Collections.emptyMap(); + } + + // Determine what this theme is capable of + Context themeContext = null; + try { + themeContext = context.createPackageContext(pkgName, Context.CONTEXT_IGNORE_SECURITY); + } catch (NameNotFoundException e) { + e.printStackTrace(); + } + + // Determine what components the theme implements. + // TODO: Some sort of verification for valid elements (ex font should have valid ttf files) + HashMap implementMap = new HashMap(); + for (Map.Entry entry : sComponentToFolderName.entrySet()) { + String component = entry.getKey(); + String folderName = entry.getValue(); + boolean hasComponent = pi.isLegacyThemeApk ? hasThemeComponentLegacy(pi, component) + : hasThemeComponent(themeContext, folderName); + implementMap.put(component, hasComponent); + } + return implementMap; + } + + 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) { + boolean found = false; + AssetManager assetManager = themeContext.getAssets(); + try { + String[] assetList = assetManager.list(component); + if (assetList != null && assetList.length > 0) { + found = true; + } + } catch (IOException e) { + Log.e(TAG, "There was an error checking for asset " + component, e); + } + return found; + } + + // Presently we are defining a "presentable" theme as any theme + // which implements 2+ components. Such themes can be shown to the user + // under the "themes" category. + public static boolean isPresentableTheme(Map implementMap) { + int count = 0; + for (Boolean isImplemented : implementMap.values()) { + count += isImplemented ? 1 : 0; + } + return count >= 2; + } +} diff --git a/src/org/cyanogenmod/themes/provider/ThemesOpenHelper.java b/src/org/cyanogenmod/themes/provider/ThemesOpenHelper.java new file mode 100644 index 0000000..092536d --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/ThemesOpenHelper.java @@ -0,0 +1,160 @@ +/* + * 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.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; +import android.provider.ThemesContract.ThemesColumns; +import android.provider.ThemesContract.MixnMatchColumns; +import android.util.Log; + +public class ThemesOpenHelper extends SQLiteOpenHelper { + private static final String TAG = ThemesOpenHelper.class.getName(); + + private static final int DATABASE_VERSION = 2; + private static final String DATABASE_NAME = "themes.db"; + private static final String DEFAULT_PKG_NAME = "default"; + + public ThemesOpenHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(ThemesTable.THEMES_TABLE_CREATE); + db.execSQL(MixnMatchTable.MIXNMATCH_TABLE_CREATE); + + ThemesTable.insertDefaults(db); + MixnMatchTable.insertDefaults(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.i(TAG, "Upgrading DB from version " + oldVersion + " to " + newVersion); + try { + if (oldVersion == 1) { + upgradeToVersion2(db); + oldVersion = 2; + } + if (oldVersion != DATABASE_VERSION) { + Log.e(TAG, "Recreating db because unknown database version: " + oldVersion); + dropTables(db); + onCreate(db); + } + } catch(SQLiteException e) { + Log.e(TAG, "onUpgrade: SQLiteException, recreating db. ", e); + Log.e(TAG, "(oldVersion was " + oldVersion + ")"); + dropTables(db); + onCreate(db); + return; + } + } + + private void upgradeToVersion2(SQLiteDatabase db) { + String addStyleColumn = String.format("ALTER TABLE %s ADD COLUMN %s TEXT", + ThemesTable.TABLE_NAME, ThemesColumns.STYLE_URI); + db.execSQL(addStyleColumn); + } + + private void dropTables(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + ThemesTable.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + MixnMatchTable.TABLE_NAME); + } + + public static class ThemesTable { + protected static final String TABLE_NAME = "themes"; + + private static final String THEMES_TABLE_CREATE = + "CREATE TABLE " + TABLE_NAME + " (" + + ThemesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + ThemesColumns.TITLE + " TEXT," + + ThemesColumns.AUTHOR + " TEXT," + + ThemesColumns.PKG_NAME + " TEXT UNIQUE NOT NULL," + + ThemesColumns.DATE_CREATED + " INTEGER," + + ThemesColumns.HOMESCREEN_URI + " TEXT," + + ThemesColumns.LOCKSCREEN_URI + " TEXT," + + ThemesColumns.STYLE_URI + " TEXT," + + ThemesColumns.WALLPAPER_URI + " TEXT," + + ThemesColumns.BOOT_ANIM_URI + " TEXT," + + ThemesColumns.FONT_URI + " TEXT," + + ThemesColumns.STATUSBAR_URI + " TEXT," + + ThemesColumns.ICON_URI + " TEXT," + + ThemesColumns.PRIMARY_COLOR + " TEXT," + + ThemesColumns.SECONDARY_COLOR + " TEXT," + + ThemesColumns.MODIFIES_LAUNCHER + " INTEGER DEFAULT 0, " + + ThemesColumns.MODIFIES_LOCKSCREEN + " INTEGER DEFAULT 0, " + + ThemesColumns.MODIFIES_ICONS + " INTEGER DEFAULT 0, " + + ThemesColumns.MODIFIES_BOOT_ANIM + " INTEGER DEFAULT 0, " + + ThemesColumns.MODIFIES_FONTS + " INTEGER DEFAULT 0, " + + ThemesColumns.MODIFIES_RINGTONES + " INTEGER DEFAULT 0, " + + ThemesColumns.MODIFIES_NOTIFICATIONS + " INTEGER DEFAULT 0, " + + ThemesColumns.MODIFIES_ALARMS + " INTEGER DEFAULT 0, " + + ThemesColumns.MODIFIES_OVERLAYS + " INTEGER DEFAULT 0, " + + ThemesColumns.PRESENT_AS_THEME + " INTEGER DEFAULT 0, " + + ThemesColumns.IS_LEGACY_THEME + " INTEGER DEFAULT 0," + + ThemesColumns.LAST_UPDATE_TIME + " INTEGER DEFAULT 0" + + ")"; + + public static void insertDefaults(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + values.put(ThemesColumns.TITLE, "Holo (Default)"); + 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); + values.put(ThemesColumns.MODIFIES_ICONS, 1); + values.put(ThemesColumns.MODIFIES_LAUNCHER, 1); + values.put(ThemesColumns.MODIFIES_LOCKSCREEN, 1); + values.put(ThemesColumns.MODIFIES_NOTIFICATIONS, 1); + values.put(ThemesColumns.MODIFIES_RINGTONES, 1); + values.put(ThemesColumns.PRESENT_AS_THEME, 1); + values.put(ThemesColumns.IS_LEGACY_THEME, 0); + values.put(ThemesColumns.MODIFIES_OVERLAYS, 1); + db.insert(TABLE_NAME, null, values); + } + } + + public static class MixnMatchTable { + protected static final String TABLE_NAME = "mixnmatch"; + private static final String MIXNMATCH_TABLE_CREATE = + "CREATE TABLE " + TABLE_NAME + " (" + + MixnMatchColumns.COL_KEY + " TEXT PRIMARY KEY," + + MixnMatchColumns.COL_VALUE + " TEXT" + + ")"; + + public static void insertDefaults(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + for(String key : MixnMatchColumns.ROWS) { + values.put(MixnMatchColumns.COL_KEY, key); + values.put(MixnMatchColumns.COL_VALUE, DEFAULT_PKG_NAME); + db.insert(TABLE_NAME, null, values); + } + } + } +} + + diff --git a/src/org/cyanogenmod/themes/provider/ThemesProvider.java b/src/org/cyanogenmod/themes/provider/ThemesProvider.java new file mode 100644 index 0000000..6c70709 --- /dev/null +++ b/src/org/cyanogenmod/themes/provider/ThemesProvider.java @@ -0,0 +1,389 @@ +/* + * 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.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.UriMatcher; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.ThemeManager; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.provider.ThemesContract; +import android.provider.ThemesContract.MixnMatchColumns; +import android.provider.ThemesContract.ThemesColumns; +import android.util.Log; + +import org.cyanogenmod.themes.provider.AppReceiver; +import org.cyanogenmod.themes.provider.ThemesOpenHelper.MixnMatchTable; +import org.cyanogenmod.themes.provider.ThemesOpenHelper.ThemesTable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class ThemesProvider extends ContentProvider { + private static final String TAG = ThemesProvider.class.getSimpleName(); + private static final boolean DEBUG = false; + private static final int MIXNMATCH = 1; + private static final int MIXNMATCH_KEY = 2; + private static final int THEMES = 3; + private static final int THEMES_ID = 4; + + private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + + private final Handler mHandler = new Handler(); + private ThemesOpenHelper mDatabase; + + static { + sUriMatcher.addURI(ThemesContract.AUTHORITY, "mixnmatch/", MIXNMATCH); + sUriMatcher.addURI(ThemesContract.AUTHORITY, "mixnmatch/*", MIXNMATCH_KEY); + sUriMatcher.addURI(ThemesContract.AUTHORITY, "themes/", THEMES); + sUriMatcher.addURI(ThemesContract.AUTHORITY, "themes/#", THEMES_ID); + } + + public static void setActiveTheme(Context context, String pkgName) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + Editor edit = prefs.edit(); + edit.putString("SelectedThemePkgName", pkgName); + edit.commit(); + } + + public static String getActiveTheme(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getString("SelectedThemePkgName", "default"); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int match = sUriMatcher.match(uri); + switch (match) { + case THEMES: + SQLiteDatabase sqlDB = mDatabase.getWritableDatabase(); + int rowsDeleted = sqlDB.delete(ThemesTable.TABLE_NAME, selection, selectionArgs); + getContext().getContentResolver().notifyChange(uri, null); + return rowsDeleted; + case MIXNMATCH: + throw new UnsupportedOperationException("Cannot delete rows in MixNMatch table"); + } + return 0; + } + + @Override + public String getType(Uri uri) { + int match = sUriMatcher.match(uri); + switch (match) { + case THEMES: + return "vnd.android.cursor.dir/themes"; + case THEMES_ID: + return "vnd.android.cursor.item/themes"; + case MIXNMATCH: + return "vnd.android.cursor.dir/mixnmatch"; + case MIXNMATCH_KEY: + return "vnd.android.cursor.item/mixnmatch"; + default: + return null; + } + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + int uriType = sUriMatcher.match(uri); + SQLiteDatabase sqlDB = mDatabase.getWritableDatabase(); + long id = 0; + switch (uriType) { + case THEMES: + id = sqlDB.insert(ThemesOpenHelper.ThemesTable.TABLE_NAME, null, values); + Intent intent = new Intent(getContext(), CopyImageService.class); + intent.putExtra(CopyImageService.EXTRA_PKG_NAME, + values.getAsString(ThemesColumns.PKG_NAME)); + getContext().startService(intent); + break; + case MIXNMATCH: + throw new UnsupportedOperationException("Cannot insert rows into MixNMatch table"); + default: + throw new IllegalArgumentException("Unknown URI: " + uri); + } + getContext().getContentResolver().notifyChange(uri, null); + return Uri.parse(MixnMatchColumns.CONTENT_URI + "/" + id); + } + + @Override + public boolean onCreate() { + mDatabase = new ThemesOpenHelper(getContext()); + + /** + * Sync database with package manager + */ + mHandler.post(new Runnable() { + public void run() { + new VerifyInstalledThemesThread().start(); + } + }); + + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + + SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); + SQLiteDatabase db = mDatabase.getReadableDatabase(); + + /* + * Choose the table to query and a sort order based on the code returned for the incoming + * URI. Here, too, only the statements for table 3 are shown. + */ + switch (sUriMatcher.match(uri)) { + case THEMES: + queryBuilder.setTables(ThemesOpenHelper.ThemesTable.TABLE_NAME); + break; + case THEMES_ID: + queryBuilder.setTables(ThemesOpenHelper.ThemesTable.TABLE_NAME); + queryBuilder.appendWhere(ThemesColumns._ID + "=" + uri.getLastPathSegment()); + break; + case MIXNMATCH: + queryBuilder.setTables(THEMES_MIXNMATCH_INNER_JOIN); + break; + case MIXNMATCH_KEY: + queryBuilder.setTables(THEMES_MIXNMATCH_INNER_JOIN); + queryBuilder.appendWhere(MixnMatchColumns.COL_KEY + "=" + uri.getLastPathSegment()); + break; + default: + return null; + } + + Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, + sortOrder); + if (cursor != null) { + cursor.setNotificationUri(getContext().getContentResolver(), uri); + } + + return cursor; + } + + private static final String THEMES_MIXNMATCH_INNER_JOIN = MixnMatchTable.TABLE_NAME + + " INNER JOIN " + ThemesTable.TABLE_NAME + " ON (" + MixnMatchColumns.COL_VALUE + + " = " + ThemesColumns.PKG_NAME + ")"; + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + + int rowsUpdated = 0; + SQLiteDatabase sqlDB = mDatabase.getWritableDatabase(); + + 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: + 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); + } + getContext().getContentResolver().notifyChange(uri, null); + return rowsUpdated; + case MIXNMATCH: + rowsUpdated = sqlDB.update(MixnMatchTable.TABLE_NAME, values, selection, selectionArgs); + getContext().getContentResolver().notifyChange(uri, null); + case MIXNMATCH_KEY: + // Don't support right now. Any need? + } + return rowsUpdated; + } + + /** + * 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. + */ + private boolean updateNotTriggeredByContentProvider(ContentValues values) { + if (values == null) return true; + return !(values.containsKey(ThemesColumns.HOMESCREEN_URI) + || values.containsKey(ThemesColumns.LOCKSCREEN_URI) || values + .containsKey(ThemesColumns.STYLE_URI)); + } + + /** + * This class has been modified from its original source. Original Source: ThemesProvider.java + * See https://github.com/tmobile/themes-platform-vendor-tmobile-providers-ThemeManager + * Copyright (C) 2010, T-Mobile USA, Inc. http://www.apache.org/licenses/LICENSE-2.0 + */ + private class VerifyInstalledThemesThread extends Thread { + private final SQLiteDatabase mDb; + + public VerifyInstalledThemesThread() { + mDb = mDatabase.getWritableDatabase(); + } + + public void run() { + android.os.Process.setThreadPriority(Thread.MIN_PRIORITY); + + long start; + + if (DEBUG) { + start = System.currentTimeMillis(); + } + + SQLiteDatabase db = mDb; + db.beginTransaction(); + try { + verifyPackages(); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + + if (DEBUG) { + Log.d(TAG, "VerifyInstalledThemesThread took " + + (System.currentTimeMillis() - start) + " ms."); + } + } + } + + private void verifyPackages() { + /* List all currently installed theme packages according to PM */ + List packages = getContext().getPackageManager().getInstalledPackages(0); + List themePackages = new ArrayList(); + Map pmThemes = new HashMap(); + for (PackageInfo info : packages) { + if (info.isThemeApk || info.isLegacyThemeApk) { + themePackages.add(info); + pmThemes.put(info.packageName, info); + } + } + + /* + * Get all the known themes according to the provider. Then discover which themes have + * been deleted, need updating, or need to be inserted into the db + */ + Cursor current = mDb.query(ThemesTable.TABLE_NAME, null, null, null, null, null, null); + List deleteList = new LinkedList(); + List updateList = new LinkedList(); + while (current.moveToNext()) { + int updateTimeIdx = current.getColumnIndex( + ThemesContract.ThemesColumns.LAST_UPDATE_TIME); + int pkgNameIdx = current.getColumnIndex(ThemesContract.ThemesColumns.PKG_NAME); + long updateTime = current.getLong(updateTimeIdx); + String pkgName = current.getString(pkgNameIdx); + + // Ignore default theme + if (pkgName.equals("default")) { + continue; + } + + // Packages which are not in PM should be deleted from db + PackageInfo info = pmThemes.get(pkgName); + if (info == null) { + deleteList.add(pkgName); + continue; + } + + // Updated packages in PM should be + // updated in the db + long pmUpdateTime = (info.lastUpdateTime == 0) ? info.firstInstallTime + : info.lastUpdateTime; + if (pmUpdateTime != updateTime) { + updateList.add(info); + } + + // The remaining packages in pmThemes + // will be the ones to insert into the provider + pmThemes.remove(pkgName); + } + + // Check currently applied components (fonts, wallpapers etc) and verify the theme is + // still installed. If it is not installed, set the component back to the default theme + List moveToDefault = new LinkedList(); + Cursor mixnmatch = mDb.query(MixnMatchTable.TABLE_NAME, null, null, null, null, null, + null); + while (mixnmatch.moveToNext()) { + String mixnmatchKey = mixnmatch.getString(mixnmatch + .getColumnIndex(MixnMatchColumns.COL_KEY)); + String component = ThemesContract.MixnMatchColumns + .mixNMatchKeyToComponent(mixnmatchKey); + + String pkg = mixnmatch.getString(mixnmatch + .getColumnIndex(MixnMatchColumns.COL_VALUE)); + if (deleteList.contains(pkg)) { + moveToDefault.add(component); + } + } + ThemeManager mService = (ThemeManager) getContext().getSystemService( + Context.THEME_SERVICE); + mService.requestThemeChange("default", moveToDefault); + + // Update the database after we revert to default + deleteThemes(deleteList); + insertThemes(pmThemes.values()); + updateThemes(updateList); + } + + private void deleteThemes(List themesToDelete) { + int rows = 0; + String where = ThemesColumns.PKG_NAME + "=?"; + for (String pkgName : themesToDelete) { + String[] whereArgs = { pkgName }; + rows += mDb.delete(ThemesTable.TABLE_NAME, where, whereArgs); + } + Log.d(TAG, "Deleted " + rows); + } + + private void insertThemes(Collection themesToInsert) { + for (PackageInfo themeInfo : themesToInsert) { + try { + ThemePackageHelper.insertPackage(getContext(), themeInfo.packageName); + } catch (NameNotFoundException e) { + Log.e(TAG, "Unable to insert theme " + themeInfo.packageName, e); + } + } + } + + private void updateThemes(List themesToUpdate) { + for (PackageInfo themeInfo : themesToUpdate) { + try { + ThemePackageHelper.updatePackage(getContext(), themeInfo.packageName); + } catch (NameNotFoundException e) { + Log.e(TAG, "Unable to update theme " + themeInfo.packageName, e); + } + } + } + } + +} -- cgit v1.2.3