RecentsPanelView.java revision 1737776f861cf92f0e142f405a37b69cd49745d4
1/*
2 * Copyright (C) 2011 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.recent;
18
19import java.util.ArrayList;
20
21import android.animation.Animator;
22import android.animation.LayoutTransition;
23import android.app.ActivityManager;
24import android.content.Context;
25import android.content.Intent;
26import android.content.res.Configuration;
27import android.graphics.Bitmap;
28import android.graphics.Matrix;
29import android.graphics.Shader.TileMode;
30import android.graphics.drawable.BitmapDrawable;
31import android.net.Uri;
32import android.provider.Settings;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.view.KeyEvent;
36import android.view.LayoutInflater;
37import android.view.MenuItem;
38import android.view.MotionEvent;
39import android.view.View;
40import android.view.ViewGroup;
41import android.view.accessibility.AccessibilityEvent;
42import android.view.animation.AnimationUtils;
43import android.widget.AdapterView;
44import android.widget.BaseAdapter;
45import android.widget.HorizontalScrollView;
46import android.widget.ImageView;
47import android.widget.PopupMenu;
48import android.widget.RelativeLayout;
49import android.widget.ScrollView;
50import android.widget.TextView;
51import android.widget.AdapterView.OnItemClickListener;
52import android.widget.ImageView.ScaleType;
53
54import com.android.systemui.R;
55import com.android.systemui.statusbar.StatusBar;
56import com.android.systemui.statusbar.phone.PhoneStatusBar;
57import com.android.systemui.statusbar.tablet.StatusBarPanel;
58import com.android.systemui.statusbar.tablet.TabletStatusBar;
59
60public class RecentsPanelView extends RelativeLayout
61        implements OnItemClickListener, RecentsCallback, StatusBarPanel, Animator.AnimatorListener {
62    static final String TAG = "RecentsPanelView";
63    static final boolean DEBUG = TabletStatusBar.DEBUG || PhoneStatusBar.DEBUG || false;
64    private Context mContext;
65    private StatusBar mBar;
66    private View mRecentsScrim;
67    private View mRecentsGlowView;
68    private View mRecentsNoApps;
69    private ViewGroup mRecentsContainer;
70
71    private boolean mShowing;
72    private Choreographer mChoreo;
73    private View mRecentsDismissButton;
74
75    private RecentTasksLoader mRecentTasksLoader;
76    private ArrayList<TaskDescription> mRecentTaskDescriptions;
77    private TaskDescriptionAdapter mListAdapter;
78    private int mThumbnailWidth;
79
80    public void setRecentTasksLoader(RecentTasksLoader loader) {
81        mRecentTasksLoader = loader;
82    }
83
84    private final class OnLongClickDelegate implements View.OnLongClickListener {
85        View mOtherView;
86        OnLongClickDelegate(View other) {
87            mOtherView = other;
88        }
89        public boolean onLongClick(View v) {
90            return mOtherView.performLongClick();
91        }
92    }
93
94    /* package */ final static class ViewHolder {
95        View thumbnailView;
96        ImageView thumbnailViewImage;
97        ImageView iconView;
98        TextView labelView;
99        TextView descriptionView;
100        TaskDescription taskDescription;
101    }
102
103    /* package */ final class TaskDescriptionAdapter extends BaseAdapter {
104        private LayoutInflater mInflater;
105
106        public TaskDescriptionAdapter(Context context) {
107            mInflater = LayoutInflater.from(context);
108        }
109
110        public int getCount() {
111            return mRecentTaskDescriptions != null ? mRecentTaskDescriptions.size() : 0;
112        }
113
114        public Object getItem(int position) {
115            return position; // we only need the index
116        }
117
118        public long getItemId(int position) {
119            return position; // we just need something unique for this position
120        }
121
122        public View getView(int position, View convertView, ViewGroup parent) {
123            ViewHolder holder;
124            if (convertView == null) {
125                convertView = mInflater.inflate(R.layout.status_bar_recent_item, parent, false);
126                holder = new ViewHolder();
127                holder.thumbnailView = convertView.findViewById(R.id.app_thumbnail);
128                holder.thumbnailViewImage = (ImageView) convertView.findViewById(
129                        R.id.app_thumbnail_image);
130                holder.iconView = (ImageView) convertView.findViewById(R.id.app_icon);
131                holder.labelView = (TextView) convertView.findViewById(R.id.app_label);
132                holder.descriptionView = (TextView) convertView.findViewById(R.id.app_description);
133
134                convertView.setTag(holder);
135            } else {
136                holder = (ViewHolder) convertView.getTag();
137            }
138
139            // index is reverse since most recent appears at the bottom...
140            final int index = mRecentTaskDescriptions.size() - position - 1;
141
142            final TaskDescription taskDescription = mRecentTaskDescriptions.get(index);
143            applyTaskDescription(holder, taskDescription, false);
144
145            holder.thumbnailView.setTag(taskDescription);
146            holder.thumbnailView.setOnLongClickListener(new OnLongClickDelegate(convertView));
147            holder.taskDescription = taskDescription;
148
149            return convertView;
150        }
151    }
152
153    @Override
154    public boolean onKeyUp(int keyCode, KeyEvent event) {
155        if (keyCode == KeyEvent.KEYCODE_BACK && !event.isCanceled()) {
156            show(false, true);
157            return true;
158        }
159        return super.onKeyUp(keyCode, event);
160    }
161
162    public boolean isInContentArea(int x, int y) {
163        // use mRecentsContainer's exact bounds to determine horizontal position
164        final int l = mRecentsContainer.getLeft();
165        final int r = mRecentsContainer.getRight();
166        // use surrounding mRecentsGlowView's position in parent determine vertical bounds
167        final int t = mRecentsGlowView.getTop();
168        final int b = mRecentsGlowView.getBottom();
169        return x >= l && x < r && y >= t && y < b;
170    }
171
172    public void show(boolean show, boolean animate) {
173        show(show, animate, null);
174    }
175
176    public void show(boolean show, boolean animate,
177            ArrayList<TaskDescription> recentTaskDescriptions) {
178        if (show) {
179            // Need to update list of recent apps before we set visibility so this view's
180            // content description is updated before it gets focus for TalkBack mode
181            refreshRecentTasksList(recentTaskDescriptions);
182
183            // if there are no apps, either bring up a "No recent apps" message, or just
184            // quit early
185            boolean noApps = (mRecentTaskDescriptions.size() == 0);
186            if (mRecentsNoApps != null) { // doesn't exist on large devices
187                mRecentsNoApps.setVisibility(noApps ? View.VISIBLE : View.INVISIBLE);
188            } else {
189                if (noApps) {
190                    if (DEBUG) Log.v(TAG, "Nothing to show");
191                    return;
192                }
193            }
194        } else {
195            mRecentTasksLoader.cancelLoadingThumbnails();
196        }
197        if (animate) {
198            if (mShowing != show) {
199                mShowing = show;
200                if (show) {
201                    setVisibility(View.VISIBLE);
202                }
203                mChoreo.startAnimation(show);
204            }
205        } else {
206            mShowing = show;
207            setVisibility(show ? View.VISIBLE : View.GONE);
208            mChoreo.jumpTo(show);
209            onAnimationEnd(null);
210        }
211        if (show) {
212            setFocusable(true);
213            setFocusableInTouchMode(true);
214            requestFocus();
215        }
216    }
217
218    public void dismiss() {
219        hide(true);
220    }
221
222    public void hide(boolean animate) {
223        if (!animate) {
224            setVisibility(View.GONE);
225        }
226        if (mBar != null) {
227            mBar.animateCollapse();
228        }
229    }
230
231    public void handleShowBackground(boolean show) {
232        if (show) {
233            mRecentsScrim.setBackgroundResource(R.drawable.status_bar_recents_background);
234        } else {
235            mRecentsScrim.setBackgroundDrawable(null);
236        }
237    }
238
239    public boolean isRecentsVisible() {
240        return getVisibility() == VISIBLE;
241    }
242
243    public void onAnimationCancel(Animator animation) {
244    }
245
246    public void onAnimationEnd(Animator animation) {
247        if (mShowing) {
248            final LayoutTransition transitioner = new LayoutTransition();
249            ((ViewGroup)mRecentsContainer).setLayoutTransition(transitioner);
250            createCustomAnimations(transitioner);
251        } else {
252            ((ViewGroup)mRecentsContainer).setLayoutTransition(null);
253            // Clear memory used by screenshots
254            mRecentTaskDescriptions.clear();
255            mListAdapter.notifyDataSetInvalidated();
256        }
257    }
258
259    public void onAnimationRepeat(Animator animation) {
260    }
261
262    public void onAnimationStart(Animator animation) {
263    }
264
265
266    /**
267     * We need to be aligned at the bottom.  LinearLayout can't do this, so instead,
268     * let LinearLayout do all the hard work, and then shift everything down to the bottom.
269     */
270    @Override
271    protected void onLayout(boolean changed, int l, int t, int r, int b) {
272        super.onLayout(changed, l, t, r, b);
273        mChoreo.setPanelHeight(mRecentsContainer.getHeight());
274    }
275
276    @Override
277    public boolean dispatchHoverEvent(MotionEvent event) {
278        // Ignore hover events outside of this panel bounds since such events
279        // generate spurious accessibility events with the panel content when
280        // tapping outside of it, thus confusing the user.
281        final int x = (int) event.getX();
282        final int y = (int) event.getY();
283        if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) {
284            return super.dispatchHoverEvent(event);
285        }
286        return true;
287    }
288
289    /**
290     * Whether the panel is showing, or, if it's animating, whether it will be
291     * when the animation is done.
292     */
293    public boolean isShowing() {
294        return mShowing;
295    }
296
297    public void setBar(StatusBar bar) {
298        mBar = bar;
299    }
300
301    public RecentsPanelView(Context context, AttributeSet attrs) {
302        this(context, attrs, 0);
303    }
304
305    public RecentsPanelView(Context context, AttributeSet attrs, int defStyle) {
306        super(context, attrs, defStyle);
307        mContext = context;
308        updateValuesFromResources();
309    }
310
311    public void updateValuesFromResources() {
312        mThumbnailWidth =
313            (int) mContext.getResources().getDimension(R.dimen.status_bar_recents_thumbnail_width);
314    }
315
316    @Override
317    protected void onFinishInflate() {
318        super.onFinishInflate();
319        mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
320        mRecentsContainer = (ViewGroup) findViewById(R.id.recents_container);
321        mListAdapter = new TaskDescriptionAdapter(mContext);
322        if (mRecentsContainer instanceof RecentsHorizontalScrollView){
323            RecentsHorizontalScrollView scrollView
324                    = (RecentsHorizontalScrollView) mRecentsContainer;
325            scrollView.setAdapter(mListAdapter);
326            scrollView.setCallback(this);
327        } else if (mRecentsContainer instanceof RecentsVerticalScrollView){
328            RecentsVerticalScrollView scrollView
329                    = (RecentsVerticalScrollView) mRecentsContainer;
330            scrollView.setAdapter(mListAdapter);
331            scrollView.setCallback(this);
332        }
333        else {
334            throw new IllegalArgumentException("missing Recents[Horizontal]ScrollView");
335        }
336
337
338        mRecentsGlowView = findViewById(R.id.recents_glow);
339        mRecentsScrim = findViewById(R.id.recents_bg_protect);
340        mRecentsNoApps = findViewById(R.id.recents_no_apps);
341        mChoreo = new Choreographer(this, mRecentsScrim, mRecentsGlowView, mRecentsNoApps, this);
342        mRecentsDismissButton = findViewById(R.id.recents_dismiss_button);
343        if (mRecentsDismissButton != null) {
344            mRecentsDismissButton.setOnClickListener(new OnClickListener() {
345                public void onClick(View v) {
346                    hide(true);
347                }
348            });
349        }
350
351        // In order to save space, we make the background texture repeat in the Y direction
352        if (mRecentsScrim != null && mRecentsScrim.getBackground() instanceof BitmapDrawable) {
353            ((BitmapDrawable) mRecentsScrim.getBackground()).setTileModeY(TileMode.REPEAT);
354        }
355    }
356
357    private void createCustomAnimations(LayoutTransition transitioner) {
358        transitioner.setDuration(200);
359        transitioner.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
360        transitioner.setAnimator(LayoutTransition.DISAPPEARING, null);
361    }
362
363    @Override
364    protected void onVisibilityChanged(View changedView, int visibility) {
365        super.onVisibilityChanged(changedView, visibility);
366        if (DEBUG) Log.v(TAG, "onVisibilityChanged(" + changedView + ", " + visibility + ")");
367
368        if (mRecentsContainer instanceof RecentsHorizontalScrollView) {
369            ((RecentsHorizontalScrollView) mRecentsContainer).onRecentsVisibilityChanged();
370        } else if (mRecentsContainer instanceof RecentsVerticalScrollView) {
371            ((RecentsVerticalScrollView) mRecentsContainer).onRecentsVisibilityChanged();
372        } else {
373            throw new IllegalArgumentException("missing Recents[Horizontal]ScrollView");
374        }
375    }
376
377
378    void applyTaskDescription(ViewHolder h, TaskDescription td, boolean anim) {
379        h.iconView.setImageDrawable(td.getIcon());
380        if (h.iconView.getVisibility() != View.VISIBLE) {
381            if (anim) {
382                h.iconView.setAnimation(AnimationUtils.loadAnimation(
383                        mContext, R.anim.recent_appear));
384            }
385            h.iconView.setVisibility(View.VISIBLE);
386        }
387        h.labelView.setText(td.getLabel());
388        h.thumbnailView.setContentDescription(td.getLabel());
389        if (h.labelView.getVisibility() != View.VISIBLE) {
390            if (anim) {
391                h.labelView.setAnimation(AnimationUtils.loadAnimation(
392                        mContext, R.anim.recent_appear));
393            }
394            h.labelView.setVisibility(View.VISIBLE);
395        }
396        Bitmap thumbnail = td.getThumbnail();
397        if (thumbnail != null) {
398            // Should remove the default image in the frame
399            // that this now covers, to improve scrolling speed.
400            // That can't be done until the anim is complete though.
401            h.thumbnailViewImage.setImageBitmap(thumbnail);
402            // scale to fill up the full width
403            Matrix scaleMatrix = new Matrix();
404            float scale = mThumbnailWidth / (float) thumbnail.getWidth();
405            scaleMatrix.setScale(scale, scale);
406            h.thumbnailViewImage.setScaleType(ScaleType.MATRIX);
407            h.thumbnailViewImage.setImageMatrix(scaleMatrix);
408            if (h.thumbnailViewImage.getVisibility() != View.VISIBLE) {
409                if (anim) {
410                    h.thumbnailViewImage.setAnimation(
411                            AnimationUtils.loadAnimation(
412                                    mContext, R.anim.recent_appear));
413                }
414                h.thumbnailViewImage.setVisibility(View.VISIBLE);
415            }
416        }
417        //h.descriptionView.setText(ad.description);
418    }
419
420    void onTaskThumbnailLoaded(TaskDescription ad) {
421        synchronized (ad) {
422            if (mRecentsContainer != null) {
423                ViewGroup container = mRecentsContainer;
424                if (container instanceof HorizontalScrollView
425                        || container instanceof ScrollView) {
426                    container = (ViewGroup)container.findViewById(
427                            R.id.recents_linear_layout);
428                }
429                // Look for a view showing this thumbnail, to update.
430                for (int i=0; i<container.getChildCount(); i++) {
431                    View v = container.getChildAt(i);
432                    if (v.getTag() instanceof ViewHolder) {
433                        ViewHolder h = (ViewHolder)v.getTag();
434                        if (h.taskDescription == ad) {
435                            applyTaskDescription(h, ad, true);
436                        }
437                    }
438                }
439            }
440        }
441    }
442
443    private void refreshRecentTasksList(ArrayList<TaskDescription> recentTasksList) {
444        if (recentTasksList != null) {
445            mRecentTaskDescriptions = recentTasksList;
446        } else {
447            mRecentTaskDescriptions = mRecentTasksLoader.getRecentTasks();
448        }
449        mListAdapter.notifyDataSetInvalidated();
450        updateUiElements(getResources().getConfiguration());
451    }
452
453    public ArrayList<TaskDescription> getRecentTasksList() {
454        return mRecentTaskDescriptions;
455    }
456
457    private void updateUiElements(Configuration config) {
458        final int items = mRecentTaskDescriptions.size();
459
460        mRecentsContainer.setVisibility(items > 0 ? View.VISIBLE : View.GONE);
461        mRecentsGlowView.setVisibility(items > 0 ? View.VISIBLE : View.GONE);
462
463        // Set description for accessibility
464        int numRecentApps = mRecentTaskDescriptions.size();
465        String recentAppsAccessibilityDescription;
466        if (numRecentApps == 0) {
467            recentAppsAccessibilityDescription =
468                getResources().getString(R.string.status_bar_no_recent_apps);
469        } else {
470            recentAppsAccessibilityDescription = getResources().getQuantityString(
471                R.plurals.status_bar_accessibility_recent_apps, numRecentApps, numRecentApps);
472        }
473        setContentDescription(recentAppsAccessibilityDescription);
474    }
475
476    public void handleOnClick(View view) {
477        TaskDescription ad = ((ViewHolder) view.getTag()).taskDescription;
478        final Context context = view.getContext();
479        final ActivityManager am = (ActivityManager)
480                context.getSystemService(Context.ACTIVITY_SERVICE);
481        if (ad.taskId >= 0) {
482            // This is an active task; it should just go to the foreground.
483            am.moveTaskToFront(ad.taskId, ActivityManager.MOVE_TASK_WITH_HOME);
484        } else {
485            Intent intent = ad.intent;
486            intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
487                    | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
488            if (DEBUG) Log.v(TAG, "Starting activity " + intent);
489            context.startActivity(intent);
490        }
491        hide(true);
492    }
493
494    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
495        handleOnClick(view);
496    }
497
498    public void handleSwipe(View view) {
499        TaskDescription ad = ((ViewHolder) view.getTag()).taskDescription;
500        if (DEBUG) Log.v(TAG, "Jettison " + ad.getLabel());
501        mRecentTaskDescriptions.remove(ad);
502
503        // Handled by widget containers to enable LayoutTransitions properly
504        // mListAdapter.notifyDataSetChanged();
505
506        if (mRecentTaskDescriptions.size() == 0) {
507            hide(false);
508        }
509
510        // Currently, either direction means the same thing, so ignore direction and remove
511        // the task.
512        final ActivityManager am = (ActivityManager)
513                mContext.getSystemService(Context.ACTIVITY_SERVICE);
514        am.removeTask(ad.persistentTaskId, ActivityManager.REMOVE_TASK_KILL_PROCESS);
515
516        // Accessibility feedback
517        setContentDescription(
518                mContext.getString(R.string.accessibility_recents_item_dismissed, ad.getLabel()));
519        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
520        setContentDescription(null);
521    }
522
523    private void startApplicationDetailsActivity(String packageName) {
524        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
525                Uri.fromParts("package", packageName, null));
526        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
527        getContext().startActivity(intent);
528    }
529
530    public void handleLongPress(
531            final View selectedView, final View anchorView, final View thumbnailView) {
532        thumbnailView.setSelected(true);
533        PopupMenu popup = new PopupMenu(mContext, anchorView == null ? selectedView : anchorView);
534        popup.getMenuInflater().inflate(R.menu.recent_popup_menu, popup.getMenu());
535        popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
536            public boolean onMenuItemClick(MenuItem item) {
537                if (item.getItemId() == R.id.recent_remove_item) {
538                    mRecentsContainer.removeViewInLayout(selectedView);
539                } else if (item.getItemId() == R.id.recent_inspect_item) {
540                    ViewHolder viewHolder = (ViewHolder) selectedView.getTag();
541                    if (viewHolder != null) {
542                        final TaskDescription ad = viewHolder.taskDescription;
543                        startApplicationDetailsActivity(ad.packageName);
544                        mBar.animateCollapse();
545                    } else {
546                        throw new IllegalStateException("Oops, no tag on view " + selectedView);
547                    }
548                } else {
549                    return false;
550                }
551                return true;
552            }
553        });
554        popup.setOnDismissListener(new PopupMenu.OnDismissListener() {
555            public void onDismiss(PopupMenu menu) {
556                thumbnailView.setSelected(false);
557            }
558        });
559        popup.show();
560    }
561}
562