diff options
Diffstat (limited to 'src/org/cyanogenmod/theme/util/Utils.java')
-rw-r--r-- | src/org/cyanogenmod/theme/util/Utils.java | 724 |
1 files changed, 724 insertions, 0 deletions
diff --git a/src/org/cyanogenmod/theme/util/Utils.java b/src/org/cyanogenmod/theme/util/Utils.java new file mode 100644 index 0000000..94afecb --- /dev/null +++ b/src/org/cyanogenmod/theme/util/Utils.java @@ -0,0 +1,724 @@ +/* + * Copyright (C) 2016 Cyanogen, Inc. + * 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.theme.util; + +import android.app.ActivityManager; +import android.app.WallpaperManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PermissionInfo; +import android.content.pm.ResolveInfo; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.ThemeConfig; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.RemoteException; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; +import android.view.IWindowManager; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; + +import org.cyanogenmod.theme.chooser.ChooserActivity; + +import cyanogenmod.externalviews.KeyguardExternalView; +import cyanogenmod.providers.CMSettings; +import cyanogenmod.providers.ThemesContract; + +import org.cyanogenmod.internal.util.ThemeUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.security.InvalidParameterException; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static android.content.res.ThemeConfig.SYSTEM_DEFAULT; + +public class Utils { + private static final String TAG = Utils.class.getSimpleName(); + private static final boolean DEBUG = false; + + private static final String OVERLAY_BASE_PATH = "overlays" + File.separator; + + public static Bitmap decodeFile(String path, int reqWidth, int reqHeight) { + BitmapFactory.Options opts = new BitmapFactory.Options(); + + // Determine insample size + opts.inJustDecodeBounds = true; + BitmapFactory.decodeFile(path, opts); + opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight); + + // Decode the bitmap, regionally if necessary + Bitmap bitmap = null; + opts.inJustDecodeBounds = false; + Rect rect = getCropRectIfNecessary(opts, reqWidth, reqHeight); + try { + if (rect != null) { + BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(path, false); + // Check if we can downsample more now that we cropped + opts.inSampleSize = calculateInSampleSize(rect.width(), rect.height(), + reqWidth, reqHeight); + bitmap = decoder.decodeRegion(rect, opts); + } else { + bitmap = BitmapFactory.decodeFile(path, opts); + } + } catch (IOException e) { + Log.e(TAG, "Unable to open resource in path" + path, e); + } + return bitmap; + } + + public static Bitmap decodeResource(Resources res, int resId, int reqWidth, int reqHeight) { + BitmapFactory.Options opts = new BitmapFactory.Options(); + + // Determine insample size + opts.inJustDecodeBounds = true; + BitmapFactory.decodeResource(res, resId, opts); + opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight); + + // Decode the bitmap, regionally if necessary + Bitmap bitmap = null; + opts.inJustDecodeBounds = false; + Rect rect = getCropRectIfNecessary(opts, reqWidth, reqHeight); + + InputStream stream = null; + try { + if (rect != null) { + stream = res.openRawResource(resId, new TypedValue()); + if (stream == null) return null; + BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(stream, false); + // Check if we can downsample a little more now that we cropped + opts.inSampleSize = calculateInSampleSize(rect.width(), rect.height(), + reqWidth, reqHeight); + bitmap = decoder.decodeRegion(rect, opts); + } else { + bitmap = BitmapFactory.decodeResource(res, resId, opts); + } + } catch (IOException e) { + Log.e(TAG, "Unable to open resource " + resId, e); + } finally { + closeQuiet(stream); + } + return bitmap; + } + + + public static Bitmap getBitmapFromAsset(Context ctx, String path,int reqWidth, int reqHeight) { + if (ctx == null || path == null) + return null; + + String ASSET_BASE = "file:///android_asset/"; + path = path.substring(ASSET_BASE.length()); + + + Bitmap bitmap = null; + try { + AssetManager assets = ctx.getAssets(); + InputStream is = assets.open(path); + + // Determine insample size + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, opts); + opts.inSampleSize = calculateInSampleSize(opts, reqWidth, reqHeight); + is.close(); + + // Decode the bitmap, regionally if neccessary + is = assets.open(path); + opts.inJustDecodeBounds = false; + Rect rect = getCropRectIfNecessary(opts, reqWidth, reqHeight); + if (rect != null) { + BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); + // Check if we can downsample a little more now that we cropped + opts.inSampleSize = calculateInSampleSize(rect.width(), rect.height(), + reqWidth, reqHeight); + bitmap = decoder.decodeRegion(rect, opts); + } else { + bitmap = BitmapFactory.decodeStream(is); + } + } catch (IOException e) { + e.printStackTrace(); + } + return bitmap; + } + + + /** + * For excessively large images with an awkward ratio we + * will want to crop them + * @return + */ + public static Rect getCropRectIfNecessary( + BitmapFactory.Options options,int reqWidth, int reqHeight) { + Rect rect = null; + // Determine downsampled size + int width = options.outWidth / options.inSampleSize; + int height = options.outHeight / options.inSampleSize; + + if ((reqHeight * 1.5 < height)) { + int bottom = height/ 4; + int top = bottom + height/2; + rect = new Rect(0, bottom, width, top); + } else if ((reqWidth * 1.5 < width)) { + int left = width / 4; + int right = left + height/2; + rect = new Rect(left, 0, right, height); + } + return rect; + } + + public static int calculateInSampleSize( + BitmapFactory.Options options, int reqWidth, int reqHeight) { + return calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight); + } + + // Modified from original source: + // http://developer.android.com/training/displaying-bitmaps/load-bitmap.html + public static int calculateInSampleSize( + int decodeWidth, int decodeHeight, int reqWidth, int reqHeight) { + // Raw height and width of image + int inSampleSize = 1; + + if (decodeHeight > reqHeight || decodeWidth > reqWidth) { + final int halfHeight = decodeHeight / 2; + final int halfWidth = decodeWidth / 2; + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while ((halfHeight / inSampleSize) > reqHeight && + (halfWidth / inSampleSize) > reqWidth) { + inSampleSize *= 2; + } + } + + return inSampleSize; + } + + public static InputStream getInputStreamFromAsset( + Context ctx, String path) throws IOException { + if (ctx == null || path == null) + return null; + InputStream is = null; + String ASSET_BASE = "file:///android_asset/"; + path = path.substring(ASSET_BASE.length()); + AssetManager assets = ctx.getAssets(); + is = assets.open(path); + return is; + } + + public static void copy(InputStream is, OutputStream os) throws IOException { + final byte[] bytes = new byte[4096]; + int len; + while ((len = is.read(bytes)) > 0) { + os.write(bytes, 0, len); + } + } + + public static void closeQuiet(InputStream stream) { + if (stream == null) + return; + try { + stream.close(); + } catch (IOException e) { + } + } + + public static void closeQuiet(OutputStream stream) { + if (stream == null) + return; + try { + stream.close(); + } catch (IOException e) { + } + } + + //Note: will not delete populated subdirs + public static void deleteFilesInDir(String dirPath) { + File fontDir = new File(dirPath); + File[] files = fontDir.listFiles(); + if (files != null) { + for(File file : fontDir.listFiles()) { + file.delete(); + } + } + } + + public static boolean hasNavigationBar(Context context) { + boolean needsNavigationBar = false; + try { + IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); + needsNavigationBar = wm.needsNavigationBar(); + } catch (RemoteException e) { + } + // Need to also check for devices with hardware keys where the user has chosen to use + // the on screen navigation bar + needsNavigationBar = needsNavigationBar || + CMSettings.Secure.getInt(context.getContentResolver(), + CMSettings.Secure.DEV_FORCE_SHOW_NAVBAR, 0) == 1; + return needsNavigationBar; + } + + public static Bitmap loadBitmapBlob(Cursor cursor, int columnIdx) { + if (columnIdx < 0) { + Log.w(TAG, "loadBitmapBlob(): Invalid index provided, returning null"); + return null; + } + + if (cursor.getType(columnIdx) == Cursor.FIELD_TYPE_STRING) { + return loadBitmapFile(cursor, columnIdx); + } + + byte[] blob = cursor.getBlob(columnIdx); + if (blob == null) return null; + return BitmapFactory.decodeByteArray(blob, 0, blob.length); + } + + public static Bitmap loadBitmapFile(Cursor cursor, int columnIdx) { + if (columnIdx < 0) { + Log.w(TAG, "loadBitmapFile(): Invalid index provided, returning null"); + return null; + } + String path = cursor.getString(columnIdx); + if (TextUtils.isEmpty(path)) { + return null; + } + + Bitmap image = null; + FileInputStream inputStream; + try { + inputStream = new FileInputStream(path); + image = BitmapFactory.decodeStream(inputStream); + inputStream.close(); + } catch (Exception e) { + Log.w(TAG, "Unable to open preview " + path, e); + } + + return image; + } + + public static String getBatteryIndex(int type) { + switch(type) { + case 2: + return ThemesContract.PreviewColumns.STATUSBAR_BATTERY_CIRCLE; + case 5: + return ThemesContract.PreviewColumns.STATUSBAR_BATTERY_LANDSCAPE; + default: + return ThemesContract.PreviewColumns.STATUSBAR_BATTERY_PORTRAIT; + } + } + + public static Bitmap getRegularWallpaperBitmap(Context context) { + WallpaperManager wm = WallpaperManager.getInstance(context); + + Bitmap bitmap = null; + // desktop wallpaper here + Bitmap wallpaper = wm.getBitmap(); + if (wallpaper == null) { + return null; + } + + Point size = new Point(); + WindowManager windowManager = + (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + windowManager.getDefaultDisplay().getRealSize(size); + + final int dw = size.x; + final int dh = size.y; + + // Center the scaled image + float scale = Math.max(1f, Math.max(dw / (float) wallpaper.getWidth(), + dh / (float) wallpaper.getHeight())); + + final int scaledWidth = Math.round((wallpaper.getWidth() * scale)); + final int scaledHeight = Math.round((wallpaper.getHeight() * scale)); + + // TODO: set xOffset to wm.getLastWallpaperX() once available + int xOffset = wm.getLastWallpaperX(); + // x offset + if (xOffset == -1) { + xOffset = 0; + } else { + xOffset *= -1; + } + + // y offsets + // TODO: set yOffset to wm.getLastWallpaperY() once available + int yOffset = wm.getLastWallpaperY(); + if (yOffset == -1) { + yOffset = 0; + } else { + yOffset *= -1; + } + + if (DEBUG) { + Log.d(TAG, "scale: " + scale); + Log.d(TAG, "scaledWidth: " + scaledWidth); + Log.d(TAG, "scaledHeight: " + scaledHeight); + Log.d(TAG, "wallpaper size: width: " + wallpaper.getWidth() + + ", height: " + wallpaper.getHeight()); + Log.d(TAG, "xOffset: " + xOffset); + Log.d(TAG, "yOffset: " + yOffset); + } + + try { + if (wallpaper.getHeight() < dh) { + // need to scale it up vertically + + if (wallpaper.getHeight() > wallpaper.getWidth()) { + // handle portrait wallpaper + float diff = scaledWidth - dw; + int diffhalf = Math.round(diff / 2); + + bitmap = Bitmap.createScaledBitmap(wallpaper, scaledWidth, scaledHeight, true); + bitmap = Bitmap.createBitmap(bitmap, diffhalf, 0, dw, dh); + bitmap = Bitmap.createBitmap(bitmap, xOffset, 0, dw, dh); + } else { + int goldenWidth = Math.round(wallpaper.getHeight() * 1.125f); + int spaceA = (wallpaper.getWidth() - goldenWidth) / 2; + int spaceB = (goldenWidth - Math.round(dh / scale)) / 2; + + bitmap = Bitmap.createBitmap(wallpaper, spaceA, 0, goldenWidth, + wallpaper.getHeight()); + int left = spaceB + Math.round(xOffset / scale); + bitmap = Bitmap.createBitmap(bitmap, left, 0, Math.round(dw / scale), + Math.round(dh / scale)); + } + + } else if (wallpaper.getWidth() < dw) { + // need to scale it up horizontally + + if (wallpaper.getHeight() > wallpaper.getWidth()) { + // handle portrait wallpaper + return wallpaper; + + } else { + // handle landscape wallpaper + float diff = wallpaper.getHeight() - wallpaper.getWidth(); + int diffhalf = Math.round(diff / 2); + + if (diffhalf < 0) { + return wallpaper; + } + + bitmap = Bitmap.createBitmap( + wallpaper, diffhalf, 0, + wallpaper.getWidth(), wallpaper.getWidth()); + + // blow it up + bitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledWidth, true); + + bitmap = Bitmap.createBitmap(bitmap, 0, 0, dw, dh); + } + + } else { + // sometimes the wallpaper manager gives incorrect offsets, + // and adds like 200 pixels randomly. If it's bigger than we can handle, calculate + // our own :) + if (yOffset + dh > wallpaper.getHeight()) { + yOffset = (wallpaper.getHeight() - dh) / 2; + } + if (xOffset + dw > wallpaper.getWidth()) { + yOffset = (wallpaper.getWidth() - dw) / 2; + } + bitmap = Bitmap.createBitmap(wallpaper, xOffset, yOffset, dw, dh); + } + } catch (IllegalArgumentException e) { + // Cropping/resizing failed so return the original + bitmap = wallpaper; + } + return bitmap; + } + + public static boolean isRecentTaskHome(Context context) { + final ActivityManager am = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + + final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasks( + 2, ActivityManager.RECENT_IGNORE_UNAVAILABLE); + if (recentTasks.size() > 1) { + ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(1); + + Intent intent = new Intent(recentInfo.baseIntent); + if (recentInfo.origActivity != null) { + intent.setComponent(recentInfo.origActivity); + } + + // Now check if this recent task is a launcher + if (isCurrentHomeActivity(context, intent.getComponent())) { + return true; + } + } + return false; + } + + public static boolean isRecentTaskThemeStore(Context context) { + final ActivityManager am = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + + final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasks( + 2, ActivityManager.RECENT_IGNORE_UNAVAILABLE); + if (recentTasks.size() > 0) { + ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(0); + + Intent intent = new Intent(recentInfo.baseIntent); + if (recentInfo.origActivity != null) { + intent.setComponent(recentInfo.origActivity); + } + + if (intent.getComponent() + .getPackageName().equals(ChooserActivity.THEME_STORE_PACKAGE)) { + return true; + } + } + return false; + } + + + public static String getTopTaskPackageName(Context context) { + final ActivityManager am = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + final List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasks(1, 0); + if (recentTasks.size() > 0) { + ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(0); + if (recentInfo.origActivity != null) { + return recentInfo.origActivity.getPackageName(); + } + if (recentInfo.baseIntent != null) { + return recentInfo.baseIntent.getComponent().getPackageName(); + } + } + return null; + } + + public static boolean hasPerAppThemesApplied(Context context) { + final Configuration config = context.getResources().getConfiguration(); + final ThemeConfig themeConfig = config != null ? config.themeConfig : null; + if (themeConfig != null) { + Map<String, ThemeConfig.AppTheme> themes = themeConfig.getAppThemes(); + for (String appPkgName : themes.keySet()) { + if (ThemeUtils.isPerAppThemeComponent(appPkgName)) { + return true; + } + } + } + return false; + } + + /** + * Method to identify if a theme explicitly overlays a particular app. Explicit is defined + * as having files in overlays/appPkgName/ + * @param context + * @param appPkgNane + * @param themePkgName + * @return + */ + public static boolean themeHasOverlayForApp(Context context, String appPkgNane, + String themePkgName) { + boolean hasExplicitOverlay = false; + if (ThemeConfig.SYSTEM_DEFAULT.equals(themePkgName)) { + hasExplicitOverlay = true; + } else { + try { + Context themeContext = context.createPackageContext(themePkgName, 0); + if (themeContext != null) { + AssetManager assets = themeContext.getAssets(); + String[] files = assets.list(OVERLAY_BASE_PATH + appPkgNane); + if (files != null && files.length > 0) hasExplicitOverlay = true; + } + } catch (Exception e) { + // don't care, we'll return false and let the caller handle things + } + } + return hasExplicitOverlay; + } + + private static boolean isCurrentHomeActivity(Context context, + ComponentName component) { + final PackageManager pm = context.getPackageManager(); + ActivityInfo homeInfo = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) + .resolveActivityInfo(pm, 0); + + return homeInfo != null + && homeInfo.packageName.equals(component.getPackageName()) + && homeInfo.name.equals(component.getClassName()); + } + + /** + * Returns the resource-IDs for all attributes specified in the given + * <declare-styleable>-resource tag as an int array. + * stackoverflow.com/questions/13816596/accessing-declare-styleable-resources-programatically + * + * @param name + * @return + */ + public static final int[] getResourceDeclareStyleableIntArray(String pkgName, String name) { + try { + //use reflection to access the resource class + Field[] fields2 = + Class.forName(pkgName + ".R$styleable").getFields(); + + //browse all fields + for (Field f : fields2) { + //pick matching field + if (f.getName().equals(name)) { + //return as int array + int[] ret = (int[])f.get(null); + return ret; + } + } + } + catch (Throwable t) { + } + + return null; + } + + /** + * Retrieves the list of dangerous permissions not granted to the supplied package. This method + * is not capable of identifying if a given permission was previously revoked by the user or + * if the user decided not to be asked again. + * + * @param context + * @param pkgName + * @return Returns an array of Strings with the name of the permissions. An empty array will + * be returned if all dangerous permissions have been already granted. + */ + public static String[] getDangerousPermissionsNotGranted(Context context, String pkgName) + throws InvalidParameterException { + LinkedList<String> permissionsNotGranted = new LinkedList<String>(); + PackageInfo pkgInfo = null; + PackageManager pm = context.getPackageManager(); + try { + pkgInfo = pm.getPackageInfo(pkgName, PackageManager.GET_PERMISSIONS); + } catch (PackageManager.NameNotFoundException e) { + throw new InvalidParameterException("Package " + pkgName + " not found"); + } + + String[] requestedPermissions = pkgInfo.requestedPermissions; + int[] requestedPermissionsFlags = pkgInfo.requestedPermissionsFlags; + + for (int indx = 0; indx < requestedPermissions.length; indx++) { + if ((requestedPermissionsFlags[indx] & PackageInfo.REQUESTED_PERMISSION_GRANTED) == 0) { + try { + PermissionInfo pi = pm.getPermissionInfo(requestedPermissions[indx],0); + + if (pi.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS) { + permissionsNotGranted.add(requestedPermissions[indx]); + if (DEBUG) { + Log.d(TAG, "Permission " + requestedPermissions[indx] + "not granted"); + } + } + } catch(PackageManager.NameNotFoundException e) { + //If package manager doesn't know of the permission we just continue since + //this permission won't end up in the list + } + } + } + return permissionsNotGranted.toArray(new String[permissionsNotGranted.size()]); + } + + /** + * Builds a ComponentName to identify the activity associated with the + * KeyguardExternalView.CATEGORY_KEYGUARD_GRANT_PERMISSION category in the given package + * @param context + * @param packageName + * @return Returns the ComponentName or null if no activity was found. + */ + private static ComponentName getPermissionGranterComponentName(Context context, + String packageName) { + Intent rIntent = new Intent() + .setPackage(packageName) + .addCategory(KeyguardExternalView.CATEGORY_KEYGUARD_GRANT_PERMISSION); + + List<ResolveInfo> resolveInfo = context.getPackageManager(). + queryIntentActivities(rIntent, PackageManager.GET_RESOLVED_FILTER); + + if (resolveInfo.size() >= 1) { + if (DEBUG) { + if (resolveInfo.size() >= 2) { + Log.w(TAG, "Got " + resolveInfo.size() + " resolvers! Defaulting to " + + resolveInfo.get(0).activityInfo.name); + } + } + } + return (resolveInfo.size() >=1 ) ? + new ComponentName(packageName, resolveInfo.get(0).activityInfo.name) : + null; + } + + /** + * Builds an intent used to request the user to grant or revoke the supplied permissions. + * The intent will set the KeyguardExternalView.CATEGORY_KEYGUARD_GRANT_PERMISSION + * category and an extra containing the list of permissions identified by + * KeyguardExternalView.EXTRA_PERMISSION_LIST + * @param context + * @param packageName + * @param permissionList + * @return Returns the intent if an activity associated with + * KeyguardExternalView.CATEGORY_KEYGUARD_GRANT_PERMISSION category was found. Otherwise, it + * returns null + */ + public static Intent buildPermissionGrantRequestIntent(Context context, String packageName, + String[] permissionList) { + ComponentName componentName = getPermissionGranterComponentName(context, packageName); + if (componentName == null) return null; + + Intent permissionIntent = new Intent() + .setComponent(componentName) + .addCategory(KeyguardExternalView.CATEGORY_KEYGUARD_GRANT_PERMISSION) + .setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS|Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(KeyguardExternalView.EXTRA_PERMISSION_LIST, permissionList); + return permissionIntent; + } + + 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; + } +} |