Several improvements to RecentActivities:

It now toggles between show/hide for each tap on the home button.

Added new bitmap generation for lighting and halo effect while loading.

Uses new CarouselViewHelper class to manage textures and threading.

Uses a "real view" to render detail text.

Activities can now overload onCreateDescription() to show a
description in Carousel.

Improved startup and resume speed by posting single event to
refresh the activity list.

Change-Id: Id5552da75b9d022d24f599d11358ddababc97006
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index f1f31cf..6057023 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -32,6 +32,7 @@
         <activity android:name=".recent.RecentApplicationsActivity"
             android:theme="@android:style/Theme.NoTitleBar"
             android:excludeFromRecents="true"
+            android:launchMode="singleInstance"
             android:exported="true">
         </activity>
 
diff --git a/packages/SystemUI/res/anim/recent_app_enter.xml b/packages/SystemUI/res/anim/recent_app_enter.xml
new file mode 100644
index 0000000..4947eee
--- /dev/null
+++ b/packages/SystemUI/res/anim/recent_app_enter.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2009, 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.
+*/
+-->
+
+<!-- Special window zoom animation: this is the element that enters the screen,
+     it starts at 200% and scales down.  Goes with zoom_exit.xml. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+        android:interpolator="@android:anim/decelerate_interpolator">
+    <scale android:fromXScale="0.25" android:toXScale="1.0"
+           android:fromYScale="0.25" android:toYScale="1.0"
+           android:pivotX="0%p" android:pivotY="0%p"
+           android:duration="500" />
+    <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+            android:duration="500"/>
+</set>
diff --git a/packages/SystemUI/res/anim/recent_app_leave.xml b/packages/SystemUI/res/anim/recent_app_leave.xml
new file mode 100644
index 0000000..3d83988
--- /dev/null
+++ b/packages/SystemUI/res/anim/recent_app_leave.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2009, 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.
+*/
+-->
+
+<!-- Special window zoom animation: this is the element that enters the screen,
+     it starts at 200% and scales down.  Goes with zoom_exit.xml. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+        android:interpolator="@android:anim/decelerate_interpolator">
+    <scale android:fromXScale="1.0" android:toXScale="0.25"
+           android:fromYScale="1.0" android:toYScale="0.25"
+           android:pivotX="0%p" android:pivotY="0%p"
+           android:duration="500" />
+    <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+            android:duration="500"/>
+</set>
diff --git a/packages/SystemUI/res/drawable/recent_overlay.png b/packages/SystemUI/res/drawable/recent_overlay.png
new file mode 100644
index 0000000..4dfa3d9
--- /dev/null
+++ b/packages/SystemUI/res/drawable/recent_overlay.png
Binary files differ
diff --git a/packages/SystemUI/res/drawable/recent_rez_border.png b/packages/SystemUI/res/drawable/recent_rez_border.png
new file mode 100644
index 0000000..ad025f5
--- /dev/null
+++ b/packages/SystemUI/res/drawable/recent_rez_border.png
Binary files differ
diff --git a/packages/SystemUI/res/layout/recents_detail_view.xml b/packages/SystemUI/res/layout/recents_detail_view.xml
new file mode 100644
index 0000000..879d0f2
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_detail_view.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2008, 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.
+*/
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <!-- Application Title -->
+    <TextView android:id="@+id/app_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:singleLine="true"/>
+
+    <!-- Application Details -->
+    <TextView
+        android:id="@+id/app_description"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall"/>
+
+</LinearLayout>
diff --git a/packages/SystemUI/src/com/android/systemui/recent/RecentApplicationsActivity.java b/packages/SystemUI/src/com/android/systemui/recent/RecentApplicationsActivity.java
index 9cc24be..bf24a1f 100644
--- a/packages/SystemUI/src/com/android/systemui/recent/RecentApplicationsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recent/RecentApplicationsActivity.java
@@ -20,7 +20,9 @@
 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;
@@ -38,37 +40,104 @@
 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.Handler;
 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 = true;
+    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 mBlankBitmap = Bitmap.createBitmap(
-            new int[] {0xff808080, 0xffffffff, 0xff808080, 0xffffffff}, 2, 2, Config.RGB_565);
+    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
-        String description; // generated by Activity.onCreateDescription()
+        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
@@ -106,14 +175,17 @@
         return null;
     }
 
