RecentsView.java revision a26fb7822ddf3511796279b847cc216bee9e7f70
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    }
59
60    RecentsConfiguration mConfig;
61    LayoutInflater mInflater;
62
63    // The space partitioning root of this container
64    SpaceNode mBSP;
65    // Whether there are any tasks
66    boolean mHasTasks;
67    // Search bar view
68    View mSearchBar;
69    // Recents view callbacks
70    RecentsViewCallbacks mCb;
71
72    public RecentsView(Context context) {
73        super(context);
74        mConfig = RecentsConfiguration.getInstance();
75        mInflater = LayoutInflater.from(context);
76        setWillNotDraw(false);
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 startOnEnterAnimation(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.startOnEnterAnimation(ctx);
171            }
172        }
173    }
174
175    /** Requests all task stacks to start their exit-recents animation */
176    public void startOnExitAnimation(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.startOnExitAnimation(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
197    /** Adds the search bar */
198    public void setSearchBar(View searchBar) {
199        // Create the search bar (and hide it if we have no recent tasks)
200        if (Constants.DebugFlags.App.EnableSearchLayout) {
201            // Remove the previous search bar if one exists
202            if (mSearchBar != null && indexOfChild(mSearchBar) > -1) {
203                removeView(mSearchBar);
204            }
205            // Add the new search bar
206            if (searchBar != null) {
207                mSearchBar = searchBar;
208                mSearchBar.setVisibility(mHasTasks ? View.VISIBLE : View.GONE);
209                addView(mSearchBar);
210
211                if (Console.Enabled) {
212                    Console.log(Constants.Log.App.SystemUIHandshake, "[RecentsView|setSearchBar]",
213                            "" + (mSearchBar.getVisibility() == View.VISIBLE),
214                            Console.AnsiBlue);
215                }
216            }
217        }
218    }
219
220    /**
221     * This is called with the full size of the window since we are handling our own insets.
222     */
223    @Override
224    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
225        int width = MeasureSpec.getSize(widthMeasureSpec);
226        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
227        int height = MeasureSpec.getSize(heightMeasureSpec);
228        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
229
230        if (Console.Enabled) {
231            Console.log(Constants.Log.UI.MeasureAndLayout, "[RecentsView|measure]",
232                    "width: " + width + " height: " + height, Console.AnsiGreen);
233            Console.logTraceTime(Constants.Log.App.TimeRecentsStartup,
234                    Constants.Log.App.TimeRecentsStartupKey, "RecentsView.onMeasure");
235        }
236
237        // Get the search bar bounds and measure the search bar layout
238        if (mSearchBar != null) {
239            Rect searchBarSpaceBounds = new Rect();
240            mConfig.getSearchBarBounds(width, height - mConfig.systemInsets.top, searchBarSpaceBounds);
241            mSearchBar.measure(
242                    MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.width(), MeasureSpec.EXACTLY),
243                    MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.height(), MeasureSpec.EXACTLY));
244        }
245
246        // We give the full width of the space, not including the right nav bar insets in landscape,
247        // to the stack view, since we want the tasks to render under the search bar in landscape.
248        // In addition, we give it the full height, not including the top inset or search bar space,
249        // since we want the tasks to render under the navigation buttons in portrait.
250        Rect taskStackBounds = new Rect();
251        mConfig.getTaskStackBounds(width, height, taskStackBounds);
252        int childWidth = width - mConfig.systemInsets.right;
253        int childHeight = taskStackBounds.height() - mConfig.systemInsets.top;
254
255        // Measure each TaskStackView
256        int childCount = getChildCount();
257        for (int i = 0; i < childCount; i++) {
258            View child = getChildAt(i);
259            if (child instanceof TaskStackView && child.getVisibility() != GONE) {
260                child.measure(MeasureSpec.makeMeasureSpec(childWidth, widthMode),
261                        MeasureSpec.makeMeasureSpec(childHeight, heightMode));
262            }
263        }
264
265        setMeasuredDimension(width, height);
266    }
267
268    /**
269     * This is called with the full size of the window since we are handling our own insets.
270     */
271    @Override
272    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
273        if (Console.Enabled) {
274            Console.log(Constants.Log.UI.MeasureAndLayout, "[RecentsView|layout]",
275                    new Rect(left, top, right, bottom) + " changed: " + changed, Console.AnsiGreen);
276            Console.logTraceTime(Constants.Log.App.TimeRecentsStartup,
277                    Constants.Log.App.TimeRecentsStartupKey, "RecentsView.onLayout");
278        }
279
280        // Get the search bar bounds so that we lay it out
281        if (mSearchBar != null) {
282            Rect searchBarSpaceBounds = new Rect();
283            mConfig.getSearchBarBounds(getMeasuredWidth(), getMeasuredHeight(), searchBarSpaceBounds);
284            mSearchBar.layout(mConfig.systemInsets.left + searchBarSpaceBounds.left,
285                    mConfig.systemInsets.top + searchBarSpaceBounds.top,
286                    mConfig.systemInsets.left + mSearchBar.getMeasuredWidth(),
287                    mConfig.systemInsets.top + mSearchBar.getMeasuredHeight());
288        }
289
290        // We offset the stack view by the left inset (if any), but lay it out under the search bar.
291        // In addition, we offset our stack views by the top inset and search bar height, but not
292        // the bottom insets because we want it to render under the navigation buttons.
293        Rect taskStackBounds = new Rect();
294        mConfig.getTaskStackBounds(getMeasuredWidth(), getMeasuredHeight(), taskStackBounds);
295        left += mConfig.systemInsets.left;
296        top += mConfig.systemInsets.top + taskStackBounds.top;
297
298        // Layout each child
299        // XXX: Based on the space node for that task view
300        int childCount = getChildCount();
301        for (int i = 0; i < childCount; i++) {
302            View child = getChildAt(i);
303            if (child instanceof TaskStackView && child.getVisibility() != GONE) {
304                TaskStackView tsv = (TaskStackView) child;
305                child.layout(left, top, left + tsv.getMeasuredWidth(), top + tsv.getMeasuredHeight());
306            }
307        }
308    }
309
310    /** Notifies each task view of the user interaction. */
311    public void onUserInteraction() {
312        // Get the first stack view
313        TaskStackView stackView = null;
314        int childCount = getChildCount();
315        for (int i = 0; i < childCount; i++) {
316            View child = getChildAt(i);
317            if (child instanceof TaskStackView) {
318                stackView = (TaskStackView) child;
319                stackView.onUserInteraction();
320            }
321        }
322    }
323
324    /** Focuses the next task in the first stack view */
325    public void focusNextTask(boolean forward) {
326        // Get the first stack view
327        TaskStackView stackView = null;
328        int childCount = getChildCount();
329        for (int i = 0; i < childCount; i++) {
330            View child = getChildAt(i);
331            if (child instanceof TaskStackView) {
332                stackView = (TaskStackView) child;
333                break;
334            }
335        }
336
337        if (stackView != null) {
338            stackView.focusNextTask(forward);
339        }
340    }
341
342    @Override
343    protected void dispatchDraw(Canvas canvas) {
344        if (Console.Enabled) {
345            Console.log(Constants.Log.UI.Draw, "[RecentsView|dispatchDraw]", "",
346                    Console.AnsiPurple);
347        }
348        super.dispatchDraw(canvas);
349    }
350
351    @Override
352    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
353        if (Console.Enabled) {
354            Console.log(Constants.Log.UI.MeasureAndLayout,
355                    "[RecentsView|fitSystemWindows]", "insets: " + insets, Console.AnsiGreen);
356        }
357
358        // Update the configuration with the latest system insets and trigger a relayout
359        mConfig.updateSystemInsets(insets.getSystemWindowInsets());
360        requestLayout();
361
362        return insets.consumeSystemWindowInsets(false, false, false, true);
363    }
364
365    /** Unfilters any filtered stacks */
366    public boolean unfilterFilteredStacks() {
367        if (mBSP != null) {
368            // Check if there are any filtered stacks and unfilter them before we back out of Recents
369            boolean stacksUnfiltered = false;
370            ArrayList<TaskStack> stacks = mBSP.getStacks();
371            for (TaskStack stack : stacks) {
372                if (stack.hasFilteredTasks()) {
373                    stack.unfilterTasks();
374                    stacksUnfiltered = true;
375                }
376            }
377            return stacksUnfiltered;
378        }
379        return false;
380    }
381
382    /**** TaskStackView.TaskStackCallbacks Implementation ****/
383
384    @Override
385    public void onTaskLaunched(final TaskStackView stackView, final TaskView tv,
386                               final TaskStack stack, final Task task) {
387        // Notify any callbacks of the launching of a new task
388        if (mCb != null) {
389            boolean isTaskInStackBounds = false;
390            if (stackView != null && tv != null) {
391                isTaskInStackBounds = stackView.isTaskInStackBounds(tv);
392            }
393            mCb.onTaskLaunching(isTaskInStackBounds);
394        }
395
396        final Runnable launchRunnable = new Runnable() {
397            @Override
398            public void run() {
399                if (Console.Enabled) {
400                    Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask,
401                            Constants.Log.App.TimeRecentsLaunchKey, "preStartActivity");
402                }
403
404                TaskViewTransform transform;
405                View sourceView = tv;
406                int offsetX = 0;
407                int offsetY = 0;
408                int stackScroll = stackView.getStackScroll();
409                if (tv == null) {
410                    // If there is no actual task view, then use the stack view as the source view
411                    // and then offset to the expected transform rect, but bound this to just
412                    // outside the display rect (to ensure we don't animate from too far away)
413                    sourceView = stackView;
414                    transform = stackView.getStackTransform(stack.indexOfTask(task), stackScroll);
415                    offsetX = transform.rect.left;
416                    offsetY = Math.min(transform.rect.top, mConfig.displayRect.height());
417                } else {
418                    transform = stackView.getStackTransform(stack.indexOfTask(task), stackScroll);
419                }
420
421                // Compute the thumbnail to scale up from
422                ActivityOptions opts = null;
423                int thumbnailWidth = transform.rect.width();
424                int thumbnailHeight = transform.rect.height();
425                if (task.thumbnail != null && thumbnailWidth > 0 && thumbnailHeight > 0 &&
426                        task.thumbnail.getWidth() > 0 && task.thumbnail.getHeight() > 0) {
427                    // Resize the thumbnail to the size of the view that we are animating from
428                    Bitmap b = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight,
429                            Bitmap.Config.ARGB_8888);
430                    Canvas c = new Canvas(b);
431                    c.drawBitmap(task.thumbnail,
432                            new Rect(0, 0, task.thumbnail.getWidth(), task.thumbnail.getHeight()),
433                            new Rect(0, 0, thumbnailWidth, thumbnailHeight), null);
434                    c.setBitmap(null);
435                    opts = ActivityOptions.makeThumbnailScaleUpAnimation(sourceView,
436                            b, offsetX, offsetY);
437                }
438
439                if (task.isActive) {
440                    // Bring an active task to the foreground
441                    RecentsTaskLoader.getInstance().getSystemServicesProxy()
442                            .moveTaskToFront(task.key.id, opts);
443                } else {
444                    // Launch the activity anew with the desired animation
445                    Intent i = new Intent(task.key.baseIntent);
446                    i.setFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
447                            | Intent.FLAG_ACTIVITY_TASK_ON_HOME
448                            | Intent.FLAG_ACTIVITY_NEW_TASK);
449                    try {
450                        UserHandle taskUser = new UserHandle(task.userId);
451                        if (opts != null) {
452                            getContext().startActivityAsUser(i, opts.toBundle(), taskUser);
453                        } else {
454                            getContext().startActivityAsUser(i, taskUser);
455                        }
456                    } catch (ActivityNotFoundException anfe) {
457                        Console.logError(getContext(), "Could not start Activity");
458                    }
459
460                    // And clean up the old task
461                    onTaskRemoved(task);
462                }
463
464                if (Console.Enabled) {
465                    Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask,
466                            Constants.Log.App.TimeRecentsLaunchKey, "startActivity");
467                }
468            }
469        };
470
471        if (Console.Enabled) {
472            Console.logTraceTime(Constants.Log.App.TimeRecentsLaunchTask,
473                    Constants.Log.App.TimeRecentsLaunchKey, "onTaskLaunched");
474        }
475
476        // Launch the app right away if there is no task view, otherwise, animate the icon out first
477        if (tv == null) {
478            post(launchRunnable);
479        } else {
480            tv.animateOnLaunchingTask(launchRunnable);
481        }
482    }
483
484    @Override
485    public void onTaskAppInfoLaunched(Task t) {
486        // Create a new task stack with the application info details activity
487        Intent baseIntent = t.key.baseIntent;
488        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
489                Uri.fromParts("package", baseIntent.getComponent().getPackageName(), null));
490        intent.setComponent(intent.resolveActivity(getContext().getPackageManager()));
491        TaskStackBuilder.create(getContext())
492                .addNextIntentWithParentStack(intent).startActivities();
493    }
494
495    @Override
496    public void onTaskRemoved(Task t) {
497        // Remove any stored data from the loader.  We currently don't bother notifying the views
498        // that the data has been unloaded because at the point we call onTaskRemoved(), the views
499        // either don't need to be updated, or have already been removed.
500        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
501        loader.deleteTaskData(t, false);
502
503        // Remove the old task from activity manager
504        int flags = t.key.baseIntent.getFlags();
505        boolean isDocument = (flags & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) ==
506                Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
507        RecentsTaskLoader.getInstance().getSystemServicesProxy().removeTask(t.key.id,
508                isDocument);
509    }
510
511    /**** RecentsPackageMonitor.PackageCallbacks Implementation ****/
512
513    @Override
514    public void onComponentRemoved(Set<ComponentName> cns) {
515        // Propagate this event down to each task stack view
516        int childCount = getChildCount();
517        for (int i = 0; i < childCount; i++) {
518            View child = getChildAt(i);
519            if (child instanceof TaskStackView) {
520                TaskStackView stackView = (TaskStackView) child;
521                stackView.onComponentRemoved(cns);
522            }
523        }
524    }
525}
526