RecentsView.java revision e1e20e11a758ee82753e8052773664823ca74746
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.Context;
22import android.content.Intent;
23import android.graphics.Bitmap;
24import android.graphics.Canvas;
25import android.graphics.Rect;
26import android.net.Uri;
27import android.os.UserHandle;
28import android.provider.Settings;
29import android.util.AttributeSet;
30import android.view.LayoutInflater;
31import android.view.View;
32import android.view.ViewStub;
33import android.view.WindowInsets;
34import android.widget.FrameLayout;
35import com.android.systemui.R;
36import com.android.systemui.recents.Constants;
37import com.android.systemui.recents.RecentsAppWidgetHostView;
38import com.android.systemui.recents.RecentsConfiguration;
39import com.android.systemui.recents.misc.SystemServicesProxy;
40import com.android.systemui.recents.model.RecentsPackageMonitor;
41import com.android.systemui.recents.model.RecentsTaskLoader;
42import com.android.systemui.recents.model.Task;
43import com.android.systemui.recents.model.TaskStack;
44
45import java.util.ArrayList;
46import java.util.List;
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 onTaskViewClicked();
58        public void onTaskLaunchFailed();
59        public void onAllTaskViewsDismissed();
60        public void onExitToHomeAnimationTriggered();
61        public void onScreenPinningRequest();
62
63        public void onTaskResize(Task t);
64    }
65
66    RecentsConfiguration mConfig;
67    LayoutInflater mInflater;
68    DebugOverlayView mDebugOverlay;
69    RecentsViewLayoutAlgorithm mLayoutAlgorithm;
70
71    ArrayList<TaskStack> mStacks;
72    List<TaskStackView> mTaskStackViews = new ArrayList<>();
73    RecentsAppWidgetHostView mSearchBar;
74    RecentsViewCallbacks mCb;
75
76    public RecentsView(Context context) {
77        super(context);
78    }
79
80    public RecentsView(Context context, AttributeSet attrs) {
81        this(context, attrs, 0);
82    }
83
84    public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
85        this(context, attrs, defStyleAttr, 0);
86    }
87
88    public RecentsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
89        super(context, attrs, defStyleAttr, defStyleRes);
90        mConfig = RecentsConfiguration.getInstance();
91        mInflater = LayoutInflater.from(context);
92        mLayoutAlgorithm = new RecentsViewLayoutAlgorithm(mConfig);
93    }
94
95    /** Sets the callbacks */
96    public void setCallbacks(RecentsViewCallbacks cb) {
97        mCb = cb;
98    }
99
100    /** Sets the debug overlay */
101    public void setDebugOverlay(DebugOverlayView overlay) {
102        mDebugOverlay = overlay;
103    }
104
105    /** Set/get the bsp root node */
106    public void setTaskStacks(ArrayList<TaskStack> stacks) {
107        int numStacks = stacks.size();
108
109        // Remove all/extra stack views
110        int numTaskStacksToKeep = 0; // Keep no tasks if we are recreating the layout
111        if (mConfig.launchedReuseTaskStackViews) {
112            numTaskStacksToKeep = Math.min(mTaskStackViews.size(), numStacks);
113        }
114        for (int i = mTaskStackViews.size() - 1; i >= numTaskStacksToKeep; i--) {
115            removeView(mTaskStackViews.remove(i));
116        }
117
118        // Update the stack views that we are keeping
119        for (int i = 0; i < numTaskStacksToKeep; i++) {
120            TaskStackView tsv = mTaskStackViews.get(i);
121            // If onRecentsHidden is not triggered, we need to the stack view again here
122            tsv.reset();
123            tsv.setStack(stacks.get(i));
124        }
125
126        // Add remaining/recreate stack views
127        mStacks = stacks;
128        for (int i = mTaskStackViews.size(); i < numStacks; i++) {
129            TaskStack stack = stacks.get(i);
130            TaskStackView stackView = new TaskStackView(getContext(), stack);
131            stackView.setCallbacks(this);
132            addView(stackView);
133            mTaskStackViews.add(stackView);
134        }
135
136        // Enable debug mode drawing on all the stacks if necessary
137        if (mConfig.debugModeEnabled) {
138            for (int i = mTaskStackViews.size() - 1; i >= 0; i--) {
139                TaskStackView stackView = mTaskStackViews.get(i);
140                stackView.setDebugOverlay(mDebugOverlay);
141            }
142        }
143
144        // Trigger a new layout
145        requestLayout();
146    }
147
148    /** Gets the list of task views */
149    List<TaskStackView> getTaskStackViews() {
150        return mTaskStackViews;
151    }
152
153    /** Gets the next task in the stack - or if the last - the top task */
154    public Task getNextTaskOrTopTask(Task taskToSearch) {
155        Task returnTask = null;
156        boolean found = false;
157        List<TaskStackView> stackViews = getTaskStackViews();
158        int stackCount = stackViews.size();
159        for (int i = stackCount - 1; i >= 0; --i) {
160            TaskStack stack = stackViews.get(i).getStack();
161            ArrayList<Task> taskList = stack.getTasks();
162            // Iterate the stack views and try and find the focused task
163            for (int j = taskList.size() - 1; j >= 0; --j) {
164                Task task = taskList.get(j);
165                // Return the next task in the line.
166                if (found)
167                    return task;
168                // Remember the first possible task as the top task.
169                if (returnTask == null)
170                    returnTask = task;
171                if (task == taskToSearch)
172                    found = true;
173            }
174        }
175        return returnTask;
176    }
177
178    /** Launches the focused task from the first stack if possible */
179    public boolean launchFocusedTask() {
180        // Get the first stack view
181        List<TaskStackView> stackViews = getTaskStackViews();
182        int stackCount = stackViews.size();
183        for (int i = 0; i < stackCount; i++) {
184            TaskStackView stackView = stackViews.get(i);
185            TaskStack stack = stackView.getStack();
186            // Iterate the stack views and try and find the focused task
187            List<TaskView> taskViews = stackView.getTaskViews();
188            int taskViewCount = taskViews.size();
189            for (int j = 0; j < taskViewCount; j++) {
190                TaskView tv = taskViews.get(j);
191                Task task = tv.getTask();
192                if (tv.isFocusedTask()) {
193                    onTaskViewClicked(stackView, tv, stack, task, false);
194                    return true;
195                }
196            }
197        }
198        return false;
199    }
200
201    /** Launches a given task. */
202    public boolean launchTask(Task task) {
203        // Get the first stack view
204        List<TaskStackView> stackViews = getTaskStackViews();
205        int stackCount = stackViews.size();
206        for (int i = 0; i < stackCount; i++) {
207            TaskStackView stackView = stackViews.get(i);
208            TaskStack stack = stackView.getStack();
209            // Iterate the stack views and try and find the given task.
210            List<TaskView> taskViews = stackView.getTaskViews();
211            int taskViewCount = taskViews.size();
212            for (int j = 0; j < taskViewCount; j++) {
213                TaskView tv = taskViews.get(j);
214                if (tv.getTask() == task) {
215                    onTaskViewClicked(stackView, tv, stack, task, false);
216                    return true;
217                }
218            }
219        }
220        return false;
221    }
222
223    /** Launches the task that Recents was launched from, if possible */
224    public boolean launchPreviousTask() {
225        // Get the first stack view
226        List<TaskStackView> stackViews = getTaskStackViews();
227        int stackCount = stackViews.size();
228        for (int i = 0; i < stackCount; i++) {
229            TaskStackView stackView = stackViews.get(i);
230            TaskStack stack = stackView.getStack();
231            ArrayList<Task> tasks = stack.getTasks();
232
233            // Find the launch task in the stack
234            if (!tasks.isEmpty()) {
235                int taskCount = tasks.size();
236                for (int j = 0; j < taskCount; j++) {
237                    if (tasks.get(j).isLaunchTarget) {
238                        Task task = tasks.get(j);
239                        TaskView tv = stackView.getChildViewForTask(task);
240                        onTaskViewClicked(stackView, tv, stack, task, false);
241                        return true;
242                    }
243                }
244            }
245        }
246        return false;
247    }
248
249    /** Requests all task stacks to start their enter-recents animation */
250    public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) {
251        // We have to increment/decrement the post animation trigger in case there are no children
252        // to ensure that it runs
253        ctx.postAnimationTrigger.increment();
254
255        List<TaskStackView> stackViews = getTaskStackViews();
256        int stackCount = stackViews.size();
257        for (int i = 0; i < stackCount; i++) {
258            TaskStackView stackView = stackViews.get(i);
259            stackView.startEnterRecentsAnimation(ctx);
260        }
261        ctx.postAnimationTrigger.decrement();
262    }
263
264    /** Requests all task stacks to start their exit-recents animation */
265    public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) {
266        // We have to increment/decrement the post animation trigger in case there are no children
267        // to ensure that it runs
268        ctx.postAnimationTrigger.increment();
269        List<TaskStackView> stackViews = getTaskStackViews();
270        int stackCount = stackViews.size();
271        for (int i = 0; i < stackCount; i++) {
272            TaskStackView stackView = stackViews.get(i);
273            stackView.startExitToHomeAnimation(ctx);
274        }
275        ctx.postAnimationTrigger.decrement();
276
277        // Notify of the exit animation
278        mCb.onExitToHomeAnimationTriggered();
279    }
280
281    /** Adds the search bar */
282    public void setSearchBar(RecentsAppWidgetHostView searchBar) {
283        // Create the search bar (and hide it if we have no recent tasks)
284        if (Constants.DebugFlags.App.EnableSearchLayout) {
285            // Remove the previous search bar if one exists
286            if (mSearchBar != null && indexOfChild(mSearchBar) > -1) {
287                removeView(mSearchBar);
288            }
289            // Add the new search bar
290            if (searchBar != null) {
291                mSearchBar = searchBar;
292                addView(mSearchBar);
293            }
294        }
295    }
296
297    /** Returns whether there is currently a search bar */
298    public boolean hasValidSearchBar() {
299        return mSearchBar != null && !mSearchBar.isReinflateRequired();
300    }
301
302    /** Sets the visibility of the search bar */
303    public void setSearchBarVisibility(int visibility) {
304        if (mSearchBar != null) {
305            mSearchBar.setVisibility(visibility);
306            // Always bring the search bar to the top
307            mSearchBar.bringToFront();
308        }
309    }
310
311    /**
312     * This is called with the full size of the window since we are handling our own insets.
313     */
314    @Override
315    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
316        int width = MeasureSpec.getSize(widthMeasureSpec);
317        int height = MeasureSpec.getSize(heightMeasureSpec);
318
319        // Get the search bar bounds and measure the search bar layout
320        if (mSearchBar != null) {
321            Rect searchBarSpaceBounds = new Rect();
322            mConfig.getSearchBarBounds(width, height, mConfig.systemInsets.top, searchBarSpaceBounds);
323            mSearchBar.measure(
324                    MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.width(), MeasureSpec.EXACTLY),
325                    MeasureSpec.makeMeasureSpec(searchBarSpaceBounds.height(), MeasureSpec.EXACTLY));
326        }
327
328        Rect taskStackBounds = new Rect();
329        mConfig.getAvailableTaskStackBounds(width, height, mConfig.systemInsets.top,
330                mConfig.systemInsets.right, taskStackBounds);
331
332        // Measure each TaskStackView with the full width and height of the window since the
333        // transition view is a child of that stack view
334        List<TaskStackView> stackViews = getTaskStackViews();
335        List<Rect> stackViewsBounds = mLayoutAlgorithm.computeStackRects(stackViews,
336                taskStackBounds);
337        int stackCount = stackViews.size();
338        for (int i = 0; i < stackCount; i++) {
339            TaskStackView stackView = stackViews.get(i);
340            if (stackView.getVisibility() != GONE) {
341                // We are going to measure the TaskStackView with the whole RecentsView dimensions,
342                // but the actual stack is going to be inset to the bounds calculated by the layout
343                // algorithm
344                stackView.setStackInsetRect(stackViewsBounds.get(i));
345                stackView.measure(widthMeasureSpec, heightMeasureSpec);
346            }
347        }
348
349        setMeasuredDimension(width, height);
350    }
351
352    /**
353     * This is called with the full size of the window since we are handling our own insets.
354     */
355    @Override
356    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
357        // Get the search bar bounds so that we lay it out
358        if (mSearchBar != null) {
359            Rect searchBarSpaceBounds = new Rect();
360            mConfig.getSearchBarBounds(getMeasuredWidth(), getMeasuredHeight(),
361                    mConfig.systemInsets.top, searchBarSpaceBounds);
362            mSearchBar.layout(searchBarSpaceBounds.left, searchBarSpaceBounds.top,
363                    searchBarSpaceBounds.right, searchBarSpaceBounds.bottom);
364        }
365
366        // Layout each TaskStackView with the full width and height of the window since the
367        // transition view is a child of that stack view
368        List<TaskStackView> stackViews = getTaskStackViews();
369        int stackCount = stackViews.size();
370        for (int i = 0; i < stackCount; i++) {
371            TaskStackView stackView = stackViews.get(i);
372            if (stackView.getVisibility() != GONE) {
373                stackView.layout(left, top, left + stackView.getMeasuredWidth(),
374                        top + stackView.getMeasuredHeight());
375            }
376        }
377    }
378
379    @Override
380    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
381        // Update the configuration with the latest system insets and trigger a relayout
382        mConfig.updateSystemInsets(insets.getSystemWindowInsets());
383        requestLayout();
384        return insets.consumeSystemWindowInsets();
385    }
386
387    /** Notifies each task view of the user interaction. */
388    public void onUserInteraction() {
389        // Get the first stack view
390        List<TaskStackView> stackViews = getTaskStackViews();
391        int stackCount = stackViews.size();
392        for (int i = 0; i < stackCount; i++) {
393            TaskStackView stackView = stackViews.get(i);
394            stackView.onUserInteraction();
395        }
396    }
397
398    /** Focuses the next task in the first stack view */
399    public void focusNextTask(boolean forward) {
400        // Get the first stack view
401        List<TaskStackView> stackViews = getTaskStackViews();
402        if (!stackViews.isEmpty()) {
403            stackViews.get(0).focusNextTask(forward, true);
404        }
405    }
406
407    /** Dismisses the focused task. */
408    public void dismissFocusedTask() {
409        // Get the first stack view
410        List<TaskStackView> stackViews = getTaskStackViews();
411        if (!stackViews.isEmpty()) {
412            stackViews.get(0).dismissFocusedTask();
413        }
414    }
415
416    /** Unfilters any filtered stacks */
417    public boolean unfilterFilteredStacks() {
418        if (mStacks != null) {
419            // Check if there are any filtered stacks and unfilter them before we back out of Recents
420            boolean stacksUnfiltered = false;
421            int numStacks = mStacks.size();
422            for (int i = 0; i < numStacks; i++) {
423                TaskStack stack = mStacks.get(i);
424                if (stack.hasFilteredTasks()) {
425                    stack.unfilterTasks();
426                    stacksUnfiltered = true;
427                }
428            }
429            return stacksUnfiltered;
430        }
431        return false;
432    }
433
434    /**** TaskStackView.TaskStackCallbacks Implementation ****/
435
436    @Override
437    public void onTaskViewClicked(final TaskStackView stackView, final TaskView tv,
438                                  final TaskStack stack, final Task task, final boolean lockToTask) {
439        // Notify any callbacks of the launching of a new task
440        if (mCb != null) {
441            mCb.onTaskViewClicked();
442        }
443
444        // Upfront the processing of the thumbnail
445        TaskViewTransform transform = new TaskViewTransform();
446        View sourceView;
447        int offsetX = 0;
448        int offsetY = 0;
449        float stackScroll = stackView.getScroller().getStackScroll();
450        if (tv == null) {
451            // If there is no actual task view, then use the stack view as the source view
452            // and then offset to the expected transform rect, but bound this to just
453            // outside the display rect (to ensure we don't animate from too far away)
454            sourceView = stackView;
455            transform = stackView.getStackAlgorithm().getStackTransform(task, stackScroll, transform, null);
456            offsetX = transform.rect.left;
457            offsetY = mConfig.displayRect.height();
458        } else {
459            sourceView = tv.mThumbnailView;
460            transform = stackView.getStackAlgorithm().getStackTransform(task, stackScroll, transform, null);
461        }
462
463        // Compute the thumbnail to scale up from
464        final SystemServicesProxy ssp =
465                RecentsTaskLoader.getInstance().getSystemServicesProxy();
466        ActivityOptions opts = null;
467        if (task.thumbnail != null && task.thumbnail.getWidth() > 0 &&
468                task.thumbnail.getHeight() > 0) {
469            Bitmap b;
470            if (tv != null) {
471                // Disable any focused state before we draw the header
472                if (tv.isFocusedTask()) {
473                    tv.unsetFocusedTask();
474                }
475
476                float scale = tv.getScaleX();
477                int fromHeaderWidth = (int) (tv.mHeaderView.getMeasuredWidth() * scale);
478                int fromHeaderHeight = (int) (tv.mHeaderView.getMeasuredHeight() * scale);
479                b = Bitmap.createBitmap(fromHeaderWidth, fromHeaderHeight,
480                        Bitmap.Config.ARGB_8888);
481                if (Constants.DebugFlags.App.EnableTransitionThumbnailDebugMode) {
482                    b.eraseColor(0xFFff0000);
483                } else {
484                    Canvas c = new Canvas(b);
485                    c.scale(tv.getScaleX(), tv.getScaleY());
486                    tv.mHeaderView.draw(c);
487                    c.setBitmap(null);
488                }
489            } else {
490                // Notify the system to skip the thumbnail layer by using an ALPHA_8 bitmap
491                b = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
492            }
493            Bitmap bImmut = b.createAshmemBitmap();
494            ActivityOptions.OnAnimationStartedListener animStartedListener = null;
495            if (lockToTask) {
496                animStartedListener = new ActivityOptions.OnAnimationStartedListener() {
497                    boolean mTriggered = false;
498                    @Override
499                    public void onAnimationStarted() {
500                        if (!mTriggered) {
501                            postDelayed(new Runnable() {
502                                @Override
503                                public void run() {
504                                    mCb.onScreenPinningRequest();
505                                }
506                            }, 350);
507                            mTriggered = true;
508                        }
509                    }
510                };
511            }
512            if (mConfig.multiStackEnabled) {
513                opts = ActivityOptions.makeCustomAnimation(sourceView.getContext(),
514                        R.anim.recents_from_unknown_enter,
515                        R.anim.recents_from_unknown_exit,
516                        sourceView.getHandler(), animStartedListener);
517            } else {
518                opts = ActivityOptions.makeThumbnailAspectScaleUpAnimation(sourceView,
519                        bImmut, offsetX, offsetY, transform.rect.width(), transform.rect.height(),
520                        sourceView.getHandler(), animStartedListener);
521            }
522        }
523
524        final ActivityOptions launchOpts = opts;
525        final Runnable launchRunnable = new Runnable() {
526            @Override
527            public void run() {
528                if (task.isActive) {
529                    // Bring an active task to the foreground
530                    ssp.moveTaskToFront(task.key.id, launchOpts);
531                } else {
532                    if (ssp.startActivityFromRecents(getContext(), task.key.id,
533                            task.activityLabel, launchOpts)) {
534                        if (launchOpts == null && lockToTask) {
535                            mCb.onScreenPinningRequest();
536                        }
537                    } else {
538                        // Dismiss the task and return the user to home if we fail to
539                        // launch the task
540                        onTaskViewDismissed(task);
541                        if (mCb != null) {
542                            mCb.onTaskLaunchFailed();
543                        }
544                    }
545                }
546            }
547        };
548
549        // Launch the app right away if there is no task view, otherwise, animate the icon out first
550        if (tv == null) {
551            launchRunnable.run();
552        } else {
553            if (task.group != null && !task.group.isFrontMostTask(task)) {
554                // For affiliated tasks that are behind other tasks, we must animate the front cards
555                // out of view before starting the task transition
556                stackView.startLaunchTaskAnimation(tv, launchRunnable, lockToTask);
557            } else {
558                // Otherwise, we can start the task transition immediately
559                stackView.startLaunchTaskAnimation(tv, null, lockToTask);
560                launchRunnable.run();
561            }
562        }
563    }
564
565    @Override
566    public void onTaskViewAppInfoClicked(Task t) {
567        // Create a new task stack with the application info details activity
568        Intent baseIntent = t.key.baseIntent;
569        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
570                Uri.fromParts("package", baseIntent.getComponent().getPackageName(), null));
571        intent.setComponent(intent.resolveActivity(getContext().getPackageManager()));
572        TaskStackBuilder.create(getContext())
573                .addNextIntentWithParentStack(intent).startActivities(null,
574                new UserHandle(t.key.userId));
575    }
576
577    @Override
578    public void onTaskViewDismissed(Task t) {
579        // Remove any stored data from the loader.  We currently don't bother notifying the views
580        // that the data has been unloaded because at the point we call onTaskViewDismissed(), the views
581        // either don't need to be updated, or have already been removed.
582        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
583        loader.deleteTaskData(t, false);
584
585        // Remove the old task from activity manager
586        loader.getSystemServicesProxy().removeTask(t.key.id);
587    }
588
589    @Override
590    public void onAllTaskViewsDismissed(ArrayList<Task> removedTasks) {
591        if (removedTasks != null) {
592            int taskCount = removedTasks.size();
593            for (int i = 0; i < taskCount; i++) {
594                onTaskViewDismissed(removedTasks.get(i));
595            }
596        }
597
598        mCb.onAllTaskViewsDismissed();
599    }
600
601    /** Final callback after Recents is finally hidden. */
602    public void onRecentsHidden() {
603        // Notify each task stack view
604        List<TaskStackView> stackViews = getTaskStackViews();
605        int stackCount = stackViews.size();
606        for (int i = 0; i < stackCount; i++) {
607            TaskStackView stackView = stackViews.get(i);
608            stackView.onRecentsHidden();
609        }
610    }
611
612    @Override
613    public void onTaskStackFilterTriggered() {
614        // Hide the search bar
615        if (mSearchBar != null) {
616            mSearchBar.animate()
617                    .alpha(0f)
618                    .setStartDelay(0)
619                    .setInterpolator(mConfig.fastOutSlowInInterpolator)
620                    .setDuration(mConfig.filteringCurrentViewsAnimDuration)
621                    .withLayer()
622                    .start();
623        }
624    }
625
626    @Override
627    public void onTaskStackUnfilterTriggered() {
628        // Show the search bar
629        if (mSearchBar != null) {
630            mSearchBar.animate()
631                    .alpha(1f)
632                    .setStartDelay(0)
633                    .setInterpolator(mConfig.fastOutSlowInInterpolator)
634                    .setDuration(mConfig.filteringNewViewsAnimDuration)
635                    .withLayer()
636                    .start();
637        }
638    }
639
640    @Override
641    public void onTaskResize(Task t) {
642        if (mCb != null) {
643            mCb.onTaskResize(t);
644        }
645    }
646
647    /**** RecentsPackageMonitor.PackageCallbacks Implementation ****/
648
649    @Override
650    public void onPackagesChanged(RecentsPackageMonitor monitor, String packageName, int userId) {
651        // Propagate this event down to each task stack view
652        List<TaskStackView> stackViews = getTaskStackViews();
653        int stackCount = stackViews.size();
654        for (int i = 0; i < stackCount; i++) {
655            TaskStackView stackView = stackViews.get(i);
656            stackView.onPackagesChanged(monitor, packageName, userId);
657        }
658    }
659}
660