1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.recents.misc;
18
19import android.app.ActivityManager;
20import android.app.ActivityManagerNative;
21import android.app.ActivityOptions;
22import android.app.AppGlobals;
23import android.app.IActivityManager;
24import android.app.SearchManager;
25import android.appwidget.AppWidgetHost;
26import android.appwidget.AppWidgetManager;
27import android.appwidget.AppWidgetProviderInfo;
28import android.content.ComponentName;
29import android.content.ContentResolver;
30import android.content.Context;
31import android.content.Intent;
32import android.content.pm.ActivityInfo;
33import android.content.pm.IPackageManager;
34import android.content.pm.PackageManager;
35import android.content.pm.ResolveInfo;
36import android.content.res.Resources;
37import android.graphics.Bitmap;
38import android.graphics.BitmapFactory;
39import android.graphics.Canvas;
40import android.graphics.Color;
41import android.graphics.Paint;
42import android.graphics.Point;
43import android.graphics.PorterDuff;
44import android.graphics.PorterDuffXfermode;
45import android.graphics.Rect;
46import android.graphics.drawable.ColorDrawable;
47import android.graphics.drawable.Drawable;
48import android.os.Bundle;
49import android.os.ParcelFileDescriptor;
50import android.os.RemoteException;
51import android.os.UserHandle;
52import android.provider.Settings;
53import android.util.Log;
54import android.util.Pair;
55import android.view.Display;
56import android.view.DisplayInfo;
57import android.view.SurfaceControl;
58import android.view.WindowManager;
59import android.view.accessibility.AccessibilityManager;
60import com.android.systemui.R;
61import com.android.systemui.recents.Constants;
62
63import java.io.IOException;
64import java.util.ArrayList;
65import java.util.Iterator;
66import java.util.List;
67import java.util.Random;
68
69/**
70 * Acts as a shim around the real system services that we need to access data from, and provides
71 * a point of injection when testing UI.
72 */
73public class SystemServicesProxy {
74    final static String TAG = "SystemServicesProxy";
75
76    final static BitmapFactory.Options sBitmapOptions;
77
78    AccessibilityManager mAccm;
79    ActivityManager mAm;
80    IActivityManager mIam;
81    AppWidgetManager mAwm;
82    PackageManager mPm;
83    IPackageManager mIpm;
84    SearchManager mSm;
85    WindowManager mWm;
86    Display mDisplay;
87    String mRecentsPackage;
88    ComponentName mAssistComponent;
89
90    Bitmap mDummyIcon;
91    int mDummyThumbnailWidth;
92    int mDummyThumbnailHeight;
93    Paint mBgProtectionPaint;
94    Canvas mBgProtectionCanvas;
95
96    static {
97        sBitmapOptions = new BitmapFactory.Options();
98        sBitmapOptions.inMutable = true;
99    }
100
101    /** Private constructor */
102    public SystemServicesProxy(Context context) {
103        mAccm = AccessibilityManager.getInstance(context);
104        mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
105        mIam = ActivityManagerNative.getDefault();
106        mAwm = AppWidgetManager.getInstance(context);
107        mPm = context.getPackageManager();
108        mIpm = AppGlobals.getPackageManager();
109        mSm = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
110        mWm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
111        mDisplay = mWm.getDefaultDisplay();
112        mRecentsPackage = context.getPackageName();
113
114        // Get the dummy thumbnail width/heights
115        Resources res = context.getResources();
116        int wId = com.android.internal.R.dimen.thumbnail_width;
117        int hId = com.android.internal.R.dimen.thumbnail_height;
118        mDummyThumbnailWidth = res.getDimensionPixelSize(wId);
119        mDummyThumbnailHeight = res.getDimensionPixelSize(hId);
120
121        // Create the protection paints
122        mBgProtectionPaint = new Paint();
123        mBgProtectionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP));
124        mBgProtectionPaint.setColor(0xFFffffff);
125        mBgProtectionCanvas = new Canvas();
126
127        // Resolve the assist intent
128        Intent assist = mSm.getAssistIntent(context, false);
129        if (assist != null) {
130            mAssistComponent = assist.getComponent();
131        }
132
133        if (Constants.DebugFlags.App.EnableSystemServicesProxy) {
134            // Create a dummy icon
135            mDummyIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
136            mDummyIcon.eraseColor(0xFF999999);
137        }
138    }
139
140    /** Returns a list of the recents tasks */
141    public List<ActivityManager.RecentTaskInfo> getRecentTasks(int numLatestTasks, int userId,
142            boolean isTopTaskHome) {
143        if (mAm == null) return null;
144
145        // If we are mocking, then create some recent tasks
146        if (Constants.DebugFlags.App.EnableSystemServicesProxy) {
147            ArrayList<ActivityManager.RecentTaskInfo> tasks =
148                    new ArrayList<ActivityManager.RecentTaskInfo>();
149            int count = Math.min(numLatestTasks, Constants.DebugFlags.App.SystemServicesProxyMockTaskCount);
150            for (int i = 0; i < count; i++) {
151                // Create a dummy component name
152                int packageIndex = i % Constants.DebugFlags.App.SystemServicesProxyMockPackageCount;
153                ComponentName cn = new ComponentName("com.android.test" + packageIndex,
154                        "com.android.test" + i + ".Activity");
155                String description = "" + i + " - " +
156                        Long.toString(Math.abs(new Random().nextLong()), 36);
157                // Create the recent task info
158                ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo();
159                rti.id = rti.persistentId = i;
160                rti.baseIntent = new Intent();
161                rti.baseIntent.setComponent(cn);
162                rti.description = description;
163                rti.firstActiveTime = rti.lastActiveTime = i;
164                if (i % 2 == 0) {
165                    rti.taskDescription = new ActivityManager.TaskDescription(description,
166                        Bitmap.createBitmap(mDummyIcon),
167                        0xFF000000 | (0xFFFFFF & new Random().nextInt()));
168                } else {
169                    rti.taskDescription = new ActivityManager.TaskDescription();
170                }
171                tasks.add(rti);
172            }
173            return tasks;
174        }
175
176        // Remove home/recents/excluded tasks
177        int minNumTasksToQuery = 10;
178        int numTasksToQuery = Math.max(minNumTasksToQuery, numLatestTasks);
179        List<ActivityManager.RecentTaskInfo> tasks = mAm.getRecentTasksForUser(numTasksToQuery,
180                ActivityManager.RECENT_IGNORE_HOME_STACK_TASKS |
181                ActivityManager.RECENT_IGNORE_UNAVAILABLE |
182                ActivityManager.RECENT_INCLUDE_PROFILES |
183                ActivityManager.RECENT_WITH_EXCLUDED, userId);
184
185        // Break early if we can't get a valid set of tasks
186        if (tasks == null) {
187            return new ArrayList<ActivityManager.RecentTaskInfo>();
188        }
189
190        boolean isFirstValidTask = true;
191        Iterator<ActivityManager.RecentTaskInfo> iter = tasks.iterator();
192        while (iter.hasNext()) {
193            ActivityManager.RecentTaskInfo t = iter.next();
194
195            // NOTE: The order of these checks happens in the expected order of the traversal of the
196            // tasks
197
198            // Check the first non-recents task, include this task even if it is marked as excluded
199            // from recents if we are currently in the app.  In other words, only remove excluded
200            // tasks if it is not the first active task.
201            boolean isExcluded = (t.baseIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
202                    == Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
203            if (isExcluded && (isTopTaskHome || !isFirstValidTask)) {
204                iter.remove();
205                continue;
206            }
207            isFirstValidTask = false;
208        }
209
210        return tasks.subList(0, Math.min(tasks.size(), numLatestTasks));
211    }
212
213    /** Returns a list of the running tasks */
214    public List<ActivityManager.RunningTaskInfo> getRunningTasks(int numTasks) {
215        if (mAm == null) return null;
216        return mAm.getRunningTasks(numTasks);
217    }
218
219    /** Returns whether the specified task is in the home stack */
220    public boolean isInHomeStack(int taskId) {
221        if (mAm == null) return false;
222
223        // If we are mocking, then just return false
224        if (Constants.DebugFlags.App.EnableSystemServicesProxy) {
225            return false;
226        }
227
228        return mAm.isInHomeStack(taskId);
229    }
230
231    /** Returns the top task thumbnail for the given task id */
232    public Bitmap getTaskThumbnail(int taskId) {
233        if (mAm == null) return null;
234
235        // If we are mocking, then just return a dummy thumbnail
236        if (Constants.DebugFlags.App.EnableSystemServicesProxy) {
237            Bitmap thumbnail = Bitmap.createBitmap(mDummyThumbnailWidth, mDummyThumbnailHeight,
238                    Bitmap.Config.ARGB_8888);
239            thumbnail.eraseColor(0xff333333);
240            return thumbnail;
241        }
242
243        Bitmap thumbnail = SystemServicesProxy.getThumbnail(mAm, taskId);
244        if (thumbnail != null) {
245            // We use a dumb heuristic for now, if the thumbnail is purely transparent in the top
246            // left pixel, then assume the whole thumbnail is transparent. Generally, proper
247            // screenshots are always composed onto a bitmap that has no alpha.
248            if (Color.alpha(thumbnail.getPixel(0, 0)) == 0) {
249                mBgProtectionCanvas.setBitmap(thumbnail);
250                mBgProtectionCanvas.drawRect(0, 0, thumbnail.getWidth(), thumbnail.getHeight(),
251                        mBgProtectionPaint);
252                mBgProtectionCanvas.setBitmap(null);
253                Log.e(TAG, "Invalid screenshot detected from getTaskThumbnail()");
254            }
255        }
256        return thumbnail;
257    }
258
259    /**
260     * Returns a task thumbnail from the activity manager
261     */
262    public static Bitmap getThumbnail(ActivityManager activityManager, int taskId) {
263        ActivityManager.TaskThumbnail taskThumbnail = activityManager.getTaskThumbnail(taskId);
264        if (taskThumbnail == null) return null;
265
266        Bitmap thumbnail = taskThumbnail.mainThumbnail;
267        ParcelFileDescriptor descriptor = taskThumbnail.thumbnailFileDescriptor;
268        if (thumbnail == null && descriptor != null) {
269            thumbnail = BitmapFactory.decodeFileDescriptor(descriptor.getFileDescriptor(),
270                    null, sBitmapOptions);
271        }
272        if (descriptor != null) {
273            try {
274                descriptor.close();
275            } catch (IOException e) {
276            }
277        }
278        return thumbnail;
279    }
280
281    /** Moves a task to the front with the specified activity options */
282    public void moveTaskToFront(int taskId, ActivityOptions opts) {
283        if (mAm == null) return;
284        if (Constants.DebugFlags.App.EnableSystemServicesProxy) return;
285
286        if (opts != null) {
287            mAm.moveTaskToFront(taskId, ActivityManager.MOVE_TASK_WITH_HOME,
288                    opts.toBundle());
289        } else {
290            mAm.moveTaskToFront(taskId, ActivityManager.MOVE_TASK_WITH_HOME);
291        }
292    }
293
294    /** Removes the task and kills the process */
295    public void removeTask(int taskId, boolean isDocument) {
296        if (mAm == null) return;
297        if (Constants.DebugFlags.App.EnableSystemServicesProxy) return;
298
299        // Remove the task, and only kill the process if it is not a document
300        mAm.removeTask(taskId, isDocument ? 0 : ActivityManager.REMOVE_TASK_KILL_PROCESS);
301    }
302
303    /**
304     * Returns the activity info for a given component name.
305     *
306     * @param cn The component name of the activity.
307     * @param userId The userId of the user that this is for.
308     */
309    public ActivityInfo getActivityInfo(ComponentName cn, int userId) {
310        if (mIpm == null) return null;
311        if (Constants.DebugFlags.App.EnableSystemServicesProxy) return new ActivityInfo();
312
313        try {
314            return mIpm.getActivityInfo(cn, PackageManager.GET_META_DATA, userId);
315        } catch (RemoteException e) {
316            e.printStackTrace();
317            return null;
318        }
319    }
320
321    /**
322     * Returns the activity info for a given component name.
323     *
324     * @param cn The component name of the activity.
325     */
326    public ActivityInfo getActivityInfo(ComponentName cn) {
327        if (mPm == null) return null;
328        if (Constants.DebugFlags.App.EnableSystemServicesProxy) return new ActivityInfo();
329
330        try {
331            return mPm.getActivityInfo(cn, PackageManager.GET_META_DATA);
332        } catch (PackageManager.NameNotFoundException e) {
333            e.printStackTrace();
334            return null;
335        }
336    }
337
338    /** Returns the activity label */
339    public String getActivityLabel(ActivityInfo info) {
340        if (mPm == null) return null;
341
342        // If we are mocking, then return a mock label
343        if (Constants.DebugFlags.App.EnableSystemServicesProxy) {
344            return "Recent Task";
345        }
346
347        return info.loadLabel(mPm).toString();
348    }
349
350    /**
351     * Returns the activity icon for the ActivityInfo for a user, badging if
352     * necessary.
353     */
354    public Drawable getActivityIcon(ActivityInfo info, int userId) {
355        if (mPm == null) return null;
356
357        // If we are mocking, then return a mock label
358        if (Constants.DebugFlags.App.EnableSystemServicesProxy) {
359            return new ColorDrawable(0xFF666666);
360        }
361
362        Drawable icon = info.loadIcon(mPm);
363        return getBadgedIcon(icon, userId);
364    }
365
366    /**
367     * Returns the given icon for a user, badging if necessary.
368     */
369    public Drawable getBadgedIcon(Drawable icon, int userId) {
370        if (userId != UserHandle.myUserId()) {
371            icon = mPm.getUserBadgedIcon(icon, new UserHandle(userId));
372        }
373        return icon;
374    }
375
376    /** Returns the package name of the home activity. */
377    public String getHomeActivityPackageName() {
378        if (mPm == null) return null;
379        if (Constants.DebugFlags.App.EnableSystemServicesProxy) return null;
380
381        ArrayList<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>();
382        ComponentName defaultHomeActivity = mPm.getHomeActivities(homeActivities);
383        if (defaultHomeActivity != null) {
384            return defaultHomeActivity.getPackageName();
385        } else if (homeActivities.size() == 1) {
386            ResolveInfo info = homeActivities.get(0);
387            if (info.activityInfo != null) {
388                return info.activityInfo.packageName;
389            }
390        }
391        return null;
392    }
393
394    /**
395     * Resolves and returns the first Recents widget from the same package as the global
396     * assist activity.
397     */
398    public AppWidgetProviderInfo resolveSearchAppWidget() {
399        if (mAwm == null) return null;
400        if (mAssistComponent == null) return null;
401
402        // Find the first Recents widget from the same package as the global assist activity
403        List<AppWidgetProviderInfo> widgets = mAwm.getInstalledProviders(
404                AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX);
405        for (AppWidgetProviderInfo info : widgets) {
406            if (info.provider.getPackageName().equals(mAssistComponent.getPackageName())) {
407                return info;
408            }
409        }
410        return null;
411    }
412
413    /**
414     * Resolves and binds the search app widget that is to appear in the recents.
415     */
416    public Pair<Integer, AppWidgetProviderInfo> bindSearchAppWidget(AppWidgetHost host) {
417        if (mAwm == null) return null;
418        if (mAssistComponent == null) return null;
419
420        // Find the first Recents widget from the same package as the global assist activity
421        AppWidgetProviderInfo searchWidgetInfo = resolveSearchAppWidget();
422
423        // Return early if there is no search widget
424        if (searchWidgetInfo == null) return null;
425
426        // Allocate a new widget id and try and bind the app widget (if that fails, then just skip)
427        int searchWidgetId = host.allocateAppWidgetId();
428        Bundle opts = new Bundle();
429        opts.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY,
430                AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX);
431        if (!mAwm.bindAppWidgetIdIfAllowed(searchWidgetId, searchWidgetInfo.provider, opts)) {
432            return null;
433        }
434        return new Pair<Integer, AppWidgetProviderInfo>(searchWidgetId, searchWidgetInfo);
435    }
436
437    /**
438     * Returns the app widget info for the specified app widget id.
439     */
440    public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) {
441        if (mAwm == null) return null;
442
443        return mAwm.getAppWidgetInfo(appWidgetId);
444    }
445
446    /**
447     * Destroys the specified app widget.
448     */
449    public void unbindSearchAppWidget(AppWidgetHost host, int appWidgetId) {
450        if (mAwm == null) return;
451
452        // Delete the app widget
453        host.deleteAppWidgetId(appWidgetId);
454    }
455
456    /**
457     * Returns whether touch exploration is currently enabled.
458     */
459    public boolean isTouchExplorationEnabled() {
460        if (mAccm == null) return false;
461
462        return mAccm.isEnabled() && mAccm.isTouchExplorationEnabled();
463    }
464
465    /**
466     * Returns a global setting.
467     */
468    public int getGlobalSetting(Context context, String setting) {
469        ContentResolver cr = context.getContentResolver();
470        return Settings.Global.getInt(cr, setting, 0);
471    }
472
473    /**
474     * Returns a system setting.
475     */
476    public int getSystemSetting(Context context, String setting) {
477        ContentResolver cr = context.getContentResolver();
478        return Settings.System.getInt(cr, setting, 0);
479    }
480
481    /**
482     * Returns the window rect.
483     */
484    public Rect getWindowRect() {
485        Rect windowRect = new Rect();
486        if (mWm == null) return windowRect;
487
488        Point p = new Point();
489        mWm.getDefaultDisplay().getRealSize(p);
490        windowRect.set(0, 0, p.x, p.y);
491        return windowRect;
492    }
493
494    /**
495     * Locks the current task.
496     */
497    public void lockCurrentTask() {
498        if (mIam == null) return;
499
500        try {
501            mIam.startLockTaskModeOnCurrent();
502        } catch (RemoteException e) {}
503    }
504
505    /**
506     * Takes a screenshot of the current surface.
507     */
508    public Bitmap takeScreenshot() {
509        DisplayInfo di = new DisplayInfo();
510        mDisplay.getDisplayInfo(di);
511        return SurfaceControl.screenshot(di.getNaturalWidth(), di.getNaturalHeight());
512    }
513
514    /**
515     * Takes a screenshot of the current app.
516     */
517    public Bitmap takeAppScreenshot() {
518        return takeScreenshot();
519    }
520
521    /** Starts an activity from recents. */
522    public boolean startActivityFromRecents(Context context, int taskId, String taskName,
523            ActivityOptions options) {
524        if (mIam != null) {
525            try {
526                mIam.startActivityFromRecents(taskId, options == null ? null : options.toBundle());
527                return true;
528            } catch (Exception e) {
529                Console.logError(context,
530                        context.getString(R.string.recents_launch_error_message, taskName));
531            }
532        }
533        return false;
534    }
535}
536