RecentsView.java revision e0e45bc26d02e2c6ec505ea006e7487f3a5bddc5
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.views;
18
19import android.app.ActivityOptions;
20import android.app.TaskStackBuilder;
21import android.content.ActivityNotFoundException;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.graphics.Bitmap;
26import android.graphics.Canvas;
27import android.graphics.Rect;
28import android.net.Uri;
29import android.os.UserHandle;
30import android.provider.Settings;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.WindowInsets;
34import android.widget.FrameLayout;
35import com.android.systemui.recents.Console;
36import com.android.systemui.recents.Constants;
37import com.android.systemui.recents.RecentsConfiguration;
38import com.android.systemui.recents.RecentsPackageMonitor;
39import com.android.systemui.recents.RecentsTaskLoader;
40import com.android.systemui.recents.model.SpaceNode;
41import com.android.systemui.recents.model.Task;
42import com.android.systemui.recents.model.TaskStack;
43
44import java.util.ArrayList;
45import java.util.Set;
46
47
48/**
49 * This view is the the top level layout that contains TaskStacks (which are laid out according
50 * to their SpaceNode bounds.
51 */
52public class RecentsView extends FrameLayout implements TaskStackView.TaskStackViewCallbacks,
53        RecentsPackageMonitor.PackageCallbacks {
54
55    /** The RecentsView callbacks */
56    public interface RecentsViewCallbacks {
57        public void onTaskLaunching(boolean isTaskInStackBounds);
58        public void onExitAnimationTriggered();
59    }
60
61    RecentsConfiguration mConfig;
62    LayoutInflater mInflater;
63
64    // The space partitioning root of this container
65    SpaceNode mBSP;
66    // Whether there are any tasks
67    boolean mHasTasks;
68    // Search bar view
69    View mSearchBar;
70    // Recents view callbacks
71    RecentsViewCallbacks mCb;
72
73    public RecentsView(Context context) {
74        super(context);
75        mConfig = RecentsConfiguration.getInstance();
76        mInflater = LayoutInflater.from(context);
77    }
78
79    /** Sets the callbacks */
80    public void setCallbacks(RecentsViewCallbacks cb) {
81        mCb = cb;
82    }
83
84    /** Set/get the bsp root node */
85    public void setBSP(SpaceNode n) {
86        mBSP = n;
87
88        // Create and add all the stacks for this partition of space.
89        mHasTasks = false;
90        removeAllViews();
91        ArrayList<TaskStack> stacks = mBSP.getStacks();
92        for (TaskStack stack : stacks) {
93            TaskStackView stackView = new TaskStackView(getContext(), stack);
94            stackView.setCallbacks(this);
95            addView(stackView);
96            mHasTasks |= (stack.getTaskCount() > 0);
97        }
98    }
99
100    /** Launches the focused task from the first stack if possible */
101    public boolean launchFocusedTask() {
102        // Get the first stack view
103        int childCount = getChildCount();
104        for (int i = 0; i < childCount; i++) {
105            View child = getChildAt(i);
106            if (child instanceof TaskStackView) {
107                TaskStackView stackView = (TaskStackView) child;
108                TaskStack stack = stackView.mStack;
109                // Iterate the stack views and try and find the focused task
110                int taskCount = stackView.getChildCount();
111                for (int j = 0; j < taskCount; j++) {
112                    TaskView tv = (TaskView) stackView.getChildAt(j);
113                    Task task = tv.getTask();
114                    if (tv.isFocusedTask()) {
115                        if (Console.Enabled) {
116                            Console.log(Constants.Log.UI.Focus, "[RecentsView|launchFocusedTask]",
117                                    "Found focused Task");
118                        }
119                        onTaskLaunched(stackView, tv, stack, task);
120                        return true;
121                    }
122                }
123            }
124        }
125        if (Console.Enabled) {
126            Console.log(Constants.Log.UI.Focus, "[RecentsView|launchFocusedTask]",
127                    "No Tasks focused");
128        }
129        return false;
130    }
131
132    /** Launches the first task from the first stack if possible */
133    public boolean launchFirstTask() {
134        // Get the first stack view
135        int childCount = getChildCount();
136        for (int i = 0; i < childCount; i++) {
137            View child = getChildAt(i);
138            if (child instanceof TaskStackView) {
139                TaskStackView stackView = (TaskStackView) child;
140                TaskStack stack = stackView.mStack;
141                ArrayList<Task> tasks = stack.getTasks();
142
143                // Get the first task in the stack
144                if (!tasks.isEmpty()) {
145                    Task task = tasks.get(tasks.size() - 1);
146                    TaskView tv = null;
147
148                    // Try and use the first child task view as the source of the launch animation
149                    if (stackView.getChildCount() > 0) {
150                        TaskView stv = (TaskView) stackView.getChildAt(stackView.getChildCount() - 1);
151                        if (stv.getTask() == task) {
152                            tv = stv;
153                        }
154                    }
155                    onTaskLaunched(stackView, tv, stack, task);
156                    return true;
157                }
158            }
159        }
160        return false;
161    }
162
163    /** Requests all task stacks to start their enter-recents animation */
164    public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) {
165        int childCount = getChildCount();
166        for (int i = 0; i < childCount; i++) {
167            View child = getChildAt(i);
168            if (child instanceof TaskStackView) {
169                TaskStackView stackView = (TaskStackView) child;
170                stackView.startEnterRecentsAnimation(ctx);
171            }
172        }
173    }
174
175    /** Requests all task stacks to start their exit-recents animation */
176    public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) {
177        // Handle the case when there are no views by incrementing and decrementing after all
178        // animations are started.
179        ctx.postAnimationTrigger.increment();
180
181        if (Constants.DebugFlags.App.EnableHomeTransition) {
182            int childCount = getChildCount();
183            for (int i = 0; i < childCount; i++) {
184                View child = getChildAt(i);
185                if (child instanceof TaskStackView) {
186                    TaskStackView stackView = (TaskStackView) child;
187                    stackView.startExitToHomeAnimation(ctx);
188                }
189            }
190        }
191
192        // Handle the case when there are no views by incrementing and decrementing after all
193        // animations are started.
194        ctx.postAnimationTrigger.decrement();
195
196        // Notify of the exit animation
197        mCb.onExitAnimationTriggered();
198    }
199
200    /** Adds the search bar */
201    public void setSearchBar(View searchBar) {
202        // Create the search bar (and hide it if we have no recent tasks)
203        if (Constants.DebugFlags.App.EnableSearchLayout) {
204            // Remove the previous search bar if one exists
205            if (mSearchBar != null && indexOfChild(mSearchBar) > -1) {
206                removeView(mSearchBar);
207            }
208            // Add the new search bar
209            if (searchBar != null) {
210                mSearchBar = searchBar;
211                mSearchBar.setVisibility(mHasTasks ? View.VISIBLE : View.GONE);
212                addView(mSearchBar);
213
214                if (Console.Enabled) {
215                    Console.log(Constants.Log.App.SystemUIHandshake, "[RecentsView|setSearchBar]",
216                            "" + (mSearchBar.getVisibility() == View.VISIBLE),
217                            Console.AnsiBlue);
218                }
219            }
220        }
221    }
222
223    /**
224     * This is called with the full size of the window since we are handling our own insets.
225     */
226    @Override
227    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
228        int width = MeasureSpec.getSize(widthMeasureSpec);
229        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
230        int height = MeasureSpec.getSize(heightMeasureSpec);
231        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
232
233        if (Console.Enabled) {
234            Console.log(Constants.Log.UI.MeasureAndLayout, "[RecentsView|measure]",
235                    "width: " + width + " height: " + height, Console.AnsiGreen);
236            Console.logTraceTime(Constants.Log.App.TimeRecentsStartup,
237                    Constants.Log.App.TimeRecentsStartupKey, "RecentsView.onMeasure");
238        }
239
240        // Get the search bar bounds and measure the search bar layout
241        if (mSearchBar != null) {
242            Rect searchBarSpaceBounds = new Rect();
243            mConfig.getSearchBarBounds(width, height - mConfig.systemInsets.top, searchBarSpaceBounds);
244            mSearchBar.measure(
245                    MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.width(), MeasureSpec.EXACTLY),
246                    MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.height(), MeasureSpec.EXACTLY));
247        }
248
249        // We give the full width of the space, not including the right nav bar insets in landscape,
250        // to the stack view, since we want the tasks to render under the search bar in landscape.
251        // In addition, we give it the full height, not including the top inset or search bar space,
252        // since we want the tasks to render under the navigation buttons in portrait.
253        Rect taskStackBounds = new Rect();
254        mConfig.getTaskStackBounds(width, height, taskStackBounds);
255        int childWidth = width - mConfig.systemInsets.right;
256        int childHeight = taskStackBounds.height() - mConfig.systemInsets.top;
257
258        // Measure each TaskStackView
259        int childCount = getChildCount();
260        for (int i = 0; i < childCount; i++) {
261            View child = getChildAt(i);
262            if (child instanceof TaskStackView && child.getVisibility() != GONE) {
263                child.measure(MeasureSpec.makeMeasureSpec(childWidth, widthMode),
264                        MeasureSpec.makeMeasureSpec(childHeight, heightMode));
265            }
266        }
267
268        setMeasuredDimension(width, height);
269    }
270
271    /**
272     * This is called with the full size of the window since we are handling our own insets.
273     */
274    @Override
275    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
276        if (Console.Enabled) {
277            Console.log(Constants.Log.UI.MeasureAndLayout, "[RecentsView|layout]",
278                    new Rect(left, top, right, bottom) + " changed: " + changed, Console.AnsiGreen);
279            Console.logTraceTime(Constants.Log.App.TimeRecentsStartup,
280                    Constants.Log.App.TimeRecentsStartupKey, "RecentsView.onLayout");
281        }
282
283        // Get the search bar bounds so that we lay it out
284        if (mSearchBar != null) {
285            Rect searchBarSpaceBounds = new Rect();
286            mConfig.getSearchBarBounds(getMeasuredWidth(), getMeasuredHeight(), searchBarSpaceBounds);
287            mSearchBar.layout(mConfig.systemInsets.left + searchBarSpaceBounds.left,
288                    mConfig.systemInsets.top + searchBarSpaceBounds.top,
289                    mConfig.systemInsets.left + mSearchBar.getMeasuredWidth(),
290                    mConfig.systemInsets.top + mSearchBar.getMeasuredHeight());
291        }
292
293        // We offset the stack view by the left inset (if any), but lay it out under the search bar.
294        // In addition, we offset our stack views by the top inset and search bar height, but not
295        // the bottom insets because we want it to render under the navigation buttons.
296        Rect taskStackBounds = new Rect();
297        mConfig.getTaskStackBounds(getMeasuredWidth(), getMeasuredHeight(), taskStackBounds);
298        left += mConfig.systemInsets.left;
299        top += mConfig.systemInsets.top + taskStackBounds.top;
300
301        // Layout each child
302        // XXX: Based on the space node for that task view
303        int childCount = getChildCount();
304        for (int i = 0; i < childCount; i++) {
305            View child = getChildAt(i);
306            if (child instanceof TaskStackView && child.getVisibility() != GONE) {
307                TaskStackView tsv = (TaskStackView) child;
308                child.layout(left, top, left + tsv.getMeasuredWidth(), top + tsv.getMeasuredHeight());
309            }
310        }
311    }
312
313    /** Notifies each task view of the user interaction. */
314    public void onUserInteraction() {
315        // Get the first stack view
316        TaskStackView stackView = null;
317        int childCount = getChildCount();
318        for (int i = 0; i < childCount; i++) {
319            View child = getChildAt(i);
320            if (child instanceof TaskStackView) {
321                stackView = (TaskStackView) child;
322                stackView.onUserInteraction();
323            }
324        }
325    }
326
327    /** Focuses the next task in the first stack view */
328    public void focusNextTask(boolean forward) {
329        // Get the first stack view
330        TaskStackView stackView = null;
331        int childCount = getChildCount();
332        for (int i = 0; i < childCount; i++) {
333            View child = getChildAt(i);
334            if (child instanceof TaskStackView) {
335                stackView = (TaskStackView) child;
336                break;
337            }
338        }
339
340        if (stackView != null) {
341            stackView.focusNextTask(forward);
342        }
343    }
344
345    @Override
346    protected void dispatchDraw(Canvas canvas) {
347        if (Console.Enabled) {
348            Console.log(Constants.Log.UI.Draw, "[RecentsView|dispatchDraw]", "",
349                    Console.AnsiPurple);
350        }
351        super.dispatchDraw(canvas);
352    }
353
354    @Override
355    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
356        if (Console.Enabled) {
357            Console.log(Constants.Log.UI.MeasureAndLayout,
358                    "[RecentsView|fitSystemWindows]", "insets: " + insets, Console.AnsiGreen);
359        }
360
361        // Update the configuration with the latest system insets and trigger a relayout
362        mConfig.updateSystemInsets(insets.getSystemWindowInsets());
363        requestLayout();
364
365        return insets.consumeSystemWindowInsets(false, false, false, true);
366    }
367
368    /** Unfilters any filtered stacks */
369    public boolean unfilterFilteredStacks() {
370        if (mBSP != null) {
371            // Check if there are any filtered stacks and unfilter them before we back out of Recents
372            boolean stacksUnfiltered = false;
373            ArrayList<TaskStack> stacks = mBSP.getStacks();
374            for (TaskStack stack : stacks) {
375                if (stack.hasFilteredTasks()) {
376                    stack.unfilterTasks();
377                    stacksUnfiltered = true;
378                }
379            }
380            return stacksUnfiltered;
381        }
382        return false;
383    }
384
385    /**** TaskStackView.TaskStackCallbacks Implementation ****/
386
387    @Override
388    public void onTaskLaunched(final TaskStackView stackView, final TaskView tv,
389                               final TaskStack stack, final Task task) {
390        // Notify any callbacks of the launching of a new task
391        if (mCb != null) {
392            boolean isTaskInStackBounds = false;
393            if (stackView != null && tv != null) {
394                isTaskInStackBounds = stackView.isTaskInStackBounds(tv);
395            }
396            mCb.onTaskLaunching(isTaskInStackBounds);
397        }
398
399        // Upfront the processing of the thumbnail
400        TaskViewTransform transform;
401        View sourceView = tv;
402        int offsetX = 0;
403        int offsetY = 0;
404        int stackScroll = stackView.getStackScroll();
405        if (tv == null) {
406            // If there is no actual task view, then use the stack view as the source view
407            // and then offset to the expected transform rect, but bound this to just
408            // outside the display rect (to ensure we don't animate from too far away)
409            sourceView = stackView;
410            transform = stackView.getStackTransform(stack.indexOfTask(task), stackScroll);
411            offsetX = transform.rect.left;
412            offsetY = Math.min(transform.rect.top, mConfig.displayRect.height());
413        } else {
414            transform = stackView.getStackTransform(stack.indexOfTask(task), stackScroll);
415        }
416
417        // Compute the thumbnail to scale up from
418        ActivityOptions opts = null;
419        int thumbnailWidth = transform.rect.width();
420        int thumbnailHeight = transform.rect.height();
421        if (task.thumbnail != null && thumbnailWidth > 0 && thumbnailHeight > 0 &&
422                task.thumbnail.getWidth() > 0 && task.thumbnail.getHeight() > 0) {
423            // Resize the thumbnail to the size of the view that we are animating from
424            Bitmap b = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight,
425                    Bitmap.Config.ARGB_8888);
426            Canvas c = new Canvas(b);
427            c.drawBitmap(task.thumbnail,
428                    new Rect(0, 0, task.thumbnail.getWidth(), task.thumbnail.getHeight()),
429                    new Rect(0, 0, thumbnailWidth, thumbnailHeight), null);
430            c.setBitmap(null);
431            opts = ActivityOptions.makeThumbnailScaleUpAnimation(sourceView,
432                    b, offsetX, offsetY);
433        }
434
435        final ActivityOptions launchOpts = opts;
436        final Runnable launchRunnable = new Runnable() {
437            @Override
438            public void run() {
439                if (Console.Enabled) {
440                    Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask,
441                            Constants.Log.App.TimeRecentsLaunchKey, "preStartActivity");
442                }
443
444                if (task.isActive) {
445                    // Bring an active task to the foreground
446                    RecentsTaskLoader.getInstance().getSystemServicesProxy()
447                            .moveTaskToFront(task.key.id, launchOpts);
448                } else {
449                    // Launch the activity anew with the desired animation
450                    Intent i = new Intent(task.key.baseIntent);
451                    i.setFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
452                            | Intent.FLAG_ACTIVITY_TASK_ON_HOME
453                            | Intent.FLAG_ACTIVITY_NEW_TASK);
454                    try {
455                        UserHandle taskUser = new UserHandle(task.userId);
456                        if (launchOpts != null) {
457                            getContext().startActivityAsUser(i, launchOpts.toBundle(), taskUser);
458                        } else {
459                            getContext().startActivityAsUser(i, taskUser);
460                        }
461                    } catch (ActivityNotFoundException anfe) {
462                        Console.logError(getContext(), "Could not start Activity");
463                    }
464
465                    // And clean up the old task
466                    onTaskRemoved(task);
467                }
468
469                if (Console.Enabled) {
470                    Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask,
471                            Constants.Log.App.TimeRecentsLaunchKey, "startActivity");
472                }
473            }
474        };
475
476        if (Console.Enabled) {
477            Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask,
478                    Constants.Log.App.TimeRecentsLaunchKey, "onTaskLaunched");
479        }
480
481        // Launch the app right away if there is no task view, otherwise, animate the icon out first
482        if (tv == null) {
483            post(launchRunnable);
484        } else {
485            stackView.animateOnLaunchingTask(tv, launchRunnable);
486        }
487    }
488
489    @Override
490    public void onTaskAppInfoLaunched(Task t) {
491        // Create a new task stack with the application info details activity
492        Intent baseIntent = t.key.baseIntent;
493        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
494                Uri.fromParts("package", baseIntent.getComponent().getPackageName(), null));
495        intent.setComponent(intent.resolveActivity(getContext().getPackageManager()));
496        TaskStackBuilder.create(getContext())
497                .addNextIntentWithParentStack(intent).startActivities();
498    }
499
500    @Override
501    public void onTaskRemoved(Task t) {
502        // Remove any stored data from the loader.  We currently don't bother notifying the views
503        // that the data has been unloaded because at the point we call onTaskRemoved(), the views
504        // either don't need to be updated, or have already been removed.
505        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
506        loader.deleteTaskData(t, false);
507
508        // Remove the old task from activity manager
509        int flags = t.key.baseIntent.getFlags();
510        boolean isDocument = (flags & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) ==
511                Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
512        RecentsTaskLoader.getInstance().getSystemServicesProxy().removeTask(t.key.id,
513                isDocument);
514    }
515
516    /**** RecentsPackageMonitor.PackageCallbacks Implementation ****/
517
518    @Override
519    public void onComponentRemoved(Set<ComponentName> cns) {
520        // Propagate this event down to each task stack view
521        int childCount = getChildCount();
522        for (int i = 0; i < childCount; i++) {
523            View child = getChildAt(i);
524            if (child instanceof TaskStackView) {
525                TaskStackView stackView = (TaskStackView) child;
526                stackView.onComponentRemoved(cns);
527            }
528        }
529    }
530}
531