RecentsPanelView.java revision 6311d0a079702b29984c0d31937345be105e1a5e
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;
20import java.util.List;
21
22import android.animation.Animator;
23import android.animation.LayoutTransition;
24import android.app.ActivityManager;
25import android.content.Context;
26import android.content.Intent;
27import android.content.pm.ActivityInfo;
28import android.content.pm.PackageManager;
29import android.content.pm.ResolveInfo;
30import android.content.res.Configuration;
31import android.content.res.Resources;
32import android.graphics.Bitmap;
33import android.graphics.BitmapFactory;
34import android.graphics.Canvas;
35import android.graphics.Matrix;
36import android.graphics.Paint;
37import android.graphics.Rect;
38import android.graphics.RectF;
39import android.graphics.Shader.TileMode;
40import android.graphics.drawable.BitmapDrawable;
41import android.graphics.drawable.Drawable;
42import android.graphics.drawable.LayerDrawable;
43import android.net.Uri;
44import android.provider.Settings;
45import android.util.AttributeSet;
46import android.util.DisplayMetrics;
47import android.util.Log;
48import android.view.KeyEvent;
49import android.view.LayoutInflater;
50import android.view.MenuItem;
51import android.view.MotionEvent;
52import android.view.View;
53import android.view.ViewGroup;
54import android.widget.AdapterView;
55import android.widget.AdapterView.OnItemClickListener;
56import android.widget.BaseAdapter;
57import android.widget.ImageView;
58import android.widget.PopupMenu;
59import android.widget.RelativeLayout;
60import android.widget.TextView;
61
62import com.android.systemui.R;
63import com.android.systemui.statusbar.StatusBar;
64import com.android.systemui.statusbar.phone.PhoneStatusBar;
65import com.android.systemui.statusbar.tablet.StatusBarPanel;
66import com.android.systemui.statusbar.tablet.TabletStatusBar;
67
68public class RecentsPanelView extends RelativeLayout
69        implements OnItemClickListener, RecentsCallback, StatusBarPanel, Animator.AnimatorListener {
70    static final String TAG = "RecentsListView";
71    static final boolean DEBUG = TabletStatusBar.DEBUG || PhoneStatusBar.DEBUG;
72    private static final int DISPLAY_TASKS = 20;
73    private static final int MAX_TASKS = DISPLAY_TASKS + 1; // allow extra for non-apps
74    private StatusBar mBar;
75    private ArrayList<ActivityDescription> mActivityDescriptions;
76    private int mIconDpi;
77    private View mRecentsScrim;
78    private View mRecentsGlowView;
79    private ViewGroup mRecentsContainer;
80    private Bitmap mGlowBitmap;
81    // TODO: add these widgets attributes to the layout file
82    private int mGlowBitmapPaddingLeftPx;
83    private int mGlowBitmapPaddingTopPx;
84    private int mGlowBitmapPaddingRightPx;
85    private int mGlowBitmapPaddingBottomPx;
86    private boolean mShowing;
87    private Choreographer mChoreo;
88    private View mRecentsDismissButton;
89    private ActivityDescriptionAdapter mListAdapter;
90
91    /* package */ final static class ActivityDescription {
92        int taskId; // application task id for curating apps
93        Bitmap thumbnail; // generated by Activity.onCreateThumbnail()
94        Drawable icon; // application package icon
95        String label; // application package label
96        CharSequence description; // generated by Activity.onCreateDescription()
97        Intent intent; // launch intent for application
98        Matrix matrix; // arbitrary rotation matrix to correct orientation
99        String packageName; // used to override animations (see onClick())
100        int position; // position in list
101
102        public ActivityDescription(Bitmap _thumbnail,
103                Drawable _icon, String _label, CharSequence _desc, Intent _intent,
104                int _id, int _pos, String _packageName)
105        {
106            thumbnail = _thumbnail;
107            icon = _icon;
108            label = _label;
109            description = _desc;
110            intent = _intent;
111            taskId = _id;
112            position = _pos;
113            packageName = _packageName;
114        }
115    }
116
117    private final class OnLongClickDelegate implements View.OnLongClickListener {
118        View mOtherView;
119        OnLongClickDelegate(View other) {
120            mOtherView = other;
121        }
122        public boolean onLongClick(View v) {
123            return mOtherView.performLongClick();
124        }
125    }
126
127    /* package */ final static class ViewHolder {
128        View thumbnailView;
129        ImageView iconView;
130        TextView labelView;
131        TextView descriptionView;
132        ActivityDescription activityDescription;
133    }
134
135    /* package */ final class ActivityDescriptionAdapter extends BaseAdapter {
136        private LayoutInflater mInflater;
137
138        public ActivityDescriptionAdapter(Context context) {
139            mInflater = LayoutInflater.from(context);
140        }
141
142        public int getCount() {
143            return mActivityDescriptions != null ? mActivityDescriptions.size() : 0;
144        }
145
146        public Object getItem(int position) {
147            return position; // we only need the index
148        }
149
150        public long getItemId(int position) {
151            return position; // we just need something unique for this position
152        }
153
154        public View getView(int position, View convertView, ViewGroup parent) {
155            ViewHolder holder;
156            if (convertView == null) {
157                convertView = mInflater.inflate(R.layout.status_bar_recent_item, null);
158                holder = new ViewHolder();
159                holder.thumbnailView = convertView.findViewById(R.id.app_thumbnail);
160                holder.iconView = (ImageView) convertView.findViewById(R.id.app_icon);
161                holder.labelView = (TextView) convertView.findViewById(R.id.app_label);
162                holder.descriptionView = (TextView) convertView.findViewById(R.id.app_description);
163                convertView.setTag(holder);
164            } else {
165                holder = (ViewHolder) convertView.getTag();
166            }
167
168            // activityId is reverse since most recent appears at the bottom...
169            final int activityId = mActivityDescriptions.size() - position - 1;
170
171            final ActivityDescription activityDescription = mActivityDescriptions.get(activityId);
172            final Bitmap thumb = activityDescription.thumbnail;
173            updateDrawable(holder.thumbnailView, compositeBitmap(mGlowBitmap, thumb));
174            holder.iconView.setImageDrawable(activityDescription.icon);
175            holder.labelView.setText(activityDescription.label);
176            holder.descriptionView.setText(activityDescription.description);
177            holder.thumbnailView.setTag(activityDescription);
178            holder.thumbnailView.setOnLongClickListener(new OnLongClickDelegate(convertView));
179            holder.activityDescription = activityDescription;
180
181            return convertView;
182        }
183    }
184
185    @Override
186    public boolean onKeyUp(int keyCode, KeyEvent event) {
187        if (keyCode == KeyEvent.KEYCODE_BACK && !event.isCanceled()) {
188            show(false, true);
189            return true;
190        }
191        return super.onKeyUp(keyCode, event);
192    }
193
194    public boolean isInContentArea(int x, int y) {
195        // use mRecentsContainer's exact bounds to determine horizontal position
196        final int l = mRecentsContainer.getLeft();
197        final int r = mRecentsContainer.getRight();
198        // use surrounding mRecentsGlowView's position in parent determine vertical bounds
199        final int t = mRecentsGlowView.getTop();
200        final int b = mRecentsGlowView.getBottom();
201        return x >= l && x < r && y >= t && y < b;
202    }
203
204    private void updateDrawable(View thumbnailView, Bitmap bitmap) {
205        Drawable d = thumbnailView.getBackground();
206        if (d instanceof LayerDrawable) {
207            LayerDrawable layerD = (LayerDrawable) d;
208            Drawable thumb = layerD.findDrawableByLayerId(R.id.base_layer);
209            if (thumb != null) {
210                layerD.setDrawableByLayerId(R.id.base_layer,
211                        new BitmapDrawable(getResources(), bitmap));
212                return;
213            }
214        }
215        Log.w(TAG, "Failed to update drawable");
216    }
217
218    public void show(boolean show, boolean animate) {
219        if (animate) {
220            if (mShowing != show) {
221                mShowing = show;
222                if (show) {
223                    setVisibility(View.VISIBLE);
224                }
225                mChoreo.startAnimation(show);
226            }
227        } else {
228            mShowing = show;
229            setVisibility(show ? View.VISIBLE : View.GONE);
230            mChoreo.jumpTo(show);
231        }
232        if (show) {
233            setFocusable(true);
234            setFocusableInTouchMode(true);
235            requestFocus();
236        }
237    }
238
239    public void onAnimationCancel(Animator animation) {
240    }
241
242    public void onAnimationEnd(Animator animation) {
243        if (mShowing) {
244            final LayoutTransition transitioner = new LayoutTransition();
245            ((ViewGroup)mRecentsContainer).setLayoutTransition(transitioner);
246            createCustomAnimations(transitioner);
247        } else {
248            ((ViewGroup)mRecentsContainer).setLayoutTransition(null);
249        }
250    }
251
252    public void onAnimationRepeat(Animator animation) {
253    }
254
255    public void onAnimationStart(Animator animation) {
256    }
257
258
259    /**
260     * We need to be aligned at the bottom.  LinearLayout can't do this, so instead,
261     * let LinearLayout do all the hard work, and then shift everything down to the bottom.
262     */
263    @Override
264    protected void onLayout(boolean changed, int l, int t, int r, int b) {
265        super.onLayout(changed, l, t, r, b);
266        mChoreo.setPanelHeight(mRecentsContainer.getHeight());
267    }
268
269    @Override
270    public boolean dispatchHoverEvent(MotionEvent event) {
271        // Ignore hover events outside of this panel bounds since such events
272        // generate spurious accessibility events with the panel content when
273        // tapping outside of it, thus confusing the user.
274        final int x = (int) event.getX();
275        final int y = (int) event.getY();
276        if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) {
277            return super.dispatchHoverEvent(event);
278        }
279        return true;
280    }
281
282    /**
283     * Whether the panel is showing, or, if it's animating, whether it will be
284     * when the animation is done.
285     */
286    public boolean isShowing() {
287        return mShowing;
288    }
289
290    public void setBar(StatusBar bar) {
291        mBar = bar;
292    }
293
294    public RecentsPanelView(Context context, AttributeSet attrs) {
295        this(context, attrs, 0);
296    }
297
298    public RecentsPanelView(Context context, AttributeSet attrs, int defStyle) {
299        super(context, attrs, defStyle);
300
301        Resources res = context.getResources();
302        boolean xlarge = (res.getConfiguration().screenLayout
303                & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE;
304
305        mIconDpi = xlarge ? DisplayMetrics.DENSITY_HIGH : res.getDisplayMetrics().densityDpi;
306
307        mGlowBitmap = BitmapFactory.decodeResource(res, R.drawable.recents_thumbnail_bg);
308        mGlowBitmapPaddingLeftPx =
309                res.getDimensionPixelSize(R.dimen.recents_thumbnail_bg_padding_left);
310        mGlowBitmapPaddingTopPx =
311                res.getDimensionPixelSize(R.dimen.recents_thumbnail_bg_padding_top);
312        mGlowBitmapPaddingRightPx =
313                res.getDimensionPixelSize(R.dimen.recents_thumbnail_bg_padding_right);
314        mGlowBitmapPaddingBottomPx =
315                res.getDimensionPixelSize(R.dimen.recents_thumbnail_bg_padding_bottom);
316    }
317
318    @Override
319    protected void onFinishInflate() {
320        super.onFinishInflate();
321        mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
322        mRecentsContainer = (ViewGroup) findViewById(R.id.recents_container);
323        mListAdapter = new ActivityDescriptionAdapter(mContext);
324        if (mRecentsContainer instanceof RecentsListView) {
325            RecentsListView listView = (RecentsListView) mRecentsContainer;
326            listView.setAdapter(mListAdapter);
327            listView.setOnItemClickListener(this);
328            listView.setCallback(this);
329        } else if (mRecentsContainer instanceof RecentsHorizontalScrollView){
330            RecentsHorizontalScrollView scrollView
331                    = (RecentsHorizontalScrollView) mRecentsContainer;
332            scrollView.setAdapter(mListAdapter);
333            scrollView.setCallback(this);
334        } else if (mRecentsContainer instanceof RecentsVerticalScrollView){
335            RecentsVerticalScrollView scrollView
336                    = (RecentsVerticalScrollView) mRecentsContainer;
337            scrollView.setAdapter(mListAdapter);
338            scrollView.setCallback(this);
339        }
340        else {
341            throw new IllegalArgumentException("missing RecentsListView/RecentsScrollView");
342        }
343
344
345        mRecentsGlowView = findViewById(R.id.recents_glow);
346        mRecentsScrim = (View) findViewById(R.id.recents_bg_protect);
347        mChoreo = new Choreographer(this, mRecentsScrim, mRecentsGlowView, this);
348        mRecentsDismissButton = findViewById(R.id.recents_dismiss_button);
349        mRecentsDismissButton.setOnClickListener(new OnClickListener() {
350            public void onClick(View v) {
351                hide(true);
352            }
353        });
354
355        // In order to save space, we make the background texture repeat in the Y direction
356        if (mRecentsScrim != null && mRecentsScrim.getBackground() instanceof BitmapDrawable) {
357            ((BitmapDrawable) mRecentsScrim.getBackground()).setTileModeY(TileMode.REPEAT);
358        }
359    }
360
361    private void createCustomAnimations(LayoutTransition transitioner) {
362        transitioner.setDuration(LayoutTransition.DISAPPEARING, 250);
363    }
364
365    @Override
366    protected void onVisibilityChanged(View changedView, int visibility) {
367        super.onVisibilityChanged(changedView, visibility);
368        if (DEBUG) Log.v(TAG, "onVisibilityChanged(" + changedView + ", " + visibility + ")");
369        if (visibility == View.VISIBLE && changedView == this) {
370            refreshApplicationList();
371        }
372    }
373
374    private Drawable getFullResDefaultActivityIcon() {
375        return getFullResIcon(Resources.getSystem(),
376                com.android.internal.R.mipmap.sym_def_app_icon);
377    }
378
379    private Drawable getFullResIcon(Resources resources, int iconId) {
380        try {
381            return resources.getDrawableForDensity(iconId, mIconDpi);
382        } catch (Resources.NotFoundException e) {
383            return getFullResDefaultActivityIcon();
384        }
385    }
386
387    private Drawable getFullResIcon(ResolveInfo info, PackageManager packageManager) {
388        Resources resources;
389        try {
390            resources = packageManager.getResourcesForApplication(
391                    info.activityInfo.applicationInfo);
392        } catch (PackageManager.NameNotFoundException e) {
393            resources = null;
394        }
395        if (resources != null) {
396            int iconId = info.activityInfo.getIconResource();
397            if (iconId != 0) {
398                return getFullResIcon(resources, iconId);
399            }
400        }
401        return getFullResDefaultActivityIcon();
402    }
403
404    private ArrayList<ActivityDescription> getRecentTasks() {
405        ArrayList<ActivityDescription> activityDescriptions = new ArrayList<ActivityDescription>();
406        final PackageManager pm = mContext.getPackageManager();
407        final ActivityManager am = (ActivityManager)
408                mContext.getSystemService(Context.ACTIVITY_SERVICE);
409
410        final List<ActivityManager.RecentTaskInfo> recentTasks =
411                am.getRecentTasks(MAX_TASKS, ActivityManager.RECENT_IGNORE_UNAVAILABLE);
412
413        ActivityInfo homeInfo = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
414                    .resolveActivityInfo(pm, 0);
415
416        int numTasks = recentTasks.size();
417
418        // skip the first activity - assume it's either the home screen or the current app.
419        final int first = 1;
420        for (int i = first, index = 0; i < numTasks && (index < MAX_TASKS); ++i) {
421            final ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(i);
422
423            Intent intent = new Intent(recentInfo.baseIntent);
424            if (recentInfo.origActivity != null) {
425                intent.setComponent(recentInfo.origActivity);
426            }
427
428            // Skip the current home activity.
429            if (homeInfo != null
430                    && homeInfo.packageName.equals(intent.getComponent().getPackageName())
431                    && homeInfo.name.equals(intent.getComponent().getClassName())) {
432                continue;
433            }
434
435            intent.setFlags((intent.getFlags()&~Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
436                    | Intent.FLAG_ACTIVITY_NEW_TASK);
437            final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
438            if (resolveInfo != null) {
439                final ActivityInfo info = resolveInfo.activityInfo;
440                final String title = info.loadLabel(pm).toString();
441                // Drawable icon = info.loadIcon(pm);
442                Drawable icon = getFullResIcon(resolveInfo, pm);
443                int id = recentTasks.get(i).id;
444                if (title != null && title.length() > 0 && icon != null) {
445                    if (DEBUG) Log.v(TAG, "creating activity desc for id=" + id + ", label=" + title);
446                    ActivityManager.TaskThumbnails thumbs = am.getTaskThumbnails(
447                            recentInfo.persistentId);
448                    ActivityDescription item = new ActivityDescription(
449                            thumbs != null ? thumbs.mainThumbnail : null,
450                            icon, title, recentInfo.description, intent, id,
451                            index, info.packageName);
452                    activityDescriptions.add(item);
453                    ++index;
454                } else {
455                    if (DEBUG) Log.v(TAG, "SKIPPING item " + id);
456                }
457            }
458        }
459        return activityDescriptions;
460    }
461
462    ActivityDescription findActivityDescription(int id)
463    {
464        ActivityDescription desc = null;
465        for (int i = 0; i < mActivityDescriptions.size(); i++) {
466            ActivityDescription item = mActivityDescriptions.get(i);
467            if (item != null && item.taskId == id) {
468                desc = item;
469                break;
470            }
471        }
472        return desc;
473    }
474
475    private void refreshApplicationList() {
476        mActivityDescriptions = getRecentTasks();
477        mListAdapter.notifyDataSetInvalidated();
478        if (mActivityDescriptions.size() > 0) {
479            if (DEBUG) Log.v(TAG, "Showing " + mActivityDescriptions.size() + " apps");
480            updateUiElements(getResources().getConfiguration());
481        } else {
482            // Immediately hide this panel
483            if (DEBUG) Log.v(TAG, "Nothing to show");
484            hide(false);
485        }
486    }
487
488    private Bitmap compositeBitmap(Bitmap background, Bitmap thumbnail) {
489        Bitmap outBitmap = background.copy(background.getConfig(), true);
490        if (thumbnail != null) {
491            Canvas canvas = new Canvas(outBitmap);
492            Paint paint = new Paint();
493            paint.setAntiAlias(true);
494            paint.setFilterBitmap(true);
495            paint.setAlpha(255);
496            final int srcWidth = thumbnail.getWidth();
497            final int srcHeight = thumbnail.getHeight();
498            if (DEBUG) Log.v(TAG, "Source thumb: " + srcWidth + "x" + srcHeight);
499            canvas.drawBitmap(thumbnail,
500                    new Rect(0, 0, srcWidth-1, srcHeight-1),
501                    new RectF(mGlowBitmapPaddingLeftPx, mGlowBitmapPaddingTopPx,
502                            outBitmap.getWidth() - mGlowBitmapPaddingRightPx,
503                            outBitmap.getHeight() - mGlowBitmapPaddingBottomPx), paint);
504            canvas.setBitmap(null);
505        }
506        return outBitmap;
507    }
508
509    private void updateUiElements(Configuration config) {
510        final int items = mActivityDescriptions.size();
511
512        mRecentsContainer.setVisibility(items > 0 ? View.VISIBLE : View.GONE);
513        mRecentsGlowView.setVisibility(items > 0 ? View.VISIBLE : View.GONE);
514    }
515
516    public void hide(boolean animate) {
517        if (!animate) {
518            setVisibility(View.GONE);
519        }
520        if (mBar != null) {
521            mBar.animateCollapse();
522        }
523    }
524
525    public void handleOnClick(View view) {
526        ActivityDescription ad = ((ViewHolder) view.getTag()).activityDescription;
527        final Context context = view.getContext();
528        final ActivityManager am = (ActivityManager)
529                context.getSystemService(Context.ACTIVITY_SERVICE);
530        if (ad.taskId >= 0) {
531            // This is an active task; it should just go to the foreground.
532            am.moveTaskToFront(ad.taskId, ActivityManager.MOVE_TASK_WITH_HOME);
533        } else {
534            Intent intent = ad.intent;
535            intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
536                    | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
537            if (DEBUG) Log.v(TAG, "Starting activity " + intent);
538            context.startActivity(intent);
539        }
540        hide(true);
541    }
542
543    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
544        handleOnClick(view);
545    }
546
547    public void handleSwipe(View view) {
548        ActivityDescription ad = ((ViewHolder) view.getTag()).activityDescription;
549        if (DEBUG) Log.v(TAG, "Jettison " + ad.label);
550        mActivityDescriptions.remove(ad);
551
552        // Handled by widget containers to enable LayoutTransitions properly
553        // mListAdapter.notifyDataSetChanged();
554
555        if (mActivityDescriptions.size() == 0) {
556            hide(false);
557        }
558
559        // Currently, either direction means the same thing, so ignore direction and remove
560        // the task.
561        final ActivityManager am = (ActivityManager)
562                mContext.getSystemService(Context.ACTIVITY_SERVICE);
563        am.removeTask(ad.taskId, ActivityManager.REMOVE_TASK_KILL_PROCESS);
564    }
565
566    private void startApplicationDetailsActivity(String packageName) {
567        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
568                Uri.fromParts("package", packageName, null));
569        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
570        getContext().startActivity(intent);
571    }
572
573    public void handleLongPress(final View selectedView, final View anchorView) {
574        PopupMenu popup = new PopupMenu(mContext, anchorView == null ? selectedView : anchorView);
575        popup.getMenuInflater().inflate(R.menu.recent_popup_menu, popup.getMenu());
576        popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
577            public boolean onMenuItemClick(MenuItem item) {
578                if (item.getItemId() == R.id.recent_remove_item) {
579                    mRecentsContainer.removeViewInLayout(selectedView);
580                } else if (item.getItemId() == R.id.recent_inspect_item) {
581                    ViewHolder viewHolder = (ViewHolder) selectedView.getTag();
582                    if (viewHolder != null) {
583                        final ActivityDescription ad = viewHolder.activityDescription;
584                        startApplicationDetailsActivity(ad.packageName);
585                        mBar.animateCollapse();
586                    } else {
587                        throw new IllegalStateException("Oops, no tag on view " + selectedView);
588                    }
589                } else {
590                    return false;
591                }
592                return true;
593            }
594        });
595        popup.show();
596    }
597}
598