/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.recent; import java.util.ArrayList; import java.util.List; import android.animation.Animator; import android.animation.LayoutTransition; import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader.TileMode; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.provider.Settings; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.ImageView; import android.widget.PopupMenu; import android.widget.RelativeLayout; import android.widget.TextView; import com.android.systemui.R; import com.android.systemui.statusbar.StatusBar; import com.android.systemui.statusbar.phone.PhoneStatusBar; import com.android.systemui.statusbar.tablet.StatusBarPanel; import com.android.systemui.statusbar.tablet.TabletStatusBar; public class RecentsPanelView extends RelativeLayout implements OnItemClickListener, RecentsCallback, StatusBarPanel, Animator.AnimatorListener { static final String TAG = "RecentsListView"; static final boolean DEBUG = TabletStatusBar.DEBUG || PhoneStatusBar.DEBUG; private static final int DISPLAY_TASKS = 20; private static final int MAX_TASKS = DISPLAY_TASKS + 1; // allow extra for non-apps private StatusBar mBar; private ArrayList mActivityDescriptions; private int mIconDpi; private View mRecentsScrim; private View mRecentsGlowView; private ViewGroup mRecentsContainer; private Bitmap mGlowBitmap; // TODO: add these widgets attributes to the layout file private int mGlowBitmapPaddingLeftPx; private int mGlowBitmapPaddingTopPx; private int mGlowBitmapPaddingRightPx; private int mGlowBitmapPaddingBottomPx; private boolean mShowing; private Choreographer mChoreo; private View mRecentsDismissButton; private ActivityDescriptionAdapter mListAdapter; /* package */ final static class ActivityDescription { int taskId; // application task id for curating apps Bitmap thumbnail; // generated by Activity.onCreateThumbnail() Drawable icon; // application package icon String label; // application package label CharSequence description; // generated by Activity.onCreateDescription() Intent intent; // launch intent for application Matrix matrix; // arbitrary rotation matrix to correct orientation String packageName; // used to override animations (see onClick()) int position; // position in list public ActivityDescription(Bitmap _thumbnail, Drawable _icon, String _label, CharSequence _desc, Intent _intent, int _id, int _pos, String _packageName) { thumbnail = _thumbnail; icon = _icon; label = _label; description = _desc; intent = _intent; taskId = _id; position = _pos; packageName = _packageName; } } private final class OnLongClickDelegate implements View.OnLongClickListener { View mOtherView; OnLongClickDelegate(View other) { mOtherView = other; } public boolean onLongClick(View v) { return mOtherView.performLongClick(); } } /* package */ final static class ViewHolder { View thumbnailView; ImageView iconView; TextView labelView; TextView descriptionView; ActivityDescription activityDescription; } /* package */ final class ActivityDescriptionAdapter extends BaseAdapter { private LayoutInflater mInflater; public ActivityDescriptionAdapter(Context context) { mInflater = LayoutInflater.from(context); } public int getCount() { return mActivityDescriptions != null ? mActivityDescriptions.size() : 0; } public Object getItem(int position) { return position; // we only need the index } public long getItemId(int position) { return position; // we just need something unique for this position } public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = mInflater.inflate(R.layout.status_bar_recent_item, null); holder = new ViewHolder(); holder.thumbnailView = convertView.findViewById(R.id.app_thumbnail); holder.iconView = (ImageView) convertView.findViewById(R.id.app_icon); holder.labelView = (TextView) convertView.findViewById(R.id.app_label); holder.descriptionView = (TextView) convertView.findViewById(R.id.app_description); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } // activityId is reverse since most recent appears at the bottom... final int activityId = mActivityDescriptions.size() - position - 1; final ActivityDescription activityDescription = mActivityDescriptions.get(activityId); final Bitmap thumb = activityDescription.thumbnail; updateDrawable(holder.thumbnailView, compositeBitmap(mGlowBitmap, thumb)); holder.iconView.setImageDrawable(activityDescription.icon); holder.labelView.setText(activityDescription.label); holder.descriptionView.setText(activityDescription.description); holder.thumbnailView.setTag(activityDescription); holder.thumbnailView.setOnLongClickListener(new OnLongClickDelegate(convertView)); holder.activityDescription = activityDescription; return convertView; } } public boolean isInContentArea(int x, int y) { // use mRecentsContainer's exact bounds to determine horizontal position final int l = mRecentsContainer.getLeft(); final int r = mRecentsContainer.getRight(); // use surrounding mRecentsGlowView's position in parent determine vertical bounds final int t = mRecentsGlowView.getTop(); final int b = mRecentsGlowView.getBottom(); return x >= l && x < r && y >= t && y < b; } private void updateDrawable(View thumbnailView, Bitmap bitmap) { Drawable d = thumbnailView.getBackground(); if (d instanceof LayerDrawable) { LayerDrawable layerD = (LayerDrawable) d; Drawable thumb = layerD.findDrawableByLayerId(R.id.base_layer); if (thumb != null) { layerD.setDrawableByLayerId(R.id.base_layer, new BitmapDrawable(getResources(), bitmap)); return; } } Log.w(TAG, "Failed to update drawable"); } public void show(boolean show, boolean animate) { if (animate) { if (mShowing != show) { mShowing = show; if (show) { setVisibility(View.VISIBLE); } mChoreo.startAnimation(show); } } else { mShowing = show; setVisibility(show ? View.VISIBLE : View.GONE); mChoreo.jumpTo(show); } } public void onAnimationCancel(Animator animation) { } public void onAnimationEnd(Animator animation) { if (mShowing) { final LayoutTransition transitioner = new LayoutTransition(); ((ViewGroup)mRecentsContainer).setLayoutTransition(transitioner); createCustomAnimations(transitioner); } else { ((ViewGroup)mRecentsContainer).setLayoutTransition(null); } } public void onAnimationRepeat(Animator animation) { } public void onAnimationStart(Animator animation) { } /** * We need to be aligned at the bottom. LinearLayout can't do this, so instead, * let LinearLayout do all the hard work, and then shift everything down to the bottom. */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mChoreo.setPanelHeight(mRecentsContainer.getHeight()); } @Override public boolean dispatchHoverEvent(MotionEvent event) { // Ignore hover events outside of this panel bounds since such events // generate spurious accessibility events with the panel content when // tapping outside of it, thus confusing the user. final int x = (int) event.getX(); final int y = (int) event.getY(); if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) { return super.dispatchHoverEvent(event); } return true; } /** * Whether the panel is showing, or, if it's animating, whether it will be * when the animation is done. */ public boolean isShowing() { return mShowing; } public void setBar(StatusBar bar) { mBar = bar; } public RecentsPanelView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RecentsPanelView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); Resources res = context.getResources(); boolean xlarge = (res.getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE; mIconDpi = xlarge ? DisplayMetrics.DENSITY_HIGH : res.getDisplayMetrics().densityDpi; mGlowBitmap = BitmapFactory.decodeResource(res, R.drawable.recents_thumbnail_bg); mGlowBitmapPaddingLeftPx = res.getDimensionPixelSize(R.dimen.recents_thumbnail_bg_padding_left); mGlowBitmapPaddingTopPx = res.getDimensionPixelSize(R.dimen.recents_thumbnail_bg_padding_top); mGlowBitmapPaddingRightPx = res.getDimensionPixelSize(R.dimen.recents_thumbnail_bg_padding_right); mGlowBitmapPaddingBottomPx = res.getDimensionPixelSize(R.dimen.recents_thumbnail_bg_padding_bottom); } @Override protected void onFinishInflate() { super.onFinishInflate(); mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mRecentsContainer = (ViewGroup) findViewById(R.id.recents_container); mListAdapter = new ActivityDescriptionAdapter(mContext); if (mRecentsContainer instanceof RecentsListView) { RecentsListView listView = (RecentsListView) mRecentsContainer; listView.setAdapter(mListAdapter); listView.setOnItemClickListener(this); listView.setCallback(this); } else if (mRecentsContainer instanceof RecentsHorizontalScrollView){ RecentsHorizontalScrollView scrollView = (RecentsHorizontalScrollView) mRecentsContainer; scrollView.setAdapter(mListAdapter); scrollView.setCallback(this); } else if (mRecentsContainer instanceof RecentsVerticalScrollView){ RecentsVerticalScrollView scrollView = (RecentsVerticalScrollView) mRecentsContainer; scrollView.setAdapter(mListAdapter); scrollView.setCallback(this); } else { throw new IllegalArgumentException("missing RecentsListView/RecentsScrollView"); } mRecentsGlowView = findViewById(R.id.recents_glow); mRecentsScrim = (View) findViewById(R.id.recents_bg_protect); mChoreo = new Choreographer(this, mRecentsScrim, mRecentsGlowView, this); mRecentsDismissButton = findViewById(R.id.recents_dismiss_button); mRecentsDismissButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { hide(true); } }); // In order to save space, we make the background texture repeat in the Y direction if (mRecentsScrim != null && mRecentsScrim.getBackground() instanceof BitmapDrawable) { ((BitmapDrawable) mRecentsScrim.getBackground()).setTileModeY(TileMode.REPEAT); } } private void createCustomAnimations(LayoutTransition transitioner) { transitioner.setDuration(LayoutTransition.DISAPPEARING, 250); } @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if (DEBUG) Log.v(TAG, "onVisibilityChanged(" + changedView + ", " + visibility + ")"); if (visibility == View.VISIBLE && changedView == this) { refreshApplicationList(); } } private Drawable getFullResDefaultActivityIcon() { return getFullResIcon(Resources.getSystem(), com.android.internal.R.mipmap.sym_def_app_icon); } private Drawable getFullResIcon(Resources resources, int iconId) { try { return resources.getDrawableForDensity(iconId, mIconDpi); } catch (Resources.NotFoundException e) { return getFullResDefaultActivityIcon(); } } private Drawable getFullResIcon(ResolveInfo info, PackageManager packageManager) { Resources resources; try { resources = packageManager.getResourcesForApplication( info.activityInfo.applicationInfo); } catch (PackageManager.NameNotFoundException e) { resources = null; } if (resources != null) { int iconId = info.activityInfo.getIconResource(); if (iconId != 0) { return getFullResIcon(resources, iconId); } } return getFullResDefaultActivityIcon(); } private ArrayList getRecentTasks() { ArrayList activityDescriptions = new ArrayList(); final PackageManager pm = mContext.getPackageManager(); final ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); final List recentTasks = am.getRecentTasks(MAX_TASKS, ActivityManager.RECENT_IGNORE_UNAVAILABLE); ActivityInfo homeInfo = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME) .resolveActivityInfo(pm, 0); int numTasks = recentTasks.size(); // skip the first activity - assume it's either the home screen or the current app. final int first = 1; for (int i = first, index = 0; i < numTasks && (index < MAX_TASKS); ++i) { final ActivityManager.RecentTaskInfo recentInfo = recentTasks.get(i); Intent intent = new Intent(recentInfo.baseIntent); if (recentInfo.origActivity != null) { intent.setComponent(recentInfo.origActivity); } // Skip the current home activity. if (homeInfo != null && homeInfo.packageName.equals(intent.getComponent().getPackageName()) && homeInfo.name.equals(intent.getComponent().getClassName())) { continue; } intent.setFlags((intent.getFlags()&~Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) | Intent.FLAG_ACTIVITY_NEW_TASK); final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0); if (resolveInfo != null) { final ActivityInfo info = resolveInfo.activityInfo; final String title = info.loadLabel(pm).toString(); // Drawable icon = info.loadIcon(pm); Drawable icon = getFullResIcon(resolveInfo, pm); int id = recentTasks.get(i).id; if (title != null && title.length() > 0 && icon != null) { if (DEBUG) Log.v(TAG, "creating activity desc for id=" + id + ", label=" + title); ActivityManager.TaskThumbnails thumbs = am.getTaskThumbnails( recentInfo.persistentId); ActivityDescription item = new ActivityDescription( thumbs != null ? thumbs.mainThumbnail : null, icon, title, recentInfo.description, intent, id, index, info.packageName); activityDescriptions.add(item); ++index; } else { if (DEBUG) Log.v(TAG, "SKIPPING item " + id); } } } return activityDescriptions; } ActivityDescription findActivityDescription(int id) { ActivityDescription desc = null; for (int i = 0; i < mActivityDescriptions.size(); i++) { ActivityDescription item = mActivityDescriptions.get(i); if (item != null && item.taskId == id) { desc = item; break; } } return desc; } private void refreshApplicationList() { mActivityDescriptions = getRecentTasks(); mListAdapter.notifyDataSetInvalidated(); if (mActivityDescriptions.size() > 0) { if (DEBUG) Log.v(TAG, "Showing " + mActivityDescriptions.size() + " apps"); updateUiElements(getResources().getConfiguration()); } else { // Immediately hide this panel if (DEBUG) Log.v(TAG, "Nothing to show"); hide(false); } } private Bitmap compositeBitmap(Bitmap background, Bitmap thumbnail) { Bitmap outBitmap = background.copy(background.getConfig(), true); if (thumbnail != null) { Canvas canvas = new Canvas(outBitmap); Paint paint = new Paint(); paint.setAntiAlias(true); paint.setFilterBitmap(true); paint.setAlpha(255); final int srcWidth = thumbnail.getWidth(); final int srcHeight = thumbnail.getHeight(); if (DEBUG) Log.v(TAG, "Source thumb: " + srcWidth + "x" + srcHeight); canvas.drawBitmap(thumbnail, new Rect(0, 0, srcWidth-1, srcHeight-1), new RectF(mGlowBitmapPaddingLeftPx, mGlowBitmapPaddingTopPx, outBitmap.getWidth() - mGlowBitmapPaddingRightPx, outBitmap.getHeight() - mGlowBitmapPaddingBottomPx), paint); } return outBitmap; } private void updateUiElements(Configuration config) { final int items = mActivityDescriptions.size(); mRecentsContainer.setVisibility(items > 0 ? View.VISIBLE : View.GONE); mRecentsGlowView.setVisibility(items > 0 ? View.VISIBLE : View.GONE); } public void hide(boolean animate) { if (!animate) { setVisibility(View.GONE); } if (mBar != null) { mBar.animateCollapse(); } } public void handleOnClick(View view) { ActivityDescription ad = ((ViewHolder) view.getTag()).activityDescription; final Context context = view.getContext(); final ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); if (ad.taskId >= 0) { // This is an active task; it should just go to the foreground. am.moveTaskToFront(ad.taskId, ActivityManager.MOVE_TASK_WITH_HOME); } else { Intent intent = ad.intent; intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY | Intent.FLAG_ACTIVITY_TASK_ON_HOME); if (DEBUG) Log.v(TAG, "Starting activity " + intent); context.startActivity(intent); } hide(true); } public void onItemClick(AdapterView parent, View view, int position, long id) { handleOnClick(view); } public void handleSwipe(View view, int direction) { ActivityDescription ad = ((ViewHolder) view.getTag()).activityDescription; if (DEBUG) Log.v(TAG, "Jettison " + ad.label); mActivityDescriptions.remove(ad); // Handled by widget containers to enable LayoutTransitions properly // mListAdapter.notifyDataSetChanged(); if (mActivityDescriptions.size() == 0) { hide(false); } // Currently, either direction means the same thing, so ignore direction and remove // the task. final ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); am.removeTask(ad.taskId, ActivityManager.REMOVE_TASK_KILL_PROCESS); } private void startApplicationDetailsActivity(String packageName) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", packageName, null)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getContext().startActivity(intent); } public void handleLongPress(final View selectedView, final View anchorView) { PopupMenu popup = new PopupMenu(mContext, anchorView == null ? selectedView : anchorView); popup.getMenuInflater().inflate(R.menu.recent_popup_menu, popup.getMenu()); popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { if (item.getItemId() == R.id.recent_remove_item) { mRecentsContainer.removeViewInLayout(selectedView); } else if (item.getItemId() == R.id.recent_inspect_item) { ViewHolder viewHolder = (ViewHolder) selectedView.getTag(); if (viewHolder != null) { final ActivityDescription ad = viewHolder.activityDescription; startApplicationDetailsActivity(ad.packageName); mBar.animateCollapse(); } else { throw new IllegalStateException("Oops, no tag on view " + selectedView); } } else { return false; } return true; } }); popup.show(); } }