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