summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAndy Mast <andy@cyngn.com>2014-05-01 14:41:43 -0700
committerAndy Mast <andy@cyngn.com>2014-05-06 14:08:44 -0700
commitd0f415ae26b2e35508adacc920c8ea4ae809ccce (patch)
treefded3b630cb39e8c35b06fe5c49d441a2800b399 /src
parent27c5437e4a2bd567937b2b6ebcdd25e31aecd339 (diff)
downloadandroid_packages_providers_ThemesProvider-d0f415ae26b2e35508adacc920c8ea4ae809ccce.zip
android_packages_providers_ThemesProvider-d0f415ae26b2e35508adacc920c8ea4ae809ccce.tar.gz
android_packages_providers_ThemesProvider-d0f415ae26b2e35508adacc920c8ea4ae809ccce.tar.bz2
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
Diffstat (limited to 'src')
-rw-r--r--src/org/cyanogenmod/themes/provider/AppReceiver.java46
-rw-r--r--src/org/cyanogenmod/themes/provider/CopyImageService.java161
-rw-r--r--src/org/cyanogenmod/themes/provider/ThemePackageHelper.java273
-rw-r--r--src/org/cyanogenmod/themes/provider/ThemesOpenHelper.java160
-rw-r--r--src/org/cyanogenmod/themes/provider/ThemesProvider.java389
5 files changed, 1029 insertions, 0 deletions
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);
+ }
+ }
+ }
+ }
+
+}