/* * 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 android.animation.Animator; import android.animation.LayoutTransition; import android.app.ActivityManager; import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Matrix; import android.graphics.Shader.TileMode; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.provider.Settings; import android.util.AttributeSet; import android.util.Log; import android.view.Display; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.animation.AnimationUtils; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.BaseAdapter; import android.widget.FrameLayout; import android.widget.HorizontalScrollView; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import android.widget.PopupMenu; import android.widget.ScrollView; import android.widget.TextView; import com.android.systemui.R; import com.android.systemui.statusbar.BaseStatusBar; import com.android.systemui.statusbar.phone.PhoneStatusBar; import com.android.systemui.statusbar.tablet.StatusBarPanel; import com.android.systemui.statusbar.tablet.TabletStatusBar; import java.util.ArrayList; public class RecentsPanelView extends FrameLayout implements OnItemClickListener, RecentsCallback, StatusBarPanel, Animator.AnimatorListener, View.OnTouchListener { static final String TAG = "RecentsPanelView"; static final boolean DEBUG = TabletStatusBar.DEBUG || PhoneStatusBar.DEBUG || false; private Context mContext; private BaseStatusBar mBar; private PopupMenu mPopup; private View mRecentsScrim; private View mRecentsNoApps; private ViewGroup mRecentsContainer; private StatusBarTouchProxy mStatusBarTouchProxy; private boolean mShowing; private boolean mWaitingToShow; private boolean mWaitingToShowAnimated; private boolean mReadyToShow; private int mNumItemsWaitingForThumbnailsAndIcons; private Choreographer mChoreo; OnRecentsPanelVisibilityChangedListener mVisibilityChangedListener; private RecentTasksLoader mRecentTasksLoader; private ArrayList mRecentTaskDescriptions; private Runnable mPreloadTasksRunnable; private boolean mRecentTasksDirty = true; private TaskDescriptionAdapter mListAdapter; private int mThumbnailWidth; private boolean mFitThumbnailToXY; public static interface OnRecentsPanelVisibilityChangedListener { public void onRecentsPanelVisibilityChanged(boolean visible); } public static interface RecentsScrollView { public int numItemsInOneScreenful(); public void setAdapter(TaskDescriptionAdapter adapter); public void setCallback(RecentsCallback callback); public void setMinSwipeAlpha(float minAlpha); } 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 thumbnailViewImage; Bitmap thumbnailViewImageBitmap; ImageView iconView; TextView labelView; TextView descriptionView; TaskDescription taskDescription; boolean loadedThumbnailAndIcon; } /* package */ final class TaskDescriptionAdapter extends BaseAdapter { private LayoutInflater mInflater; public TaskDescriptionAdapter(Context context) { mInflater = LayoutInflater.from(context); } public int getCount() { return mRecentTaskDescriptions != null ? mRecentTaskDescriptions.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 createView(ViewGroup parent) { View convertView = mInflater.inflate(R.layout.status_bar_recent_item, parent, false); ViewHolder holder = new ViewHolder(); holder.thumbnailView = convertView.findViewById(R.id.app_thumbnail); holder.thumbnailViewImage = (ImageView) convertView.findViewById(R.id.app_thumbnail_image); // If we set the default thumbnail now, we avoid an onLayout when we update // the thumbnail later (if they both have the same dimensions) if (mRecentTasksLoader != null) { updateThumbnail(holder, mRecentTasksLoader.getDefaultThumbnail(), false, false); } holder.iconView = (ImageView) convertView.findViewById(R.id.app_icon); if (mRecentTasksLoader != null) { holder.iconView.setImageBitmap(mRecentTasksLoader.getDefaultIcon()); } holder.labelView = (TextView) convertView.findViewById(R.id.app_label); holder.descriptionView = (TextView) convertView.findViewById(R.id.app_description); convertView.setTag(holder); return convertView; } public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = createView(parent); } ViewHolder holder = (ViewHolder) convertView.getTag(); // index is reverse since most recent appears at the bottom... final int index = mRecentTaskDescriptions.size() - position - 1; final TaskDescription td = mRecentTaskDescriptions.get(index); holder.labelView.setText(td.getLabel()); holder.thumbnailView.setContentDescription(td.getLabel()); holder.loadedThumbnailAndIcon = td.isLoaded(); if (td.isLoaded()) { updateThumbnail(holder, td.getThumbnail(), true, false); updateIcon(holder, td.getIcon(), true, false); mNumItemsWaitingForThumbnailsAndIcons--; } holder.thumbnailView.setTag(td); holder.thumbnailView.setOnLongClickListener(new OnLongClickDelegate(convertView)); holder.taskDescription = td; return convertView; } public void recycleView(View v) { ViewHolder holder = (ViewHolder) v.getTag(); updateThumbnail(holder, mRecentTasksLoader.getDefaultThumbnail(), false, false); holder.iconView.setImageBitmap(mRecentTasksLoader.getDefaultIcon()); holder.iconView.setVisibility(INVISIBLE); holder.labelView.setText(null); holder.thumbnailView.setContentDescription(null); holder.thumbnailView.setTag(null); holder.thumbnailView.setOnLongClickListener(null); holder.thumbnailView.setVisibility(INVISIBLE); holder.taskDescription = null; holder.loadedThumbnailAndIcon = false; } } public int numItemsInOneScreenful() { if (mRecentsContainer instanceof RecentsScrollView){ RecentsScrollView scrollView = (RecentsScrollView) mRecentsContainer; return scrollView.numItemsInOneScreenful(); } else { throw new IllegalArgumentException("missing Recents[Horizontal]ScrollView"); } } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && !event.isCanceled()) { show(false, true); return true; } return super.onKeyUp(keyCode, event); } private boolean pointInside(int x, int y, View v) { final int l = v.getLeft(); final int r = v.getRight(); final int t = v.getTop(); final int b = v.getBottom(); return x >= l && x < r && y >= t && y < b; } public boolean isInContentArea(int x, int y) { if (pointInside(x, y, mRecentsContainer)) { return true; } else if (mStatusBarTouchProxy != null && pointInside(x, y, mStatusBarTouchProxy)) { return true; } else { return false; } } public void show(boolean show, boolean animate) { if (show) { refreshRecentTasksList(null, true); mWaitingToShow = true; mWaitingToShowAnimated = animate; showIfReady(); } else { show(show, animate, null, false); } } private void showIfReady() { // mWaitingToShow = there was a touch up on the recents button // mReadyToShow = we've created views for the first screenful of items if (mWaitingToShow && mReadyToShow) { // && mNumItemsWaitingForThumbnailsAndIcons <= 0 show(true, mWaitingToShowAnimated, null, false); } } public void show(boolean show, boolean animate, ArrayList recentTaskDescriptions, boolean firstScreenful) { // For now, disable animations. We may want to re-enable in the future animate = false; if (show) { // Need to update list of recent apps before we set visibility so this view's // content description is updated before it gets focus for TalkBack mode refreshRecentTasksList(recentTaskDescriptions, firstScreenful); // if there are no apps, either bring up a "No recent apps" message, or just // quit early boolean noApps = (mRecentTaskDescriptions.size() == 0); if (mRecentsNoApps != null) { mRecentsNoApps.setVisibility(noApps ? View.VISIBLE : View.INVISIBLE); } else { if (noApps) { if (DEBUG) Log.v(TAG, "Nothing to show"); // Need to set recent tasks to dirty so that next time we load, we // refresh the list of tasks mRecentTasksLoader.cancelLoadingThumbnailsAndIcons(); mRecentTasksDirty = true; mWaitingToShow = false; mReadyToShow = false; return; } } } else { // Need to set recent tasks to dirty so that next time we load, we // refresh the list of tasks mRecentTasksLoader.cancelLoadingThumbnailsAndIcons(); mRecentTasksDirty = true; mWaitingToShow = false; mReadyToShow = false; } 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); onAnimationEnd(null); } if (show) { setFocusable(true); setFocusableInTouchMode(true); requestFocus(); } else { if (mPopup != null) { mPopup.dismiss(); } } } public void dismiss() { hide(true); } public void hide(boolean animate) { if (!animate) { setVisibility(View.GONE); } if (mBar != null) { // This will indirectly cause show(false, ...) to get called mBar.animateCollapse(); } } 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); clearRecentTasksList(); } } 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(BaseStatusBar bar) { mBar = bar; } public void setStatusBarView(View statusBarView) { if (mStatusBarTouchProxy != null) { mStatusBarTouchProxy.setStatusBar(statusBarView); } } public void setRecentTasksLoader(RecentTasksLoader loader) { mRecentTasksLoader = loader; } public void setOnVisibilityChangedListener(OnRecentsPanelVisibilityChangedListener l) { mVisibilityChangedListener = l; } public void setVisibility(int visibility) { if (mVisibilityChangedListener != null) { mVisibilityChangedListener.onRecentsPanelVisibilityChanged(visibility == VISIBLE); } super.setVisibility(visibility); } public RecentsPanelView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RecentsPanelView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mContext = context; updateValuesFromResources(); } public void updateValuesFromResources() { final Resources res = mContext.getResources(); mThumbnailWidth = Math.round(res.getDimension(R.dimen.status_bar_recents_thumbnail_width)); mFitThumbnailToXY = res.getBoolean(R.bool.config_recents_thumbnail_image_fits_to_xy); } @Override protected void onFinishInflate() { super.onFinishInflate(); mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mRecentsContainer = (ViewGroup) findViewById(R.id.recents_container); mStatusBarTouchProxy = (StatusBarTouchProxy) findViewById(R.id.status_bar_touch_proxy); mListAdapter = new TaskDescriptionAdapter(mContext); if (mRecentsContainer instanceof RecentsScrollView){ RecentsScrollView scrollView = (RecentsScrollView) mRecentsContainer; scrollView.setAdapter(mListAdapter); scrollView.setCallback(this); } else { throw new IllegalArgumentException("missing Recents[Horizontal]ScrollView"); } mRecentsScrim = findViewById(R.id.recents_bg_protect); mRecentsNoApps = findViewById(R.id.recents_no_apps); mChoreo = new Choreographer(this, mRecentsScrim, mRecentsContainer, mRecentsNoApps, this); if (mRecentsScrim != null) { Display d = ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay(); if (!ActivityManager.isHighEndGfx(d)) { mRecentsScrim.setBackgroundDrawable(null); } else if (mRecentsScrim.getBackground() instanceof BitmapDrawable) { // In order to save space, we make the background texture repeat in the Y direction ((BitmapDrawable) mRecentsScrim.getBackground()).setTileModeY(TileMode.REPEAT); } } mPreloadTasksRunnable = new Runnable() { public void run() { // If we set our visibility to INVISIBLE here, we avoid an extra call to // onLayout later when we become visible (because onLayout is always called // when going from GONE) if (!mShowing) { setVisibility(INVISIBLE); refreshRecentTasksList(); } } }; } public void setMinSwipeAlpha(float minAlpha) { if (mRecentsContainer instanceof RecentsScrollView){ RecentsScrollView scrollView = (RecentsScrollView) mRecentsContainer; scrollView.setMinSwipeAlpha(minAlpha); } } private void createCustomAnimations(LayoutTransition transitioner) { transitioner.setDuration(200); transitioner.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0); transitioner.setAnimator(LayoutTransition.DISAPPEARING, null); } private void updateIcon(ViewHolder h, Drawable icon, boolean show, boolean anim) { if (icon != null) { h.iconView.setImageDrawable(icon); if (show && h.iconView.getVisibility() != View.VISIBLE) { if (anim) { h.iconView.setAnimation( AnimationUtils.loadAnimation(mContext, R.anim.recent_appear)); } h.iconView.setVisibility(View.VISIBLE); } } } private void updateThumbnail(ViewHolder h, Bitmap thumbnail, boolean show, boolean anim) { if (thumbnail != null) { // Should remove the default image in the frame // that this now covers, to improve scrolling speed. // That can't be done until the anim is complete though. h.thumbnailViewImage.setImageBitmap(thumbnail); // scale the image to fill the full width of the ImageView. do this only if // we haven't set a bitmap before, or if the bitmap size has changed if (h.thumbnailViewImageBitmap == null || h.thumbnailViewImageBitmap.getWidth() != thumbnail.getWidth() || h.thumbnailViewImageBitmap.getHeight() != thumbnail.getHeight()) { if (mFitThumbnailToXY) { h.thumbnailViewImage.setScaleType(ScaleType.FIT_XY); } else { Matrix scaleMatrix = new Matrix(); float scale = mThumbnailWidth / (float) thumbnail.getWidth(); scaleMatrix.setScale(scale, scale); h.thumbnailViewImage.setScaleType(ScaleType.MATRIX); h.thumbnailViewImage.setImageMatrix(scaleMatrix); } } if (show && h.thumbnailView.getVisibility() != View.VISIBLE) { if (anim) { h.thumbnailView.setAnimation( AnimationUtils.loadAnimation(mContext, R.anim.recent_appear)); } h.thumbnailView.setVisibility(View.VISIBLE); } h.thumbnailViewImageBitmap = thumbnail; } } void onTaskThumbnailLoaded(TaskDescription td) { synchronized (td) { if (mRecentsContainer != null) { ViewGroup container = mRecentsContainer; if (container instanceof RecentsScrollView) { container = (ViewGroup) container.findViewById( R.id.recents_linear_layout); } // Look for a view showing this thumbnail, to update. for (int i=0; i < container.getChildCount(); i++) { View v = container.getChildAt(i); if (v.getTag() instanceof ViewHolder) { ViewHolder h = (ViewHolder)v.getTag(); if (!h.loadedThumbnailAndIcon && h.taskDescription == td) { // only fade in the thumbnail if recents is already visible-- we // show it immediately otherwise //boolean animateShow = mShowing && // mRecentsContainer.getAlpha() > ViewConfiguration.ALPHA_THRESHOLD; boolean animateShow = false; updateIcon(h, td.getIcon(), true, animateShow); updateThumbnail(h, td.getThumbnail(), true, animateShow); h.loadedThumbnailAndIcon = true; mNumItemsWaitingForThumbnailsAndIcons--; } } } } } showIfReady(); } // additional optimization when we have sofware system buttons - start loading the recent // tasks on touch down @Override public boolean onTouch(View v, MotionEvent ev) { if (!mShowing) { int action = ev.getAction() & MotionEvent.ACTION_MASK; if (action == MotionEvent.ACTION_DOWN) { post(mPreloadTasksRunnable); } else if (action == MotionEvent.ACTION_CANCEL) { setVisibility(GONE); clearRecentTasksList(); // Remove the preloader if we haven't called it yet removeCallbacks(mPreloadTasksRunnable); } else if (action == MotionEvent.ACTION_UP) { // Remove the preloader if we haven't called it yet removeCallbacks(mPreloadTasksRunnable); if (!v.isPressed()) { setVisibility(GONE); clearRecentTasksList(); } } } return false; } public void preloadRecentTasksList() { if (!mShowing) { mPreloadTasksRunnable.run(); } } public void clearRecentTasksList() { // Clear memory used by screenshots if (!mShowing && mRecentTaskDescriptions != null) { mRecentTasksLoader.cancelLoadingThumbnailsAndIcons(); mRecentTaskDescriptions.clear(); mListAdapter.notifyDataSetInvalidated(); mRecentTasksDirty = true; } } public void refreshRecentTasksList() { refreshRecentTasksList(null, false); } private void refreshRecentTasksList( ArrayList recentTasksList, boolean firstScreenful) { if (mRecentTasksDirty) { if (recentTasksList != null) { mFirstScreenful = true; onTasksLoaded(recentTasksList); } else { mFirstScreenful = true; mRecentTasksLoader.loadTasksInBackground(); } mRecentTasksDirty = false; } } boolean mFirstScreenful; public void onTasksLoaded(ArrayList tasks) { if (!mFirstScreenful && tasks.size() == 0) { return; } mNumItemsWaitingForThumbnailsAndIcons = mFirstScreenful ? tasks.size() : mRecentTaskDescriptions == null ? 0 : mRecentTaskDescriptions.size(); if (mRecentTaskDescriptions == null) { mRecentTaskDescriptions = new ArrayList(tasks); } else { mRecentTaskDescriptions.addAll(tasks); } mListAdapter.notifyDataSetInvalidated(); updateUiElements(getResources().getConfiguration()); mReadyToShow = true; mFirstScreenful = false; showIfReady(); } public ArrayList getRecentTasksList() { return mRecentTaskDescriptions; } public boolean getFirstScreenful() { return mFirstScreenful; } private void updateUiElements(Configuration config) { final int items = mRecentTaskDescriptions.size(); mRecentsContainer.setVisibility(items > 0 ? View.VISIBLE : View.GONE); // Set description for accessibility int numRecentApps = mRecentTaskDescriptions.size(); String recentAppsAccessibilityDescription; if (numRecentApps == 0) { recentAppsAccessibilityDescription = getResources().getString(R.string.status_bar_no_recent_apps); } else { recentAppsAccessibilityDescription = getResources().getQuantityString( R.plurals.status_bar_accessibility_recent_apps, numRecentApps, numRecentApps); } setContentDescription(recentAppsAccessibilityDescription); } public void handleOnClick(View view) { ViewHolder holder = (ViewHolder)view.getTag(); TaskDescription ad = holder.taskDescription; final Context context = view.getContext(); final ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); holder.thumbnailViewImage.setDrawingCacheEnabled(true); Bitmap bm = holder.thumbnailViewImage.getDrawingCache(); ActivityOptions opts = ActivityOptions.makeThumbnailScaleUpAnimation( holder.thumbnailViewImage, bm, 0, 0, new ActivityOptions.OnAnimationStartedListener() { @Override public void onAnimationStarted() { hide(true); } }); 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, opts.toBundle()); } else { Intent intent = ad.intent; intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY | Intent.FLAG_ACTIVITY_TASK_ON_HOME | Intent.FLAG_ACTIVITY_NEW_TASK); if (DEBUG) Log.v(TAG, "Starting activity " + intent); context.startActivity(intent, opts.toBundle()); } holder.thumbnailViewImage.setDrawingCacheEnabled(false); } public void onItemClick(AdapterView parent, View view, int position, long id) { handleOnClick(view); } public void handleSwipe(View view) { TaskDescription ad = ((ViewHolder) view.getTag()).taskDescription; if (DEBUG) Log.v(TAG, "Jettison " + ad.getLabel()); mRecentTaskDescriptions.remove(ad); // Handled by widget containers to enable LayoutTransitions properly // mListAdapter.notifyDataSetChanged(); if (mRecentTaskDescriptions.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.persistentTaskId, ActivityManager.REMOVE_TASK_KILL_PROCESS); // Accessibility feedback setContentDescription( mContext.getString(R.string.accessibility_recents_item_dismissed, ad.getLabel())); sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); setContentDescription(null); } 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 boolean onInterceptTouchEvent(MotionEvent ev) { if (mPopup != null) { return true; } else { return super.onInterceptTouchEvent(ev); } } public void handleLongPress( final View selectedView, final View anchorView, final View thumbnailView) { thumbnailView.setSelected(true); final PopupMenu popup = new PopupMenu(mContext, anchorView == null ? selectedView : anchorView); mPopup = popup; 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 TaskDescription ad = viewHolder.taskDescription; startApplicationDetailsActivity(ad.packageName); mBar.animateCollapse(); } else { throw new IllegalStateException("Oops, no tag on view " + selectedView); } } else { return false; } return true; } }); popup.setOnDismissListener(new PopupMenu.OnDismissListener() { public void onDismiss(PopupMenu menu) { thumbnailView.setSelected(false); mPopup = null; } }); popup.show(); } }