/* * Copyright © 2014 Jeff Corcoran * * This file is part of Hangar. * * Hangar is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Hangar is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Hangar. If not, see <http://www.gnu.org/licenses/>. * */ package ca.mimic.apphangar; import android.annotation.TargetApi; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManager; import android.appwidget.AppWidgetManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.Log; import android.util.TypedValue; import android.widget.RemoteViews; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; public class Tools { final static String TAG = "Apphangar"; // 15 min buffer for the timestamp final static int USAGE_STATS_QUERY_TIMEBUFFER = 900000; // Query usage stats this amount of time final static int USAGE_STATS_QUERY_TIMEFRAME = 46800000; final static String USAGE_STATS_SERVICE_NAME = "usagestats"; final static int AWAKE_REFRESH = 60000; final static String REFRESH_ACTION = "ca.mimic.hangar.SCREEN_ON_REFRESH"; final static String REPLACE_ACTION = "android.intent.action.PACKAGE_REPLACED"; final static String BOOT_ACTION = "android.intent.action.BOOT_COMPLETED"; static int mBackgroundResource; protected static void HangarLog(String message) { if (BuildConfig.BUILD_TYPE.equals("debug")) Log.d(TAG, message); } protected boolean isPinned(Context context, String packageName) { ArrayList<String> appList = getPinned(context); for (String app : appList) { if (app.equals(packageName)) { return true; } } return false; } protected ArrayList<String> getPinned(Context context) { SharedPreferences settingsPrefs = context.getSharedPreferences(context.getPackageName(), Context.MODE_MULTI_PROCESS); String pinnedApps = settingsPrefs.getString(Settings.PINNED_APPS, ""); return new ArrayList<String>(Arrays.asList(pinnedApps.split(" "))); } protected boolean togglePinned(Context context, String packageName, SharedPreferences.Editor settingsEditor) { ArrayList<String> appList = getPinned(context); Boolean removed = false; String pinnedApps = ""; for (String app : appList) { if (app.equals(packageName)) { removed = true; continue; } pinnedApps += app + " "; } if (!removed) { pinnedApps += packageName; } settingsEditor.putString(Settings.PINNED_APPS, pinnedApps.trim()); settingsEditor.commit(); return !removed; } protected static int getViewBackgroundResource () { return mBackgroundResource; } protected static void setViewBackgroundColor (RemoteViews view, int color, int resource) { if (resource != 0) { mBackgroundResource = resource; view.setImageViewResource(R.id.rootBackground, resource); } view.setInt(R.id.rootBackground, "setColorFilter", color); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { view.setInt(R.id.rootBackground, "setImageAlpha", Color.alpha(color)); } else { view.setInt(R.id.rootBackground, "setAlpha", Color.alpha(color)); } } public static String getApplicationName(Context context, String packageName) { final PackageManager pm = context.getPackageManager(); ApplicationInfo ai; try { ai = pm.getApplicationInfo(packageName, 0); } catch (final PackageManager.NameNotFoundException e) { ai = null; } return (String) (ai != null ? pm.getApplicationLabel(ai) : ""); } public static int getUid(Context context, String packageName) { final PackageManager pm = context.getPackageManager(); ApplicationInfo ai; try { ai = pm.getApplicationInfo(packageName, 0); } catch (final PackageManager.NameNotFoundException e) { ai = null; } return (ai != null ? ai.uid : 0); } protected static String getLauncher(Context context) { final Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); final ResolveInfo res = context.getPackageManager().resolveActivity(intent, 0); if (!"android".equals(res.activityInfo.packageName)) { return res.activityInfo.packageName; } return null; } public static int dpToPx(Context context, int dp) { Resources r = context.getResources(); return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()); } protected static void updateWidget(Context mContext) { Intent i = new Intent(mContext, StatsWidget.class); i.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); mContext.sendBroadcast(i); i = new Intent(mContext, AppsWidget.class); i.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); mContext.sendBroadcast(i); } protected static class TaskInfoOrder { int launchOrder; float launchScore; int secondsOrder; float secondsScore; int placeOrder; TaskInfo origTask; TaskInfoOrder(TaskInfo task) { origTask = task; } TaskInfo getOrig() { return origTask; } } protected static class TaskInfo { protected String appName = ""; protected String packageName = ""; protected String className = ""; protected int launches = 0; protected int seconds = 0; protected int totalseconds = 0; TaskInfo (String string) { packageName = string; } } protected static class LollipopTaskInfo { protected String packageName = ""; protected String lastRecentPackageName = ""; protected String className = ""; protected String lastPackageName = ""; protected long lastUsedStamp; protected long timeInFGDelta; protected long timeInFG; LollipopTaskInfo (String string) { packageName = string; } } public static Bitmap drawableToBitmap (Drawable drawable) { if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable)drawable).getBitmap(); } Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } public static boolean isLollipop(boolean exact) { boolean match = exact ? android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP : android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; return match; } @TargetApi(21) public static List<UsageStats> getUsageStats(Context context) { final UsageStatsManager usageStatsManager = (UsageStatsManager)context.getSystemService(USAGE_STATS_SERVICE_NAME); // Context.USAGE_STATS_SERVICE); long time = System.currentTimeMillis(); List<UsageStats> stats = usageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - USAGE_STATS_QUERY_TIMEFRAME, time); if (stats.size() > 1) { Collections.sort(stats, new Tools.UsageStatsComparator()); } return stats; } @TargetApi(21) public static String firstPackage(List<UsageStats> stats) { return stats.get(0).getPackageName(); } @TargetApi(21) public static LollipopTaskInfo parseUsageStats(List<UsageStats> stats, LollipopTaskInfo lollipopTaskInfo) { UsageStats aRunner = stats.get(0); UsageStats bRunner = null; if (lollipopTaskInfo == null) { // setup new lollipopTaskInfo object! lollipopTaskInfo = new Tools.LollipopTaskInfo(aRunner.getPackageName()); } else if (lollipopTaskInfo.packageName.equals(aRunner.getPackageName())) { // Tools.HangarLog("Last package same as current top, skipping! [" + lollipopTaskInfo.packageName + "]"); return lollipopTaskInfo; } // TODO change this to keep track of all usagestats and compare timeinFg deltas // Will need to refactor buildTasks to manage bulk time change to db as well as // new runningTask. for (UsageStats s : stats) { if (s.getPackageName().equals(lollipopTaskInfo.packageName)) { bRunner = s; } } lollipopTaskInfo.lastPackageName = lollipopTaskInfo.packageName; lollipopTaskInfo.packageName = aRunner.getPackageName(); if (bRunner == null) { Tools.HangarLog("Couldn't find previous task [" + lollipopTaskInfo.packageName + "]"); } else { lollipopTaskInfo.timeInFGDelta = (lollipopTaskInfo.timeInFG > 0) ? bRunner.getTotalTimeInForeground() - lollipopTaskInfo.timeInFG : 0; } lollipopTaskInfo.timeInFG = aRunner.getTotalTimeInForeground(); Tools.HangarLog("New [" + lollipopTaskInfo.packageName + "] old [" + lollipopTaskInfo.lastPackageName + "] old FG delta: " + lollipopTaskInfo.timeInFGDelta); return lollipopTaskInfo; } protected static class AppRowComparator implements Comparator<AppsRowItem> { final int TIME_SPENT = 0; final int ALPHABETICAL = 1; final int PINNED = 0; final int BLACKLISTED = 1; final int NONE = 2; int mTopType; int mSortType; AppRowComparator (int topType, int sortType){ mTopType = topType; mSortType = sortType; } @Override public int compare(AppsRowItem r1, AppsRowItem r2) { int firstCompare = 0; switch (mTopType) { case PINNED: firstCompare = r2.getPinned().compareTo(r1.getPinned()); break; case BLACKLISTED: firstCompare = r2.getBlacklisted().compareTo(r1.getBlacklisted()); break; case NONE: break; } if (firstCompare == 0) { switch (mSortType) { case ALPHABETICAL: return r1.getName().compareToIgnoreCase(r2.getName()); case TIME_SPENT: Integer o1 = r1.getSeconds(); Integer o2 = r2.getSeconds(); return o2.compareTo(o1); } } return firstCompare; } } protected static class TasksModelComparator implements Comparator<TasksModel> { String mType = "seconds"; TasksModelComparator(String type) { mType = type; } @Override public int compare(TasksModel t1, TasksModel t2) { Integer o1 = 0; Integer o2 = 0; if (mType.equals("seconds")) { o1 = t1.getSeconds(); o2 = t2.getSeconds(); } int firstCompare = o2.compareTo(o1); if (firstCompare == 0) { return t1.getBlacklisted().compareTo(t2.getBlacklisted()); } return firstCompare; } } @TargetApi(21) protected static class UsageStatsComparator implements Comparator<UsageStats> { Long o1; Long o2; @Override public int compare(UsageStats t1, UsageStats t2) { o1 = t1.getLastTimeUsed(); o2 = t2.getLastTimeUsed(); return o2.compareTo(o1); } } protected static class TaskComparator implements Comparator<TaskInfoOrder> { private String mType; private int weightPriority; private Float numToCompare; private Float baseRecency; public TaskComparator (String type, int weight, int num){ mType = type; weightPriority = weight; Float calNum = num / 10f; baseRecency = (calNum < 2.5) ? 2.5f : calNum; numToCompare = baseRecency + 1.5f; HangarLog("num: " + num + " calNum: " + calNum + " baseRecency: " + baseRecency + " numToCompare: " + numToCompare); } public int compare(TaskInfoOrder c1, TaskInfoOrder c2) { Float a1; Float a2; Float c1p = c1.placeOrder * baseRecency; Float c2p = c2.placeOrder * baseRecency; if (mType.equals("launch")) { a1 = c1.launchScore; a2 = c2.launchScore; } else if (mType.equals("seconds")) { a1 = c1.secondsScore; a2 = c2.secondsScore; } else { switch (weightPriority) { case 1: a1 = (float) c1.secondsOrder + c1.launchOrder + c1.placeOrder * numToCompare; a2 = (float) c2.secondsOrder + c2.launchOrder + c2.placeOrder * numToCompare; break; case 2: a1 = (float) c1.secondsOrder + (c1.launchOrder * numToCompare) + c1p; a2 = (float) c2.secondsOrder + (c2.launchOrder * numToCompare) + c2p; break; case 3: a1 = (c1.secondsOrder * numToCompare) + c1.launchOrder + c1p; a2 = (c2.secondsOrder * numToCompare) + c2.launchOrder + c2p; break; default: a1 = (float) c1.secondsOrder + c1.launchOrder + c1p; a2 = (float) c2.secondsOrder + c2.launchOrder + c2p; } } return a2.compareTo(a1); } } public Boolean isInArray(ArrayList<String> list, String str) { for (String curVal : list){ if (curVal.equals(str)) { return true; } } return false; } protected ResolveInfo cachedImageResolveInfo(Context mContext, String packageName) { ResolveInfo rInfo = null; try { Intent intent = mContext.getPackageManager().getLaunchIntentForPackage(packageName); rInfo = mContext.getPackageManager().resolveActivity(intent, 0); } catch (Exception NullPointerException) { Tools.HangarLog("bad PackageName: " + packageName + " -- deleting!"); TasksDataSource db = TasksDataSource.getInstance(mContext); db.open(); db.deletePackageName(packageName); } return rInfo; } protected synchronized ArrayList<TaskInfo> reorderTasks(ArrayList<TaskInfo> taskList, TasksDataSource db, int weightPriority, boolean widget) { Tools.HangarLog("reorderTasks: " + taskList.size() + " widget? " + widget); int highestSeconds = db.getHighestSeconds(); int highestLaunch = db.getHighestLaunch(); int count = 1; int subtractor = taskList.size() + 1; ArrayList<TaskInfoOrder> taskListE = new ArrayList<TaskInfoOrder>(); for (TaskInfo task : taskList) { TaskInfoOrder newTask = new TaskInfoOrder(task); int taskSeconds = task.totalseconds > 0 ? task.totalseconds : 1; newTask.launchScore = (float) task.launches / highestLaunch * 10; newTask.placeOrder = subtractor - count; newTask.secondsScore = (float) taskSeconds / highestSeconds * 10; taskListE.add(newTask); count ++; } Collections.sort(taskListE, new TaskComparator("launch", weightPriority, taskList.size())); int c = 0; for (int i=taskListE.size()-1; i >= 0; i--) { taskListE.get(c).launchOrder = (i + 1); c++; } c = 0; Collections.sort(taskListE, new TaskComparator("seconds", weightPriority, taskList.size())); for (int i=taskListE.size()-1; i >= 0; i--) { taskListE.get(c).secondsOrder = (i + 1); c++; } Collections.sort(taskListE, new TaskComparator("final", weightPriority, taskList.size())); taskList.clear(); int order = taskListE.size(); db.blankOrder(widget); for (TaskInfoOrder taskE : taskListE) { taskList.add(taskE.getOrig()); db.setOrder(taskE.getOrig().packageName, order, widget); order--; } return taskList; } protected ArrayList<TaskInfo> reorderTasks(ArrayList<TaskInfo> taskList, TasksDataSource db, int weightPriority) { return reorderTasks(taskList, db, weightPriority, false); } protected synchronized static void reorderWidgetTasks(TasksDataSource db, Context context) { SharedPreferences settingsPrefs = context.getSharedPreferences(context.getPackageName(), Context.MODE_MULTI_PROCESS); SharedPreferences widgetPrefs = context.getSharedPreferences("AppsWidget", Context.MODE_MULTI_PROCESS); boolean weightedRecents = widgetPrefs.getBoolean(Settings.WEIGHTED_RECENTS_PREFERENCE, Settings.WEIGHTED_RECENTS_DEFAULT); int weightPriority = Integer.parseInt(widgetPrefs.getString(Settings.WEIGHT_PRIORITY_PREFERENCE, Integer.toString(Settings.WEIGHT_PRIORITY_DEFAULT))); boolean wR = settingsPrefs.getBoolean(Settings.WEIGHTED_RECENTS_PREFERENCE, Settings.WEIGHTED_RECENTS_DEFAULT); int wP = Integer.parseInt(settingsPrefs.getString(Settings.WEIGHT_PRIORITY_PREFERENCE, Integer.toString(Settings.WEIGHT_PRIORITY_DEFAULT))); HangarLog("reorderWidgetTasks wR: " + wR + " wP: " + wP + " weightPriority: " + weightPriority + " weightedRecents: " + weightedRecents); if ((weightedRecents && !wR) || (weightedRecents && wP != weightPriority)) { ArrayList<TaskInfo> appList = buildTaskList(context, db, Settings.TASKLIST_QUEUE_LIMIT); new Tools().reorderTasks(appList, db, weightPriority, true); } else { db.blankOrder(true); } } protected static ArrayList<String> getBlacklisted(TasksDataSource db) { ArrayList<String> blPNames = new ArrayList<String>(); List<TasksModel> blTasks = db.getBlacklisted(); for (TasksModel task : blTasks) { blPNames.add(task.getPackageName()); } blPNames.add("com.android.systemui"); blPNames.add("com.android.phone"); blPNames.add("com.android.packageinstaller"); return blPNames; } protected static boolean isBlacklistedOrBad(String packageName, Context context, TasksDataSource db) { try { PackageManager pkgm = context.getPackageManager(); Intent intent = pkgm.getLaunchIntentForPackage(packageName); if (intent == null) throw new PackageManager.NameNotFoundException(); } catch (PackageManager.NameNotFoundException e) { return true; } for (String blTask : getBlacklisted(db)) { if (packageName.equals(blTask)) { return true; } } return false; } protected synchronized static ArrayList<Tools.TaskInfo> buildTaskList(Context context, TasksDataSource db, int queueSize, boolean weighted, boolean widget) { ArrayList<Tools.TaskInfo> taskList = new ArrayList<Tools.TaskInfo>(); List<TasksModel> tasks; ArrayList<String> pinnedApps = new ArrayList<String>(); SharedPreferences settingsPrefs = context.getSharedPreferences(context.getPackageName(), Context.MODE_MULTI_PROCESS); int pinnedSort = Integer.parseInt(settingsPrefs.getString(Settings.PINNED_SORT_PREFERENCE, Integer.toString(Settings.PINNED_SORT_DEFAULT))); boolean ignorePinned = settingsPrefs.getBoolean(Settings.IGNORE_PINNED_PREFERENCE, Settings.IGNORE_PINNED_DEFAULT); if (!ignorePinned) pinnedApps = new Tools().getPinned(context); Tools.HangarLog("buildTaskList queueSize: " + queueSize + " weighted: " + weighted + " taskList: " + taskList + " pinnedApps: " + pinnedApps.size()); if (queueSize == 0) { // queueSize 0 gets pinnedTasks. if (pinnedApps.size() == 0) { return taskList; } tasks = db.getPinnedTasks(pinnedApps, pinnedSort); } else if (weighted) { tasks = db.getOrderedTasks(queueSize, widget, pinnedApps); } else { tasks = db.getAllTasks(queueSize, pinnedApps); } for (TasksModel taskM : tasks) { String taskPackage = taskM.getPackageName(); if (isBlacklistedOrBad(taskPackage, context, db)) continue; Tools.TaskInfo dbTask = new Tools.TaskInfo(taskPackage); dbTask.appName = taskM.getName(); dbTask.className = taskM.getClassName(); dbTask.launches = taskM.getLaunches(); dbTask.totalseconds = taskM.getSeconds(); try { PackageManager pkgm = context.getPackageManager(); pkgm.getApplicationInfo(taskPackage, 0); } catch (PackageManager.NameNotFoundException e) { db.deleteTask(taskM); continue; } taskList.add(dbTask); } return taskList; } protected static ArrayList<Tools.TaskInfo> buildTaskList(Context context, TasksDataSource db, int queueSize) { return buildTaskList(context, db, queueSize, false, false); } protected static ArrayList<Tools.TaskInfo> buildPinnedList(Context context, TasksDataSource db) { return buildTaskList(context, db, 0, false, false); } protected ArrayList<Tools.TaskInfo> addMoreAppsButton(ArrayList<Tools.TaskInfo> taskList, int count) { HangarLog("addMoreAppsButton: taskList.size(): " + taskList.size() + " count: " + count); Tools.TaskInfo moreAppsTask = new TaskInfo(Settings.MORE_APPS_PACKAGE); if (count >= taskList.size()) { taskList.add(moreAppsTask); } else { taskList.add(count, moreAppsTask); } return taskList; } protected ArrayList<Tools.TaskInfo> getPinnedTasks (Context context, ArrayList<Tools.TaskInfo> pinnedListOrig, ArrayList<Tools.TaskInfo> pageListOrig, int count, boolean moreApps) { ArrayList<TaskInfo> pinnedList = new ArrayList<TaskInfo>(); ArrayList<TaskInfo> pageList = new ArrayList<TaskInfo>(); if (pinnedListOrig != null) pinnedList = new ArrayList<TaskInfo>(pinnedListOrig); if (pageListOrig != null) pageList = new ArrayList<TaskInfo>(pageListOrig); SharedPreferences settingsPrefs = context.getSharedPreferences(context.getPackageName(), Context.MODE_MULTI_PROCESS); int pinnedPlacement = Integer.parseInt(settingsPrefs.getString(Settings.PINNED_PLACEMENT_PREFERENCE, Integer.toString(Settings.PINNED_PLACEMENT_DEFAULT))); if (pinnedList.size() > 0) { if (pinnedPlacement == Settings.PINNED_PLACEMENT_LEFT) { pinnedList.addAll(pageList); if (moreApps) pinnedList = new Tools().addMoreAppsButton(pinnedList, count-1); return pinnedList; } else { int index = count - pinnedList.size(); if (index < 0) index = 0; try { if (moreApps) { // Slice off 1 less for More Apps! pageList.addAll((index == 0) ? index : index - 1, pinnedList); // This too pageList = new Tools().addMoreAppsButton(pageList, count - 1); } else { pageList.addAll(index, pinnedList); } } catch (IndexOutOfBoundsException e) { // taskList is smaller than count. Tools.HangarLog("outofbounds count: " + count + " index: " + index + ": " + e); if (index > pageList.size()) { boolean smallList = (pageList.size() + pinnedList.size() < count); if (moreApps && !smallList) { pageList.addAll(pageList.size() - 1, pinnedList); } else { pageList.addAll(pinnedList); } if (moreApps) pageList = new Tools().addMoreAppsButton(pageList, (smallList) ? pageList.size() : pageList.size() - 1); } } return pageList; } } else if (moreApps) { pageList = new Tools().addMoreAppsButton(pageList, count - 1); } return pageList; } }