/* * Copyright (C) 2016 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.cyanogenmod.internal.util; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.ContextWrapper; import android.content.IntentFilter; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageParser; import android.content.res.AssetManager; import android.content.res.ThemeConfig; import android.database.Cursor; import android.media.RingtoneManager; import android.net.Uri; import android.os.FileUtils; import android.os.SystemProperties; import android.provider.MediaStore; import android.text.TextUtils; import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.Log; import android.view.WindowManager; import cyanogenmod.providers.CMSettings; import cyanogenmod.providers.ThemesContract.ThemesColumns; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.CRC32; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import static android.content.res.ThemeConfig.SYSTEM_DEFAULT; /** * @hide */ public class ThemeUtils { private static final String TAG = ThemeUtils.class.getSimpleName(); // Package name for any app which does not have a specific theme applied private static final String DEFAULT_PKG = "default"; private static final Set SUPPORTED_THEME_COMPONENTS = new ArraySet<>(); static { SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_ALARMS); SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_BOOT_ANIM); SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_FONTS); SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_ICONS); SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_LAUNCHER); SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_LIVE_LOCK_SCREEN); SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_LOCKSCREEN); SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_NAVIGATION_BAR); SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_NOTIFICATIONS); SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_OVERLAYS); SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_RINGTONES); SUPPORTED_THEME_COMPONENTS.add(ThemesColumns.MODIFIES_STATUS_BAR); } // Constants for theme change broadcast public static final String ACTION_THEME_CHANGED = "org.cyanogenmod.intent.action.THEME_CHANGED"; public static final String EXTRA_COMPONENTS = "components"; public static final String EXTRA_REQUEST_TYPE = "request_type"; public static final String EXTRA_UPDATE_TIME = "update_time"; // path to asset lockscreen and wallpapers directory public static final String LOCKSCREEN_WALLPAPER_PATH = "lockscreen"; public static final String WALLPAPER_PATH = "wallpapers"; // path to external theme resources, i.e. bootanimation.zip public static final String SYSTEM_THEME_PATH = "/data/system/theme"; public static final String SYSTEM_THEME_FONT_PATH = SYSTEM_THEME_PATH + File.separator + "fonts"; public static final String SYSTEM_THEME_RINGTONE_PATH = SYSTEM_THEME_PATH + File.separator + "ringtones"; public static final String SYSTEM_THEME_NOTIFICATION_PATH = SYSTEM_THEME_PATH + File.separator + "notifications"; public static final String SYSTEM_THEME_ALARM_PATH = SYSTEM_THEME_PATH + File.separator + "alarms"; public static final String SYSTEM_THEME_ICON_CACHE_DIR = SYSTEM_THEME_PATH + File.separator + "icons"; // internal path to bootanimation.zip inside theme apk public static final String THEME_BOOTANIMATION_PATH = "assets/bootanimation/bootanimation.zip"; public static final String SYSTEM_MEDIA_PATH = "/system/media/audio"; public static final String SYSTEM_ALARMS_PATH = SYSTEM_MEDIA_PATH + File.separator + "alarms"; public static final String SYSTEM_RINGTONES_PATH = SYSTEM_MEDIA_PATH + File.separator + "ringtones"; public static final String SYSTEM_NOTIFICATIONS_PATH = SYSTEM_MEDIA_PATH + File.separator + "notifications"; private static final String MEDIA_CONTENT_URI = "content://media/internal/audio/media"; public static final int SYSTEM_TARGET_API = 0; /* Path to cached theme resources */ public static final String RESOURCE_CACHE_DIR = "/data/resource-cache/"; /* Path inside a theme APK to the overlay folder */ public static final String OVERLAY_PATH = "assets/overlays/"; public static final String ICONS_PATH = "assets/icons/"; public static final String COMMON_RES_PATH = "assets/overlays/common/"; public static final String IDMAP_SUFFIX = "@idmap"; public static final String COMMON_RES_TARGET = "common"; public static final String ICON_HASH_FILENAME = "hash"; public static final String FONT_XML = "fonts.xml"; public static String getDefaultThemePackageName(Context context) { final String defaultThemePkg = CMSettings.Secure.getString(context.getContentResolver(), CMSettings.Secure.DEFAULT_THEME_PACKAGE); if (!TextUtils.isEmpty(defaultThemePkg)) { PackageManager pm = context.getPackageManager(); try { if (pm.getPackageInfo(defaultThemePkg, 0) != null) { return defaultThemePkg; } } catch (PackageManager.NameNotFoundException e) { // doesn't exist so system will be default Log.w(TAG, "Default theme " + defaultThemePkg + " not found", e); } } return SYSTEM_DEFAULT; } /** * Returns a mutable list of all theme components * @return */ public static List getAllComponents() { List components = new ArrayList<>(SUPPORTED_THEME_COMPONENTS.size()); components.addAll(SUPPORTED_THEME_COMPONENTS); return components; } /** * Returns a mutable list of all the theme components supported by a given package * NOTE: This queries the themes content provider. If there isn't a provider installed * or if it is too early in the boot process this method will not work. */ public static List getSupportedComponents(Context context, String pkgName) { List supportedComponents = new ArrayList<>(); String selection = ThemesColumns.PKG_NAME + "= ?"; String[] selectionArgs = new String[]{ pkgName }; Cursor c = context.getContentResolver().query(ThemesColumns.CONTENT_URI, null, selection, selectionArgs, null); if (c != null) { if (c.moveToFirst()) { List allComponents = getAllComponents(); for (String component : allComponents) { int index = c.getColumnIndex(component); if (c.getInt(index) == 1) { supportedComponents.add(component); } } } c.close(); } return supportedComponents; } /** * Get the components from the default theme. If the default theme is not SYSTEM then any * components that are not in the default theme will come from SYSTEM to create a complete * component map. * @param context * @return */ public static Map getDefaultComponents(Context context) { String defaultThemePkg = getDefaultThemePackageName(context); List defaultComponents = null; List systemComponents = getSupportedComponents(context, SYSTEM_DEFAULT); if (!DEFAULT_PKG.equals(defaultThemePkg)) { defaultComponents = getSupportedComponents(context, defaultThemePkg); } Map componentMap = new HashMap<>(systemComponents.size()); if (defaultComponents != null) { for (String component : defaultComponents) { componentMap.put(component, defaultThemePkg); } } for (String component : systemComponents) { if (!componentMap.containsKey(component)) { componentMap.put(component, SYSTEM_DEFAULT); } } return componentMap; } /** * Get the path to the icons for the given theme * @param pkgName * @return */ public static String getIconPackDir(String pkgName) { return getOverlayResourceCacheDir(pkgName) + File.separator + "icons"; } public static String getIconHashFile(String pkgName) { return getIconPackDir(pkgName) + File.separator + ICON_HASH_FILENAME; } public static String getIconPackApkPath(String pkgName) { return getIconPackDir(pkgName) + "/resources.apk"; } public static String getIconPackResPath(String pkgName) { return getIconPackDir(pkgName) + "/resources.arsc"; } public static String getIdmapPath(String targetPkgName, String overlayPkgName) { return getTargetCacheDir(targetPkgName, overlayPkgName) + File.separator + "idmap"; } public static String getOverlayPathToTarget(String targetPkgName) { StringBuilder sb = new StringBuilder(); sb.append(OVERLAY_PATH); sb.append(targetPkgName); sb.append('/'); return sb.toString(); } public static String getCommonPackageName(String themePackageName) { if (TextUtils.isEmpty(themePackageName)) return null; return COMMON_RES_TARGET; } /** * Create SYSTEM_THEME_PATH directory if it does not exist */ public static void createThemeDirIfNotExists() { createDirIfNotExists(SYSTEM_THEME_PATH); } /** * Create SYSTEM_FONT_PATH directory if it does not exist */ public static void createFontDirIfNotExists() { createDirIfNotExists(SYSTEM_THEME_FONT_PATH); } /** * Create SYSTEM_THEME_RINGTONE_PATH directory if it does not exist */ public static void createRingtoneDirIfNotExists() { createDirIfNotExists(SYSTEM_THEME_RINGTONE_PATH); } /** * Create SYSTEM_THEME_NOTIFICATION_PATH directory if it does not exist */ public static void createNotificationDirIfNotExists() { createDirIfNotExists(SYSTEM_THEME_NOTIFICATION_PATH); } /** * Create SYSTEM_THEME_ALARM_PATH directory if it does not exist */ public static void createAlarmDirIfNotExists() { createDirIfNotExists(SYSTEM_THEME_ALARM_PATH); } /** * Create SYSTEM_THEME_ICON_CACHE_DIR directory if it does not exist */ public static void createIconCacheDirIfNotExists() { createDirIfNotExists(SYSTEM_THEME_ICON_CACHE_DIR); } public static void createCacheDirIfNotExists() throws IOException { File file = new File(RESOURCE_CACHE_DIR); if (!file.exists() && !file.mkdir()) { throw new IOException("Could not create dir: " + file.toString()); } FileUtils.setPermissions(file, FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH, -1, -1); } public static void createResourcesDirIfNotExists(String targetPkgName, String overlayPkgName) throws IOException { createDirIfNotExists(getOverlayResourceCacheDir(overlayPkgName)); File file = new File(getTargetCacheDir(targetPkgName, overlayPkgName)); if (!file.exists() && !file.mkdir()) { throw new IOException("Could not create dir: " + file.toString()); } FileUtils.setPermissions(file, FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH, -1, -1); } public static void createIconDirIfNotExists(String pkgName) throws IOException { createDirIfNotExists(getOverlayResourceCacheDir(pkgName)); File file = new File(getIconPackDir(pkgName)); if (!file.exists() && !file.mkdir()) { throw new IOException("Could not create dir: " + file.toString()); } FileUtils.setPermissions(file, FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH, -1, -1); } public static void clearIconCache() { FileUtils.deleteContents(new File(SYSTEM_THEME_ICON_CACHE_DIR)); } public static void registerThemeChangeReceiver(final Context context, final BroadcastReceiver receiver) { IntentFilter filter = new IntentFilter(ACTION_THEME_CHANGED); context.registerReceiver(receiver, filter); } public static String getLockscreenWallpaperPath(AssetManager assetManager) throws IOException { String[] assets = assetManager.list(LOCKSCREEN_WALLPAPER_PATH); String asset = getFirstNonEmptyAsset(assets); if (asset == null) return null; return LOCKSCREEN_WALLPAPER_PATH + File.separator + asset; } public static String getWallpaperPath(AssetManager assetManager) throws IOException { String[] assets = assetManager.list(WALLPAPER_PATH); String asset = getFirstNonEmptyAsset(assets); if (asset == null) return null; return WALLPAPER_PATH + File.separator + asset; } public static List getWallpaperPathList(AssetManager assetManager) throws IOException { List wallpaperList = new ArrayList(); String[] assets = assetManager.list(WALLPAPER_PATH); for (String asset : assets) { if (!TextUtils.isEmpty(asset)) { wallpaperList.add(WALLPAPER_PATH + File.separator + asset); } } return wallpaperList; } /** * Get the root path of the resource cache for the given theme * @param themePkgName * @return Root resource cache path for the given theme */ public static String getOverlayResourceCacheDir(String themePkgName) { return RESOURCE_CACHE_DIR + themePkgName; } /** * Get the path of the resource cache for the given target and theme * @param targetPkgName * @param themePkg * @return Path to the resource cache for this target and theme */ public static String getTargetCacheDir(String targetPkgName, PackageInfo themePkg) { return getTargetCacheDir(targetPkgName, themePkg.packageName); } public static String getTargetCacheDir(String targetPkgName, PackageParser.Package themePkg) { return getTargetCacheDir(targetPkgName, themePkg.packageName); } public static String getTargetCacheDir(String targetPkgName, String themePkgName) { return getOverlayResourceCacheDir(themePkgName) + File.separator + targetPkgName; } /** * Creates a theme'd context using the overlay applied to SystemUI * @param context Base context * @return Themed context */ public static Context createUiContext(final Context context) { try { Context uiContext = context.createPackageContext("com.android.systemui", Context.CONTEXT_RESTRICTED); return new ThemedUiContext(uiContext, context.getApplicationContext()); } catch (PackageManager.NameNotFoundException e) { } return null; } /** * Scale the boot animation to better fit the device by editing the desc.txt found * in the bootanimation.zip * @param context Context to use for getting an instance of the WindowManager * @param input InputStream of the original bootanimation.zip * @param dst Path to store the newly created bootanimation.zip * @throws IOException */ public static void copyAndScaleBootAnimation(Context context, InputStream input, String dst) throws IOException { final OutputStream os = new FileOutputStream(dst); final ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(os)); final ZipInputStream bootAni = new ZipInputStream(new BufferedInputStream(input)); ZipEntry ze; zos.setMethod(ZipOutputStream.STORED); final byte[] bytes = new byte[4096]; int len; while ((ze = bootAni.getNextEntry()) != null) { ZipEntry entry = new ZipEntry(ze.getName()); entry.setMethod(ZipEntry.STORED); entry.setCrc(ze.getCrc()); entry.setSize(ze.getSize()); entry.setCompressedSize(ze.getSize()); if (!ze.getName().equals("desc.txt")) { // just copy this entry straight over into the output zip zos.putNextEntry(entry); while ((len = bootAni.read(bytes)) > 0) { zos.write(bytes, 0, len); } } else { String line; BufferedReader reader = new BufferedReader(new InputStreamReader(bootAni)); final String[] info = reader.readLine().split(" "); int scaledWidth; int scaledHeight; WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getRealMetrics(dm); // just in case the device is in landscape orientation we will // swap the values since most (if not all) animations are portrait if (dm.widthPixels > dm.heightPixels) { scaledWidth = dm.heightPixels; scaledHeight = dm.widthPixels; } else { scaledWidth = dm.widthPixels; scaledHeight = dm.heightPixels; } int width = Integer.parseInt(info[0]); int height = Integer.parseInt(info[1]); if (width == height) scaledHeight = scaledWidth; else { // adjust scaledHeight to retain original aspect ratio float scale = (float)scaledWidth / (float)width; int newHeight = (int)((float)height * scale); if (newHeight < scaledHeight) scaledHeight = newHeight; } CRC32 crc32 = new CRC32(); int size = 0; ByteBuffer buffer = ByteBuffer.wrap(bytes); line = String.format("%d %d %s\n", scaledWidth, scaledHeight, info[2]); buffer.put(line.getBytes()); size += line.getBytes().length; crc32.update(line.getBytes()); while ((line = reader.readLine()) != null) { line = String.format("%s\n", line); buffer.put(line.getBytes()); size += line.getBytes().length; crc32.update(line.getBytes()); } entry.setCrc(crc32.getValue()); entry.setSize(size); entry.setCompressedSize(size); zos.putNextEntry(entry); zos.write(buffer.array(), 0, size); } zos.closeEntry(); } zos.close(); } public static boolean isValidAudible(String fileName) { return (fileName != null && (fileName.endsWith(".mp3") || fileName.endsWith(".ogg"))); } public static boolean setAudible(Context context, File ringtone, int type, String name) { final String path = ringtone.getAbsolutePath(); final String mimeType = name.endsWith(".ogg") ? "audio/ogg" : "audio/mp3"; ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.DATA, path); values.put(MediaStore.MediaColumns.TITLE, name); values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); values.put(MediaStore.MediaColumns.SIZE, ringtone.length()); values.put(MediaStore.Audio.Media.IS_RINGTONE, type == RingtoneManager.TYPE_RINGTONE); values.put(MediaStore.Audio.Media.IS_NOTIFICATION, type == RingtoneManager.TYPE_NOTIFICATION); values.put(MediaStore.Audio.Media.IS_ALARM, type == RingtoneManager.TYPE_ALARM); values.put(MediaStore.Audio.Media.IS_MUSIC, false); Uri uri = MediaStore.Audio.Media.getContentUriForPath(path); Uri newUri = null; Cursor c = context.getContentResolver().query(uri, new String[] {MediaStore.MediaColumns._ID}, MediaStore.MediaColumns.DATA + "='" + path + "'", null, null); if (c != null && c.getCount() > 0) { c.moveToFirst(); long id = c.getLong(0); c.close(); newUri = Uri.withAppendedPath(Uri.parse(MEDIA_CONTENT_URI), "" + id); context.getContentResolver().update(uri, values, MediaStore.MediaColumns._ID + "=" + id, null); } if (newUri == null) newUri = context.getContentResolver().insert(uri, values); try { RingtoneManager.setActualDefaultRingtoneUri(context, type, newUri); } catch (Exception e) { return false; } return true; } public static boolean setDefaultAudible(Context context, int type) { final String audiblePath = getDefaultAudiblePath(type); if (audiblePath != null) { Uri uri = MediaStore.Audio.Media.getContentUriForPath(audiblePath); Cursor c = context.getContentResolver().query(uri, new String[] {MediaStore.MediaColumns._ID}, MediaStore.MediaColumns.DATA + "='" + audiblePath + "'", null, null); if (c != null && c.getCount() > 0) { c.moveToFirst(); long id = c.getLong(0); c.close(); uri = Uri.withAppendedPath( Uri.parse(MEDIA_CONTENT_URI), "" + id); } if (uri != null) RingtoneManager.setActualDefaultRingtoneUri(context, type, uri); } else { return false; } return true; } public static String getDefaultAudiblePath(int type) { final String name; final String path; switch (type) { case RingtoneManager.TYPE_ALARM: name = SystemProperties.get("ro.config.alarm_alert", null); path = name != null ? SYSTEM_ALARMS_PATH + File.separator + name : null; break; case RingtoneManager.TYPE_NOTIFICATION: name = SystemProperties.get("ro.config.notification_sound", null); path = name != null ? SYSTEM_NOTIFICATIONS_PATH + File.separator + name : null; break; case RingtoneManager.TYPE_RINGTONE: name = SystemProperties.get("ro.config.ringtone", null); path = name != null ? SYSTEM_RINGTONES_PATH + File.separator + name : null; break; default: path = null; break; } return path; } public static void clearAudibles(Context context, String audiblePath) { final File audibleDir = new File(audiblePath); if (audibleDir.exists()) { String[] files = audibleDir.list(); final ContentResolver resolver = context.getContentResolver(); for (String s : files) { final String filePath = audiblePath + File.separator + s; Uri uri = MediaStore.Audio.Media.getContentUriForPath(filePath); resolver.delete(uri, MediaStore.MediaColumns.DATA + "=\"" + filePath + "\"", null); (new File(filePath)).delete(); } } } public static InputStream getInputStreamFromAsset(Context ctx, String path) throws IOException { if (ctx == null || path == null) return null; InputStream is; String ASSET_BASE = "file:///android_asset/"; path = path.substring(ASSET_BASE.length()); AssetManager assets = ctx.getAssets(); is = assets.open(path); return is; } /** * Convenience method to determine if a theme component is a per app theme and not a standard * component. * @param component * @return */ public static boolean isPerAppThemeComponent(String component) { return !(DEFAULT_PKG.equals(component) || ThemeConfig.SYSTEMUI_STATUS_BAR_PKG.equals(component) || ThemeConfig.SYSTEMUI_NAVBAR_PKG.equals(component)); } /** * Returns the first non-empty asset name. Empty assets can occur if the APK is built * with folders included as zip entries in the APK. Searching for files inside "folderName" via * assetManager.list("folderName") can cause these entries to be included as empty strings. * @param assets * @return */ private static String getFirstNonEmptyAsset(String[] assets) { if (assets == null) return null; String filename = null; for(String asset : assets) { if (!TextUtils.isEmpty(asset)) { filename = asset; break; } } return filename; } private static boolean dirExists(String dirPath) { final File dir = new File(dirPath); return dir.exists() && dir.isDirectory(); } private static void createDirIfNotExists(String dirPath) { if (!dirExists(dirPath)) { File dir = new File(dirPath); if (dir.mkdir()) { FileUtils.setPermissions(dir, FileUtils.S_IRWXU | FileUtils.S_IRWXG| FileUtils.S_IROTH | FileUtils.S_IXOTH, -1, -1); } } } private static class ThemedUiContext extends ContextWrapper { private Context mAppContext; public ThemedUiContext(Context context, Context appContext) { super(context); mAppContext = appContext; } @Override public Context getApplicationContext() { return mAppContext; } @Override public String getPackageName() { return mAppContext.getPackageName(); } } }