-    final CarouselCallback mCarouselCallback = new CarouselCallback() {
+    private class LocalCarouselViewHelper extends CarouselViewHelper {
+        private Paint mPaint = new Paint();
+        private DetailTextureParameters mDetailParams = new DetailTextureParameters(10.0f, 20.0f);
 
-        public void onAnimationFinished() {
-
+        public LocalCarouselViewHelper(Context context) {
+            super(context);
         }
 
-        public void onAnimationStarted() {
-
+        @Override
+        public DetailTextureParameters getDetailTextureParameters(int id) {
+            return mDetailParams;
         }
 
         public void onCardSelected(int n) {
@@ -125,7 +197,7 @@
                     try {
                         if (DBG) Log.v(TAG, "Starting intent " + item.intent);
                         startActivity(item.intent);
-                        //overridePendingTransition(R.anim.zoom_enter, R.anim.zoom_exit);
+                        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);
                     }
@@ -134,49 +206,89 @@
             }
         }
 
-        public void onInvalidateTexture(int n) {
-
-        }
-
-        public void onRequestGeometry(int n) {
-
-        }
-
-        public void onInvalidateGeometry(int n) {
-
-        }
-
-        public void onRequestTexture(final int n) {
-            if (DBG) Log.v(TAG, "onRequestTexture(" + n + ")");
-            if (n < mActivityDescriptions.size()) {
-                mCarouselView.post(new Runnable() {
-                    public void run() {
-                        ActivityDescription info = mActivityDescriptions.get(n);
-                        if (info != null) {
-                            if (DBG) Log.v(TAG, "FOUND ACTIVITY THUMBNAIL " + info.thumbnail);
-                            Bitmap bitmap = info.thumbnail == null ? mBlankBitmap : info.thumbnail;
-                            mCarouselView.setTextureForItem(n, bitmap);
-                        } else {
-                            if (DBG) Log.v(TAG, "FAILED TO GET ACTIVITY THUMBNAIL FOR ITEM " + n);
-                        }
-                    }
-                });
+        @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;
         }
 
-        public void onInvalidateDetailTexture(int n) {
-
-        }
-
-        public void onRequestDetailTexture(int n) {
-
-        }
-
-        public void onReportFirstCardPosition(int n) {
-
+        @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 {
@@ -192,6 +304,7 @@
             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)
@@ -202,13 +315,34 @@
                 } else {
                     info.matrix = null;
                 }
-                mCarouselView.setTextureForItem(info.position, info.thumbnail);
+                mCarouselView.setTextureForItem(info.position, compositeBitmap(info));
             } 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);
@@ -218,25 +352,35 @@
 
         getWindow().getDecorView().setBackgroundColor(0x80000000);
         setContentView(R.layout.recent_apps_activity);
-        mCarouselView = (CarouselView)findViewById(R.id.carousel);
-        mNoRecentsView = (View) findViewById(R.id.no_applications_message);
-        //mCarouselView = new CarouselView(this);
-        //setContentView(mCarouselView);
-        mCarouselView.setSlotCount(CARD_SLOTS);
-        mCarouselView.setVisibleSlots(VISIBLE_SLOTS);
-        mCarouselView.createCards(1);
-        mCarouselView.setStartAngle((float) -(2.0f*Math.PI * 5 / CARD_SLOTS));
-        mCarouselView.setDefaultBitmap(mBlankBitmap);
-        mCarouselView.setLoadingBitmap(mBlankBitmap);
-        mCarouselView.setCallback(mCarouselCallback);
-        mCarouselView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
-
-        mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
-        mPortraitMode = decorView.getHeight() > decorView.getWidth();
-
-        refresh();
 
 
+        if (mCarouselView == null) {
+            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.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
@@ -264,7 +408,7 @@
                 ActivityDescription desc = findActivityDescription(r.id);
                 if (desc != null) {
                     desc.thumbnail = r.thumbnail;
-                    desc.label = r.topActivity.flattenToShortString();
+                    desc.description = r.description;
                     if ((mPortraitMode && thumbWidth > thumbHeight)
                             || (!mPortraitMode && thumbWidth < thumbHeight)) {
                         Matrix matrix = new Matrix();
@@ -336,17 +480,34 @@
         }
     }
 
-    private void refresh() {
-        updateRecentTasks();
-        updateRunningTasks();
-        if (mActivityDescriptions.size() == 0) {
-            // show "No Recent Takss"
-            mNoRecentsView.setVisibility(View.VISIBLE);
-            mCarouselView.setVisibility(View.GONE);
-        } else {
+    private final Runnable mRefreshRunnable = new Runnable() {
+        public void run() {
+            updateRecentTasks();
+            updateRunningTasks();
+            showCarousel(mActivityDescriptions.size() > 0);
+        }
+    };
+
+    private void showCarousel(boolean show) {
+        if (show) {
+            // 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);
         }
     }
 }