/* * 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.IActivityManager; 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.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; import android.util.Log; import android.util.Pair; import android.view.Display; import android.view.DisplayInfo; import android.view.SurfaceControl; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import com.android.systemui.R; import com.android.systemui.recents.Constants; 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; AccessibilityManager mAccm; ActivityManager mAm; IActivityManager mIam; AppWidgetManager mAwm; PackageManager mPm; IPackageManager mIpm; SearchManager mSm; WindowManager mWm; Display mDisplay; String mRecentsPackage; ComponentName mAssistComponent; Bitmap mDummyIcon; int mDummyThumbnailWidth; int mDummyThumbnailHeight; Paint mBgProtectionPaint; Canvas mBgProtectionCanvas; static { sBitmapOptions = new BitmapFactory.Options(); sBitmapOptions.inMutable = true; } /** 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(); mSm = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE); mWm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mDisplay = mWm.getDefaultDisplay(); mRecentsPackage = context.getPackageName(); // 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 Intent assist = mSm.getAssistIntent(context, false); if (assist != null) { mAssistComponent = assist.getComponent(); } 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 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 tasks = new ArrayList(); 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 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 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 */ public List getRunningTasks(int numTasks) { if (mAm == null) return null; return mAm.getRunningTasks(numTasks); } /** 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) { // 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 and kills the process */ public void removeTask(int taskId, boolean isDocument) { if (mAm == null) return; if (Constants.DebugFlags.App.EnableSystemServicesProxy) return; // Remove the task, and only kill the process if it is not a document mAm.removeTask(taskId, isDocument ? 0 : ActivityManager.REMOVE_TASK_KILL_PROCESS); } /** * 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 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 package name of the home activity. */ public String getHomeActivityPackageName() { if (mPm == null) return null; if (Constants.DebugFlags.App.EnableSystemServicesProxy) return null; ArrayList homeActivities = new ArrayList(); 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; } /** * Resolves and returns the first Recents widget from the same package as the global * assist activity. */ public AppWidgetProviderInfo resolveSearchAppWidget() { if (mAwm == null) return null; if (mAssistComponent == null) return null; // Find the first Recents widget from the same package as the global assist activity List 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. */ public Pair bindSearchAppWidget(AppWidgetHost host) { if (mAwm == null) return null; if (mAssistComponent == null) return null; // Find the first Recents widget from the same package as the global assist activity AppWidgetProviderInfo searchWidgetInfo = resolveSearchAppWidget(); // Return early if there is no search widget if (searchWidgetInfo == 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, searchWidgetInfo.provider, opts)) { return null; } return new Pair(searchWidgetId, searchWidgetInfo); } /** * Returns the app widget info for the specified app widget id. */ public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) { if (mAwm == null) return null; return mAwm.getAppWidgetInfo(appWidgetId); } /** * Destroys the specified app widget. */ public void unbindSearchAppWidget(AppWidgetHost host, int appWidgetId) { if (mAwm == null) return; // Delete the app widget host.deleteAppWidgetId(appWidgetId); } /** * 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 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; } /** * Locks the current task. */ public void lockCurrentTask() { if (mIam == null) return; try { mIam.startLockTaskModeOnCurrent(); } catch (RemoteException e) {} } /** * Takes a screenshot of the current surface. */ public Bitmap takeScreenshot() { DisplayInfo di = new DisplayInfo(); mDisplay.getDisplayInfo(di); return SurfaceControl.screenshot(di.getNaturalWidth(), di.getNaturalHeight()); } /** * Takes a screenshot of the current app. */ public Bitmap takeAppScreenshot() { return takeScreenshot(); } /** 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; } }