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