diff options
author | Andy Mast <andy@cyngn.com> | 2014-05-01 14:41:43 -0700 |
---|---|---|
committer | Andy Mast <andy@cyngn.com> | 2014-05-06 14:08:44 -0700 |
commit | d0f415ae26b2e35508adacc920c8ea4ae809ccce (patch) | |
tree | fded3b630cb39e8c35b06fe5c49d441a2800b399 | |
parent | 27c5437e4a2bd567937b2b6ebcdd25e31aecd339 (diff) | |
download | android_packages_providers_ThemesProvider-d0f415ae26b2e35508adacc920c8ea4ae809ccce.tar.gz android_packages_providers_ThemesProvider-d0f415ae26b2e35508adacc920c8ea4ae809ccce.tar.bz2 android_packages_providers_ThemesProvider-d0f415ae26b2e35508adacc920c8ea4ae809ccce.zip |
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
-rw-r--r-- | .gitignore | 11 | ||||
-rw-r--r-- | Android.mk | 12 | ||||
-rw-r--r-- | AndroidManifest.xml | 47 | ||||
-rw-r--r-- | MODULE_LICENSE_APACHE2 (renamed from EMPTY) | 0 | ||||
-rw-r--r-- | res/drawable-xxhdpi/ic_app_themes.png | bin | 0 -> 1096 bytes | |||
-rw-r--r-- | res/values/strings.xml | 21 | ||||
-rw-r--r-- | src/org/cyanogenmod/themes/provider/AppReceiver.java | 46 | ||||
-rw-r--r-- | src/org/cyanogenmod/themes/provider/CopyImageService.java | 161 | ||||
-rw-r--r-- | src/org/cyanogenmod/themes/provider/ThemePackageHelper.java | 273 | ||||
-rw-r--r-- | src/org/cyanogenmod/themes/provider/ThemesOpenHelper.java | 160 | ||||
-rw-r--r-- | src/org/cyanogenmod/themes/provider/ThemesProvider.java | 389 |
11 files changed, 1120 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8da36db --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Project files and paths. +.classpath +.project +*.iml +**/*.iml +.settings/ +bin/ +libs/ +gen/ +.idea/ +project.properties diff --git a/Android.mk b/Android.mk new file mode 100644 index 0000000..a763f72 --- /dev/null +++ b/Android.mk @@ -0,0 +1,12 @@ +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_MODULE_TAGS := optional + +LOCAL_PACKAGE_NAME := ThemesProvider +LOCAL_CERTIFICATE := platform +LOCAL_PRIVILEGED_MODULE := true + +include $(BUILD_PACKAGE) diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..8a89803 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.cyanogenmod.themes.provider" + android:sharedUserId="org.cyanogenmod.themes" + android:versionCode="1" + android:versionName="1.0" > + + <uses-permission android:name="android.permission.ACCESS_THEME_MANAGER" /> + + <uses-sdk + android:minSdkVersion="19" + android:targetSdkVersion="19" /> + + <application + android:allowBackup="true" + android:icon="@drawable/ic_app_themes" + android:label="@string/app_name" > + + <provider + android:name="org.cyanogenmod.themes.provider.ThemesProvider" + android:authorities="com.cyanogenmod.themes" + android:exported="true" /> + + <service android:name=".CopyImageService" > + </service> + + <receiver android:name="org.cyanogenmod.themes.provider.AppReceiver" > + <intent-filter> + <action android:name="android.intent.action.PACKAGE_ADDED" /> + <action android:name="android.intent.action.PACKAGE_REPLACED" /> + + <category android:name="com.tmobile.intent.category.THEME_PACKAGE_INSTALL_STATE_CHANGE" /> + + <data android:scheme="package" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" /> + <action android:name="android.intent.action.PACKAGE_DATA_CLEARED" /> + + <category android:name="com.tmobile.intent.category.THEME_PACKAGE_INSTALL_STATE_CHANGE" /> + + <data android:scheme="package" /> + </intent-filter> + </receiver> + </application> + +</manifest> diff --git a/EMPTY b/MODULE_LICENSE_APACHE2 index e69de29..e69de29 100644 --- a/EMPTY +++ b/MODULE_LICENSE_APACHE2 diff --git a/res/drawable-xxhdpi/ic_app_themes.png b/res/drawable-xxhdpi/ic_app_themes.png Binary files differnew file mode 100644 index 0000000..fbbf03b --- /dev/null +++ b/res/drawable-xxhdpi/ic_app_themes.png diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 0000000..fed2be2 --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> +<resources> + + <string name="app_name">Themes Provider</string> + +</resources> 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<String, String> sComponentToFolderName = new HashMap<String, String>(); + 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<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); + } + return true; + } + + private static void insertPackageInternal(Context context, PackageInfo pi, + Map<String, Boolean> 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<String, Boolean> 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<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.LAST_UPDATE_TIME, pi.lastUpdateTime); + + // Insert theme capabilities + for (Map.Entry<String, Boolean> 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<String, Boolean> 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<String, Boolean> 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<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.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<String> moveToDefault = new LinkedList<String>(); // 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<String, Boolean> 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<String, Boolean> implementMap = new HashMap<String, Boolean>(); + 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); + 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<String, Boolean> 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<PackageInfo> packages = getContext().getPackageManager().getInstalledPackages(0); + List<PackageInfo> themePackages = new ArrayList<PackageInfo>(); + Map<String, PackageInfo> pmThemes = new HashMap<String, PackageInfo>(); + 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<String> deleteList = new LinkedList<String>(); + List<PackageInfo> updateList = new LinkedList<PackageInfo>(); + 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<String> moveToDefault = new LinkedList<String>(); + 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<String> 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<PackageInfo> 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<PackageInfo> themesToUpdate) { + for (PackageInfo themeInfo : themesToUpdate) { + try { + ThemePackageHelper.updatePackage(getContext(), themeInfo.packageName); + } catch (NameNotFoundException e) { + Log.e(TAG, "Unable to update theme " + themeInfo.packageName, e); + } + } + } + } + +} |