/* * Copyright (C) 2014 The Android Open Source 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 com.android.systemui.recents.misc; import android.app.ActivityManager; import android.app.ActivityManagerNative; import android.app.ActivityOptions; import android.app.AppGlobals; import android.app.IActivityContainer; import android.app.IActivityManager; import android.app.ITaskStackListener; import android.app.SearchManager; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.util.Log; import android.util.MutableBoolean; import android.util.Pair; import android.util.SparseArray; import android.view.Display; import android.view.DisplayInfo; import android.view.SurfaceControl; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import com.android.internal.app.AssistUtils; import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.recents.Constants; import com.android.systemui.recents.Recents; import com.android.systemui.recents.RecentsAppWidgetHost; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Random; /** * Acts as a shim around the real system services that we need to access data from, and provides * a point of injection when testing UI. */ public class SystemServicesProxy { final static String TAG = "SystemServicesProxy"; final static BitmapFactory.Options sBitmapOptions; final static HandlerThread sBgThread; static { sBgThread = new HandlerThread("Recents-SystemServicesProxy", android.os.Process.THREAD_PRIORITY_BACKGROUND); sBgThread.start(); sBitmapOptions = new BitmapFactory.Options(); sBitmapOptions.inMutable = true; } AccessibilityManager mAccm; ActivityManager mAm; IActivityManager mIam; AppWidgetManager mAwm; PackageManager mPm; IPackageManager mIpm; AssistUtils mAssistUtils; WindowManager mWm; Display mDisplay; String mRecentsPackage; ComponentName mAssistComponent; Handler mBgThreadHandler; Bitmap mDummyIcon; int mDummyThumbnailWidth; int mDummyThumbnailHeight; Paint mBgProtectionPaint; Canvas mBgProtectionCanvas; /** Private constructor */ public SystemServicesProxy(Context context) { mAccm = AccessibilityManager.getInstance(context); mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); mIam = ActivityManagerNative.getDefault(); mAwm = AppWidgetManager.getInstance(context); mPm = context.getPackageManager(); mIpm = AppGlobals.getPackageManager(); mAssistUtils = new AssistUtils(context); mWm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mDisplay = mWm.getDefaultDisplay(); mRecentsPackage = context.getPackageName(); mBgThreadHandler = new Handler(sBgThread.getLooper()); // Get the dummy thumbnail width/heights Resources res = context.getResources(); int wId = com.android.internal.R.dimen.thumbnail_width; int hId = com.android.internal.R.dimen.thumbnail_height; mDummyThumbnailWidth = res.getDimensionPixelSize(wId); mDummyThumbnailHeight = res.getDimensionPixelSize(hId); // Create the protection paints mBgProtectionPaint = new Paint(); mBgProtectionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP)); mBgProtectionPaint.setColor(0xFFffffff); mBgProtectionCanvas = new Canvas(); // Resolve the assist intent mAssistComponent = mAssistUtils.getAssistComponentForUser(UserHandle.myUserId()); if (Constants.DebugFlags.App.EnableSystemServicesProxy) { // Create a dummy icon mDummyIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); mDummyIcon.eraseColor(0xFF999999); } } /** Returns a list of the recents tasks */ public List<ActivityManager.RecentTaskInfo> getRecentTasks(int numLatestTasks, int userId, boolean isTopTaskHome) { if (mAm == null) return null; // If we are mocking, then create some recent tasks if (Constants.DebugFlags.App.EnableSystemServicesProxy) { ArrayList<ActivityManager.RecentTaskInfo> tasks = new ArrayList<ActivityManager.RecentTaskInfo>(); int count = Math.min(numLatestTasks, Constants.DebugFlags.App.SystemServicesProxyMockTaskCount); for (int i = 0; i < count; i++) { // Create a dummy component name int packageIndex = i % Constants.DebugFlags.App.SystemServicesProxyMockPackageCount; ComponentName cn = new ComponentName("com.android.test" + packageIndex, "com.android.test" + i + ".Activity"); String description = "" + i + " - " + Long.toString(Math.abs(new Random().nextLong()), 36); // Create the recent task info ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo(); rti.id = rti.persistentId = i; rti.baseIntent = new Intent(); rti.baseIntent.setComponent(cn); rti.description = description; rti.firstActiveTime = rti.lastActiveTime = i; if (i % 2 == 0) { rti.taskDescription = new ActivityManager.TaskDescription(description, Bitmap.createBitmap(mDummyIcon), 0xFF000000 | (0xFFFFFF & new Random().nextInt())); } else { rti.taskDescription = new ActivityManager.TaskDescription(); } tasks.add(rti); } return tasks; } // Remove home/recents/excluded tasks int minNumTasksToQuery = 10; int numTasksToQuery = Math.max(minNumTasksToQuery, numLatestTasks); List<ActivityManager.RecentTaskInfo> tasks = mAm.getRecentTasksForUser(numTasksToQuery, ActivityManager.RECENT_IGNORE_HOME_STACK_TASKS | ActivityManager.RECENT_IGNORE_UNAVAILABLE | ActivityManager.RECENT_INCLUDE_PROFILES | ActivityManager.RECENT_WITH_EXCLUDED, userId); // Break early if we can't get a valid set of tasks if (tasks == null) { return new ArrayList<>(); } boolean isFirstValidTask = true; Iterator<ActivityManager.RecentTaskInfo> iter = tasks.iterator(); while (iter.hasNext()) { ActivityManager.RecentTaskInfo t = iter.next(); // NOTE: The order of these checks happens in the expected order of the traversal of the // tasks // Check the first non-recents task, include this task even if it is marked as excluded // from recents if we are currently in the app. In other words, only remove excluded // tasks if it is not the first active task. boolean isExcluded = (t.baseIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) == Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; if (isExcluded && (isTopTaskHome || !isFirstValidTask)) { iter.remove(); continue; } isFirstValidTask = false; } return tasks.subList(0, Math.min(tasks.size(), numLatestTasks)); } /** Returns a list of the running tasks */ private List<ActivityManager.RunningTaskInfo> getRunningTasks(int numTasks) { if (mAm == null) return null; return mAm.getRunningTasks(numTasks); } /** Returns the top task. */ public ActivityManager.RunningTaskInfo getTopMostTask() { List<ActivityManager.RunningTaskInfo> tasks = getRunningTasks(1); if (tasks != null && !tasks.isEmpty()) { return tasks.get(0); } return null; } /** Returns whether the recents is currently running */ public boolean isRecentsTopMost(ActivityManager.RunningTaskInfo topTask, MutableBoolean isHomeTopMost) { if (topTask != null) { ComponentName topActivity = topTask.topActivity; // Check if the front most activity is recents if (topActivity.getPackageName().equals(Recents.sRecentsPackage) && topActivity.getClassName().equals(Recents.sRecentsActivity)) { if (isHomeTopMost != null) { isHomeTopMost.value = false; } return true; } if (isHomeTopMost != null) { isHomeTopMost.value = isInHomeStack(topTask.id); } } return false; } /** Get the bounds of a stack / task. */ public Rect getTaskBounds(int stackId) { ActivityManager.StackInfo info = getAllStackInfos().get(stackId); if (info != null) return info.bounds; return new Rect(); } /** Resize a given task. */ public void resizeTask(int taskId, Rect bounds) { if (mIam == null) return; try { mIam.resizeTask(taskId, bounds); } catch (RemoteException e) { e.printStackTrace(); } } /** Returns the stack info for all stacks. */ public SparseArray<ActivityManager.StackInfo> getAllStackInfos() { if (mIam == null) return new SparseArray<ActivityManager.StackInfo>(); try { SparseArray<ActivityManager.StackInfo> stacks = new SparseArray<ActivityManager.StackInfo>(); List<ActivityManager.StackInfo> infos = mIam.getAllStackInfos(); int stackCount = infos.size(); for (int i = 0; i < stackCount; i++) { ActivityManager.StackInfo info = infos.get(i); stacks.put(info.stackId, info); } return stacks; } catch (RemoteException e) { e.printStackTrace(); return new SparseArray<ActivityManager.StackInfo>(); } } /** Returns the focused stack id. */ public int getFocusedStack() { if (mIam == null) return -1; try { return mIam.getFocusedStackId(); } catch (RemoteException e) { e.printStackTrace(); return -1; } } /** Returns whether the specified task is in the home stack */ public boolean isInHomeStack(int taskId) { if (mAm == null) return false; // If we are mocking, then just return false if (Constants.DebugFlags.App.EnableSystemServicesProxy) { return false; } return mAm.isInHomeStack(taskId); } /** Returns the top task thumbnail for the given task id */ public Bitmap getTaskThumbnail(int taskId) { if (mAm == null) return null; // If we are mocking, then just return a dummy thumbnail if (Constants.DebugFlags.App.EnableSystemServicesProxy) { Bitmap thumbnail = Bitmap.createBitmap(mDummyThumbnailWidth, mDummyThumbnailHeight, Bitmap.Config.ARGB_8888); thumbnail.eraseColor(0xff333333); return thumbnail; } Bitmap thumbnail = SystemServicesProxy.getThumbnail(mAm, taskId); if (thumbnail != null) { thumbnail.setHasAlpha(false); // We use a dumb heuristic for now, if the thumbnail is purely transparent in the top // left pixel, then assume the whole thumbnail is transparent. Generally, proper // screenshots are always composed onto a bitmap that has no alpha. if (Color.alpha(thumbnail.getPixel(0, 0)) == 0) { mBgProtectionCanvas.setBitmap(thumbnail); mBgProtectionCanvas.drawRect(0, 0, thumbnail.getWidth(), thumbnail.getHeight(), mBgProtectionPaint); mBgProtectionCanvas.setBitmap(null); Log.e(TAG, "Invalid screenshot detected from getTaskThumbnail()"); } } return thumbnail; } /** * Returns a task thumbnail from the activity manager */ public static Bitmap getThumbnail(ActivityManager activityManager, int taskId) { ActivityManager.TaskThumbnail taskThumbnail = activityManager.getTaskThumbnail(taskId); if (taskThumbnail == null) return null; Bitmap thumbnail = taskThumbnail.mainThumbnail; ParcelFileDescriptor descriptor = taskThumbnail.thumbnailFileDescriptor; if (thumbnail == null && descriptor != null) { thumbnail = BitmapFactory.decodeFileDescriptor(descriptor.getFileDescriptor(), null, sBitmapOptions); } if (descriptor != null) { try { descriptor.close(); } catch (IOException e) { } } return thumbnail; } /** Moves a task to the front with the specified activity options. */ public void moveTaskToFront(int taskId, ActivityOptions opts) { if (mAm == null) return; if (Constants.DebugFlags.App.EnableSystemServicesProxy) return; if (opts != null) { mAm.moveTaskToFront(taskId, ActivityManager.MOVE_TASK_WITH_HOME, opts.toBundle()); } else { mAm.moveTaskToFront(taskId, ActivityManager.MOVE_TASK_WITH_HOME); } } /** Removes the task */ public void removeTask(final int taskId) { if (mAm == null) return; if (Constants.DebugFlags.App.EnableSystemServicesProxy) return; // Remove the task. mBgThreadHandler.post(new Runnable() { @Override public void run() { mAm.removeTask(taskId); } }); } /** * Returns the activity info for a given component name. * * @param cn The component name of the activity. * @param userId The userId of the user that this is for. */ public ActivityInfo getActivityInfo(ComponentName cn, int userId) { if (mIpm == null) return null; if (Constants.DebugFlags.App.EnableSystemServicesProxy) return new ActivityInfo(); try { return mIpm.getActivityInfo(cn, PackageManager.GET_META_DATA, userId); } catch (RemoteException e) { e.printStackTrace(); return null; } } /** * Returns the activity info for a given component name. * * @param cn The component name of the activity. */ public ActivityInfo getActivityInfo(ComponentName cn) { if (mPm == null) return null; if (Constants.DebugFlags.App.EnableSystemServicesProxy) return new ActivityInfo(); try { return mPm.getActivityInfo(cn, PackageManager.GET_META_DATA); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); return null; } } /** Returns the activity label */ public String getActivityLabel(ActivityInfo info) { if (mPm == null) return null; // If we are mocking, then return a mock label if (Constants.DebugFlags.App.EnableSystemServicesProxy) { return "Recent Task"; } return info.loadLabel(mPm).toString(); } /** Returns the application label */ public String getApplicationLabel(Intent baseIntent, int userId) { if (mPm == null) return null; // If we are mocking, then return a mock label if (Constants.DebugFlags.App.EnableSystemServicesProxy) { return "Recent Task"; } ResolveInfo ri = mPm.resolveActivityAsUser(baseIntent, 0, userId); CharSequence label = (ri != null) ? ri.loadLabel(mPm) : null; return (label != null) ? label.toString() : null; } /** Returns the content description for a given task */ public String getContentDescription(Intent baseIntent, int userId, String activityLabel, Resources res) { String applicationLabel = getApplicationLabel(baseIntent, userId); if (applicationLabel == null) { return getBadgedLabel(activityLabel, userId); } String badgedApplicationLabel = getBadgedLabel(applicationLabel, userId); return applicationLabel.equals(activityLabel) ? badgedApplicationLabel : res.getString(R.string.accessibility_recents_task_header, badgedApplicationLabel, activityLabel); } /** * Returns the activity icon for the ActivityInfo for a user, badging if * necessary. */ public Drawable getActivityIcon(ActivityInfo info, int userId) { if (mPm == null) return null; // If we are mocking, then return a mock label if (Constants.DebugFlags.App.EnableSystemServicesProxy) { return new ColorDrawable(0xFF666666); } Drawable icon = info.loadIcon(mPm); return getBadgedIcon(icon, userId); } /** * Returns the given icon for a user, badging if necessary. */ public Drawable getBadgedIcon(Drawable icon, int userId) { if (userId != UserHandle.myUserId()) { icon = mPm.getUserBadgedIcon(icon, new UserHandle(userId)); } return icon; } /** * Returns the given label for a user, badging if necessary. */ public String getBadgedLabel(String label, int userId) { if (userId != UserHandle.myUserId()) { label = mPm.getUserBadgedLabel(label, new UserHandle(userId)).toString(); } return label; } /** Returns the package name of the home activity. */ public String getHomeActivityPackageName() { if (mPm == null) return null; if (Constants.DebugFlags.App.EnableSystemServicesProxy) return null; ArrayList<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>(); ComponentName defaultHomeActivity = mPm.getHomeActivities(homeActivities); if (defaultHomeActivity != null) { return defaultHomeActivity.getPackageName(); } else if (homeActivities.size() == 1) { ResolveInfo info = homeActivities.get(0); if (info.activityInfo != null) { return info.activityInfo.packageName; } } return null; } /** * Returns whether the foreground user is the owner. */ public boolean isForegroundUserOwner() { if (mAm == null) return false; return mAm.getCurrentUser() == UserHandle.USER_OWNER; } /** * Returns the current search widget id. */ public int getSearchAppWidgetId(Context context) { return Prefs.getInt(context, Prefs.Key.SEARCH_APP_WIDGET_ID, -1); } /** * Returns the current search widget info, binding a new one if necessary. */ public AppWidgetProviderInfo getOrBindSearchAppWidget(Context context, AppWidgetHost host) { int searchWidgetId = Prefs.getInt(context, Prefs.Key.SEARCH_APP_WIDGET_ID, -1); AppWidgetProviderInfo searchWidgetInfo = mAwm.getAppWidgetInfo(searchWidgetId); AppWidgetProviderInfo resolvedSearchWidgetInfo = resolveSearchAppWidget(); // Return the search widget info if it hasn't changed if (searchWidgetInfo != null && resolvedSearchWidgetInfo != null && searchWidgetInfo.provider.equals(resolvedSearchWidgetInfo.provider)) { if (Prefs.getString(context, Prefs.Key.SEARCH_APP_WIDGET_PACKAGE, null) == null) { Prefs.putString(context, Prefs.Key.SEARCH_APP_WIDGET_PACKAGE, searchWidgetInfo.provider.getPackageName()); } return searchWidgetInfo; } // Delete the old widget if (searchWidgetId != -1) { host.deleteAppWidgetId(searchWidgetId); } // And rebind a new search widget if (resolvedSearchWidgetInfo != null) { Pair<Integer, AppWidgetProviderInfo> widgetInfo = bindSearchAppWidget(host, resolvedSearchWidgetInfo); if (widgetInfo != null) { Prefs.putInt(context, Prefs.Key.SEARCH_APP_WIDGET_ID, widgetInfo.first); Prefs.putString(context, Prefs.Key.SEARCH_APP_WIDGET_PACKAGE, widgetInfo.second.provider.getPackageName()); return widgetInfo.second; } } // If we fall through here, then there is no resolved search widget, so clear the state Prefs.remove(context, Prefs.Key.SEARCH_APP_WIDGET_ID); Prefs.remove(context, Prefs.Key.SEARCH_APP_WIDGET_PACKAGE); return null; } /** * Returns the first Recents widget from the same package as the global assist activity. */ private AppWidgetProviderInfo resolveSearchAppWidget() { if (mAssistComponent == null) return null; List<AppWidgetProviderInfo> widgets = mAwm.getInstalledProviders( AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX); for (AppWidgetProviderInfo info : widgets) { if (info.provider.getPackageName().equals(mAssistComponent.getPackageName())) { return info; } } return null; } /** * Resolves and binds the search app widget that is to appear in the recents. */ private Pair<Integer, AppWidgetProviderInfo> bindSearchAppWidget(AppWidgetHost host, AppWidgetProviderInfo resolvedSearchWidgetInfo) { if (mAwm == null) return null; if (mAssistComponent == null) return null; // Allocate a new widget id and try and bind the app widget (if that fails, then just skip) int searchWidgetId = host.allocateAppWidgetId(); Bundle opts = new Bundle(); opts.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX); if (!mAwm.bindAppWidgetIdIfAllowed(searchWidgetId, resolvedSearchWidgetInfo.provider, opts)) { host.deleteAppWidgetId(searchWidgetId); return null; } return new Pair<>(searchWidgetId, resolvedSearchWidgetInfo); } /** * Returns whether touch exploration is currently enabled. */ public boolean isTouchExplorationEnabled() { if (mAccm == null) return false; return mAccm.isEnabled() && mAccm.isTouchExplorationEnabled(); } /** * Returns a global setting. */ public int getGlobalSetting(Context context, String setting) { ContentResolver cr = context.getContentResolver(); return Settings.Global.getInt(cr, setting, 0); } /** * Returns a system setting. */ public int getSystemSetting(Context context, String setting) { ContentResolver cr = context.getContentResolver(); return Settings.System.getInt(cr, setting, 0); } /** * Returns a system property. */ public String getSystemProperty(String key) { return SystemProperties.get(key); } /** * Returns the window rect. */ public Rect getWindowRect() { Rect windowRect = new Rect(); if (mWm == null) return windowRect; Point p = new Point(); mWm.getDefaultDisplay().getRealSize(p); windowRect.set(0, 0, p.x, p.y); return windowRect; } /** Starts an activity from recents. */ public boolean startActivityFromRecents(Context context, int taskId, String taskName, ActivityOptions options) { if (mIam != null) { try { mIam.startActivityFromRecents(taskId, options == null ? null : options.toBundle()); return true; } catch (Exception e) { Console.logError(context, context.getString(R.string.recents_launch_error_message, taskName)); } } return false; } /** Starts an in-place animation on the front most application windows. */ public void startInPlaceAnimationOnFrontMostApplication(ActivityOptions opts) { if (mIam == null) return; try { mIam.startInPlaceAnimationOnFrontMostApplication(opts); } catch (Exception e) { e.printStackTrace(); } } /** Registers a task stack listener with the system. */ public void registerTaskStackListener(ITaskStackListener listener) { if (mIam == null) return; try { mIam.registerTaskStackListener(listener); } catch (Exception e) { e.printStackTrace(); } } }