Load bookmarks asynchronously

 Bug: 5297900

Change-Id: I8b728cfe06799099e21c402d5da7087507209ffa
diff --git a/res/values/ids.xml b/res/values/ids.xml
index f342d4c..211b02f 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -20,4 +20,6 @@
     <item type="id" name="child_position" />
     <item type="id" name="child_id" />
     <item type="id" name="tab_view" />
+    <item type="id" name="position" />
+    <item type="id" name="load_object" />
 </resources>
diff --git a/src/com/android/browser/BrowserBookmarksAdapter.java b/src/com/android/browser/BrowserBookmarksAdapter.java
index fcc3f27..be3c211 100644
--- a/src/com/android/browser/BrowserBookmarksAdapter.java
+++ b/src/com/android/browser/BrowserBookmarksAdapter.java
@@ -19,19 +19,24 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
 import android.provider.BrowserContract.Bookmarks;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.CursorAdapter;
 import android.widget.ImageView;
 import android.widget.ImageView.ScaleType;
 import android.widget.TextView;
 
-public class BrowserBookmarksAdapter extends CursorAdapter {
+import com.android.browser.util.ThreadedCursorAdapter;
+import com.android.browser.view.BookmarkContainer;
+
+public class BrowserBookmarksAdapter extends
+        ThreadedCursorAdapter<BrowserBookmarksAdapterItem> {
+
     LayoutInflater mInflater;
     int mCurrentView;
+    Context mContext;
 
     /**
      *  Create a new BrowserBookmarksAdapter.
@@ -39,30 +44,53 @@
     public BrowserBookmarksAdapter(Context context, int defaultView) {
         // Make sure to tell the CursorAdapter to avoid the observer and auto-requery
         // since the Loader will do that for us.
-        super(context, null, 0);
+        super(context, null);
         mInflater = LayoutInflater.from(context);
+        mContext = context;
         selectView(defaultView);
     }
 
     @Override
-    public void bindView(View view, Context context, Cursor cursor) {
+    public View newView(Context context, ViewGroup parent) {
         if (mCurrentView == BrowserBookmarksPage.VIEW_LIST) {
-            bindListView(view, context, cursor);
+            return mInflater.inflate(R.layout.bookmark_list, parent, false);
         } else {
-            bindGridView(view, context, cursor);
+            return mInflater.inflate(R.layout.bookmark_thumbnail, parent, false);
         }
     }
 
-    CharSequence getTitle(Cursor cursor, Context context) {
+    @Override
+    public void bindView(View view, BrowserBookmarksAdapterItem object) {
+        BookmarkContainer container = (BookmarkContainer) view;
+        container.setIgnoreRequestLayout(true);
+        if (mCurrentView == BrowserBookmarksPage.VIEW_LIST) {
+            bindListView(view, mContext, object);
+        } else {
+            bindGridView(view, mContext, object);
+        }
+        container.setIgnoreRequestLayout(false);
+    }
+
+    void clearView(View view) {
+        if (mCurrentView == BrowserBookmarksPage.VIEW_LIST) {
+            ImageView favicon = (ImageView) view.findViewById(R.id.favicon);
+            favicon.setImageBitmap(null);
+        } else {
+            ImageView thumb = (ImageView) view.findViewById(R.id.thumb);
+            thumb.setImageBitmap(null);
+        }
+    }
+
+    CharSequence getTitle(Cursor cursor) {
         int type = cursor.getInt(BookmarksLoader.COLUMN_INDEX_TYPE);
         switch (type) {
         case Bookmarks.BOOKMARK_TYPE_OTHER_FOLDER:
-            return context.getText(R.string.other_bookmarks);
+            return mContext.getText(R.string.other_bookmarks);
         }
         return cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE);
     }
 
-    void bindGridView(View view, Context context, Cursor cursor) {
+    void bindGridView(View view, Context context, BrowserBookmarksAdapterItem item) {
         // We need to set this to handle rotation and other configuration change
         // events. If the padding didn't change, this is a no op.
         int padding = context.getResources()
@@ -72,63 +100,42 @@
         ImageView thumb = (ImageView) view.findViewById(R.id.thumb);
         TextView tv = (TextView) view.findViewById(R.id.label);
 
-        tv.setText(getTitle(cursor, context));
-        if (cursor.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) != 0) {
+        tv.setText(item.title);
+        if (item.is_folder) {
             // folder
             thumb.setImageResource(R.drawable.thumb_bookmark_widget_folder_holo);
             thumb.setScaleType(ScaleType.FIT_END);
-            thumb.setBackgroundDrawable(null);
+            thumb.setBackground(null);
         } else {
-            byte[] thumbData = cursor.getBlob(BookmarksLoader.COLUMN_INDEX_THUMBNAIL);
-            Bitmap thumbBitmap = null;
-            if (thumbData != null) {
-                thumbBitmap = BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length);
-            }
-
             thumb.setScaleType(ScaleType.CENTER_CROP);
-            if (thumbBitmap == null) {
+            if (item.thumbnail == null) {
                 thumb.setImageResource(R.drawable.browser_thumbnail);
             } else {
-                thumb.setImageBitmap(thumbBitmap);
+                thumb.setImageDrawable(item.thumbnail);
             }
             thumb.setBackgroundResource(R.drawable.border_thumb_bookmarks_widget_holo);
         }
     }
 
-    void bindListView(View view, Context context, Cursor cursor) {
+    void bindListView(View view, Context context, BrowserBookmarksAdapterItem item) {
         ImageView favicon = (ImageView) view.findViewById(R.id.favicon);
         TextView tv = (TextView) view.findViewById(R.id.label);
 
-        tv.setText(getTitle(cursor, context));
-        if (cursor.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) != 0) {
+        tv.setText(item.title);
+        if (item.is_folder) {
             // folder
             favicon.setImageResource(R.drawable.ic_folder_holo_dark);
-            favicon.setBackgroundDrawable(null);
+            favicon.setBackground(null);
         } else {
-            byte[] faviconData = cursor.getBlob(BookmarksLoader.COLUMN_INDEX_FAVICON);
-            Bitmap faviconBitmap = null;
-            if (faviconData != null) {
-                faviconBitmap = BitmapFactory.decodeByteArray(faviconData, 0, faviconData.length);
-            }
-
-            if (faviconBitmap == null) {
+            if (item.favicon == null) {
                 favicon.setImageResource(R.drawable.app_web_browser_sm);
             } else {
-                favicon.setImageBitmap(faviconBitmap);
+                favicon.setImageDrawable(item.favicon);
             }
             favicon.setBackgroundResource(R.drawable.bookmark_list_favicon_bg);
         }
     }
 
-    @Override
-    public View newView(Context context, Cursor cursor, ViewGroup parent) {
-        if (mCurrentView == BrowserBookmarksPage.VIEW_LIST) {
-            return mInflater.inflate(R.layout.bookmark_list, parent, false);
-        } else {
-            return mInflater.inflate(R.layout.bookmark_thumbnail, parent, false);
-        }
-    }
-
     public void selectView(int view) {
         if (view != BrowserBookmarksPage.VIEW_LIST
                 && view != BrowserBookmarksPage.VIEW_THUMBNAILS) {
@@ -142,7 +149,34 @@
     }
 
     @Override
-    public Cursor getItem(int position) {
-        return (Cursor) super.getItem(position);
+    public BrowserBookmarksAdapterItem getRowObject(Cursor c,
+            BrowserBookmarksAdapterItem item) {
+        if (item == null) {
+            item = new BrowserBookmarksAdapterItem();
+        }
+        Bitmap favicon = item.favicon != null ? item.favicon.getBitmap() : null;
+        Bitmap thumbnail = item.thumbnail != null ? item.thumbnail.getBitmap() : null;
+        favicon = BrowserBookmarksPage.getBitmap(c,
+                BookmarksLoader.COLUMN_INDEX_FAVICON, favicon);
+        thumbnail = BrowserBookmarksPage.getBitmap(c,
+                BookmarksLoader.COLUMN_INDEX_THUMBNAIL, thumbnail);
+        if (favicon != null
+                && (item.favicon == null || item.favicon.getBitmap() != favicon)) {
+            item.favicon = new BitmapDrawable(mContext.getResources(), favicon);
+        }
+        if (thumbnail != null
+                && (item.thumbnail == null || item.thumbnail.getBitmap() != thumbnail)) {
+            item.thumbnail = new BitmapDrawable(mContext.getResources(), thumbnail);
+        }
+        item.is_folder = c.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) != 0;
+        item.title = getTitle(c);
+        item.url = c.getString(BookmarksLoader.COLUMN_INDEX_URL);
+        return item;
+    }
+
+    @Override
+    public BrowserBookmarksAdapterItem getLoadingObject() {
+        BrowserBookmarksAdapterItem item = new BrowserBookmarksAdapterItem();
+        return item;
     }
 }
diff --git a/src/com/android/browser/BrowserBookmarksAdapterItem.java b/src/com/android/browser/BrowserBookmarksAdapterItem.java
new file mode 100644
index 0000000..913b0fd
--- /dev/null
+++ b/src/com/android/browser/BrowserBookmarksAdapterItem.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2012 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.browser;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+
+public class BrowserBookmarksAdapterItem {
+    public String url;
+    public CharSequence title;
+    public BitmapDrawable favicon;
+    public BitmapDrawable thumbnail;
+    public boolean is_folder;
+}
diff --git a/src/com/android/browser/BrowserBookmarksPage.java b/src/com/android/browser/BrowserBookmarksPage.java
index 2c8a27a..5a609b1 100644
--- a/src/com/android/browser/BrowserBookmarksPage.java
+++ b/src/com/android/browser/BrowserBookmarksPage.java
@@ -32,6 +32,7 @@
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
@@ -235,11 +236,30 @@
     }
 
     static Bitmap getBitmap(Cursor cursor, int columnIndex) {
+        return getBitmap(cursor, columnIndex, null);
+    }
+
+    static ThreadLocal<Options> sOptions = new ThreadLocal<Options>() {
+        @Override
+        protected Options initialValue() {
+            return new Options();
+        };
+    };
+    static Bitmap getBitmap(Cursor cursor, int columnIndex, Bitmap inBitmap) {
         byte[] data = cursor.getBlob(columnIndex);
         if (data == null) {
             return null;
         }
-        return BitmapFactory.decodeByteArray(data, 0, data.length);
+        Options opts = sOptions.get();
+        opts.inBitmap = inBitmap;
+        opts.inSampleSize = 1;
+        opts.inScaled = false;
+        try {
+            return BitmapFactory.decodeByteArray(data, 0, data.length, opts);
+        } catch (IllegalArgumentException ex) {
+            // Failed to re-use bitmap, create a new one
+            return BitmapFactory.decodeByteArray(data, 0, data.length);
+        }
     }
 
     private MenuItem.OnMenuItemClickListener mContextItemClickListener =
diff --git a/src/com/android/browser/util/ThreadedCursorAdapter.java b/src/com/android/browser/util/ThreadedCursorAdapter.java
new file mode 100644
index 0000000..fe59ad1
--- /dev/null
+++ b/src/com/android/browser/util/ThreadedCursorAdapter.java
@@ -0,0 +1,193 @@
+/*
+ * 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.browser.util;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.BaseAdapter;
+import android.widget.CursorAdapter;
+
+import com.android.browser.R;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+
+public abstract class ThreadedCursorAdapter<T> extends BaseAdapter {
+
+    private static final String LOGTAG = "tca";
+    private static final boolean DEBUG = false;
+
+    private Context mContext;
+    private Object mCursorLock = new Object();
+    private CursorAdapter mCursorAdapter;
+    private T mLoadingObject;
+    private Handler mLoadHandler;
+    private Handler mHandler;
+    private int mSize;
+
+    private class LoadContainer {
+        WeakReference<View> view;
+        int position;
+        T bind_object;
+        Adapter owner;
+        boolean loaded;
+    }
+
+    public ThreadedCursorAdapter(Context context, Cursor c) {
+        mContext = context;
+        mCursorAdapter = new CursorAdapter(context, c, 0) {
+
+            @Override
+            public View newView(Context context, Cursor cursor, ViewGroup parent) {
+                throw new IllegalStateException("not supported");
+            }
+
+            @Override
+            public void bindView(View view, Context context, Cursor cursor) {
+                throw new IllegalStateException("not supported");
+            }
+
+            @Override
+            public void notifyDataSetChanged() {
+                super.notifyDataSetChanged();
+                mSize = getCount();
+                ThreadedCursorAdapter.this.notifyDataSetChanged();
+            }
+
+            @Override
+            public void notifyDataSetInvalidated() {
+                super.notifyDataSetInvalidated();
+                mSize = getCount();
+                ThreadedCursorAdapter.this.notifyDataSetInvalidated();
+            }
+
+        };
+        mSize = mCursorAdapter.getCount();
+        HandlerThread thread = new HandlerThread("threaded_adapter_" + this,
+                Process.THREAD_PRIORITY_BACKGROUND);
+        thread.start();
+        mLoadHandler = new Handler(thread.getLooper()) {
+            @SuppressWarnings("unchecked")
+            @Override
+            public void handleMessage(Message msg) {
+                if (DEBUG) {
+                    Log.d(LOGTAG, "loading: " + msg.what);
+                }
+                loadRowObject(msg.what, (LoadContainer) msg.obj);
+            }
+        };
+        mHandler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                @SuppressWarnings("unchecked")
+                LoadContainer container = (LoadContainer) msg.obj;
+                if (container == null) {
+                    return;
+                }
+                View view = container.view.get();
+                if (view == null
+                        || container.owner != ThreadedCursorAdapter.this
+                        || container.position != msg.what) {
+                    return;
+                }
+                container.loaded = true;
+                bindView(view, container.bind_object);
+            }
+        };
+    }
+
+    @Override
+    public int getCount() {
+        return mSize;
+    }
+
+    @Override
+    public Cursor getItem(int position) {
+        return (Cursor) mCursorAdapter.getItem(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    private void loadRowObject(int position, LoadContainer container) {
+        if (container == null
+                || container.position != position
+                || container.owner != ThreadedCursorAdapter.this
+                || container.view.get() == null) {
+            return;
+        }
+        synchronized (mCursorLock) {
+            Cursor c = (Cursor) mCursorAdapter.getItem(position);
+            container.bind_object = getRowObject(c, container.bind_object);
+        }
+        mHandler.obtainMessage(position, container).sendToTarget();
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (convertView == null) {
+            convertView = newView(mContext, parent);
+        }
+        @SuppressWarnings("unchecked")
+        LoadContainer container = (LoadContainer) convertView.getTag(R.id.load_object);
+        if (container == null) {
+            container = new LoadContainer();
+            container.view = new WeakReference<View>(convertView);
+            convertView.setTag(R.id.load_object, container);
+        }
+        if (container.position == position
+                && container.owner == this
+                && container.loaded) {
+            bindView(convertView, container.bind_object);
+        } else {
+            bindView(convertView, cachedLoadObject());
+            container.position = position;
+            container.loaded = false;
+            container.owner = this;
+            mLoadHandler.obtainMessage(position, container).sendToTarget();
+        }
+        return convertView;
+    }
+
+    private T cachedLoadObject() {
+        if (mLoadingObject == null) {
+            mLoadingObject = getLoadingObject();
+        }
+        return mLoadingObject;
+    }
+
+    public void changeCursor(Cursor cursor) {
+        synchronized (mCursorLock) {
+            mCursorAdapter.changeCursor(cursor);
+        }
+    }
+
+    public abstract View newView(Context context, ViewGroup parent);
+    public abstract void bindView(View view, T object);
+    public abstract T getRowObject(Cursor c, T recycleObject);
+    public abstract T getLoadingObject();
+}
\ No newline at end of file
diff --git a/src/com/android/browser/view/BookmarkContainer.java b/src/com/android/browser/view/BookmarkContainer.java
index 260b05e..5175589 100644
--- a/src/com/android/browser/view/BookmarkContainer.java
+++ b/src/com/android/browser/view/BookmarkContainer.java
@@ -29,7 +29,8 @@
 public class BookmarkContainer extends RelativeLayout implements OnClickListener {
 
     private OnClickListener mClickListener;
-    
+    private boolean mIgnoreRequestLayout = false;
+
     public BookmarkContainer(Context context) {
         super(context);
         init();
@@ -89,4 +90,15 @@
             mClickListener.onClick(view);
         }
     }
+
+    public void setIgnoreRequestLayout(boolean ignore) {
+        mIgnoreRequestLayout = ignore;
+    }
+
+    @Override
+    public void requestLayout() {
+        if (!mIgnoreRequestLayout) {
+            super.requestLayout();
+        }
+    }
 }