blob: a98ef0bb4a6b9b3efc51ed2db2df8af0120822b4 [file] [log] [blame]
/*
* Copyright (C) 2010 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 com.android.systemui.R;
import com.android.ex.carousel.CarouselView;
import com.android.ex.carousel.CarouselViewHelper;
import com.android.ex.carousel.CarouselRS.CarouselCallback;
import com.android.ex.carousel.CarouselViewHelper.DetailTextureParameters;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManagerNative;
import android.app.IActivityManager;
import android.app.IThumbnailReceiver;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.ActivityNotFoundException;
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.PaintFlagsDrawFilter;
import android.graphics.PorterDuffXfermode;
import android.graphics.PorterDuff;
import android.graphics.Bitmap.Config;
import android.graphics.drawable.Drawable;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.view.View.MeasureSpec;
import android.widget.TextView;
public class RecentApplicationsActivity extends Activity {
private static final String TAG = "RecentApplicationsActivity";
private static boolean DBG = false;
private static final int CARD_SLOTS = 56;
private static final int VISIBLE_SLOTS = 7;
private static final int MAX_TASKS = VISIBLE_SLOTS * 2;
// TODO: these should be configurable
private static final int DETAIL_TEXTURE_MAX_WIDTH = 200;
private static final int DETAIL_TEXTURE_MAX_HEIGHT = 80;
private static final int TEXTURE_WIDTH = 256;
private static final int TEXTURE_HEIGHT = 256;
private ActivityManager mActivityManager;
private List<RunningTaskInfo> mRunningTaskList;
private boolean mPortraitMode = true;
private ArrayList<ActivityDescription> mActivityDescriptions
= new ArrayList<ActivityDescription>();
private CarouselView mCarouselView;
private LocalCarouselViewHelper mHelper;
private View mNoRecentsView;
private Bitmap mLoadingBitmap;
private Bitmap mRecentOverlay;
private boolean mHidden = false;
private boolean mHiding = false;
private DetailInfo mDetailInfo;
/**
* This class is a container for all items associated with the DetailView we'll
* be drawing to a bitmap and sending to Carousel.
*
*/
static final class DetailInfo {
public DetailInfo(View _view, TextView _title, TextView _desc) {
view = _view;
title = _title;
description = _desc;
}
/**
* Draws view into the given bitmap, if provided
* @param bitmap
*/
public Bitmap draw(Bitmap bitmap) {
resizeView(view, DETAIL_TEXTURE_MAX_WIDTH, DETAIL_TEXTURE_MAX_HEIGHT);
int desiredWidth = view.getWidth();
int desiredHeight = view.getHeight();
if (bitmap == null || desiredWidth != bitmap.getWidth()
|| desiredHeight != bitmap.getHeight()) {
bitmap = Bitmap.createBitmap(desiredWidth, desiredHeight, Config.ARGB_8888);
}
Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
return bitmap;
}
/**
* Force a layout pass on the given view.
*/
private void resizeView(View view, int maxWidth, int maxHeight) {
int widthSpec = MeasureSpec.getMode(MeasureSpec.AT_MOST)
| MeasureSpec.getSize(maxWidth);
int heightSpec = MeasureSpec.getMode(MeasureSpec.AT_MOST)
| MeasureSpec.getSize(maxHeight);
view.measure(widthSpec, heightSpec);
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
Log.v(TAG, "RESIZED VIEW: " + view.getWidth() + ", " + view.getHeight());
}
public View view;
public TextView title;
public TextView description;
}
static class ActivityDescription {
int id;
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
int position; // position in list
public ActivityDescription(Bitmap _thumbnail,
Drawable _icon, String _label, String _desc, int _id, int _pos)
{
thumbnail = _thumbnail;
icon = _icon;
label = _label;
description = _desc;
id = _id;
position = _pos;
}
public void clear() {
icon = null;
thumbnail = null;
label = null;
description = null;
intent = null;
matrix = null;
id = -1;
position = -1;
}
};
private ActivityDescription findActivityDescription(int id) {
for (int i = 0; i < mActivityDescriptions.size(); i++) {
ActivityDescription item = mActivityDescriptions.get(i);
if (item != null && item.id == id) {
return item;
}
}
return null;
}
private class LocalCarouselViewHelper extends CarouselViewHelper {
private DetailTextureParameters mDetailParams = new DetailTextureParameters(10.0f, 20.0f);
public LocalCarouselViewHelper(Context context) {
super(context);
}
@Override
public DetailTextureParameters getDetailTextureParameters(int id) {
return mDetailParams;
}
public void onCardSelected(int n) {
if (n < mActivityDescriptions.size()) {
ActivityDescription item = mActivityDescriptions.get(n);
if (item.id >= 0) {
// This is an active task; it should just go to the foreground.
final ActivityManager am = (ActivityManager)
getSystemService(Context.ACTIVITY_SERVICE);
am.moveTaskToFront(item.id, ActivityManager.MOVE_TASK_WITH_HOME);
} else if (item.intent != null) {
// prepare a launch intent and send it
item.intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
| Intent.FLAG_ACTIVITY_TASK_ON_HOME);
try {
if (DBG) Log.v(TAG, "Starting intent " + item.intent);
startActivity(item.intent);
overridePendingTransition(R.anim.recent_app_enter, R.anim.recent_app_leave);
} catch (ActivityNotFoundException e) {
if (DBG) Log.w("Recent", "Unable to launch recent task", e);
}
finish();
}
}
}
@Override
public Bitmap getTexture(final int id) {
if (DBG) Log.v(TAG, "onRequestTexture(" + id + ")");
ActivityDescription info;
synchronized(mActivityDescriptions) {
info = mActivityDescriptions.get(id);
}
Bitmap bitmap = null;
if (info != null) {
bitmap = compositeBitmap(info);
}
return bitmap;
}
@Override
public Bitmap getDetailTexture(int n) {
Bitmap bitmap = null;
if (n < mActivityDescriptions.size()) {
ActivityDescription item = mActivityDescriptions.get(n);
mDetailInfo.title.setText(item.label);
mDetailInfo.description.setText(item.description);
bitmap = mDetailInfo.draw(null);
}
return bitmap;
}
};
private Bitmap compositeBitmap(ActivityDescription info) {
final int targetWidth = TEXTURE_WIDTH;
final int targetHeight = TEXTURE_HEIGHT;
final int border = 3; // inset along the edge for thumnnail content
final int overlap = 1; // how many pixels of overlap between border and thumbnail
final Resources res = getResources();
if (mRecentOverlay == null) {
mRecentOverlay = BitmapFactory.decodeResource(res, R.drawable.recent_overlay);
}
// Create a bitmap of the proper size/format and set the canvas to draw to it
final Bitmap result = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(result);
canvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG, Paint.FILTER_BITMAP_FLAG));
Paint paint = new Paint();
paint.setFilterBitmap(false);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
canvas.save();
if (info.thumbnail != null) {
// Draw the thumbnail
int sourceWidth = targetWidth - 2 * (border - overlap);
int sourceHeight = targetHeight - 2 * (border - overlap);
final float scaleX = (float) sourceWidth / info.thumbnail.getWidth();
final float scaleY = (float) sourceHeight / info.thumbnail.getHeight();
canvas.translate(border * 0.5f, border * 0.5f);
canvas.scale(scaleX, scaleY);
canvas.drawBitmap(info.thumbnail, 0, 0, paint);
} else {
// Draw the Loading bitmap placeholder, TODO: Remove when RS handles blending
final float scaleX = (float) targetWidth / mLoadingBitmap.getWidth();
final float scaleY = (float) targetHeight / mLoadingBitmap.getHeight();
canvas.scale(scaleX, scaleY);
canvas.drawBitmap(mLoadingBitmap, 0, 0, paint);
}
canvas.restore();
// Draw overlay
canvas.save();
final float scaleOverlayX = (float) targetWidth / mRecentOverlay.getWidth();
final float scaleOverlayY = (float) targetHeight / mRecentOverlay.getHeight();
canvas.scale(scaleOverlayX, scaleOverlayY);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
canvas.drawBitmap(mRecentOverlay, 0, 0, paint);
canvas.restore();
// Draw icon
if (info.icon != null) {
canvas.save();
info.icon.draw(canvas);
canvas.restore();
}
return result;
}
private final IThumbnailReceiver mThumbnailReceiver = new IThumbnailReceiver.Stub() {
public void finished() throws RemoteException {
}
public void newThumbnail(final int id, final Bitmap bitmap, CharSequence description)
throws RemoteException {
int w = bitmap.getWidth();
int h = bitmap.getHeight();
if (DBG) Log.v(TAG, "New thumbnail for id=" + id + ", dimensions=" + w + "x" + h
+ " description '" + description + "'");
ActivityDescription info = findActivityDescription(id);
if (info != null) {
info.thumbnail = bitmap;
info.description = description;
final int thumbWidth = bitmap.getWidth();
final int thumbHeight = bitmap.getHeight();
if ((mPortraitMode && thumbWidth > thumbHeight)
|| (!mPortraitMode && thumbWidth < thumbHeight)) {
Matrix matrix = new Matrix();
matrix.setRotate(90.0f, (float) thumbWidth / 2, (float) thumbHeight / 2);
info.matrix = matrix;
} else {
info.matrix = null;
}
// Force Carousel to request new textures for this item.
mCarouselView.setTextureForItem(info.position, null);
mCarouselView.setDetailTextureForItem(info.position, 0, 0, 0, 0, null);
} else {
if (DBG) Log.v(TAG, "Can't find view for id " + id);
}
}
};
/**
* We never really finish() RecentApplicationsActivity, since we don't want to
* get destroyed and pay the start-up cost to restart it.
*/
@Override
public void finish() {
moveTaskToBack(true);
}
@Override
protected void onNewIntent(Intent intent) {
mHidden = !mHidden;
if (mHidden) {
mHiding = true;
moveTaskToBack(true);
} else {
mHiding = false;
}
super.onNewIntent(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Resources res = getResources();
final View decorView = getWindow().getDecorView();
getWindow().getDecorView().setBackgroundColor(0x80000000);
if (mCarouselView == null) {
long t = System.currentTimeMillis();
setContentView(R.layout.recent_apps_activity);
long elapsed = System.currentTimeMillis() - t;
Log.v(TAG, "Recents layout took " + elapsed + "ms to load");
mLoadingBitmap = BitmapFactory.decodeResource(res, R.drawable.recent_rez_border);
mCarouselView = (CarouselView)findViewById(R.id.carousel);
mHelper = new LocalCarouselViewHelper(this);
mHelper.setCarouselView(mCarouselView);
mCarouselView.setSlotCount(CARD_SLOTS);
mCarouselView.setVisibleSlots(VISIBLE_SLOTS);
mCarouselView.createCards(0);
mCarouselView.setStartAngle((float) -(2.0f*Math.PI * 5 / CARD_SLOTS));
mCarouselView.setDefaultBitmap(mLoadingBitmap);
mCarouselView.setLoadingBitmap(mLoadingBitmap);
mCarouselView.setRezInCardCount(3.0f);
mCarouselView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
mNoRecentsView = (View) findViewById(R.id.no_applications_message);
mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
mPortraitMode = decorView.getHeight() > decorView.getWidth();
// Load detail view which will be used to render text
View detail = getLayoutInflater().inflate(R.layout.recents_detail_view, null);
TextView title = (TextView) detail.findViewById(R.id.app_title);
TextView description = (TextView) detail.findViewById(R.id.app_description);
mDetailInfo = new DetailInfo(detail, title, description);
refresh();
}
}
@Override
protected void onResume() {
super.onResume();
refresh();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mPortraitMode = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT;
if (DBG) Log.v(TAG, "CONFIG CHANGE, mPortraitMode = " + mPortraitMode);
refresh();
}
void updateRunningTasks() {
mRunningTaskList = mActivityManager.getRunningTasks(MAX_TASKS, 0, mThumbnailReceiver);
if (DBG) Log.v(TAG, "Portrait: " + mPortraitMode);
for (RunningTaskInfo r : mRunningTaskList) {
if (r.thumbnail != null) {
int thumbWidth = r.thumbnail.getWidth();
int thumbHeight = r.thumbnail.getHeight();
if (DBG) Log.v(TAG, "Got thumbnail " + thumbWidth + "x" + thumbHeight);
ActivityDescription desc = findActivityDescription(r.id);
if (desc != null) {
desc.thumbnail = r.thumbnail;
desc.description = r.description;
if ((mPortraitMode && thumbWidth > thumbHeight)
|| (!mPortraitMode && thumbWidth < thumbHeight)) {
Matrix matrix = new Matrix();
matrix.setRotate(90.0f, (float) thumbWidth / 2, (float) thumbHeight / 2);
desc.matrix = matrix;
}
} else {
if (DBG) Log.v(TAG, "Couldn't find ActivityDesc for id=" + r.id);
}
} else {
if (DBG) Log.v(TAG, "*** RUNNING THUMBNAIL WAS NULL ***");
}
}
}
private void updateRecentTasks() {
final PackageManager pm = getPackageManager();
final ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
final List<ActivityManager.RecentTaskInfo> recentTasks =
am.getRecentTasks(MAX_TASKS, ActivityManager.RECENT_IGNORE_UNAVAILABLE);
ActivityInfo homeInfo = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
.resolveActivityInfo(pm, 0);
// IconUtilities iconUtilities = new IconUtilities(this); // FIXME
int numTasks = recentTasks.size();
mActivityDescriptions.clear();
for (int i = 0, 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);
int id = recentTasks.get(i).id;
if (id != -1 && title != null && title.length() > 0 && icon != null) {
// icon = null; FIXME: iconUtilities.createIconDrawable(icon);
ActivityDescription item = new ActivityDescription(
null, icon, title, null, id, index);
item.intent = intent;
mActivityDescriptions.add(item);
if (DBG) Log.v(TAG, "Added item[" + index
+ "], id=" + item.id
+ ", title=" + item.label);
++index;
} else {
if (DBG) Log.v(TAG, "SKIPPING item " + id);
}
}
}
}
private final Runnable mRefreshRunnable = new Runnable() {
public void run() {
updateRecentTasks();
updateRunningTasks();
showCarousel(mActivityDescriptions.size() > 0);
}
};
private void showCarousel(boolean show) {
if (show) {
mCarouselView.createCards(mActivityDescriptions.size());
for (int i = 1; i < mActivityDescriptions.size(); i++) {
// Force Carousel to update textures. Note we don't do this for the first item,
// since it will be updated when mThumbnailReceiver returns a thumbnail.
// TODO: only do this for apps that have changed.
mCarouselView.setTextureForItem(i, null);
mCarouselView.setDetailTextureForItem(i, 0, 0, 0, 0, null);
}
// Make carousel visible
mNoRecentsView.setVisibility(View.GONE);
mCarouselView.setVisibility(View.VISIBLE);
mCarouselView.createCards(mActivityDescriptions.size());
} else {
// show "No Recent Tasks"
mNoRecentsView.setVisibility(View.VISIBLE);
mCarouselView.setVisibility(View.GONE);
}
}
private void refresh() {
if (!mHiding && mCarouselView != null) {
// Don't update the view now. Instead, post a request so it happens next time
// we reach the looper after a delay. This way we can fold multiple refreshes
// into just the latest.
mCarouselView.removeCallbacks(mRefreshRunnable);
mCarouselView.postDelayed(mRefreshRunnable, 50);
}
}
}