Initial commit of 3D gallery source code to project.
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..06e22e7
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,16 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := user
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := Gallery3D
+LOCAL_CERTIFICATE := media
+
+LOCAL_OVERRIDES_PACKAGES := Gallery
+
+include $(BUILD_PACKAGE)
+
+# Use the following include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..3437837
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest package="com.cooliris.media"
+    android:versionCode="30680"
+	android:versionName="1.1.30680"
+	xmlns:android="http://schemas.android.com/apk/res/android">
+	<application android:icon="@drawable/icon" android:label="@string/app_name"
+		android:debuggable="true">
+		<activity android:name=".Gallery" android:label="@string/app_name"
+			android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
+			android:configChanges="keyboardHidden|orientation"
+			android:clearTaskOnLaunch="true" android:noHistory="false" android:stateNotNeeded="true">
+			<intent-filter>
+				<action android:name="android.intent.action.MAIN" />
+				<category android:name="android.intent.category.LAUNCHER" />
+			</intent-filter>
+			<intent-filter>
+				<action android:name="android.intent.action.GET_CONTENT" />
+				<category android:name="android.intent.category.OPENABLE" />
+				<data android:mimeType="vnd.android.cursor.dir/image" />
+			</intent-filter>
+			<intent-filter>
+				<action android:name="android.intent.action.GET_CONTENT" />
+				<category android:name="android.intent.category.OPENABLE" />
+				<category android:name="android.intent.category.DEFAULT" />
+				<data android:mimeType="image/*" />
+				<data android:mimeType="video/*" />
+			</intent-filter>
+			<intent-filter>
+				<action android:name="android.intent.action.PICK" />
+				<category android:name="android.intent.category.DEFAULT" />
+				<data android:mimeType="image/*" />
+				<data android:mimeType="video/*" />
+			</intent-filter>
+			<intent-filter>
+				<action android:name="android.intent.action.PICK" />
+				<category android:name="android.intent.category.DEFAULT" />
+				<data android:mimeType="vnd.android.cursor.dir/image" />
+			</intent-filter>
+			<intent-filter>
+				<action android:name="android.intent.action.VIEW" />
+				<category android:name="android.intent.category.DEFAULT" />
+				<data android:mimeType="vnd.android.cursor.dir/image" />
+			</intent-filter>
+			<intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="vnd.android.cursor.dir/image" />
+            </intent-filter>
+			<intent-filter>
+				<action android:name="android.intent.action.VIEW" />
+				<category android:name="android.intent.category.DEFAULT" />
+				<data android:mimeType="image/*" />
+			</intent-filter>
+		</activity>
+		<activity android:name="CropImage"
+			android:configChanges="orientation|keyboardHidden" android:label="@string/crop_label">
+			<intent-filter android:label="@string/crop_label">
+				<action android:name="com.android.camera.action.CROP" />
+				<data android:mimeType="image/*" />
+				<category android:name="android.intent.category.DEFAULT" />
+				<category android:name="android.intent.category.ALTERNATIVE" />
+				<category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
+			</intent-filter>
+		</activity>
+		<activity android:name="MovieView"
+                android:label="@string/movie_view_label"
+                android:screenOrientation="landscape"
+                android:configChanges="orientation|keyboardHidden"
+                android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
+             <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="rtsp" />
+             </intent-filter>
+             <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="video/*" />
+                <data android:mimeType="application/sdp" />
+             </intent-filter>
+             <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="http" />
+                <data android:mimeType="video/mpeg4" />
+                <data android:mimeType="video/mp4" />
+                <data android:mimeType="video/3gp" />
+                <data android:mimeType="video/3gpp" />
+                <data android:mimeType="video/3gpp2" />
+             </intent-filter>
+        </activity>
+		<activity android:name="Photographs" android:icon="@drawable/icon">
+			<intent-filter android:label="@string/camera_setas_wallpaper">
+				<action android:name="android.intent.action.ATTACH_DATA" />
+				<data android:mimeType="image/*" />
+				<category android:name="android.intent.category.DEFAULT" />
+			</intent-filter>
+			<intent-filter android:label="Photographs">
+				<action android:name="android.intent.action.SET_WALLPAPER" />
+				<category android:name="android.intent.category.DEFAULT" />
+			</intent-filter>
+		</activity>
+
+		<provider android:label="Picasa Web Albums" android:name="com.cooliris.picasa.PicasaContentProvider"
+			android:grantUriPermissions="true"
+			android:syncable="true"
+			android:authorities="com.cooliris.picasa.contentprovider">
+	    </provider>
+		<service android:label="Picasa Sync Service" android:name="com.cooliris.picasa.PicasaService">
+			<intent-filter>
+				<action android:name="android.content.SyncAdapter" />
+			</intent-filter>
+			<meta-data android:name="android.content.SyncAdapter" android:resource="@xml/syncadapter" />
+		</service>
+		
+		<service android:label="CacheService" android:name="com.cooliris.cache.CacheService">
+		</service>
+		<receiver android:label="BootReceiver" android:name="com.cooliris.cache.BootReceiver"
+			android:enabled="true" android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
+			<intent-filter>
+				<category android:name="android.intent.category.DEFAULT" />
+				<action android:name="android.intent.action.BOOT_COMPLETE" />
+			</intent-filter>
+			<intent-filter>
+                <action android:name="android.intent.action.MEDIA_SCANNER_FINISHED" />
+                <data android:scheme="file" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.MEDIA_MOUNTED" />
+                <data android:scheme="file" />
+            </intent-filter>
+		</receiver>
+		<receiver android:name="PhotoAppWidgetProvider" android:label="@string/gadget_title">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+            </intent-filter>
+            <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget_info" />
+        </receiver>
+
+        <!-- We configure a widget by asking to pick a photo, then crop it, and store the config internally -->
+        <activity android:name="PhotoAppWidgetConfigure">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
+            </intent-filter>
+        </activity>
+
+        <!-- We also allow direct binding where the caller provides a bitmap and
+             appWidgetId to bind.  We require the permission because this changes our
+             internal database without user confirmation. -->
+        <activity android:name="PhotoAppWidgetBind" android:exported="true"
+                android:theme="@android:style/Theme.NoDisplay"
+                android:permission="android.permission.BIND_APPWIDGET" />
+	<receiver android:name="com.cooliris.picasa.PicasaReceiver"><intent-filter><action android:name="android.accounts.LOGIN_ACCOUNTS_CHANGED_ACTION"></action>
+</intent-filter>
+</receiver>
+</application>
+	<uses-sdk android:minSdkVersion="5" />
+	<uses-permission android:name="android.permission.SET_WALLPAPER" />
+	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+	<uses-permission android:name="android.permission.GET_ACCOUNTS" />
+	<uses-permission android:name="android.permission.USE_CREDENTIALS" />
+	<uses-permission android:name="android.permission.INTERNET" />
+	<uses-permission android:name="android.permission.VIBRATE" />
+	<uses-permission android:name="android.permission.WAKE_LOCK" />
+	<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+	<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
+	<supports-screens android:smallScreens="false"
+		android:normalScreens="true" android:largeScreens="true"
+		android:anyDensity="true" />
+
+
+</manifest>
diff --git a/res/drawable-hdpi/appwidget_bg.9.png b/res/drawable-hdpi/appwidget_bg.9.png
new file mode 100755
index 0000000..3b29eae
--- /dev/null
+++ b/res/drawable-hdpi/appwidget_bg.9.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_camera.png b/res/drawable-hdpi/btn_camera.png
new file mode 100644
index 0000000..dccc850
--- /dev/null
+++ b/res/drawable-hdpi/btn_camera.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_camera_focus.png b/res/drawable-hdpi/btn_camera_focus.png
new file mode 100644
index 0000000..f6ff841
--- /dev/null
+++ b/res/drawable-hdpi/btn_camera_focus.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_camera_pressed.png b/res/drawable-hdpi/btn_camera_pressed.png
new file mode 100644
index 0000000..977468a
--- /dev/null
+++ b/res/drawable-hdpi/btn_camera_pressed.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_hud_zoom_in_normal.png b/res/drawable-hdpi/btn_hud_zoom_in_normal.png
new file mode 100644
index 0000000..622b416
--- /dev/null
+++ b/res/drawable-hdpi/btn_hud_zoom_in_normal.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_hud_zoom_in_pressed.png b/res/drawable-hdpi/btn_hud_zoom_in_pressed.png
new file mode 100644
index 0000000..9c14235
--- /dev/null
+++ b/res/drawable-hdpi/btn_hud_zoom_in_pressed.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_hud_zoom_out_normal.png b/res/drawable-hdpi/btn_hud_zoom_out_normal.png
new file mode 100644
index 0000000..e25b5bd
--- /dev/null
+++ b/res/drawable-hdpi/btn_hud_zoom_out_normal.png
Binary files differ
diff --git a/res/drawable-hdpi/btn_hud_zoom_out_pressed.png b/res/drawable-hdpi/btn_hud_zoom_out_pressed.png
new file mode 100644
index 0000000..5173d40
--- /dev/null
+++ b/res/drawable-hdpi/btn_hud_zoom_out_pressed.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_fs_details.png b/res/drawable-hdpi/ic_fs_details.png
new file mode 100644
index 0000000..8f3ee26
--- /dev/null
+++ b/res/drawable-hdpi/ic_fs_details.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_spinner1.png b/res/drawable-hdpi/ic_spinner1.png
new file mode 100644
index 0000000..6318b7a
--- /dev/null
+++ b/res/drawable-hdpi/ic_spinner1.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_spinner2.png b/res/drawable-hdpi/ic_spinner2.png
new file mode 100644
index 0000000..af786c3
--- /dev/null
+++ b/res/drawable-hdpi/ic_spinner2.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_spinner3.png b/res/drawable-hdpi/ic_spinner3.png
new file mode 100644
index 0000000..07c0981
--- /dev/null
+++ b/res/drawable-hdpi/ic_spinner3.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_spinner4.png b/res/drawable-hdpi/ic_spinner4.png
new file mode 100644
index 0000000..a5ab782
--- /dev/null
+++ b/res/drawable-hdpi/ic_spinner4.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_spinner5.png b/res/drawable-hdpi/ic_spinner5.png
new file mode 100644
index 0000000..2a1471d
--- /dev/null
+++ b/res/drawable-hdpi/ic_spinner5.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_spinner6.png b/res/drawable-hdpi/ic_spinner6.png
new file mode 100644
index 0000000..9e1f5d3
--- /dev/null
+++ b/res/drawable-hdpi/ic_spinner6.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_spinner7.png b/res/drawable-hdpi/ic_spinner7.png
new file mode 100644
index 0000000..dbfa735
--- /dev/null
+++ b/res/drawable-hdpi/ic_spinner7.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_spinner8.png b/res/drawable-hdpi/ic_spinner8.png
new file mode 100644
index 0000000..534a8cf
--- /dev/null
+++ b/res/drawable-hdpi/ic_spinner8.png
Binary files differ
diff --git a/res/drawable-hdpi/icon.png b/res/drawable-hdpi/icon.png
new file mode 100755
index 0000000..f18048b
--- /dev/null
+++ b/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/res/drawable-hdpi/icon_camera_small.png b/res/drawable-hdpi/icon_camera_small.png
new file mode 100644
index 0000000..084c4ec
--- /dev/null
+++ b/res/drawable-hdpi/icon_camera_small.png
Binary files differ
diff --git a/res/drawable-hdpi/icon_cancel.png b/res/drawable-hdpi/icon_cancel.png
new file mode 100644
index 0000000..8165efd
--- /dev/null
+++ b/res/drawable-hdpi/icon_cancel.png
Binary files differ
diff --git a/res/drawable-hdpi/icon_delete.png b/res/drawable-hdpi/icon_delete.png
new file mode 100644
index 0000000..f2e6bbd
--- /dev/null
+++ b/res/drawable-hdpi/icon_delete.png
Binary files differ
diff --git a/res/drawable-hdpi/icon_folder_small.png b/res/drawable-hdpi/icon_folder_small.png
new file mode 100644
index 0000000..33ef42b
--- /dev/null
+++ b/res/drawable-hdpi/icon_folder_small.png
Binary files differ
diff --git a/res/drawable-hdpi/icon_home_small.png b/res/drawable-hdpi/icon_home_small.png
new file mode 100644
index 0000000..479b5d1
--- /dev/null
+++ b/res/drawable-hdpi/icon_home_small.png
Binary files differ
diff --git a/res/drawable-hdpi/icon_more.png b/res/drawable-hdpi/icon_more.png
new file mode 100644
index 0000000..b2502a1
--- /dev/null
+++ b/res/drawable-hdpi/icon_more.png
Binary files differ
diff --git a/res/drawable-hdpi/icon_picasa_small.png b/res/drawable-hdpi/icon_picasa_small.png
new file mode 100644
index 0000000..2e1a771
--- /dev/null
+++ b/res/drawable-hdpi/icon_picasa_small.png
Binary files differ
diff --git a/res/drawable-hdpi/icon_play.png b/res/drawable-hdpi/icon_play.png
new file mode 100644
index 0000000..0c61f32
--- /dev/null
+++ b/res/drawable-hdpi/icon_play.png
Binary files differ
diff --git a/res/drawable-hdpi/icon_share.png b/res/drawable-hdpi/icon_share.png
new file mode 100644
index 0000000..83ff864
--- /dev/null
+++ b/res/drawable-hdpi/icon_share.png
Binary files differ
diff --git a/res/drawable-hdpi/mode_grid.png b/res/drawable-hdpi/mode_grid.png
new file mode 100644
index 0000000..3748833
--- /dev/null
+++ b/res/drawable-hdpi/mode_grid.png
Binary files differ
diff --git a/res/drawable-hdpi/mode_stack.png b/res/drawable-hdpi/mode_stack.png
new file mode 100644
index 0000000..b3b0f41
--- /dev/null
+++ b/res/drawable-hdpi/mode_stack.png
Binary files differ
diff --git a/res/drawable-hdpi/photo_inner.9.png b/res/drawable-hdpi/photo_inner.9.png
new file mode 100644
index 0000000..dcd2d22
--- /dev/null
+++ b/res/drawable-hdpi/photo_inner.9.png
Binary files differ
diff --git a/res/drawable-hdpi/popup.9.png b/res/drawable-hdpi/popup.9.png
new file mode 100644
index 0000000..154ec9a
--- /dev/null
+++ b/res/drawable-hdpi/popup.9.png
Binary files differ
diff --git a/res/drawable-hdpi/popup_triangle_bottom.png b/res/drawable-hdpi/popup_triangle_bottom.png
new file mode 100644
index 0000000..9ff36e9
--- /dev/null
+++ b/res/drawable-hdpi/popup_triangle_bottom.png
Binary files differ
diff --git a/res/drawable-hdpi/scroller_new.png b/res/drawable-hdpi/scroller_new.png
new file mode 100644
index 0000000..ed5fb26
--- /dev/null
+++ b/res/drawable-hdpi/scroller_new.png
Binary files differ
diff --git a/res/drawable-hdpi/scroller_pressed_new.png b/res/drawable-hdpi/scroller_pressed_new.png
new file mode 100644
index 0000000..5e176ac
--- /dev/null
+++ b/res/drawable-hdpi/scroller_pressed_new.png
Binary files differ
diff --git a/res/drawable-hdpi/scroller_track_fill.png b/res/drawable-hdpi/scroller_track_fill.png
new file mode 100644
index 0000000..5199950
--- /dev/null
+++ b/res/drawable-hdpi/scroller_track_fill.png
Binary files differ
diff --git a/res/drawable-hdpi/videooverlay.png b/res/drawable-hdpi/videooverlay.png
new file mode 100644
index 0000000..1718832
--- /dev/null
+++ b/res/drawable-hdpi/videooverlay.png
Binary files differ
diff --git a/res/drawable-mdpi/appwidget_bg.9.png b/res/drawable-mdpi/appwidget_bg.9.png
new file mode 100755
index 0000000..afe41b6
--- /dev/null
+++ b/res/drawable-mdpi/appwidget_bg.9.png
Binary files differ
diff --git a/res/drawable-mdpi/photo_inner.9.png b/res/drawable-mdpi/photo_inner.9.png
new file mode 100755
index 0000000..f51eb35
--- /dev/null
+++ b/res/drawable-mdpi/photo_inner.9.png
Binary files differ
diff --git a/res/drawable/appwidget_bg.9.png b/res/drawable/appwidget_bg.9.png
new file mode 100755
index 0000000..afe41b6
--- /dev/null
+++ b/res/drawable/appwidget_bg.9.png
Binary files differ
diff --git a/res/drawable/btn_camera.png b/res/drawable/btn_camera.png
new file mode 100644
index 0000000..8369d48
--- /dev/null
+++ b/res/drawable/btn_camera.png
Binary files differ
diff --git a/res/drawable/btn_camera_focus.png b/res/drawable/btn_camera_focus.png
new file mode 100644
index 0000000..5345044
--- /dev/null
+++ b/res/drawable/btn_camera_focus.png
Binary files differ
diff --git a/res/drawable/btn_camera_normal.png b/res/drawable/btn_camera_normal.png
new file mode 100644
index 0000000..309af9d
--- /dev/null
+++ b/res/drawable/btn_camera_normal.png
Binary files differ
diff --git a/res/drawable/btn_camera_pressed.png b/res/drawable/btn_camera_pressed.png
new file mode 100644
index 0000000..e4ce9e0
--- /dev/null
+++ b/res/drawable/btn_camera_pressed.png
Binary files differ
diff --git a/res/drawable/btn_camera_small_normal.png b/res/drawable/btn_camera_small_normal.png
new file mode 100644
index 0000000..5132b81
--- /dev/null
+++ b/res/drawable/btn_camera_small_normal.png
Binary files differ
diff --git a/res/drawable/btn_grid.png b/res/drawable/btn_grid.png
new file mode 100644
index 0000000..70132fb
--- /dev/null
+++ b/res/drawable/btn_grid.png
Binary files differ
diff --git a/res/drawable/btn_grid_focus.png b/res/drawable/btn_grid_focus.png
new file mode 100644
index 0000000..d4563d8
--- /dev/null
+++ b/res/drawable/btn_grid_focus.png
Binary files differ
diff --git a/res/drawable/btn_grid_mode_focus.png b/res/drawable/btn_grid_mode_focus.png
new file mode 100644
index 0000000..c91028d
--- /dev/null
+++ b/res/drawable/btn_grid_mode_focus.png
Binary files differ
diff --git a/res/drawable/btn_grid_mode_normal.png b/res/drawable/btn_grid_mode_normal.png
new file mode 100644
index 0000000..c5b2468
--- /dev/null
+++ b/res/drawable/btn_grid_mode_normal.png
Binary files differ
diff --git a/res/drawable/btn_grid_mode_pressed.png b/res/drawable/btn_grid_mode_pressed.png
new file mode 100644
index 0000000..c27420e
--- /dev/null
+++ b/res/drawable/btn_grid_mode_pressed.png
Binary files differ
diff --git a/res/drawable/btn_grid_pressed.png b/res/drawable/btn_grid_pressed.png
new file mode 100644
index 0000000..e2d82ef
--- /dev/null
+++ b/res/drawable/btn_grid_pressed.png
Binary files differ
diff --git a/res/drawable/btn_hud_camera_focus.png b/res/drawable/btn_hud_camera_focus.png
new file mode 100644
index 0000000..60c27c8
--- /dev/null
+++ b/res/drawable/btn_hud_camera_focus.png
Binary files differ
diff --git a/res/drawable/btn_hud_camera_normal.png b/res/drawable/btn_hud_camera_normal.png
new file mode 100644
index 0000000..44e533f
--- /dev/null
+++ b/res/drawable/btn_hud_camera_normal.png
Binary files differ
diff --git a/res/drawable/btn_hud_camera_pressed.png b/res/drawable/btn_hud_camera_pressed.png
new file mode 100644
index 0000000..969c5bb
--- /dev/null
+++ b/res/drawable/btn_hud_camera_pressed.png
Binary files differ
diff --git a/res/drawable/btn_hud_zoom_in_normal.png b/res/drawable/btn_hud_zoom_in_normal.png
new file mode 100644
index 0000000..d6b4f4b
--- /dev/null
+++ b/res/drawable/btn_hud_zoom_in_normal.png
Binary files differ
diff --git a/res/drawable/btn_hud_zoom_in_pressed.png b/res/drawable/btn_hud_zoom_in_pressed.png
new file mode 100644
index 0000000..5470383
--- /dev/null
+++ b/res/drawable/btn_hud_zoom_in_pressed.png
Binary files differ
diff --git a/res/drawable/btn_hud_zoom_out_normal.png b/res/drawable/btn_hud_zoom_out_normal.png
new file mode 100644
index 0000000..8293e1e
--- /dev/null
+++ b/res/drawable/btn_hud_zoom_out_normal.png
Binary files differ
diff --git a/res/drawable/btn_hud_zoom_out_pressed.png b/res/drawable/btn_hud_zoom_out_pressed.png
new file mode 100644
index 0000000..d24e67d
--- /dev/null
+++ b/res/drawable/btn_hud_zoom_out_pressed.png
Binary files differ
diff --git a/res/drawable/btn_location_filter.png b/res/drawable/btn_location_filter.png
new file mode 100644
index 0000000..a13dcf0
--- /dev/null
+++ b/res/drawable/btn_location_filter.png
Binary files differ
diff --git a/res/drawable/btn_location_filter_unscaled.png b/res/drawable/btn_location_filter_unscaled.png
new file mode 100644
index 0000000..383efd2
--- /dev/null
+++ b/res/drawable/btn_location_filter_unscaled.png
Binary files differ
diff --git a/res/drawable/btn_next_indicator_normal.png b/res/drawable/btn_next_indicator_normal.png
new file mode 100644
index 0000000..0436d88
--- /dev/null
+++ b/res/drawable/btn_next_indicator_normal.png
Binary files differ
diff --git a/res/drawable/btn_play.png b/res/drawable/btn_play.png
new file mode 100644
index 0000000..9bddc60
--- /dev/null
+++ b/res/drawable/btn_play.png
Binary files differ
diff --git a/res/drawable/btn_stack.png b/res/drawable/btn_stack.png
new file mode 100644
index 0000000..51672df
--- /dev/null
+++ b/res/drawable/btn_stack.png
Binary files differ
diff --git a/res/drawable/btn_stack_focus.png b/res/drawable/btn_stack_focus.png
new file mode 100644
index 0000000..34ca8dd
--- /dev/null
+++ b/res/drawable/btn_stack_focus.png
Binary files differ
diff --git a/res/drawable/btn_stack_mode_focus.png b/res/drawable/btn_stack_mode_focus.png
new file mode 100644
index 0000000..f0a1efb
--- /dev/null
+++ b/res/drawable/btn_stack_mode_focus.png
Binary files differ
diff --git a/res/drawable/btn_stack_mode_normal.png b/res/drawable/btn_stack_mode_normal.png
new file mode 100644
index 0000000..4b75758
--- /dev/null
+++ b/res/drawable/btn_stack_mode_normal.png
Binary files differ
diff --git a/res/drawable/btn_stack_mode_pressed.png b/res/drawable/btn_stack_mode_pressed.png
new file mode 100644
index 0000000..4b98d73
--- /dev/null
+++ b/res/drawable/btn_stack_mode_pressed.png
Binary files differ
diff --git a/res/drawable/btn_stack_pressed.png b/res/drawable/btn_stack_pressed.png
new file mode 100644
index 0000000..1d0fdac
--- /dev/null
+++ b/res/drawable/btn_stack_pressed.png
Binary files differ
diff --git a/res/drawable/camera_crop_height.png b/res/drawable/camera_crop_height.png
new file mode 100644
index 0000000..b089aec
--- /dev/null
+++ b/res/drawable/camera_crop_height.png
Binary files differ
diff --git a/res/drawable/camera_crop_width.png b/res/drawable/camera_crop_width.png
new file mode 100644
index 0000000..65216af
--- /dev/null
+++ b/res/drawable/camera_crop_width.png
Binary files differ
diff --git a/res/drawable/default_background.png b/res/drawable/default_background.png
new file mode 100644
index 0000000..5db876d
--- /dev/null
+++ b/res/drawable/default_background.png
Binary files differ
diff --git a/res/drawable/fullscreen_hud_bg.png b/res/drawable/fullscreen_hud_bg.png
new file mode 100644
index 0000000..245e5bd
--- /dev/null
+++ b/res/drawable/fullscreen_hud_bg.png
Binary files differ
diff --git a/res/drawable/grid_check_off.png b/res/drawable/grid_check_off.png
new file mode 100644
index 0000000..3313d18
--- /dev/null
+++ b/res/drawable/grid_check_off.png
Binary files differ
diff --git a/res/drawable/grid_check_on.png b/res/drawable/grid_check_on.png
new file mode 100644
index 0000000..09cf22d
--- /dev/null
+++ b/res/drawable/grid_check_on.png
Binary files differ
diff --git a/res/drawable/grid_empty.png b/res/drawable/grid_empty.png
new file mode 100644
index 0000000..483a130
--- /dev/null
+++ b/res/drawable/grid_empty.png
Binary files differ
diff --git a/res/drawable/grid_frame.png b/res/drawable/grid_frame.png
new file mode 100644
index 0000000..d1a320b
--- /dev/null
+++ b/res/drawable/grid_frame.png
Binary files differ
diff --git a/res/drawable/grid_loading.png b/res/drawable/grid_loading.png
new file mode 100644
index 0000000..2bc57cb
--- /dev/null
+++ b/res/drawable/grid_loading.png
Binary files differ
diff --git a/res/drawable/grid_placeholder.png b/res/drawable/grid_placeholder.png
new file mode 100644
index 0000000..5151730
--- /dev/null
+++ b/res/drawable/grid_placeholder.png
Binary files differ
diff --git a/res/drawable/ic_fs_details.png b/res/drawable/ic_fs_details.png
new file mode 100644
index 0000000..dd7a82a
--- /dev/null
+++ b/res/drawable/ic_fs_details.png
Binary files differ
diff --git a/res/drawable/ic_menu_crop.png b/res/drawable/ic_menu_crop.png
new file mode 100644
index 0000000..e26fe9b
--- /dev/null
+++ b/res/drawable/ic_menu_crop.png
Binary files differ
diff --git a/res/drawable/ic_menu_mapmode.png b/res/drawable/ic_menu_mapmode.png
new file mode 100644
index 0000000..c1a136a
--- /dev/null
+++ b/res/drawable/ic_menu_mapmode.png
Binary files differ
diff --git a/res/drawable/ic_menu_rotate_left.png b/res/drawable/ic_menu_rotate_left.png
new file mode 100644
index 0000000..90cc657
--- /dev/null
+++ b/res/drawable/ic_menu_rotate_left.png
Binary files differ
diff --git a/res/drawable/ic_menu_rotate_right.png b/res/drawable/ic_menu_rotate_right.png
new file mode 100644
index 0000000..75f7f5d
--- /dev/null
+++ b/res/drawable/ic_menu_rotate_right.png
Binary files differ
diff --git a/res/drawable/ic_menu_set_as.png b/res/drawable/ic_menu_set_as.png
new file mode 100644
index 0000000..2c3d893
--- /dev/null
+++ b/res/drawable/ic_menu_set_as.png
Binary files differ
diff --git a/res/drawable/ic_menu_view_details.png b/res/drawable/ic_menu_view_details.png
new file mode 100644
index 0000000..aef2db0
--- /dev/null
+++ b/res/drawable/ic_menu_view_details.png
Binary files differ
diff --git a/res/drawable/icon.png b/res/drawable/icon.png
new file mode 100644
index 0000000..7ea0b8c
--- /dev/null
+++ b/res/drawable/icon.png
Binary files differ
diff --git a/res/drawable/icon_camera_mini.png b/res/drawable/icon_camera_mini.png
new file mode 100644
index 0000000..ab0905c
--- /dev/null
+++ b/res/drawable/icon_camera_mini.png
Binary files differ
diff --git a/res/drawable/icon_camera_small.png b/res/drawable/icon_camera_small.png
new file mode 100644
index 0000000..5132b81
--- /dev/null
+++ b/res/drawable/icon_camera_small.png
Binary files differ
diff --git a/res/drawable/icon_camera_small_unscaled.png b/res/drawable/icon_camera_small_unscaled.png
new file mode 100644
index 0000000..29436ff
--- /dev/null
+++ b/res/drawable/icon_camera_small_unscaled.png
Binary files differ
diff --git a/res/drawable/icon_delete.png b/res/drawable/icon_delete.png
new file mode 100644
index 0000000..90dea86
--- /dev/null
+++ b/res/drawable/icon_delete.png
Binary files differ
diff --git a/res/drawable/icon_edit.png b/res/drawable/icon_edit.png
new file mode 100644
index 0000000..67ddfe0
--- /dev/null
+++ b/res/drawable/icon_edit.png
Binary files differ
diff --git a/res/drawable/icon_folder_small.png b/res/drawable/icon_folder_small.png
new file mode 100644
index 0000000..18ac991
--- /dev/null
+++ b/res/drawable/icon_folder_small.png
Binary files differ
diff --git a/res/drawable/icon_folder_small_unscaled.png b/res/drawable/icon_folder_small_unscaled.png
new file mode 100644
index 0000000..9e18010
--- /dev/null
+++ b/res/drawable/icon_folder_small_unscaled.png
Binary files differ
diff --git a/res/drawable/icon_home_small.png b/res/drawable/icon_home_small.png
new file mode 100644
index 0000000..f8ff493
--- /dev/null
+++ b/res/drawable/icon_home_small.png
Binary files differ
diff --git a/res/drawable/icon_home_small_unscaled.png b/res/drawable/icon_home_small_unscaled.png
new file mode 100644
index 0000000..085028a
--- /dev/null
+++ b/res/drawable/icon_home_small_unscaled.png
Binary files differ
diff --git a/res/drawable/icon_location_small.png b/res/drawable/icon_location_small.png
new file mode 100644
index 0000000..6b29e49
--- /dev/null
+++ b/res/drawable/icon_location_small.png
Binary files differ
diff --git a/res/drawable/icon_more.png b/res/drawable/icon_more.png
new file mode 100644
index 0000000..44d5213
--- /dev/null
+++ b/res/drawable/icon_more.png
Binary files differ
diff --git a/res/drawable/icon_picasa_small.png b/res/drawable/icon_picasa_small.png
new file mode 100644
index 0000000..f35b386
--- /dev/null
+++ b/res/drawable/icon_picasa_small.png
Binary files differ
diff --git a/res/drawable/icon_picasa_small_unscaled.png b/res/drawable/icon_picasa_small_unscaled.png
new file mode 100644
index 0000000..166b803
--- /dev/null
+++ b/res/drawable/icon_picasa_small_unscaled.png
Binary files differ
diff --git a/res/drawable/icon_play.png b/res/drawable/icon_play.png
new file mode 100644
index 0000000..2ea9510
--- /dev/null
+++ b/res/drawable/icon_play.png
Binary files differ
diff --git a/res/drawable/icon_share.png b/res/drawable/icon_share.png
new file mode 100644
index 0000000..f6b286a
--- /dev/null
+++ b/res/drawable/icon_share.png
Binary files differ
diff --git a/res/drawable/indicator_autocrop.png b/res/drawable/indicator_autocrop.png
new file mode 100644
index 0000000..d960b1f
--- /dev/null
+++ b/res/drawable/indicator_autocrop.png
Binary files differ
diff --git a/res/drawable/logo_cooliris.png b/res/drawable/logo_cooliris.png
new file mode 100644
index 0000000..8742867
--- /dev/null
+++ b/res/drawable/logo_cooliris.png
Binary files differ
diff --git a/res/drawable/mode_grid.png b/res/drawable/mode_grid.png
new file mode 100644
index 0000000..be42f12
--- /dev/null
+++ b/res/drawable/mode_grid.png
Binary files differ
diff --git a/res/drawable/mode_stack.png b/res/drawable/mode_stack.png
new file mode 100644
index 0000000..362a89f
--- /dev/null
+++ b/res/drawable/mode_stack.png
Binary files differ
diff --git a/res/drawable/pathbar_bg.png b/res/drawable/pathbar_bg.png
new file mode 100644
index 0000000..357f1d7
--- /dev/null
+++ b/res/drawable/pathbar_bg.png
Binary files differ
diff --git a/res/drawable/pathbar_cap.png b/res/drawable/pathbar_cap.png
new file mode 100644
index 0000000..a2d5c27
--- /dev/null
+++ b/res/drawable/pathbar_cap.png
Binary files differ
diff --git a/res/drawable/pathbar_join.png b/res/drawable/pathbar_join.png
new file mode 100644
index 0000000..97c4200
--- /dev/null
+++ b/res/drawable/pathbar_join.png
Binary files differ
diff --git a/res/drawable/photo_inner.9.png b/res/drawable/photo_inner.9.png
new file mode 100755
index 0000000..f51eb35
--- /dev/null
+++ b/res/drawable/photo_inner.9.png
Binary files differ
diff --git a/res/drawable/popup.9.png b/res/drawable/popup.9.png
new file mode 100644
index 0000000..b9380ba
--- /dev/null
+++ b/res/drawable/popup.9.png
Binary files differ
diff --git a/res/drawable/popup_option_selected.9.png b/res/drawable/popup_option_selected.9.png
new file mode 100644
index 0000000..de41f40
--- /dev/null
+++ b/res/drawable/popup_option_selected.9.png
Binary files differ
diff --git a/res/drawable/popup_triangle_bottom.png b/res/drawable/popup_triangle_bottom.png
new file mode 100644
index 0000000..42dc82b
--- /dev/null
+++ b/res/drawable/popup_triangle_bottom.png
Binary files differ
diff --git a/res/drawable/scroller_new.png b/res/drawable/scroller_new.png
new file mode 100644
index 0000000..40f9e1a
--- /dev/null
+++ b/res/drawable/scroller_new.png
Binary files differ
diff --git a/res/drawable/scroller_pressed_new.png b/res/drawable/scroller_pressed_new.png
new file mode 100644
index 0000000..14cc0b2
--- /dev/null
+++ b/res/drawable/scroller_pressed_new.png
Binary files differ
diff --git a/res/drawable/selection_bg_upper.png b/res/drawable/selection_bg_upper.png
new file mode 100644
index 0000000..7f46d75
--- /dev/null
+++ b/res/drawable/selection_bg_upper.png
Binary files differ
diff --git a/res/drawable/selection_lower_bg.png b/res/drawable/selection_lower_bg.png
new file mode 100644
index 0000000..859d7eb
--- /dev/null
+++ b/res/drawable/selection_lower_bg.png
Binary files differ
diff --git a/res/drawable/selection_menu_bg.png b/res/drawable/selection_menu_bg.png
new file mode 100644
index 0000000..2dec3b6
--- /dev/null
+++ b/res/drawable/selection_menu_bg.png
Binary files differ
diff --git a/res/drawable/selection_menu_bg_pressed.png b/res/drawable/selection_menu_bg_pressed.png
new file mode 100644
index 0000000..a84458a
--- /dev/null
+++ b/res/drawable/selection_menu_bg_pressed.png
Binary files differ
diff --git a/res/drawable/selection_menu_bg_pressed_left.png b/res/drawable/selection_menu_bg_pressed_left.png
new file mode 100644
index 0000000..7565c3d
--- /dev/null
+++ b/res/drawable/selection_menu_bg_pressed_left.png
Binary files differ
diff --git a/res/drawable/selection_menu_bg_pressed_right.png b/res/drawable/selection_menu_bg_pressed_right.png
new file mode 100644
index 0000000..efe62f3
--- /dev/null
+++ b/res/drawable/selection_menu_bg_pressed_right.png
Binary files differ
diff --git a/res/drawable/selection_menu_divider.png b/res/drawable/selection_menu_divider.png
new file mode 100644
index 0000000..b2658e2
--- /dev/null
+++ b/res/drawable/selection_menu_divider.png
Binary files differ
diff --git a/res/drawable/stack_frame.png b/res/drawable/stack_frame.png
new file mode 100644
index 0000000..bde0540
--- /dev/null
+++ b/res/drawable/stack_frame.png
Binary files differ
diff --git a/res/drawable/stack_frame_focus.png b/res/drawable/stack_frame_focus.png
new file mode 100644
index 0000000..192527b
--- /dev/null
+++ b/res/drawable/stack_frame_focus.png
Binary files differ
diff --git a/res/drawable/stack_frame_gold.png b/res/drawable/stack_frame_gold.png
new file mode 100644
index 0000000..ffbede6
--- /dev/null
+++ b/res/drawable/stack_frame_gold.png
Binary files differ
diff --git a/res/drawable/stack_frame_pressed.png b/res/drawable/stack_frame_pressed.png
new file mode 100644
index 0000000..a631a3f
--- /dev/null
+++ b/res/drawable/stack_frame_pressed.png
Binary files differ
diff --git a/res/drawable/status_bar_shadow.png b/res/drawable/status_bar_shadow.png
new file mode 100644
index 0000000..d1d2694
--- /dev/null
+++ b/res/drawable/status_bar_shadow.png
Binary files differ
diff --git a/res/drawable/test_bg_jpg.png b/res/drawable/test_bg_jpg.png
new file mode 100644
index 0000000..0662b4e
--- /dev/null
+++ b/res/drawable/test_bg_jpg.png
Binary files differ
diff --git a/res/drawable/timebar_bg.png b/res/drawable/timebar_bg.png
new file mode 100644
index 0000000..fb2030b
--- /dev/null
+++ b/res/drawable/timebar_bg.png
Binary files differ
diff --git a/res/drawable/timebar_knob.png b/res/drawable/timebar_knob.png
new file mode 100644
index 0000000..b794116
--- /dev/null
+++ b/res/drawable/timebar_knob.png
Binary files differ
diff --git a/res/drawable/timebar_knob_old.png b/res/drawable/timebar_knob_old.png
new file mode 100644
index 0000000..19e6517
--- /dev/null
+++ b/res/drawable/timebar_knob_old.png
Binary files differ
diff --git a/res/drawable/timebar_knob_pressed.png b/res/drawable/timebar_knob_pressed.png
new file mode 100644
index 0000000..931b618
--- /dev/null
+++ b/res/drawable/timebar_knob_pressed.png
Binary files differ
diff --git a/res/drawable/timebar_next.png b/res/drawable/timebar_next.png
new file mode 100644
index 0000000..0d8fd77
--- /dev/null
+++ b/res/drawable/timebar_next.png
Binary files differ
diff --git a/res/drawable/timebar_prev.png b/res/drawable/timebar_prev.png
new file mode 100644
index 0000000..a9189f1
--- /dev/null
+++ b/res/drawable/timebar_prev.png
Binary files differ
diff --git a/res/drawable/transparent.png b/res/drawable/transparent.png
new file mode 100644
index 0000000..95007b9
--- /dev/null
+++ b/res/drawable/transparent.png
Binary files differ
diff --git a/res/drawable/videooverlay.png b/res/drawable/videooverlay.png
new file mode 100644
index 0000000..8b39eed
--- /dev/null
+++ b/res/drawable/videooverlay.png
Binary files differ
diff --git a/res/layout/cropimage.xml b/res/layout/cropimage.xml
new file mode 100644
index 0000000..8e8a488
--- /dev/null
+++ b/res/layout/cropimage.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent">
+
+    <RelativeLayout
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"
+            android:orientation="horizontal">
+        <view class="com.cooliris.media.CropImageView" android:id="@+id/image"
+                android:background="#55000000"
+                android:layout_width="fill_parent"
+                android:layout_height="fill_parent"
+                android:layout_x="0dip"
+                android:layout_y="0dip"
+        />
+        <RelativeLayout android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:paddingLeft="10dip"
+                android:paddingRight="10dip"
+                android:layout_alignParentBottom="true"
+                android:layout_centerHorizontal="true">
+            <Button
+                    android:id="@+id/save"
+                    android:layout_width="100dip"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentLeft="true"
+                    android:text="@string/crop_save_text"
+            />
+            <Button
+                    android:id="@+id/discard"
+                    android:layout_width="100dip"
+                    android:layout_height="wrap_content"
+                    android:layout_alignParentRight="true"
+                    android:text="@string/crop_discard_text"
+            />
+        </RelativeLayout>
+    </RelativeLayout>
+</FrameLayout>
diff --git a/res/layout/main.xml b/res/layout/main.xml
new file mode 100644
index 0000000..840fb4c
--- /dev/null
+++ b/res/layout/main.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent">
+</LinearLayout>
diff --git a/res/layout/movie_view.xml b/res/layout/movie_view.xml
new file mode 100644
index 0000000..9e3ba89
--- /dev/null
+++ b/res/layout/movie_view.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/root"
+    android:layout_width="fill_parent" 
+    android:layout_height="fill_parent">
+
+    <VideoView android:id="@+id/surface_view" 
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"
+            android:layout_centerInParent="true" />
+
+    <LinearLayout android:id="@+id/progress_indicator"
+            android:orientation="vertical"
+            android:layout_centerInParent="true"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content">
+
+        <ProgressBar android:id="@android:id/progress"
+                style="?android:attr/progressBarStyleLarge"
+                android:layout_gravity="center"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+
+        <TextView android:paddingTop="5dip"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:text="@string/loading_video" android:textSize="14sp"
+                android:textColor="#ffffffff" />
+    </LinearLayout>
+
+</RelativeLayout>
diff --git a/res/layout/photo_frame.xml b/res/layout/photo_frame.xml
new file mode 100755
index 0000000..59060ad
--- /dev/null
+++ b/res/layout/photo_frame.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:background="@drawable/appwidget_bg">
+
+    <FrameLayout
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:padding="3dip">
+       
+		<FrameLayout
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"
+            android:padding="1px"
+            android:foreground="@drawable/photo_inner">
+     
+		    <ImageView
+		        android:id="@+id/photo"
+		        android:layout_width="fill_parent"
+		        android:layout_height="fill_parent"
+		        android:scaleType="centerCrop"
+		        android:cropToPadding="true" />
+		        
+        </FrameLayout>		        
+	        
+    </FrameLayout>        
+
+</FrameLayout>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
new file mode 100644
index 0000000..ee8221c
--- /dev/null
+++ b/res/values-cs/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerie"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"leden"</item>
+    <item msgid="4680813504147866612">"únor"</item>
+    <item msgid="1934595560893153574">"březen"</item>
+    <item msgid="9020107212348500338">"duben"</item>
+    <item msgid="5734859077892484949">"květen"</item>
+    <item msgid="8546985030184126468">"červen"</item>
+    <item msgid="3639654472839533748">"červenec"</item>
+    <item msgid="8434242278630875235">"srpen"</item>
+    <item msgid="8493511009771049442">"září"</item>
+    <item msgid="685181459001441496">"říjen"</item>
+    <item msgid="6662413035764778250">"listopad"</item>
+    <item msgid="3784870986696899313">"prosinec"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Rámeček fotografie"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Filmy"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Načítání videa..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Pokračovat v přehrávání videa"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Pokračovat v přehrávání od %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Pokračovat v přehrávání"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Začít znovu"</string>
+    <string name="photos" msgid="3921471644099818826">"fotografie"</string>
+    <string name="videos" msgid="9168892901395116842">"videa"</string>
+    <string name="camera" msgid="2730811566218090802">"Fotoaparát"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Neznámé místo"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Uložit"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Zahodit"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Začněte klepnutím na obličej."</string>
+    <string name="saving_image" msgid="3051745378545909260">"Ukládání fotografie..."</string>
+    <string name="crop_label" msgid="521114301871349328">"Oříznout fotografii"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Čekejte prosím..."</string>
+    <string name="set_image" msgid="7246975856983303047">"Fotografie bude použita jako"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Nastavování tapety, čekejte prosím..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Fotografie"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapeta"</string>
+    <string name="details_ok" msgid="6848594369924424312">"OK"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Načítání nových alb a fotografií"</string>
+    <string name="initializing" msgid="2374228157398540466">"Načítání"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Vyberte položku ze své sbírky"</string>
+    <string name="no_items" msgid="3117870234034732172">"Váš výběr neobsahuje žádné položky"</string>
+    <string name="home" msgid="3335112006212947568">"Plocha"</string>
+    <string name="pick" msgid="7248789132035843128">"Vybrat"</string>
+    <string name="delete" msgid="2839695998251824487">"Smazat"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Potvrdit smazání"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Jste si jisti?"</string>
+    <string name="cancel" msgid="3637516880917356226">"Zrušit"</string>
+    <string name="share" msgid="3619042788254195341">"Sdílet"</string>
+    <string name="more" msgid="1526449516720792387">"Další"</string>
+    <string name="select_all" msgid="8623593677101437957">"Vybrat vše"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Zrušit výběr všech"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Prezentace"</string>
+    <string name="play" msgid="8116587039599675035">"Přehrát"</string>
+    <string name="menu" msgid="1819649153380636719">"Menu"</string>
+    <string name="details" msgid="8415120088556445230">"Podrobnosti"</string>
+    <string name="album_selected" msgid="3441280740465738452">"vybrané album"</string>
+    <string name="item_selected" msgid="6137767503817908841">"vybraná položka"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"vybraná alba"</string>
+    <string name="items_selected" msgid="8548754404879264861">"vybrané položky"</string>
+    <string name="album" msgid="5198388817734336004">"Album"</string>
+    <string name="start" msgid="4316892252528232165">"Spustit"</string>
+    <string name="end" msgid="3645506196012005500">"Ukončit"</string>
+    <string name="location" msgid="3432705876921618314">"Místo"</string>
+    <string name="title" msgid="7622928349908052569">"Název"</string>
+    <string name="type" msgid="4329478642546287192">"Typ"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Pořízeno dne"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Zobrazit na mapě"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Otočit doleva"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Otočit doprava"</string>
+    <string name="crop" msgid="7970750655414797277">"Oříznout"</string>
+    <string name="set_as" msgid="3636764710790507868">"Nastavit jako"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Nastavit jako tapetu"</string>
+    <string name="item" msgid="636303673288563698">"položka"</string>
+    <string name="items" msgid="6403254716052150916">"položky"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Nastavit jako ikonu kontaktu"</string>
+    <string name="miles_around" msgid="267745511737880822">"mil okolo"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Neznámé datum"</string>
+    <string name="video_err" msgid="7917736494827857757">"Video nelze přehrát"</string>
+</resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
new file mode 100644
index 0000000..d6616ed
--- /dev/null
+++ b/res/values-da/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galleri"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"Jan."</item>
+    <item msgid="4680813504147866612">"Feb."</item>
+    <item msgid="1934595560893153574">"Mar."</item>
+    <item msgid="9020107212348500338">"Apr."</item>
+    <item msgid="5734859077892484949">"Maj"</item>
+    <item msgid="8546985030184126468">"Jun."</item>
+    <item msgid="3639654472839533748">"Jul."</item>
+    <item msgid="8434242278630875235">"Aug."</item>
+    <item msgid="8493511009771049442">"Sep."</item>
+    <item msgid="685181459001441496">"Okt."</item>
+    <item msgid="6662413035764778250">"Nov."</item>
+    <item msgid="3784870986696899313">"Dec."</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Billedramme"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Film"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Indlæser video ..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Genoptag video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Genoptag afspilning fra %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Genoptag afspilning"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Start igen"</string>
+    <string name="photos" msgid="3921471644099818826">"fotos"</string>
+    <string name="videos" msgid="9168892901395116842">"videoer"</string>
+    <string name="camera" msgid="2730811566218090802">"Kamera"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Ukendt placering"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Gem"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Kassér"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Tryk på et ansigt for at begynde."</string>
+    <string name="saving_image" msgid="3051745378545909260">"Gemmer billede ..."</string>
+    <string name="crop_label" msgid="521114301871349328">"Beskær billede"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Vent ..."</string>
+    <string name="set_image" msgid="7246975856983303047">"Indstil billedet som"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Indstiller tapet. Vent et øjeblik ..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Billeder"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapet"</string>
+    <string name="details_ok" msgid="6848594369924424312">"OK"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Indlæser nye albummer og fotos"</string>
+    <string name="initializing" msgid="2374228157398540466">"Indlæser"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Vælg et element fra din samling"</string>
+    <string name="no_items" msgid="3117870234034732172">"Der er ingen emner i din samling"</string>
+    <string name="home" msgid="3335112006212947568">"Start"</string>
+    <string name="pick" msgid="7248789132035843128">"Vælg"</string>
+    <string name="delete" msgid="2839695998251824487">"Slet"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Bekræft sletning"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Er du sikker?"</string>
+    <string name="cancel" msgid="3637516880917356226">"Annuller"</string>
+    <string name="share" msgid="3619042788254195341">"Del"</string>
+    <string name="more" msgid="1526449516720792387">"Flere"</string>
+    <string name="select_all" msgid="8623593677101437957">"Vælg alle"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Fravælg alle"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Diasshow"</string>
+    <string name="play" msgid="8116587039599675035">"Afspil"</string>
+    <string name="menu" msgid="1819649153380636719">"Menu"</string>
+    <string name="details" msgid="8415120088556445230">"Detaljer"</string>
+    <string name="album_selected" msgid="3441280740465738452">"valgt album"</string>
+    <string name="item_selected" msgid="6137767503817908841">"valgt element"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"valgte albummer"</string>
+    <string name="items_selected" msgid="8548754404879264861">"valgte elementer"</string>
+    <string name="album" msgid="5198388817734336004">"Album"</string>
+    <string name="start" msgid="4316892252528232165">"Start"</string>
+    <string name="end" msgid="3645506196012005500">"Afslut"</string>
+    <string name="location" msgid="3432705876921618314">"Placering"</string>
+    <string name="title" msgid="7622928349908052569">"Titel"</string>
+    <string name="type" msgid="4329478642546287192">"Type"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Optaget den"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Vis på kort"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Roter til venstre"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Roter til højre"</string>
+    <string name="crop" msgid="7970750655414797277">"Beskær"</string>
+    <string name="set_as" msgid="3636764710790507868">"Indstil som"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Indstil som baggrund"</string>
+    <string name="item" msgid="636303673288563698">"element"</string>
+    <string name="items" msgid="6403254716052150916">"elementer"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Indstil som kontaktikon"</string>
+    <string name="miles_around" msgid="267745511737880822">"mile omkring"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Dato ukendt"</string>
+    <string name="video_err" msgid="7917736494827857757">"Kan ikke afspille video"</string>
+</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
new file mode 100644
index 0000000..767b976
--- /dev/null
+++ b/res/values-de/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerie"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"Jan."</item>
+    <item msgid="4680813504147866612">"Feb."</item>
+    <item msgid="1934595560893153574">"März"</item>
+    <item msgid="9020107212348500338">"Apr."</item>
+    <item msgid="5734859077892484949">"Mai"</item>
+    <item msgid="8546985030184126468">"Juni"</item>
+    <item msgid="3639654472839533748">"Juli"</item>
+    <item msgid="8434242278630875235">"Aug."</item>
+    <item msgid="8493511009771049442">"Sep."</item>
+    <item msgid="685181459001441496">"Okt."</item>
+    <item msgid="6662413035764778250">"Nov."</item>
+    <item msgid="3784870986696899313">"Dez."</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Bildrahmen"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Filme"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Video wird geladen..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Mit Video fortfahren"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Mit Wiedergabe fortfahren ab %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Mit Wiedergabe fortfahren"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Starten"</string>
+    <string name="photos" msgid="3921471644099818826">"Fotos"</string>
+    <string name="videos" msgid="9168892901395116842">"Videos"</string>
+    <string name="camera" msgid="2730811566218090802">"Kamera"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Unbekannter Standort"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Speichern"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Verwerfen"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Tippen Sie zum Beginnen auf ein Gesicht."</string>
+    <string name="saving_image" msgid="3051745378545909260">"Bild wird gespeichert..."</string>
+    <string name="crop_label" msgid="521114301871349328">"Bild zuschneiden"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Bitte warten..."</string>
+    <string name="set_image" msgid="7246975856983303047">"Bild festlegen als"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Hintergrund wird eingestellt, bitte warten..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Bilder"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Hintergrund"</string>
+    <string name="details_ok" msgid="6848594369924424312">"OK"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Neue Alben und Fotos werden geladen"</string>
+    <string name="initializing" msgid="2374228157398540466">"Wird geladen"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Ein Element aus Ihrer Sammlung auswählen"</string>
+    <string name="no_items" msgid="3117870234034732172">"In Ihrer Sammlung befinden sich keine Elemente."</string>
+    <string name="home" msgid="3335112006212947568">"Startseite"</string>
+    <string name="pick" msgid="7248789132035843128">"Auswählen"</string>
+    <string name="delete" msgid="2839695998251824487">"Löschen"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Löschen bestätigen"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Sind Sie sicher?"</string>
+    <string name="cancel" msgid="3637516880917356226">"Abbrechen"</string>
+    <string name="share" msgid="3619042788254195341">"Freigeben"</string>
+    <string name="more" msgid="1526449516720792387">"Mehr"</string>
+    <string name="select_all" msgid="8623593677101437957">"Alles auswählen"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Auswahl für alle aufheben"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Diashow"</string>
+    <string name="play" msgid="8116587039599675035">"Wiedergeben"</string>
+    <string name="menu" msgid="1819649153380636719">"Menü"</string>
+    <string name="details" msgid="8415120088556445230">"Details"</string>
+    <string name="album_selected" msgid="3441280740465738452">"Album ausgewählt"</string>
+    <string name="item_selected" msgid="6137767503817908841">"Element ausgewählt"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"Alben ausgewählt"</string>
+    <string name="items_selected" msgid="8548754404879264861">"Elemente ausgewählt"</string>
+    <string name="album" msgid="5198388817734336004">"Album"</string>
+    <string name="start" msgid="4316892252528232165">"Starten"</string>
+    <string name="end" msgid="3645506196012005500">"Ende"</string>
+    <string name="location" msgid="3432705876921618314">"Ort"</string>
+    <string name="title" msgid="7622928349908052569">"Titel"</string>
+    <string name="type" msgid="4329478642546287192">"Typ"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Aufgenommen am"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Auf Karte anzeigen"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Nach links drehen"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Nach rechts drehen"</string>
+    <string name="crop" msgid="7970750655414797277">"Zuschneiden"</string>
+    <string name="set_as" msgid="3636764710790507868">"Festlegen als"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Als Hintergrundbild festlegen"</string>
+    <string name="item" msgid="636303673288563698">"Element"</string>
+    <string name="items" msgid="6403254716052150916">"Elemente"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Als Kontaktsymbol festlegen"</string>
+    <string name="miles_around" msgid="267745511737880822">"Meilen um"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Datum unbekannt"</string>
+    <string name="video_err" msgid="7917736494827857757">"Video kann nicht wiedergegeben werden."</string>
+</resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
new file mode 100644
index 0000000..f3a1866
--- /dev/null
+++ b/res/values-el/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Συλλογή"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"Ιαν"</item>
+    <item msgid="4680813504147866612">"Φεβ"</item>
+    <item msgid="1934595560893153574">"Μαρ"</item>
+    <item msgid="9020107212348500338">"Απρ"</item>
+    <item msgid="5734859077892484949">"Μάι"</item>
+    <item msgid="8546985030184126468">"Ιουν"</item>
+    <item msgid="3639654472839533748">"Ιουλ"</item>
+    <item msgid="8434242278630875235">"Αυγ"</item>
+    <item msgid="8493511009771049442">"Σεπ"</item>
+    <item msgid="685181459001441496">"Οκτ"</item>
+    <item msgid="6662413035764778250">"Νοε"</item>
+    <item msgid="3784870986696899313">"Δεκ"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Πλαίσιο εικόνας"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Ταινίες"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Φόρτωση βίντεο..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Συνέχιση βίντεο"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Συνέχιση αναπαραγωγής από το %s;"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Συνέχιση αναπαραγωγής"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Έναρξη από την αρχή"</string>
+    <string name="photos" msgid="3921471644099818826">"φωτογραφίες"</string>
+    <string name="videos" msgid="9168892901395116842">"βίντεο"</string>
+    <string name="camera" msgid="2730811566218090802">"Φωτογραφική μηχανή"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Άγνωστη τοποθεσία"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Αποθήκευση"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Απόρριψη"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Πατήστε σε ένα πρόσωπο για να ξεκινήσετε."</string>
+    <string name="saving_image" msgid="3051745378545909260">"Αποθήκευση εικόνας..."</string>
+    <string name="crop_label" msgid="521114301871349328">"Περικοπή εικόνας"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Περιμένετε..."</string>
+    <string name="set_image" msgid="7246975856983303047">"Ορισμός εικόνας ως"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Ρύθμιση ταπετσαρίας, περιμένετε..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Εικόνες"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Ταπετσαρία"</string>
+    <string name="details_ok" msgid="6848594369924424312">"OK"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Φόρτωση νέων λευκωμάτων και φωτογραφιών"</string>
+    <string name="initializing" msgid="2374228157398540466">"Φόρτωση"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Επιλέξτε ένα αντικείμενο από τη συλλογή σας"</string>
+    <string name="no_items" msgid="3117870234034732172">"Δεν υπάρχουν στοιχεία στη συλλογή σας"</string>
+    <string name="home" msgid="3335112006212947568">"Αρχική σελίδα"</string>
+    <string name="pick" msgid="7248789132035843128">"Επιλογή"</string>
+    <string name="delete" msgid="2839695998251824487">"Διαγραφή"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Επιβεβαίωση διαγραφής"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Είστε σίγουροι;"</string>
+    <string name="cancel" msgid="3637516880917356226">"Ακύρωση"</string>
+    <string name="share" msgid="3619042788254195341">"Κοινή χρήση"</string>
+    <string name="more" msgid="1526449516720792387">"Περισσότερα"</string>
+    <string name="select_all" msgid="8623593677101437957">"Επιλογή όλων"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Κατάργηση επιλογής όλων"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Προβολή διαφανειών"</string>
+    <string name="play" msgid="8116587039599675035">"Αναπαραγωγή"</string>
+    <string name="menu" msgid="1819649153380636719">"Μενού"</string>
+    <string name="details" msgid="8415120088556445230">"Λεπτομέρειες"</string>
+    <string name="album_selected" msgid="3441280740465738452">"λεύκωμα επιλέχθηκε"</string>
+    <string name="item_selected" msgid="6137767503817908841">"αντικείμενο επιλέχθηκε"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"λευκώματα επιλέχθηκαν"</string>
+    <string name="items_selected" msgid="8548754404879264861">"αντικείμενα επιλέχθηκαν"</string>
+    <string name="album" msgid="5198388817734336004">"Λεύκωμα"</string>
+    <string name="start" msgid="4316892252528232165">"Εκκίνηση"</string>
+    <string name="end" msgid="3645506196012005500">"Τέλος"</string>
+    <string name="location" msgid="3432705876921618314">"Τοποθεσία"</string>
+    <string name="title" msgid="7622928349908052569">"Τίτλος"</string>
+    <string name="type" msgid="4329478642546287192">"Τύπος"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Ελήφθη την"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Εμφάνιση στον χάρτη"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Αριστερή περιστροφή"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Δεξιά περιστροφή"</string>
+    <string name="crop" msgid="7970750655414797277">"Περικοπή"</string>
+    <string name="set_as" msgid="3636764710790507868">"Ορισμός ως"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Ορισμός ως ταπετσαρία"</string>
+    <string name="item" msgid="636303673288563698">"αντικείμενο"</string>
+    <string name="items" msgid="6403254716052150916">"αντικείμενα"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Ορισμός ως εικονιδίου επαφής"</string>
+    <string name="miles_around" msgid="267745511737880822">"μίλια"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Άγνωστη ημερομηνία"</string>
+    <string name="video_err" msgid="7917736494827857757">"Δεν είναι δυνατή η αναπαραγωγή του βίντεο"</string>
+</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..8673a60
--- /dev/null
+++ b/res/values-es-rUS/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galería"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"Ene."</item>
+    <item msgid="4680813504147866612">"Feb."</item>
+    <item msgid="1934595560893153574">"Mar."</item>
+    <item msgid="9020107212348500338">"Abr."</item>
+    <item msgid="5734859077892484949">"Mayo"</item>
+    <item msgid="8546985030184126468">"Jun."</item>
+    <item msgid="3639654472839533748">"Jul."</item>
+    <item msgid="8434242278630875235">"Ago."</item>
+    <item msgid="8493511009771049442">"Sep."</item>
+    <item msgid="685181459001441496">"Oct."</item>
+    <item msgid="6662413035764778250">"Nov."</item>
+    <item msgid="3784870986696899313">"Dic."</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Marco de imagen"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Películas"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Cargando el video..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Retomar video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"¿Deseas retomar la reproducción desde %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Retomar la reproducción"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Empezar de nuevo"</string>
+    <string name="photos" msgid="3921471644099818826">"fotos"</string>
+    <string name="videos" msgid="9168892901395116842">"videos"</string>
+    <string name="camera" msgid="2730811566218090802">"Cámara"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Ubicación desconocida"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Guardar"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Eliminar"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Golpea una cara para comenzar."</string>
+    <string name="saving_image" msgid="3051745378545909260">"Guardando imagen..."</string>
+    <string name="crop_label" msgid="521114301871349328">"Cortar la imagen"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Espera, por favor..."</string>
+    <string name="set_image" msgid="7246975856983303047">"Definir imagen como"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Configurando papel tapiz. Espera, por favor..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Imágenes"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Papel tapiz"</string>
+    <string name="details_ok" msgid="6848594369924424312">"Aceptar"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Cargando nuevos álbumes y fotos"</string>
+    <string name="initializing" msgid="2374228157398540466">"Cargando"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Seleccionar un elemento de tu colección"</string>
+    <string name="no_items" msgid="3117870234034732172">"No hay artículos en su colección."</string>
+    <string name="home" msgid="3335112006212947568">"Página principal"</string>
+    <string name="pick" msgid="7248789132035843128">"Seleccionar"</string>
+    <string name="delete" msgid="2839695998251824487">"Eliminar"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Confirmar supresión"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Listas de reproducción"</string>
+    <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
+    <string name="share" msgid="3619042788254195341">"Compartir"</string>
+    <string name="more" msgid="1526449516720792387">"Más"</string>
+    <string name="select_all" msgid="8623593677101437957">"Seleccionar todo"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Desmarcar todos"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Presentación de diapositivas"</string>
+    <string name="play" msgid="8116587039599675035">"Reproducir"</string>
+    <string name="menu" msgid="1819649153380636719">"Menú"</string>
+    <string name="details" msgid="8415120088556445230">"Detalles"</string>
+    <string name="album_selected" msgid="3441280740465738452">"álbum seleccionado"</string>
+    <string name="item_selected" msgid="6137767503817908841">"elemento seleccionado"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"álbumes seleccionados"</string>
+    <string name="items_selected" msgid="8548754404879264861">"elementos seleccionados"</string>
+    <string name="album" msgid="5198388817734336004">"Álbum"</string>
+    <string name="start" msgid="4316892252528232165">"Inicio"</string>
+    <string name="end" msgid="3645506196012005500">"Finalizar"</string>
+    <string name="location" msgid="3432705876921618314">"Ubicación"</string>
+    <string name="title" msgid="7622928349908052569">"Título"</string>
+    <string name="type" msgid="4329478642546287192">"Tipo"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Aceptar"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Mostrar mapa"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Rotar hacia la izquierda"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Rotar hacia la derecha"</string>
+    <string name="crop" msgid="7970750655414797277">"Cortar"</string>
+    <string name="set_as" msgid="3636764710790507868">"Definir como"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Establecer como fondo de pantalla"</string>
+    <string name="item" msgid="636303673288563698">"elemento"</string>
+    <string name="items" msgid="6403254716052150916">"elementos"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Definir como ícono de contacto"</string>
+    <string name="miles_around" msgid="267745511737880822">"millas alrededor"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Fecha desconocida"</string>
+    <string name="video_err" msgid="7917736494827857757">"No se puede reproducir el video."</string>
+</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
new file mode 100644
index 0000000..0e94101
--- /dev/null
+++ b/res/values-es/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galería"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"Ene."</item>
+    <item msgid="4680813504147866612">"Feb."</item>
+    <item msgid="1934595560893153574">"Mar."</item>
+    <item msgid="9020107212348500338">"Abr."</item>
+    <item msgid="5734859077892484949">"May."</item>
+    <item msgid="8546985030184126468">"Jun."</item>
+    <item msgid="3639654472839533748">"Jul."</item>
+    <item msgid="8434242278630875235">"Ago."</item>
+    <item msgid="8493511009771049442">"Set."</item>
+    <item msgid="685181459001441496">"Oct."</item>
+    <item msgid="6662413035764778250">"Nov."</item>
+    <item msgid="3784870986696899313">"Dic."</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Picture frame"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Películas"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Cargando vídeo…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Reanudar vídeo"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Reanudar reproducción a partir de %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Reanudar reproducción"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Volver a reproducir"</string>
+    <string name="photos" msgid="3921471644099818826">"fotos"</string>
+    <string name="videos" msgid="9168892901395116842">"vídeos"</string>
+    <string name="camera" msgid="2730811566218090802">"Cámara"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Ubicación desconocida"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Guardar"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Descartar"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Toca una cara para empezar."</string>
+    <string name="saving_image" msgid="3051745378545909260">"Guardando imagen..."</string>
+    <string name="crop_label" msgid="521114301871349328">"Recortar imagen"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Por favor, espera..."</string>
+    <string name="set_image" msgid="7246975856983303047">"Establecer imagen como"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Estableciendo fondo de pantalla..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Imágenes"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fondo de pantalla"</string>
+    <string name="details_ok" msgid="6848594369924424312">"Aceptar"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Cargando nuevos álbumes y fotos"</string>
+    <string name="initializing" msgid="2374228157398540466">"Cargando"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Selecciona un elemento de tu colección."</string>
+    <string name="no_items" msgid="3117870234034732172">"No hay ningún elemento en tu colección."</string>
+    <string name="home" msgid="3335112006212947568">"Página principal"</string>
+    <string name="pick" msgid="7248789132035843128">"Seleccionar"</string>
+    <string name="delete" msgid="2839695998251824487">"Eliminar"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Confirmar eliminación"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"¿Estás seguro?"</string>
+    <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
+    <string name="share" msgid="3619042788254195341">"Compartir"</string>
+    <string name="more" msgid="1526449516720792387">"Más"</string>
+    <string name="select_all" msgid="8623593677101437957">"Seleccionar todo"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Desmarcar todo"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Presentación"</string>
+    <string name="play" msgid="8116587039599675035">"Reproducir"</string>
+    <string name="menu" msgid="1819649153380636719">"Menú"</string>
+    <string name="details" msgid="8415120088556445230">"Detalles"</string>
+    <string name="album_selected" msgid="3441280740465738452">"álbum seleccionado"</string>
+    <string name="item_selected" msgid="6137767503817908841">"elemento seleccionado"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"álbumes seleccionados"</string>
+    <string name="items_selected" msgid="8548754404879264861">"elementos seleccionados"</string>
+    <string name="album" msgid="5198388817734336004">"Álbum"</string>
+    <string name="start" msgid="4316892252528232165">"Iniciar"</string>
+    <string name="end" msgid="3645506196012005500">"Finalizar"</string>
+    <string name="location" msgid="3432705876921618314">"Ubicación"</string>
+    <string name="title" msgid="7622928349908052569">"Título"</string>
+    <string name="type" msgid="4329478642546287192">"Tipo"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Realizado/a el"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Mostrar en el mapa"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Girar a la izquierda"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Girar a la derecha"</string>
+    <string name="crop" msgid="7970750655414797277">"Recortar"</string>
+    <string name="set_as" msgid="3636764710790507868">"Establecer como"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Establecer como fondo de pantalla"</string>
+    <string name="item" msgid="636303673288563698">"elemento"</string>
+    <string name="items" msgid="6403254716052150916">"elementos"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Establecer como icono de contacto"</string>
+    <string name="miles_around" msgid="267745511737880822">"millas por"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Fecha desconocida"</string>
+    <string name="video_err" msgid="7917736494827857757">"No se puede reproducir ningún vídeo."</string>
+</resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
new file mode 100644
index 0000000..72f5f6f
--- /dev/null
+++ b/res/values-fr/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerie"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"Janv."</item>
+    <item msgid="4680813504147866612">"Févr."</item>
+    <item msgid="1934595560893153574">"Mars"</item>
+    <item msgid="9020107212348500338">"Avr."</item>
+    <item msgid="5734859077892484949">"Mai"</item>
+    <item msgid="8546985030184126468">"Juin"</item>
+    <item msgid="3639654472839533748">"Juil."</item>
+    <item msgid="8434242278630875235">"Août"</item>
+    <item msgid="8493511009771049442">"Sept."</item>
+    <item msgid="685181459001441496">"Oct."</item>
+    <item msgid="6662413035764778250">"Nov."</item>
+    <item msgid="3784870986696899313">"Déc."</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Cadre d\'image"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Films"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Chargement de la vidéo..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Reprendre la vidéo"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Reprendre la lecture à partir de %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Reprendre la lecture"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Démarrer"</string>
+    <string name="photos" msgid="3921471644099818826">"photos"</string>
+    <string name="videos" msgid="9168892901395116842">"vidéos"</string>
+    <string name="camera" msgid="2730811566218090802">"Appareil photo"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Lieu inconnu"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Enregistrer"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Annuler"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Appuyez sur un visage pour commencer."</string>
+    <string name="saving_image" msgid="3051745378545909260">"Enregistrement de l\'image"</string>
+    <string name="crop_label" msgid="521114301871349328">"Rogner l\'image"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Veuillez patienter..."</string>
+    <string name="set_image" msgid="7246975856983303047">"Définir l\'image comme"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Configuration du fond d\'écran en cours. Veuillez patienter..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Images"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Fond d\'écran"</string>
+    <string name="details_ok" msgid="6848594369924424312">"OK"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Chargement de photos et d\'albums nouveaux"</string>
+    <string name="initializing" msgid="2374228157398540466">"Chargement"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Choisissez un élément dans votre collection."</string>
+    <string name="no_items" msgid="3117870234034732172">"Votre collection ne contient aucun élément"</string>
+    <string name="home" msgid="3335112006212947568">"Domicile"</string>
+    <string name="pick" msgid="7248789132035843128">"Choisir"</string>
+    <string name="delete" msgid="2839695998251824487">"Supprimer"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Confirmer la suppression"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Souhaitez-vous confirmer ?"</string>
+    <string name="cancel" msgid="3637516880917356226">"Annuler"</string>
+    <string name="share" msgid="3619042788254195341">"Partager"</string>
+    <string name="more" msgid="1526449516720792387">"Plus"</string>
+    <string name="select_all" msgid="8623593677101437957">"Tout sélectionner"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Tout désélectionner"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Diaporama"</string>
+    <string name="play" msgid="8116587039599675035">"Lire"</string>
+    <string name="menu" msgid="1819649153380636719">"Menu"</string>
+    <string name="details" msgid="8415120088556445230">"Détails"</string>
+    <string name="album_selected" msgid="3441280740465738452">"album sélectionné"</string>
+    <string name="item_selected" msgid="6137767503817908841">"élément sélectionné"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"albums sélectionnés"</string>
+    <string name="items_selected" msgid="8548754404879264861">"éléments sélectionnés"</string>
+    <string name="album" msgid="5198388817734336004">"Album"</string>
+    <string name="start" msgid="4316892252528232165">"Démarrer"</string>
+    <string name="end" msgid="3645506196012005500">"Fin"</string>
+    <string name="location" msgid="3432705876921618314">"Lieu"</string>
+    <string name="title" msgid="7622928349908052569">"Titre"</string>
+    <string name="type" msgid="4329478642546287192">"Type"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Prise le"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Afficher sur la carte"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Rotation à gauche"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Rotation à droite"</string>
+    <string name="crop" msgid="7970750655414797277">"Rogner"</string>
+    <string name="set_as" msgid="3636764710790507868">"Définir comme"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Définir comme fond d\'écran"</string>
+    <string name="item" msgid="636303673288563698">"élément"</string>
+    <string name="items" msgid="6403254716052150916">"éléments"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Définir comme icône de contact"</string>
+    <string name="miles_around" msgid="267745511737880822">"km autour de"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Date inconnue"</string>
+    <string name="video_err" msgid="7917736494827857757">"Impossible de lire la vidéo."</string>
+</resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
new file mode 100644
index 0000000..9062ce5
--- /dev/null
+++ b/res/values-it/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galleria"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"Gen"</item>
+    <item msgid="4680813504147866612">"Feb"</item>
+    <item msgid="1934595560893153574">"Mar"</item>
+    <item msgid="9020107212348500338">"Apr"</item>
+    <item msgid="5734859077892484949">"Mag"</item>
+    <item msgid="8546985030184126468">"Giu"</item>
+    <item msgid="3639654472839533748">"Lug"</item>
+    <item msgid="8434242278630875235">"Ago"</item>
+    <item msgid="8493511009771049442">"Set"</item>
+    <item msgid="685181459001441496">"Ott"</item>
+    <item msgid="6662413035764778250">"Nov"</item>
+    <item msgid="3784870986696899313">"Dic"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Cornice immagine"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Film"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Caricamento video..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Riprendi video"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Riprendi riproduzione da %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Riprendi riproduzione"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Ricomincia"</string>
+    <string name="photos" msgid="3921471644099818826">"foto"</string>
+    <string name="videos" msgid="9168892901395116842">"video"</string>
+    <string name="camera" msgid="2730811566218090802">"Fotocamera"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Luogo sconosciuto"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Salva"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Annulla"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Tocca un viso per iniziare."</string>
+    <string name="saving_image" msgid="3051745378545909260">"Salvataggio foto..."</string>
+    <string name="crop_label" msgid="521114301871349328">"Ritaglia foto"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Attendere..."</string>
+    <string name="set_image" msgid="7246975856983303047">"Imposta foto come"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Impostazione sfondo, attendi..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Foto"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Sfondo"</string>
+    <string name="details_ok" msgid="6848594369924424312">"OK"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Caricamento di nuovi album e foto in corso"</string>
+    <string name="initializing" msgid="2374228157398540466">"Caricamento in corso"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Scegli un elemento dalla raccolta"</string>
+    <string name="no_items" msgid="3117870234034732172">"Non sono presenti elementi nella tua raccolta"</string>
+    <string name="home" msgid="3335112006212947568">"Home"</string>
+    <string name="pick" msgid="7248789132035843128">"Scegli"</string>
+    <string name="delete" msgid="2839695998251824487">"Elimina"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Conferma eliminazione"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Procedere?"</string>
+    <string name="cancel" msgid="3637516880917356226">"Annulla"</string>
+    <string name="share" msgid="3619042788254195341">"Condividi"</string>
+    <string name="more" msgid="1526449516720792387">"Altro"</string>
+    <string name="select_all" msgid="8623593677101437957">"Seleziona tutto"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Deseleziona tutto"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Presentazione"</string>
+    <string name="play" msgid="8116587039599675035">"Riproduci"</string>
+    <string name="menu" msgid="1819649153380636719">"Menu"</string>
+    <string name="details" msgid="8415120088556445230">"Dettagli"</string>
+    <string name="album_selected" msgid="3441280740465738452">"album selezionato"</string>
+    <string name="item_selected" msgid="6137767503817908841">"elemento selezionato"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"album selezionati"</string>
+    <string name="items_selected" msgid="8548754404879264861">"elementi selezionati"</string>
+    <string name="album" msgid="5198388817734336004">"Album"</string>
+    <string name="start" msgid="4316892252528232165">"Inizia"</string>
+    <string name="end" msgid="3645506196012005500">"Fine"</string>
+    <string name="location" msgid="3432705876921618314">"Luogo"</string>
+    <string name="title" msgid="7622928349908052569">"Titolo"</string>
+    <string name="type" msgid="4329478642546287192">"Tipo"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Foto scattata/Video girato in data"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Mostra sulla mappa"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Ruota a sinistra"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Ruota a destra"</string>
+    <string name="crop" msgid="7970750655414797277">"Ritaglia"</string>
+    <string name="set_as" msgid="3636764710790507868">"Imposta come"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Imposta come sfondo"</string>
+    <string name="item" msgid="636303673288563698">"elemento"</string>
+    <string name="items" msgid="6403254716052150916">"elementi"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Icona Imposta come contatto"</string>
+    <string name="miles_around" msgid="267745511737880822">"chilometri da"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Data sconosciuta"</string>
+    <string name="video_err" msgid="7917736494827857757">"Impossibile riprodurre il video"</string>
+</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
new file mode 100644
index 0000000..4378b80
--- /dev/null
+++ b/res/values-ja/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"ギャラリー"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"1月"</item>
+    <item msgid="4680813504147866612">"2月"</item>
+    <item msgid="1934595560893153574">"3月"</item>
+    <item msgid="9020107212348500338">"4月"</item>
+    <item msgid="5734859077892484949">"5月"</item>
+    <item msgid="8546985030184126468">"6月"</item>
+    <item msgid="3639654472839533748">"7月"</item>
+    <item msgid="8434242278630875235">"8月"</item>
+    <item msgid="8493511009771049442">"9月"</item>
+    <item msgid="685181459001441496">"10月"</item>
+    <item msgid="6662413035764778250">"11月"</item>
+    <item msgid="3784870986696899313">"12月"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"写真フレーム"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"映画"</string>
+    <string name="loading_video" msgid="4013492720121891585">"動画を読み込み中..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"動画の再開"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"再生を%sから再開しますか?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"再生を再開"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"最初から再生"</string>
+    <string name="photos" msgid="3921471644099818826">"写真"</string>
+    <string name="videos" msgid="9168892901395116842">"動画"</string>
+    <string name="camera" msgid="2730811566218090802">"カメラ"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"不明な場所"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"保存"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"キャンセル"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"顔をタップして開始します。"</string>
+    <string name="saving_image" msgid="3051745378545909260">"写真を保存中..."</string>
+    <string name="crop_label" msgid="521114301871349328">"トリミング"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"お待ちください..."</string>
+    <string name="set_image" msgid="7246975856983303047">"登録"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"壁紙を設定しています。しばらくお待ちください..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"カメラで撮影した画像"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"壁紙"</string>
+    <string name="details_ok" msgid="6848594369924424312">"OK"</string>
+    <string name="loading_new" msgid="7893793767294787579">"新しいアルバムと写真を読み込み中"</string>
+    <string name="initializing" msgid="2374228157398540466">"読み込み中"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"コレクションからアイテムをしてください"</string>
+    <string name="no_items" msgid="3117870234034732172">"コレクションにアイテムが含まれていません"</string>
+    <string name="home" msgid="3335112006212947568">"自宅"</string>
+    <string name="pick" msgid="7248789132035843128">"選択"</string>
+    <string name="delete" msgid="2839695998251824487">"削除"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"削除の確認"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"よろしいですか?"</string>
+    <string name="cancel" msgid="3637516880917356226">"キャンセル"</string>
+    <string name="share" msgid="3619042788254195341">"共有"</string>
+    <string name="more" msgid="1526449516720792387">"その他"</string>
+    <string name="select_all" msgid="8623593677101437957">"すべて選択"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"選択をすべて解除"</string>
+    <string name="slideshow" msgid="4355906903247112975">"スライドショー"</string>
+    <string name="play" msgid="8116587039599675035">"再生"</string>
+    <string name="menu" msgid="1819649153380636719">"メニュー"</string>
+    <string name="details" msgid="8415120088556445230">"詳細情報"</string>
+    <string name="album_selected" msgid="3441280740465738452">"冊のアルバムを選択済み"</string>
+    <string name="item_selected" msgid="6137767503817908841">"件のアイテムを選択済み"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"冊のアルバムを選択済み"</string>
+    <string name="items_selected" msgid="8548754404879264861">"件のアイテムを選択済み"</string>
+    <string name="album" msgid="5198388817734336004">"アルバム"</string>
+    <string name="start" msgid="4316892252528232165">"開始"</string>
+    <string name="end" msgid="3645506196012005500">"終了"</string>
+    <string name="location" msgid="3432705876921618314">"場所"</string>
+    <string name="title" msgid="7622928349908052569">"タイトル"</string>
+    <string name="type" msgid="4329478642546287192">"種類"</string>
+    <string name="taken_on" msgid="3269486910757192479">"撮影"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"地図に表示"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"左に回転"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"右に回転"</string>
+    <string name="crop" msgid="7970750655414797277">"トリミング"</string>
+    <string name="set_as" msgid="3636764710790507868">"登録"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"壁紙として設定"</string>
+    <string name="item" msgid="636303673288563698">"アイテム"</string>
+    <string name="items" msgid="6403254716052150916">"アイテム"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"連絡先のアイコンとして設定"</string>
+    <string name="miles_around" msgid="267745511737880822">"マイル範囲"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"日付が不明"</string>
+    <string name="video_err" msgid="7917736494827857757">"動画を再生できません"</string>
+</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
new file mode 100644
index 0000000..1205f3a
--- /dev/null
+++ b/res/values-ko/strings.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"갤러리"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"1월"</item>
+    <item msgid="4680813504147866612">"2월"</item>
+    <item msgid="1934595560893153574">"3월"</item>
+    <item msgid="9020107212348500338">"4월"</item>
+    <item msgid="5734859077892484949">"5월"</item>
+    <item msgid="8546985030184126468">"6월"</item>
+    <item msgid="3639654472839533748">"7월"</item>
+    <item msgid="8434242278630875235">"8월"</item>
+    <item msgid="8493511009771049442">"9월"</item>
+    <item msgid="685181459001441496">"10월"</item>
+    <item msgid="6662413035764778250">"11월"</item>
+    <item msgid="3784870986696899313">"12월"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"사진 프레임"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"영화"</string>
+    <string name="loading_video" msgid="4013492720121891585">"동영상 로드 중..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"동영상 다시 시작"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"%s에서 재생을 다시 시작하시겠습니까?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"재생 다시 시작"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"시작"</string>
+    <string name="photos" msgid="3921471644099818826">"사진"</string>
+    <string name="videos" msgid="9168892901395116842">"동영상"</string>
+    <string name="camera" msgid="2730811566218090802">"카메라"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"알 수 없는 위치"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"저장"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"취소"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"시작하려면 얼굴을 탭하세요."</string>
+    <string name="saving_image" msgid="3051745378545909260">"사진 저장 중..."</string>
+    <string name="crop_label" msgid="521114301871349328">"사진 자르기"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"잠시 기다려 주세요..."</string>
+    <string name="set_image" msgid="7246975856983303047">"사진을 다음으로 설정"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"배경화면을 설정하는 중입니다. 잠시 기다려 주세요..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"사진"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"배경화면"</string>
+    <string name="details_ok" msgid="6848594369924424312">"확인"</string>
+    <string name="loading_new" msgid="7893793767294787579">"새 앨범 및 사진 로드 중"</string>
+    <string name="initializing" msgid="2374228157398540466">"로드 중"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"컬렉션에서 항목 선택"</string>
+    <string name="no_items" msgid="3117870234034732172">"컬렉션에 항목이 없습니다."</string>
+    <string name="home" msgid="3335112006212947568">"홈"</string>
+    <string name="pick" msgid="7248789132035843128">"선택"</string>
+    <string name="delete" msgid="2839695998251824487">"삭제"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"삭제 확인"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"계속하시겠습니까?"</string>
+    <string name="cancel" msgid="3637516880917356226">"취소"</string>
+    <string name="share" msgid="3619042788254195341">"공유"</string>
+    <string name="more" msgid="1526449516720792387">"추가 작업"</string>
+    <string name="select_all" msgid="8623593677101437957">"모두 선택"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"모두 선택취소"</string>
+    <string name="slideshow" msgid="4355906903247112975">"슬라이드쇼"</string>
+    <string name="play" msgid="8116587039599675035">"재생"</string>
+    <string name="menu" msgid="1819649153380636719">"메뉴"</string>
+    <string name="details" msgid="8415120088556445230">"세부정보"</string>
+    <string name="album_selected" msgid="3441280740465738452">"앨범이 선택됨"</string>
+    <string name="item_selected" msgid="6137767503817908841">"항목이 선택됨"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"앨범이 선택됨"</string>
+    <string name="items_selected" msgid="8548754404879264861">"항목이 선택됨"</string>
+    <string name="album" msgid="5198388817734336004">"앨범"</string>
+    <string name="start" msgid="4316892252528232165">"시작"</string>
+    <string name="end" msgid="3645506196012005500">"종료"</string>
+    <string name="location" msgid="3432705876921618314">"위치"</string>
+    <string name="title" msgid="7622928349908052569">"제목"</string>
+    <string name="type" msgid="4329478642546287192">"유형"</string>
+    <string name="taken_on" msgid="3269486910757192479">"촬영 날짜"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"지도에 표시"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"왼쪽으로 회전"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"오른쪽으로 회전"</string>
+    <string name="crop" msgid="7970750655414797277">"자르기"</string>
+    <string name="set_as" msgid="3636764710790507868">"다음으로 설정"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"배경화면으로 설정"</string>
+    <string name="item" msgid="636303673288563698">"항목"</string>
+    <string name="items" msgid="6403254716052150916">"항목"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"연락처 아이콘으로 설정"</string>
+    <string name="miles_around" msgid="267745511737880822">"마일"</string>
+    <!-- no translation found for at (4109376691525252109) -->
+    <skip />
+    <string name="date_unknown" msgid="4277113471036917386">"날짜를 알 수 없음"</string>
+    <string name="video_err" msgid="7917736494827857757">"동영상을 재생할 수 없음"</string>
+</resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
new file mode 100644
index 0000000..25c2338
--- /dev/null
+++ b/res/values-nb/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galleri"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"Jan"</item>
+    <item msgid="4680813504147866612">"Feb"</item>
+    <item msgid="1934595560893153574">"Mar"</item>
+    <item msgid="9020107212348500338">"Apr"</item>
+    <item msgid="5734859077892484949">"Mai"</item>
+    <item msgid="8546985030184126468">"Jun"</item>
+    <item msgid="3639654472839533748">"Jul"</item>
+    <item msgid="8434242278630875235">"Aug"</item>
+    <item msgid="8493511009771049442">"Sep"</item>
+    <item msgid="685181459001441496">"Okt"</item>
+    <item msgid="6662413035764778250">"Nov"</item>
+    <item msgid="3784870986696899313">"Des"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Bilderamme"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Filmer"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Laster video…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Fortsett avspilling"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Fortsett avspilling fra %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Fortsett avspilling"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Begynn på nytt"</string>
+    <string name="photos" msgid="3921471644099818826">"bilder"</string>
+    <string name="videos" msgid="9168892901395116842">"videoer"</string>
+    <string name="camera" msgid="2730811566218090802">"Kamera"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Ukjent sted"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Lagre"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Forkast"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Trykk på et ansikt for å begynne."</string>
+    <string name="saving_image" msgid="3051745378545909260">"Lagrer bilde…"</string>
+    <string name="crop_label" msgid="521114301871349328">"Beskjær bilde"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Vent litt…"</string>
+    <string name="set_image" msgid="7246975856983303047">"Bruk bilde som"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Setter bakgrunnsbilde, vent litt…"</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Bilder"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Bakgrunnsbilde"</string>
+    <string name="details_ok" msgid="6848594369924424312">"OK"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Laster inn nye albumer og bilder"</string>
+    <string name="initializing" msgid="2374228157398540466">"Henter"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Velg et element fra samlingen"</string>
+    <string name="no_items" msgid="3117870234034732172">"Det er ingen elementer i samlingen"</string>
+    <string name="home" msgid="3335112006212947568">"Startsiden"</string>
+    <string name="pick" msgid="7248789132035843128">"Velg"</string>
+    <string name="delete" msgid="2839695998251824487">"Slett"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Bekreft sletting"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Er du sikker?"</string>
+    <string name="cancel" msgid="3637516880917356226">"Avbryt"</string>
+    <string name="share" msgid="3619042788254195341">"Del"</string>
+    <string name="more" msgid="1526449516720792387">"Mer"</string>
+    <string name="select_all" msgid="8623593677101437957">"Marker alle"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Fjern alle markeringer"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Lysbildevisning"</string>
+    <string name="play" msgid="8116587039599675035">"Spill av"</string>
+    <string name="menu" msgid="1819649153380636719">"Meny"</string>
+    <string name="details" msgid="8415120088556445230">"Detaljer"</string>
+    <string name="album_selected" msgid="3441280740465738452">"album valgt"</string>
+    <string name="item_selected" msgid="6137767503817908841">"element valgt"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"albumer valgt"</string>
+    <string name="items_selected" msgid="8548754404879264861">"elementer valgt"</string>
+    <string name="album" msgid="5198388817734336004">"Album"</string>
+    <string name="start" msgid="4316892252528232165">"Start"</string>
+    <string name="end" msgid="3645506196012005500">"Avslutt"</string>
+    <string name="location" msgid="3432705876921618314">"Sted"</string>
+    <string name="title" msgid="7622928349908052569">"Tittel"</string>
+    <string name="type" msgid="4329478642546287192">"Type"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Fra"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Vis på kartet"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Roter til venstre"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Roter til høyre"</string>
+    <string name="crop" msgid="7970750655414797277">"Beskjær"</string>
+    <string name="set_as" msgid="3636764710790507868">"Angi som"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Bruk som bakgrunn"</string>
+    <string name="item" msgid="636303673288563698">"element"</string>
+    <string name="items" msgid="6403254716052150916">"elementer"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Angi som kontaktikon"</string>
+    <string name="miles_around" msgid="267745511737880822">"kilometers omkrets fra"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Dato ukjent"</string>
+    <string name="video_err" msgid="7917736494827857757">"Kunne ikke spille av videoen"</string>
+</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
new file mode 100644
index 0000000..6860926
--- /dev/null
+++ b/res/values-nl/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galerij"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"jan"</item>
+    <item msgid="4680813504147866612">"feb"</item>
+    <item msgid="1934595560893153574">"mrt"</item>
+    <item msgid="9020107212348500338">"apr"</item>
+    <item msgid="5734859077892484949">"mei"</item>
+    <item msgid="8546985030184126468">"jun"</item>
+    <item msgid="3639654472839533748">"jul"</item>
+    <item msgid="8434242278630875235">"aug"</item>
+    <item msgid="8493511009771049442">"sep"</item>
+    <item msgid="685181459001441496">"okt"</item>
+    <item msgid="6662413035764778250">"nov"</item>
+    <item msgid="3784870986696899313">"dec"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Fotolijstje"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Films"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Video laden..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Video hervatten"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Afspelen hervatten vanaf %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Afspelen hervatten"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Opnieuw starten"</string>
+    <string name="photos" msgid="3921471644099818826">"foto\'s"</string>
+    <string name="videos" msgid="9168892901395116842">"video\'s"</string>
+    <string name="camera" msgid="2730811566218090802">"Camera"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Onbekende locatie"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Opslaan"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Ongedaan maken"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Tik op een gezicht om te beginnen."</string>
+    <string name="saving_image" msgid="3051745378545909260">"Foto opslaan..."</string>
+    <string name="crop_label" msgid="521114301871349328">"Foto bijsnijden"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Een ogenblik geduld..."</string>
+    <string name="set_image" msgid="7246975856983303047">"Foto instellen als"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Achtergrond wordt ingesteld. Een ogenblik geduld..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Foto\'s"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Achtergrond"</string>
+    <string name="details_ok" msgid="6848594369924424312">"OK"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Nieuwe albums en foto\'s laden"</string>
+    <string name="initializing" msgid="2374228157398540466">"Laden"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Kies een item uit uw verzameling"</string>
+    <string name="no_items" msgid="3117870234034732172">"Er staan geen items in uw verzameling"</string>
+    <string name="home" msgid="3335112006212947568">"Startpagina"</string>
+    <string name="pick" msgid="7248789132035843128">"Kiezen"</string>
+    <string name="delete" msgid="2839695998251824487">"Verwijderen"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Verwijderen bevestigen"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Weet u het zeker?"</string>
+    <string name="cancel" msgid="3637516880917356226">"Annuleren"</string>
+    <string name="share" msgid="3619042788254195341">"Delen"</string>
+    <string name="more" msgid="1526449516720792387">"Meer"</string>
+    <string name="select_all" msgid="8623593677101437957">"Alles selecteren"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Selectie ongedaan maken"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Diavoorstelling"</string>
+    <string name="play" msgid="8116587039599675035">"Afspelen"</string>
+    <string name="menu" msgid="1819649153380636719">"Menu"</string>
+    <string name="details" msgid="8415120088556445230">"Details"</string>
+    <string name="album_selected" msgid="3441280740465738452">"album geselecteerd"</string>
+    <string name="item_selected" msgid="6137767503817908841">"item geselecteerd"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"albums geselecteerd"</string>
+    <string name="items_selected" msgid="8548754404879264861">"items geselecteerd"</string>
+    <string name="album" msgid="5198388817734336004">"Album"</string>
+    <string name="start" msgid="4316892252528232165">"Start"</string>
+    <string name="end" msgid="3645506196012005500">"Beëindigen"</string>
+    <string name="location" msgid="3432705876921618314">"Locatie"</string>
+    <string name="title" msgid="7622928349908052569">"Titel"</string>
+    <string name="type" msgid="4329478642546287192">"Type"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Genomen op"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Op kaart weergeven"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Linksom draaien"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Rechtsom draaien"</string>
+    <string name="crop" msgid="7970750655414797277">"Bijsnijden"</string>
+    <string name="set_as" msgid="3636764710790507868">"Instellen als"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Instellen als achtergrond"</string>
+    <string name="item" msgid="636303673288563698">"item"</string>
+    <string name="items" msgid="6403254716052150916">"items"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Instellen als pictogram voor contact"</string>
+    <string name="miles_around" msgid="267745511737880822">"kilometers rondom"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Datum onbekend"</string>
+    <string name="video_err" msgid="7917736494827857757">"Kan video niet afspelen"</string>
+</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
new file mode 100644
index 0000000..a62db4d
--- /dev/null
+++ b/res/values-pl/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galeria"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"Sty"</item>
+    <item msgid="4680813504147866612">"Lut"</item>
+    <item msgid="1934595560893153574">"Mar"</item>
+    <item msgid="9020107212348500338">"Kwi"</item>
+    <item msgid="5734859077892484949">"Maj"</item>
+    <item msgid="8546985030184126468">"Cze"</item>
+    <item msgid="3639654472839533748">"Lip"</item>
+    <item msgid="8434242278630875235">"Sie"</item>
+    <item msgid="8493511009771049442">"Wrz"</item>
+    <item msgid="685181459001441496">"Paź"</item>
+    <item msgid="6662413035764778250">"Lis"</item>
+    <item msgid="3784870986696899313">"Gru"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Ramka zdjęcia"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Filmy"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Ładowanie filmu..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Wznów film"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Wznowić odtwarzanie od %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Wznów odtwarzanie"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Rozpocznij"</string>
+    <string name="photos" msgid="3921471644099818826">"zdjęcia"</string>
+    <string name="videos" msgid="9168892901395116842">"filmy wideo"</string>
+    <string name="camera" msgid="2730811566218090802">"Aparat"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Nieznana lokalizacja"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Zapisz"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Odrzuć"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Dotknij twarzy, aby rozpocząć"</string>
+    <string name="saving_image" msgid="3051745378545909260">"Trwa zapisywanie zdjęcia…"</string>
+    <string name="crop_label" msgid="521114301871349328">"Przytnij zdjęcie"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Proszę czekać…"</string>
+    <string name="set_image" msgid="7246975856983303047">"Ustaw zdjęcie jako"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Ustawianie tapety, proszę czekać…"</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Zdjęcia"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Tapeta"</string>
+    <string name="details_ok" msgid="6848594369924424312">"OK"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Wczytywanie nowych albumów i zdjęć"</string>
+    <string name="initializing" msgid="2374228157398540466">"Wczytywanie"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Wybierz element z kolekcji"</string>
+    <string name="no_items" msgid="3117870234034732172">"W Twojej kolekcji nie ma żadnych elementów"</string>
+    <string name="home" msgid="3335112006212947568">"Ekran główny"</string>
+    <string name="pick" msgid="7248789132035843128">"Wybierz"</string>
+    <string name="delete" msgid="2839695998251824487">"Usuń"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Potwierdź usunięcie"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Na pewno?"</string>
+    <string name="cancel" msgid="3637516880917356226">"Anuluj"</string>
+    <string name="share" msgid="3619042788254195341">"Udostępnij"</string>
+    <string name="more" msgid="1526449516720792387">"Więcej"</string>
+    <string name="select_all" msgid="8623593677101437957">"Zaznacz wszystko"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Usuń zaznaczenie wszystkich"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Pokaz slajdów"</string>
+    <string name="play" msgid="8116587039599675035">"Odtwórz"</string>
+    <string name="menu" msgid="1819649153380636719">"Menu"</string>
+    <string name="details" msgid="8415120088556445230">"Szczegóły"</string>
+    <string name="album_selected" msgid="3441280740465738452">"wybrany album"</string>
+    <string name="item_selected" msgid="6137767503817908841">"wybrany element"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"wybranych albumów"</string>
+    <string name="items_selected" msgid="8548754404879264861">"wybranych elementów"</string>
+    <string name="album" msgid="5198388817734336004">"Album"</string>
+    <string name="start" msgid="4316892252528232165">"Start"</string>
+    <string name="end" msgid="3645506196012005500">"Zakończ"</string>
+    <string name="location" msgid="3432705876921618314">"Lokalizacja"</string>
+    <string name="title" msgid="7622928349908052569">"Tytuł"</string>
+    <string name="type" msgid="4329478642546287192">"Typ"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Zapisano:"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Pokaż na mapie"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Obróć w lewo"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Obróć w prawo"</string>
+    <string name="crop" msgid="7970750655414797277">"Przytnij"</string>
+    <string name="set_as" msgid="3636764710790507868">"Ustaw jako"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Ustaw jako tapetę"</string>
+    <string name="item" msgid="636303673288563698">"element"</string>
+    <string name="items" msgid="6403254716052150916">"elementy"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Ustaw jako ikonę kontaktu"</string>
+    <string name="miles_around" msgid="267745511737880822">"mil od"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Data nieznana"</string>
+    <string name="video_err" msgid="7917736494827857757">"Nie można odtworzyć filmu wideo"</string>
+</resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..5b3299b
--- /dev/null
+++ b/res/values-pt-rPT/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galeria"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"Jan"</item>
+    <item msgid="4680813504147866612">"Fev"</item>
+    <item msgid="1934595560893153574">"Mar"</item>
+    <item msgid="9020107212348500338">"Abr"</item>
+    <item msgid="5734859077892484949">"Mai"</item>
+    <item msgid="8546985030184126468">"Jun"</item>
+    <item msgid="3639654472839533748">"Jul"</item>
+    <item msgid="8434242278630875235">"Ago"</item>
+    <item msgid="8493511009771049442">"Set"</item>
+    <item msgid="685181459001441496">"Out"</item>
+    <item msgid="6662413035764778250">"Nov"</item>
+    <item msgid="3784870986696899313">"Dez"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Moldura da imagem"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Filmes"</string>
+    <string name="loading_video" msgid="4013492720121891585">"A carregar vídeo..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Retomar o vídeo"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Retomar reprodução a partir de %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Retomar a reprodução"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Recomeçar"</string>
+    <string name="photos" msgid="3921471644099818826">"fotografias"</string>
+    <string name="videos" msgid="9168892901395116842">"vídeos"</string>
+    <string name="camera" msgid="2730811566218090802">"Câmara"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Localização desconhecida"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Guardar"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Rejeitar"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Toque num rosto para começar."</string>
+    <string name="saving_image" msgid="3051745378545909260">"A guardar imagem..."</string>
+    <string name="crop_label" msgid="521114301871349328">"Recortar imagem"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Aguarde..."</string>
+    <string name="set_image" msgid="7246975856983303047">"Definir imagem como:"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"A definir a imagem de fundo, aguarde..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Imagens"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Imagem de fundo"</string>
+    <string name="details_ok" msgid="6848594369924424312">"OK"</string>
+    <string name="loading_new" msgid="7893793767294787579">"A carregar novos álbuns e fotografias"</string>
+    <string name="initializing" msgid="2374228157398540466">"A carregar"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Escolha um item a partir da colecção"</string>
+    <string name="no_items" msgid="3117870234034732172">"Não existem itens na sua colecção"</string>
+    <string name="home" msgid="3335112006212947568">"Página inicial"</string>
+    <string name="pick" msgid="7248789132035843128">"Escolher"</string>
+    <string name="delete" msgid="2839695998251824487">"Eliminar"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Confirmar eliminação"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Tem a certeza?"</string>
+    <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
+    <string name="share" msgid="3619042788254195341">"Partilhar"</string>
+    <string name="more" msgid="1526449516720792387">"Mais"</string>
+    <string name="select_all" msgid="8623593677101437957">"Seleccionar tudo"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Desmarcar tudo"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Apresentação de diapositivos"</string>
+    <string name="play" msgid="8116587039599675035">"Reproduzir"</string>
+    <string name="menu" msgid="1819649153380636719">"Menu"</string>
+    <string name="details" msgid="8415120088556445230">"Detalhes"</string>
+    <string name="album_selected" msgid="3441280740465738452">"álbum seleccionado"</string>
+    <string name="item_selected" msgid="6137767503817908841">"item seleccionado"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"álbuns seleccionados"</string>
+    <string name="items_selected" msgid="8548754404879264861">"itens seleccionados"</string>
+    <string name="album" msgid="5198388817734336004">"Álbum"</string>
+    <string name="start" msgid="4316892252528232165">"Iniciar"</string>
+    <string name="end" msgid="3645506196012005500">"Terminar"</string>
+    <string name="location" msgid="3432705876921618314">"Localização"</string>
+    <string name="title" msgid="7622928349908052569">"Título"</string>
+    <string name="type" msgid="4329478642546287192">"Tipo"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Criado(a) em"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Mostrar no mapa"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Rodar para a esquerda"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Rodar para a direita"</string>
+    <string name="crop" msgid="7970750655414797277">"Recortar"</string>
+    <string name="set_as" msgid="3636764710790507868">"Definir como"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Definir como imagem de fundo"</string>
+    <string name="item" msgid="636303673288563698">"item"</string>
+    <string name="items" msgid="6403254716052150916">"itens"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Definir como ícone de contacto"</string>
+    <string name="miles_around" msgid="267745511737880822">"milhas aproximadas"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Data desconhecida"</string>
+    <string name="video_err" msgid="7917736494827857757">"Não é possível reproduzir vídeo"</string>
+</resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
new file mode 100644
index 0000000..b9420e5
--- /dev/null
+++ b/res/values-pt/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galeria"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"Jan"</item>
+    <item msgid="4680813504147866612">"Fev"</item>
+    <item msgid="1934595560893153574">"Mar"</item>
+    <item msgid="9020107212348500338">"Abr"</item>
+    <item msgid="5734859077892484949">"Mai"</item>
+    <item msgid="8546985030184126468">"Jun"</item>
+    <item msgid="3639654472839533748">"Jul"</item>
+    <item msgid="8434242278630875235">"Ago"</item>
+    <item msgid="8493511009771049442">"Set"</item>
+    <item msgid="685181459001441496">"Out"</item>
+    <item msgid="6662413035764778250">"Nov"</item>
+    <item msgid="3784870986696899313">"Dez"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Frame da imagem"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Filmes"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Carregando vídeo..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Retomar vídeo"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Retomar reprodução de %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Retomar a reprodução"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Reiniciar"</string>
+    <string name="photos" msgid="3921471644099818826">"fotos"</string>
+    <string name="videos" msgid="9168892901395116842">"vídeos"</string>
+    <string name="camera" msgid="2730811566218090802">"Câmera"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Local desconhecido"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Salvar"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Descartar"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Toque em uma face para começar."</string>
+    <string name="saving_image" msgid="3051745378545909260">"Salvando imagem…"</string>
+    <string name="crop_label" msgid="521114301871349328">"Cortar imagem"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Aguarde..."</string>
+    <string name="set_image" msgid="7246975856983303047">"Definir imagem como"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Configurando o papel de parede, aguarde…"</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Imagens"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Papel de parede"</string>
+    <string name="details_ok" msgid="6848594369924424312">"OK"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Carregando novos álbuns e fotos"</string>
+    <string name="initializing" msgid="2374228157398540466">"Carregando"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Escolha um item da sua coleção"</string>
+    <string name="no_items" msgid="3117870234034732172">"Não há itens na sua coleção"</string>
+    <string name="home" msgid="3335112006212947568">"Página inicial"</string>
+    <string name="pick" msgid="7248789132035843128">"Escolher"</string>
+    <string name="delete" msgid="2839695998251824487">"Excluir"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Confirmar exclusão"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Tem certeza?"</string>
+    <string name="cancel" msgid="3637516880917356226">"Cancelar"</string>
+    <string name="share" msgid="3619042788254195341">"Compartilhar"</string>
+    <string name="more" msgid="1526449516720792387">"Mais"</string>
+    <string name="select_all" msgid="8623593677101437957">"Selecionar todos"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Desmarcar tudo"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Apresentação de slides"</string>
+    <string name="play" msgid="8116587039599675035">"Reproduzir"</string>
+    <string name="menu" msgid="1819649153380636719">"Menu"</string>
+    <string name="details" msgid="8415120088556445230">"Detalhes"</string>
+    <string name="album_selected" msgid="3441280740465738452">"álbum selecionado"</string>
+    <string name="item_selected" msgid="6137767503817908841">"item selecionado"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"álbuns selecionados"</string>
+    <string name="items_selected" msgid="8548754404879264861">"itens selecionados"</string>
+    <string name="album" msgid="5198388817734336004">"Álbum"</string>
+    <string name="start" msgid="4316892252528232165">"Iniciar"</string>
+    <string name="end" msgid="3645506196012005500">"Finalizar"</string>
+    <string name="location" msgid="3432705876921618314">"Local"</string>
+    <string name="title" msgid="7622928349908052569">"Título"</string>
+    <string name="type" msgid="4329478642546287192">"Tipo"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Feito(a) em"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Mostrar no mapa"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Girar para a esquerda"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Girar para a direita"</string>
+    <string name="crop" msgid="7970750655414797277">"Cortar"</string>
+    <string name="set_as" msgid="3636764710790507868">"Definir como"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Definir como plano de fundo"</string>
+    <string name="item" msgid="636303673288563698">"item"</string>
+    <string name="items" msgid="6403254716052150916">"itens"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Definir como ícone do contato"</string>
+    <string name="miles_around" msgid="267745511737880822">"milhas de distância de"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Data desconhecida"</string>
+    <string name="video_err" msgid="7917736494827857757">"Não é possível reproduzir o vídeo"</string>
+</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
new file mode 100644
index 0000000..7aeb601
--- /dev/null
+++ b/res/values-ru/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Фотоальбом"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"янв"</item>
+    <item msgid="4680813504147866612">"фев"</item>
+    <item msgid="1934595560893153574">"мар"</item>
+    <item msgid="9020107212348500338">"апр"</item>
+    <item msgid="5734859077892484949">"май"</item>
+    <item msgid="8546985030184126468">"июн"</item>
+    <item msgid="3639654472839533748">"июл"</item>
+    <item msgid="8434242278630875235">"авг"</item>
+    <item msgid="8493511009771049442">"сен"</item>
+    <item msgid="685181459001441496">"окт"</item>
+    <item msgid="6662413035764778250">"ноя"</item>
+    <item msgid="3784870986696899313">"дек"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Рамка фотографии"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Фильмы"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Загрузка видео…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Продолжение просмотра видео"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Продолжить воспроизведение с %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Продолжить воспроизведение"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Начать с начала"</string>
+    <string name="photos" msgid="3921471644099818826">"фото"</string>
+    <string name="videos" msgid="9168892901395116842">"видео"</string>
+    <string name="camera" msgid="2730811566218090802">"Камера"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Неизвестное местоположение"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Сохранить"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Отменить"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Нажмите лицо, чтобы начать."</string>
+    <string name="saving_image" msgid="3051745378545909260">"Сохранение картинки..."</string>
+    <string name="crop_label" msgid="521114301871349328">"Обрезать фотографию"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Подождите..."</string>
+    <string name="set_image" msgid="7246975856983303047">"Установить картинку как"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Установка обоев, подождите..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Картинки"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Обои"</string>
+    <string name="details_ok" msgid="6848594369924424312">"ОК"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Загрузка новых альбомов и фотографий"</string>
+    <string name="initializing" msgid="2374228157398540466">"Загрузка"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Выбрать элемент из коллекции"</string>
+    <string name="no_items" msgid="3117870234034732172">"В вашей коллекции ничего нет."</string>
+    <string name="home" msgid="3335112006212947568">"Главная"</string>
+    <string name="pick" msgid="7248789132035843128">"Выбрать"</string>
+    <string name="delete" msgid="2839695998251824487">"Удалить"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Подтвердить удаление"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Вы уверены?"</string>
+    <string name="cancel" msgid="3637516880917356226">"Отмена"</string>
+    <string name="share" msgid="3619042788254195341">"Отправить"</string>
+    <string name="more" msgid="1526449516720792387">"Дополнительно"</string>
+    <string name="select_all" msgid="8623593677101437957">"Выбрать все"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Снять все выделения"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Слайд-шоу"</string>
+    <string name="play" msgid="8116587039599675035">"Воспроизвести"</string>
+    <string name="menu" msgid="1819649153380636719">"Меню"</string>
+    <string name="details" msgid="8415120088556445230">"Сведения"</string>
+    <string name="album_selected" msgid="3441280740465738452">"альбом выбран"</string>
+    <string name="item_selected" msgid="6137767503817908841">"элемент выбран"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"альб. выбрано"</string>
+    <string name="items_selected" msgid="8548754404879264861">"элем. выбрано"</string>
+    <string name="album" msgid="5198388817734336004">"Альбом"</string>
+    <string name="start" msgid="4316892252528232165">"Начать"</string>
+    <string name="end" msgid="3645506196012005500">"Завершить"</string>
+    <string name="location" msgid="3432705876921618314">"Местоположение"</string>
+    <string name="title" msgid="7622928349908052569">"Название"</string>
+    <string name="type" msgid="4329478642546287192">"Тип"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Дата создания:"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Показать на карте"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Повернуть влево"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Повернуть вправо"</string>
+    <string name="crop" msgid="7970750655414797277">"Обрезать"</string>
+    <string name="set_as" msgid="3636764710790507868">"Установить как"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Установить как фоновый рисунок"</string>
+    <string name="item" msgid="636303673288563698">"элемент"</string>
+    <string name="items" msgid="6403254716052150916">"элем."</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Задать как значок контакта"</string>
+    <string name="miles_around" msgid="267745511737880822">"км от:"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Дата неизвестна"</string>
+    <string name="video_err" msgid="7917736494827857757">"Не удается воспроизвести видео"</string>
+</resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
new file mode 100644
index 0000000..1682a7a
--- /dev/null
+++ b/res/values-sv/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galleri"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"jan"</item>
+    <item msgid="4680813504147866612">"feb"</item>
+    <item msgid="1934595560893153574">"mar"</item>
+    <item msgid="9020107212348500338">"apr"</item>
+    <item msgid="5734859077892484949">"maj"</item>
+    <item msgid="8546985030184126468">"jun"</item>
+    <item msgid="3639654472839533748">"jul"</item>
+    <item msgid="8434242278630875235">"aug"</item>
+    <item msgid="8493511009771049442">"sep"</item>
+    <item msgid="685181459001441496">"okt"</item>
+    <item msgid="6662413035764778250">"nov"</item>
+    <item msgid="3784870986696899313">"dec"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Bildram"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Filmer"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Läser in video…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Fortsätt spela videon"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Fortsätt spela upp från %s?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Fortsätt spela upp"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Börja om"</string>
+    <string name="photos" msgid="3921471644099818826">"foton"</string>
+    <string name="videos" msgid="9168892901395116842">"videor"</string>
+    <string name="camera" msgid="2730811566218090802">"Kamera"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Okänd plats"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Spara"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Ignorera"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Peka på ett ansikte när du vill börja."</string>
+    <string name="saving_image" msgid="3051745378545909260">"Sparar bild…"</string>
+    <string name="crop_label" msgid="521114301871349328">"Beskär bild"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Vänta…"</string>
+    <string name="set_image" msgid="7246975856983303047">"Använd bild som"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Anger bakgrund, vänta…"</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Bilder"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Bakgrund"</string>
+    <string name="details_ok" msgid="6848594369924424312">"OK"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Läser in nya album och foton"</string>
+    <string name="initializing" msgid="2374228157398540466">"Läser in"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Välj ett objekt i din samling"</string>
+    <string name="no_items" msgid="3117870234034732172">"Det finns inga objekt i din samling"</string>
+    <string name="home" msgid="3335112006212947568">"Startsida"</string>
+    <string name="pick" msgid="7248789132035843128">"Välj"</string>
+    <string name="delete" msgid="2839695998251824487">"Ta bort"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Bekräfta borttagning"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Är du säker?"</string>
+    <string name="cancel" msgid="3637516880917356226">"Avbryt"</string>
+    <string name="share" msgid="3619042788254195341">"Dela"</string>
+    <string name="more" msgid="1526449516720792387">"Mer"</string>
+    <string name="select_all" msgid="8623593677101437957">"Markera alla"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Avmarkera alla"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Bildspel"</string>
+    <string name="play" msgid="8116587039599675035">"Spela"</string>
+    <string name="menu" msgid="1819649153380636719">"Meny"</string>
+    <string name="details" msgid="8415120088556445230">"Information"</string>
+    <string name="album_selected" msgid="3441280740465738452">"album har markerats"</string>
+    <string name="item_selected" msgid="6137767503817908841">"objekt har markerats"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"album har markerats"</string>
+    <string name="items_selected" msgid="8548754404879264861">"objekt har markerats"</string>
+    <string name="album" msgid="5198388817734336004">"Album"</string>
+    <string name="start" msgid="4316892252528232165">"Start"</string>
+    <string name="end" msgid="3645506196012005500">"Avsluta"</string>
+    <string name="location" msgid="3432705876921618314">"Plats"</string>
+    <string name="title" msgid="7622928349908052569">"Titel"</string>
+    <string name="type" msgid="4329478642546287192">"Typ"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Skapat den"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Visa på karta"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Rotera åt vänster"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Rotera åt höger"</string>
+    <string name="crop" msgid="7970750655414797277">"Beskär"</string>
+    <string name="set_as" msgid="3636764710790507868">"Använd som"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Använd som bakgrund"</string>
+    <string name="item" msgid="636303673288563698">"objekt"</string>
+    <string name="items" msgid="6403254716052150916">"objekt"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Använd som kontaktikon"</string>
+    <string name="miles_around" msgid="267745511737880822">"miles från"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Datum okänt"</string>
+    <string name="video_err" msgid="7917736494827857757">"Det gick inte att spela videon"</string>
+</resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
new file mode 100644
index 0000000..98e63e6
--- /dev/null
+++ b/res/values-tr/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"Galeri"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"Oca"</item>
+    <item msgid="4680813504147866612">"Şub"</item>
+    <item msgid="1934595560893153574">"Mar"</item>
+    <item msgid="9020107212348500338">"Nis"</item>
+    <item msgid="5734859077892484949">"May"</item>
+    <item msgid="8546985030184126468">"Haz"</item>
+    <item msgid="3639654472839533748">"Tem"</item>
+    <item msgid="8434242278630875235">"Ağu"</item>
+    <item msgid="8493511009771049442">"Eyl"</item>
+    <item msgid="685181459001441496">"Eki"</item>
+    <item msgid="6662413035764778250">"Kas"</item>
+    <item msgid="3784870986696899313">"Ara"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"Resim çerçevesi"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"Filmler"</string>
+    <string name="loading_video" msgid="4013492720121891585">"Video yükleniyor..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"Videoyu sürdür"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"Yürütme şuradan devam ettirilsin mi: %s ?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"Yürütmeyi sürdür"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"Başlat"</string>
+    <string name="photos" msgid="3921471644099818826">"fotoğraf"</string>
+    <string name="videos" msgid="9168892901395116842">"video"</string>
+    <string name="camera" msgid="2730811566218090802">"Kamera"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"Bilinmeyen konum"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"Kaydet"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"Sil"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"Başlamak için bir yüze hafifçe dokunun"</string>
+    <string name="saving_image" msgid="3051745378545909260">"Resim kaydediliyor..."</string>
+    <string name="crop_label" msgid="521114301871349328">"Resmi kırp"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"Lütfen bekleyin..."</string>
+    <string name="set_image" msgid="7246975856983303047">"Resmi şu şekilde ayarla:"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"Duvar kağıdı ayarlanıyor, lütfen bekleyin..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"Resimler"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"Duvar Kağıdı"</string>
+    <string name="details_ok" msgid="6848594369924424312">"Tamam"</string>
+    <string name="loading_new" msgid="7893793767294787579">"Yeni albümler ve fotoğraflar yükleniyor"</string>
+    <string name="initializing" msgid="2374228157398540466">"Yükleniyor"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"Koleksiyonunuzdan bir öğe seçin"</string>
+    <string name="no_items" msgid="3117870234034732172">"Koleksiyonunuzda hiçbir öğe bulunmuyor"</string>
+    <string name="home" msgid="3335112006212947568">"Ana Sayfa"</string>
+    <string name="pick" msgid="7248789132035843128">"Seç"</string>
+    <string name="delete" msgid="2839695998251824487">"Sil"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"Silme İşlemini Onayla"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"Emin misiniz?"</string>
+    <string name="cancel" msgid="3637516880917356226">"İptal"</string>
+    <string name="share" msgid="3619042788254195341">"Paylaş"</string>
+    <string name="more" msgid="1526449516720792387">"Diğer"</string>
+    <string name="select_all" msgid="8623593677101437957">"Tümünü Seç"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"Tüm Seçimleri Kaldır"</string>
+    <string name="slideshow" msgid="4355906903247112975">"Slayt Gösterisi"</string>
+    <string name="play" msgid="8116587039599675035">"Yürüt"</string>
+    <string name="menu" msgid="1819649153380636719">"Menü"</string>
+    <string name="details" msgid="8415120088556445230">"Ayrıntılar"</string>
+    <string name="album_selected" msgid="3441280740465738452">"albüm seçildi"</string>
+    <string name="item_selected" msgid="6137767503817908841">"öğe seçildi"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"albüm seçildi"</string>
+    <string name="items_selected" msgid="8548754404879264861">"öğe seçildi"</string>
+    <string name="album" msgid="5198388817734336004">"Albüm"</string>
+    <string name="start" msgid="4316892252528232165">"Başlat"</string>
+    <string name="end" msgid="3645506196012005500">"Sonlandır"</string>
+    <string name="location" msgid="3432705876921618314">"Konum"</string>
+    <string name="title" msgid="7622928349908052569">"Başlık"</string>
+    <string name="type" msgid="4329478642546287192">"Tür"</string>
+    <string name="taken_on" msgid="3269486910757192479">"Çekim zamanı:"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"Haritada göster"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"Sola Döndür"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"Sağa Döndür"</string>
+    <string name="crop" msgid="7970750655414797277">"Kırp"</string>
+    <string name="set_as" msgid="3636764710790507868">"Şu şekilde ayarla:"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"Duvar kağıdı olarak ayarla"</string>
+    <string name="item" msgid="636303673288563698">"öğe"</string>
+    <string name="items" msgid="6403254716052150916">"öğe"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"Kişi simgesi olarak ayarla"</string>
+    <string name="miles_around" msgid="267745511737880822">"mil civarında"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"Tarih bilinmiyor"</string>
+    <string name="video_err" msgid="7917736494827857757">"Video oynatılamıyor"</string>
+</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..35633b8
--- /dev/null
+++ b/res/values-zh-rCN/strings.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"图库"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"1 月"</item>
+    <item msgid="4680813504147866612">"2 月"</item>
+    <item msgid="1934595560893153574">"3 月"</item>
+    <item msgid="9020107212348500338">"4 月"</item>
+    <item msgid="5734859077892484949">"5 月"</item>
+    <item msgid="8546985030184126468">"6 月"</item>
+    <item msgid="3639654472839533748">"7 月"</item>
+    <item msgid="8434242278630875235">"8 月"</item>
+    <item msgid="8493511009771049442">"9 月"</item>
+    <item msgid="685181459001441496">"10 月"</item>
+    <item msgid="6662413035764778250">"11 月"</item>
+    <item msgid="3784870986696899313">"12 月"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"相框"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"电影"</string>
+    <string name="loading_video" msgid="4013492720121891585">"正在载入视频..."</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"重新播放视频"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"从 %s 开始重新播放?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"重新播放"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"重新开始"</string>
+    <string name="photos" msgid="3921471644099818826">"照片"</string>
+    <string name="videos" msgid="9168892901395116842">"视频"</string>
+    <string name="camera" msgid="2730811566218090802">"相机"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"未知地点"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"保存"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"舍弃"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"点按一张脸开始裁剪。"</string>
+    <string name="saving_image" msgid="3051745378545909260">"正在保存图片..."</string>
+    <string name="crop_label" msgid="521114301871349328">"修剪图片"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"请稍候..."</string>
+    <string name="set_image" msgid="7246975856983303047">"将图片设置为"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"正在设置壁纸,请稍候..."</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"图片"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"壁纸"</string>
+    <string name="details_ok" msgid="6848594369924424312">"确定"</string>
+    <string name="loading_new" msgid="7893793767294787579">"正在载入新相册和新照片"</string>
+    <!-- no translation found for initializing (2374228157398540466) -->
+    <skip />
+    <string name="pick_prompt" msgid="576608096149718121">"在您的集合中挑选一个项"</string>
+    <!-- no translation found for no_items (3117870234034732172) -->
+    <skip />
+    <string name="home" msgid="3335112006212947568">"住宅"</string>
+    <string name="pick" msgid="7248789132035843128">"精选"</string>
+    <string name="delete" msgid="2839695998251824487">"删除"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"确认删除"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"确定吗?"</string>
+    <string name="cancel" msgid="3637516880917356226">"取消"</string>
+    <string name="share" msgid="3619042788254195341">"分享"</string>
+    <string name="more" msgid="1526449516720792387">"更多"</string>
+    <string name="select_all" msgid="8623593677101437957">"全选"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"取消全选"</string>
+    <string name="slideshow" msgid="4355906903247112975">"播放幻灯片"</string>
+    <string name="play" msgid="8116587039599675035">"播放"</string>
+    <string name="menu" msgid="1819649153380636719">"选单"</string>
+    <string name="details" msgid="8415120088556445230">"详细信息"</string>
+    <string name="album_selected" msgid="3441280740465738452">"已选定  个相册"</string>
+    <string name="item_selected" msgid="6137767503817908841">"已选定  个项"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"已选定  个相册"</string>
+    <string name="items_selected" msgid="8548754404879264861">"已选定  个项"</string>
+    <string name="album" msgid="5198388817734336004">"相册"</string>
+    <string name="start" msgid="4316892252528232165">"启动"</string>
+    <string name="end" msgid="3645506196012005500">"结束"</string>
+    <string name="location" msgid="3432705876921618314">"地点"</string>
+    <string name="title" msgid="7622928349908052569">"标题"</string>
+    <string name="type" msgid="4329478642546287192">"类型"</string>
+    <string name="taken_on" msgid="3269486910757192479">"拍摄日期:"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"显示在地图上"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"向左旋转"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"向右旋转"</string>
+    <string name="crop" msgid="7970750655414797277">"修剪"</string>
+    <string name="set_as" msgid="3636764710790507868">"设置为"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"设置为壁纸"</string>
+    <string name="item" msgid="636303673288563698">"项"</string>
+    <string name="items" msgid="6403254716052150916">"项"</string>
+    <!-- no translation found for set_as_contact_icon (749479412834888190) -->
+    <skip />
+    <string name="miles_around" msgid="267745511737880822">"周围  英里"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"日期未知"</string>
+    <string name="video_err" msgid="7917736494827857757">"无法播放视频"</string>
+</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..3a81d02
--- /dev/null
+++ b/res/values-zh-rTW/strings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 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.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name" msgid="1928079047368929634">"圖片庫"</string>
+  <string-array name="months_abbreviated">
+    <item msgid="9066537518640988368">"1 月"</item>
+    <item msgid="4680813504147866612">"2 月"</item>
+    <item msgid="1934595560893153574">"3 月"</item>
+    <item msgid="9020107212348500338">"4 月"</item>
+    <item msgid="5734859077892484949">"5 月"</item>
+    <item msgid="8546985030184126468">"6 月"</item>
+    <item msgid="3639654472839533748">"7 月"</item>
+    <item msgid="8434242278630875235">"8 月"</item>
+    <item msgid="8493511009771049442">"9 月"</item>
+    <item msgid="685181459001441496">"10 月"</item>
+    <item msgid="6662413035764778250">"11 月"</item>
+    <item msgid="3784870986696899313">"12 月"</item>
+  </string-array>
+    <string name="gadget_title" msgid="259405922673466798">"相框"</string>
+    <string name="details_ms" msgid="940634969189855292">"%1$02d:%2$02d"</string>
+    <string name="details_hms" msgid="3215779248094151255">"%1$d:%2$02d:%3$02d"</string>
+    <string name="movie_view_label" msgid="7363495772706775465">"電影"</string>
+    <string name="loading_video" msgid="4013492720121891585">"正在載入影片…"</string>
+    <string name="resume_playing_title" msgid="8996677350649355013">"繼續播放影片"</string>
+    <string name="resume_playing_message" msgid="5184414518126703481">"要從 %s 繼續播放嗎?"</string>
+    <string name="resume_playing_resume" msgid="3847915469173852416">"繼續播放"</string>
+    <string name="resume_playing_restart" msgid="5471008499835769292">"重新開始"</string>
+    <string name="photos" msgid="3921471644099818826">"張相片"</string>
+    <string name="videos" msgid="9168892901395116842">"部影片"</string>
+    <string name="camera" msgid="2730811566218090802">"相機"</string>
+    <string name="location_unknown" msgid="7522685931923663795">"未知地點"</string>
+    <string name="crop_save_text" msgid="8140440041190264400">"儲存"</string>
+    <string name="crop_discard_text" msgid="5303657888280340603">"放棄"</string>
+    <string name="multiface_crop_help" msgid="3127018992717032779">"輕觸所需的臉孔開始裁剪。"</string>
+    <string name="saving_image" msgid="3051745378545909260">"儲存相片中…"</string>
+    <string name="crop_label" msgid="521114301871349328">"裁剪相片"</string>
+    <string name="running_face_detection" msgid="2293932204708167704">"請稍候…"</string>
+    <string name="set_image" msgid="7246975856983303047">"設定相片為…"</string>
+    <string name="wallpaper" msgid="9222901738515471972">"設定桌布中,請稍候…"</string>
+    <string name="camera_pick_wallpaper" msgid="7026385960511811641">"圖片"</string>
+    <string name="camera_setas_wallpaper" msgid="797463183863414289">"桌布"</string>
+    <string name="details_ok" msgid="6848594369924424312">"確定"</string>
+    <string name="loading_new" msgid="7893793767294787579">"正在載入新相簿和相片"</string>
+    <string name="initializing" msgid="2374228157398540466">"載入中"</string>
+    <string name="pick_prompt" msgid="576608096149718121">"從圖片集中挑選一個項目"</string>
+    <string name="no_items" msgid="3117870234034732172">"您的圖片集裡並沒有任何項目"</string>
+    <string name="home" msgid="3335112006212947568">"主螢幕鍵"</string>
+    <string name="pick" msgid="7248789132035843128">"挑選"</string>
+    <string name="delete" msgid="2839695998251824487">"刪除"</string>
+    <string name="confirm_delete" msgid="5731757674837098707">"確認刪除"</string>
+    <string name="are_you_sure" msgid="6293680869731897736">"您確定嗎?"</string>
+    <string name="cancel" msgid="3637516880917356226">"取消"</string>
+    <string name="share" msgid="3619042788254195341">"分享"</string>
+    <string name="more" msgid="1526449516720792387">"更多"</string>
+    <string name="select_all" msgid="8623593677101437957">"全選"</string>
+    <string name="deselect_all" msgid="7397531298370285581">"取消選取全部"</string>
+    <string name="slideshow" msgid="4355906903247112975">"投影播放"</string>
+    <string name="play" msgid="8116587039599675035">"播放"</string>
+    <string name="menu" msgid="1819649153380636719">"選單"</string>
+    <string name="details" msgid="8415120088556445230">"詳細資料"</string>
+    <string name="album_selected" msgid="3441280740465738452">"本相簿已選取"</string>
+    <string name="item_selected" msgid="6137767503817908841">"個項目已選取"</string>
+    <string name="albums_selected" msgid="1815225920157190983">"本相簿已選取"</string>
+    <string name="items_selected" msgid="8548754404879264861">"個項目已選取"</string>
+    <string name="album" msgid="5198388817734336004">"相簿"</string>
+    <string name="start" msgid="4316892252528232165">"開始"</string>
+    <string name="end" msgid="3645506196012005500">"結束"</string>
+    <string name="location" msgid="3432705876921618314">"地點"</string>
+    <string name="title" msgid="7622928349908052569">"標題"</string>
+    <string name="type" msgid="4329478642546287192">"類型"</string>
+    <string name="taken_on" msgid="3269486910757192479">"拍攝時間"</string>
+    <string name="show_on_map" msgid="6157544221201750980">"在地圖上顯示"</string>
+    <string name="rotate_left" msgid="7412075232752726934">"向左旋轉"</string>
+    <string name="rotate_right" msgid="7340681085011826618">"向右旋轉"</string>
+    <string name="crop" msgid="7970750655414797277">"裁剪"</string>
+    <string name="set_as" msgid="3636764710790507868">"設為"</string>
+    <string name="set_as_wallpaper" msgid="5704970402417438945">"設為桌布"</string>
+    <string name="item" msgid="636303673288563698">"個項目"</string>
+    <string name="items" msgid="6403254716052150916">"個項目"</string>
+    <string name="set_as_contact_icon" msgid="749479412834888190">"設為聯絡人圖示"</string>
+    <string name="miles_around" msgid="267745511737880822">"距離 (英哩數)"</string>
+    <string name="date_unknown" msgid="4277113471036917386">"未知日期"</string>
+    <string name="video_err" msgid="7917736494827857757">"無法播放影片"</string>
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..b790e48
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+	<string name="app_name">Gallery</string>
+	<string-array name="months_abbreviated">
+		<item>Jan</item>
+		<item>Feb</item>
+		<item>Mar</item>
+		<item>Apr</item>
+		<item>May</item>
+		<item>Jun</item>
+		<item>Jul</item>
+		<item>Aug</item>
+		<item>Sep</item>
+		<item>Oct</item>
+		<item>Nov</item>
+		<item>Dec</item>
+	</string-array>
+	<!-- Title for picture frame gadget to show in list of all available gadgets -->
+    <string name="gadget_title">Picture frame</string>
+
+	<!-- Used to format short video duration in Details dialog. minutes:seconds e.g. 00:30 -->
+    <string name="details_ms">%1$02d:%2$02d</string>
+    <!-- Used to format video duration in Details dialog. hours:minutes:seconds e.g. 0:21:30 -->
+    <string name="details_hms">%1$d:%2$02d:%3$02d</string>
+    <!-- Activity label. This might show up in the activity-picker -->
+    <string name="movie_view_label">Movies</string>
+    <!-- shown in the video player view while the video is being loaded, before it starts playing -->
+    <string name="loading_video">Loading video\u2026</string>
+    <!-- Movie View Resume Playing dialog title -->
+    <string name="resume_playing_title">Resume video</string>
+
+    <!-- Movie View Start Playing dialog title -->
+    <string name="resume_playing_message">Resume playing from %s ?</string>
+    <!-- Movie View Start Playing button "Resume from bookmark" -->
+    <string name="resume_playing_resume">Resume playing</string>
+
+    <!-- Movie View Start Playing button "Beginning" -->
+    <string name="resume_playing_restart">Start over</string>
+    <string name="photos">photos</string>
+	<string name="videos">videos</string>
+	<string name="camera">Camera</string>
+
+	<!-- Button indicating that the cropped image should be saved -->
+	<string name="crop_save_text">Save</string>
+	<!-- Button indicating that the cropped image should be reverted back to the original -->
+	<string name="crop_discard_text">Discard</string>
+	<!-- Hint that appears when cropping an image with more than one face -->
+	<string name="multiface_crop_help">Tap a face to begin.</string>
+	<!-- Toast/alert that the image is being saved to the SD card -->
+	<string name="saving_image">Saving picture\u2026</string>
+	<!-- menu pick: crop the currently selected image -->
+	<string name="crop_label">Crop picture</string>
+	<!-- Toast/alert that the face detection is being run -->
+	<string name="running_face_detection">Please wait\u2026</string>
+
+	<!-- Displayed in the title of the dialog for things to do with a picture
+		 that is to be "set as" (e.g. set as contact photo or set as wallpaper) -->
+	<string name="set_image">Set picture as</string>
+	<!-- Toast/alert after saving wallpaper -->
+	<string name="wallpaper">Setting wallpaper, please wait\u2026</string>
+	<!-- String indicating an action of picking a picture to use as wallpaper
+		 (e.g. set wallpaper from "Pictures") -->
+	<string name="camera_pick_wallpaper">Pictures</string>
+	<string name="camera_setas_wallpaper">Wallpaper</string>
+
+    <!-- Details dialog "OK" button. Dismisses dialog. -->
+    <string name="details_ok">OK</string>
+    <string name="loading_new">Loading new albums and photos</string>
+    <string name="initializing">Loading</string>
+    <string name="pick_prompt">Pick an item from your collection</string>
+    <string name="no_items">There are no items in your collection</string>
+    <string name="home">Home</string>
+    <string name="pick">Pick</string>
+    <string name="delete">Delete</string>
+    <string name="confirm_delete">Confirm Delete</string>
+    <string name="are_you_sure">Are you sure?</string>
+    <string name="cancel">Cancel</string>
+    <string name="share">Share</string>
+    <string name="no_sd_card">SD Card unmounted or not present</string>
+
+	<!-- String indicating more actions are available -->
+    <string name="more">More</string>
+    <string name="select_all">Select All</string>
+    <string name="deselect_all">Deselect All</string>
+    <string name="slideshow">Slideshow</string>
+
+    <!-- String to play a video -->
+    <string name="play">Play</string>
+    <string name="menu">Menu</string>
+    <string name="details">Details</string>
+
+    <!-- String appended at the end depending upon what is selected eg. 1 album selected -->
+    <string name="album_selected">album selected</string>
+    <string name="item_selected">item selected</string>
+    <string name="albums_selected">albums selected</string>
+    <string name="items_selected">items selected</string>
+
+    <string name="album">Album</string>
+    <string name="start">Start</string>
+    <string name="end">End</string>
+    <string name="location">Location</string>
+    <string name="location_unknown">Unknown location</string>
+    <string name="title">Title</string>
+    <string name="type">Type</string>
+
+    <!-- String indicating timestamp of photo or video -->
+    <string name="taken_on">Taken on</string>
+    <string name="added_on">Added on</string>
+    <string name="show_on_map">Show on map</string>
+    <string name="rotate_left">Rotate Left</string>
+    <string name="rotate_right">Rotate Right</string>
+    <string name="crop">Crop</string>
+    <string name="set_as">Set as</string>
+    <string name="set_as_wallpaper">Set as wallpaper</string>
+    <string name="item">item</string>
+    <string name="items">items</string>
+    <string name="set_as_contact_icon">Set as contact icon</string>
+
+    <!-- String indicating rough location eg. 25 miles around San Francisco -->
+    <string name="miles_around">miles around</string>
+    <!-- String indicating an approximate location eg. Around Palo Alto, CA -->
+    <string name="around">Around</string>
+    <string name="date_unknown">Date unknown</string>
+    <string name="video_err">Unable to play video</string>
+</resources>
\ No newline at end of file
diff --git a/res/xml/appwidget_info.xml b/res/xml/appwidget_info.xml
new file mode 100644
index 0000000..b6893bc
--- /dev/null
+++ b/res/xml/appwidget_info.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 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.
+-->
+
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+    android:minWidth="146dip"
+    android:minHeight="146dip"
+    android:updatePeriodMillis="0"
+    android:initialLayout="@layout/photo_frame"
+    android:configure="com.cooliris.media.PhotoAppWidgetConfigure"
+    >
+</appwidget-provider>
diff --git a/res/xml/syncadapter.xml b/res/xml/syncadapter.xml
new file mode 100644
index 0000000..43b838f
--- /dev/null
+++ b/res/xml/syncadapter.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 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.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the SyncAdapter. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:contentAuthority="com.cooliris.picasa.contentprovider"
+    android:accountType="com.google"
+/>
\ No newline at end of file
diff --git a/src/com/cooliris/cache/BootReceiver.java b/src/com/cooliris/cache/BootReceiver.java
new file mode 100644
index 0000000..fb2bc73
--- /dev/null
+++ b/src/com/cooliris/cache/BootReceiver.java
@@ -0,0 +1,35 @@
+package com.cooliris.cache;
+
+import com.cooliris.media.Gallery;
+import com.cooliris.media.SingleDataSource;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+
+public class BootReceiver extends BroadcastReceiver {
+    private final String TAG = "BootReceiver"; 
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        Log.i(TAG, "Got intent with action " + action);
+        if (Intent.ACTION_MEDIA_SCANNER_FINISHED.equals(action)) {
+            CacheService.markDirty(context);
+            CacheService.startCache(context, true);
+        } else if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
+            // Do nothing, wait for the mediascanner to be done after mounting.
+            ;
+        } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)) {
+            Uri fileUri = intent.getData();
+            long bucketId = SingleDataSource.parseBucketIdFromFileUri(fileUri.toString());
+            if (!CacheService.isPresentInCache(bucketId)) {
+                CacheService.markDirty(context);
+            }
+        } else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
+            Gallery.NEEDS_REFRESH = true;
+        }
+    }
+}
diff --git a/src/com/cooliris/cache/CacheService.java b/src/com/cooliris/cache/CacheService.java
new file mode 100644
index 0000000..af83cb5
--- /dev/null
+++ b/src/com/cooliris/cache/CacheService.java
@@ -0,0 +1,1027 @@
+package com.cooliris.cache;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.nio.LongBuffer;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicReference;
+
+import android.app.IntentService;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Process;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+import android.util.Log;
+
+import com.cooliris.media.DataSource;
+import com.cooliris.media.DiskCache;
+import com.cooliris.media.LocalDataSource;
+import com.cooliris.media.LongSparseArray;
+import com.cooliris.media.MediaFeed;
+import com.cooliris.media.MediaItem;
+import com.cooliris.media.MediaSet;
+import com.cooliris.media.R;
+import com.cooliris.media.Shared;
+import com.cooliris.media.SortCursor;
+import com.cooliris.media.UriTexture;
+import com.cooliris.media.Utils;
+
+public final class CacheService extends IntentService {
+    private static final String TAG = "CacheService";
+    public static final String ACTION_CACHE = "com.cooliris.cache.action.CACHE";
+    public static final DiskCache sAlbumCache = new DiskCache("local-album-cache");
+
+    // Wait 2 seconds to start the thumbnailer so that the application can load without any overheads.
+    private static final int THUMBNAILER_WAIT_IN_MS = 2000;
+    private static final int DEFAULT_THUMBNAIL_WIDTH = 128;
+    private static final int DEFAULT_THUMBNAIL_HEIGHT = 96;
+
+    public static final String DEFAULT_IMAGE_SORT_ORDER = Images.ImageColumns.DATE_TAKEN + " ASC, "
+            + Images.ImageColumns.DATE_ADDED + " ASC";
+    public static final String DEFAULT_VIDEO_SORT_ORDER = Video.VideoColumns.DATE_TAKEN + " ASC, " + Video.VideoColumns.DATE_ADDED
+            + " ASC";
+    public static final String DEFAULT_BUCKET_SORT_ORDER = "upper(" + Images.ImageColumns.BUCKET_DISPLAY_NAME + ") ASC";
+
+    // Must preserve order between these indices and the order of the terms in BUCKET_PROJECTION_IMAGES, BUCKET_PROJECTION_VIDEOS.
+    // Not using SortedHashMap for efficieny reasons.
+    public static final int BUCKET_ID_INDEX = 0;
+    public static final int BUCKET_NAME_INDEX = 1;
+    public static final String[] BUCKET_PROJECTION_IMAGES = new String[] { Images.ImageColumns.BUCKET_ID,
+            Images.ImageColumns.BUCKET_DISPLAY_NAME };
+
+    public static final String[] BUCKET_PROJECTION_VIDEOS = new String[] { Video.VideoColumns.BUCKET_ID,
+            Video.VideoColumns.BUCKET_DISPLAY_NAME };
+
+    // Must preserve order between these indices and the order of the terms in THUMBNAIL_PROJECTION.
+    public static final int THUMBNAIL_ID_INDEX = 0;
+    public static final int THUMBNAIL_DATE_MODIFIED_INDEX = 1;
+    public static final int THUMBNAIL_DATA_INDEX = 2;
+    public static final String[] THUMBNAIL_PROJECTION = new String[] { Images.ImageColumns._ID, Images.ImageColumns.DATE_MODIFIED,
+            Images.ImageColumns.DATA };
+
+    // Must preserve order between these indices and the order of the terms in INITIAL_PROJECTION_IMAGES and
+    // INITIAL_PROJECTION_VIDEOS.
+    public static final int MEDIA_ID_INDEX = 0;
+    public static final int MEDIA_CAPTION_INDEX = 1;
+    public static final int MEDIA_MIME_TYPE_INDEX = 2;
+    public static final int MEDIA_LATITUDE_INDEX = 3;
+    public static final int MEDIA_LONGITUDE_INDEX = 4;
+    public static final int MEDIA_DATE_TAKEN_INDEX = 5;
+    public static final int MEDIA_DATE_ADDED_INDEX = 6;
+    public static final int MEDIA_DATE_MODIFIED_INDEX = 7;
+    public static final int MEDIA_DATA_INDEX = 8;
+    public static final int MEDIA_ORIENTATION_OR_DURATION_INDEX = 9;
+    public static final int MEDIA_BUCKET_ID_INDEX = 10;
+    public static final String[] PROJECTION_IMAGES = new String[] { Images.ImageColumns._ID, Images.ImageColumns.TITLE,
+            Images.ImageColumns.MIME_TYPE, Images.ImageColumns.LATITUDE, Images.ImageColumns.LONGITUDE,
+            Images.ImageColumns.DATE_TAKEN, Images.ImageColumns.DATE_ADDED, Images.ImageColumns.DATE_MODIFIED,
+            Images.ImageColumns.DATA, Images.ImageColumns.ORIENTATION, Images.ImageColumns.BUCKET_ID };
+
+    private static final String[] PROJECTION_VIDEOS = new String[] { Video.VideoColumns._ID, Video.VideoColumns.TITLE,
+            Video.VideoColumns.MIME_TYPE, Video.VideoColumns.LATITUDE, Video.VideoColumns.LONGITUDE, Video.VideoColumns.DATE_TAKEN,
+            Video.VideoColumns.DATE_ADDED, Video.VideoColumns.DATE_MODIFIED, Video.VideoColumns.DATA, Video.VideoColumns.DURATION,
+            Video.VideoColumns.BUCKET_ID };
+
+    public static final String BASE_CONTENT_STRING_IMAGES = (Images.Media.EXTERNAL_CONTENT_URI).toString() + "/";
+    public static final String BASE_CONTENT_STRING_VIDEOS = (Video.Media.EXTERNAL_CONTENT_URI).toString() + "/";
+    private static final AtomicReference<Thread> CACHE_THREAD = new AtomicReference<Thread>();
+    private static final AtomicReference<Thread> THUMBNAIL_THREAD = new AtomicReference<Thread>();
+
+    // Special indices in the Albumcache.
+    private static final int ALBUM_CACHE_METADATA_INDEX = -1;
+    private static final int ALBUM_CACHE_DIRTY_INDEX = -2;
+    private static final int ALBUM_CACHE_INCOMPLETE_INDEX = -3;
+    private static final int ALBUM_CACHE_DIRTY_BUCKET_INDEX = -4;
+    private static final int ALBUM_CACHE_LOCALE_INDEX = -5;
+
+    private static final DateFormat mDateFormat = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
+    private static final DateFormat mAltDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+    private static boolean QUEUE_DIRTY_SET;
+    private static boolean QUEUE_DIRTY_ALL;
+
+    public static String getCachePath(String subFolderName) {
+        return Environment.getExternalStorageDirectory() + "/Android/data/com.cooliris.media/cache/" + subFolderName;
+    }
+
+    public static void startCache(final Context context, boolean checkthumbnails) {
+        Locale locale = getLocaleForAlbumCache();
+        Locale defaultLocale = Locale.getDefault();
+        if (locale == null || !locale.equals(defaultLocale)) {
+            sAlbumCache.deleteAll();
+            putLocaleForAlbumCache(Locale.getDefault());
+        }
+        final Intent intent = new Intent(ACTION_CACHE, null, context, CacheService.class);
+        intent.putExtra("checkthumbnails", checkthumbnails);
+        context.startService(intent);
+    }
+
+    public static boolean isCacheReady(final boolean onlyMediaSets) {
+        if (onlyMediaSets) {
+            return (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null);
+        } else {
+            return (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null && sAlbumCache
+                    .get(ALBUM_CACHE_INCOMPLETE_INDEX, 0) == null);
+        }
+    }
+
+    public static boolean isCacheReady(long setId) {
+        boolean isReady = (sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0) != null
+                && sAlbumCache.get(ALBUM_CACHE_DIRTY_INDEX, 0) == null && sAlbumCache.get(ALBUM_CACHE_INCOMPLETE_INDEX, 0) == null);
+        if (!isReady) {
+            return isReady;
+        }
+        // Also, we need to check if this setId is dirty.
+        byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0);
+        if (existingData != null && existingData.length > 0) {
+            long[] ids = toLongArray(existingData);
+            int numIds = ids.length;
+            for (int i = 0; i < numIds; ++i) {
+                if (ids[i] == setId) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    public static boolean isPresentInCache(long setId) {
+        return sAlbumCache.get(setId, 0) != null;
+    }
+
+    public final static void markDirty(final Context context) {
+        byte[] data = new byte[] { 1 };
+        sAlbumCache.put(ALBUM_CACHE_DIRTY_INDEX, data);
+        if (CACHE_THREAD.get() == null) {
+            restartThread(CACHE_THREAD, "CacheRefresh", new Runnable() {
+                public void run() {
+                    refresh(context);
+                }
+            });
+        } else {
+            QUEUE_DIRTY_ALL = true;
+        }
+    }
+
+    public final static void markDirtyImmediate(long id) {
+        byte[] data = longToByteArray(id);
+        byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0);
+        if (existingData != null && existingData.length > 0) {
+            long[] ids = toLongArray(existingData);
+            int numIds = ids.length;
+            for (int i = 0; i < numIds; ++i) {
+                if (ids[i] == id) {
+                    return;
+                }
+            }
+            // Add this to the existing keys and concatenate the byte arrays.
+            data = concat(data, existingData);
+        }
+        sAlbumCache.put(ALBUM_CACHE_DIRTY_BUCKET_INDEX, data);
+    }
+
+    public final static void markDirty(final Context context, long id) {
+        markDirtyImmediate(id);
+        if (CACHE_THREAD.get() == null) {
+            restartThread(CACHE_THREAD, "CacheRefreshDirtySets", new Runnable() {
+                public void run() {
+                    refreshDirtySets(context);
+                }
+            });
+        } else {
+            QUEUE_DIRTY_SET = true;
+        }
+    }
+
+    public static boolean setHasItems(ContentResolver cr, long setId) {
+        final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
+        final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
+        StringBuffer whereString = new StringBuffer(Images.ImageColumns.BUCKET_ID + "=" + setId);
+        final Cursor cursorImages = cr.query(uriImages, BUCKET_PROJECTION_IMAGES, whereString.toString(), null, null);
+        if (cursorImages != null && cursorImages.getCount() > 0) {
+            cursorImages.close();
+            return true;
+        }
+        final Cursor cursorVideos = cr.query(uriVideos, BUCKET_PROJECTION_VIDEOS, whereString.toString(), null, null);
+        if (cursorVideos != null && cursorVideos.getCount() > 0) {
+            cursorVideos.close();
+            return true;
+        }
+        return false;
+    }
+
+    public static void loadMediaSets(final MediaFeed feed, final DataSource source, final boolean includeImages,
+            final boolean includeVideos) {
+        int timeElapsed = 0;
+        while (!isCacheReady(true) && timeElapsed < 10000) {
+            try {
+                Thread.sleep(300);
+            } catch (InterruptedException e) {
+                return;
+            }
+            timeElapsed += 300;
+        }
+        byte[] albumData = sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0);
+        if (albumData != null && albumData.length > 0) {
+            final DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256));
+            try {
+                final int numAlbums = dis.readInt();
+                Log.i(TAG, "Loading " + numAlbums + " albums.");
+                for (int i = 0; i < numAlbums; ++i) {
+                    final long setId = dis.readLong();
+                    final String name = Utils.readUTF(dis);
+                    final boolean hasImages = dis.readBoolean();
+                    final boolean hasVideos = dis.readBoolean();
+                    MediaSet mediaSet = feed.getMediaSet(setId);
+                    if (mediaSet == null) {
+                        mediaSet = feed.addMediaSet(setId, source);
+                    }
+                    if ((includeImages && hasImages) || (includeVideos && hasVideos)) {
+                        mediaSet.mName = name;
+                        mediaSet.mHasImages = hasImages;
+                        mediaSet.mHasVideos = hasVideos;
+                        mediaSet.mPicasaAlbumId = Shared.INVALID;
+                        mediaSet.generateTitle(true);
+                    }
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Error loading albums.");
+                sAlbumCache.deleteAll();
+                putLocaleForAlbumCache(Locale.getDefault());
+            }
+        } else {
+            Log.d(TAG, "No albums found.");
+        }
+    }
+
+    public static void loadMediaSet(final MediaFeed feed, final DataSource source, final long bucketId) {
+        int timeElapsed = 0;
+        while (!isCacheReady(false) && timeElapsed < 10000) {
+            try {
+                Thread.sleep(300);
+            } catch (InterruptedException e) {
+                return;
+            }
+            timeElapsed += 300;
+        }
+        byte[] albumData = sAlbumCache.get(ALBUM_CACHE_METADATA_INDEX, 0);
+        if (albumData != null && albumData.length > 0) {
+            DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256));
+            try {
+                int numAlbums = dis.readInt();
+                for (int i = 0; i < numAlbums; ++i) {
+                    long setId = dis.readLong();
+                    MediaSet mediaSet = null;
+                    if (setId == bucketId) {
+                        mediaSet = feed.getMediaSet(setId);
+                        if (mediaSet == null) {
+                            mediaSet = feed.addMediaSet(setId, source);
+                        }
+                    } else {
+                        mediaSet = new MediaSet();
+                    }
+                    mediaSet.mName = Utils.readUTF(dis);
+                    if (setId == bucketId) {
+                        mediaSet.mPicasaAlbumId = Shared.INVALID;
+                        mediaSet.generateTitle(true);
+                        return;
+                    }
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Error finding album " + bucketId);
+                sAlbumCache.deleteAll();
+                putLocaleForAlbumCache(Locale.getDefault());
+            }
+        } else {
+            Log.d(TAG, "No album found for album id " + bucketId);
+        }
+    }
+
+    public static void loadMediaItemsIntoMediaFeed(final MediaFeed feed, final MediaSet set, final int rangeStart,
+            final int rangeEnd, final boolean includeImages, final boolean includeVideos) {
+        int timeElapsed = 0;
+        byte[] albumData = null;
+        while (!isCacheReady(set.mId) && timeElapsed < 30000) {
+            try {
+                Thread.sleep(300);
+            } catch (InterruptedException e) {
+                return;
+            }
+            timeElapsed += 300;
+        }
+        albumData = sAlbumCache.get(set.mId, 0);
+        if (albumData != null && set.mNumItemsLoaded < set.getNumExpectedItems()) {
+            final DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(albumData), 256));
+            try {
+                int numItems = dis.readInt();
+                Log.i(TAG, "Loading Set Id " + set.mId + " with " + numItems + " items.");
+                set.setNumExpectedItems(numItems);
+                set.mMinTimestamp = dis.readLong();
+                set.mMaxTimestamp = dis.readLong();
+                for (int i = 0; i < numItems; ++i) {
+                    MediaItem item = new MediaItem();
+                    // Must preserve order with method that writes to cache.
+                    item.mId = dis.readLong();
+                    item.mCaption = Utils.readUTF(dis);
+                    item.mMimeType = Utils.readUTF(dis);
+                    item.setMediaType(dis.readInt());
+                    item.mLatitude = dis.readDouble();
+                    item.mLongitude = dis.readDouble();
+                    item.mDateTakenInMs = dis.readLong();
+                    item.mTriedRetrievingExifDateTaken = dis.readBoolean();
+                    item.mDateAddedInSec = dis.readLong();
+                    item.mDateModifiedInSec = dis.readLong();
+                    item.mDurationInSec = dis.readInt();
+                    item.mRotation = (float) dis.readInt();
+                    item.mFilePath = Utils.readUTF(dis);
+                    int itemMediaType = item.getMediaType();
+                    if ((itemMediaType == MediaItem.MEDIA_TYPE_IMAGE && includeImages)
+                            || (itemMediaType == MediaItem.MEDIA_TYPE_VIDEO && includeVideos)) {
+                        String baseUri = (itemMediaType == MediaItem.MEDIA_TYPE_IMAGE) ? BASE_CONTENT_STRING_IMAGES
+                                : BASE_CONTENT_STRING_VIDEOS;
+                        item.mContentUri = baseUri + item.mId;
+                        feed.addItemToMediaSet(item, set);
+                    }
+                }
+                dis.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Error loading items for album " + set.mName);
+                sAlbumCache.deleteAll();
+                putLocaleForAlbumCache(Locale.getDefault());
+            }
+        } else {
+            Log.d(TAG, "No items found for album " + set.mName);
+        }
+        set.updateNumExpectedItems();
+        set.generateTitle(true);
+    }
+
+    public static void populateVideoItemFromCursor(MediaItem item, ContentResolver cr, Cursor cursor, String baseUri) {
+        item.setMediaType(MediaItem.MEDIA_TYPE_VIDEO);
+        populateMediaItemFromCursor(item, cr, cursor, baseUri);
+    }
+
+    public static void populateMediaItemFromCursor(MediaItem item, ContentResolver cr, Cursor cursor, String baseUri) {
+        item.mId = cursor.getLong(CacheService.MEDIA_ID_INDEX);
+        item.mCaption = cursor.getString(CacheService.MEDIA_CAPTION_INDEX);
+        item.mMimeType = cursor.getString(CacheService.MEDIA_MIME_TYPE_INDEX);
+        item.mLatitude = cursor.getDouble(CacheService.MEDIA_LATITUDE_INDEX);
+        item.mLongitude = cursor.getDouble(CacheService.MEDIA_LONGITUDE_INDEX);
+        item.mDateTakenInMs = cursor.getLong(CacheService.MEDIA_DATE_TAKEN_INDEX);
+        item.mDateAddedInSec = cursor.getLong(CacheService.MEDIA_DATE_ADDED_INDEX);
+        item.mDateModifiedInSec = cursor.getLong(CacheService.MEDIA_DATE_MODIFIED_INDEX);
+        item.mFilePath = cursor.getString(CacheService.MEDIA_DATA_INDEX);
+
+        int itemMediaType = item.getMediaType();
+        // Check to see if a new date taken is available.
+        long dateTaken = fetchDateTaken(item);
+        if (dateTaken != -1L) {
+            item.mDateTakenInMs = dateTaken;
+            ContentValues values = new ContentValues();
+            if (itemMediaType == MediaItem.MEDIA_TYPE_VIDEO) {
+                values.put(Video.VideoColumns.DATE_TAKEN, item.mDateTakenInMs);
+            } else {
+                values.put(Images.ImageColumns.DATE_TAKEN, item.mDateTakenInMs);
+            }
+            if (item.mContentUri != null) {
+                cr.update(Uri.parse(item.mContentUri), values, null, null);
+            }
+        }
+
+        int orientationDurationValue = cursor.getInt(CacheService.MEDIA_ORIENTATION_OR_DURATION_INDEX);
+        if (itemMediaType == MediaItem.MEDIA_TYPE_IMAGE) {
+            item.mRotation = orientationDurationValue;
+        } else {
+            item.mDurationInSec = orientationDurationValue;
+        }
+        item.mContentUri = baseUri + item.mId;
+    }
+
+    // Returns -1 if we failed to examine EXIF information or EXIF parsing failed.
+    public static long fetchDateTaken(MediaItem item) {
+        if (!item.isDateTakenValid() && !item.mTriedRetrievingExifDateTaken
+                && (item.mFilePath.endsWith(".jpg") || item.mFilePath.endsWith(".jpeg"))) {
+            try {
+                ExifInterface exif = new ExifInterface(item.mFilePath);
+                String dateTakenStr = exif.getAttribute(ExifInterface.TAG_DATETIME);
+                if (dateTakenStr != null) {
+                    try {
+                        Date dateTaken = mDateFormat.parse(dateTakenStr);
+                        return dateTaken.getTime();
+                    } catch (ParseException pe) {
+                        try {
+                            Date dateTaken = mAltDateFormat.parse(dateTakenStr);
+                            return dateTaken.getTime();
+                        } catch (ParseException pe2) {
+                            Log.i(TAG, "Unable to parse date out of string - " + dateTakenStr);
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                Log.i(TAG, "Error reading Exif information, probably not a jpeg.");
+            }
+
+            // Ensures that we only try retrieving EXIF date taken once.
+            item.mTriedRetrievingExifDateTaken = true;
+        }
+        return -1L;
+    }
+
+    public static byte[] queryThumbnail(final Context context, final long thumbId, final long origId, final boolean isVideo,
+            final long timestamp) {
+        final DiskCache thumbnailCache = (isVideo) ? LocalDataSource.sThumbnailCacheVideo : LocalDataSource.sThumbnailCache;
+        return queryThumbnail(context, thumbId, origId, isVideo, thumbnailCache, timestamp);
+    }
+
+    private static byte[] queryThumbnail(final Context context, final long thumbId, final long origId, final boolean isVideo,
+            final DiskCache thumbnailCache, final long timestamp) {
+        Thread thumbnailThread = THUMBNAIL_THREAD.getAndSet(null);
+        if (thumbnailThread != null) {
+            thumbnailThread.interrupt();
+        }
+        byte[] bitmap = thumbnailCache.get(thumbId, timestamp);
+        if (bitmap == null) {
+            Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
+            long time = SystemClock.uptimeMillis();
+            bitmap = buildThumbnailForId(context, thumbnailCache, thumbId, origId, isVideo, DEFAULT_THUMBNAIL_WIDTH,
+                    DEFAULT_THUMBNAIL_HEIGHT);
+            Log.i(TAG, "Built thumbnail and screennail for " + origId + " in " + (SystemClock.uptimeMillis() - time));
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+        }
+        return bitmap;
+    }
+
+    private static void buildThumbnails(final Context context) {
+        Log.i(TAG, "Preparing DiskCache for all thumbnails.");
+        final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
+        final ContentResolver cr = context.getContentResolver();
+        final Cursor cursorImages = cr.query(uriImages, THUMBNAIL_PROJECTION, null, null, null);
+        final DiskCache thumbnailCache = LocalDataSource.sThumbnailCache;
+
+        if (cursorImages != null && cursorImages.moveToFirst()) {
+            final int size = cursorImages.getCount();
+            final long[] ids = new long[size];
+            final long[] thumbnailIds = new long[size];
+            final long[] timestamp = new long[size];
+            int ctr = 0;
+            do {
+                if (Thread.interrupted()) {
+                    return;
+                }
+                synchronized (thumbnailCache) {
+                    ids[ctr] = cursorImages.getLong(THUMBNAIL_ID_INDEX);
+                    timestamp[ctr] = cursorImages.getLong(THUMBNAIL_DATE_MODIFIED_INDEX);
+                    thumbnailIds[ctr] = Utils.Crc64Long(cursorImages.getString(THUMBNAIL_DATA_INDEX));
+                    ++ctr;
+                }
+            } while (cursorImages.moveToNext());
+            cursorImages.close();
+
+            for (int i = 0; i < size; ++i) {
+                if (Thread.interrupted()) {
+                    return;
+                }
+                final long id = ids[i];
+                final long timeModifiedInSec = timestamp[i];
+                final long thumbnailId = thumbnailIds[i];
+                if (!thumbnailCache.isDataAvailable(thumbnailId, timeModifiedInSec * 1000)) {
+                    buildThumbnailForId(context, thumbnailCache, thumbnailId, id, false, DEFAULT_THUMBNAIL_WIDTH,
+                            DEFAULT_THUMBNAIL_HEIGHT);
+                }
+            }
+        }
+        Log.i(TAG, "DiskCache ready for all thumbnails.");
+    }
+
+    private static byte[] buildThumbnailForId(final Context context, final DiskCache thumbnailCache, final long thumbId,
+            final long origId, final boolean isVideo, final int thumbnailWidth, final int thumbnailHeight) {
+        if (origId == Shared.INVALID) {
+            return null;
+        }
+        Log.i(TAG, "Building thumbnail for item " + origId);
+        try {
+            Bitmap bitmap = null;
+            Thread.sleep(1);
+            if (!isVideo) {
+                final String uriString = BASE_CONTENT_STRING_IMAGES + origId;
+                UriTexture.invalidateCache(thumbId, 1024);
+                try {
+                    bitmap = UriTexture.createFromUri(context, uriString, 1024, 1024, thumbId, null);
+                } catch (IOException e) {
+                    return null;
+                } catch (URISyntaxException e) {
+                    return null;
+                }
+            } else {
+                // TODO: We need to generate video thumbnails if it is not eclair
+                bitmap = MediaStore.Video.Thumbnails.getThumbnail(context.getContentResolver(), origId,
+                        MediaStore.Video.Thumbnails.MICRO_KIND, null);
+            }
+            if (bitmap == null) {
+                return null;
+            }
+            final byte[] retVal = writeBitmapToCache(thumbnailCache, thumbId, origId, bitmap, thumbnailWidth, thumbnailHeight);
+            Log.i(TAG, "Generated thumbnail for item " + origId);
+            return retVal;
+        } catch (InterruptedException e) {
+            return null;
+        }
+    }
+
+    public static byte[] writeBitmapToCache(final DiskCache thumbnailCache, final long thumbId, final long origId,
+            final Bitmap bitmap, final int thumbnailWidth, final int thumbnailHeight) {
+        final int width = bitmap.getWidth();
+        final int height = bitmap.getHeight();
+        // Detect faces to find the focal point, otherwise fall back to the image center.
+        int focusX = width / 2;
+        int focusY = height / 2;
+        // We have commented out face detection since it slows down the generation of the thumbnail and screennail.
+
+        // final FaceDetector faceDetector = new FaceDetector(width, height, 1);
+        // final FaceDetector.Face[] faces = new FaceDetector.Face[1];
+        // final int numFaces = faceDetector.findFaces(bitmap, faces);
+        // if (numFaces > 0 && faces[0].confidence() >= FaceDetector.Face.CONFIDENCE_THRESHOLD) {
+        // final PointF midPoint = new PointF();
+        // faces[0].getMidPoint(midPoint);
+        // focusX = (int) midPoint.x;
+        // focusY = (int) midPoint.y;
+        // }
+
+        // Crop to thumbnail aspect ratio biased towards the focus point.
+        int cropX;
+        int cropY;
+        int cropWidth;
+        int cropHeight;
+        float scaleFactor;
+        if (thumbnailWidth * height < thumbnailHeight * width) {
+            // Vertically constrained.
+            cropWidth = thumbnailWidth * height / thumbnailHeight;
+            cropX = Math.max(0, Math.min(focusX - cropWidth / 2, width - cropWidth));
+            cropY = 0;
+            cropHeight = height;
+            scaleFactor = (float) thumbnailHeight / height;
+        } else {
+            // Horizontally constrained.
+            cropHeight = thumbnailHeight * width / thumbnailWidth;
+            cropY = Math.max(0, Math.min(focusY - cropHeight / 2, height - cropHeight));
+            cropX = 0;
+            cropWidth = width;
+            scaleFactor = (float) thumbnailWidth / width;
+        }
+        final Bitmap finalBitmap = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, Bitmap.Config.RGB_565);
+        final Canvas canvas = new Canvas(finalBitmap);
+        final Paint paint = new Paint();
+        paint.setFilterBitmap(true);
+        canvas.drawColor(0);
+        canvas.drawBitmap(bitmap, new Rect(cropX, cropY, cropX + cropWidth, cropY + cropHeight), new Rect(0, 0, thumbnailWidth,
+                thumbnailHeight), paint);
+        bitmap.recycle();
+
+        // Store (long thumbnailId, short focusX, short focusY, JPEG data).
+        final ByteArrayOutputStream cacheOutput = new ByteArrayOutputStream(16384);
+        final DataOutputStream dataOutput = new DataOutputStream(cacheOutput);
+        byte[] retVal = null;
+        try {
+            dataOutput.writeLong(origId);
+            dataOutput.writeShort((int) ((focusX - cropX) * scaleFactor));
+            dataOutput.writeShort((int) ((focusY - cropY) * scaleFactor));
+            dataOutput.flush();
+            finalBitmap.compress(Bitmap.CompressFormat.JPEG, 80, cacheOutput);
+            retVal = cacheOutput.toByteArray();
+            synchronized (thumbnailCache) {
+                thumbnailCache.put(thumbId, retVal);
+            }
+            cacheOutput.close();
+        } catch (Exception e) {
+            ;
+        }
+        return retVal;
+    }
+
+    public CacheService() {
+        super("CacheService");
+    }
+
+    @Override
+    protected void onHandleIntent(final Intent intent) {
+        Log.i(TAG, "Starting CacheService");
+        if (Environment.getExternalStorageState() == Environment.MEDIA_BAD_REMOVAL) {
+            sAlbumCache.deleteAll();
+            putLocaleForAlbumCache(Locale.getDefault());
+        }
+        Locale locale = getLocaleForAlbumCache();
+        if (locale != null && locale.equals(Locale.getDefault())) {
+            // The cache is in the same locale as the system locale.
+            if (!isCacheReady(false)) {
+                // The albums and their items have not yet been cached, we need to run the service.
+                startNewCacheThread();
+            } else {
+                startNewCacheThreadForDirtySets();
+            }
+        } else {
+            // The locale has changed, we need to regenerate the strings.
+            sAlbumCache.deleteAll();
+            putLocaleForAlbumCache(Locale.getDefault());
+            startNewCacheThread();
+        }
+        if (intent.getBooleanExtra("checkthumbnails", false)) {
+            startNewThumbnailThread(this);
+        } else {
+            Thread existingThread = THUMBNAIL_THREAD.getAndSet(null);
+            if (existingThread != null) {
+                existingThread.interrupt();
+            }
+        }
+    }
+
+    private static void putLocaleForAlbumCache(Locale locale) {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(bos);
+        try {
+            Utils.writeUTF(dos, locale.getCountry());
+            Utils.writeUTF(dos, locale.getLanguage());
+            Utils.writeUTF(dos, locale.getVariant());
+            dos.flush();
+            bos.flush();
+            byte[] data = bos.toByteArray();
+            sAlbumCache.put(ALBUM_CACHE_LOCALE_INDEX, data);
+            sAlbumCache.flush();
+            dos.close();
+            bos.close();
+        } catch (IOException e) {
+            // Could not write locale to cache.
+            Log.i(TAG, "Error writing locale to cache.");
+            ;
+        }
+    }
+
+    private static Locale getLocaleForAlbumCache() {
+        byte[] data = sAlbumCache.get(ALBUM_CACHE_LOCALE_INDEX, 0);
+        if (data != null && data.length > 0) {
+            ByteArrayInputStream bis = new ByteArrayInputStream(data);
+            DataInputStream dis = new DataInputStream(bis);
+            try {
+                String country = Utils.readUTF(dis);
+                if (country == null)
+                    country = "";
+                String language = Utils.readUTF(dis);
+                if (language == null)
+                    language = "";
+                String variant = Utils.readUTF(dis);
+                if (variant == null)
+                    variant = "";
+                Locale locale = new Locale(language, country, variant);
+                dis.close();
+                bis.close();
+                return locale;
+            } catch (IOException e) {
+                // Could not read locale in cache.
+                Log.i(TAG, "Error reading locale from cache.");
+                return null;
+            }
+        }
+        return null;
+    }
+
+    private static void restartThread(final AtomicReference<Thread> threadRef, String name, final Runnable action) {
+        // Create a new thread.
+        Thread newThread = new Thread() {
+            public void run() {
+                try {
+                    action.run();
+                } finally {
+                    threadRef.compareAndSet(this, null);
+                }
+            }
+        };
+        newThread.setName(name);
+        newThread.start();
+
+        // Interrupt any existing thread.
+        Thread existingThread = threadRef.getAndSet(newThread);
+        if (existingThread != null) {
+            existingThread.interrupt();
+        }
+    }
+
+    public static void startNewThumbnailThread(final Context context) {
+        restartThread(THUMBNAIL_THREAD, "ThumbnailRefresh", new Runnable() {
+            public void run() {
+                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+                try {
+                    // It is an optimization to prevent the thumbnailer from running while the application loads
+                    Thread.sleep(THUMBNAILER_WAIT_IN_MS);
+                } catch (InterruptedException e) {
+                    return;
+                }
+                CacheService.buildThumbnails(context);
+            }
+        });
+    }
+
+    private void startNewCacheThread() {
+        restartThread(CACHE_THREAD, "CacheRefresh", new Runnable() {
+            public void run() {
+                refresh(CacheService.this);
+            }
+        });
+    }
+
+    private void startNewCacheThreadForDirtySets() {
+        restartThread(CACHE_THREAD, "CacheRefreshDirtySets", new Runnable() {
+            public void run() {
+                refreshDirtySets(CacheService.this);
+            }
+        });
+    }
+
+    private static final byte[] concat(byte[] A, byte[] B) {
+        byte[] C = (byte[]) new byte[A.length + B.length];
+        System.arraycopy(A, 0, C, 0, A.length);
+        System.arraycopy(B, 0, C, A.length, B.length);
+        return C;
+    }
+
+    private static final long[] toLongArray(byte[] data) {
+        ByteBuffer bBuffer = ByteBuffer.wrap(data);
+        LongBuffer lBuffer = bBuffer.asLongBuffer();
+        int numLongs = lBuffer.capacity();
+        long[] retVal = new long[numLongs];
+        for (int i = 0; i < numLongs; ++i) {
+            retVal[i] = lBuffer.get(i);
+        }
+        return retVal;
+    }
+
+    private static byte[] longToByteArray(long l) {
+        byte[] bArray = new byte[8];
+        ByteBuffer bBuffer = ByteBuffer.wrap(bArray);
+        LongBuffer lBuffer = bBuffer.asLongBuffer();
+        lBuffer.put(0, l);
+        return bArray;
+    }
+
+    private final static void refresh(final Context context) {
+        // First we build the album cache.
+        // This is the meta-data about the albums / buckets on the SD card.
+        Log.i(TAG, "Refreshing cache.");
+        sAlbumCache.deleteAll();
+        putLocaleForAlbumCache(Locale.getDefault());
+        
+        ArrayList<MediaSet> sets = new ArrayList<MediaSet>();
+        LongSparseArray<MediaSet> acceleratedSets = new LongSparseArray<MediaSet>();
+        Log.i(TAG, "Building albums.");
+        final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI.buildUpon().appendQueryParameter("distinct", "true").build();
+        final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI.buildUpon().appendQueryParameter("distinct", "true").build();
+        final ContentResolver cr = context.getContentResolver();
+
+        final Cursor cursorImages = cr.query(uriImages, BUCKET_PROJECTION_IMAGES, null, null, DEFAULT_BUCKET_SORT_ORDER);
+        final Cursor cursorVideos = cr.query(uriVideos, BUCKET_PROJECTION_VIDEOS, null, null, DEFAULT_BUCKET_SORT_ORDER);
+        Cursor[] cursors = new Cursor[2];
+        cursors[0] = cursorImages;
+        cursors[1] = cursorVideos;
+        final SortCursor sortCursor = new SortCursor(cursors, Images.ImageColumns.BUCKET_DISPLAY_NAME, SortCursor.TYPE_STRING, true);
+
+        if (sortCursor != null && sortCursor.moveToFirst()) {
+            sets.ensureCapacity(sortCursor.getCount());
+            acceleratedSets = new LongSparseArray<MediaSet>(sortCursor.getCount());
+            MediaSet cameraSet = new MediaSet();
+            cameraSet.mId = LocalDataSource.CAMERA_BUCKET_ID;
+            cameraSet.mName = context.getResources().getString(R.string.camera);
+            sets.add(cameraSet);
+            acceleratedSets.put(cameraSet.mId, cameraSet);
+            do {
+                if (Thread.interrupted()) {
+                    return;
+                }
+                long setId = sortCursor.getLong(BUCKET_ID_INDEX);
+                MediaSet mediaSet = findSet(setId, acceleratedSets);
+                if (mediaSet == null) {
+                    mediaSet = new MediaSet();
+                    mediaSet.mId = setId;
+                    mediaSet.mName = sortCursor.getString(BUCKET_NAME_INDEX);
+                    sets.add(mediaSet);
+                    acceleratedSets.put(setId, mediaSet);
+                }
+                mediaSet.mHasImages |= (sortCursor.getCurrentCursorIndex() == 0);
+                mediaSet.mHasVideos |= (sortCursor.getCurrentCursorIndex() == 1);
+            } while (sortCursor.moveToNext());
+            sortCursor.close();
+        }
+
+        byte[] data = new byte[] { 1 };
+        sAlbumCache.put(ALBUM_CACHE_INCOMPLETE_INDEX, data);
+        writeSetsToCache(sets);
+        Log.i(TAG, "Done building albums.");
+        // Now we must cache the items contained in every album / bucket.
+        populateMediaItemsForSets(context, sets, acceleratedSets, false);
+        sAlbumCache.delete(ALBUM_CACHE_INCOMPLETE_INDEX);
+        if (QUEUE_DIRTY_ALL) {
+            QUEUE_DIRTY_ALL = false;
+            refresh(context);
+        }
+    }
+
+    private final static void refreshDirtySets(final Context context) {
+        byte[] existingData = sAlbumCache.get(ALBUM_CACHE_DIRTY_BUCKET_INDEX, 0);
+        if (existingData != null && existingData.length > 0) {
+            long[] ids = toLongArray(existingData);
+            int numIds = ids.length;
+            if (numIds > 0) {
+                ArrayList<MediaSet> sets = new ArrayList<MediaSet>(numIds);
+                LongSparseArray<MediaSet> acceleratedSets = new LongSparseArray<MediaSet>(numIds);
+                for (int i = 0; i < numIds; ++i) {
+                    MediaSet set = new MediaSet();
+                    set.mId = ids[i];
+                    sets.add(set);
+                    acceleratedSets.put(set.mId, set);
+                }
+                Log.i(TAG, "Refreshing dirty albums");
+                populateMediaItemsForSets(context, sets, acceleratedSets, true);
+            }
+        }
+        if (QUEUE_DIRTY_SET) {
+            QUEUE_DIRTY_SET = false;
+            refreshDirtySets(context);
+        } else {
+            sAlbumCache.delete(ALBUM_CACHE_DIRTY_BUCKET_INDEX);
+        }
+    }
+
+    private final static void populateMediaItemsForSets(final Context context, final ArrayList<MediaSet> sets,
+            final LongSparseArray<MediaSet> acceleratedSets, boolean useWhere) {
+        if (sets == null || sets.size() == 0 || Thread.interrupted()) {
+            return;
+        }
+        Log.i(TAG, "Building items.");
+        final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
+        final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
+        final ContentResolver cr = context.getContentResolver();
+
+        String whereClause = null;
+        if (useWhere) {
+            int numSets = sets.size();
+            StringBuffer whereString = new StringBuffer(Images.ImageColumns.BUCKET_ID + " in (");
+            for (int i = 0; i < numSets; ++i) {
+                whereString.append(sets.get(i).mId);
+                if (i != numSets - 1) {
+                    whereString.append(",");
+                }
+            }
+            whereString.append(")");
+            whereClause = whereString.toString();
+            Log.i(TAG, "Updating dirty albums where " + whereClause);
+        }
+
+        final Cursor cursorImages = cr.query(uriImages, PROJECTION_IMAGES, whereClause, null, DEFAULT_IMAGE_SORT_ORDER);
+        final Cursor cursorVideos = cr.query(uriVideos, PROJECTION_VIDEOS, whereClause, null, DEFAULT_VIDEO_SORT_ORDER);
+        Cursor[] cursors = new Cursor[2];
+        cursors[0] = cursorImages;
+        cursors[1] = cursorVideos;
+        final SortCursor sortCursor = new SortCursor(cursors, Images.ImageColumns.DATE_TAKEN, SortCursor.TYPE_NUMERIC, true);
+        if (Thread.interrupted()) {
+            return;
+        }
+
+        if (sortCursor != null && sortCursor.moveToFirst()) {
+            int count = sortCursor.getCount();
+            int numSets = sets.size();
+            int approximateCountPerSet = count / numSets;
+            for (int i = 0; i < numSets; ++i) {
+                MediaSet set = sets.get(i);
+                set.setNumExpectedItems(approximateCountPerSet);
+            }
+            do {
+                if (Thread.interrupted()) {
+                    return;
+                }
+                MediaItem item = new MediaItem();
+                boolean isVideo = (sortCursor.getCurrentCursorIndex() == 1);
+                if (isVideo) {
+                    populateVideoItemFromCursor(item, cr, sortCursor, BASE_CONTENT_STRING_VIDEOS);
+                } else {
+                    populateMediaItemFromCursor(item, cr, sortCursor, BASE_CONTENT_STRING_IMAGES);
+                }
+                long setId = sortCursor.getLong(MEDIA_BUCKET_ID_INDEX);
+                MediaSet set = findSet(setId, acceleratedSets);
+                if (set != null) {
+                    set.addItem(item);
+                }
+            } while (sortCursor.moveToNext());
+        }
+        if (sets.size() > 0) {
+            writeItemsToCache(sets);
+            Log.i(TAG, "Done building items.");
+        }
+    }
+
+    private static final void writeSetsToCache(final ArrayList<MediaSet> sets) {
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        final int numSets = sets.size();
+        final DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256));
+        try {
+            dos.writeInt(numSets);
+            for (int i = 0; i < numSets; ++i) {
+                if (Thread.interrupted()) {
+                    return;
+                }
+                final MediaSet set = sets.get(i);
+                dos.writeLong(set.mId);
+                Utils.writeUTF(dos, set.mName);
+                dos.writeBoolean(set.mHasImages);
+                dos.writeBoolean(set.mHasVideos);
+            }
+            dos.flush();
+            sAlbumCache.put(ALBUM_CACHE_METADATA_INDEX, bos.toByteArray());
+            dos.close();
+            if (numSets == 0) {
+                sAlbumCache.deleteAll();
+                putLocaleForAlbumCache(Locale.getDefault());
+            }
+            sAlbumCache.flush();
+        } catch (IOException e) {
+            Log.e(TAG, "Error writing albums to diskcache.");
+            sAlbumCache.deleteAll();
+            putLocaleForAlbumCache(Locale.getDefault());
+        }
+    }
+
+    private static final void writeItemsToCache(final ArrayList<MediaSet> sets) {
+        final int numSets = sets.size();
+        for (int i = 0; i < numSets; ++i) {
+            if (Thread.interrupted()) {
+                return;
+            }
+            writeItemsForASet(sets.get(i));
+        }
+        sAlbumCache.flush();
+    }
+
+    private static final void writeItemsForASet(final MediaSet set) {
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        final DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256));
+        try {
+            ArrayList<MediaItem> items = set.getItems();
+            int numItems = set.getNumItems();
+            dos.writeInt(numItems);
+            dos.writeLong(set.mMinTimestamp);
+            dos.writeLong(set.mMaxTimestamp);
+            for (int i = 0; i < numItems; ++i) {
+                MediaItem item = items.get(i);
+                if (set.mId == LocalDataSource.CAMERA_BUCKET_ID || set.mId == LocalDataSource.DOWNLOAD_BUCKET_ID) {
+                    // Reverse the display order for the camera bucket - want the latest first.
+                    item = items.get(numItems - i - 1);
+                }
+                dos.writeLong(item.mId);
+                Utils.writeUTF(dos, item.mCaption);
+                Utils.writeUTF(dos, item.mMimeType);
+                dos.writeInt(item.getMediaType());
+                dos.writeDouble(item.mLatitude);
+                dos.writeDouble(item.mLongitude);
+                dos.writeLong(item.mDateTakenInMs);
+                dos.writeBoolean(item.mTriedRetrievingExifDateTaken);
+                dos.writeLong(item.mDateAddedInSec);
+                dos.writeLong(item.mDateModifiedInSec);
+                dos.writeInt(item.mDurationInSec);
+                dos.writeInt((int) item.mRotation);
+                Utils.writeUTF(dos, item.mFilePath);
+            }
+            dos.flush();
+            sAlbumCache.put(set.mId, bos.toByteArray());
+            dos.close();
+        } catch (IOException e) {
+            Log.e(TAG, "Error writing to diskcache for set " + set.mName);
+            sAlbumCache.deleteAll();
+            putLocaleForAlbumCache(Locale.getDefault());
+        }
+    }
+
+    private static final MediaSet findSet(long id, LongSparseArray<MediaSet> acceleratedTable) {
+        // This is the accelerated lookup table for the MediaSet based on set id.
+        return acceleratedTable.get(id);
+    }
+}
diff --git a/src/com/cooliris/media/AdaptiveBackgroundTexture.java b/src/com/cooliris/media/AdaptiveBackgroundTexture.java
new file mode 100644
index 0000000..47f38cf
--- /dev/null
+++ b/src/com/cooliris/media/AdaptiveBackgroundTexture.java
@@ -0,0 +1,171 @@
+package com.cooliris.media;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.LightingColorFilter;
+import android.graphics.Paint;
+
+// CR: class comment
+public final class AdaptiveBackgroundTexture extends Texture {
+    private static final int RED_MASK = 0xff0000;
+    private static final int RED_MASK_SHIFT = 16;
+    private static final int GREEN_MASK = 0x00ff00;
+    private static final int GREEN_MASK_SHIFT = 8;
+    private static final int BLUE_MASK = 0x0000ff;
+    private static final int RADIUS = 4;
+    private static final int KERNEL_SIZE = RADIUS * 2 + 1;
+    private static final int NUM_COLORS = 256;
+    private static final int MAX_COLOR_VALUE = NUM_COLORS - 1;
+    private static final int[] KERNEL_NORM = new int[KERNEL_SIZE * NUM_COLORS];
+    private static final int MULTIPLY_COLOR = 0xffaaaaaa;
+    private static final int START_FADE_X = 96;
+    private static final int THUMBNAIL_MAX_X = 128;
+
+    private final int mWidth;
+    private final int mHeight;
+    private final Bitmap mSource;
+    private Texture mBaseTexture;
+
+    static {
+        // Build a lookup table from summed to normalized kernel values.
+        for (int i = KERNEL_SIZE * NUM_COLORS - 1; i >= 0; --i) {
+            KERNEL_NORM[i] = i / KERNEL_SIZE;
+        }
+    }
+
+    public AdaptiveBackgroundTexture(Bitmap source, int width, int height) {
+        mSource = source;
+        mWidth = width;
+        mHeight = height;
+        mBaseTexture = null;
+    }
+
+    public AdaptiveBackgroundTexture(Texture texture, int width, int height) {
+        mBaseTexture = texture;
+        mSource = null;
+        mWidth = width;
+        mHeight = height;
+    }
+
+    @Override
+    protected boolean shouldQueue() {
+        return true;
+    }
+    
+    @Override
+    public boolean isCached() {
+        return true;
+    }
+
+    @Override
+    protected Bitmap load(RenderView view) {
+        // Determine a crop rectangle for the source image that is the aspect ratio of the destination.
+        Bitmap source = mSource;
+        if (source == null) {
+            if (mBaseTexture != null) {
+                source = mBaseTexture.load(view);
+                if (source == null) {
+                	return null;
+                }
+            } else {
+                return null;
+            }
+        }
+        source = Utils.resizeBitmap(source, THUMBNAIL_MAX_X);
+        int sourceWidth = source.getWidth();
+        int sourceHeight = source.getHeight();
+        int destWidth = mWidth;
+        int destHeight = mHeight;
+        float fitX = (float) sourceWidth / destWidth;  // CR: cast destWidth to float as well to be clean; no space after the cast.
+        float fitY = (float) sourceHeight / destHeight;
+        float scale;
+        int cropX;
+        int cropY;
+        int cropWidth;
+        int cropHeight;
+        if (fitX < fitY) {
+            // Full width, partial height.
+            cropWidth = sourceWidth;
+            cropHeight = (int) (destHeight * fitX);  // CR: no space after the cast.
+            cropX = 0;
+            cropY = (sourceHeight - cropHeight) / 2;
+            scale = 1f / fitX;  // CR: i think 1.0f is clearer than 1f.
+        } else {
+            // Full height, partial or full width.
+            cropWidth = (int) (destHeight * fitY);
+            cropHeight = sourceHeight;
+            cropX = (sourceWidth - cropWidth) / 2;
+            cropY = 0;
+            scale = 1f / fitY;
+        }
+
+        // Create a source and destination buffer for the image.
+        int numPixels = cropWidth * cropHeight;
+        int[] in = new int[numPixels];
+        int[] tmp = new int[numPixels];
+
+        // Get the source pixels as 32-bit ARGB.
+        source.getPixels(in, 0, cropWidth, cropX, cropY, cropWidth, cropHeight);
+
+        // Box blur is a separable kernel, so it is decomposed into a horizontal and vertical pass.
+        // The filter function applies the kernel across each row and transposes the output.
+        // Hence we apply it twice to provide efficient horizontal and vertical convolution.
+        // The filter discards the alpha channel.
+        boxBlurFilter(in, tmp, cropWidth, cropHeight, cropWidth);
+        boxBlurFilter(tmp, in, cropHeight, cropWidth, START_FADE_X);
+
+        // Return a bitmap scaled to the desired size.
+        Bitmap filtered = Bitmap.createBitmap(in, cropWidth, cropHeight, Bitmap.Config.ARGB_8888);
+
+        // Composite the bitmap scaled to the target size and darken the pixels.
+        Bitmap output = Bitmap.createBitmap(destWidth, destHeight, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(output);
+        Paint paint = new Paint();
+        paint.setFilterBitmap(true);
+        paint.setDither(true);
+        paint.setColorFilter(new LightingColorFilter(MULTIPLY_COLOR, 0));
+        canvas.scale(scale, scale);
+        canvas.drawBitmap(filtered, 0f, 0f, paint);
+        filtered.recycle();
+        
+        // Clear the texture
+        mBaseTexture = null;
+        return output;
+    }
+
+    private static void boxBlurFilter(int[] in, int[] out, int width, int height, int startFadeX) {
+        int inPos = 0;
+        int maxX = width - 1;
+        for (int y = 0; y < height; ++y) {
+            // Evaluate the kernel for the first pixel in the row.
+            int red = 0;
+            int green = 0;
+            int blue = 0;
+            for (int i = -RADIUS; i <= RADIUS; ++i) {
+                int argb = in[inPos + FloatUtils.clamp(i, 0, maxX)];
+                red += (argb & RED_MASK) >> RED_MASK_SHIFT;
+                green += (argb & GREEN_MASK) >> GREEN_MASK_SHIFT;
+                blue += argb & BLUE_MASK;
+            }
+            // Compute the alpha value.
+            int alpha = (y < startFadeX) ? 0xff : ((height - y - 1) * MAX_COLOR_VALUE / (height - startFadeX));
+            // Compute output values for the row.
+            int outPos = y;
+            for (int x = 0; x != width; ++x) {  // CR: x < width
+                // Output the current pixel.
+                out[outPos] = (alpha << 24) | (KERNEL_NORM[red] << RED_MASK_SHIFT) | (KERNEL_NORM[green] << GREEN_MASK_SHIFT) | KERNEL_NORM[blue];
+                // Slide to the next pixel, adding the new rightmost pixel and
+                // subtracting the former leftmost.
+                int prevX = FloatUtils.clamp(x - RADIUS, 0, maxX);
+                int nextX = FloatUtils.clamp(x + RADIUS + 1, 0, maxX);
+                int prevArgb = in[inPos + prevX];
+                int nextArgb = in[inPos + nextX];
+                red += ((nextArgb & RED_MASK) - (prevArgb & RED_MASK)) >> RED_MASK_SHIFT;
+                green += ((nextArgb & GREEN_MASK) - (prevArgb & GREEN_MASK)) >> GREEN_MASK_SHIFT;
+                blue += (nextArgb & BLUE_MASK) - (prevArgb & BLUE_MASK);
+                outPos += height;
+            }
+            inPos += width;
+        }
+    }
+}
diff --git a/src/com/cooliris/media/ArrayUtils.java b/src/com/cooliris/media/ArrayUtils.java
new file mode 100644
index 0000000..7c1305c
--- /dev/null
+++ b/src/com/cooliris/media/ArrayUtils.java
@@ -0,0 +1,68 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public final class ArrayUtils {
+    public static final void computeSortedIntersection(ArrayList<MediaItem> firstList, final ArrayList<MediaItem> secondList, int maxSize,
+            ArrayList<MediaItem> intersectionList, MediaItem[] hash) {
+        // Assumes that firstList is generally larger than the second list.
+        // Build a simple filter to speed up containment testing.
+        int mask = hash.length - 1;
+        int numItemsToHash = Math.min(secondList.size(), 2 * hash.length);
+        for (int i = 0; i < numItemsToHash; ++i) {
+            MediaItem item = secondList.get(i);
+            if (item != null) {
+                hash[item.hashCode() & mask] = item;
+            }
+        }
+
+        // Build the intersection array.
+        int firstListSize = firstList.size();
+        for (int i = 0; i < firstListSize; ++i) {
+            MediaItem firstListItem = firstList.get(i);
+            MediaItem hashItem = hash[firstListItem.hashCode() & mask];
+            if (hashItem != null && ((hashItem.mId != Shared.INVALID && hashItem.mId == firstListItem.mId) || contains(secondList, firstListItem))) {
+                intersectionList.add(firstListItem);
+                if (--maxSize == 0) {
+                    break;
+                }
+            }
+        }
+
+        // Clear the hash table.
+        Arrays.fill(hash, null);
+    }
+
+    public static final boolean contains(Object[] array, Object object) {
+        if (object == null) {
+            return false;
+        }
+        int length = array.length;
+        for (int i = 0; i < length; ++i) {
+            if (object.equals(array[i])) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+	public static void clear(Object[] array) {
+		int length = array.length;
+		for (int i = 0; i < length; i++) {
+			array[i] = null;
+		}
+	}
+
+    public static final boolean contains(ArrayList<MediaItem> items, MediaItem item) {
+        final int numItems = items.size();
+        if (item.mId == Shared.INVALID)
+            return false;
+        for (int i = 0; i < numItems; ++i) {
+            MediaItem thisItem = items.get(i);
+            if (item.mId == thisItem.mId)
+                return true;
+        }
+        return false;
+    }
+}
diff --git a/src/com/cooliris/media/BackgroundLayer.java b/src/com/cooliris/media/BackgroundLayer.java
new file mode 100644
index 0000000..e325b0d
--- /dev/null
+++ b/src/com/cooliris/media/BackgroundLayer.java
@@ -0,0 +1,130 @@
+package com.cooliris.media;
+
+import java.util.HashMap;
+import javax.microedition.khronos.opengles.GL11;
+import android.util.Log;
+import com.cooliris.media.RenderView.Lists;
+
+public class BackgroundLayer extends Layer {
+    private static final String TAG = "AdaptiveBackground";
+    private final GridLayer mGridLayer;
+    private CrossFadingTexture mBackground;
+    private static final int MAX_ADAPTIVES_TO_KEEP_IN_MEMORY = 8;
+
+    private final HashMap<Texture, AdaptiveBackgroundTexture> mCacheAdaptiveTexture = new HashMap<Texture, AdaptiveBackgroundTexture>();
+    private int mCount;
+    private int mBackgroundBlitWidth;
+    private int mBackgroundOverlap;
+    private Texture mFallbackBackground = null;
+    private static final float Z_FAR_PLANE = 0.9999f;
+    private static final float PARALLAX = 0.5f;
+    private static final int ADAPTIVE_BACKGROUND_WIDTH = 256;
+    private static final int ADAPTIVE_BACKGROUND_HEIGHT = 128;
+
+    BackgroundLayer(GridLayer layer) {
+        mGridLayer = layer;
+    }
+
+    @Override
+    public void generate(RenderView view, Lists lists) {
+        lists.blendedList.add(this);
+        lists.updateList.add(this);
+        lists.opaqueList.add(this);
+    }
+
+    @Override
+    public boolean update(RenderView view, float frameInterval) {
+        if (mFallbackBackground == null || !mFallbackBackground.isLoaded())
+            return false;
+        if (mBackground == null) {
+            mBackground = new CrossFadingTexture(mFallbackBackground);
+        }
+        final boolean retVal = mBackground.update(frameInterval);
+        int cameraPosition = (int) mGridLayer.getScrollPosition();
+        final int backgroundSpacing = mBackgroundBlitWidth - mBackgroundOverlap;
+        cameraPosition = (int) ((cameraPosition / backgroundSpacing) * backgroundSpacing);
+        final DisplayItem displayItem = mGridLayer.getDisplayItemForScrollPosition(cameraPosition);
+        if (displayItem != null) {
+            mBackground.setTexture(getAdaptive(view, displayItem));
+        }
+        return retVal;
+    }
+
+    private Texture getAdaptive(RenderView view, DisplayItem item) {
+        if (item == null) {
+            return mFallbackBackground;
+        }
+        Texture itemThumbnail = item.getThumbnailImage(view.getContext(), null);
+        if (item == null || itemThumbnail == null || !itemThumbnail.isLoaded()) {
+            return mFallbackBackground;
+        }
+        HashMap<Texture, AdaptiveBackgroundTexture> adaptives = mCacheAdaptiveTexture;
+        AdaptiveBackgroundTexture retVal = adaptives.get(itemThumbnail);
+        if (retVal == null) {
+            retVal = new AdaptiveBackgroundTexture(itemThumbnail, ADAPTIVE_BACKGROUND_WIDTH, ADAPTIVE_BACKGROUND_HEIGHT);
+            if (mCount == MAX_ADAPTIVES_TO_KEEP_IN_MEMORY) {
+                mCount = 0;
+                adaptives.clear();
+                Log.i(TAG, "Clearing unused adaptive backgrounds.");
+            }
+            ++mCount;
+            adaptives.put(itemThumbnail, retVal);
+        }
+        return retVal;
+    }
+
+    @Override
+    public void renderOpaque(RenderView view, GL11 gl) {
+        gl.glClear(GL11.GL_COLOR_BUFFER_BIT);
+        mFallbackBackground = view.getResource(R.drawable.default_background, false);
+        view.loadTexture(mFallbackBackground);
+    }
+
+    @Override
+    public void renderBlended(RenderView view, GL11 gl) {
+        if (mBackground == null)
+            return;
+        gl.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
+        CrossFadingTexture anchorTexture = mBackground;
+        boolean bind = anchorTexture.bind(view, gl);
+        if (!bind) {
+            view.bind(mFallbackBackground);
+        }
+        
+        // We stitch this crossfading texture, and to cover all cases of overlap we need to perform 3 draws.
+        int cameraPosition = (int) (mGridLayer.getScrollPosition() * PARALLAX);
+        int backgroundSpacing = mBackgroundBlitWidth - mBackgroundOverlap;
+        int anchorEdge = -cameraPosition % (backgroundSpacing);
+        int rightEdge = anchorEdge + backgroundSpacing;
+        
+        view.draw2D(rightEdge, 0, Z_FAR_PLANE, mBackgroundBlitWidth, mHeight);
+
+        view.draw2D(anchorEdge, 0, Z_FAR_PLANE, mBackgroundBlitWidth, mHeight);
+
+        int leftEdge = anchorEdge - backgroundSpacing;
+        view.draw2D(leftEdge, 0, Z_FAR_PLANE, mBackgroundBlitWidth, mHeight);
+        
+        if (bind) {
+            anchorTexture.unbind(view, gl);
+        }
+        
+        gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);
+        gl.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
+    }
+
+    @Override
+    protected void onSizeChanged() {
+        mBackgroundBlitWidth = (int) (mWidth * 1.5f);
+        mBackgroundOverlap = (int) (mBackgroundBlitWidth * 0.25f);
+    }
+
+    public void clear() {
+        clearCache();
+        mBackground = null;
+    }
+
+    public void clearCache() {
+        mCacheAdaptiveTexture.clear();
+    }
+}
diff --git a/src/com/cooliris/media/BaseCancelable.java b/src/com/cooliris/media/BaseCancelable.java
new file mode 100644
index 0000000..9741ec9
--- /dev/null
+++ b/src/com/cooliris/media/BaseCancelable.java
@@ -0,0 +1,136 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 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.
+ */
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * An abstract class for the interface <code>Cancelable</code>. Subclass can
+ * simply override the <code>execute()</code> function to provide an
+ * implementation of <code>Cancelable</code>.
+ */
+public abstract class BaseCancelable<T> implements Cancelable<T> {
+
+    /**
+     * The state of the task, possible transitions are:
+     * <pre>
+     *     INITIAL -> CANCELED
+     *     EXECUTING -> COMPLETE, CANCELING, ERROR, CANCELED
+     *     CANCELING -> CANCELED
+     * </pre>
+     * When the task stop, it must be end with one of the following states:
+     * COMPLETE, CANCELED, or ERROR;
+     */
+    private static final int STATE_INITIAL = (1 << 0);
+    private static final int STATE_EXECUTING = (1 << 1);
+    private static final int STATE_CANCELING = (1 << 2);
+    private static final int STATE_CANCELED = (1 << 3);
+    private static final int STATE_ERROR = (1 << 4);
+    private static final int STATE_COMPLETE = (1 << 5);
+
+    private int mState = STATE_INITIAL;
+
+    private Throwable mError;
+    private T mResult;
+    private Cancelable<?> mCurrentTask;
+
+    protected abstract T execute() throws Exception;
+
+    /**
+     * Frees the result (which is not null) when the task has been canceled.
+     */
+    protected void freeCanceledResult(T result) {
+        // Do nothing by default;
+    }
+
+    private boolean isInStates(int states) {
+        return (states & mState) != 0;
+    }
+
+    private T handleTerminalStates() throws ExecutionException {
+        if (mState == STATE_CANCELED) {
+            throw new CancellationException();
+        }
+        if (mState == STATE_ERROR) {
+            throw new ExecutionException(mError);
+        }
+        if (mState == STATE_COMPLETE) return mResult;
+        throw new IllegalStateException();
+    }
+
+    public synchronized void await() throws InterruptedException {
+        while (!isInStates(STATE_COMPLETE | STATE_CANCELED | STATE_ERROR)) {
+            wait();
+        }
+    }
+
+    public final T get() throws InterruptedException, ExecutionException {
+        synchronized (this) {
+            if (mState != STATE_INITIAL) {
+                await();
+                return handleTerminalStates();
+            }
+            mState = STATE_EXECUTING;
+        }
+        try {
+            mResult = execute();
+        } catch (CancellationException e) {
+            mState = STATE_CANCELED;
+        } catch (InterruptedException e) {
+            mState = STATE_CANCELED;
+        } catch (Throwable error) {
+            synchronized (this) {
+                if (mState != STATE_CANCELING) {
+                    mError = error;
+                    mState = STATE_ERROR;
+                }
+            }
+        }
+        synchronized (this) {
+            if (mState == STATE_CANCELING) mState = STATE_CANCELED;
+            if (mState == STATE_EXECUTING) mState = STATE_COMPLETE;
+            notifyAll();
+            if (mState == STATE_CANCELED && mResult != null) {
+                freeCanceledResult(mResult);
+            }
+            return handleTerminalStates();
+        }
+    }
+
+    /**
+     * Requests the task to be canceled.
+     *
+     * @return true if the task is running and has not been canceled; false
+     *     otherwise
+     */
+
+    public synchronized boolean requestCancel() {
+        if (mState == STATE_INITIAL) {
+            mState = STATE_CANCELED;
+            notifyAll();
+            return false;
+        }
+        if (mState == STATE_EXECUTING) {
+            if (mCurrentTask != null) mCurrentTask.requestCancel();
+            mState = STATE_CANCELING;
+            return true;
+        }
+        return false;
+    }
+}
+
diff --git a/src/com/cooliris/media/BitmapManager.java b/src/com/cooliris/media/BitmapManager.java
new file mode 100644
index 0000000..4b7d77c
--- /dev/null
+++ b/src/com/cooliris/media/BitmapManager.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.cooliris.media;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.util.Iterator;
+import java.util.WeakHashMap;
+
+/**
+ * This class provides several utilities to cancel bitmap decoding.
+ *
+ * The function decodeFileDescriptor() is used to decode a bitmap. During
+ * decoding if another thread wants to cancel it, it calls the function
+ * cancelThreadDecoding() specifying the Thread which is in decoding.
+ *
+ * cancelThreadDecoding() is sticky until allowThreadDecoding() is called.
+ *
+ * You can also cancel decoding for a set of threads using ThreadSet as
+ * the parameter for cancelThreadDecoding. To put a thread into a ThreadSet,
+ * use the add() method. A ThreadSet holds (weak) references to the threads,
+ * so you don't need to remove Thread from it if some thread dies.
+ */
+public class BitmapManager {
+    private static final String TAG = "BitmapManager";
+    private static enum State {CANCEL, ALLOW}
+    private static class ThreadStatus {
+        public State mState = State.ALLOW;
+        public BitmapFactory.Options mOptions;
+
+        @Override
+        public String toString() {
+            String s;
+            if (mState == State.CANCEL) {
+                s = "Cancel";
+            } else if (mState == State.ALLOW) {
+                s = "Allow";
+            } else {
+                s = "?";
+            }
+            s = "thread state = " + s + ", options = " + mOptions;
+            return s;
+        }
+    }
+
+    public static class ThreadSet implements Iterable<Thread> {
+        private final WeakHashMap<Thread, Object> mWeakCollection =
+                new WeakHashMap<Thread, Object>();
+
+        public void add(Thread t) {
+            mWeakCollection.put(t, null);
+        }
+        public void remove(Thread t) {
+            mWeakCollection.remove(t);
+        }
+        public Iterator<Thread> iterator() {
+            return mWeakCollection.keySet().iterator();
+        }
+    }
+
+    private final WeakHashMap<Thread, ThreadStatus> mThreadStatus =
+            new WeakHashMap<Thread, ThreadStatus>();
+
+    private static BitmapManager sManager = null;
+
+    private BitmapManager() {
+    }
+
+    /**
+     * Get thread status and create one if specified.
+     */
+    private synchronized ThreadStatus getOrCreateThreadStatus(Thread t) {
+        ThreadStatus status = mThreadStatus.get(t);
+        if (status == null) {
+            status = new ThreadStatus();
+            mThreadStatus.put(t, status);
+        }
+        return status;
+    }
+
+    /**
+     * The following three methods are used to keep track of
+     * BitmapFaction.Options used for decoding and cancelling.
+     */
+    private synchronized void setDecodingOptions(Thread t,
+            BitmapFactory.Options options) {
+        getOrCreateThreadStatus(t).mOptions = options;
+    }
+
+    synchronized void removeDecodingOptions(Thread t) {
+        ThreadStatus status = mThreadStatus.get(t);
+        status.mOptions = null;
+    }
+
+    /**
+     * The following two methods are used to allow/cancel a set of threads
+     * for bitmap decoding.
+     */
+    public synchronized void allowThreadDecoding(ThreadSet threads) {
+        for (Thread t : threads) {
+            allowThreadDecoding(t);
+        }
+    }
+
+    public synchronized void cancelThreadDecoding(ThreadSet threads) {
+        for (Thread t : threads) {
+            cancelThreadDecoding(t);
+        }
+    }
+
+    /**
+     * The following three methods are used to keep track of which thread
+     * is being disabled for bitmap decoding.
+     */
+    public synchronized boolean canThreadDecoding(Thread t) {
+        ThreadStatus status = mThreadStatus.get(t);
+        if (status == null) {
+            // allow decoding by default
+            return true;
+        }
+
+        // CR: return this shit directly; no need for 'result'.
+        boolean result = (status.mState != State.CANCEL);
+        return result;
+    }
+
+    public synchronized void allowThreadDecoding(Thread t) {
+        getOrCreateThreadStatus(t).mState = State.ALLOW;
+    }
+
+    public synchronized void cancelThreadDecoding(Thread t) {
+        ThreadStatus status = getOrCreateThreadStatus(t);
+        status.mState = State.CANCEL;
+        if (status.mOptions != null) {
+            status.mOptions.requestCancelDecode();
+        }
+
+        // Wake up threads in waiting list
+        notifyAll();
+    }
+
+    public static synchronized BitmapManager instance() {
+        if (sManager == null) {
+            sManager = new BitmapManager();
+        }
+        return sManager;
+    }
+
+    /**
+     * The real place to delegate bitmap decoding to BitmapFactory.
+     */
+    public Bitmap decodeFileDescriptor(FileDescriptor fd,
+                                       BitmapFactory.Options options) {
+        if (options.mCancel) {
+            return null;
+        }
+
+        Thread thread = Thread.currentThread();
+        if (!canThreadDecoding(thread)) {
+            Log.d(TAG, "Thread " + thread + " is not allowed to decode.");
+            return null;
+        }
+
+        setDecodingOptions(thread, options);
+        Bitmap b = BitmapFactory.decodeFileDescriptor(fd, null, options);
+
+        removeDecodingOptions(thread);
+        return b;
+    }
+}
diff --git a/src/com/cooliris/media/BitmapTexture.java b/src/com/cooliris/media/BitmapTexture.java
new file mode 100644
index 0000000..a973c18
--- /dev/null
+++ b/src/com/cooliris/media/BitmapTexture.java
@@ -0,0 +1,17 @@
+package com.cooliris.media;
+
+import android.graphics.Bitmap;
+
+public class BitmapTexture extends Texture {
+    // A simple flexible texture class that enables a Texture from a bitmap.
+    final Bitmap mBitmap;
+    BitmapTexture(Bitmap bitmap) {
+        mBitmap = bitmap;
+    }
+    
+    @Override
+    protected Bitmap load(RenderView view) {
+        return mBitmap;
+    }
+
+}
diff --git a/src/com/cooliris/media/Cancelable.java b/src/com/cooliris/media/Cancelable.java
new file mode 100644
index 0000000..42c61ae
--- /dev/null
+++ b/src/com/cooliris/media/Cancelable.java
@@ -0,0 +1,55 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 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.
+ */
+
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * The interface for all the tasks that could be canceled.
+ */
+public interface Cancelable<T> {
+    /*
+     * Requests this <code>Cancelable</code> to be canceled. This function will
+     * return <code>true</code> if and only if the task is originally running
+     * and now begin requested for cancel.
+     *
+     * If subclass need to do more things to cancel the task. It can override
+     * the code like this:
+     * <pre>
+     *     @Override
+     *     public boolean requestCancel() {
+     *         if (super.requestCancel()) {
+     *             // do necessary work to cancel the task
+     *             return true;
+     *         }
+     *         return false;
+     *     }
+     * </pre>
+     */
+    public boolean requestCancel();
+
+    public void await() throws InterruptedException;
+
+    /**
+     * Gets the results of this <code>Cancelable</code> task.
+     *
+     * @throws ExecutionException if exception is thrown during the execution of
+     *         the task
+     */
+    public T get() throws InterruptedException, ExecutionException;
+}
\ No newline at end of file
diff --git a/src/com/cooliris/media/CanvasLayer.java b/src/com/cooliris/media/CanvasLayer.java
new file mode 100644
index 0000000..d01c4da
--- /dev/null
+++ b/src/com/cooliris/media/CanvasLayer.java
@@ -0,0 +1,127 @@
+package com.cooliris.media;
+
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.opengl.GLUtils;
+
+public abstract class CanvasLayer extends Layer {
+    private int mTextureId;
+    private int mTextureWidth;
+    private int mTextureHeight;
+    private float mNormalizedWidth;
+    private float mNormalizedHeight;
+
+    private final Canvas mCanvas = new Canvas();
+    private final Bitmap.Config mBitmapConfig;
+    private Bitmap mBitmap = null;
+    private boolean mNeedsDraw = false;
+    private boolean mNeedsResize = false;
+
+    public CanvasLayer(Bitmap.Config bitmapConfig) {
+        mBitmapConfig = bitmapConfig;
+    }
+
+    public void setNeedsDraw() {
+        mNeedsDraw = true;
+    }
+
+    public final float getNormalizedWidth() {
+        return mNormalizedWidth;
+    }
+
+    public final float getNormalizedHeight() {
+        return mNormalizedHeight;
+    }
+
+    @Override
+    protected void onSizeChanged() {
+        mNeedsResize = true;
+    }
+
+    @Override
+    protected void onSurfaceCreated(RenderView view, GL11 gl) {
+        mTextureId = 0;
+    }
+
+    protected boolean bind(GL11 gl) {
+        int width = (int) mWidth;
+        int height = (int) mHeight;
+        int textureId = mTextureId;
+        int textureWidth = mTextureWidth;
+        int textureHeight = mTextureHeight;
+        Canvas canvas = mCanvas;
+        Bitmap bitmap = mBitmap;
+        boolean updateSubTexture = true;
+
+        if (mNeedsResize) {
+            // Clear the resize flag and mark as needing draw.
+            mNeedsResize = false;
+            mNeedsDraw = true;
+
+            // Compute the power-of-2 padded size for the texture.
+            int newTextureWidth = Shared.nextPowerOf2(width);
+            int newTextureHeight = Shared.nextPowerOf2(height);
+
+            // Reallocate the bitmap only if the padded size has changed.
+            if (textureWidth != newTextureWidth || textureHeight != newTextureHeight) {
+                // Mark that the partial texture update should not be used.
+                updateSubTexture = false;
+
+                // Allocate a texture if needed.
+                if (textureId == 0) {
+                    int[] textureIdOut = new int[1];
+                    gl.glGenTextures(1, textureIdOut, 0);
+                    textureId = textureIdOut[0];
+                    mTextureId = textureId;
+
+                    // Set texture parameters.
+                    gl.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
+                    gl.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
+                    gl.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
+                    gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+                    gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+                }
+
+                // Set the new texture width and height.
+                textureWidth = newTextureWidth;
+                textureHeight = newTextureHeight;
+                mTextureWidth = newTextureWidth;
+                mTextureHeight = newTextureHeight;
+                mNormalizedWidth = (float) width / textureWidth;
+                mNormalizedHeight = (float) height / textureHeight;
+
+                // Recycle the existing bitmap and create a new one.
+                if (bitmap != null)
+                    bitmap.recycle();
+                bitmap = Bitmap.createBitmap(textureWidth, textureHeight, mBitmapConfig);
+                canvas.setBitmap(bitmap);
+                mBitmap = bitmap;
+            }
+        }
+
+        // Bind the texture to the context.
+        if (textureId == 0)
+            return false;
+        gl.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
+
+        // Redraw the contents of the texture if needed.
+        if (mNeedsDraw) {
+            mNeedsDraw = false;
+            draw(canvas, bitmap, width, height);
+            if (updateSubTexture) {
+                GLUtils.texSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, bitmap);
+            } else {
+                int[] cropRect = { 0, height, width, -height };
+                gl.glTexParameteriv(GL11.GL_TEXTURE_2D, GL11Ext.GL_TEXTURE_CROP_RECT_OES, cropRect, 0);
+                GLUtils.texImage2D(GL11.GL_TEXTURE_2D, 0, bitmap, 0);
+            }
+        }
+
+        return true;
+    }
+
+    protected abstract void draw(Canvas canvas, Bitmap backing, int width, int height);
+}
diff --git a/src/com/cooliris/media/CanvasTexture.java b/src/com/cooliris/media/CanvasTexture.java
new file mode 100644
index 0000000..27baaa4
--- /dev/null
+++ b/src/com/cooliris/media/CanvasTexture.java
@@ -0,0 +1,174 @@
+package com.cooliris.media;
+
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.opengl.GLUtils;
+
+public abstract class CanvasTexture {
+    private int mWidth;
+    private int mHeight;
+    private int mTextureId;
+    private int mTextureWidth;
+    private int mTextureHeight;
+    private float mNormalizedWidth;
+    private float mNormalizedHeight;
+
+    private final Canvas mCanvas = new Canvas();
+    private final Bitmap.Config mBitmapConfig;
+    private Bitmap mBitmap = null;
+    private boolean mNeedsDraw = false;
+    private boolean mNeedsResize = false;
+
+    public CanvasTexture(Bitmap.Config bitmapConfig) {
+        mBitmapConfig = bitmapConfig;
+    }
+
+    public final void setNeedsDraw() {
+        mNeedsDraw = true;
+    }
+
+    public final int getWidth() {
+        return mWidth;
+    }
+
+    public final int getHeight() {
+        return mHeight;
+    }
+
+    public final float getNormalizedWidth() {
+        return mNormalizedWidth;
+    }
+
+    public final float getNormalizedHeight() {
+        return mNormalizedHeight;
+    }
+
+    public final void setSize(int width, int height) {
+        mWidth = width;
+        mHeight = height;
+        mNeedsResize = true;
+        mTextureWidth = -1;
+        mTextureHeight = -1;
+        onSizeChanged();
+    }
+
+    public void resetTexture() {
+        // Happens when restoring the scene. Need to manage this more automatically.
+        mTextureId = 0;
+        mNeedsResize = true;
+    }
+
+    // This code seems largely a dup of CanvasLayer.
+    public boolean bind(GL11 gl) {
+        int width = (int) mWidth;
+        int height = (int) mHeight;
+        int textureId = mTextureId;
+        int textureWidth = mTextureWidth;
+        int textureHeight = mTextureHeight;
+        Canvas canvas = mCanvas;
+        Bitmap bitmap = mBitmap;
+
+        if (mNeedsResize || mTextureId == 0) {
+            // Clear the resize flag and mark as needing draw.
+            mNeedsDraw = true;
+
+            // Compute the power-of-2 padded size for the texture.
+            int newTextureWidth = Shared.nextPowerOf2(width);
+            int newTextureHeight = Shared.nextPowerOf2(height);
+
+            // Reallocate the bitmap only if the padded size has changed.
+            // TODO: reuse same texture if it is already large enough, just
+            // change clip rect.
+            if (textureWidth != newTextureWidth || textureHeight != newTextureHeight || mTextureId == 0) {
+                // Allocate a texture if needed.
+                if (textureId == 0) {
+                    int[] textureIdOut = new int[1];
+                    gl.glGenTextures(1, textureIdOut, 0);
+                    textureId = textureIdOut[0];
+                    mNeedsResize = false;
+                    mTextureId = textureId;
+
+                    // Set texture parameters.
+                    gl.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
+                    gl.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
+                    gl.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
+                    gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+                    gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+                }
+
+                // Set the new texture width and height.
+                textureWidth = newTextureWidth;
+                textureHeight = newTextureHeight;
+                mTextureWidth = newTextureWidth;
+                mTextureHeight = newTextureHeight;
+                mNormalizedWidth = (float) width / textureWidth;
+                mNormalizedHeight = (float) height / textureHeight;
+
+                // Recycle the existing bitmap and create a new one.
+                if (bitmap != null)
+                    bitmap.recycle();
+                bitmap = Bitmap.createBitmap(textureWidth, textureHeight, mBitmapConfig);
+                canvas.setBitmap(bitmap);
+                mBitmap = bitmap;
+            }
+        }
+
+        // Bind the texture to the context.
+        if (textureId == 0) {
+            return false;
+        }
+        gl.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
+
+        // Redraw the contents of the texture if needed.
+        if (mNeedsDraw) {
+            mNeedsDraw = false;
+            renderCanvas(canvas, bitmap, width, height);
+            int[] cropRect = { 0, height, width, -height };
+            gl.glTexParameteriv(GL11.GL_TEXTURE_2D, GL11Ext.GL_TEXTURE_CROP_RECT_OES, cropRect, 0);
+            GLUtils.texImage2D(GL11.GL_TEXTURE_2D, 0, bitmap, 0);
+
+        }
+
+        return true;
+    }
+
+    public void draw(RenderView view, GL11 gl, int x, int y) {
+        if (bind(gl)) {
+            view.draw2D(x, y, 0, mWidth, mHeight);
+        }
+    }
+
+    public void drawWithEffect(RenderView view, GL11 gl, float x, float y, float anchorX, float anchorY, float alpha, float scale) {
+        if (bind(gl)) {
+            float width = mWidth;
+            float height = mHeight;
+
+            // Apply scale transform if not identity.
+            if (scale != 1) {  // CR: 1.0f
+                float originX = x + anchorX * width;
+                float originY = y + anchorY * height;
+                width *= scale;
+                height *= scale;
+                x = originX - anchorX * width;
+                y = originY - anchorY * height;
+            }
+
+            // Set alpha if needed.
+            if (alpha != 1f) {  // CR: 1.0f
+                view.setAlpha(alpha);
+            }
+            view.draw2D(x, y, 0, width, height);
+            if (alpha != 1f) {
+                view.resetColor();
+            }
+        }
+    }
+
+    protected abstract void onSizeChanged();
+
+    protected abstract void renderCanvas(Canvas canvas, Bitmap backing, int width, int height);
+
+} // CR: superfluous newline above.
diff --git a/src/com/cooliris/media/ConcatenatedDataSource.java b/src/com/cooliris/media/ConcatenatedDataSource.java
new file mode 100644
index 0000000..f09b151
--- /dev/null
+++ b/src/com/cooliris/media/ConcatenatedDataSource.java
@@ -0,0 +1,62 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+
+import android.util.Log;
+
+public final class ConcatenatedDataSource implements DataSource {
+    private static final String TAG = "ConcatenatedDataSource";
+    private final DataSource mFirst;
+    private final DataSource mSecond;
+
+    public ConcatenatedDataSource(DataSource first, DataSource second) {
+        mFirst = first;
+        mSecond = second;
+    }
+
+    public void loadMediaSets(final MediaFeed feed) {
+        mFirst.loadMediaSets(feed);
+        mSecond.loadMediaSets(feed);
+    }
+
+    public void loadItemsForSet(final MediaFeed feed, final MediaSet parentSet, int rangeStart, int rangeEnd) {
+        if (parentSet != null) {
+            DataSource dataSource = parentSet.mDataSource;
+            if (dataSource != null) {
+                dataSource.loadItemsForSet(feed, parentSet, rangeStart, rangeEnd);
+            } else {
+                Log.e(TAG, "MediaSet was not added to the feed");
+            }
+        }
+    }
+
+    public boolean performOperation(int operation, final ArrayList<MediaBucket> mediaBuckets, Object data) {
+        ArrayList<MediaBucket> singleBucket = new ArrayList<MediaBucket>(1);
+        singleBucket.add(null);
+        int numBuckets = mediaBuckets.size();
+        boolean retVal = true;
+        for (int i = 0; i < numBuckets; ++i) {  // CR: iterator for
+            MediaBucket bucket = mediaBuckets.get(i);
+            MediaSet set = bucket.mediaSet;
+            if (set != null) {
+	            DataSource dataSource = set.mDataSource;
+	            if (dataSource != null) {
+	                singleBucket.set(0, bucket);
+	                retVal &= dataSource.performOperation(operation, singleBucket, data);
+	            } else {
+	                Log.e(TAG, "MediaSet was not added to the feed");
+	            }
+            }
+        }
+        return retVal;
+    }
+
+    public DiskCache getThumbnailCache() {
+        throw new UnsupportedOperationException("ConcatenatedDataSource should not create MediaItems");
+    }
+    
+    public void shutdown() {
+        mFirst.shutdown();
+        mSecond.shutdown();
+    }
+}
diff --git a/src/com/cooliris/media/ConcurrentPool.java b/src/com/cooliris/media/ConcurrentPool.java
new file mode 100644
index 0000000..b8cd98a
--- /dev/null
+++ b/src/com/cooliris/media/ConcurrentPool.java
@@ -0,0 +1,28 @@
+package com.cooliris.media;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReferenceArray;
+
+public final class ConcurrentPool<E extends Object> {
+    private AtomicReferenceArray<E> mFreeList;
+    private AtomicInteger mFreeListIndex;
+
+    public ConcurrentPool(E[] objects) {
+        mFreeList = new AtomicReferenceArray<E>(objects);
+        mFreeListIndex = new AtomicInteger(objects.length);
+    }
+
+    public E create() {
+        final int index = mFreeListIndex.decrementAndGet();
+        E object = mFreeList.get(index);
+        mFreeList.set(index, null);
+        return object;
+    }
+
+    public void delete(E object) {
+        final int index = mFreeListIndex.getAndIncrement();
+        while (!mFreeList.compareAndSet(index, null, object)) {
+            Thread.yield();
+        }
+    }
+}
diff --git a/src/com/cooliris/media/ContextMenu.java b/src/com/cooliris/media/ContextMenu.java
new file mode 100644
index 0000000..967326e
--- /dev/null
+++ b/src/com/cooliris/media/ContextMenu.java
@@ -0,0 +1,27 @@
+//package com.cooliris.media;
+//
+//// Deprecated class. Need to remove from perforce.
+//
+//import javax.microedition.khronos.opengles.GL11;
+//
+//public final class ContextMenu extends Layer {
+//
+//    public void openWithRect(int x, int y, int width, int height) {
+//
+//    }
+//
+//    public void close() {
+//
+//    }
+//
+//    @Override
+//    public void renderBlended(RenderView view, GL11 gl) {
+//        gl.glClear(GL11.GL_COLOR_BUFFER_BIT);
+//    }
+//
+//    @Override
+//    public void generate(RenderView view, RenderView.Lists lists) {
+//        lists.blendedList.add(this);
+//        lists.hitTestList.add(this);
+//    }
+//}
diff --git a/src/com/cooliris/media/CropImage.java b/src/com/cooliris/media/CropImage.java
new file mode 100644
index 0000000..0e06b2a
--- /dev/null
+++ b/src/com/cooliris/media/CropImage.java
@@ -0,0 +1,784 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 2007 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.
+ */
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.media.ExifInterface;
+import android.media.FaceDetector;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * The activity can crop specific region of interest from an image.
+ */
+public class CropImage extends MonitoredActivity {
+    private static final String TAG = "CropImage";
+
+    // These are various options can be specified in the intent.
+    private Bitmap.CompressFormat mOutputFormat = Bitmap.CompressFormat.JPEG; // only used with mSaveUri
+    private Uri mSaveUri = null;
+    private int mAspectX, mAspectY;  // CR: two definitions per line == sad panda.
+    private boolean mDoFaceDetection = true;
+    private boolean mCircleCrop = false;
+    private final Handler mHandler = new Handler();
+
+    // These options specifiy the output image size and whether we should
+    // scale the output to fit it (or just crop it).
+    private int mOutputX, mOutputY;
+    private boolean mScale;
+    private boolean mScaleUp = true;
+
+    boolean mWaitingToPick; // Whether we are wait the user to pick a face.
+    boolean mSaving; // Whether the "save" button is already clicked.
+
+    private CropImageView mImageView;
+    private ContentResolver mContentResolver;
+
+    private Bitmap mBitmap;
+    private MediaItem mItem;
+    private final BitmapManager.ThreadSet mDecodingThreads = new BitmapManager.ThreadSet();
+    HighlightView mCrop;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        mContentResolver = getContentResolver();
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.cropimage);
+
+        mImageView = (CropImageView) findViewById(R.id.image);
+
+        // CR: remove TODO's.
+        // TODO: we may need to show this indicator for the main gallery application
+        // MenuHelper.showStorageToast(this);
+
+        Intent intent = getIntent();
+        Bundle extras = intent.getExtras();
+
+        if (extras != null) {
+            if (extras.getString("circleCrop") != null) {
+                mCircleCrop = true;
+                mAspectX = 1;
+                mAspectY = 1;
+            }
+            mSaveUri = (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT);
+            if (mSaveUri != null) {
+                String outputFormatString = extras.getString("outputFormat");
+                if (outputFormatString != null) {
+                    mOutputFormat = Bitmap.CompressFormat.valueOf(outputFormatString);
+                }
+            }
+            mBitmap = (Bitmap) extras.getParcelable("data");
+            mAspectX = extras.getInt("aspectX");
+            mAspectY = extras.getInt("aspectY");
+            mOutputX = extras.getInt("outputX");
+            mOutputY = extras.getInt("outputY");
+            mScale = extras.getBoolean("scale", true);
+            mScaleUp = extras.getBoolean("scaleUpIfNeeded", true);
+            mDoFaceDetection = extras.containsKey("noFaceDetection") ? !extras.getBoolean("noFaceDetection") : true;
+        }
+
+        if (mBitmap == null) {
+            // Create a MediaItem representing the URI.
+        	Uri target = intent.getData();
+            String targetScheme = target.getScheme();
+            int rotation = 0;
+            
+            if (targetScheme.equals("content")) {
+            	mItem = LocalDataSource.createMediaItemFromUri(this, target);
+            }
+            try {
+            	if (mItem != null) {
+	                mBitmap = UriTexture.createFromUri(this, mItem.mContentUri, 1024, 1024, 0, null);
+	                rotation = (int)mItem.mRotation;
+	            } else {
+	                mBitmap = UriTexture.createFromUri(this, target.toString(), 1024, 1024, 0, null);
+	                if (targetScheme.equals("file")) {
+	                	ExifInterface exif = new ExifInterface(target.getPath());
+	        			rotation = (int)Shared.exifOrientationToDegrees(exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
+	        							ExifInterface.ORIENTATION_NORMAL));
+	                }
+	            }
+            } catch (IOException e) {
+            } catch (URISyntaxException e) {
+            }
+
+			if (mBitmap != null && rotation != 0f) {
+				mBitmap = Util.rotate(mBitmap, rotation);
+			}
+        }
+
+        if (mBitmap == null) {
+            Log.e(TAG, "Cannot load bitmap, exiting.");
+            finish();
+            return;
+        }
+
+        // Make UI fullscreen.
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+        findViewById(R.id.discard).setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                setResult(RESULT_CANCELED);
+                finish();
+            }
+        });
+
+        findViewById(R.id.save).setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                onSaveClicked();
+            }
+        });
+
+        startFaceDetection();
+    }
+
+    private void startFaceDetection() {
+        if (isFinishing()) {
+            return;
+        }
+
+        mImageView.setImageBitmapResetBase(mBitmap, true);
+
+        Util.startBackgroundJob(this, null, getResources().getString(R.string.running_face_detection), new Runnable() {
+            public void run() {
+                final CountDownLatch latch = new CountDownLatch(1);
+                final Bitmap b = mBitmap;
+                mHandler.post(new Runnable() {
+                    public void run() {
+                        if (b != mBitmap && b != null) {
+                            mImageView.setImageBitmapResetBase(b, true);
+                            mBitmap.recycle();
+                            mBitmap = b;
+                        }
+                        if (mImageView.getScale() == 1.0f) {
+                            mImageView.center(true, true);
+                        }
+                        latch.countDown();
+                    }
+                });
+                try {
+                    latch.await();
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+                mRunFaceDetection.run();
+            }
+        }, mHandler);
+    }
+
+    private void onSaveClicked() {
+        // CR: TODO!
+        // TODO this code needs to change to use the decode/crop/encode single
+        // step api so that we don't require that the whole (possibly large)
+        // bitmap doesn't have to be read into memory
+        if (mSaving)
+            return;
+
+        if (mCrop == null) {
+            return;
+        }
+
+        mSaving = true;
+
+        Rect r = mCrop.getCropRect();
+
+        int width = r.width();  // CR: final == happy panda!
+        int height = r.height();
+
+        // If we are circle cropping, we want alpha channel, which is the
+        // third param here.
+        Bitmap croppedImage = Bitmap.createBitmap(width, height, mCircleCrop ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);
+        {
+            Canvas canvas = new Canvas(croppedImage);
+            Rect dstRect = new Rect(0, 0, width, height);
+            canvas.drawBitmap(mBitmap, r, dstRect, null);
+        }
+
+        if (mCircleCrop) {
+            // OK, so what's all this about?
+            // Bitmaps are inherently rectangular but we want to return
+            // something that's basically a circle. So we fill in the
+            // area around the circle with alpha. Note the all important
+            // PortDuff.Mode.CLEAR.
+            Canvas c = new Canvas(croppedImage);
+            Path p = new Path();
+            p.addCircle(width / 2F, height / 2F, width / 2F, Path.Direction.CW);
+            c.clipPath(p, Region.Op.DIFFERENCE);
+            c.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
+        }
+
+        // If the output is required to a specific size then scale or fill.
+        if (mOutputX != 0 && mOutputY != 0) {
+            if (mScale) {
+                // Scale the image to the required dimensions.
+                Bitmap old = croppedImage;
+                croppedImage = Util.transform(new Matrix(), croppedImage, mOutputX, mOutputY, mScaleUp);
+                if (old != croppedImage) {
+                    old.recycle();
+                }
+            } else {
+
+                /*
+                 * Don't scale the image crop it to the size requested. Create an new image with the cropped image in the center and
+                 * the extra space filled.
+                 */
+
+                // Don't scale the image but instead fill it so it's the
+                // required dimension
+                Bitmap b = Bitmap.createBitmap(mOutputX, mOutputY, Bitmap.Config.RGB_565);
+                Canvas canvas = new Canvas(b);
+
+                Rect srcRect = mCrop.getCropRect();
+                Rect dstRect = new Rect(0, 0, mOutputX, mOutputY);
+
+                int dx = (srcRect.width() - dstRect.width()) / 2;
+                int dy = (srcRect.height() - dstRect.height()) / 2;
+
+                // If the srcRect is too big, use the center part of it.
+                srcRect.inset(Math.max(0, dx), Math.max(0, dy));
+
+                // If the dstRect is too big, use the center part of it.
+                dstRect.inset(Math.max(0, -dx), Math.max(0, -dy));
+
+                // Draw the cropped bitmap in the center.
+                canvas.drawBitmap(mBitmap, srcRect, dstRect, null);
+
+                // Set the cropped bitmap as the new bitmap.
+                croppedImage.recycle();
+                croppedImage = b;
+            }
+        }
+
+        // Return the cropped image directly or save it to the specified URI.
+        Bundle myExtras = getIntent().getExtras();
+        if (myExtras != null && (myExtras.getParcelable("data") != null || myExtras.getBoolean("return-data"))) {
+            Bundle extras = new Bundle();
+            extras.putParcelable("data", croppedImage);
+            setResult(RESULT_OK, (new Intent()).setAction("inline-data").putExtras(extras));
+            finish();
+        } else {
+            final Bitmap b = croppedImage;
+            final Runnable save = new Runnable() {
+                public void run() {
+                    saveOutput(b);
+                }
+            };
+            Util.startBackgroundJob(this, null, getResources().getString(R.string.saving_image), save, mHandler);
+        }
+    }
+
+    private void saveOutput(Bitmap croppedImage) {
+        if (mSaveUri != null) {
+            OutputStream outputStream = null;
+            try {
+                outputStream = mContentResolver.openOutputStream(mSaveUri);
+                if (outputStream != null) {
+                    croppedImage.compress(mOutputFormat, 75, outputStream);
+                }
+                // TODO ExifInterface write
+            } catch (IOException ex) {
+                Log.e(TAG, "Cannot open file: " + mSaveUri, ex);
+            } finally {
+                Util.closeSilently(outputStream);
+            }
+            Bundle extras = new Bundle();
+            setResult(RESULT_OK, new Intent(mSaveUri.toString()).putExtras(extras));
+        } else {
+            Bundle extras = new Bundle();
+            extras.putString("rect", mCrop.getCropRect().toString());
+            if (mItem == null) {
+                // CR: Comments should be full sentences.
+                // this image doesn't belong to the local data source
+                // we can add it locally if necessary
+            } else {
+                File oldPath = new File(mItem.mFilePath);
+                File directory = new File(oldPath.getParent());
+
+                int x = 0;
+                String fileName = oldPath.getName();
+                fileName = fileName.substring(0, fileName.lastIndexOf("."));
+
+                // Try file-1.jpg, file-2.jpg, ... until we find a filename which
+                // does not exist yet.
+                while (true) {
+                    x += 1;
+                    String candidate = directory.toString() + "/" + fileName + "-" + x + ".jpg";
+                    boolean exists = (new File(candidate)).exists();
+                    if (!exists) {  // CR: inline the expression for exists here--it's clear enough.
+                        break;
+                    }
+                }
+                try {
+                    MediaItem item = mItem;
+                    String finalFileName = fileName + "-" + x + ".jpg";
+                    // TODO this is going to cause the orientation to reset.
+                    Uri newUri = ImageManager.addImage(mContentResolver, item.mCaption, item.mDateTakenInMs, item.mLatitude,
+                                                       item.mLongitude, 0, directory.toString(), finalFileName);
+                    boolean complete = false;
+                    try {
+                        String[] projection = new String[] { ImageColumns._ID, ImageColumns.MINI_THUMB_MAGIC };
+                        Cursor c = mContentResolver.query(newUri, projection, null, null, null);
+                        try {
+                            c.moveToPosition(0);
+                        } finally {
+                            c.close();
+                        }
+                        ContentValues values = new ContentValues();
+                        values.put(ImageColumns.MINI_THUMB_MAGIC, 0);
+                        mContentResolver.update(newUri, values, null, null);
+                        OutputStream outputStream = null;
+                        try {
+                            outputStream = mContentResolver.openOutputStream(newUri);
+                            if (outputStream != null) {
+                                croppedImage.compress(mOutputFormat, 75, outputStream);
+                            }
+                        } catch (IOException ex) {
+                            // TODO: report error to caller
+                            Log.e(TAG, "Cannot open file: " + newUri, ex);
+                        } finally {
+                            Util.closeSilently(outputStream);
+                        }
+                        complete = true;
+                    } finally {
+                        if (!complete) {
+                            try {
+                                mContentResolver.delete(newUri, null, null);
+                            } catch (Throwable t) {
+                                // Ignore it while clean up.
+                            }
+                        }
+                    }
+                    setResult(RESULT_OK, new Intent().setAction(newUri.toString()).putExtras(extras));
+                } catch (Exception e) {  // CR: e.
+                    // CR: sentences!
+                    Log.e(TAG, "Store image fail, continue anyway", e);
+                }
+            }
+        }
+        croppedImage.recycle();
+        finish();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        BitmapManager.instance().cancelThreadDecoding(mDecodingThreads);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+    }
+
+    Runnable mRunFaceDetection = new Runnable() {
+        float mScale = 1F;
+        Matrix mImageMatrix;
+        FaceDetector.Face[] mFaces = new FaceDetector.Face[3];
+        int mNumFaces;
+
+        // For each face, we create a HightlightView for it.
+        private void handleFace(FaceDetector.Face f) {
+            PointF midPoint = new PointF();
+
+            int r = ((int) (f.eyesDistance() * mScale)) * 2;
+            f.getMidPoint(midPoint);
+            midPoint.x *= mScale;
+            midPoint.y *= mScale;
+
+            int midX = (int) midPoint.x;
+            int midY = (int) midPoint.y;
+
+            HighlightView hv = new HighlightView(mImageView);
+
+            int width = mBitmap.getWidth();
+            int height = mBitmap.getHeight();
+
+            Rect imageRect = new Rect(0, 0, width, height);
+
+            RectF faceRect = new RectF(midX, midY, midX, midY);
+            faceRect.inset(-r, -r);
+            if (faceRect.left < 0) {
+                faceRect.inset(-faceRect.left, -faceRect.left);
+            }
+
+            if (faceRect.top < 0) {
+                faceRect.inset(-faceRect.top, -faceRect.top);
+            }
+
+            if (faceRect.right > imageRect.right) {
+                faceRect.inset(faceRect.right - imageRect.right, faceRect.right - imageRect.right);
+            }
+
+            if (faceRect.bottom > imageRect.bottom) {
+                faceRect.inset(faceRect.bottom - imageRect.bottom, faceRect.bottom - imageRect.bottom);
+            }
+
+            hv.setup(mImageMatrix, imageRect, faceRect, mCircleCrop, mAspectX != 0 && mAspectY != 0);
+
+            mImageView.add(hv);
+        }
+
+        // Create a default HightlightView if we found no face in the picture.
+        private void makeDefault() {
+            HighlightView hv = new HighlightView(mImageView);
+
+            int width = mBitmap.getWidth();
+            int height = mBitmap.getHeight();
+
+            Rect imageRect = new Rect(0, 0, width, height);
+
+            // CR: sentences!
+            // make the default size about 4/5 of the width or height
+            int cropWidth = Math.min(width, height) * 4 / 5;
+            int cropHeight = cropWidth;
+
+            if (mAspectX != 0 && mAspectY != 0) {
+                if (mAspectX > mAspectY) {
+                    cropHeight = cropWidth * mAspectY / mAspectX;
+                } else {
+                    cropWidth = cropHeight * mAspectX / mAspectY;
+                }
+            }
+
+            int x = (width - cropWidth) / 2;
+            int y = (height - cropHeight) / 2;
+
+            RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight);
+            hv.setup(mImageMatrix, imageRect, cropRect, mCircleCrop, mAspectX != 0 && mAspectY != 0);
+            mImageView.add(hv);
+        }
+
+        // Scale the image down for faster face detection.
+        private Bitmap prepareBitmap() {
+            if (mBitmap == null) {
+                return null;
+            }
+
+            // 256 pixels wide is enough.
+            if (mBitmap.getWidth() > 256) {
+                mScale = 256.0F / mBitmap.getWidth();  // CR: F => f (or change all f to F).
+            }
+            Matrix matrix = new Matrix();
+            matrix.setScale(mScale, mScale);
+            Bitmap faceBitmap = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(), mBitmap.getHeight(), matrix, true);
+            return faceBitmap;
+        }
+
+        public void run() {
+            mImageMatrix = mImageView.getImageMatrix();
+            Bitmap faceBitmap = prepareBitmap();
+
+            mScale = 1.0F / mScale;
+            if (faceBitmap != null && mDoFaceDetection) {
+                FaceDetector detector = new FaceDetector(faceBitmap.getWidth(), faceBitmap.getHeight(), mFaces.length);
+                mNumFaces = detector.findFaces(faceBitmap, mFaces);
+            }
+
+            if (faceBitmap != null && faceBitmap != mBitmap) {
+                faceBitmap.recycle();
+            }
+
+            mHandler.post(new Runnable() {
+                public void run() {
+                    mWaitingToPick = mNumFaces > 1;
+                    if (mNumFaces > 0) {
+                        for (int i = 0; i < mNumFaces; i++) {
+                            handleFace(mFaces[i]);
+                        }
+                    } else {
+                        makeDefault();
+                    }
+                    mImageView.invalidate();
+                    if (mImageView.mHighlightViews.size() == 1) {
+                        mCrop = mImageView.mHighlightViews.get(0);
+                        mCrop.setFocus(true);
+                    }
+
+                    if (mNumFaces > 1) {
+                        // CR: no need for the variable t. just do Toast.makeText(...).show().
+                        Toast t = Toast.makeText(CropImage.this, R.string.multiface_crop_help, Toast.LENGTH_SHORT);
+                        t.show();
+                    }
+                }
+            });
+        }
+    };
+}
+
+class CropImageView extends ImageViewTouchBase {
+    ArrayList<HighlightView> mHighlightViews = new ArrayList<HighlightView>();
+    HighlightView mMotionHighlightView = null;
+    float mLastX, mLastY;
+    int mMotionEdge;
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (mBitmapDisplayed.getBitmap() != null) {
+            for (HighlightView hv : mHighlightViews) {
+                hv.mMatrix.set(getImageMatrix());
+                hv.invalidate();
+                if (hv.mIsFocused) {
+                    centerBasedOnHighlightView(hv);
+                }
+            }
+        }
+    }
+
+    public CropImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void zoomTo(float scale, float centerX, float centerY) {
+        super.zoomTo(scale, centerX, centerY);
+        for (HighlightView hv : mHighlightViews) {
+            hv.mMatrix.set(getImageMatrix());
+            hv.invalidate();
+        }
+    }
+
+    @Override
+    protected void zoomIn() {
+        super.zoomIn();
+        for (HighlightView hv : mHighlightViews) {
+            hv.mMatrix.set(getImageMatrix());
+            hv.invalidate();
+        }
+    }
+
+    @Override
+    protected void zoomOut() {
+        super.zoomOut();
+        for (HighlightView hv : mHighlightViews) {
+            hv.mMatrix.set(getImageMatrix());
+            hv.invalidate();
+        }
+    }
+
+    @Override
+    protected void postTranslate(float deltaX, float deltaY) {
+        super.postTranslate(deltaX, deltaY);
+        for (int i = 0; i < mHighlightViews.size(); i++) {
+            HighlightView hv = mHighlightViews.get(i);
+            hv.mMatrix.postTranslate(deltaX, deltaY);
+            hv.invalidate();
+        }
+    }
+
+    // According to the event's position, change the focus to the first
+    // hitting cropping rectangle.
+    private void recomputeFocus(MotionEvent event) {
+        for (int i = 0; i < mHighlightViews.size(); i++) {
+            HighlightView hv = mHighlightViews.get(i);
+            hv.setFocus(false);
+            hv.invalidate();
+        }
+
+        for (int i = 0; i < mHighlightViews.size(); i++) {
+            HighlightView hv = mHighlightViews.get(i);
+            int edge = hv.getHit(event.getX(), event.getY());
+            if (edge != HighlightView.GROW_NONE) {
+                if (!hv.hasFocus()) {
+                    hv.setFocus(true);
+                    hv.invalidate();
+                }
+                break;
+            }
+        }
+        invalidate();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        CropImage cropImage = (CropImage) getContext();
+        if (cropImage.mSaving) {
+            return false;
+        }
+
+        switch (event.getAction()) {
+        case MotionEvent.ACTION_DOWN:  // CR: inline case blocks.
+            if (cropImage.mWaitingToPick) {
+                recomputeFocus(event);
+            } else {
+                for (int i = 0; i < mHighlightViews.size(); i++) {  // CR: iterator for; if not, then i++ => ++i.
+                    HighlightView hv = mHighlightViews.get(i);
+                    int edge = hv.getHit(event.getX(), event.getY());
+                    if (edge != HighlightView.GROW_NONE) {
+                        mMotionEdge = edge;
+                        mMotionHighlightView = hv;
+                        mLastX = event.getX();
+                        mLastY = event.getY();
+                        // CR: get rid of the extraneous parens below.
+                        mMotionHighlightView.setMode((edge == HighlightView.MOVE) ? HighlightView.ModifyMode.Move
+                                : HighlightView.ModifyMode.Grow);
+                        break;
+                    }
+                }
+            }
+            break;
+        // CR: vertical space before case blocks.
+        case MotionEvent.ACTION_UP:
+            if (cropImage.mWaitingToPick) {
+                for (int i = 0; i < mHighlightViews.size(); i++) {
+                    HighlightView hv = mHighlightViews.get(i);
+                    if (hv.hasFocus()) {
+                        cropImage.mCrop = hv;
+                        for (int j = 0; j < mHighlightViews.size(); j++) {
+                            if (j == i) {  // CR: if j != i do your shit; no need for continue.
+                                continue;
+                            }
+                            mHighlightViews.get(j).setHidden(true);
+                        }
+                        centerBasedOnHighlightView(hv);
+                        ((CropImage) getContext()).mWaitingToPick = false;
+                        return true;
+                    }
+                }
+            } else if (mMotionHighlightView != null) {
+                centerBasedOnHighlightView(mMotionHighlightView);
+                mMotionHighlightView.setMode(HighlightView.ModifyMode.None);
+            }
+            mMotionHighlightView = null;
+            break;
+        case MotionEvent.ACTION_MOVE:
+            if (cropImage.mWaitingToPick) {
+                recomputeFocus(event);
+            } else if (mMotionHighlightView != null) {
+                mMotionHighlightView.handleMotion(mMotionEdge, event.getX() - mLastX, event.getY() - mLastY);
+                mLastX = event.getX();
+                mLastY = event.getY();
+
+                if (true) {
+                    // This section of code is optional. It has some user
+                    // benefit in that moving the crop rectangle against
+                    // the edge of the screen causes scrolling but it means
+                    // that the crop rectangle is no longer fixed under
+                    // the user's finger.
+                    ensureVisible(mMotionHighlightView);
+                }
+            }
+            break;
+        }
+
+        switch (event.getAction()) {
+        case MotionEvent.ACTION_UP:
+            center(true, true);
+            break;
+        case MotionEvent.ACTION_MOVE:
+            // if we're not zoomed then there's no point in even allowing
+            // the user to move the image around. This call to center puts
+            // it back to the normalized location (with false meaning don't
+            // animate).
+            if (getScale() == 1F) {
+                center(true, true);
+            }
+            break;
+        }
+
+        return true;
+    }
+
+    // Pan the displayed image to make sure the cropping rectangle is visible.
+    private void ensureVisible(HighlightView hv) {
+        Rect r = hv.mDrawRect;
+
+        int panDeltaX1 = Math.max(0, getLeft() - r.left);
+        int panDeltaX2 = Math.min(0, getRight() - r.right);
+
+        int panDeltaY1 = Math.max(0, getTop() - r.top);
+        int panDeltaY2 = Math.min(0, getBottom() - r.bottom);
+
+        int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2;
+        int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2;
+
+        if (panDeltaX != 0 || panDeltaY != 0) {
+            panBy(panDeltaX, panDeltaY);
+        }
+    }
+
+    // If the cropping rectangle's size changed significantly, change the
+    // view's center and scale according to the cropping rectangle.
+    private void centerBasedOnHighlightView(HighlightView hv) {
+        Rect drawRect = hv.mDrawRect;
+
+        float width = drawRect.width();
+        float height = drawRect.height();
+
+        float thisWidth = getWidth();
+        float thisHeight = getHeight();
+
+        float z1 = thisWidth / width * .6F;
+        float z2 = thisHeight / height * .6F;
+
+        float zoom = Math.min(z1, z2);
+        zoom = zoom * this.getScale();
+        zoom = Math.max(1F, zoom);
+
+        if ((Math.abs(zoom - getScale()) / zoom) > .1) {
+            float[] coordinates = new float[] { hv.mCropRect.centerX(), hv.mCropRect.centerY() };
+            getImageMatrix().mapPoints(coordinates);
+            zoomTo(zoom, coordinates[0], coordinates[1], 300F);  // CR: 300.0f.
+        }
+
+        ensureVisible(hv);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        for (int i = 0; i < mHighlightViews.size(); i++) {
+            mHighlightViews.get(i).draw(canvas);
+        }
+    }
+
+    public void add(HighlightView hv) {
+        mHighlightViews.add(hv);
+        invalidate();
+    }
+}
diff --git a/src/com/cooliris/media/CrossFadingTexture.java b/src/com/cooliris/media/CrossFadingTexture.java
new file mode 100644
index 0000000..0b640b4
--- /dev/null
+++ b/src/com/cooliris/media/CrossFadingTexture.java
@@ -0,0 +1,135 @@
+package com.cooliris.media;
+
+import javax.microedition.khronos.opengles.GL11;
+
+import android.util.Log;
+
+public class CrossFadingTexture {
+    private static final String TAG = "CrossFadingTexture";
+
+    private Texture mTexture;
+    private Texture mFadingTexture;
+    private float mMixRatio = 0.0f;
+    private float mAnimatedMixRatio = 0.0f;
+    private boolean mBindUsingMixed = false;
+    private boolean mBind = false;
+    private boolean mFadeNecessary = false;
+
+    public CrossFadingTexture() { }
+    
+    public CrossFadingTexture(Texture initialTexture) {
+        mMixRatio = 1.0f;
+        mAnimatedMixRatio = 1.0f;
+        mFadeNecessary = false;
+        mTexture = initialTexture;
+        mFadingTexture = initialTexture;
+    }
+
+    public void clear() {
+        mTexture = null;
+        mFadingTexture = null;
+    }
+
+    public CrossFadingTexture(Texture source, Texture destination) {
+        mFadingTexture = source;
+        mTexture = destination;
+        mMixRatio = 1.0f;
+        mAnimatedMixRatio = 0.0f;
+        Log.i(TAG, "Creating crossfading texture");
+    }
+
+    public Texture getTexture() {
+        return mTexture;
+    }
+
+    public void setTexture(Texture texture) {
+        if (mTexture == texture || texture == null || (mAnimatedMixRatio > 0.0f && mAnimatedMixRatio < 1.0f)) {
+            return;
+        }
+        mFadeNecessary = false;
+        if (mFadingTexture == null) {
+            mFadeNecessary = true;
+        }
+        if (mTexture != null) {
+            mFadingTexture = mTexture;
+        } else {
+            mFadingTexture = texture;
+        }
+        mTexture = texture;
+        mAnimatedMixRatio = 0.0f;
+        mMixRatio = 1.0f;
+    }
+
+    public void setTextureImmediate(Texture texture) {
+        if (texture == null || texture.isLoaded() == false || mTexture == texture) {
+            return;
+        }
+        if (mTexture != null) {
+            mFadingTexture = mTexture;
+        }
+        mTexture = texture;
+        mMixRatio = 1.0f;
+    }
+
+    public boolean update(float timeElapsed) {
+        if (mTexture != null && mFadingTexture != null && mTexture.isLoaded() && mFadingTexture.isLoaded()) {
+            mAnimatedMixRatio = FloatUtils.animate(mAnimatedMixRatio, mMixRatio, timeElapsed * 0.5f);
+            return (mMixRatio != mAnimatedMixRatio);
+        } else {
+            mAnimatedMixRatio = 0.0f;
+            return false;
+        }
+    }
+    
+    public boolean bind(RenderView view, GL11 gl) {
+        if (mBind) {
+            return true;  // Already bound.
+        }
+        if (mFadingTexture != null && mFadingTexture.mState == Texture.STATE_ERROR) {
+            mFadingTexture = null;
+        }
+        if (mTexture != null && mTexture.mState == Texture.STATE_ERROR) {
+            mTexture = null;
+        }
+        mBindUsingMixed = false;
+        boolean fadingTextureLoaded = false;
+        boolean textureLoaded = false;
+        if (mFadingTexture != null) {
+            fadingTextureLoaded = view.bind(mFadingTexture);
+        }
+        if (mTexture != null) {
+            view.bind(mTexture);
+            textureLoaded = mTexture.isLoaded();
+        }
+        if (mFadeNecessary) {
+            if (view.getAlpha() > mAnimatedMixRatio) {
+                view.setAlpha(mAnimatedMixRatio);
+            }
+            if (mAnimatedMixRatio == 1.0f) {
+                mFadeNecessary = false;
+            }
+        }
+        if (textureLoaded == false && fadingTextureLoaded == false) {
+            return false;
+        }
+        mBind = true;
+        if (mAnimatedMixRatio <= 0.0f && fadingTextureLoaded) {
+            view.bind(mFadingTexture);
+        } else if (mAnimatedMixRatio >= 1.0f || !fadingTextureLoaded ||
+                view.getAlpha() < mAnimatedMixRatio || mFadingTexture == mTexture) {
+            view.bind(mTexture);
+        } else {
+            mBindUsingMixed = true;
+            view.bindMixed(mFadingTexture, mTexture, mAnimatedMixRatio);
+        }
+        return true;
+    }
+
+    public void unbind(RenderView view, GL11 gl) {
+        if (mBindUsingMixed && mBind) {
+            view.unbindMixed();
+            mBindUsingMixed = false;
+        }
+        mBind = false;
+    }
+}
diff --git a/src/com/cooliris/media/DataSource.java b/src/com/cooliris/media/DataSource.java
new file mode 100644
index 0000000..c05eacf
--- /dev/null
+++ b/src/com/cooliris/media/DataSource.java
@@ -0,0 +1,18 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+
+public interface DataSource {
+    // Load the sets to be displayed.
+    void loadMediaSets(final MediaFeed feed);
+
+    // Pass in Shared.INFINITY for the rangeEnd to load all items.
+    void loadItemsForSet(final MediaFeed feed, final MediaSet parentSet, int rangeStart, int rangeEnd);
+    
+    // Called when the data source will no longer be used.
+    void shutdown();
+    
+    boolean performOperation(int operation, ArrayList<MediaBucket> mediaBuckets, Object data);
+
+    DiskCache getThumbnailCache();
+}
diff --git a/src/com/cooliris/media/Deque.java b/src/com/cooliris/media/Deque.java
new file mode 100644
index 0000000..d7175c5
--- /dev/null
+++ b/src/com/cooliris/media/Deque.java
@@ -0,0 +1,112 @@
+package com.cooliris.media;
+
+public final class Deque<E extends Object> {
+    private static final int DEFAULT_INITIAL_CAPACITY = 16;
+
+    private E[] mArray;
+    private int mHead = 0;
+    private int mTail = 0;
+
+    @SuppressWarnings("unchecked")
+    public Deque() {
+        mArray = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
+    }
+
+    @SuppressWarnings("unchecked")
+    public Deque(int initialCapacity) {
+        // CR: check initialCapacity & (initialCapacity - 1) == 0.
+        mArray = (E[]) new Object[initialCapacity];
+    }
+
+    public boolean isEmpty() {
+        return mHead == mTail;
+    }
+
+    public int size() {
+        return (mTail - mHead) & (mArray.length - 1);  // CR: wtf?!? this definitely needs a comment.
+    }
+
+    public void clear() {
+        E[] array = mArray;
+        int head = mHead;
+        int tail = mTail;
+        if (head != tail) {
+            int mask = array.length - 1;
+            do {
+                array[head] = null;
+                head = (head + 1) & mask;
+            } while (head != tail);
+            mHead = 0;
+            mTail = 0;
+        }
+    }
+
+    public E get(int index) {
+        E[] array = mArray;
+        if (index >= size()) {
+            throw new IndexOutOfBoundsException();
+        }
+        return array[(mHead + index) & (array.length - 1)];
+    }
+
+    public void addFirst(E e) {
+        E[] array = mArray;
+        int head = (mHead - 1) & (array.length - 1);
+        mHead = head;
+        array[head] = e;
+        if (head == mTail) {
+            expand();
+        }
+    }
+
+    public void addLast(E e) {
+        E[] array = mArray;
+        int tail = mTail;
+        array[tail] = e;
+        tail = (tail + 1) & (array.length - 1);
+        mTail = tail;
+        if (mHead == tail) {
+            expand();
+        }
+    }
+
+    public E pollFirst() {
+        E[] array = mArray;
+        int head = mHead;
+        E result = array[head];
+        if (result == null) {
+            return null;
+        }
+        array[head] = null;
+        mHead = (head + 1) & (array.length - 1);
+        return result;
+    }
+
+    public E pollLast() {
+        E[] array = mArray;
+        int tail = (mTail - 1) & (array.length - 1);
+        E result = array[tail];
+        if (result == null) {
+            return null;
+        }
+        array[tail] = null;
+        mTail = tail;
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    private void expand() {
+        // Must be called only when head == tail.
+        E[] array = mArray;
+        int head = mHead;
+        int capacity = array.length;
+        int rightSize = capacity - head;
+        int newCapacity = capacity << 1;
+        Object[] newArray = new Object[newCapacity];
+        System.arraycopy(array, head, newArray, 0, rightSize);
+        System.arraycopy(array, 0, newArray, rightSize, head);
+        mArray = (E[]) newArray;
+        mHead = 0;
+        mTail = capacity;
+    }
+}
diff --git a/src/com/cooliris/media/DetailMode.java b/src/com/cooliris/media/DetailMode.java
new file mode 100644
index 0000000..5bbd4dd
--- /dev/null
+++ b/src/com/cooliris/media/DetailMode.java
@@ -0,0 +1,147 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.format.DateFormat;
+
+public final class DetailMode {
+    public static CharSequence[] populateDetailModeStrings(Context context, ArrayList<MediaBucket> buckets) {
+        int numBuckets = buckets.size();
+        if (MediaBucketList.isSetSelection(buckets) && numBuckets == 1) {
+            // If just 1 set was selected, save the trouble of processing the items in the set again.
+            // We have already processed details for that set.
+            return populateSetViewDetailModeStrings(context, MediaBucketList.getFirstSetSelection(buckets), 1);
+        } else if (MediaBucketList.isSetSelection(buckets) || MediaBucketList.isMultipleItemSelection(buckets)) {
+            // Cycle through the items and add them to the selection items set.
+            MediaSet selectedItemsSet = new MediaSet();
+            for (int i = 0; i < numBuckets; i++) {
+                MediaBucket bucket = buckets.get(i);
+                ArrayList<MediaItem> currItems = null;
+                int numCurrItems = 0;
+                if (MediaBucketList.isSetSelection(bucket)) {
+                    MediaSet currSet = bucket.mediaSet;
+                    if (currSet != null) {
+                        currItems = currSet.getItems();
+                        numCurrItems = currSet.getNumItems();
+                    }
+                } else {
+                    currItems = bucket.mediaItems;
+                    numCurrItems = currItems.size();
+                }
+                if (currItems != null) {
+                    for (int j = 0; j < numCurrItems; j++) {
+                        selectedItemsSet.addItem(currItems.get(j));
+                    }
+                }
+            }
+            return populateSetViewDetailModeStrings(context, selectedItemsSet, numBuckets);
+        } else {
+            return populateItemViewDetailModeStrings(context, MediaBucketList.getFirstItemSelection(buckets)); 
+        }
+    }
+
+    private static CharSequence[] populateSetViewDetailModeStrings(Context context, MediaSet selectedItemsSet, int numOriginalSets) {
+        if (selectedItemsSet == null) {
+            return null;
+        }
+        Resources resources = context.getResources();
+        CharSequence[] strings = new CharSequence[5];
+
+        // Number of albums selected.
+        if (numOriginalSets == 1) {
+            strings[0] = "1 " + resources.getString(R.string.album_selected);
+        } else {
+            strings[0] = Integer.toString(numOriginalSets) + " " + resources.getString(R.string.albums_selected);
+        }
+
+        // Number of items selected.
+        int numItems = selectedItemsSet.mNumItemsLoaded;
+        if (numItems == 1) {
+            strings[1] = "1 " + resources.getString(R.string.item_selected);
+        } else {
+            strings[1] = Integer.toString(numItems) + " " + resources.getString(R.string.items_selected);
+        }
+
+        // Start and end times of the selected items.
+        if (selectedItemsSet.areTimestampsAvailable()) {
+            long minTimestamp = selectedItemsSet.mMinTimestamp;
+            long maxTimestamp = selectedItemsSet.mMaxTimestamp;
+            if (selectedItemsSet.isPicassaSet()) {
+                minTimestamp -= Gallery.CURRENT_TIME_ZONE.getOffset(minTimestamp);
+                maxTimestamp -= Gallery.CURRENT_TIME_ZONE.getOffset(maxTimestamp);
+            }
+            strings[2] = resources.getString(R.string.start) + ": " + DateFormat.format("h:mmaa MMM dd yyyy", minTimestamp);
+            strings[3] = resources.getString(R.string.end) + ": " + DateFormat.format("h:mmaa MMM dd yyyy", maxTimestamp);
+        } else if (selectedItemsSet.areAddedTimestampsAvailable()) {
+            long minTimestamp = selectedItemsSet.mMinAddedTimestamp;
+            long maxTimestamp = selectedItemsSet.mMaxAddedTimestamp;
+            if (selectedItemsSet.isPicassaSet()) {
+                minTimestamp -= Gallery.CURRENT_TIME_ZONE.getOffset(minTimestamp);
+                maxTimestamp -= Gallery.CURRENT_TIME_ZONE.getOffset(maxTimestamp);
+            }
+            strings[2] = resources.getString(R.string.start) + ": " + DateFormat.format("h:mmaa MMM dd yyyy", minTimestamp);
+            strings[3] = resources.getString(R.string.end) + ": " + DateFormat.format("h:mmaa MMM dd yyyy", maxTimestamp);            
+        } else {
+            strings[2] = resources.getString(R.string.start) + ": " + resources.getString(R.string.date_unknown);
+            strings[3] = resources.getString(R.string.end) + ": " + resources.getString(R.string.date_unknown);
+        }
+
+        // The location of the selected items.
+        String locationString = null;
+        if (selectedItemsSet.mLatLongDetermined) {
+            locationString = selectedItemsSet.mReverseGeocodedLocation;
+            if (locationString == null) {
+                // Try computing the location if it does not exist.
+                ReverseGeocoder reverseGeocoder = ((Gallery) context).getReverseGeocoder();
+                locationString = reverseGeocoder.computeMostGranularCommonLocation(selectedItemsSet);
+            }
+        }
+        if (locationString == null || locationString.length() == 0) {
+            locationString = resources.getString(R.string.location_unknown);
+        }
+        strings[4] = resources.getString(R.string.location) + ": " + locationString;
+        return strings;
+    }
+
+    private static CharSequence[] populateItemViewDetailModeStrings(Context context, MediaItem item) {
+        if (item == null) {
+            return null;
+        }
+        Resources resources = context.getResources(); 
+        CharSequence[] strings = new CharSequence[5];
+        strings[0] = resources.getString(R.string.title) + ": " + item.mCaption;
+        strings[1] = resources.getString(R.string.type) + ": " + item.getDisplayMimeType();
+        if (item.isDateTakenValid()) {
+            long dateTaken = item.mDateTakenInMs;
+            if (item.isPicassaItem()) {
+                dateTaken -= Gallery.CURRENT_TIME_ZONE.getOffset(dateTaken);
+            }
+            strings[2] = resources.getString(R.string.taken_on) + ": " + DateFormat.format("h:mmaa MMM dd yyyy", dateTaken);
+        } else if (item.isDateAddedValid()) {
+            long dateAdded = item.mDateAddedInSec * 1000;
+            if (item.isPicassaItem()) {
+                dateAdded -= Gallery.CURRENT_TIME_ZONE.getOffset(dateAdded);
+            }
+            // TODO: Make this added_on as soon as translations are ready.
+            //strings[2] = resources.getString(R.string.added_on) + ": " + DateFormat.format("h:mmaa MMM dd yyyy", dateAdded);
+            strings[2] = resources.getString(R.string.taken_on) + ": " + DateFormat.format("h:mmaa MMM dd yyyy", dateAdded);
+        } else {
+            strings[2] = resources.getString(R.string.taken_on) + ": " + resources.getString(R.string.date_unknown);
+        }
+        MediaSet parentMediaSet = item.mParentMediaSet;
+        if (parentMediaSet == null) {
+            strings[3] = resources.getString(R.string.album) + ":";
+        } else {
+            strings[3] = resources.getString(R.string.album) + ": " + parentMediaSet.mName;
+        }
+        ReverseGeocoder reverseGeocoder = ((Gallery) context).getReverseGeocoder();
+        String locationString = item.getReverseGeocodedLocation(reverseGeocoder);
+        if (locationString == null || locationString.length() == 0) {
+            locationString = context.getResources().getString(R.string.location_unknown);
+        }
+        strings[4] = resources.getString(R.string.location) + ": " + locationString;
+        return strings;
+    }
+}
\ No newline at end of file
diff --git a/src/com/cooliris/media/DirectLinkedList.java b/src/com/cooliris/media/DirectLinkedList.java
new file mode 100644
index 0000000..6bd4657
--- /dev/null
+++ b/src/com/cooliris/media/DirectLinkedList.java
@@ -0,0 +1,80 @@
+package com.cooliris.media;
+
+public final class DirectLinkedList<E> {
+    private Entry<E> mHead;
+    private Entry<E> mTail;
+    private int mSize = 0;
+
+    public static final class Entry<E> {
+        Entry(E value) {
+            this.value = value;
+        }
+
+        public final E value;
+        public Entry<E> previous = null;
+        public Entry<E> next = null;
+        public boolean inserted = false;
+    }
+
+    public DirectLinkedList() {
+    }
+
+    public boolean isEmpty() {
+        return mSize == 0;
+    }
+
+    public int size() {
+        return mSize;
+    }
+
+    public void add(Entry<E> entry) {
+        // Requires that entry not be inserted in a list.
+        final Entry<E> tail = mTail;
+        if (tail != null) {
+            tail.next = entry;
+            entry.previous = tail;
+        } else {
+            mHead = entry;
+        }
+        mTail = entry;
+        entry.inserted = true;
+        ++mSize;
+    }
+
+    public Entry<E> remove(Entry<E> entry) {
+        // Requires that entry be inserted into this list.
+        final Entry<E> previous = entry.previous;
+        final Entry<E> next = entry.next;
+        if (next != null) {
+            next.previous = previous;
+            entry.next = null;
+        } else {
+            mTail = previous;
+        }
+        if (previous != null) {
+            previous.next = next;
+            entry.previous = null;
+        } else {
+            mHead = next;
+        }
+        entry.inserted = false;
+        --mSize;
+        if (mSize < 0)
+            mSize = 0;
+        return next;
+    }
+
+    public Entry<E> getHead() {
+        return mHead;
+    }
+
+    public Entry<E> getTail() {
+        return mTail;
+    }
+
+    public void clear() {
+        mHead = null;
+        mTail = null;
+        mSize = 0;
+    }
+}
diff --git a/src/com/cooliris/media/DiskCache.java b/src/com/cooliris/media/DiskCache.java
new file mode 100644
index 0000000..8375aa8
--- /dev/null
+++ b/src/com/cooliris/media/DiskCache.java
@@ -0,0 +1,337 @@
+package com.cooliris.media;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+import com.cooliris.cache.CacheService;
+
+import android.util.Log;
+
+public final class DiskCache {
+    private static final String TAG = "DiskCache";
+    private static final int CHUNK_SIZE = 1048576; // 1MB.
+    private static final int INDEX_HEADER_MAGIC = 0xcafe;
+    private static final int INDEX_HEADER_VERSION = 2;
+    private static final String INDEX_FILE_NAME = "index";
+    private static final String CHUNK_FILE_PREFIX = "chunk_";
+    private final String mCacheDirectoryPath;
+    private LongSparseArray<Record> mIndexMap;
+    private final LongSparseArray<RandomAccessFile> mChunkFiles = new LongSparseArray<RandomAccessFile>();
+    private int mTailChunk = 0;
+    private int mNumInsertions = 0;
+
+    public DiskCache(String cacheDirectoryName) {
+        String cacheDirectoryPath = CacheService.getCachePath(cacheDirectoryName);
+
+        // Create the cache directory if needed.
+        File cacheDirectory = new File(cacheDirectoryPath);
+        if (!cacheDirectory.isDirectory() && !cacheDirectory.mkdirs()) {
+            Log.e(TAG, "Unable to create cache directory " + cacheDirectoryPath);
+        }
+        mCacheDirectoryPath = cacheDirectoryPath;
+        loadIndex();
+    }
+
+    @Override
+    public void finalize() {
+        shutdown();
+    }
+
+    public void load() {
+        loadIndex();
+    }
+
+    public byte[] get(long key, long timestamp) {
+        // Look up the record for the given key.
+        Record record = null;
+        synchronized (mIndexMap) {
+            record = mIndexMap.get(key);
+        }
+        if (record != null) {
+            // Read the chunk from the file.
+            if (record.timestamp < timestamp && timestamp < System.currentTimeMillis()) {
+                Log.i(TAG, "Old copy: CacheTimestamp " + record.timestamp + " new timestamp " + timestamp);
+                return null;
+            }
+            try {
+                RandomAccessFile chunkFile = getChunkFile(record.chunk);
+                if (chunkFile != null) {
+                    byte[] data = new byte[record.size];
+                    chunkFile.seek(record.offset);
+                    chunkFile.readFully(data);
+                    return data;
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Unable to read from chunk file");
+            }
+        }
+        return null;
+    }
+
+    public boolean isDataAvailable(long key, long timestamp) {
+        Record record = null;
+        synchronized (mIndexMap) {
+            record = mIndexMap.get(key);
+        }
+        if (record == null) {
+            return false;
+        }
+        if (record.timestamp < timestamp && timestamp < System.currentTimeMillis()) {
+            Log.i(TAG, "Old copy: CacheTimestamp " + record.timestamp + " new timestamp " + timestamp);
+            return false;
+        }
+        return true;
+    }
+
+    public void put(long key, byte[] data) {
+        // Check to see if the record already exists.
+        Record record = null;
+        synchronized (mIndexMap) {
+            record = mIndexMap.get(key);
+        }
+        if (record != null && data.length <= record.sizeOnDisk) {
+            // We just replace the chunk.
+            int currentChunk = record.chunk;
+            try {
+                RandomAccessFile chunkFile = getChunkFile(record.chunk);
+                if (chunkFile != null) {
+                    chunkFile.seek(record.offset);
+                    chunkFile.write(data);
+                    synchronized (mIndexMap) {
+                        mIndexMap.put(key, new Record(currentChunk, record.offset, data.length, record.sizeOnDisk, System.currentTimeMillis()));
+                    }
+                    return;
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Unable to read from chunk file");
+            }
+        }
+        // Append a new chunk to the current chunk.
+        final int chunk = mTailChunk;
+        final RandomAccessFile chunkFile = getChunkFile(chunk);
+        if (chunkFile != null) {
+            try {
+                final int offset = (int) chunkFile.length();
+                chunkFile.seek(offset);
+                chunkFile.write(data);
+                synchronized (mIndexMap) {
+                    mIndexMap.put(key, new Record(chunk, offset, data.length, data.length, System.currentTimeMillis()));
+                }
+                if (offset + data.length > CHUNK_SIZE) {
+                    ++mTailChunk;
+                }
+
+                if (++mNumInsertions == 64) { // CR: 64 => constant
+                    // Flush the index file at a regular interval. To avoid
+                    // writing the entire
+                    // index each time the format could be changed to an
+                    // append-only journal with
+                    // a snapshot generated on exit.
+                    flush();
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Unable to write new entry to chunk file");
+            }
+        } else {
+            Log.e(TAG, "getChunkFile() returned null");
+        }
+    }
+
+    public void delete(long key) {
+        synchronized (mIndexMap) {
+            mIndexMap.remove(key);
+        }
+    }
+
+    public void deleteAll() {
+        // Close all open files and clear data structures.
+        shutdown();
+
+        // Delete all cache files.
+        File cacheDirectory = new File(mCacheDirectoryPath);
+        String[] cacheFiles = cacheDirectory.list();
+        if (cacheFiles == null)
+            return;
+        for (String cacheFile : cacheFiles) {
+            new File(cacheDirectory, cacheFile).delete();
+        }
+    }
+
+    public void flush() {
+        if (mNumInsertions != 0) {
+            mNumInsertions = 0;
+            writeIndex();
+        }
+    }
+
+    public void close() {
+        writeIndex();
+        shutdown();
+    }
+
+    private void shutdown() {
+        synchronized (mChunkFiles) {
+            for (int i = 0, size = mChunkFiles.size(); i < size; ++i) {
+                try {
+                    mChunkFiles.valueAt(i).close();
+                } catch (Exception e) {
+                    Log.e(TAG, "Unable to close chunk file");
+                }
+            }
+            mChunkFiles.clear();
+        }
+        if (mIndexMap != null) {
+            synchronized (mIndexMap) {
+                if (mIndexMap != null) {
+                    mIndexMap.clear();
+                }
+            }
+        }
+    }
+
+    private String getIndexFilePath() {
+        return mCacheDirectoryPath + INDEX_FILE_NAME;
+    }
+
+    private void loadIndex() {
+        final String indexFilePath = getIndexFilePath();
+        try {
+            // Open the input stream.
+            final FileInputStream fileInput = new FileInputStream(indexFilePath);
+            final BufferedInputStream bufferedInput = new BufferedInputStream(fileInput, 1024);
+            final DataInputStream dataInput = new DataInputStream(bufferedInput);
+
+            // Read the header.
+            final int magic = dataInput.readInt();
+            final int version = dataInput.readInt();
+            boolean valid = true;
+            if (magic != INDEX_HEADER_MAGIC) {
+                Log.e(TAG, "Index file appears to be corrupt (" + magic + " != " + INDEX_HEADER_MAGIC + "), " + indexFilePath);
+                valid = false;
+            }
+            if (valid && version != INDEX_HEADER_VERSION) {
+                // Future versions can implement upgrade in this case.
+                Log.e(TAG, "Index file version " + version + " not supported");
+                valid = false;
+            }
+            if (valid) {
+                mTailChunk = dataInput.readShort();
+            }
+
+            // Read the entries.
+            if (valid) {
+                // Parse the index file body into the in-memory map.
+                final int numEntries = dataInput.readInt();
+                mIndexMap = new LongSparseArray<Record>(numEntries);
+                synchronized (mIndexMap) {
+                    for (int i = 0; i < numEntries; ++i) {
+                        final long key = dataInput.readLong();
+                        final int chunk = dataInput.readShort();
+                        final int offset = dataInput.readInt();
+                        final int size = dataInput.readInt();
+                        final int sizeOnDisk = dataInput.readInt();
+                        final long timestamp = dataInput.readLong();
+                        mIndexMap.append(key, new Record(chunk, offset, size, sizeOnDisk, timestamp));
+                    }
+                }
+            }
+
+            dataInput.close();
+            if (!valid) {
+                deleteAll();
+            }
+
+        } catch (FileNotFoundException e) {
+            // If the file does not exist the cache is empty, so just continue.
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to read the index file " + indexFilePath);
+        } finally {
+            if (mIndexMap == null) {
+                mIndexMap = new LongSparseArray<Record>();
+            }
+        }
+    }
+
+    private void writeIndex() {
+        final String indexFilePath = getIndexFilePath();
+        try {
+            // Create a temporary file to write the index into.
+            File tempFile = File.createTempFile("DiskCacheIndex", null);
+            final FileOutputStream fileOutput = new FileOutputStream(tempFile);
+            final BufferedOutputStream bufferedOutput = new BufferedOutputStream(fileOutput, 1024);
+            final DataOutputStream dataOutput = new DataOutputStream(bufferedOutput);
+
+            // Write the index header.
+            final int numRecords = mIndexMap.size();
+            dataOutput.writeInt(INDEX_HEADER_MAGIC);
+            dataOutput.writeInt(INDEX_HEADER_VERSION);
+            dataOutput.writeShort(mTailChunk);
+            dataOutput.writeInt(numRecords);
+
+            // Write the records.
+            for (int i = 0; i < numRecords; ++i) {
+                final long key = mIndexMap.keyAt(i);
+                final Record record = mIndexMap.valueAt(i);
+                dataOutput.writeLong(key);
+                dataOutput.writeShort(record.chunk);
+                dataOutput.writeInt(record.offset);
+                dataOutput.writeInt(record.size);
+                dataOutput.writeInt(record.sizeOnDisk);
+                dataOutput.writeLong(record.timestamp);
+            }
+
+            // Close the file.
+            dataOutput.close();
+
+            Log.d(TAG, "Wrote index with " + numRecords + " records.");
+
+            // Atomically overwrite the old index file.
+            tempFile.renameTo(new File(indexFilePath));
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to write the index file " + indexFilePath);
+        }
+    }
+
+    private RandomAccessFile getChunkFile(int chunk) {
+        RandomAccessFile chunkFile = null;
+        synchronized (mChunkFiles) {
+            chunkFile = mChunkFiles.get(chunk);
+        }
+        if (chunkFile == null) {
+            final String chunkFilePath = mCacheDirectoryPath + CHUNK_FILE_PREFIX + chunk;
+            try {
+                chunkFile = new RandomAccessFile(chunkFilePath, "rw");
+            } catch (FileNotFoundException e) {
+                Log.e(TAG, "Unable to create or open the chunk file " + chunkFilePath);
+            }
+            synchronized (mChunkFiles) {
+                mChunkFiles.put(chunk, chunkFile);
+            }
+        }
+        return chunkFile;
+    }
+
+    private static final class Record {
+        public Record(int chunk, int offset, int size, int sizeOnDisk, long timestamp) {
+            this.chunk = chunk;
+            this.offset = offset;
+            this.size = size;
+            this.timestamp = timestamp;
+            this.sizeOnDisk = sizeOnDisk;
+        }
+
+        public final long timestamp;
+        public final int chunk;
+        public final int offset;
+        public final int size;
+        public final int sizeOnDisk;
+    }
+}
diff --git a/src/com/cooliris/media/DisplayItem.java b/src/com/cooliris/media/DisplayItem.java
new file mode 100644
index 0000000..8e50567
--- /dev/null
+++ b/src/com/cooliris/media/DisplayItem.java
@@ -0,0 +1,219 @@
+package com.cooliris.media;
+
+import java.util.Random;
+
+import android.content.Context;
+
+import com.cooliris.media.FloatUtils;
+
+/**
+ * A simple structure for a MediaItem that can be rendered.
+ */
+public final class DisplayItem {
+    private static final float STACK_SPACING = 0.2f;
+    private DirectLinkedList.Entry<DisplayItem> mAnimatablesEntry = new DirectLinkedList.Entry<DisplayItem>(this);
+    private static final Random random = new Random();
+    private Vector3f mStacktopPosition = new Vector3f(-1.0f, -1.0f, -1.0f);
+    private Vector3f mJitteredPosition = new Vector3f();
+    private boolean mHasFocus;
+    private Vector3f mTargetPosition = new Vector3f();
+    private float mTargetTheta;
+    private float mImageTheta;
+    private int mStackId;
+    private MediaItemTexture mThumbnailImage = null;
+    private Texture mScreennailImage = null;
+    private UriTexture mHiResImage = null;
+    private float mConvergenceSpeed = 1.0f;
+
+    public final MediaItem mItemRef;
+    public float mAnimatedTheta;
+    public float mAnimatedImageTheta;
+    public float mAnimatedPlaceholderFade = 0f;
+    public boolean mAlive;
+    public Vector3f mAnimatedPosition = new Vector3f();
+
+    public DisplayItem(MediaItem item) {
+        mItemRef = item;
+        mAnimatedImageTheta = item.mRotation;
+        mImageTheta = item.mRotation;
+        if (item == null)
+            throw new UnsupportedOperationException("Cannot create a displayitem from a null MediaItem.");
+    }
+
+    public DirectLinkedList.Entry<DisplayItem> getAnimatablesEntry() {
+        return mAnimatablesEntry;
+    }
+
+    public final void rotateImageBy(float theta) {
+        mImageTheta += theta;
+    }
+
+    public final void set(Vector3f position, int stackIndex, boolean performTransition) {
+        mConvergenceSpeed = 1.0f;
+        Vector3f animatedPosition = mAnimatedPosition;
+        Vector3f targetPosition = mTargetPosition;
+        int seed = stackIndex;
+        int randomSeed = stackIndex;
+
+        if (seed > 3) {
+            seed = 3;
+            randomSeed = 0;
+        }
+
+        if (!mAlive) {
+            animatedPosition.set(position);
+            animatedPosition.z = -3.0f + stackIndex * STACK_SPACING;
+        }
+
+        targetPosition.set(position);
+        if (mStackId != stackIndex && stackIndex >= 0) {
+            mStackId = stackIndex;
+        }
+
+        if (randomSeed == 0) {
+            mTargetTheta = 0.0f;
+            mTargetPosition.z = seed * STACK_SPACING;
+            mJitteredPosition.set(0, 0, seed * STACK_SPACING);
+        } else {
+            int sign = (seed % 2 == 0) ? 1 : -1;
+            if (seed != 0 && !mStacktopPosition.equals(position) && mTargetTheta == 0) {
+                mTargetTheta = 30.0f * (0.5f - (float) Math.random());
+                mJitteredPosition.x = sign * 12.0f * seed + (0.5f - random.nextFloat()) * 4 * seed;
+                mJitteredPosition.y = sign * 4 + ((sign == 1) ? -8.0f : sign * (random.nextFloat()) * 16.0f);
+                mJitteredPosition.x *= Gallery.PIXEL_DENSITY;
+                mJitteredPosition.y *= Gallery.PIXEL_DENSITY;
+                mJitteredPosition.z = seed * STACK_SPACING;
+            }
+        }
+        mTargetPosition.add(mJitteredPosition);
+        mStacktopPosition.set(position);
+    } 
+
+    public int getStackIndex() {
+        return mStackId;
+    }
+
+    public Texture getThumbnailImage(Context context, MediaItemTexture.Config config) {
+        MediaItemTexture texture = mThumbnailImage;
+        if (texture == null && config != null) {
+            if (mItemRef.mId != Shared.INVALID) {
+                texture = new MediaItemTexture(context, config, mItemRef);
+            }
+            mThumbnailImage = texture;
+        }
+        return texture;
+    }
+
+    public Texture getScreennailImage(Context context) {
+        Texture texture = mScreennailImage;
+        if (texture == null || texture.mState == Texture.STATE_ERROR) {
+            MediaSet parentMediaSet = mItemRef.mParentMediaSet;
+            if (parentMediaSet != null && parentMediaSet.mDataSource.getThumbnailCache() == LocalDataSource.sThumbnailCache) {
+                if (mItemRef.mId != Shared.INVALID && mItemRef.mId != 0) {
+                    texture = new MediaItemTexture(context, null, mItemRef);
+                } else if (mItemRef.mContentUri != null) {
+                    texture = new UriTexture(mItemRef.mContentUri);
+                }
+            } else {
+                texture = new UriTexture(mItemRef.mScreennailUri);
+                ((UriTexture)texture).setCacheId(Utils.Crc64Long(mItemRef.mFilePath));
+            }
+            mScreennailImage = texture;
+        }
+        return texture;
+    }
+
+    public void clearScreennailImage() {
+        if (mScreennailImage != null) {
+            mScreennailImage = null;
+            mHiResImage = null;
+        }
+    }
+
+    public void clearHiResImage() {
+        mHiResImage = null;
+    }
+
+    public void clearThumbnail() {
+        mThumbnailImage = null;
+    }
+
+    /**
+     * Use this function to query the animation state of the display item
+     * 
+     * @return true if the display item is animating
+     */
+    public boolean isAnimating() {
+        return mAlive && (!mAnimatedPosition.equals(mTargetPosition) ||
+                mAnimatedTheta != mTargetTheta || mAnimatedImageTheta != mImageTheta ||
+                mAnimatedPlaceholderFade != 1f);
+    }
+
+    /**
+     * This function should be called every time the frame needs to be updated.
+     */
+    public final void update(float timeElapsedInSec) {
+        if (mAlive) {
+            timeElapsedInSec *= 1.25f;
+            Vector3f animatedPosition = mAnimatedPosition;
+            Vector3f targetPosition = mTargetPosition;
+            timeElapsedInSec *= mConvergenceSpeed;
+            animatedPosition.x = FloatUtils.animate(animatedPosition.x, targetPosition.x, timeElapsedInSec);
+            animatedPosition.y = FloatUtils.animate(animatedPosition.y, targetPosition.y, timeElapsedInSec);
+            mAnimatedTheta = FloatUtils.animate(mAnimatedTheta, mTargetTheta, timeElapsedInSec);
+            mAnimatedImageTheta = FloatUtils.animate(mAnimatedImageTheta, mImageTheta, timeElapsedInSec);
+            mAnimatedPlaceholderFade = FloatUtils.animate(mAnimatedPlaceholderFade, 1f, timeElapsedInSec);
+            animatedPosition.z = FloatUtils.animate(animatedPosition.z, targetPosition.z, timeElapsedInSec);
+        }
+    }
+
+    /**
+     * Commits all animations for the Display Item
+     */
+    public final void commit() {
+        mAnimatedPosition.set(mTargetPosition);
+        mAnimatedTheta = mTargetTheta;
+        mAnimatedImageTheta = mImageTheta;
+    }
+
+    public final void setHasFocus(boolean hasFocus, boolean pushDown) {
+        mConvergenceSpeed = 2.0f;
+        mHasFocus = hasFocus;
+        int seed = mStackId;
+        if (seed > 3) {
+            seed = 3;
+        }
+        if (hasFocus) {
+            mTargetPosition.set(mStacktopPosition);
+            mTargetPosition.add(mJitteredPosition);
+            mTargetPosition.add(mJitteredPosition);
+            mTargetPosition.z = seed * STACK_SPACING + (pushDown ? 1.0f : -0.5f);
+        } else {
+            mTargetPosition.set(mStacktopPosition);
+            mTargetPosition.add(mJitteredPosition);
+            mTargetPosition.z = seed * STACK_SPACING;
+        }
+    }
+
+    public final boolean getHasFocus() {
+        return mHasFocus;
+    }
+
+    public final Texture getHiResImage(Context context) {
+        UriTexture texture = mHiResImage;
+        if (texture == null) {
+            texture = new UriTexture(mItemRef.mContentUri);
+            texture.setCacheId(Utils.Crc64Long(mItemRef.mFilePath));
+            mHiResImage = texture;
+        }
+        return texture;
+    }
+
+    public boolean isAlive() {
+        return mAlive;
+    }
+
+    public float getImageTheta() {
+        return mImageTheta;
+    }
+}
diff --git a/src/com/cooliris/media/DisplayList.java b/src/com/cooliris/media/DisplayList.java
new file mode 100644
index 0000000..c58de5c
--- /dev/null
+++ b/src/com/cooliris/media/DisplayList.java
@@ -0,0 +1,120 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+// CR: comment.
+public final class DisplayList {
+    private DirectLinkedList<DisplayItem> mAnimatables = new DirectLinkedList<DisplayItem>();
+    private HashMap<MediaItem, DisplayItem> mDisplayMap = new HashMap<MediaItem, DisplayItem>(1024);
+    private ArrayList<DisplayItem> mItems = new ArrayList<DisplayItem>(1024);
+
+    public DisplayItem get(MediaItem item) {
+        HashMap<MediaItem, DisplayItem> displayMap = mDisplayMap;
+        DisplayItem displayItem = displayMap.get(item);
+        if (displayItem == null) {
+            displayItem = new DisplayItem(item);
+            displayMap.put(item, displayItem);
+            mItems.add(displayItem);
+        }
+        return displayItem;
+    }
+
+    public void setPositionAndStackIndex(DisplayItem item, Vector3f position, int stackId, boolean performTransition) {
+        item.set(position, stackId, performTransition);
+        if (!performTransition) {
+            item.commit();
+        } else {
+            markIfDirty(item);
+        }
+    }
+
+    public void setHasFocus(DisplayItem item, boolean hasFocus, boolean pushDown) {
+        boolean currentHasFocus = item.getHasFocus();
+        if (currentHasFocus != hasFocus) {
+            item.setHasFocus(hasFocus, pushDown);
+            markIfDirty(item);
+        }
+    }
+
+    public ArrayList<DisplayItem> getAllDisplayItems() {
+        return mItems;
+    }
+
+    public void update(float timeElapsed) {
+        final DirectLinkedList<DisplayItem> animatables = mAnimatables;
+        synchronized (animatables) {
+            DirectLinkedList.Entry<DisplayItem> entry = animatables.getHead();
+            while (entry != null) {
+                DisplayItem item = entry.value;
+                item.update(timeElapsed);
+                if (!item.isAnimating()) {
+                    entry = animatables.remove(entry);
+                } else {
+                    entry = entry.next;
+                }
+            }
+        }
+    }
+
+    public int getNumAnimatables() {
+        return mAnimatables.size();
+    }
+
+    public void setAlive(DisplayItem item, boolean alive) {
+        item.mAlive = alive;
+        if (alive && item.isAnimating()) {
+            final DirectLinkedList.Entry<DisplayItem> entry = item.getAnimatablesEntry();
+            if (!entry.inserted) {
+                mAnimatables.add(entry);
+            }
+        }
+    }
+
+    public void commit(DisplayItem item) {
+        item.commit();
+        final DirectLinkedList<DisplayItem> animatables = mAnimatables;
+        synchronized (animatables) {
+            animatables.remove(item.getAnimatablesEntry());
+        }
+    }
+
+    public void addToAnimatables(DisplayItem item) {
+        final DirectLinkedList.Entry<DisplayItem> entry = item.getAnimatablesEntry();
+        if (!entry.inserted) {
+            final DirectLinkedList<DisplayItem> animatables = mAnimatables;
+            synchronized (animatables) {
+                animatables.add(entry);
+            }
+        }
+    }
+
+    private void markIfDirty(DisplayItem item) {
+        if (item.isAnimating()) {
+            addToAnimatables(item);
+        }
+    }
+
+    public void clear() {
+        mDisplayMap.clear();
+        synchronized (mItems) {
+            mItems.clear();
+        }
+    }
+
+    public void clearExcept(DisplayItem[] displayItems) {
+        HashMap<MediaItem, DisplayItem> displayMap = mDisplayMap;
+        displayMap.clear();
+        synchronized (mItems) {
+            mItems.clear();
+            int numItems = displayItems.length;
+            for (int i = 0; i < numItems; ++i) {
+                DisplayItem displayItem = displayItems[i];
+                if (displayItem != null) {
+                    displayMap.put(displayItem.mItemRef, displayItem);
+                    mItems.add(displayItem);
+                }
+            }
+        }
+    }
+}
diff --git a/src/com/cooliris/media/DisplaySlot.java b/src/com/cooliris/media/DisplaySlot.java
new file mode 100644
index 0000000..ca760d4
--- /dev/null
+++ b/src/com/cooliris/media/DisplaySlot.java
@@ -0,0 +1,108 @@
+package com.cooliris.media;
+
+import java.util.HashMap;
+
+// CR: this stuff needs comments really badly.
+public final class DisplaySlot {
+    private MediaSet mSetRef;
+    private String mTitle;
+    private StringTexture mTitleImage;
+    private String mLocation;
+    private StringTexture mLocationImage;
+
+    private static final StringTexture.Config CAPTION_STYLE = new StringTexture.Config();
+    private static final StringTexture.Config CLUSTER_STYLE = new StringTexture.Config();
+    private static final StringTexture.Config LOCATION_STYLE = new StringTexture.Config();
+
+    static {
+        CAPTION_STYLE.sizeMode = StringTexture.Config.SIZE_TEXT_TO_BOUNDS;
+        CAPTION_STYLE.fontSize = 16 * Gallery.PIXEL_DENSITY;
+        CAPTION_STYLE.bold = true;
+        CAPTION_STYLE.width = (Gallery.PIXEL_DENSITY < 1.5f) ? 128 : 256;
+        CAPTION_STYLE.height = (Gallery.PIXEL_DENSITY < 1.5f) ? 32 : 64;
+        CAPTION_STYLE.yalignment = StringTexture.Config.ALIGN_TOP;
+        CAPTION_STYLE.xalignment = StringTexture.Config.ALIGN_HCENTER;
+
+        CLUSTER_STYLE.sizeMode = StringTexture.Config.SIZE_TEXT_TO_BOUNDS;
+        CLUSTER_STYLE.width = (Gallery.PIXEL_DENSITY < 1.5f) ? 128 : 256;
+        CLUSTER_STYLE.height = (Gallery.PIXEL_DENSITY < 1.5f) ? 32 : 64;
+        CLUSTER_STYLE.yalignment = StringTexture.Config.ALIGN_TOP;
+        CLUSTER_STYLE.fontSize = 16 * Gallery.PIXEL_DENSITY;
+        CLUSTER_STYLE.bold = true;
+        CLUSTER_STYLE.xalignment = StringTexture.Config.ALIGN_HCENTER;
+
+        LOCATION_STYLE.sizeMode = StringTexture.Config.SIZE_TEXT_TO_BOUNDS;
+        LOCATION_STYLE.fontSize = 12 * Gallery.PIXEL_DENSITY;
+        LOCATION_STYLE.width = (Gallery.PIXEL_DENSITY < 1.5f) ? 128 : 256;
+        LOCATION_STYLE.height = (Gallery.PIXEL_DENSITY < 1.5f) ? 32 : 64;
+        LOCATION_STYLE.fontSize = 12 * Gallery.PIXEL_DENSITY;
+        LOCATION_STYLE.xalignment = StringTexture.Config.ALIGN_HCENTER;
+    }
+
+    public void setMediaSet(MediaSet set) {
+        mSetRef = set;
+        mTitle = null;
+        mTitleImage = null;
+        mLocationImage = null;
+    }
+
+    public MediaSet getMediaSet() {
+        return mSetRef;
+    }
+
+    public boolean hasValidLocation() {
+        if (mSetRef != null) {
+            return (mSetRef.mReverseGeocodedLocation != null);
+        } else {
+            return false;
+        }
+    }
+
+    private StringTexture getTextureForString(String string, HashMap<String, StringTexture> textureTable, StringTexture.Config config) {
+        StringTexture texture = null;
+        if (textureTable != null && textureTable.containsKey(string)) {
+            texture = textureTable.get(string);
+        }
+        if (texture == null) {
+            texture = new StringTexture(string, config);
+            if (textureTable != null) {
+                textureTable.put(string, texture);
+            }
+        }
+        return texture;
+    }
+
+    public StringTexture getTitleImage(HashMap<String, StringTexture> textureTable) {
+        if (mSetRef == null) {
+            return null;
+        }
+        StringTexture texture = mTitleImage;
+        String title = mSetRef.mTruncTitleString;
+        if (texture == null && title != null && !(title.equals(mTitle))) {
+            texture = getTextureForString(title, textureTable, ((mSetRef.mId != Shared.INVALID && mSetRef.mId != 0) ? CAPTION_STYLE : CLUSTER_STYLE));
+            mTitleImage = texture;
+            mTitle = title;
+        }
+        return texture;
+    }
+
+    public StringTexture getLocationImage(ReverseGeocoder reverseGeocoder, HashMap<String, StringTexture> textureTable) {
+        if (mSetRef == null || mSetRef.mTitleString == null) {
+            return null;
+        }
+        if (mLocationImage == null) {
+            if (!mSetRef.mReverseGeocodedLocationRequestMade && reverseGeocoder != null) {
+                reverseGeocoder.enqueue(mSetRef);
+                mSetRef.mReverseGeocodedLocationRequestMade = true;
+            }
+            if (mSetRef.mReverseGeocodedLocationComputed) {
+                String geocodedLocation = mSetRef.mReverseGeocodedLocation;
+                if (geocodedLocation != null) {
+                    mLocation = geocodedLocation;
+                    mLocationImage = getTextureForString(mLocation, textureTable, LOCATION_STYLE);                  
+                }
+            }
+        }
+        return mLocationImage;
+    }
+}
diff --git a/src/com/cooliris/media/FlatLocalDataSource.java b/src/com/cooliris/media/FlatLocalDataSource.java
new file mode 100644
index 0000000..87caca5
--- /dev/null
+++ b/src/com/cooliris/media/FlatLocalDataSource.java
@@ -0,0 +1,35 @@
+//package com.cooliris.media;
+//
+//// Deprecated class. Need to remove from perforce
+//
+//import java.util.ArrayList;
+//
+//public class FlatLocalDataSource implements MediaFeed.DataSource {
+//    private final boolean mIncludeImages;
+//    private final boolean mIncludeVideos;
+//    
+//    public FlatLocalDataSource(boolean includeImages, boolean includeVideos) {
+//        mIncludeImages = includeImages;
+//        mIncludeVideos = includeVideos;
+//    }
+//    
+//    public DiskCache getThumbnailCache() {
+//        return LocalDataSource.sThumbnailCache;
+//    }
+//
+//    public void loadItemsForSet(MediaFeed feed, MediaSet parentSet, int rangeStart, int rangeEnd) {
+//        // TODO Auto-generated method stub
+//        
+//    }
+//
+//    public void loadMediaSets(MediaFeed feed) {
+//        MediaSet set = feed.addMediaSet(0, this);
+//        set.name = "Local Media";
+//    }
+//
+//    public boolean performOperation(int operation, ArrayList<MediaBucket> mediaBuckets, Object data) {
+//        // TODO Auto-generated method stub
+//        return false;
+//    }
+//
+//}
diff --git a/src/com/cooliris/media/FloatAnim.java b/src/com/cooliris/media/FloatAnim.java
new file mode 100644
index 0000000..a99eccf
--- /dev/null
+++ b/src/com/cooliris/media/FloatAnim.java
@@ -0,0 +1,62 @@
+package com.cooliris.media;
+
+import android.util.FloatMath;
+
+public final class FloatAnim {
+    private float mValue;
+    private float mDelta;
+    private float mDuration;
+    private long mStartTime;
+
+    public FloatAnim(float value) {
+        mValue = value;
+        mStartTime = 0;
+    }
+    
+    public boolean isAnimating() {
+        return mStartTime != 0;
+    }
+    
+    public float getTimeRemaining(long currentTime) {
+        float duration = (currentTime - mStartTime) * 0.001f;
+        if (mDuration > duration)  // CR: braces
+            return mDuration - duration;
+        else
+            return 0.0f;
+    }
+
+    public float getValue(long currentTime) {
+        if (mStartTime == 0) {
+            return mValue;
+        } else {
+            return getInterpolatedValue(currentTime);
+        }
+    }
+
+    public void animateValue(float value, float duration, long currentTime) {
+        mDelta = getValue(currentTime) - value;
+        mValue = value;
+        mDuration = duration;
+        mStartTime = currentTime;
+    }
+
+    public void setValue(float value) {
+        mValue = value;
+        mStartTime = 0;
+    }
+
+    public void skip() {
+        mStartTime = 0;
+    }
+
+    private float getInterpolatedValue(long currentTime) {
+        float ratio = (float) (currentTime - mStartTime) * 0.001f / mDuration;
+        if (ratio >= 1f) {  // CR: 1.0f
+            mStartTime = 0;
+            return mValue;
+        } else {
+            ratio = 0.5f - 0.5f * FloatMath.cos(ratio * 3.14159265f);  // CR: (float)Math.PI
+            return mValue + (1f - ratio) * mDelta;
+        }
+    }
+}
diff --git a/src/com/cooliris/media/FloatUtils.java b/src/com/cooliris/media/FloatUtils.java
new file mode 100644
index 0000000..32bc454
--- /dev/null
+++ b/src/com/cooliris/media/FloatUtils.java
@@ -0,0 +1,130 @@
+package com.cooliris.media;
+
+/**
+ * A static class for some useful operations on Floats and Vectors
+ */
+
+public class FloatUtils {
+    private static final float ANIMATION_SPEED = 4.0f;
+    
+    /**
+     * This function animates a float value to another float value
+     * @param prevVal: The previous value (or the animated value)
+     * @param targetVal: The target value
+     * @param timeElapsed Time elapsed since the last time this function was called
+     * @return The new animated value that is closer to the target value and clamped to the target value
+     */
+    public static final float animate(float prevVal, float targetVal, float timeElapsed) {
+        timeElapsed = timeElapsed * ANIMATION_SPEED;
+        return animateAfterFactoringSpeed(prevVal, targetVal, timeElapsed);
+    }
+    
+    /**
+     * This function animates a Tuple3f value to another Tuple3f value
+     * @param animVal: The animating Tuple
+     * @param targetVal: The target value for the Tuple
+     * @param timeElapsed: Time elapsed since the last time this function was called
+     */
+    public static final void animate(Vector3f animVal, Vector3f targetVal, float timeElapsed) {
+        timeElapsed = timeElapsed * ANIMATION_SPEED;
+        animVal.x = animateAfterFactoringSpeed(animVal.x, targetVal.x, timeElapsed);
+        animVal.y = animateAfterFactoringSpeed(animVal.y, targetVal.y, timeElapsed);
+        animVal.z = animateAfterFactoringSpeed(animVal.z, targetVal.z, timeElapsed);
+    }
+    
+    /**
+     * Clamp a float to a lower bound
+     * @param val: the input float value
+     * @param minVal: the minimum value to use to clamp
+     * @return the clamped value
+     */
+    public static final float clampMin(float val, float minVal) {
+        if (val < minVal)
+            return minVal;  // CR: braces
+        else
+            return val;
+    }
+    
+    /**
+     * Clamp a float to an upper bound
+     * @param val: the input float value
+     * @param maxVal: the maximum value to use to clamp
+     * @return the clamped value
+     */
+    public static final float clampMax(float val, float maxVal) {
+        if (val > maxVal)
+            return maxVal;
+        else
+            return val;
+    }
+    
+    // CR: these comments are barely useful. they mostly just fill space. If anything, a one-liner would be sufficient.
+    /**
+     * Clamp a float to a lower and upper bound
+     * @param val: the input float value
+     * @param minVal: the minimum value to use to clamp
+     * @param maxVal: the maximum value to use to clamp
+     * @return the clamped value
+     */
+    public static final float clamp(float val, float minVal, float maxVal) {
+        if (val < minVal)
+            return minVal;
+        else if (val > maxVal)
+            return maxVal;
+        else
+            return val;
+    }
+    
+    /**
+     * Clamp an integer to a lower and upper bound
+     * @param val: the input float value
+     * @param minVal: the minimum value to use to clamp
+     * @param maxVal: the maximum value to use to clamp
+     * @return the clamped value
+     */
+    public static final int clamp(int val, int minVal, int maxVal) {
+        if (val < minVal)
+            return minVal;
+        else if (val > maxVal)
+            return maxVal;
+        else
+            return val;
+    }
+    
+    /**
+     * Function to check whether a point lies inside a rectangle
+     * @param left: the x coordinate of the left most point
+     * @param right: the x coordinate of the right most point
+     * @param top: the y coordinate of the top most point
+     * @param bottom: the y coordinate of the bottom most point
+     * @param posX: the input point's x coordinate
+     * @param posY: the input point's y coordinate
+     * @return true if point is inside the rectangle else return false
+     */
+    public static final boolean boundsContainsPoint(float left, float right, float top, float bottom, float posX, float posY) {
+      // CR: return ... (one statement).
+        if (posX < left || posX > right || posY < top || posY > bottom)
+            return false;
+        else
+            return true;
+    }
+
+    private static final float animateAfterFactoringSpeed(float prevVal, float targetVal, float timeElapsed) {
+        if (prevVal == targetVal)
+            return targetVal;
+        float newVal = prevVal + ((targetVal - prevVal) * timeElapsed);
+        if (Math.abs(newVal - prevVal) < 0.0001f)
+            return targetVal;
+        if (newVal == prevVal) {
+            return targetVal;
+        } else {  // } else if (...) { ... }; no need for a new level of indentation.
+            if (prevVal > targetVal && newVal < targetVal) {
+                return targetVal;
+            } else if (prevVal < targetVal && newVal > targetVal) {
+                return targetVal;
+            } else {
+                return newVal;
+            }
+        }
+    }
+}
diff --git a/src/com/cooliris/media/GLSurfaceView.java b/src/com/cooliris/media/GLSurfaceView.java
new file mode 100644
index 0000000..96cba0b
--- /dev/null
+++ b/src/com/cooliris/media/GLSurfaceView.java
@@ -0,0 +1,1302 @@
+///*
+// * Copyright (C) 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.
+// */
+//
+//package com.cooliris.media;
+//
+//import java.io.Writer;
+//import java.util.ArrayList;
+//
+//import javax.microedition.khronos.egl.EGL10;
+//import javax.microedition.khronos.egl.EGL11;
+//import javax.microedition.khronos.egl.EGLConfig;
+//import javax.microedition.khronos.egl.EGLContext;
+//import javax.microedition.khronos.egl.EGLDisplay;
+//import javax.microedition.khronos.egl.EGLSurface;
+//import javax.microedition.khronos.opengles.GL;
+//import javax.microedition.khronos.opengles.GL10;
+//
+//import android.content.Context;
+//import android.util.AttributeSet;
+//import android.util.Log;
+//import android.view.SurfaceHolder;
+//import android.view.SurfaceView;
+//
+///**
+// * An implementation of SurfaceView that uses the dedicated surface for
+// * displaying OpenGL rendering.
+// * <p>
+// * A GLSurfaceView provides the following features:
+// * <p>
+// * <ul>
+// * <li>Manages a surface, which is a special piece of memory that can be
+// * composited into the Android view system.
+// * <li>Manages an EGL display, which enables OpenGL to render into a surface.
+// * <li>Accepts a user-provided Renderer object that does the actual rendering.
+// * <li>Renders on a dedicated thread to decouple rendering performance from the
+// * UI thread.
+// * <li>Supports both on-demand and continuous rendering.
+// * <li>Optionally wraps, traces, and/or error-checks the renderer's OpenGL calls.
+// * </ul>
+// *
+// * <h3>Using GLSurfaceView</h3>
+// * <p>
+// * Typically you use GLSurfaceView by subclassing it and overriding one or more of the
+// * View system input event methods. If your application does not need to override event
+// * methods then GLSurfaceView can be used as-is. For the most part
+// * GLSurfaceView behavior is customized by calling "set" methods rather than by subclassing.
+// * For example, unlike a regular View, drawing is delegated to a separate Renderer object which
+// * is registered with the GLSurfaceView
+// * using the {@link #setRenderer(Renderer)} call.
+// * <p>
+// * <h3>Initializing GLSurfaceView</h3>
+// * All you have to do to initialize a GLSurfaceView is call {@link #setRenderer(Renderer)}.
+// * However, if desired, you can modify the default behavior of GLSurfaceView by calling one or
+// * more of these methods before calling setRenderer:
+// * <ul>
+// * <li>{@link #setDebugFlags(int)}
+// * <li>{@link #setEGLConfigChooser(boolean)}
+// * <li>{@link #setEGLConfigChooser(EGLConfigChooser)}
+// * <li>{@link #setEGLConfigChooser(int, int, int, int, int, int)}
+// * <li>{@link #setGLWrapper(GLWrapper)}
+// * </ul>
+// * <p>
+// * <h4>Choosing an EGL Configuration</h4>
+// * A given Android device may support multiple possible types of drawing surfaces.
+// * The available surfaces may differ in how may channels of data are present, as
+// * well as how many bits are allocated to each channel. Therefore, the first thing
+// * GLSurfaceView has to do when starting to render is choose what type of surface to use.
+// * <p>
+// * By default GLSurfaceView chooses an available surface that's closest to a 16-bit R5G6B5 surface
+// * with a 16-bit depth buffer and no stencil. If you would prefer a different surface (for example,
+// * if you do not need a depth buffer) you can override the default behavior by calling one of the
+// * setEGLConfigChooser methods.
+// * <p>
+// * <h4>Debug Behavior</h4>
+// * You can optionally modify the behavior of GLSurfaceView by calling
+// * one or more of the debugging methods {@link #setDebugFlags(int)},
+// * and {@link #setGLWrapper}. These methods may be called before and/or after setRenderer, but
+// * typically they are called before setRenderer so that they take effect immediately.
+// * <p>
+// * <h4>Setting a Renderer</h4>
+// * Finally, you must call {@link #setRenderer} to register a {@link Renderer}.
+// * The renderer is
+// * responsible for doing the actual OpenGL rendering.
+// * <p>
+// * <h3>Rendering Mode</h3>
+// * Once the renderer is set, you can control whether the renderer draws
+// * continuously or on-demand by calling
+// * {@link #setRenderMode}. The default is continuous rendering.
+// * <p>
+// * <h3>Activity Life-cycle</h3>
+// * A GLSurfaceView must be notified when the activity is paused and resumed. GLSurfaceView clients
+// * are required to call {@link #onPause()} when the activity pauses and
+// * {@link #onResume()} when the activity resumes. These calls allow GLSurfaceView to
+// * pause and resume the rendering thread, and also allow GLSurfaceView to release and recreate
+// * the OpenGL display.
+// * <p>
+// * <h3>Handling events</h3>
+// * <p>
+// * To handle an event you will typically subclass GLSurfaceView and override the
+// * appropriate method, just as you would with any other View. However, when handling
+// * the event, you may need to communicate with the Renderer object
+// * that's running in the rendering thread. You can do this using any
+// * standard Java cross-thread communication mechanism. In addition,
+// * one relatively easy way to communicate with your renderer is
+// * to call
+// * {@link #queueEvent(Runnable)}. For example:
+// * <pre class="prettyprint">
+// * class MyGLSurfaceView extends GLSurfaceView {
+// *
+// *     private MyRenderer mMyRenderer;
+// *
+// *     public void start() {
+// *         mMyRenderer = ...;
+// *         setRenderer(mMyRenderer);
+// *     }
+// *
+// *     public boolean onKeyDown(int keyCode, KeyEvent event) {
+// *         if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+// *             queueEvent(new Runnable() {
+// *                 // This method will be called on the rendering
+// *                 // thread:
+// *                 public void run() {
+// *                     mMyRenderer.handleDpadCenter();
+// *                 }});
+// *             return true;
+// *         }
+// *         return super.onKeyDown(keyCode, event);
+// *     }
+// * }
+// * </pre>
+// *
+// */
+//public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
+//    private final static boolean LOG_THREADS = false;
+//    /**
+//     * The renderer only renders
+//     * when the surface is created, or when {@link #requestRender} is called.
+//     *
+//     * @see #getRenderMode()
+//     * @see #setRenderMode(int)
+//     */
+//    public final static int RENDERMODE_WHEN_DIRTY = 0;
+//    /**
+//     * The renderer is called
+//     * continuously to re-render the scene.
+//     *
+//     * @see #getRenderMode()
+//     * @see #setRenderMode(int)
+//     * @see #requestRender()
+//     */
+//    public final static int RENDERMODE_CONTINUOUSLY = 1;
+//
+//    /**
+//     * Check glError() after every GL call and throw an exception if glError indicates
+//     * that an error has occurred. This can be used to help track down which OpenGL ES call
+//     * is causing an error.
+//     *
+//     * @see #getDebugFlags
+//     * @see #setDebugFlags
+//     */
+//    public final static int DEBUG_CHECK_GL_ERROR = 1;
+//
+//    /**
+//     * Log GL calls to the system log at "verbose" level with tag "GLSurfaceView".
+//     *
+//     * @see #getDebugFlags
+//     * @see #setDebugFlags
+//     */
+//    public final static int DEBUG_LOG_GL_CALLS = 2;
+//
+//    /**
+//     * Standard View constructor. In order to render something, you
+//     * must call {@link #setRenderer} to register a renderer.
+//     */
+//    public GLSurfaceView(Context context) {
+//        super(context);
+//        init();
+//    }
+//
+//    /**
+//     * Standard View constructor. In order to render something, you
+//     * must call {@link #setRenderer} to register a renderer.
+//     */
+//    public GLSurfaceView(Context context, AttributeSet attrs) {
+//        super(context, attrs);
+//        init();
+//    }
+//
+//    private void init() {
+//        // Install a SurfaceHolder.Callback so we get notified when the
+//        // underlying surface is created and destroyed
+//        SurfaceHolder holder = getHolder();
+//        holder.addCallback(this);
+//    }
+//
+//    /**
+//     * Set the glWrapper. If the glWrapper is not null, its
+//     * {@link GLWrapper#wrap(GL)} method is called
+//     * whenever a surface is created. A GLWrapper can be used to wrap
+//     * the GL object that's passed to the renderer. Wrapping a GL
+//     * object enables examining and modifying the behavior of the
+//     * GL calls made by the renderer.
+//     * <p>
+//     * Wrapping is typically used for debugging purposes.
+//     * <p>
+//     * The default value is null.
+//     * @param glWrapper the new GLWrapper
+//     */
+//    public void setGLWrapper(GLWrapper glWrapper) {
+//        mGLWrapper = glWrapper;
+//    }
+//
+//    /**
+//     * Set the debug flags to a new value. The value is
+//     * constructed by OR-together zero or more
+//     * of the DEBUG_CHECK_* constants. The debug flags take effect
+//     * whenever a surface is created. The default value is zero.
+//     * @param debugFlags the new debug flags
+//     * @see #DEBUG_CHECK_GL_ERROR
+//     * @see #DEBUG_LOG_GL_CALLS
+//     */
+//    public void setDebugFlags(int debugFlags) {
+//        mDebugFlags = debugFlags;
+//    }
+//
+//    /**
+//     * Get the current value of the debug flags.
+//     * @return the current value of the debug flags.
+//     */
+//    public int getDebugFlags() {
+//        return mDebugFlags;
+//    }
+//
+//    /**
+//     * Set the renderer associated with this view. Also starts the thread that
+//     * will call the renderer, which in turn causes the rendering to start.
+//     * <p>This method should be called once and only once in the life-cycle of
+//     * a GLSurfaceView.
+//     * <p>The following GLSurfaceView methods can only be called <em>before</em>
+//     * setRenderer is called:
+//     * <ul>
+//     * <li>{@link #setEGLConfigChooser(boolean)}
+//     * <li>{@link #setEGLConfigChooser(EGLConfigChooser)}
+//     * <li>{@link #setEGLConfigChooser(int, int, int, int, int, int)}
+//     * </ul>
+//     * <p>
+//     * The following GLSurfaceView methods can only be called <em>after</em>
+//     * setRenderer is called:
+//     * <ul>
+//     * <li>{@link #getRenderMode()}
+//     * <li>{@link #onPause()}
+//     * <li>{@link #onResume()}
+//     * <li>{@link #queueEvent(Runnable)}
+//     * <li>{@link #requestRender()}
+//     * <li>{@link #setRenderMode(int)}
+//     * </ul>
+//     *
+//     * @param renderer the renderer to use to perform OpenGL drawing.
+//     */
+//    public void setRenderer(Renderer renderer) {
+//        checkRenderThreadState();
+//        if (mEGLConfigChooser == null) {
+//            mEGLConfigChooser = new SimpleEGLConfigChooser(true);
+//        }
+//        if (mEGLContextFactory == null) {
+//            mEGLContextFactory = new DefaultContextFactory();
+//        }
+//        if (mEGLWindowSurfaceFactory == null) {
+//            mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
+//        }
+//        mGLThread = new GLThread(renderer);
+//        mGLThread.start();
+//    }
+//
+//    /**
+//     * Install a custom EGLContextFactory.
+//     * <p>If this method is
+//     * called, it must be called before {@link #setRenderer(Renderer)}
+//     * is called.
+//     * <p>
+//     * If this method is not called, then by default
+//     * a context will be created with no shared context and
+//     * with a null attribute list.
+//     */
+//    public void setEGLContextFactory(EGLContextFactory factory) {
+//        checkRenderThreadState();
+//        mEGLContextFactory = factory;
+//    }
+//
+//    /**
+//     * Install a custom EGLWindowSurfaceFactory.
+//     * <p>If this method is
+//     * called, it must be called before {@link #setRenderer(Renderer)}
+//     * is called.
+//     * <p>
+//     * If this method is not called, then by default
+//     * a window surface will be created with a null attribute list.
+//     */
+//    public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) {
+//        checkRenderThreadState();
+//        mEGLWindowSurfaceFactory = factory;
+//    }
+//
+//    /**
+//     * Install a custom EGLConfigChooser.
+//     * <p>If this method is
+//     * called, it must be called before {@link #setRenderer(Renderer)}
+//     * is called.
+//     * <p>
+//     * If no setEGLConfigChooser method is called, then by default the
+//     * view will choose a config as close to 16-bit RGB as possible, with
+//     * a depth buffer as close to 16 bits as possible.
+//     * @param configChooser
+//     */
+//    public void setEGLConfigChooser(EGLConfigChooser configChooser) {
+//        checkRenderThreadState();
+//        mEGLConfigChooser = configChooser;
+//    }
+//
+//    /**
+//     * Install a config chooser which will choose a config
+//     * as close to 16-bit RGB as possible, with or without an optional depth
+//     * buffer as close to 16-bits as possible.
+//     * <p>If this method is
+//     * called, it must be called before {@link #setRenderer(Renderer)}
+//     * is called.
+//     * <p>
+//      * If no setEGLConfigChooser method is called, then by default the
+//     * view will choose a config as close to 16-bit RGB as possible, with
+//     * a depth buffer as close to 16 bits as possible.
+//     *
+//     * @param needDepth
+//     */
+//    public void setEGLConfigChooser(boolean needDepth) {
+//        setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth));
+//    }
+//
+//    /**
+//     * Install a config chooser which will choose a config
+//     * with at least the specified component sizes, and as close
+//     * to the specified component sizes as possible.
+//     * <p>If this method is
+//     * called, it must be called before {@link #setRenderer(Renderer)}
+//     * is called.
+//     * <p>
+//     * If no setEGLConfigChooser method is called, then by default the
+//     * view will choose a config as close to 16-bit RGB as possible, with
+//     * a depth buffer as close to 16 bits as possible.
+//     *
+//     */
+//    public void setEGLConfigChooser(int redSize, int greenSize, int blueSize,
+//            int alphaSize, int depthSize, int stencilSize) {
+//        setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize,
+//                blueSize, alphaSize, depthSize, stencilSize));
+//    }
+//    /**
+//     * Set the rendering mode. When renderMode is
+//     * RENDERMODE_CONTINUOUSLY, the renderer is called
+//     * repeatedly to re-render the scene. When renderMode
+//     * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface
+//     * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY.
+//     * <p>
+//     * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance
+//     * by allowing the GPU and CPU to idle when the view does not need to be updated.
+//     * <p>
+//     * This method can only be called after {@link #setRenderer(Renderer)}
+//     *
+//     * @param renderMode one of the RENDERMODE_X constants
+//     * @see #RENDERMODE_CONTINUOUSLY
+//     * @see #RENDERMODE_WHEN_DIRTY
+//     */
+//    public void setRenderMode(int renderMode) {
+//        mGLThread.setRenderMode(renderMode);
+//    }
+//
+//    /**
+//     * Get the current rendering mode. May be called
+//     * from any thread. Must not be called before a renderer has been set.
+//     * @return the current rendering mode.
+//     * @see #RENDERMODE_CONTINUOUSLY
+//     * @see #RENDERMODE_WHEN_DIRTY
+//     */
+//    public int getRenderMode() {
+//        return mGLThread.getRenderMode();
+//    }
+//
+//    /**
+//     * Request that the renderer render a frame.
+//     * This method is typically used when the render mode has been set to
+//     * {@link #RENDERMODE_WHEN_DIRTY}, so that frames are only rendered on demand.
+//     * May be called
+//     * from any thread. Must not be called before a renderer has been set.
+//     */
+//    public void requestRender() {
+//        mGLThread.requestRender();
+//    }
+//
+//    /**
+//     * This method is part of the SurfaceHolder.Callback interface, and is
+//     * not normally called or subclassed by clients of GLSurfaceView.
+//     */
+//    public void surfaceCreated(SurfaceHolder holder) {
+//        mGLThread.surfaceCreated();
+//    }
+//
+//    /**
+//     * This method is part of the SurfaceHolder.Callback interface, and is
+//     * not normally called or subclassed by clients of GLSurfaceView.
+//     */
+//    public void surfaceDestroyed(SurfaceHolder holder) {
+//        // Surface will be destroyed when we return
+//        mGLThread.surfaceDestroyed();
+//    }
+//
+//    /**
+//     * This method is part of the SurfaceHolder.Callback interface, and is
+//     * not normally called or subclassed by clients of GLSurfaceView.
+//     */
+//    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
+//        mGLThread.onWindowResize(w, h);
+//    }
+//
+//    /**
+//     * Inform the view that the activity is paused. The owner of this view must
+//     * call this method when the activity is paused. Calling this method will
+//     * pause the rendering thread.
+//     * Must not be called before a renderer has been set.
+//     */
+//    public void onPause() {
+//        mGLThread.onPause();
+//    }
+//
+//    /**
+//     * Inform the view that the activity is resumed. The owner of this view must
+//     * call this method when the activity is resumed. Calling this method will
+//     * recreate the OpenGL display and resume the rendering
+//     * thread.
+//     * Must not be called before a renderer has been set.
+//     */
+//    public void onResume() {
+//        mGLThread.onResume();
+//    }
+//
+//    /**
+//     * Queue a runnable to be run on the GL rendering thread. This can be used
+//     * to communicate with the Renderer on the rendering thread.
+//     * Must not be called before a renderer has been set.
+//     * @param r the runnable to be run on the GL rendering thread.
+//     */
+//    public void queueEvent(Runnable r) {
+//        mGLThread.queueEvent(r);
+//    }
+//
+//    /**
+//     * This method is used as part of the View class and is not normally
+//     * called or subclassed by clients of GLSurfaceView.
+//     * Must not be called before a renderer has been set.
+//     */
+//    @Override
+//    protected void onDetachedFromWindow() {
+//        super.onDetachedFromWindow();
+//        mGLThread.requestExitAndWait();
+//    }
+//
+//    // ----------------------------------------------------------------------
+//
+//    /**
+//     * An interface used to wrap a GL interface.
+//     * <p>Typically
+//     * used for implementing debugging and tracing on top of the default
+//     * GL interface. You would typically use this by creating your own class
+//     * that implemented all the GL methods by delegating to another GL instance.
+//     * Then you could add your own behavior before or after calling the
+//     * delegate. All the GLWrapper would do was instantiate and return the
+//     * wrapper GL instance:
+//     * <pre class="prettyprint">
+//     * class MyGLWrapper implements GLWrapper {
+//     *     GL wrap(GL gl) {
+//     *         return new MyGLImplementation(gl);
+//     *     }
+//     *     static class MyGLImplementation implements GL,GL10,GL11,... {
+//     *         ...
+//     *     }
+//     * }
+//     * </pre>
+//     * @see #setGLWrapper(GLWrapper)
+//     */
+//    public interface GLWrapper {
+//        /**
+//         * Wraps a gl interface in another gl interface.
+//         * @param gl a GL interface that is to be wrapped.
+//         * @return either the input argument or another GL object that wraps the input argument.
+//         */
+//        GL wrap(GL gl);
+//    }
+//
+//    /**
+//     * A generic renderer interface.
+//     * <p>
+//     * The renderer is responsible for making OpenGL calls to render a frame.
+//     * <p>
+//     * GLSurfaceView clients typically create their own classes that implement
+//     * this interface, and then call {@link GLSurfaceView#setRenderer} to
+//     * register the renderer with the GLSurfaceView.
+//     * <p>
+//     * <h3>Threading</h3>
+//     * The renderer will be called on a separate thread, so that rendering
+//     * performance is decoupled from the UI thread. Clients typically need to
+//     * communicate with the renderer from the UI thread, because that's where
+//     * input events are received. Clients can communicate using any of the
+//     * standard Java techniques for cross-thread communication, or they can
+//     * use the {@link GLSurfaceView#queueEvent(Runnable)} convenience method.
+//     * <p>
+//     * <h3>EGL Context Lost</h3>
+//     * There are situations where the EGL rendering context will be lost. This
+//     * typically happens when device wakes up after going to sleep. When
+//     * the EGL context is lost, all OpenGL resources (such as textures) that are
+//     * associated with that context will be automatically deleted. In order to
+//     * keep rendering correctly, a renderer must recreate any lost resources
+//     * that it still needs. The {@link #onSurfaceCreated(GL10, EGLConfig)} method
+//     * is a convenient place to do this.
+//     *
+//     *
+//     * @see #setRenderer(Renderer)
+//     */
+//    public interface Renderer {
+//        /**
+//         * Called when the surface is created or recreated.
+//         * <p>
+//         * Called when the rendering thread
+//         * starts and whenever the EGL context is lost. The context will typically
+//         * be lost when the Android device awakes after going to sleep.
+//         * <p>
+//         * Since this method is called at the beginning of rendering, as well as
+//         * every time the EGL context is lost, this method is a convenient place to put
+//         * code to create resources that need to be created when the rendering
+//         * starts, and that need to be recreated when the EGL context is lost.
+//         * Textures are an example of a resource that you might want to create
+//         * here.
+//         * <p>
+//         * Note that when the EGL context is lost, all OpenGL resources associated
+//         * with that context will be automatically deleted. You do not need to call
+//         * the corresponding "glDelete" methods such as glDeleteTextures to
+//         * manually delete these lost resources.
+//         * <p>
+//         * @param gl the GL interface. Use <code>instanceof</code> to
+//         * test if the interface supports GL11 or higher interfaces.
+//         * @param config the EGLConfig of the created surface. Can be used
+//         * to create matching pbuffers.
+//         */
+//        void onSurfaceCreated(GL10 gl, EGLConfig config);
+//
+//        /**
+//         * Called when the surface changed size.
+//         * <p>
+//         * Called after the surface is created and whenever
+//         * the OpenGL ES surface size changes.
+//         * <p>
+//         * Typically you will set your viewport here. If your camera
+//         * is fixed then you could also set your projection matrix here:
+//         * <pre class="prettyprint">
+//         * void onSurfaceChanged(GL10 gl, int width, int height) {
+//         *     gl.glViewport(0, 0, width, height);
+//         *     // for a fixed camera, set the projection too
+//         *     float ratio = (float) width / height;
+//         *     gl.glMatrixMode(GL10.GL_PROJECTION);
+//         *     gl.glLoadIdentity();
+//         *     gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
+//         * }
+//         * </pre>
+//         * @param gl the GL interface. Use <code>instanceof</code> to
+//         * test if the interface supports GL11 or higher interfaces.
+//         * @param width
+//         * @param height
+//         */
+//        void onSurfaceChanged(GL10 gl, int width, int height);
+//
+//        /**
+//         * Called to draw the current frame.
+//         * <p>
+//         * This method is responsible for drawing the current frame.
+//         * <p>
+//         * The implementation of this method typically looks like this:
+//         * <pre class="prettyprint">
+//         * void onDrawFrame(GL10 gl) {
+//         *     gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
+//         *     //... other gl calls to render the scene ...
+//         * }
+//         * </pre>
+//         * @param gl the GL interface. Use <code>instanceof</code> to
+//         * test if the interface supports GL11 or higher interfaces.
+//         */
+//        void onDrawFrame(GL10 gl);
+//    }
+//
+//    /**
+//     * An interface for customizing the eglCreateContext and eglDestroyContext calls.
+//     * <p>
+//     * This interface must be implemented by clients wishing to call
+//     * {@link GLSurfaceView#setEGLContextFactory(EGLContextFactory)}
+//     */
+//    public interface EGLContextFactory {
+//        EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig);
+//        void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context);
+//    }
+//
+//    private static class DefaultContextFactory implements EGLContextFactory {
+//
+//        public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) {
+//            return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, null);
+//        }
+//
+//        public void destroyContext(EGL10 egl, EGLDisplay display,
+//                EGLContext context) {
+//            egl.eglDestroyContext(display, context);
+//        }
+//    }
+//
+//    /**
+//     * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls.
+//     * <p>
+//     * This interface must be implemented by clients wishing to call
+//     * {@link GLSurfaceView#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)}
+//     */
+//    public interface EGLWindowSurfaceFactory {
+//        EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config,
+//                Object nativeWindow);
+//        void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface);
+//    }
+//
+//    private static class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory {
+//
+//        public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display,
+//                EGLConfig config, Object nativeWindow) {
+//            return egl.eglCreateWindowSurface(display, config, nativeWindow, null);
+//        }
+//
+//        public void destroySurface(EGL10 egl, EGLDisplay display,
+//                EGLSurface surface) {
+//            egl.eglDestroySurface(display, surface);
+//        }
+//    }
+//
+//    /**
+//     * An interface for choosing an EGLConfig configuration from a list of
+//     * potential configurations.
+//     * <p>
+//     * This interface must be implemented by clients wishing to call
+//     * {@link GLSurfaceView#setEGLConfigChooser(EGLConfigChooser)}
+//     */
+//    public interface EGLConfigChooser {
+//        /**
+//         * Choose a configuration from the list. Implementors typically
+//         * implement this method by calling
+//         * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the
+//         * EGL specification available from The Khronos Group to learn how to call eglChooseConfig.
+//         * @param egl the EGL10 for the current display.
+//         * @param display the current display.
+//         * @return the chosen configuration.
+//         */
+//        EGLConfig chooseConfig(EGL10 egl, EGLDisplay display);
+//    }
+//
+//    private static abstract class BaseConfigChooser
+//            implements EGLConfigChooser {
+//        public BaseConfigChooser(int[] configSpec) {
+//            mConfigSpec = configSpec;
+//        }
+//        public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
+//            int[] num_config = new int[1];
+//            egl.eglChooseConfig(display, mConfigSpec, null, 0, num_config);
+//
+//            int numConfigs = num_config[0];
+//
+//            if (numConfigs <= 0) {
+//                throw new IllegalArgumentException(
+//                        "No configs match configSpec");
+//            }
+//
+//            EGLConfig[] configs = new EGLConfig[numConfigs];
+//            egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs,
+//                    num_config);
+//            EGLConfig config = chooseConfig(egl, display, configs);
+//            if (config == null) {
+//                throw new IllegalArgumentException("No config chosen");
+//            }
+//            return config;
+//        }
+//
+//        abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display,
+//                EGLConfig[] configs);
+//
+//        protected int[] mConfigSpec;
+//    }
+//
+//    private static class ComponentSizeChooser extends BaseConfigChooser {
+//        public ComponentSizeChooser(int redSize, int greenSize, int blueSize,
+//                int alphaSize, int depthSize, int stencilSize) {
+//            super(new int[] {
+//                    EGL10.EGL_RED_SIZE, redSize,
+//                    EGL10.EGL_GREEN_SIZE, greenSize,
+//                    EGL10.EGL_BLUE_SIZE, blueSize,
+//                    EGL10.EGL_ALPHA_SIZE, alphaSize,
+//                    EGL10.EGL_DEPTH_SIZE, depthSize,
+//                    EGL10.EGL_STENCIL_SIZE, stencilSize,
+//                    EGL10.EGL_NONE});
+//            mValue = new int[1];
+//            mRedSize = redSize;
+//            mGreenSize = greenSize;
+//            mBlueSize = blueSize;
+//            mAlphaSize = alphaSize;
+//            mDepthSize = depthSize;
+//            mStencilSize = stencilSize;
+//       }
+//
+//        @Override
+//        public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display,
+//                EGLConfig[] configs) {
+//            EGLConfig closestConfig = null;
+//            int closestDistance = 1000;
+//            for(EGLConfig config : configs) {
+//                int d = findConfigAttrib(egl, display, config,
+//                        EGL10.EGL_DEPTH_SIZE, 0);
+//                int s = findConfigAttrib(egl, display, config,
+//                        EGL10.EGL_STENCIL_SIZE, 0);
+//                if (d >= mDepthSize && s>= mStencilSize) {
+//                    int r = findConfigAttrib(egl, display, config,
+//                            EGL10.EGL_RED_SIZE, 0);
+//                    int g = findConfigAttrib(egl, display, config,
+//                             EGL10.EGL_GREEN_SIZE, 0);
+//                    int b = findConfigAttrib(egl, display, config,
+//                              EGL10.EGL_BLUE_SIZE, 0);
+//                    int a = findConfigAttrib(egl, display, config,
+//                            EGL10.EGL_ALPHA_SIZE, 0);
+//                    int distance = Math.abs(r - mRedSize)
+//                                + Math.abs(g - mGreenSize)
+//                                + Math.abs(b - mBlueSize)
+//                                + Math.abs(a - mAlphaSize);
+//                    if (distance < closestDistance) {
+//                        closestDistance = distance;
+//                        closestConfig = config;
+//                    }
+//                }
+//            }
+//            return closestConfig;
+//        }
+//
+//        private int findConfigAttrib(EGL10 egl, EGLDisplay display,
+//                EGLConfig config, int attribute, int defaultValue) {
+//
+//            if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) {
+//                return mValue[0];
+//            }
+//            return defaultValue;
+//        }
+//
+//        private int[] mValue;
+//        // Subclasses can adjust these values:
+//        protected int mRedSize;
+//        protected int mGreenSize;
+//        protected int mBlueSize;
+//        protected int mAlphaSize;
+//        protected int mDepthSize;
+//        protected int mStencilSize;
+//        }
+//
+//    /**
+//     * This class will choose a supported surface as close to
+//     * RGB565 as possible, with or without a depth buffer.
+//     *
+//     */
+//    private static class SimpleEGLConfigChooser extends ComponentSizeChooser {
+//        public SimpleEGLConfigChooser(boolean withDepthBuffer) {
+//            super(4, 4, 4, 0, withDepthBuffer ? 16 : 0, 0);
+//            // Adjust target values. This way we'll accept a 4444 or
+//            // 555 buffer if there's no 565 buffer available.
+//            mRedSize = 5;
+//            mGreenSize = 6;
+//            mBlueSize = 5;
+//        }
+//    }
+//
+//    /**
+//     * An EGL helper class.
+//     */
+//
+//    private class EglHelper {
+//        public EglHelper() {
+//
+//        }
+//
+//        /**
+//         * Initialize EGL for a given configuration spec.
+//         * @param configSpec
+//         */
+//        public void start(){
+//            /*
+//             * Get an EGL instance
+//             */
+//            mEgl = (EGL10) EGLContext.getEGL();
+//
+//            /*
+//             * Get to the default display.
+//             */
+//            mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+//
+//            /*
+//             * We can now initialize EGL for that display
+//             */
+//            int[] version = new int[2];
+//            mEgl.eglInitialize(mEglDisplay, version);
+//            mEglConfig = mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay);
+//
+//            /*
+//            * Create an OpenGL ES context. This must be done only once, an
+//            * OpenGL context is a somewhat heavy object.
+//            */
+//            mEglContext = mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig);
+//            if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
+//                throw new RuntimeException("createContext failed");
+//            }
+//
+//            mEglSurface = null;
+//        }
+//
+//        /*
+//         * React to the creation of a new surface by creating and returning an
+//         * OpenGL interface that renders to that surface.
+//         */
+//        public GL createSurface(SurfaceHolder holder) {
+//            /*
+//             *  The window size has changed, so we need to create a new
+//             *  surface.
+//             */
+//            if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
+//
+//                /*
+//                 * Unbind and destroy the old EGL surface, if
+//                 * there is one.
+//                 */
+//                mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
+//                        EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
+//                mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface);
+//            }
+//
+//            /*
+//             * Create an EGL surface we can render into.
+//             */
+//            mEglSurface = mEGLWindowSurfaceFactory.createWindowSurface(mEgl,
+//                    mEglDisplay, mEglConfig, holder);
+//
+//            if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
+//                throw new RuntimeException("createWindowSurface failed");
+//            }
+//
+//            /*
+//             * Before we can issue GL commands, we need to make sure
+//             * the context is current and bound to a surface.
+//             */
+//            if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
+//                throw new RuntimeException("eglMakeCurrent failed.");
+//            }
+//
+//            GL gl = mEglContext.getGL();
+//            if (mGLWrapper != null) {
+//                gl = mGLWrapper.wrap(gl);
+//            }
+//
+//            if ((mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS)) != 0) {
+//                int configFlags = 0;
+//                Writer log = null;
+//                if ((mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) {
+//                    //configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR;
+//                }
+//                if ((mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) {
+//                    log = new LogWriter();
+//                }
+//                //gl = GLDebugHelper.wrap(gl, configFlags, log);
+//            }
+//            return gl;
+//        }
+//
+//        /**
+//         * Display the current render surface.
+//         * @return false if the context has been lost.
+//         */
+//        public boolean swap() {
+//            mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
+//
+//            /*
+//             * Always check for EGL_CONTEXT_LOST, which means the context
+//             * and all associated data were lost (For instance because
+//             * the device went to sleep). We need to sleep until we
+//             * get a new surface.
+//             */
+//            return mEgl.eglGetError() != EGL11.EGL_CONTEXT_LOST;
+//        }
+//
+//        public void destroySurface() {
+//            if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
+//                mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
+//                        EGL10.EGL_NO_SURFACE,
+//                        EGL10.EGL_NO_CONTEXT);
+//                mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface);
+//                mEglSurface = null;
+//            }
+//        }
+//
+//        public void finish() {
+//            if (mEglContext != null) {
+//                mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext);
+//                mEglContext = null;
+//            }
+//            if (mEglDisplay != null) {
+//                mEgl.eglTerminate(mEglDisplay);
+//                mEglDisplay = null;
+//            }
+//        }
+//
+//        EGL10 mEgl;
+//        EGLDisplay mEglDisplay;
+//        EGLSurface mEglSurface;
+//        EGLConfig mEglConfig;
+//        EGLContext mEglContext;
+//    }
+//
+//    /**
+//     * A generic GL Thread. Takes care of initializing EGL and GL. Delegates
+//     * to a Renderer instance to do the actual drawing. Can be configured to
+//     * render continuously or on request.
+//     *
+//     * All potentially blocking synchronization is done through the
+//     * sGLThreadManager object. This avoids multiple-lock ordering issues.
+//     *
+//     */
+//    class GLThread extends Thread {
+//        GLThread(Renderer renderer) {
+//            super();
+//            mWidth = 0;
+//            mHeight = 0;
+//            mRequestRender = true;
+//            mRenderMode = RENDERMODE_CONTINUOUSLY;
+//            mRenderer = renderer;
+//        }
+//
+//        @Override
+//        public void run() {
+//            setName("GLThread " + getId());
+//            if (LOG_THREADS) {
+//                Log.i("GLThread", "starting tid=" + getId());
+//            }
+//
+//            try {
+//                guardedRun();
+//            } catch (InterruptedException e) {
+//                // fall thru and exit normally
+//            } finally {
+//                sGLThreadManager.threadExiting(this);
+//            }
+//        }
+//
+//        /*
+//         * This private method should only be called inside a
+//         * synchronized(sGLThreadManager) block.
+//         */
+//        private void stopEglLocked() {
+//            if (mHaveEgl) {
+//                mHaveEgl = false;
+//                mEglHelper.destroySurface();
+//                mEglHelper.finish();
+//                sGLThreadManager.releaseEglSurfaceLocked(this);
+//            }
+//        }
+//
+//        private void guardedRun() throws InterruptedException {
+//            mEglHelper = new EglHelper();
+//            try {
+//                GL10 gl = null;
+//                boolean createEglSurface = false;
+//                int w = 0;
+//                int h = 0;
+//                Runnable event = null;
+//
+//                while (true) {
+//                    synchronized (sGLThreadManager) {
+//                        while (true) {
+//                            if (mShouldExit) {
+//                                return;
+//                            }
+//
+//                            if (! mEventQueue.isEmpty()) {
+//                                event = mEventQueue.remove(0);
+//                                break;
+//                            }
+//
+//                            // Do we need to release the EGL surface?
+//                            if (mHaveEgl && mPaused) {
+//                                stopEglLocked();
+//                            }
+//
+//                            // Have we lost the surface view surface?
+//                            if ((! mHasSurface) && (! mWaitingForSurface)) {
+//                                if (mHaveEgl) {
+//                                    stopEglLocked();
+//                                }
+//                                mWaitingForSurface = true;
+//                                sGLThreadManager.notifyAll();
+//                            }
+//
+//                            // Have we acquired the surface view surface?
+//                            if (mHasSurface && mWaitingForSurface) {
+//                                mWaitingForSurface = false;
+//                                sGLThreadManager.notifyAll();
+//                            }
+//
+//                            // Ready to draw?
+//                            if ((!mPaused) && mHasSurface
+//                                && (mWidth > 0) && (mHeight > 0)
+//                                && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY))) {
+//
+//                                // If we don't have an egl surface, try to acquire one.
+//                                if ((! mHaveEgl) && sGLThreadManager.tryAcquireEglSurfaceLocked(this)) {
+//                                    mHaveEgl = true;
+//                                    mEglHelper.start();
+//                                    createEglSurface = true;
+//                                    sGLThreadManager.notifyAll();
+//                                }
+//
+//                                if (mHaveEgl) {
+//                                    if ( mSizeChanged) {
+//                                        createEglSurface = true;
+//                                        w = mWidth;
+//                                        h = mHeight;
+//                                        mSizeChanged = false;
+//                                    }
+//                                    mRequestRender = false;
+//                                    sGLThreadManager.notifyAll();
+//                                    break;
+//                                }
+//                            }
+//
+//                            // By design, this is the only place in a GLThread thread where we wait().
+//                            if (LOG_THREADS) {
+//                                Log.i("GLThread", "waiting tid=" + getId());
+//                            }
+//                            sGLThreadManager.wait();
+//                        }
+//                    } // end of synchronized(sGLThreadManager)
+//
+//                    if (event != null) {
+//                        event.run();
+//                        event = null;
+//                        continue;
+//                    }
+//
+//                    if (createEglSurface) {
+//                        gl = (GL10) mEglHelper.createSurface(getHolder());
+//                        mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
+//                        mRenderer.onSurfaceChanged(gl, w, h);
+//                        createEglSurface = false;
+//                    }
+//
+//                    mRenderer.onDrawFrame(gl);
+//                    mEglHelper.swap();
+//                 }
+//            } finally {
+//                /*
+//                 * clean-up everything...
+//                 */
+//                synchronized (sGLThreadManager) {
+//                    stopEglLocked();
+//                }
+//            }
+//        }
+//
+//        public void setRenderMode(int renderMode) {
+//            if ( !((RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= RENDERMODE_CONTINUOUSLY)) ) {
+//                throw new IllegalArgumentException("renderMode");
+//            }
+//            synchronized(sGLThreadManager) {
+//                mRenderMode = renderMode;
+//                sGLThreadManager.notifyAll();
+//            }
+//        }
+//
+//        public int getRenderMode() {
+//            synchronized(sGLThreadManager) {
+//                return mRenderMode;
+//            }
+//        }
+//
+//        public void requestRender() {
+//            synchronized(sGLThreadManager) {
+//                mRequestRender = true;
+//                sGLThreadManager.notifyAll();
+//            }
+//        }
+//
+//        public void surfaceCreated() {
+//            synchronized(sGLThreadManager) {
+//                if (LOG_THREADS) {
+//                    Log.i("GLThread", "surfaceCreated tid=" + getId());
+//                }
+//                mHasSurface = true;
+//                sGLThreadManager.notifyAll();
+//            }
+//        }
+//
+//        public void surfaceDestroyed() {
+//            synchronized(sGLThreadManager) {
+//                if (LOG_THREADS) {
+//                    Log.i("GLThread", "surfaceDestroyed tid=" + getId());
+//                }
+//                mHasSurface = false;
+//                sGLThreadManager.notifyAll();
+//                while((!mWaitingForSurface) && (!mExited)) {
+//                    try {
+//                        sGLThreadManager.wait();
+//                    } catch (InterruptedException e) {
+//                        Thread.currentThread().interrupt();
+//                    }
+//                }
+//            }
+//        }
+//
+//        public void onPause() {
+//            synchronized (sGLThreadManager) {
+//                mPaused = true;
+//                sGLThreadManager.notifyAll();
+//            }
+//        }
+//
+//        public void onResume() {
+//            synchronized (sGLThreadManager) {
+//                mPaused = false;
+//                mRequestRender = true;
+//                sGLThreadManager.notifyAll();
+//            }
+//        }
+//
+//        public void onWindowResize(int w, int h) {
+//            synchronized (sGLThreadManager) {
+//                mWidth = w;
+//                mHeight = h;
+//                mSizeChanged = true;
+//                sGLThreadManager.notifyAll();
+//            }
+//        }
+//
+//        public void requestExitAndWait() {
+//            // don't call this from GLThread thread or it is a guaranteed
+//            // deadlock!
+//            synchronized(sGLThreadManager) {
+//                mShouldExit = true;
+//                sGLThreadManager.notifyAll();
+//                while (! mExited) {
+//                    try {
+//                        sGLThreadManager.wait();
+//                    } catch (InterruptedException ex) {
+//                        Thread.currentThread().interrupt();
+//                    }
+//                }
+//            }
+//        }
+//
+//        /**
+//         * Queue an "event" to be run on the GL rendering thread.
+//         * @param r the runnable to be run on the GL rendering thread.
+//         */
+//        public void queueEvent(Runnable r) {
+//            if (r == null) {
+//                throw new IllegalArgumentException("r must not be null");
+//            }
+//            synchronized(sGLThreadManager) {
+//                mEventQueue.add(r);
+//                sGLThreadManager.notifyAll();
+//            }
+//        }
+//
+//        // Once the thread is started, all accesses to the following member
+//        // variables are protected by the sGLThreadManager monitor
+//        private boolean mShouldExit;
+//        private boolean mExited;
+//        private boolean mPaused;
+//        private boolean mHasSurface;
+//        private boolean mWaitingForSurface;
+//        private boolean mHaveEgl;
+//        private int mWidth;
+//        private int mHeight;
+//        private int mRenderMode;
+//        private boolean mRequestRender;
+//        private ArrayList<Runnable> mEventQueue = new ArrayList<Runnable>();
+//        // End of member variables protected by the sGLThreadManager monitor.
+//
+//        private Renderer mRenderer;
+//        private EglHelper mEglHelper;
+//    }
+//
+//    static class LogWriter extends Writer {
+//
+//        @Override public void close() {
+//            flushBuilder();
+//        }
+//
+//        @Override public void flush() {
+//            flushBuilder();
+//        }
+//
+//        @Override public void write(char[] buf, int offset, int count) {
+//            for(int i = 0; i < count; i++) {
+//                char c = buf[offset + i];
+//                if ( c == '\n') {
+//                    flushBuilder();
+//                }
+//                else {
+//                    mBuilder.append(c);
+//                }
+//            }
+//        }
+//
+//        private void flushBuilder() {
+//            if (mBuilder.length() > 0) {
+//                Log.v("GLSurfaceView", mBuilder.toString());
+//                mBuilder.delete(0, mBuilder.length());
+//            }
+//        }
+//
+//        private StringBuilder mBuilder = new StringBuilder();
+//    }
+//
+//
+//    private void checkRenderThreadState() {
+//        if (mGLThread != null) {
+//            throw new IllegalStateException(
+//                    "setRenderer has already been called for this instance.");
+//        }
+//    }
+//
+//    private static class GLThreadManager {
+//
+//        public synchronized void threadExiting(GLThread thread) {
+//            if (LOG_THREADS) {
+//                Log.i("GLThread", "exiting tid=" +  thread.getId());
+//            }
+//            thread.mExited = true;
+//            if (mEglOwner == thread) {
+//                mEglOwner = null;
+//            }
+//            notifyAll();
+//        }
+//
+//        /*
+//         * Tries once to acquire the right to use an EGL
+//         * surface. Does not block. Requires that we are already
+//         * in the sGLThreadManager monitor when this is called.
+//         * @return true if the right to use an EGL surface was acquired.
+//         */
+//        public boolean tryAcquireEglSurfaceLocked(GLThread thread) {
+//            if (mEglOwner == thread || mEglOwner == null) {
+//                mEglOwner = thread;
+//                notifyAll();
+//                return true;
+//            }
+//            return false;
+//        }
+//        /*
+//         * Releases the EGL surface. Requires that we are already in the
+//         * sGLThreadManager monitor when this is called.
+//         */
+//        public void releaseEglSurfaceLocked(GLThread thread) {
+//            if (mEglOwner == thread) {
+//                mEglOwner = null;
+//            }
+//            notifyAll();
+//        }
+//
+//        private GLThread mEglOwner;
+//    }
+//
+//    private static final GLThreadManager sGLThreadManager = new GLThreadManager();
+//    private boolean mSizeChanged = true;
+//
+//    private GLThread mGLThread;
+//    private EGLConfigChooser mEGLConfigChooser;
+//    private EGLContextFactory mEGLContextFactory;
+//    private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory;
+//    private GLWrapper mGLWrapper;
+//    private int mDebugFlags;
+//}
diff --git a/src/com/cooliris/media/Gallery.java b/src/com/cooliris/media/Gallery.java
new file mode 100644
index 0000000..9e40582
--- /dev/null
+++ b/src/com/cooliris/media/Gallery.java
@@ -0,0 +1,334 @@
+package com.cooliris.media;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.TimeZone;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.widget.Toast;
+import android.media.MediaScannerConnection;
+
+import com.cooliris.cache.CacheService;
+
+public final class Gallery extends Activity {
+    public static final TimeZone CURRENT_TIME_ZONE = TimeZone.getDefault();
+    public static float PIXEL_DENSITY = 1.0f;
+    public static int DENSITY_DPI = DisplayMetrics.DENSITY_DEFAULT;
+    public static boolean NEEDS_REFRESH = true;
+    public static final int CROP_MSG_INTERNAL = 100;
+    private static final String TAG = "Gallery";
+    private static final int CROP_MSG = 10;
+    private RenderView mRenderView = null;
+    private GridLayer mGridLayer;
+    private final Handler mHandler = new Handler();
+    private ReverseGeocoder mReverseGeocoder;
+    private boolean mPause;
+    private MediaScannerConnection mConnection;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        boolean isCacheReady = CacheService.isCacheReady(false);
+        CacheService.startCache(this, false);
+        mReverseGeocoder = new ReverseGeocoder(this);
+        DisplayMetrics metrics = new DisplayMetrics();
+        getWindowManager().getDefaultDisplay().getMetrics(metrics);
+        PIXEL_DENSITY = metrics.density;
+        mRenderView = new RenderView(this);
+        mGridLayer = new GridLayer(this, (int) (96.0f * PIXEL_DENSITY), (int) (72.0f * PIXEL_DENSITY), new GridLayoutInterface(4),
+                mRenderView);
+        mRenderView.setRootLayer(mGridLayer);
+        setContentView(mRenderView);
+        final boolean imageManagerHasStorage = ImageManager.quickHasStorage();
+        if (!isPickIntent() && !isViewIntent()) {
+            PicasaDataSource picasaDataSource = new PicasaDataSource(this);
+            if (imageManagerHasStorage) {
+                LocalDataSource localDataSource = new LocalDataSource(this);
+                ConcatenatedDataSource combinedDataSource = new ConcatenatedDataSource(localDataSource, picasaDataSource);
+                mGridLayer.setDataSource(combinedDataSource);
+            } else {
+                mGridLayer.setDataSource(picasaDataSource);
+            }
+            if (!imageManagerHasStorage) {
+                Toast.makeText(this, getResources().getString(R.string.no_sd_card), Toast.LENGTH_LONG).show();
+            } else {
+                if (!isCacheReady) {
+                    Toast.makeText(this, getResources().getString(R.string.loading_new), Toast.LENGTH_LONG).show();
+                } else {
+                    // Toast.makeText(this, getResources().getString(R.string.initializing), Toast.LENGTH_SHORT).show();
+                }
+            }
+        } else if (!isViewIntent()) {
+            Intent intent = getIntent();
+            if (intent != null) {
+                String type = intent.resolveType(this);
+                boolean includeImages = isImageType(type);
+                boolean includeVideos = isVideoType(type);
+                LocalDataSource localDataSource = new LocalDataSource(this);
+                ((LocalDataSource) localDataSource).setMimeFilter(!includeImages, !includeVideos);
+                if (includeImages) {
+                    PicasaDataSource picasaDataSource = new PicasaDataSource(this);
+                    if (imageManagerHasStorage) {
+                        ConcatenatedDataSource combinedDataSource = new ConcatenatedDataSource(localDataSource, picasaDataSource);
+                        mGridLayer.setDataSource(combinedDataSource);
+                    } else {
+                        mGridLayer.setDataSource(picasaDataSource);
+                    }
+                } else {
+                    mGridLayer.setDataSource(localDataSource);
+                }
+                mGridLayer.setPickIntent(true);
+                if (!imageManagerHasStorage) {
+                    Toast.makeText(this, getResources().getString(R.string.no_sd_card), Toast.LENGTH_LONG).show();
+                } else {
+                    Toast.makeText(this, getResources().getString(R.string.pick_prompt), Toast.LENGTH_LONG).show();
+                }
+            }
+        } else {
+            // View intent for images.
+            Uri uri = getIntent().getData();
+            boolean slideshow = getIntent().getBooleanExtra("slideshow", false);
+            SingleDataSource localDataSource = new SingleDataSource(this, uri.toString(), slideshow);
+            mGridLayer.setDataSource(localDataSource);
+            mGridLayer.setViewIntent(true, Utils.getBucketNameFromUri(uri));
+            if (SingleDataSource.isSingleImageMode(uri.toString())) {
+                mGridLayer.setSingleImage(false);
+            } else if (slideshow) {
+                mGridLayer.setSingleImage(true);
+                mGridLayer.startSlideshow();
+            }
+            // Toast.makeText(this, getResources().getString(R.string.initializing), Toast.LENGTH_SHORT).show();
+        }
+        Log.i(TAG, "onCreate");
+    }
+
+    public ReverseGeocoder getReverseGeocoder() {
+        return mReverseGeocoder;
+    }
+
+    public Handler getHandler() {
+        return mHandler;
+    }
+
+    @Override
+    public void onRestart() {
+        super.onRestart();
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mRenderView.onResume();
+        if (NEEDS_REFRESH) {
+            NEEDS_REFRESH = false;
+            CacheService.markDirtyImmediate(LocalDataSource.CAMERA_BUCKET_ID);
+            CacheService.markDirtyImmediate(LocalDataSource.DOWNLOAD_BUCKET_ID);
+            CacheService.startCache(this, false);
+        }
+        mPause = false;
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mRenderView.onPause();
+        mPause = true;
+    }
+
+    public boolean isPaused() {
+        return mPause;
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        if (mGridLayer != null)
+            mGridLayer.stop();
+        if (mReverseGeocoder != null) {
+            mReverseGeocoder.flushCache();
+        }
+        LocalDataSource.sThumbnailCache.flush();
+        LocalDataSource.sThumbnailCacheVideo.flush();
+        PicasaDataSource.sThumbnailCache.flush();
+        CacheService.startCache(this, true);
+    }
+
+    @Override
+    public void onDestroy() {
+        // Force GLThread to exit.
+        setContentView(R.layout.main);
+        DataSource dataSource = mGridLayer.getDataSource();
+        if (mGridLayer != null) {
+            if (dataSource != null) {
+                dataSource.shutdown();
+            }
+            mGridLayer.shutdown();
+        }
+        if (mReverseGeocoder != null)
+            mReverseGeocoder.shutdown();
+        mRenderView.shutdown();
+        mRenderView = null;
+        mGridLayer = null;
+        super.onDestroy();
+        Log.i(TAG, "onDestroy");
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        if (mGridLayer != null) {
+            mGridLayer.markDirty(30);
+        }
+        mRenderView.requestRender();
+        Log.i(TAG, "onConfigurationChanged");
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return mRenderView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
+    }
+
+    private boolean isPickIntent() {
+        String action = getIntent().getAction();
+        return (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action));
+    }
+
+    private boolean isViewIntent() {
+        String action = getIntent().getAction();
+        return Intent.ACTION_VIEW.equals(action);
+    }
+
+    private boolean isImageType(String type) {
+        return type.equals("vnd.android.cursor.dir/image") || type.equals("image/*");
+    }
+
+    private boolean isVideoType(String type) {
+        return type.equals("vnd.android.cursor.dir/video") || type.equals("video/*");
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+        case CROP_MSG: {
+            if (resultCode == RESULT_OK) {
+                setResult(resultCode, data);
+                finish();
+            }
+            break;
+        }
+        case CROP_MSG_INTERNAL: {
+            // We cropped an image, we must try to set the focus of the camera to that image.
+            if (resultCode == RESULT_OK) {
+                String contentUri = data.getAction();
+                if (mGridLayer != null) {
+                    mGridLayer.focusItem(contentUri);
+                }
+            }
+            break;
+        }
+        }
+    }
+
+    @Override
+    public void onLowMemory() {
+        if (mRenderView != null) {
+            mRenderView.handleLowMemory();
+        }
+    }
+
+    public void launchCropperOrFinish(final MediaItem item) {
+        final Bundle myExtras = getIntent().getExtras();
+        String cropValue = myExtras != null ? myExtras.getString("crop") : null;
+        final String contentUri = item.mContentUri;
+        if (cropValue != null) {
+            Bundle newExtras = new Bundle();
+            if (cropValue.equals("circle")) {
+                newExtras.putString("circleCrop", "true");
+            }
+            Intent cropIntent = new Intent();
+            cropIntent.setData(Uri.parse(contentUri));
+            cropIntent.setClass(this, CropImage.class);
+            cropIntent.putExtras(newExtras);
+            // Pass through any extras that were passed in.
+            cropIntent.putExtras(myExtras);
+            startActivityForResult(cropIntent, CROP_MSG);
+        } else {
+            if (contentUri.startsWith("http://")) {
+                // This is a http uri, we must save it locally first and generate a content uri from it.
+                final ProgressDialog dialog = ProgressDialog.show(this, this.getResources().getString(R.string.initializing),
+                        getResources().getString(R.string.running_face_detection), true, false);
+                if (contentUri != null) {
+                    MediaScannerConnection.MediaScannerConnectionClient client = new MediaScannerConnection.MediaScannerConnectionClient() {
+                        public void onMediaScannerConnected() {
+                            if (mConnection != null) {
+                                try {
+                                    final String path = UriTexture.writeHttpDataInDirectory(Gallery.this, contentUri,
+                                            LocalDataSource.DOWNLOAD_BUCKET_NAME);
+                                    if (path != null) {
+                                        mConnection.scanFile(path, item.mMimeType);
+                                    } else {
+                                        shutdown("");
+                                    }
+                                } catch (Exception e) {
+                                    shutdown("");
+                                }
+                            }
+                        }
+
+                        public void onScanCompleted(String path, Uri uri) {
+                            shutdown(uri.toString());
+                        }
+                        
+                        public void shutdown(String uri) {
+                            dialog.dismiss();
+                            performReturn(myExtras, uri.toString());
+                            if (mConnection != null) {
+                                mConnection.disconnect();
+                            }
+                        }
+                    };
+                    MediaScannerConnection connection = new MediaScannerConnection(Gallery.this, client);
+                    connection.connect();
+                    mConnection = connection;
+                }
+            } else {
+                performReturn(myExtras, contentUri);
+            }
+        }
+    }
+
+    private void performReturn(Bundle myExtras, String contentUri) {
+        Intent result = new Intent(null, Uri.parse(contentUri));
+        if (myExtras != null && myExtras.getBoolean("return-data")) {
+            // The size of a transaction should be below 100K.
+            Bitmap bitmap = null;
+            try {
+                bitmap = UriTexture.createFromUri(this, contentUri, 1024, 1024, 0, null);
+            } catch (IOException e) { 
+                ;
+            } catch (URISyntaxException e) {
+                ;
+            }
+            if (bitmap != null) {
+                result.putExtra("data", bitmap);
+            }
+        }
+        setResult(RESULT_OK, result);
+        finish();
+    }
+}
diff --git a/src/com/cooliris/media/GridCamera.java b/src/com/cooliris/media/GridCamera.java
new file mode 100644
index 0000000..a0115dd
--- /dev/null
+++ b/src/com/cooliris/media/GridCamera.java
@@ -0,0 +1,325 @@
+package com.cooliris.media;
+
+import android.os.Bundle;
+
+public final class GridCamera {
+    public static final float MAX_CAMERA_SPEED = 12.0f;
+    public static final float EYE_CONVERGENCE_SPEED = 3.0f;
+    public static final float EYE_X = 0;
+    public static final float EYE_Y = 0;
+    public static final float EYE_Z = 8.0f; // Initial z distance.
+    private static final float DEFAULT_PORTRAIT_ASPECT = 320.0f / 480.0f;
+    private static final float DEFAULT_LANDSCAPE_ASPECT = 1.0f / DEFAULT_PORTRAIT_ASPECT;
+    
+    public float mEyeX;
+    public float mEyeY;
+    public float mEyeZ;
+    public float mLookAtX;
+    public float mLookAtY;
+    public float mLookAtZ;
+    public float mUpX;
+    public float mUpY;
+    public float mUpZ;
+    
+    // To tilt the wall.
+    public float mEyeOffsetX;
+    public float mEyeOffsetY;
+    private float mEyeEdgeOffsetX;
+    private float mEyeEdgeOffsetXAnim;
+    private float mAmountExceeding;
+
+
+    // Animation speed, 1.0f is normal speed.
+    public float mConvergenceSpeed;
+    
+    // Camera field of view and its relation to the grid item width.
+    public float mFov;
+    public float mScale;
+    public float mOneByScale;
+    public int mWidth;
+    public int mHeight;
+    public int mItemHeight;
+    public int mItemWidth;
+    public float mAspectRatio;
+    public float mDefaultAspectRatio;
+
+    // Camera positional information.
+    private float mPosX;
+    private float mPosY;
+    private float mPosZ;
+    private float mTargetPosX;
+    private float mTargetPosY;
+    private float mTargetPosZ;
+    private float mEyeOffsetAnimX;
+    private float mEyeOffsetAnimY;
+    private float mTargetEyeX;
+    
+    // Screen width and height.
+    private int mWidthBy2;
+    private int mHeightBy2;
+    private float mTanFovBy2;
+    
+    public GridCamera(int width, int height, int itemWidth, int itemHeight) {
+        reset();
+        viewportChanged(width, height, itemWidth, itemHeight);
+        mConvergenceSpeed = 1.0f;
+    }
+
+    public void onRestoreInstanceState(Bundle savedInstanceState) {
+        mEyeX = savedInstanceState.getInt(new String("Camera.mEyeX")) + EYE_X;
+        mTargetPosX = savedInstanceState.getFloat(new String("Camera.mTargetPosX"));
+        mTargetPosY = savedInstanceState.getFloat(new String("Camera.mTargetPosY"));
+        mTargetPosZ = savedInstanceState.getFloat(new String("Camera.mTargetPosZ"));
+        commitMove();
+    }
+
+    public void onSaveInstanceState(Bundle outState) {
+        outState.putFloat(new String("Camera.mEyeX"), mEyeX - EYE_X);
+        outState.putFloat(new String("Camera.mTargetPosX"), mTargetPosX);
+        outState.putFloat(new String("Camera.mTargetPosY"), mTargetPosY);
+        outState.putFloat(new String("Camera.mTargetPosZ"), mTargetPosZ);
+    }
+
+    public void reset() {
+        mTargetEyeX = 0;
+        mEyeX = EYE_X;
+        mEyeY = EYE_Y;
+        mEyeZ = EYE_Z;
+        mLookAtX = EYE_X;
+        mLookAtY = EYE_Y;
+        mLookAtZ = 0;
+        mUpX = 0;
+        mUpY = 1.0f;
+        mUpZ = 0;
+        mPosX = 0;
+        mPosY = 0;
+        mPosZ = 0;
+        mTargetPosX = 0;
+        mTargetPosY = 0;
+        mTargetPosZ = 0;
+    }
+
+    public void viewportChanged(int w, int h, float itemWidth, float itemHeight) {
+        // For pixel precision we need to use this formula.
+        /* fov = 2tan-1(qFactor/2*defaultZ) where qFactor = height/ItemHeight */
+        float qFactor = h / (float) itemHeight;
+        float fov = 2.0f * (float) Math.toDegrees(Math.atan2(qFactor / 2, GridCamera.EYE_Z));
+        mWidth = w;
+        mHeight = h;
+        mWidthBy2 = w >> 1;
+        mHeightBy2 = h >> 1;
+        mAspectRatio = (h == 0) ? 1.0f : (float)w / (float)h;
+        mDefaultAspectRatio = (w > h) ? DEFAULT_LANDSCAPE_ASPECT : DEFAULT_PORTRAIT_ASPECT;
+        mTanFovBy2 = (float) Math.tan(Math.toRadians(fov * 0.5f));
+        mItemHeight = (int) itemHeight;
+        mItemWidth = (int) itemWidth;
+        mScale = itemHeight;
+        mOneByScale = 1.0f / (float) itemHeight;
+        mFov = fov;
+    }
+
+    public void convertToScreenSpace(int posX, int posY, int posZ, Vector3f retVal) {
+        // TODO
+    }
+
+    public void convertToCameraSpace(float posX, float posY, float posZ, Vector3f retVal) {
+        float posXx = posX - mWidthBy2;
+        float posYx = posY - mHeightBy2;
+        convertToRelativeCameraSpace(posXx, posYx, posZ, retVal);
+        retVal.x += (EYE_X + mTargetPosX);
+        retVal.y += (mTargetPosY);
+    }
+
+    public void convertToRelativeCameraSpace(float posX, float posY, float posZ, Vector3f retVal) {
+        float posXx = posX;
+        float posYx = posY;
+        posXx = posXx / mWidth;
+        posYx = posYx / mHeight;
+        float posZx = posZ;
+        float zDiscriminant = (mTanFovBy2 * (mTargetPosZ + EYE_Z + posZx));
+        zDiscriminant *= 2.0f;
+        float yRange = zDiscriminant;
+        float xRange = zDiscriminant * mAspectRatio;
+        posXx = ((posXx * xRange));
+        posYx = ((posYx * yRange));
+        retVal.x = posXx;
+        retVal.y = posYx;
+    }
+
+    public float getDistanceToFitRect(float f, float g) {
+        final float thisAspectRatio = (float) f / (float) g;
+        float h = g;
+        if (thisAspectRatio > mAspectRatio) {
+            // The width will hit the screen.
+            h = (f * mHeight) / mWidth;
+        }
+        // To fit ITEM_HEIGHT pixels perfectly, the targetZ value must be the 1.0f for the given fov
+        // Thus to fit h pixels,
+        h = h / mItemHeight;
+        float targetZ = h / mTanFovBy2;
+        targetZ = targetZ * 0.5f;
+        return -(EYE_Z - targetZ);
+    }
+
+    public void moveXTo(float posX) {
+        mTargetPosX = posX;
+    }
+
+    public void moveYTo(float posY) {
+        mTargetPosY = posY;
+    }
+
+    public void moveZTo(float posZ) {
+        mTargetPosZ = posZ;
+    }
+
+    public void moveTo(float posX, float posY, float posZ) {
+        float delta = posX - mTargetPosX;
+        float maxDelta = mWidth * 2.0f * mOneByScale;
+        delta = FloatUtils.clamp(delta, -maxDelta, maxDelta);
+        mTargetPosX += delta;
+        mTargetPosY = posY;
+        mTargetPosZ = posZ;
+    }
+
+    public void moveBy(float posX, float posY, float posZ) {
+        moveTo(posX + mTargetPosX, posY + mTargetPosY, posZ + mTargetPosZ);
+    }
+
+    public void commitMove() {
+        mPosX = mTargetPosX;
+        mPosY = mTargetPosY;
+        mPosZ = mTargetPosZ;
+    }
+    
+    public void commitMoveInX() {
+        mPosX = mTargetPosX;
+    }
+
+    public void commitMoveInY() {
+        mPosY = mTargetPosY;
+    }
+
+    public void commitMoveInZ() {
+        mPosZ = mTargetPosZ;
+    }
+    
+    public boolean computeConstraints(boolean applyConstraints, boolean applyOverflowFeedback, Vector3f firstSlotPosition,
+            Vector3f lastSlotPosition) {
+        boolean retVal = false;
+        float minX = (firstSlotPosition.x) * (1.0f / mItemHeight);
+        float maxX = (lastSlotPosition.x) * (1.0f / mItemHeight);
+        if (mTargetPosX < minX) {
+            mAmountExceeding += mTargetPosX - minX;
+            mTargetPosX = minX;
+            mPosX = minX;
+            if (applyConstraints) {
+                mTargetPosX = minX;
+            }
+            retVal = true;
+        }
+        if (mTargetPosX > maxX) {
+            mAmountExceeding += mTargetPosX - maxX;
+            mTargetPosX = maxX;
+            mPosX = maxX;
+            if (applyConstraints) {
+                mTargetPosX = maxX;
+            }
+            retVal = true;
+        }
+        if (!retVal) {
+            float scrollingFromEdgeX = 0.0f;
+            if (mAmountExceeding < 0.0f) {
+                scrollingFromEdgeX = mTargetPosX - minX;
+            } else {
+                scrollingFromEdgeX = maxX - mTargetPosX;
+            }
+            if (scrollingFromEdgeX > 0.1f) {
+                mAmountExceeding = 0.0f;
+            }
+        }
+        if (applyConstraints) {
+            mEyeEdgeOffsetX = 0.0f;
+            // We look at amount exceeding and calculate target position in the reverse direction.
+            final float maxBounceBack = 0.8f;
+            if (mAmountExceeding < -maxBounceBack)
+                mAmountExceeding = -maxBounceBack;
+            if (mAmountExceeding > maxBounceBack)
+                mAmountExceeding = maxBounceBack;
+            mTargetPosX -= mAmountExceeding * 0.8f;
+            if (mTargetPosX > maxX)
+                mTargetPosX = maxX;
+            if (mTargetPosX < minX)
+                mTargetPosX = minX;
+            mAmountExceeding = 0.0f;
+        } else {
+            float amountExceedingToUse = mAmountExceeding;
+            final float maxThreshold = 0.6f;
+            if (amountExceedingToUse > maxThreshold)
+                amountExceedingToUse = maxThreshold;
+            if (amountExceedingToUse < -maxThreshold)
+                amountExceedingToUse = -maxThreshold;
+            if (applyOverflowFeedback)
+                mEyeEdgeOffsetX = -10.0f * amountExceedingToUse;
+            else
+                mEyeEdgeOffsetX = 0.0f;
+        }
+        return retVal;
+    }
+
+    public void stopMovement() {
+        mTargetPosX = mPosX;
+        mTargetPosY = mPosY;
+        mTargetPosZ = mPosZ;
+    }
+
+    public void stopMovementInX() {
+        mTargetPosX = mPosX;
+    }
+
+    public void stopMovementInY() {
+        mTargetPosY = mPosY;
+    }
+
+    public void stopMovementInZ() {
+        mTargetPosZ = mPosZ;
+    }
+
+    public boolean isAnimating() {
+        return (mPosX != mTargetPosX || mPosY != mTargetPosY || mPosZ != mTargetPosZ || mEyeOffsetAnimX != mEyeOffsetX || mEyeEdgeOffsetXAnim != mEyeEdgeOffsetX);
+    }
+    
+    public boolean isZAnimating() {
+        return mPosZ != mTargetPosZ;
+    }
+
+    public void update(float timeElapsed) {
+        float factor = mConvergenceSpeed;
+        timeElapsed = (timeElapsed * factor);
+        mPosX = FloatUtils.animate(mPosX, mTargetPosX, timeElapsed);
+        mPosY = FloatUtils.animate(mPosY, mTargetPosY, timeElapsed);
+        mPosZ = FloatUtils.animate(mPosZ, mTargetPosZ, timeElapsed);
+        if (mEyeZ != EYE_Z) {
+            mEyeOffsetX = 0;
+            mEyeOffsetY = 0;
+        }
+        mEyeOffsetAnimX = FloatUtils.animate(mEyeOffsetAnimX, mEyeOffsetX, timeElapsed);
+        mEyeOffsetAnimY = FloatUtils.animate(mEyeOffsetAnimY, mEyeOffsetY, timeElapsed);
+        mEyeEdgeOffsetXAnim = FloatUtils.animate(mEyeEdgeOffsetXAnim, mEyeEdgeOffsetX, timeElapsed);
+        mTargetEyeX = EYE_X + mPosX;
+        if (mEyeZ == EYE_Z) {
+            mEyeX = mTargetEyeX;
+            // Enable the line below for achieving tilt while you scroll the wall.
+            // FloatUtils.animate(eyeX_, targetEyeX_, timeElapsedx - (timeElapsedx * 0.35f));
+        } else {
+            mEyeX = mTargetEyeX;
+        }
+        mEyeX += (mEyeOffsetAnimX + mEyeEdgeOffsetXAnim);
+        mLookAtX = EYE_X + mPosX;
+        mEyeY = EYE_Y + mPosY;
+        mLookAtY = EYE_Y + mPosY;
+        mEyeZ = EYE_Z + mPosZ;
+        mLookAtZ = mPosZ;
+    }
+}
+
diff --git a/src/com/cooliris/media/GridCameraManager.java b/src/com/cooliris/media/GridCameraManager.java
new file mode 100644
index 0000000..d34a7a6
--- /dev/null
+++ b/src/com/cooliris/media/GridCameraManager.java
@@ -0,0 +1,241 @@
+package com.cooliris.media;
+
+public final class GridCameraManager {
+    private GridCamera mCamera;
+    private ConcurrentPool<Vector3f> mPool;
+
+    public GridCameraManager(GridCamera camera) {
+        mCamera = camera;
+        Vector3f[] vectorPool = new Vector3f[128];
+        int length = vectorPool.length;
+        for (int i = 0; i < length; ++i) {
+            vectorPool[i] = new Vector3f();
+        }
+        mPool = new ConcurrentPool<Vector3f>(vectorPool);
+    }
+
+    public void centerCameraForSlot(LayoutInterface layout, int slotIndex, float baseConvergence, Vector3f deltaAnchorPositionIn,
+            int selectedSlotIndex, float zoomValue, float imageTheta, int state) {
+        final GridCamera camera = mCamera;
+        final ConcurrentPool<Vector3f> pool = mPool;
+        synchronized (camera) {
+            final boolean zoomin = (selectedSlotIndex != Shared.INVALID);
+            final int theta = (int) imageTheta;
+            final int portrait = (theta / 90) % 2;
+            if (slotIndex == selectedSlotIndex) {
+                camera.mConvergenceSpeed = baseConvergence * (zoomin ? 2.0f : 2.0f);
+            }
+            final float oneByZoom = 1.0f / zoomValue;
+            if (slotIndex >= 0) {
+                final Vector3f position = pool.create();
+                final Vector3f deltaAnchorPosition = pool.create();
+                try {
+                    deltaAnchorPosition.set(deltaAnchorPositionIn);
+                    GridCameraManager.getSlotPositionForSlotIndex(slotIndex, camera, layout, deltaAnchorPosition, position);
+                    position.x = (zoomValue == 1.0f) ? ((position.x) * camera.mOneByScale) : camera.mLookAtX;
+                    position.y = (zoomValue == 1.0f) ? 0 : camera.mLookAtY;
+                    if (state == GridLayer.STATE_MEDIA_SETS || state == GridLayer.STATE_TIMELINE) {
+                        position.y = -0.1f;
+                    }
+                    float width = camera.mItemWidth;
+                    float height = camera.mItemHeight;
+                    if (portrait != 0) {
+                        float temp = width;
+                        width = height;
+                        height = temp;
+                    }
+                    camera.moveTo(position.x, position.y, zoomin ? camera.getDistanceToFitRect(width * oneByZoom, height * oneByZoom) : 0);
+                } finally {
+                    pool.delete(position);
+                    pool.delete(deltaAnchorPosition);        
+                }
+            } else {
+                camera.moveYTo(0);
+                camera.moveZTo(0);
+            }
+        }
+    }
+
+    // CR: line too long. Documentation--what are the semantics of the return value?
+    /**
+     */
+    public boolean constrainCameraForSlot(LayoutInterface layout, int slotIndex, Vector3f deltaAnchorPositionIn,
+            float currentFocusItemWidth, float currentFocusItemHeight) {
+        GridCamera camera = mCamera;
+        ConcurrentPool<Vector3f> pool = mPool;
+        boolean retVal = false;
+        synchronized (camera) {
+            Vector3f position = pool.create();
+            Vector3f deltaAnchorPosition = pool.create();
+            Vector3f topLeft = pool.create();
+            Vector3f bottomRight = pool.create();
+            Vector3f imgTopLeft = pool.create();
+            Vector3f imgBottomRight = pool.create();
+
+            try {
+                if (slotIndex >= 0) {
+                    deltaAnchorPosition.set(deltaAnchorPositionIn);
+                    GridCameraManager.getSlotPositionForSlotIndex(slotIndex, camera, layout, deltaAnchorPosition, position);
+                    position.x *= camera.mOneByScale;
+                    position.y = 0.0f;
+                    float width = (currentFocusItemWidth / 2);
+                    float height = (currentFocusItemHeight / 2);
+                    imgTopLeft.set(position.x - width, position.y - height, 0);
+                    imgBottomRight.set(position.x + width, position.y + height, 0);
+                    camera.convertToCameraSpace(0, 0, 0, topLeft);
+                    camera.convertToCameraSpace(camera.mWidth, camera.mHeight, 0, bottomRight);
+                    float leftExtent = topLeft.x - imgTopLeft.x;
+                    float rightExtent = bottomRight.x - imgBottomRight.x;
+                    camera.mConvergenceSpeed = 2.0f;
+
+                    if (leftExtent < 0) {
+                        retVal = true;
+                        camera.moveBy(-leftExtent, 0, 0);
+                    }
+                    if (rightExtent > 0) {
+                        retVal = true;
+                        camera.moveBy(-rightExtent, 0, 0);
+                    }
+                    float topExtent = topLeft.y - imgTopLeft.y;
+                    float bottomExtent = bottomRight.y - imgBottomRight.y;
+                    if (topExtent < 0) {
+                        camera.moveBy(0, -topExtent, 0);
+                    }
+                    if (bottomExtent > 0) {
+                        camera.moveBy(0, -bottomExtent, 0);
+                    }
+                } 
+            } finally {
+                pool.delete(position);
+                pool.delete(deltaAnchorPosition);
+                pool.delete(topLeft);
+                pool.delete(bottomRight);
+                pool.delete(imgTopLeft);
+                pool.delete(imgBottomRight);
+            }
+        }
+        return retVal;
+    }
+
+    public void computeVisibleRange(MediaFeed feed, LayoutInterface layout, Vector3f deltaAnchorPositionIn,
+            IndexRange outVisibleRange, IndexRange outBufferedVisibleRange, IndexRange outCompleteRange, int state) {
+        GridCamera camera = mCamera;
+        ConcurrentPool<Vector3f> pool = mPool;
+        float offset = (camera.mLookAtX * camera.mScale);
+        int itemWidth = camera.mItemWidth;
+        float maxIncrement = camera.mWidth * 0.5f + itemWidth;
+        float left = -maxIncrement + offset;
+        float right = left + 2.0f * maxIncrement;
+        if (state == GridLayer.STATE_MEDIA_SETS || state == GridLayer.STATE_TIMELINE) {
+            right += (itemWidth * 0.5f);
+        }
+        float top = -maxIncrement;
+        float bottom = camera.mHeight + maxIncrement;
+        // the hint to compute the visible display items
+        int numSlots = 0;
+        if (feed != null) {
+            numSlots = feed.getNumSlots();
+        }
+        synchronized (outCompleteRange) {
+            outCompleteRange.set(0, numSlots - 1);
+        }
+
+        Vector3f position = pool.create();
+        Vector3f deltaAnchorPosition = pool.create();
+        try {
+            int firstVisibleSlotIndex = 0;
+            int lastVisibleSlotIndex = numSlots - 1;
+            int leftEdge = firstVisibleSlotIndex;
+            int rightEdge = lastVisibleSlotIndex;
+            int index = (leftEdge + rightEdge) / 2;
+            lastVisibleSlotIndex = firstVisibleSlotIndex;
+            deltaAnchorPosition.set(deltaAnchorPositionIn);
+            while (index != leftEdge) {
+                GridCameraManager.getSlotPositionForSlotIndex(index, camera, layout, deltaAnchorPosition, position);
+                if (FloatUtils.boundsContainsPoint(left, right, top, bottom, position.x, position.y)) {
+                    // this index is visible
+                    firstVisibleSlotIndex = index;
+                    lastVisibleSlotIndex = index;
+                    break;
+                } else {
+                    if (position.x > left) {
+                        rightEdge = index;
+                    } else {
+                        leftEdge = index;
+                    }
+                    index = (leftEdge + rightEdge) / 2;
+                }
+            }
+            // CR: comments would make me a happy panda.
+            while (firstVisibleSlotIndex >= 0 && firstVisibleSlotIndex < numSlots) {
+                GridCameraManager.getSlotPositionForSlotIndex(firstVisibleSlotIndex, camera, layout, deltaAnchorPosition, position);
+                // CR: !fubar instead of fubar == false.
+                if (FloatUtils.boundsContainsPoint(left, right, top, bottom, position.x, position.y) == false) {
+                    ++firstVisibleSlotIndex;
+                    break;
+                } else {
+                    --firstVisibleSlotIndex;
+                }
+            }
+            while (lastVisibleSlotIndex >= 0 && lastVisibleSlotIndex < numSlots) {
+                GridCameraManager.getSlotPositionForSlotIndex(lastVisibleSlotIndex, camera, layout, deltaAnchorPosition, position);
+                if (FloatUtils.boundsContainsPoint(left, right, top, bottom, position.x, position.y) == false) {
+                    --lastVisibleSlotIndex;
+                    break;
+                } else {
+                    ++lastVisibleSlotIndex;
+                }
+            }
+            if (firstVisibleSlotIndex < 0)
+                firstVisibleSlotIndex = 0;
+            if (lastVisibleSlotIndex >= numSlots)
+                lastVisibleSlotIndex = numSlots - 1;
+            synchronized (outVisibleRange) {
+                outVisibleRange.set(firstVisibleSlotIndex, lastVisibleSlotIndex);
+            }
+            if (feed != null) {
+                feed.setVisibleRange(firstVisibleSlotIndex, lastVisibleSlotIndex);
+            }
+            final int buffer = 24;
+            firstVisibleSlotIndex = ((firstVisibleSlotIndex - buffer) / buffer) * buffer;
+            lastVisibleSlotIndex += buffer;
+            lastVisibleSlotIndex = (lastVisibleSlotIndex / buffer) * buffer;
+            if (firstVisibleSlotIndex < 0) {
+                firstVisibleSlotIndex = 0;
+            }
+            if (lastVisibleSlotIndex >= numSlots) {
+                lastVisibleSlotIndex = numSlots - 1;
+            }
+            synchronized (outBufferedVisibleRange) {
+                outBufferedVisibleRange.set(firstVisibleSlotIndex, lastVisibleSlotIndex);
+            }
+        } finally {
+            pool.delete(position);
+            pool.delete(deltaAnchorPosition);
+        }
+    }
+
+    public static final void getSlotPositionForSlotIndex(int slotIndex, GridCamera camera, LayoutInterface layout,
+            Vector3f deltaAnchorPosition, Vector3f outVal) {
+        layout.getPositionForSlotIndex(slotIndex, camera.mItemWidth, camera.mItemHeight, outVal);
+        outVal.subtract(deltaAnchorPosition);
+    }
+
+    public static final float getFillScreenZoomValue(GridCamera camera, Pool<Vector3f> pool, float currentFocusItemWidth,
+            float currentFocusItemHeight) {
+        Vector3f topLeft = pool.create();
+        Vector3f bottomRight = pool.create();
+        float potentialZoomValue = 1.0f;
+        try {
+            camera.convertToCameraSpace(0, 0, 0, topLeft);
+            camera.convertToCameraSpace(camera.mWidth, camera.mHeight, 0, bottomRight);
+            float xExtent = Math.abs(topLeft.x - bottomRight.x) / currentFocusItemWidth;
+            float yExtent = Math.abs(topLeft.y - bottomRight.y) / currentFocusItemHeight;
+            potentialZoomValue = Math.max(xExtent, yExtent);
+        } finally {
+            pool.delete(topLeft);
+            pool.delete(bottomRight);
+        }
+        return potentialZoomValue;
+    }
+}
\ No newline at end of file
diff --git a/src/com/cooliris/media/GridDrawManager.java b/src/com/cooliris/media/GridDrawManager.java
new file mode 100644
index 0000000..f1e7ffb
--- /dev/null
+++ b/src/com/cooliris/media/GridDrawManager.java
@@ -0,0 +1,675 @@
+package com.cooliris.media;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+
+import android.content.Context;
+
+public final class GridDrawManager {
+    public static final int PASS_THUMBNAIL_CONTENT = 0;
+    public static final int PASS_FOCUS_CONTENT = 1;
+    public static final int PASS_FRAME = 2;
+    public static final int PASS_PLACEHOLDER = 3;
+    public static final int PASS_FRAME_PLACEHOLDER = 4;
+    public static final int PASS_TEXT_LABEL = 5;
+    public static final int PASS_SELECTION_LABEL = 6;
+    public static final int PASS_VIDEO_LABEL = 7;
+    public static final int PASS_LOCATION_LABEL = 8;
+    public static final int PASS_MEDIASET_SOURCE_LABEL = 9;
+
+    private MediaItemTexture.Config mThumbnailConfig = new MediaItemTexture.Config();
+    private DisplayItem[] mDisplayItems;
+    private DisplaySlot[] mDisplaySlots;
+    private DisplayList mDisplayList;
+    private GridCamera mCamera;
+    private IndexRange mBufferedVisibleRange;
+    private IndexRange mVisibleRange;
+    private int mSelectedSlot;
+    private int mCurrentFocusSlot;
+    private GridDrawables mDrawables;
+    private DisplayItem[] mItemsDrawn;
+    private int mDrawnCounter;
+    private float mTargetFocusMixRatio = 0.0f;
+    private float mFocusMixRatio = 0.0f;
+    private final FloatAnim mSelectedMixRatio = new FloatAnim(0f);
+    private float mCurrentFocusItemWidth;
+    private float mCurrentFocusItemHeight;
+    private boolean mCurrentFocusIsPressed;
+    private final Texture mNoItemsTexture;
+
+    private Comparator<DisplayItem> mDisplayItemComparator = new Comparator<DisplayItem>() {
+        public int compare(DisplayItem a, DisplayItem b) {
+            if (a == null || b == null) {
+                return 0;
+            }
+            float delta = (a.mAnimatedPosition.z - b.mAnimatedPosition.z);
+            if (delta > 0) {
+                return 1;
+            } else if (delta < 0) {
+                return -1;
+            } else {
+                return 0;
+            }
+        }
+    };
+
+    public GridDrawManager(Context context, GridCamera camera, GridDrawables drawables, DisplayList displayList,
+            DisplayItem[] displayItems, DisplaySlot[] displaySlots) {
+        mThumbnailConfig.thumbnailWidth = 128;
+        mThumbnailConfig.thumbnailHeight = 96;
+        mDisplayItems = displayItems;
+        mDisplaySlots = displaySlots;
+        mDisplayList = displayList;
+        mDrawables = drawables;
+        mCamera = camera;
+        mItemsDrawn = new DisplayItem[GridLayer.MAX_ITEMS_DRAWABLE];
+
+        StringTexture.Config stc = new StringTexture.Config();
+        stc.bold = true;
+        stc.fontSize = 16 * Gallery.PIXEL_DENSITY;
+        stc.sizeMode = StringTexture.Config.SIZE_EXACT;
+        stc.overflowMode = StringTexture.Config.OVERFLOW_FADE;
+        mNoItemsTexture = new StringTexture(context.getResources().getString(R.string.no_items), stc);
+
+    }
+
+    public void prepareDraw(IndexRange bufferedVisibleRange, IndexRange visibleRange, int selectedSlot, int currentFocusSlot,
+            boolean currentFocusIsPressed) {
+        mBufferedVisibleRange = bufferedVisibleRange;
+        mVisibleRange = visibleRange;
+        mSelectedSlot = selectedSlot;
+        mCurrentFocusSlot = currentFocusSlot;
+        mCurrentFocusIsPressed = currentFocusIsPressed;
+    }
+
+    public boolean update(float timeElapsed) {
+        mFocusMixRatio = FloatUtils.animate(mFocusMixRatio, mTargetFocusMixRatio, timeElapsed);
+        mTargetFocusMixRatio = 0.0f;
+        if (mFocusMixRatio != mTargetFocusMixRatio || mSelectedMixRatio.isAnimating()) {
+            return true;
+        }
+        return false;
+    }
+
+    public void drawThumbnails(RenderView view, GL11 gl, int state) {
+        GridDrawables drawables = mDrawables;
+        DisplayList displayList = mDisplayList;
+        DisplayItem[] displayItems = mDisplayItems;
+        int firstBufferedVisibleSlot = mBufferedVisibleRange.begin;
+        int lastBufferedVisibleSlot = mBufferedVisibleRange.end;
+        int firstVisibleSlot = mVisibleRange.begin;
+        int lastVisibleSlot = mVisibleRange.end;
+        int selectedSlotIndex = mSelectedSlot;
+        int currentFocusSlot = mCurrentFocusSlot;
+        DisplayItem[] itemsDrawn = mItemsDrawn;
+        itemsDrawn[0] = null; // No items drawn yet.
+        int drawnCounter = 0;
+        GridQuad grid = drawables.mGrid;
+        grid.bindArrays(gl);
+        int numTexturesQueued = 0;
+        for (int itrSlotIndex = firstBufferedVisibleSlot; itrSlotIndex <= lastBufferedVisibleSlot; ++itrSlotIndex) {
+            int index = itrSlotIndex;
+            boolean priority = !(index < firstVisibleSlot || index > lastVisibleSlot);
+            int startSlotIndex = 0;
+            for (int j = GridLayer.MAX_DISPLAYED_ITEMS_PER_SLOT - 1; j >= 0; --j) {
+                DisplayItem displayItem = displayItems[(index - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT + j];
+                if (displayItem == null) {
+                    continue;
+                } else {
+                    Texture texture = displayItem.getThumbnailImage(view.getContext(), mThumbnailConfig);
+                    if (texture != null && texture.isLoaded() == false) {
+                        startSlotIndex = j;
+                        break;
+                    }
+                }
+            }
+            // Prime the textures in the reverse order.
+            for (int j = 0; j < GridLayer.MAX_DISPLAYED_ITEMS_PER_SLOT; ++j) {
+                DisplayItem displayItem = displayItems[(index - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT + j];
+                if (displayItem == null) {
+                    continue;
+                } else {
+                    if (selectedSlotIndex != Shared.INVALID && (index <= selectedSlotIndex - 2 || index >= selectedSlotIndex + 2)) {
+                        displayItem.clearScreennailImage();
+                    }
+                    Texture texture = displayItem.getThumbnailImage(view.getContext(), mThumbnailConfig);
+                    if (texture != null && !texture.isLoaded() && numTexturesQueued <= 6) {
+                        boolean isUncachedVideo = texture.isUncachedVideo();
+                        if (!isUncachedVideo)
+                            view.prime(texture, priority);
+                        view.bind(texture);
+                        if (priority && !isUncachedVideo)
+                            ++numTexturesQueued;
+                    }
+                }
+            }
+            if (itrSlotIndex == selectedSlotIndex) {
+                continue;
+            }
+            view.prime(drawables.mTexturePlaceholder, true);
+            Texture placeholder = (state == GridLayer.STATE_GRID_VIEW) ? drawables.mTexturePlaceholder : null;
+            final boolean pushDown = (state == GridLayer.STATE_GRID_VIEW || state == GridLayer.STATE_FULL_SCREEN) ? false : true;
+            for (int j = startSlotIndex; j < GridLayer.MAX_DISPLAYED_ITEMS_PER_SLOT; ++j) {
+                DisplayItem displayItem = displayItems[(index - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT + j];
+                if (displayItem == null) {
+                    break;
+                } else {
+                    if (currentFocusSlot == index) {
+                        displayList.setHasFocus(displayItem, true, pushDown);
+                        mTargetFocusMixRatio = 1.0f;
+                    } else {
+                        displayList.setHasFocus(displayItem, false, pushDown);
+                    }
+                    Texture texture = displayItem.getThumbnailImage(view.getContext(), mThumbnailConfig);
+                    if (texture != null) {
+                        if ((!displayItem.isAnimating() || !texture.isLoaded())
+                                && displayItem.getStackIndex() > GridLayer.MAX_ITEMS_PER_SLOT) {
+                            displayItem.mAlive = true;
+                            continue;
+                        }
+                        if (index < firstVisibleSlot || index > lastVisibleSlot) {
+                            if (view.bind(texture)) {
+                                mDisplayList.setAlive(displayItem, true);
+                            }
+                            continue;
+                        }
+                        drawDisplayItem(view, gl, displayItem, texture, PASS_THUMBNAIL_CONTENT, placeholder,
+                                displayItem.mAnimatedPlaceholderFade);
+                    } else {
+                        // Move on to the next stack.
+                        break;
+                    }
+                    if (drawnCounter >= GridLayer.MAX_ITEMS_DRAWABLE - 1 || drawnCounter < 0) {
+                        break;
+                    }
+                    // Insert in order of z.
+                    itemsDrawn[drawnCounter++] = displayItem;
+                    itemsDrawn[drawnCounter] = null;
+                }
+            }
+        }
+        mDrawnCounter = drawnCounter;
+        grid.unbindArrays(gl);
+        drawables.swapGridQuads(gl);
+    }
+
+    public float getFocusQuadWidth() {
+        return mCurrentFocusItemWidth;
+    }
+
+    public float getFocusQuadHeight() {
+        return mCurrentFocusItemHeight;
+    }
+
+    public void drawFocusItems(RenderView view, GL11 gl, float zoomValue, boolean slideshowMode, float timeElapsedSinceView) {
+        int selectedSlotIndex = mSelectedSlot;
+        GridDrawables drawables = mDrawables;
+        GridCamera camera = mCamera;
+        DisplayItem[] displayItems = mDisplayItems;
+        int firstBufferedVisibleSlot = mBufferedVisibleRange.begin;
+        int lastBufferedVisibleSlot = mBufferedVisibleRange.end;
+        boolean isCameraZAnimating = mCamera.isZAnimating();
+        for (int i = firstBufferedVisibleSlot; i <= lastBufferedVisibleSlot; ++i) {
+            if (selectedSlotIndex != Shared.INVALID && (i >= selectedSlotIndex - 2 && i <= selectedSlotIndex + 2)) {
+                continue;
+            }
+            DisplayItem displayItem = displayItems[(i - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT];
+            if (displayItem != null) {
+                displayItem.clearScreennailImage();
+            }
+        }
+        if (selectedSlotIndex != Shared.INVALID) {
+            float camX = camera.mLookAtX * camera.mScale;
+            int centerIndexInDrawnArray = (selectedSlotIndex - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT;
+            if (centerIndexInDrawnArray < 0 || centerIndexInDrawnArray >= displayItems.length) {
+                return;
+            }
+            DisplayItem centerDisplayItem = displayItems[centerIndexInDrawnArray];
+            if (centerDisplayItem == null || centerDisplayItem.mItemRef.mId == Shared.INVALID) {
+                return;
+            }
+            boolean focusItemTextureLoaded = false;
+            Texture centerTexture = centerDisplayItem.getScreennailImage(view.getContext());
+            if (centerTexture != null && centerTexture.isLoaded()) {
+                focusItemTextureLoaded = true;
+            }
+            float centerTranslateX = centerDisplayItem.mAnimatedPosition.x;
+            final boolean skipPrevious = centerTranslateX < camX;
+            view.setAlpha(1.0f);
+            gl.glEnable(GL11.GL_BLEND);
+            gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE);
+            float backupImageTheta = 0.0f;
+            for (int i = -1; i <= 1; ++i) {
+                if (slideshowMode && timeElapsedSinceView > 1.0f && i != 0)
+                    continue;
+                float viewAspect = camera.mAspectRatio;
+                int selectedSlotToUse = selectedSlotIndex + i;
+                if (selectedSlotToUse >= 0 && selectedSlotToUse <= lastBufferedVisibleSlot) {
+                    int indexInDrawnArray = (selectedSlotToUse - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT;
+                    if (indexInDrawnArray < 0 || indexInDrawnArray >= displayItems.length) {
+                        return;
+                    }
+                    DisplayItem displayItem = displayItems[indexInDrawnArray];
+                    MediaItem item = displayItem.mItemRef;
+                    final Texture thumbnailTexture = displayItem.getThumbnailImage(view.getContext(), mThumbnailConfig);
+                    Texture texture = displayItem.getScreennailImage(view.getContext());
+                    if (isCameraZAnimating && (texture == null || !texture.isLoaded())) {
+                        texture = thumbnailTexture;
+                        mSelectedMixRatio.setValue(0f);
+                        mSelectedMixRatio.animateValue(1f, 0.75f, view.getFrameTime());
+                    }
+                    Texture hiRes = (zoomValue != 1.0f && i == 0 && item.getMediaType() != MediaItem.MEDIA_TYPE_VIDEO) ? displayItem
+                            .getHiResImage(view.getContext())
+                            : null;
+                    if (Gallery.PIXEL_DENSITY > 1.0f) {
+                        hiRes = texture;
+                    }
+                    if (i != 0) {
+                        displayItem.clearHiResImage();
+                    }
+                    if (hiRes != null) {
+                        if (!hiRes.isLoaded()) {
+                            view.bind(hiRes);
+                            view.prime(hiRes, true);
+                        } else {
+                            texture = hiRes;
+                        }
+                    }
+                    final Texture fsTexture = texture;
+                    if (texture == null || !texture.isLoaded()) {
+                        if (Math.abs(centerTranslateX - camX) < 0.1f) {
+                            if (focusItemTextureLoaded && i != 0) {
+                                view.bind(texture);
+                            }
+                            if (i == 0) {
+                                view.bind(texture);
+                                view.prime(texture, true);
+                            }
+                        }
+                        texture = thumbnailTexture;
+                        if (i == 0) {
+                            mSelectedMixRatio.setValue(0f);
+                            mSelectedMixRatio.animateValue(1f, 0.75f, view.getFrameTime());
+                        }
+                    }
+                    if (!slideshowMode && skipPrevious && i == -1) {
+                        continue;
+                    }
+                    if (!skipPrevious && i == 1) {
+                        continue;
+                    }
+                    int theta = (int) displayItem.getImageTheta();
+                    // If it is in slideshow mode, we draw the previous item in
+                    // the next item's position.
+                    if (slideshowMode && timeElapsedSinceView < 1.0f && timeElapsedSinceView != 0) {
+                        if (i == -1) {
+                            int nextSlotToUse = selectedSlotToUse + 1;
+                            if (nextSlotToUse >= 0 && nextSlotToUse <= lastBufferedVisibleSlot) {
+                                int nextIndexInDrawnArray = (nextSlotToUse - firstBufferedVisibleSlot)
+                                        * GridLayer.MAX_ITEMS_PER_SLOT;
+                                if (nextIndexInDrawnArray >= 0 && nextIndexInDrawnArray < displayItems.length) {
+                                    float currentImageTheta = displayItem.mAnimatedImageTheta;
+                                    displayItem = displayItems[nextIndexInDrawnArray];
+                                    backupImageTheta = displayItem.mAnimatedImageTheta;
+                                    displayItem.mAnimatedImageTheta = currentImageTheta;
+                                    view.setAlpha(1.0f - timeElapsedSinceView);
+                                }
+                            }
+                        } else if (i == 0) {
+                            displayItem.mAnimatedImageTheta = backupImageTheta;
+                            view.setAlpha(timeElapsedSinceView);
+                        }
+                    }
+                    if (texture != null) {
+                        int vboIndex = i + 1;
+                        float alpha = view.getAlpha();
+                        float selectedMixRatio = mSelectedMixRatio.getValue(view.getFrameTime());
+                        if (selectedMixRatio != 1f) {
+                            texture = thumbnailTexture;
+                            view.setAlpha(alpha * (1.0f - selectedMixRatio));
+                        }
+                        GridQuad quad = drawables.mFullscreenGrid[vboIndex];
+                        float u = texture.getNormalizedWidth();
+                        float v = texture.getNormalizedHeight();
+                        float imageWidth = texture.getWidth();
+                        float imageHeight = texture.getHeight();
+                        boolean portrait = ((theta / 90) % 2 == 1);
+                        if (portrait) {
+                            viewAspect = 1.0f / viewAspect;
+                        }
+                        quad.resizeQuad(viewAspect, u, v, imageWidth, imageHeight);
+                        quad.bindArrays(gl);
+                        drawDisplayItem(view, gl, displayItem, texture, PASS_FOCUS_CONTENT, null, 0.0f);
+                        quad.unbindArrays(gl);
+                        if (selectedMixRatio != 0.0f && selectedMixRatio != 1.0f) {
+                            texture = fsTexture;
+                            if (texture != null) {
+                                float drawAlpha = selectedMixRatio;
+                                view.setAlpha(alpha * drawAlpha);
+                                u = texture.getNormalizedWidth();
+                                v = texture.getNormalizedHeight();
+                                imageWidth = texture.getWidth();
+                                imageHeight = texture.getHeight();
+                                quad.resizeQuad(viewAspect, u, v, imageWidth, imageHeight);
+                                quad.bindArrays(gl);
+                                drawDisplayItem(view, gl, displayItem, fsTexture, PASS_FOCUS_CONTENT, null, 1.0f);
+                                quad.unbindArrays(gl);
+                            }
+                        }
+                        if (i == 0 || slideshowMode) {
+                            mCurrentFocusItemWidth = quad.getWidth();
+                            mCurrentFocusItemHeight = quad.getHeight();
+                            if (portrait) {
+                                // Swap these values.
+                                float itemWidth = mCurrentFocusItemWidth;
+                                mCurrentFocusItemWidth = mCurrentFocusItemHeight;
+                                mCurrentFocusItemHeight = itemWidth;
+                            }
+                        }
+                        view.setAlpha(alpha);
+                        if (item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO) {
+                            // The play graphic overlay.
+                            drawables.mVideoGrid.bindArrays(gl);
+                            drawDisplayItem(view, gl, displayItem, drawables.mTextureVideo, PASS_VIDEO_LABEL, null, 0);
+                            drawables.mVideoGrid.unbindArrays(gl);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    public void drawBlendedComponents(RenderView view, GL11 gl, float alpha, int state, int hudMode, float stackMixRatio,
+            float gridMixRatio, MediaBucketList bucketList, boolean isFeedLoading) {
+        int firstBufferedVisibleSlot = mBufferedVisibleRange.begin;
+        int lastBufferedVisibleSlot = mBufferedVisibleRange.end;
+        int firstVisibleSlot = mVisibleRange.begin;
+        int lastVisibleSlot = mVisibleRange.end;
+        DisplayItem[] displayItems = mDisplayItems;
+        GridDrawables drawables = mDrawables;
+
+        // We draw the frames around the drawn items.
+        boolean currentFocusIsPressed = mCurrentFocusIsPressed;
+        if (state != GridLayer.STATE_FULL_SCREEN) {
+            drawables.mFrame.bindArrays(gl);
+            Texture texturePlaceHolder = (state == GridLayer.STATE_GRID_VIEW) ? drawables.mTextureGridFrame
+                    : drawables.mTextureFrame;
+            for (int i = firstBufferedVisibleSlot; i <= lastBufferedVisibleSlot; ++i) {
+                if (i < firstVisibleSlot || i > lastVisibleSlot) {
+                    continue;
+                }
+                boolean slotIsAlive = false;
+                if (state != GridLayer.STATE_MEDIA_SETS && state != GridLayer.STATE_TIMELINE) {
+                    for (int j = 0; j < GridLayer.MAX_DISPLAYED_ITEMS_PER_SLOT; ++j) {
+                        DisplayItem displayItem = displayItems[(i - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT + j];
+                        if (displayItem != null) {
+                            slotIsAlive |= displayItem.mAlive;
+                        }
+                    }
+                    if (!slotIsAlive) {
+                        DisplayItem displayItem = displayItems[(i - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT];
+                        if (displayItem != null) {
+                            drawDisplayItem(view, gl, displayItem, texturePlaceHolder, PASS_FRAME_PLACEHOLDER, null, 0);
+                        }
+                    }
+                }
+            }
+            Texture texturePressed = drawables.mTextureFramePressed;
+            Texture textureFocus = drawables.mTextureFrameFocus;
+            Texture textureGrid = drawables.mTextureGridFrame;
+            Texture texture = drawables.mTextureFrame;
+
+            int drawnCounter = mDrawnCounter;
+            DisplayItem[] itemsDrawn = mItemsDrawn;
+            if (texture != null) {
+                if (drawnCounter > 0) {
+                    Arrays.sort(itemsDrawn, 0, drawnCounter, mDisplayItemComparator);
+                    float timeElapsedSinceGridView = gridMixRatio;
+                    float timeElapsedSinceStackView = stackMixRatio;
+                    for (int i = drawnCounter - 1; i >= 0; --i) {
+                        DisplayItem itemDrawn = itemsDrawn[i];
+                        if (itemDrawn == null) {
+                            continue;
+                        }
+                        boolean displayItemPresentInSelectedItems = bucketList.find(itemDrawn.mItemRef);
+                        Texture previousTexture = (displayItemPresentInSelectedItems) ? texturePressed : texture;
+                        Texture textureToUse = (itemDrawn.getHasFocus()) ? (currentFocusIsPressed ? texturePressed : textureFocus)
+                                : ((displayItemPresentInSelectedItems) ? texturePressed : textureGrid);
+                        float ratio = timeElapsedSinceGridView;
+                        if (itemDrawn.mAlive) {
+                            if (state != GridLayer.STATE_GRID_VIEW) {
+                                previousTexture = (displayItemPresentInSelectedItems) ? texturePressed : texture;
+                                textureToUse = (itemDrawn.getHasFocus()) ? (currentFocusIsPressed ? texturePressed : textureFocus)
+                                        : previousTexture;
+                                if (timeElapsedSinceStackView == 1.0f) {
+                                    ratio = mFocusMixRatio;
+                                } else {
+                                    ratio = timeElapsedSinceStackView;
+                                    previousTexture = textureGrid;
+                                }
+                            }
+                            drawDisplayItem(view, gl, itemDrawn, textureToUse, PASS_FRAME, previousTexture, ratio);
+                        }
+                    }
+                }
+            }
+            drawables.mFrame.unbindArrays(gl);
+            gl.glDepthFunc(GL10.GL_ALWAYS);
+            if (state == GridLayer.STATE_MEDIA_SETS || state == GridLayer.STATE_TIMELINE) {
+                DisplaySlot[] displaySlots = mDisplaySlots;
+                drawables.mTextGrid.bindArrays(gl);
+                final float textOffsetY = 0.82f;
+                gl.glTranslatef(0.0f, -textOffsetY, 0.0f);
+                HashMap<String, StringTexture> stringTextureTable = drawables.mStringTextureTable;
+                ReverseGeocoder reverseGeocoder = ((Gallery) view.getContext()).getReverseGeocoder();
+
+                boolean itemsPresent = false;
+
+                for (int i = firstBufferedVisibleSlot; i <= lastBufferedVisibleSlot; ++i) {
+                    itemsPresent = true;
+                    DisplayItem displayItem = displayItems[(i - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT];
+                    if (displayItem != null) {
+                        DisplaySlot displaySlot = displaySlots[i - firstBufferedVisibleSlot];
+                        Texture textureString = displaySlot.getTitleImage(stringTextureTable);
+                        view.loadTexture(textureString);
+                        if (textureString != null) {
+                            if (i < firstVisibleSlot || i > lastVisibleSlot) {
+                                continue;
+                            }
+                            drawDisplayItem(view, gl, displayItem, textureString, PASS_TEXT_LABEL, null, 0);
+                        }
+
+                    }
+                }
+
+                if (!itemsPresent && !isFeedLoading) {
+                    // Draw the no items texture.
+                    int wWidth = view.getWidth();
+                    int wHeight = view.getHeight();
+
+                    // Size this to be 40 pxls less than screen width
+                    mNoItemsTexture.mWidth = wWidth - 40;
+
+                    int x = (int) Math.floor((wWidth / 2) - (mNoItemsTexture.getWidth() / 2));
+                    int y = (int) Math.floor((wHeight / 2) - (mNoItemsTexture.getHeight() / 2));
+                    view.draw2D(mNoItemsTexture, x, y);
+                }
+
+                float yLocOffset = 0.2f;
+                gl.glTranslatef(0.0f, -yLocOffset, 0.0f);
+                for (int i = firstBufferedVisibleSlot; i <= lastBufferedVisibleSlot; ++i) {
+                    DisplayItem displayItem = displayItems[(i - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT];
+                    if (displayItem != null) {
+                        DisplaySlot displaySlot = displaySlots[i - firstBufferedVisibleSlot];
+                        StringTexture textureString = displaySlot.getLocationImage(reverseGeocoder, stringTextureTable);
+                        if (textureString != null) {
+                            drawDisplayItem(view, gl, displayItem, textureString, PASS_TEXT_LABEL, null, 0);
+                        }
+                    }
+                }
+                if (state == GridLayer.STATE_TIMELINE) {
+                    drawables.mLocationGrid.bindArrays(gl);
+                    Texture locationTexture = drawables.mTextureLocation;
+                    final float yLocationLabelOffset = 0.19f;
+                    for (int i = firstBufferedVisibleSlot; i <= lastBufferedVisibleSlot; ++i) {
+                        DisplayItem displayItem = displayItems[(i - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT];
+                        if (displayItem != null) {
+                            if (displayItem.mAlive == true) {
+                                DisplaySlot displaySlot = displaySlots[i - firstBufferedVisibleSlot];
+                                if (displaySlot.hasValidLocation()) {
+                                    StringTexture textureString = displaySlot.getLocationImage(reverseGeocoder, stringTextureTable);
+                                    float textWidth = (textureString != null) ? textureString.computeTextWidth() : 0;
+                                    textWidth *= (mCamera.mOneByScale * 0.5f);
+                                    if (textWidth == 0.0f) {
+                                        textWidth -= 0.18f;
+                                    }
+                                    textWidth += 0.1f;
+                                    gl.glTranslatef(textWidth, -yLocationLabelOffset, 0.0f);
+                                    drawDisplayItem(view, gl, displayItem, locationTexture, PASS_LOCATION_LABEL, null, 0);
+                                    gl.glTranslatef(-textWidth, yLocationLabelOffset, 0.0f);
+                                }
+                            }
+                        }
+                    }
+
+                    drawables.mLocationGrid.unbindArrays(gl);
+                } else if (state == GridLayer.STATE_MEDIA_SETS && stackMixRatio > 0.0f) {
+                    drawables.mSourceIconGrid.bindArrays(gl);
+                    Texture transparentTexture = drawables.mTextureTransparent;
+                    for (int i = firstBufferedVisibleSlot; i <= lastBufferedVisibleSlot; ++i) {
+                        DisplayItem displayItem = displayItems[(i - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT];
+                        if (displayItem != null) {
+                            if (displayItem.mAlive == true) {
+                                DisplaySlot displaySlot = displaySlots[i - firstBufferedVisibleSlot];
+                                Texture locationTexture = view.getResource(drawables
+                                        .getIconForSet(displaySlot.getMediaSet(), false), false);
+
+                                // Draw the icon at 0.85 alpha over the top item in the stack.
+                                gl.glTranslatef(0.24f, 0.5f, 0);
+                                drawDisplayItem(view, gl, displayItem, locationTexture, PASS_MEDIASET_SOURCE_LABEL,
+                                        transparentTexture, 0.85f);
+                                gl.glTranslatef(-0.24f, -0.5f, 0);
+                            }
+                        }
+                    }
+                    drawables.mSourceIconGrid.unbindArrays(gl);
+                }
+                gl.glTranslatef(0.0f, yLocOffset, 0.0f);
+                gl.glTranslatef(0.0f, textOffsetY, 0.0f);
+                drawables.mTextGrid.unbindArrays(gl);
+            }
+            if (hudMode == HudLayer.MODE_SELECT && state != GridLayer.STATE_FULL_SCREEN) {
+                Texture textureSelectedOn = drawables.mTextureCheckmarkOn;
+                Texture textureSelectedOff = drawables.mTextureCheckmarkOff;
+                view.prime(textureSelectedOn, true);
+                view.prime(textureSelectedOff, true);
+                drawables.mSelectedGrid.bindArrays(gl);
+                for (int i = firstBufferedVisibleSlot; i <= lastBufferedVisibleSlot; ++i) {
+                    DisplayItem displayItem = displayItems[(i - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT];
+                    if (displayItem != null) {
+                        Texture textureToUse = bucketList.find(displayItem.mItemRef) ? textureSelectedOn : textureSelectedOff;
+                        drawDisplayItem(view, gl, displayItem, textureToUse, PASS_SELECTION_LABEL, null, 0);
+                    }
+                }
+                drawables.mSelectedGrid.unbindArrays(gl);
+            }
+            drawables.mVideoGrid.bindArrays(gl);
+            Texture videoTexture = drawables.mTextureVideo;
+            for (int i = firstBufferedVisibleSlot; i <= lastBufferedVisibleSlot; ++i) {
+                DisplayItem displayItem = displayItems[(i - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT];
+                if (displayItem != null && displayItem.mAlive) {
+                    if (displayItem.mItemRef.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO) {
+                        drawDisplayItem(view, gl, displayItem, videoTexture, PASS_VIDEO_LABEL, null, 0);
+                    }
+                }
+            }
+            drawables.mVideoGrid.unbindArrays(gl);
+            gl.glDepthFunc(GL10.GL_LEQUAL);
+        }
+    }
+
+    private void drawDisplayItem(RenderView view, GL11 gl, DisplayItem displayItem, Texture texture, int pass,
+            Texture previousTexture, float mixRatio) {
+        GridCamera camera = mCamera;
+        Vector3f animatedPosition = displayItem.mAnimatedPosition;
+        float translateXf = animatedPosition.x * camera.mOneByScale;
+        float translateYf = animatedPosition.y * camera.mOneByScale;
+        float translateZf = -animatedPosition.z;
+        int stackId = displayItem.getStackIndex();
+        if (pass == PASS_PLACEHOLDER || pass == PASS_FRAME_PLACEHOLDER) {
+            translateZf = -0.04f;
+        } else {
+            if (pass == PASS_FRAME)
+                translateZf += 0.02f;
+            if ((pass == PASS_TEXT_LABEL || pass == PASS_LOCATION_LABEL || pass == PASS_SELECTION_LABEL) && !displayItem.isAlive()) {
+                translateZf = 0.0f;
+            }
+            if (pass == PASS_TEXT_LABEL) {
+                translateZf = 0.0f;
+            }
+        }
+        boolean usingMixedTextures = false;
+        boolean bind = false;
+        if ((pass != PASS_THUMBNAIL_CONTENT)
+                || (stackId < GridLayer.MAX_DISPLAYED_ITEMS_PER_SLOT && texture.isLoaded() && (previousTexture == null || previousTexture
+                        .isLoaded()))) {
+            if (mixRatio == 1.0f || previousTexture == null || texture == previousTexture) {
+                bind = view.bind(texture);
+            } else if (mixRatio != 0.0f) {
+                if (!texture.isLoaded() || !previousTexture.isLoaded()) {
+                    // Submit the previous texture to the load queue
+                    view.bind(previousTexture);
+                    bind = view.bind(texture);
+                } else {
+                    usingMixedTextures = true;
+                    bind = view.bindMixed(previousTexture, texture, mixRatio);
+                }
+            } else {
+                bind = view.bind(previousTexture);
+            }
+        } else if (stackId >= GridLayer.MAX_DISPLAYED_ITEMS_PER_SLOT && pass == PASS_THUMBNAIL_CONTENT) {
+            mDisplayList.setAlive(displayItem, true);
+        }
+        if (!texture.isLoaded() || !bind) {
+            if (pass == PASS_THUMBNAIL_CONTENT) {
+                if (previousTexture != null && previousTexture.isLoaded() && translateZf == 0.0f) {
+                    translateZf = -0.08f;
+                    bind |= view.bind(previousTexture);
+                }
+                if (!bind) {
+                    return;
+                }
+            } else {
+                return;
+            }
+        } else {
+            if (pass == PASS_THUMBNAIL_CONTENT || pass == PASS_FOCUS_CONTENT) {
+                if (!displayItem.mAlive) {
+                    mDisplayList.setAlive(displayItem, true);
+                }
+            }
+        }
+        gl.glTranslatef(-translateXf, -translateYf, -translateZf);
+        float theta = (pass == PASS_FOCUS_CONTENT) ? displayItem.mAnimatedImageTheta + displayItem.mAnimatedTheta
+                : displayItem.mAnimatedTheta;
+        gl.glRotatef(theta, 0.0f, 0.0f, 1.0f);
+        float orientation = 0.0f;
+        if (pass == PASS_THUMBNAIL_CONTENT && displayItem.mAnimatedImageTheta != 0.0f) {
+            orientation = displayItem.mAnimatedImageTheta;
+        }
+        if (pass == PASS_FRAME || pass == PASS_FRAME_PLACEHOLDER) {
+            GridQuadFrame.draw(gl);
+        } else {
+            GridQuad.draw(gl, orientation);
+        }
+        gl.glRotatef(-theta, 0.0f, 0.0f, 1.0f);
+        gl.glTranslatef(translateXf, translateYf, translateZf);
+        if (usingMixedTextures) {
+            view.unbindMixed();
+        }
+    }
+}
diff --git a/src/com/cooliris/media/GridDrawables.java b/src/com/cooliris/media/GridDrawables.java
new file mode 100644
index 0000000..54159ac
--- /dev/null
+++ b/src/com/cooliris/media/GridDrawables.java
@@ -0,0 +1,211 @@
+package com.cooliris.media;
+
+import java.util.HashMap;
+
+import javax.microedition.khronos.opengles.GL11;
+import android.os.Process;
+
+public final class GridDrawables {
+    // The display primitives.
+    public GridQuad mGrid;
+    public final GridQuadFrame mFrame;
+    public final GridQuad mTextGrid;
+    public final GridQuad mSelectedGrid;
+    public final GridQuad mVideoGrid;
+    public final GridQuad mLocationGrid;
+    public final GridQuad mSourceIconGrid;
+    public final GridQuad[] mFullscreenGrid = new GridQuad[3];
+    private GridQuad mGridNew;
+
+    // All the resource Textures.
+    private static final int TEXTURE_FRAME = R.drawable.stack_frame;
+    private static final int TEXTURE_GRID_FRAME = R.drawable.grid_frame;
+    private static final int TEXTURE_FRAME_FOCUS = R.drawable.stack_frame_focus;
+    private static final int TEXTURE_FRAME_PRESSED = R.drawable.stack_frame_gold;
+    private static final int TEXTURE_LOCATION = R.drawable.btn_location_filter_unscaled;
+    private static final int TEXTURE_VIDEO = R.drawable.videooverlay;
+    private static final int TEXTURE_CHECKMARK_ON = R.drawable.grid_check_on;
+    private static final int TEXTURE_CHECKMARK_OFF = R.drawable.grid_check_off;
+    private static final int TEXTURE_CAMERA_SMALL = R.drawable.icon_camera_small_unscaled;
+    private static final int TEXTURE_PICASA_SMALL = R.drawable.icon_picasa_small_unscaled;
+    public static final int[] TEXTURE_SPINNER = new int[8];
+    private static final int TEXTURE_TRANSPARENT = R.drawable.transparent;
+    private static final int TEXTURE_PLACEHOLDER = R.drawable.grid_placeholder;
+    
+    public Texture mTextureFrame;
+    public Texture mTextureGridFrame;
+    public Texture mTextureFrameFocus;
+    public Texture mTextureFramePressed;
+    public Texture mTextureLocation;
+    public Texture mTextureVideo;
+    public Texture mTextureCheckmarkOn;
+    public Texture mTextureCheckmarkOff;
+    public Texture mTextureCameraSmall;
+    public Texture mTexturePicasaSmall;
+    public Texture[] mTextureSpinner = new Texture[8];
+    public Texture mTextureTransparent;
+    public Texture mTexturePlaceholder;
+
+    // The textures generated from strings.
+    public final HashMap<String, StringTexture> mStringTextureTable = new HashMap<String, StringTexture>(128);
+
+    public GridDrawables(final int itemWidth, final int itemHeight) {
+        // We first populate the spinner textures.
+        final int[] textureSpinner = TEXTURE_SPINNER;
+        textureSpinner[0] = R.drawable.ic_spinner1;
+        textureSpinner[1] = R.drawable.ic_spinner2;
+        textureSpinner[2] = R.drawable.ic_spinner3;
+        textureSpinner[3] = R.drawable.ic_spinner4;
+        textureSpinner[4] = R.drawable.ic_spinner5;
+        textureSpinner[5] = R.drawable.ic_spinner6;
+        textureSpinner[6] = R.drawable.ic_spinner7;
+        textureSpinner[7] = R.drawable.ic_spinner8;
+        
+        final float height = 1.0f;
+        final float width = (float) (height * itemWidth) / (float) itemHeight;
+        final float aspectRatio = (float) itemWidth / (float) itemHeight;
+        final float oneByAspect = 1.0f / aspectRatio;
+
+        // We create the grid quad.
+        mGrid = GridQuad.createGridQuad(width, height, 0, 0, 1.0f, oneByAspect, false);
+
+        // We create the quads used in fullscreen.
+        mFullscreenGrid[0] = GridQuad.createGridQuad(width, height, 0, 0, 1.0f, oneByAspect, false);
+        mFullscreenGrid[0].setDynamic(true);
+        mFullscreenGrid[1] = GridQuad.createGridQuad(width, height, 0, 0, 1.0f, oneByAspect, false);
+        mFullscreenGrid[1].setDynamic(true);
+        mFullscreenGrid[2] = GridQuad.createGridQuad(width, height, 0, 0, 1.0f, oneByAspect, false);
+        mFullscreenGrid[2].setDynamic(true);
+
+        // We create supplementary quads for the checkmarks, video overlay and location button
+        float sizeOfSelectedIcon = 32 * Gallery.PIXEL_DENSITY;  // In pixels.
+        sizeOfSelectedIcon /= itemHeight;
+        float sizeOfLocationIcon = 52 * Gallery.PIXEL_DENSITY;  // In pixels.
+        sizeOfLocationIcon /= itemHeight;
+        float sizeOfSourceIcon = 76 * Gallery.PIXEL_DENSITY;  // In pixels.
+        sizeOfSourceIcon /= itemHeight;
+        mSelectedGrid = GridQuad.createGridQuad(sizeOfSelectedIcon, sizeOfSelectedIcon, -0.5f, 0.25f, 1.0f, 1.0f, false);
+        mVideoGrid = GridQuad.createGridQuad(sizeOfSelectedIcon, sizeOfSelectedIcon, -0.08f, -0.09f, 1.0f, 1.0f, false);
+        mLocationGrid = GridQuad.createGridQuad(sizeOfLocationIcon, sizeOfLocationIcon, 0, 0, 1.0f, 1.0f, false);
+        mSourceIconGrid = GridQuad.createGridQuad(sizeOfSourceIcon, sizeOfSourceIcon, 0, 0, 1.0f, 1.0f, false);
+        
+        // We create the quad for the text label.
+        float seedTextWidth = (Gallery.PIXEL_DENSITY < 1.5f) ? 128.0f : 256.0f;
+        float textWidth = (seedTextWidth / (float) itemWidth) * width;
+        float textHeightPow2 = (Gallery.PIXEL_DENSITY < 1.5f) ? 32.0f : 64.0f;
+        float textHeight = (textHeightPow2 / (float) itemHeight) * height;
+        float textOffsetY = 0.0f;
+        mTextGrid = GridQuad.createGridQuad(textWidth, textHeight, 0, textOffsetY, 1.0f, 1.0f, false);
+
+        // We finally create the frame around every grid item
+        mFrame = GridQuadFrame.createFrame(width, height, itemWidth, itemHeight);
+        
+        Thread creationThread = new Thread() {
+            public void run() {
+                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+                try {
+                    Thread.sleep(2000);
+                } catch (InterruptedException e) {
+                    ;
+                }
+                mGridNew = GridQuad.createGridQuad(width, height, 0, 0, 1.0f, oneByAspect, true);
+            }
+        };
+        creationThread.start();
+    }
+    
+    public void swapGridQuads(GL11 gl) {
+        if (mGridNew != null) {
+            mGrid.freeHardwareBuffers(gl);
+            mGrid = mGridNew;
+            mGrid.generateHardwareBuffers(gl);
+            mGridNew = null;
+        }
+    }
+
+    public void onSurfaceCreated(RenderView view, GL11 gl) {
+        // The grid quad.
+        mGrid.freeHardwareBuffers(gl);
+        mGrid.generateHardwareBuffers(gl);
+
+        // The fullscreen quads.
+        mFullscreenGrid[0].freeHardwareBuffers(gl);
+        mFullscreenGrid[1].freeHardwareBuffers(gl);
+        mFullscreenGrid[2].freeHardwareBuffers(gl);
+        mFullscreenGrid[0].generateHardwareBuffers(gl);
+        mFullscreenGrid[1].generateHardwareBuffers(gl);
+        mFullscreenGrid[2].generateHardwareBuffers(gl);
+
+        // Supplementary quads.
+        mSelectedGrid.freeHardwareBuffers(gl);
+        mVideoGrid.freeHardwareBuffers(gl);
+        mLocationGrid.freeHardwareBuffers(gl);
+        mSourceIconGrid.freeHardwareBuffers(gl);
+        mSelectedGrid.generateHardwareBuffers(gl);
+        mVideoGrid.generateHardwareBuffers(gl);
+        mLocationGrid.generateHardwareBuffers(gl);
+        mSourceIconGrid.generateHardwareBuffers(gl);
+
+        // Text quads.
+        mTextGrid.freeHardwareBuffers(gl);
+        mTextGrid.generateHardwareBuffers(gl);
+
+        // Frame mesh.
+        mFrame.freeHardwareBuffers(gl);
+        mFrame.generateHardwareBuffers(gl);
+
+        // Clear the string table.
+        mStringTextureTable.clear();
+        
+        // Regenerate all the textures.
+        mTextureFrame = view.getResource(TEXTURE_FRAME, false);
+        mTextureGridFrame = view.getResource(TEXTURE_GRID_FRAME, false);
+        view.loadTexture(mTextureGridFrame);
+        mTextureFrameFocus = view.getResource(TEXTURE_FRAME_FOCUS, false);
+        mTextureFramePressed = view.getResource(TEXTURE_FRAME_PRESSED, false);
+        mTextureLocation = view.getResource(TEXTURE_LOCATION, false);
+        mTextureVideo = view.getResource(TEXTURE_VIDEO, false);
+        mTextureCheckmarkOn = view.getResource(TEXTURE_CHECKMARK_ON, false);
+        mTextureCheckmarkOff = view.getResource(TEXTURE_CHECKMARK_OFF, false);
+        mTextureCameraSmall = view.getResource(TEXTURE_CAMERA_SMALL, false);
+        mTexturePicasaSmall = view.getResource(TEXTURE_PICASA_SMALL, false);
+        mTextureTransparent = view.getResource(TEXTURE_TRANSPARENT, false);
+        mTexturePlaceholder = view.getResource(TEXTURE_PLACEHOLDER, false);
+        
+        mTextureSpinner[0] = view.getResource(R.drawable.ic_spinner1);
+        mTextureSpinner[1] = view.getResource(R.drawable.ic_spinner2);
+        mTextureSpinner[2] = view.getResource(R.drawable.ic_spinner3);
+        mTextureSpinner[3] = view.getResource(R.drawable.ic_spinner4);
+        mTextureSpinner[4] = view.getResource(R.drawable.ic_spinner5);
+        mTextureSpinner[5] = view.getResource(R.drawable.ic_spinner6);
+        mTextureSpinner[6] = view.getResource(R.drawable.ic_spinner7);
+        mTextureSpinner[7] = view.getResource(R.drawable.ic_spinner8);
+    }
+
+    public int getIconForSet(MediaSet set, boolean scaled) {
+        // We return the scaled version for HUD rendering and the unscaled version for 3D rendering.
+        if (scaled) {
+            if (set == null) {
+                return R.drawable.icon_folder_small;
+            }
+            if (set.mPicasaAlbumId != Shared.INVALID) {
+                return R.drawable.icon_picasa_small;
+            } else if (set.mId == LocalDataSource.CAMERA_BUCKET_ID) {
+                return R.drawable.icon_camera_small;
+            } else {
+                return R.drawable.icon_folder_small;
+            }
+        } else {
+            if (set == null) {
+                return R.drawable.icon_folder_small_unscaled;
+            }
+            if (set.mPicasaAlbumId != Shared.INVALID) {
+                return R.drawable.icon_picasa_small_unscaled;
+            } else if (set.mId == LocalDataSource.CAMERA_BUCKET_ID) {
+                return R.drawable.icon_camera_small_unscaled;
+            } else {
+                return R.drawable.icon_folder_small_unscaled;
+            }
+        }
+    }
+}
diff --git a/src/com/cooliris/media/GridInputProcessor.java b/src/com/cooliris/media/GridInputProcessor.java
new file mode 100644
index 0000000..78f5072
--- /dev/null
+++ b/src/com/cooliris/media/GridInputProcessor.java
@@ -0,0 +1,674 @@
+package com.cooliris.media;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.os.SystemClock;
+import android.view.GestureDetector;
+import android.view.HapticFeedbackConstants;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+public final class GridInputProcessor implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
+    private int mCurrentFocusSlot;
+    private boolean mCurrentFocusIsPressed;
+    private int mCurrentSelectedSlot;
+
+    private float mPrevTiltValueLowPass;
+    private float mPrevShakeValueHighPass;
+    private float mShakeValue;
+    private int mTouchPosX;
+    private int mTouchPosY;
+    private int mActionCode;
+    private long mPrevTouchTime;
+    private float mFirstTouchPosX;
+    private float mFirstTouchPosY;
+    private float mPrevTouchPosX;
+    private float mPrevTouchPosY;
+    private float mTouchVelX;
+    private float mTouchVelY;
+    private boolean mProcessTouch;
+    private boolean mTouchMoved;
+    private float mDpadIgnoreTime = 0.0f;
+    private GridCamera mCamera;
+    private GridLayer mLayer;
+    private Context mContext;
+    private Pool<Vector3f> mPool;
+    private DisplayItem[] mDisplayItems;
+    private boolean mPrevHitEdge;
+    private boolean mTouchFeedbackDelivered;
+    private GestureDetector mGestureDetector;
+    private RenderView mView;
+
+    public GridInputProcessor(Context context, GridCamera camera, GridLayer layer, RenderView view, Pool<Vector3f> pool,
+            DisplayItem[] displayItems) {
+        mPool = pool;
+        mCamera = camera;
+        mLayer = layer;
+        mCurrentFocusSlot = Shared.INVALID;
+        mCurrentSelectedSlot = Shared.INVALID;
+        mContext = context;
+        mDisplayItems = displayItems;
+        mGestureDetector = new GestureDetector(context, this);
+        mGestureDetector.setIsLongpressEnabled(true);
+        mView = view;
+    }
+
+    public int getCurrentFocusSlot() {
+        return mCurrentFocusSlot;
+    }
+
+    public int getCurrentSelectedSlot() {
+        return mCurrentSelectedSlot;
+    }
+
+    public void setCurrentSelectedSlot(int slot) {
+        mCurrentSelectedSlot = slot;
+        GridLayer layer = mLayer;
+        layer.setState(GridLayer.STATE_FULL_SCREEN);
+        mCamera.mConvergenceSpeed = 2.0f;
+        DisplayItem displayItem = layer.getDisplayItemForSlotId(slot);
+        MediaItem item = null;
+        if (displayItem != null)
+            item = displayItem.mItemRef;
+        layer.getHud().fullscreenSelectionChanged(item, mCurrentSelectedSlot + 1, layer.getCompleteRange().end + 1);
+    }
+
+    public void onSensorChanged(RenderView view, SensorEvent event, int state) {
+        switch (event.sensor.getType()) {
+        case Sensor.TYPE_ACCELEROMETER:
+        case Sensor.TYPE_ORIENTATION:
+            float[] values = event.values;
+            float valueToUse = (mCamera.mWidth < mCamera.mHeight) ? values[0] : -values[1];
+            float tiltValue = 0.8f * mPrevTiltValueLowPass + 0.2f * valueToUse;
+            if (Math.abs(tiltValue) < 0.5f)
+                tiltValue = 0.0f;
+            if (state == GridLayer.STATE_FULL_SCREEN)
+                tiltValue = 0.0f;
+            if (tiltValue != 0.0f)
+                view.requestRender();
+            mCamera.mEyeOffsetX = -3.0f * tiltValue;
+            float shakeValue = values[1] * values[1] + values[2] * values[2];
+            mShakeValue = shakeValue - mPrevShakeValueHighPass;
+            mPrevShakeValueHighPass = shakeValue;
+            if (mShakeValue < 16.0f) {
+                mShakeValue = 0;
+            } else {
+                mShakeValue = mShakeValue * 4.0f;
+                if (mShakeValue > 200) {
+                    mShakeValue = 200;
+                }
+            }
+            break;
+        }
+    }
+
+    public boolean onTouchEvent(MotionEvent event) {
+        mTouchPosX = (int) (event.getX());
+        mTouchPosY = (int) (event.getY());
+        mActionCode = event.getAction();
+        long timestamp = SystemClock.elapsedRealtime();
+        long delta = timestamp - mPrevTouchTime;
+        mPrevTouchTime = timestamp;
+        float timeElapsed = (float) delta;
+        timeElapsed = timeElapsed * 0.001f; // division by 1000 for seconds
+        switch (mActionCode) {
+        case MotionEvent.ACTION_UP:
+            if (mProcessTouch == false) {
+                touchBegan(mTouchPosX, mTouchPosY);
+            }
+            touchEnded(mTouchPosX, mTouchPosY, timeElapsed);
+            break;
+        case MotionEvent.ACTION_DOWN:
+            mPrevTouchTime = timestamp;
+            touchBegan(mTouchPosX, mTouchPosY);
+            break;
+        case MotionEvent.ACTION_MOVE:
+            touchMoved(mTouchPosX, mTouchPosY, timeElapsed);
+            break;
+        }
+        mGestureDetector.onTouchEvent(event);
+        return true;
+    }
+
+    public boolean onKeyDown(int keyCode, KeyEvent event, int state) {
+        GridLayer layer = mLayer;
+        if (keyCode == KeyEvent.KEYCODE_BACK) {
+            if (layer.getViewIntent())
+                return false;
+            if (layer.getHud().getMode() == HudLayer.MODE_SELECT) {
+                layer.deselectAll();
+                return true;
+            }
+            if (layer.inSlideShowMode()) {
+                layer.endSlideshow();
+                layer.getHud().setAlpha(1.0f);
+                return true;
+            }
+            float zoomValue = layer.getZoomValue();
+            if (zoomValue != 1.0f) {
+                layer.setZoomValue(1.0f);
+                layer.centerCameraForSlot(mCurrentSelectedSlot, 1.0f);
+                return true;
+            }
+            layer.goBack();
+            if (state == GridLayer.STATE_MEDIA_SETS)
+                return false;
+            return true;
+        }
+        if (mDpadIgnoreTime < 0.1f)
+            return true;
+        mDpadIgnoreTime = 0.0f;
+        IndexRange bufferedVisibleRange = layer.getBufferedVisibleRange();
+        int firstBufferedVisibleSlot = bufferedVisibleRange.begin;
+        int lastBufferedVisibleSlot = bufferedVisibleRange.end;
+        int anchorSlot = layer.getAnchorSlotIndex(GridLayer.ANCHOR_CENTER);
+        if (state == GridLayer.STATE_FULL_SCREEN) {
+            layer.endSlideshow();
+            boolean needsVibrate = false;
+            if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+                needsVibrate = !layer.changeFocusToNextSlot(1.0f);
+            }
+            if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
+                needsVibrate = !layer.changeFocusToPreviousSlot(1.0f);
+            }
+            if (needsVibrate) {
+                vibrateShort();
+            }
+            if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && !mCamera.isAnimating()) {
+                if (layer.getZoomValue() == 1.0f)
+                    layer.zoomInToSelectedItem();
+                else
+                    layer.setZoomValue(1.0f);
+            }
+            if (keyCode == KeyEvent.KEYCODE_MENU) {
+                if (layer.getHud().getMode() == HudLayer.MODE_NORMAL)
+                    layer.enterSelectionMode();
+                else
+                    layer.deselectAll();
+            }
+        } else {
+            mCurrentFocusIsPressed = false;
+            int numRows = ((GridLayoutInterface) layer.getLayoutInterface()).mNumRows;
+            if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && mCurrentFocusSlot != Shared.INVALID) {
+                if (layer.getHud().getMode() != HudLayer.MODE_SELECT) {
+                    boolean centerCamera = layer.tapGesture(mCurrentFocusSlot, false);
+                    if (centerCamera) {
+                        int slotId = mCurrentFocusSlot;
+                        selectSlot(slotId);
+                    }
+                    mCurrentFocusSlot = Shared.INVALID;
+                    return true;
+                } else {
+                    layer.addSlotToSelectedItems(mCurrentFocusSlot, true, true);
+                }
+                mCurrentFocusIsPressed = true;
+            } else if (keyCode == KeyEvent.KEYCODE_MENU && mCurrentFocusSlot != Shared.INVALID) {
+                if (layer.getHud().getMode() == HudLayer.MODE_NORMAL)
+                    layer.enterSelectionMode();
+                else
+                    layer.deselectAll();
+            } else if (mCurrentFocusSlot == Shared.INVALID) {
+                mCurrentFocusSlot = anchorSlot;
+            } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+                mCurrentFocusSlot += numRows;
+            } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
+                mCurrentFocusSlot -= numRows;
+            } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
+                --mCurrentFocusSlot;
+            } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
+                ++mCurrentFocusSlot;
+            }
+            if (mCurrentFocusSlot > lastBufferedVisibleSlot) {
+                mCurrentFocusSlot = lastBufferedVisibleSlot;
+            }
+            if (mCurrentFocusSlot < firstBufferedVisibleSlot)
+                mCurrentFocusSlot = firstBufferedVisibleSlot;
+            if (mCurrentFocusSlot != Shared.INVALID) {
+                layer.centerCameraForSlot(mCurrentFocusSlot, 1.0f);
+            }
+        }
+        return false;
+    }
+
+    private void touchBegan(int posX, int posY) {
+        mPrevTouchPosX = posX;
+        mPrevTouchPosY = posY;
+        mFirstTouchPosX = posX;
+        mFirstTouchPosY = posY;
+        mTouchVelX = 0;
+        mTouchVelY = 0;
+        mProcessTouch = true;
+        mTouchMoved = false;
+        mCamera.stopMovementInX();
+        GridLayer layer = mLayer;
+        mCurrentFocusSlot = layer.getSlotIndexForScreenPosition(posX, posY);
+        mCurrentFocusIsPressed = true;
+        mTouchFeedbackDelivered = false;
+        HudLayer hud = layer.getHud();
+        if (hud.getMode() == HudLayer.MODE_SELECT)
+            hud.closeSelectionMenu();
+        if (layer.getState() == GridLayer.STATE_FULL_SCREEN && hud.getMode() == HudLayer.MODE_SELECT) {
+            layer.deselectAll();
+            hud.setAlpha(1.0f);
+        }
+        int slotId = layer.getSlotIndexForScreenPosition(posX, posY);
+        if (slotId != Shared.INVALID && layer.getState() != GridLayer.STATE_FULL_SCREEN) {
+            vibrateShort();
+        }
+    }
+
+    private void touchMoved(int posX, int posY, float timeElapsedx) {
+        if (mProcessTouch) {
+            GridLayer layer = mLayer;
+            GridCamera camera = mCamera;
+            float deltaX = -(posX - mPrevTouchPosX); // negation since the wall
+            // moves in a direction
+            // opposite to that of
+            // the touch
+            float deltaY = -(posY - mPrevTouchPosY);
+            if (Math.abs(deltaX) >= 10.0f || Math.abs(deltaY) >= 10.0f) {
+                mTouchMoved = true;
+            }
+            Pool<Vector3f> pool = mPool;
+            Vector3f firstPosition = pool.create();
+            Vector3f lastPosition = pool.create();
+            Vector3f deltaAnchorPosition = pool.create();
+            Vector3f worldPosDelta = pool.create();
+            try {
+                deltaAnchorPosition.set(layer.getDeltaAnchorPosition());
+                LayoutInterface layout = layer.getLayoutInterface();
+                GridCameraManager.getSlotPositionForSlotIndex(0, camera, layout, deltaAnchorPosition, firstPosition);
+                int lastSlotIndex = 0;
+                IndexRange completeRange = layer.getCompleteRange();
+                synchronized (completeRange) {
+                    lastSlotIndex = completeRange.end;
+                }
+                GridCameraManager.getSlotPositionForSlotIndex(lastSlotIndex, camera, layout, deltaAnchorPosition, lastPosition);
+
+                camera.convertToRelativeCameraSpace(deltaX, deltaY, 0, worldPosDelta);
+                deltaX = worldPosDelta.x;
+                deltaY = worldPosDelta.y;
+                camera.moveBy(deltaX, (layer.getZoomValue() == 1.0f) ? 0 : deltaY, 0);
+                deltaX *= camera.mScale;
+                deltaY *= camera.mScale;
+            } finally {
+                pool.delete(firstPosition);
+                pool.delete(lastPosition);
+                pool.delete(deltaAnchorPosition);
+                pool.delete(worldPosDelta);
+            }
+            if (layer.getZoomValue() == 1.0f) {
+                if (camera
+                        .computeConstraints(false, (layer.getState() != GridLayer.STATE_FULL_SCREEN), firstPosition, lastPosition)) {
+                    deltaX = 0.0f;
+                    // vibrate
+                    if (!mTouchFeedbackDelivered) {
+                        mTouchFeedbackDelivered = true;
+                        vibrateLong();
+                    }
+                }
+            }
+            mTouchVelX = deltaX * timeElapsedx;
+            mTouchVelY = deltaY * timeElapsedx;
+            float maxVelXx = (mCamera.mWidth * 0.5f);
+            float maxVelYx = (mCamera.mHeight);
+            mTouchVelX = FloatUtils.clamp(mTouchVelX, -maxVelXx, maxVelXx);
+            mTouchVelY = FloatUtils.clamp(mTouchVelY, -maxVelYx, maxVelYx);
+            mPrevTouchPosX = posX;
+            mPrevTouchPosY = posY;
+            // you want the movement to track the finger immediately
+            if (mTouchMoved == false)
+                mCurrentFocusSlot = layer.getSlotIndexForScreenPosition(posX, posY);
+            else
+                mCurrentFocusSlot = Shared.INVALID;
+            if (!mCamera.isZAnimating()) {
+                mCamera.commitMoveInX();
+                mCamera.commitMoveInY();
+            }
+            int anchorSlotIndex = layer.getAnchorSlotIndex(GridLayer.ANCHOR_LEFT);
+            DisplayItem[] displayItems = mDisplayItems;
+            IndexRange bufferedVisibleRange = layer.getBufferedVisibleRange();
+            int firstBufferedVisibleSlot = bufferedVisibleRange.begin;
+            int lastBufferedVisibleSlot = bufferedVisibleRange.end;
+            synchronized (displayItems) {
+                if (anchorSlotIndex >= firstBufferedVisibleSlot && anchorSlotIndex <= lastBufferedVisibleSlot) {
+                    DisplayItem item = displayItems[(anchorSlotIndex - firstBufferedVisibleSlot) * GridLayer.MAX_ITEMS_PER_SLOT];
+                    if (item != null) {
+                        layer.getHud().setTimeBarTime(item.mItemRef.mDateTakenInMs);
+                    }
+                }
+            }
+        }
+    }
+
+    private void touchEnded(int posX, int posY, float timeElapsedx) {
+        if (mProcessTouch == false)
+            return;
+        int maxPixelsBeforeSwitch = mCamera.mWidth / 8;
+        mCamera.mConvergenceSpeed = 2.0f;
+        GridLayer layer = mLayer;
+        if (layer.getExpandedSlot() == Shared.INVALID && !layer.feedAboutToChange()) {
+            if (mCurrentSelectedSlot != Shared.INVALID) {
+                if (layer.getState() == GridLayer.STATE_FULL_SCREEN) {
+                    if (!mTouchMoved) {
+                        // tap gesture for fullscreen
+                        if (layer.getZoomValue() == 1.0f)
+                            layer.changeFocusToSlot(mCurrentSelectedSlot, 1.0f);
+                    } else if (layer.getZoomValue() == 1.0f) {
+                        // we want to snap to a new slotIndex based on where the
+                        // current position is
+                        if (layer.inSlideShowMode()) {
+                            layer.endSlideshow();
+                        }
+                        float deltaX = posX - mFirstTouchPosX;
+                        float deltaY = posY - mFirstTouchPosY;
+                        if (deltaY != 0) {
+                            // it has moved vertically
+                        }
+                        layer.changeFocusToSlot(mCurrentSelectedSlot, 1.0f);
+                        HudLayer hud = layer.getHud();
+                        if (deltaX > maxPixelsBeforeSwitch && hud.getMode() != HudLayer.MODE_SELECT) {
+                            layer.changeFocusToPreviousSlot(1.0f);
+                        } else if (deltaX < -maxPixelsBeforeSwitch && hud.getMode() != HudLayer.MODE_SELECT) {
+                            layer.changeFocusToNextSlot(1.0f);
+                        }
+                    } else {
+                        // in zoomed state
+                        // we do nothing for now, but we should clamp to the
+                        // image bounds
+                        boolean hitEdge = layer.constrainCameraForSlot(mCurrentSelectedSlot);
+                        // mPrevHitEdge = false;
+                        if (hitEdge && mPrevHitEdge) {
+                            float deltaX = posX - mFirstTouchPosX;
+                            float deltaY = posY - mFirstTouchPosY;
+                            maxPixelsBeforeSwitch *= 4;
+                            if (deltaY != 0) {
+                                // it has moved vertically
+                            }
+                            mPrevHitEdge = false;
+                            HudLayer hud = layer.getHud();
+                            if (deltaX > maxPixelsBeforeSwitch && hud.getMode() != HudLayer.MODE_SELECT) {
+                                layer.changeFocusToPreviousSlot(1.0f);
+                            } else if (deltaX < -maxPixelsBeforeSwitch && hud.getMode() != HudLayer.MODE_SELECT) {
+                                layer.changeFocusToNextSlot(1.0f);
+                            } else {
+                                mPrevHitEdge = hitEdge;
+                            }
+                        } else {
+                            mPrevHitEdge = hitEdge;
+                        }
+                    }
+                }
+            } else {
+                if (!layer.feedAboutToChange() && layer.getZoomValue() == 1.0f) {
+                    constrainCamera(true);
+                }
+            }
+        }
+        mCurrentFocusSlot = Shared.INVALID;
+        mCurrentFocusIsPressed = false;
+        mPrevTouchPosX = posX;
+        mPrevTouchPosY = posY;
+        mProcessTouch = false;
+    }
+
+    private void constrainCamera(boolean b) {
+        Pool<Vector3f> pool = mPool;
+        GridLayer layer = mLayer;
+        Vector3f firstPosition = pool.create();
+        Vector3f lastPosition = pool.create();
+        Vector3f deltaAnchorPosition = pool.create();
+        try {
+            deltaAnchorPosition.set(layer.getDeltaAnchorPosition());
+            GridCamera camera = mCamera;
+            LayoutInterface layout = layer.getLayoutInterface();
+            GridCameraManager.getSlotPositionForSlotIndex(0, camera, layout, deltaAnchorPosition, firstPosition);
+            int lastSlotIndex = 0;
+            IndexRange completeRange = layer.getCompleteRange();
+            synchronized (completeRange) {
+                lastSlotIndex = completeRange.end;
+            }
+            GridCameraManager.getSlotPositionForSlotIndex(lastSlotIndex, camera, layout, deltaAnchorPosition, lastPosition);
+            camera.computeConstraints(true, (layer.getState() != GridLayer.STATE_FULL_SCREEN), firstPosition, lastPosition);
+        } finally {
+            pool.delete(firstPosition);
+            pool.delete(lastPosition);
+            pool.delete(deltaAnchorPosition);
+        }
+    }
+
+    public int getSelectedSlot() {
+        return mCurrentSelectedSlot;
+    }
+
+    public int getFocusSlot() {
+        return mCurrentFocusSlot;
+    }
+
+    public void clearSelection() {
+        mCurrentSelectedSlot = Shared.INVALID;
+    }
+
+    public void clearFocus() {
+        mCurrentFocusSlot = Shared.INVALID;
+    }
+
+    public boolean isFocusItemPressed() {
+        return mCurrentFocusIsPressed;
+    }
+
+    public void update(float timeElapsed) {
+        mDpadIgnoreTime += timeElapsed;
+    }
+
+    public void setCurrentFocusSlot(int slotId) {
+        mCurrentSelectedSlot = slotId;
+    }
+
+    public boolean onDown(MotionEvent e) {
+        // TODO Auto-generated method stub
+        return true;
+    }
+
+    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+        if (mCurrentSelectedSlot == Shared.INVALID) {
+            mCamera.moveYTo(0);
+            mCamera.moveZTo(0);
+            mCamera.mConvergenceSpeed = 1.0f;
+            float normalizedVelocity = velocityX * mCamera.mOneByScale;
+            // mCamera.moveBy(-velocityX * mCamera.mOneByScale * 0.25f, 0, 0);
+            // constrainCamera(true);
+            IndexRange visibleRange = mLayer.getVisibleRange();
+            int numVisibleSlots = visibleRange.end - visibleRange.begin;
+            if (numVisibleSlots > 0) {
+                float fastFlingVelocity = 20.0f;
+                int slotsToSkip = (int) (numVisibleSlots * (-normalizedVelocity / fastFlingVelocity));
+                int maxSlots = numVisibleSlots;
+                if (slotsToSkip > maxSlots)
+                    slotsToSkip = maxSlots;
+                if (slotsToSkip < -maxSlots)
+                    slotsToSkip = -maxSlots;
+                if (slotsToSkip <= 1) {
+                    if (velocityX > 0)
+                        slotsToSkip = -2;
+                    else
+                        slotsToSkip = 2;
+                }
+                int slotToGetTo = mLayer.getAnchorSlotIndex(GridLayer.ANCHOR_CENTER) + slotsToSkip;
+                if (slotToGetTo < 0)
+                    slotToGetTo = 0;
+                int lastSlot = mLayer.getCompleteRange().end;
+                if (slotToGetTo > lastSlot)
+                    slotToGetTo = lastSlot;
+                mLayer.centerCameraForSlot(slotToGetTo, 1.0f);
+            }
+            constrainCamera(true);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public void onLongPress(MotionEvent e) {
+        if (mLayer.getFeed() != null && mLayer.getFeed().isSingleImageMode()) {
+            HudLayer hud = mLayer.getHud();
+            hud.getPathBar().setHidden(true);
+            hud.getMenuBar().setHidden(true);
+            if (hud.getMode() != HudLayer.MODE_NORMAL)
+                hud.setMode(HudLayer.MODE_NORMAL);
+        }
+        if (mCurrentFocusSlot != Shared.INVALID) {
+            vibrateLong();
+            GridLayer layer = mLayer;
+            if (layer.getState() == GridLayer.STATE_FULL_SCREEN) {
+                layer.deselectAll();
+            }
+            HudLayer hud = layer.getHud();
+            hud.enterSelectionMode();
+            layer.addSlotToSelectedItems(mCurrentFocusSlot, true, true);
+        }
+    }
+
+    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+        // TODO Auto-generated method stub
+        return false;
+    }
+
+    public void onShowPress(MotionEvent e) {
+        // TODO Auto-generated method stub
+
+    }
+
+    public boolean onSingleTapUp(MotionEvent e) {
+        GridLayer layer = mLayer;
+        int posX = (int) e.getX();
+        int posY = (int) e.getY();
+        if (mCurrentSelectedSlot != Shared.INVALID) {
+            // Fullscreen mode.
+            mCamera.mConvergenceSpeed = 2.0f;
+            int slotId = mCurrentSelectedSlot;
+            if (layer.getZoomValue() == 1.0f) {
+                layer.centerCameraForSlot(slotId, 1.0f);
+            } else {
+                layer.constrainCameraForSlot(slotId);
+            }
+            DisplayItem displayItem = layer.getDisplayItemForSlotId(slotId);
+            if (displayItem != null) {
+                final MediaItem item = displayItem.mItemRef;
+                int heightBy2 = mCamera.mHeight / 2;
+                boolean posYInBounds = (Math.abs(posY - heightBy2) < 64);
+                if (posX < 32 && posYInBounds) {
+                    layer.changeFocusToPreviousSlot(1.0f);
+                } else if (posX > mCamera.mWidth - 32 && posYInBounds) {
+                    layer.changeFocusToNextSlot(1.0f);
+                } else if (item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO) {
+                    Utils.playVideo(mContext, item);
+                } else {
+                    // We stop any slideshow.
+                    HudLayer hud = layer.getHud();
+                    layer.endSlideshow();
+                    if (hud.getMode() == HudLayer.MODE_SELECT) {
+                        hud.setAlpha(1.0f);
+                    }
+                }
+            }
+        } else {
+            int slotId = layer.getSlotIndexForScreenPosition(posX, posY);
+            if (slotId != Shared.INVALID) {
+                HudLayer hud = layer.getHud();
+                if (hud.getMode() == HudLayer.MODE_SELECT) {
+                    layer.addSlotToSelectedItems(slotId, true, true);
+                } else {
+                    boolean centerCamera = (mCurrentSelectedSlot == Shared.INVALID) ? layer.tapGesture(slotId, false) : true;
+                    if (centerCamera) {
+                        // We check if this item is a video or not.
+                        selectSlot(slotId);
+                    }
+                }
+            } else {
+                int state = layer.getState();
+                if (state != GridLayer.STATE_FULL_SCREEN && state != GridLayer.STATE_GRID_VIEW
+                        && layer.getHud().getMode() != HudLayer.MODE_SELECT) {
+                    slotId = layer.getMetadataSlotIndexForScreenPosition(posX, posY);
+                    if (slotId != Shared.INVALID) {
+                        layer.tapGesture(slotId, true);
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
+    private void selectSlot(int slotId) {
+        GridLayer layer = mLayer;
+        if (layer.getState() == GridLayer.STATE_GRID_VIEW) {
+            DisplayItem displayItem = layer.getDisplayItemForSlotId(slotId);
+            if (displayItem != null) {
+                final MediaItem item = displayItem.mItemRef;
+                if (layer.getPickIntent()) {
+                    // we need to return this item
+                    ((Gallery) mContext).getHandler().post(new Runnable() {
+                        public void run() {
+                            ((Gallery) mContext).launchCropperOrFinish(item);
+                        }
+                    });
+                    return;
+                }
+                if (item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO) {
+                    Utils.playVideo(mContext, item);
+                } else {
+                    mCurrentSelectedSlot = slotId;
+                    layer.endSlideshow();
+                    layer.setState(GridLayer.STATE_FULL_SCREEN);
+                    mCamera.mConvergenceSpeed = 2.0f;
+                    layer.getHud().fullscreenSelectionChanged(item, mCurrentSelectedSlot + 1, layer.getCompleteRange().end + 1);
+                }
+            }
+        }
+    }
+
+    public boolean onDoubleTap(MotionEvent e) {
+        final GridLayer layer = mLayer;
+        if (layer.getState() == GridLayer.STATE_FULL_SCREEN && !mCamera.isZAnimating()) {
+            float posX = e.getX();
+            float posY = e.getY();
+            final Vector3f retVal = new Vector3f();
+            posX -= (mCamera.mWidth / 2);
+            posY -= (mCamera.mHeight / 2);
+            mCamera.convertToRelativeCameraSpace(posX, posY, 0, retVal);
+            if (layer.getZoomValue() == 1.0f) {
+                layer.setZoomValue(3f);
+                mCamera.update(0.001f);
+                mCamera.moveBy(retVal.x, retVal.y, 0);
+                layer.constrainCameraForSlot(mCurrentSelectedSlot);
+            } else {
+                layer.setZoomValue(1.0f);
+            }
+            mCamera.mConvergenceSpeed = 2.0f;
+        } else {
+            return onSingleTapConfirmed(e);
+        }
+        return true;
+    }
+
+    public boolean onDoubleTapEvent(MotionEvent e) {
+        return false;
+    }
+
+    public boolean onSingleTapConfirmed(MotionEvent e) {
+        return false;
+    }
+
+    public boolean touchPressed() {
+        return mProcessTouch;
+    }
+
+    private void vibrateShort() {
+        mView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+    }
+
+    private void vibrateLong() {
+        mView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+    }
+}
diff --git a/src/com/cooliris/media/GridLayer.java b/src/com/cooliris/media/GridLayer.java
new file mode 100644
index 0000000..5716edd
--- /dev/null
+++ b/src/com/cooliris/media/GridLayer.java
@@ -0,0 +1,1394 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+import javax.microedition.khronos.opengles.GL11;
+
+import android.hardware.SensorEvent;
+import android.opengl.GLU;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.content.Context;
+
+public final class GridLayer extends RootLayer implements MediaFeed.Listener, TimeBar.Listener {
+    public static final int STATE_MEDIA_SETS = 0;
+    public static final int STATE_GRID_VIEW = 1;
+    public static final int STATE_FULL_SCREEN = 2;
+    public static final int STATE_TIMELINE = 3;
+
+    public static final int ANCHOR_LEFT = 0;
+    public static final int ANCHOR_RIGHT = 1;
+    public static final int ANCHOR_CENTER = 2;
+
+    public static final int MAX_ITEMS_PER_SLOT = 24;
+    public static final int MAX_DISPLAYED_ITEMS_PER_SLOT = 4;
+    public static final int MAX_DISPLAY_SLOTS = 96;
+    public static final int MAX_ITEMS_DRAWABLE = MAX_ITEMS_PER_SLOT * MAX_DISPLAY_SLOTS;
+
+    private static final float SLIDESHOW_TRANSITION_TIME = 3.5f;
+
+    private HudLayer mHud;
+    private int mState;
+    private IndexRange mBufferedVisibleRange;
+    private IndexRange mVisibleRange;
+    private IndexRange mPreviousDataRange;
+    private IndexRange mCompleteRange;
+    private Pool<Vector3f> mTempVec;
+    private final ArrayList<MediaItem> mTempList = new ArrayList<MediaItem>();
+    private final MediaItem[] mTempHash = new MediaItem[64];
+    private Vector3f mDeltaAnchorPositionUncommited;
+    private Vector3f mDeltaAnchorPosition;
+
+    // The display primitives.
+    private GridDrawables mDrawables;
+    private float mSelectedAlpha = 0.0f;
+    private float mTargetAlpha = 0.0f;
+
+    private GridCamera mCamera;
+    private GridCameraManager mCameraManager;
+    private GridDrawManager mDrawManager;
+    private GridInputProcessor mInputProcessor;
+
+    private boolean mFeedAboutToChange;
+    private boolean mPerformingLayoutChange;
+    private boolean mFeedChanged;
+
+    private LayoutInterface mLayoutInterface;
+    private LayoutInterface mPrevLayoutInterface;
+    private MediaFeed mMediaFeed;
+    private boolean mInAlbum = false;
+    private int mCurrentExpandedSlot;
+
+    private ArrayList<MediaItem> mVisibleItems;
+    private DisplayList mDisplayList = new DisplayList();
+    private DisplayItem[] mDisplayItems = new DisplayItem[MAX_ITEMS_DRAWABLE];
+    private DisplaySlot[] mDisplaySlots = new DisplaySlot[MAX_DISPLAY_SLOTS];
+    private float mTimeElapsedSinceTransition;
+    private BackgroundLayer mBackground;
+    private boolean mLocationFilter;
+    private float mZoomValue = 1.0f;
+    private float mCurrentFocusItemWidth = 1.0f;
+    private float mCurrentFocusItemHeight = 1.0f;
+    private float mTimeElapsedSinceGridViewReady = 0.0f;
+
+    private boolean mSlideshowMode;
+    private boolean mNoDeleteMode = false;
+    private float mTimeElapsedSinceView;
+    private MediaBucketList mBucketList = new MediaBucketList();
+    private float mTimeElapsedSinceStackViewReady;
+    private Pool<Vector3f> mTempVecAlt;
+    private Context mContext;
+    private RenderView mView;
+    private boolean mPickIntent;
+    private boolean mViewIntent;
+    private WakeLock mWakeLock;
+    private int mStartMemoryRange;
+    private int mFramesDirty;
+    private String mRequestFocusContentUri;
+
+    public GridLayer(Context context, int itemWidth, int itemHeight, LayoutInterface layoutInterface, RenderView view) {
+        mBackground = new BackgroundLayer(this);
+        mContext = context;
+        mView = view;
+        Vector3f[] vectorPool = new Vector3f[128];
+        int length = vectorPool.length;
+        for (int i = 0; i < length; ++i) {
+            vectorPool[i] = new Vector3f();
+        }
+        Vector3f[] vectorPoolRenderThread = new Vector3f[128];
+        length = vectorPoolRenderThread.length;
+        for (int i = 0; i < length; ++i) {
+            vectorPoolRenderThread[i] = new Vector3f();
+        }
+        mTempVec = new Pool<Vector3f>(vectorPool);
+        mTempVecAlt = new Pool<Vector3f>(vectorPoolRenderThread);
+        DisplaySlot[] displaySlots = mDisplaySlots;
+        for (int i = 0; i < MAX_DISPLAY_SLOTS; ++i) {
+            DisplaySlot slot = new DisplaySlot();
+            displaySlots[i] = slot;
+        }
+        mLayoutInterface = layoutInterface;
+        mCamera = new GridCamera(0, 0, itemWidth, itemHeight);
+        mDrawables = new GridDrawables(itemWidth, itemHeight);
+        mBufferedVisibleRange = new IndexRange();
+        mVisibleRange = new IndexRange();
+        mCompleteRange = new IndexRange();
+        mPreviousDataRange = new IndexRange();
+        mPreviousDataRange.begin = Shared.INVALID;
+        mPreviousDataRange.end = Shared.INVALID;
+        mDeltaAnchorPosition = new Vector3f();
+        mDeltaAnchorPositionUncommited = new Vector3f();
+        mPrevLayoutInterface = new GridLayoutInterface(1);
+        mVisibleItems = new ArrayList<MediaItem>();
+        mHud = new HudLayer(context);
+        mHud.setGridLayer(this);
+        mHud.getTimeBar().setListener(this);
+        mHud.getPathBar().pushLabel(R.drawable.icon_home_small, context.getResources().getString(R.string.app_name),
+                new Runnable() {
+                    public void run() {
+                        if (mHud.getAlpha() == 1.0f) {
+                            if (!mFeedAboutToChange) {
+                                setState(STATE_MEDIA_SETS);
+                            }
+                        } else {
+                            mHud.setAlpha(1.0f);
+                        }
+                    }
+                });
+        mCameraManager = new GridCameraManager(mCamera);
+        mDrawManager = new GridDrawManager(context, mCamera, mDrawables, mDisplayList, mDisplayItems, mDisplaySlots);
+        mInputProcessor = new GridInputProcessor(context, mCamera, this, mView, mTempVec, mDisplayItems);
+        setState(STATE_MEDIA_SETS);
+    }
+
+    public HudLayer getHud() {
+        return mHud;
+    }
+
+    public void shutdown() {
+        if (mMediaFeed != null) {
+            mMediaFeed.shutdown();
+        }
+        mContext = null;
+        mInputProcessor = null;
+        mBackground = null;
+        mBucketList = null;
+        mCameraManager = null;
+        mDrawManager = null;
+        mHud.shutDown();
+        mHud = null;
+        mView = null;
+
+        mDisplayItems = null;
+        mDisplayList = null;
+        mDisplaySlots = null;
+    }
+
+    public void stop() {
+        endSlideshow();
+        mBackground.clear();
+        handleLowMemory();
+    }
+
+    @Override
+    public void generate(RenderView view, RenderView.Lists lists) {
+        lists.updateList.add(this);
+        lists.opaqueList.add(this);
+        mBackground.generate(view, lists);
+        lists.blendedList.add(this);
+        lists.hitTestList.add(this);
+        mHud.generate(view, lists);
+    }
+
+    @Override
+    protected void onSizeChanged() {
+        mHud.setSize(mWidth, mHeight);
+        mHud.setAlpha(1.0f);
+        mBackground.setSize(mWidth, mHeight);
+        mTimeElapsedSinceTransition = 0.0f;
+        if (mView != null) {
+            mView.requestRender();
+        }
+    }
+
+    public int getState() {
+        return mState;
+    }
+
+    public void setState(int state) {
+        boolean feedUnchanged = false;
+        if (mState == state) {
+            feedUnchanged = true;
+        }
+        GridLayoutInterface layoutInterface = (GridLayoutInterface) mLayoutInterface;
+        GridLayoutInterface oldLayout = (GridLayoutInterface) mPrevLayoutInterface;
+        oldLayout.mNumRows = layoutInterface.mNumRows;
+        oldLayout.mSpacingX = layoutInterface.mSpacingX;
+        oldLayout.mSpacingY = layoutInterface.mSpacingY;
+        GridCamera camera = mCamera;
+        int numMaxRows = (camera.mHeight >= camera.mWidth) ? 4 : 3;
+        MediaFeed feed = mMediaFeed;
+        boolean performLayout = true;
+        mZoomValue = 1.0f;
+        float yStretch = camera.mDefaultAspectRatio / camera.mAspectRatio;
+        if (yStretch < 1.0f) {
+            yStretch = 1.0f;
+        }
+        switch (state) {
+        case STATE_GRID_VIEW:
+            mTimeElapsedSinceGridViewReady = 0.0f;
+            if (feed != null && feedUnchanged == false) {
+                boolean updatedData = feed.restorePreviousClusteringState();
+                if (updatedData) {
+                    performLayout = false;
+                }
+            }
+            layoutInterface.mNumRows = numMaxRows;
+            layoutInterface.mSpacingX = (int) (10 * Gallery.PIXEL_DENSITY);
+            layoutInterface.mSpacingY = (int) (10 * Gallery.PIXEL_DENSITY);
+            if (mState == STATE_MEDIA_SETS) {
+                // Entering album.
+                mInAlbum = true;
+                MediaSet set = feed.getCurrentSet();
+                int icon = mDrawables.getIconForSet(set, true);
+                if (set != null) {
+                    mHud.getPathBar().pushLabel(icon, set.mNoCountTitleString, new Runnable() {
+                        public void run() {
+                            if (mFeedAboutToChange) {
+                                return;
+                            }
+                            if (mHud.getAlpha() == 1.0f) {
+                                disableLocationFiltering();
+                                mInputProcessor.clearSelection();
+                                setState(STATE_GRID_VIEW);
+                            } else {
+                                mHud.setAlpha(1.0f);
+                            }
+                        }
+                    });
+                }
+            }
+            if (mState == STATE_FULL_SCREEN) {
+                mHud.getPathBar().popLabel();
+            }
+            break;
+        case STATE_TIMELINE:
+            mTimeElapsedSinceStackViewReady = 0.0f;
+            if (feed != null && feedUnchanged == false) {
+                feed.performClustering();
+                performLayout = false;
+            }
+            disableLocationFiltering();
+            layoutInterface.mNumRows = numMaxRows - 1;
+            layoutInterface.mSpacingX = (int) (100 * Gallery.PIXEL_DENSITY);
+            layoutInterface.mSpacingY = (int) (70 * Gallery.PIXEL_DENSITY * yStretch);
+            break;
+        case STATE_FULL_SCREEN:
+            layoutInterface.mNumRows = 1;
+            layoutInterface.mSpacingX = (int) (40 * Gallery.PIXEL_DENSITY);
+            layoutInterface.mSpacingY = (int) (40 * Gallery.PIXEL_DENSITY);
+            if (mState != STATE_FULL_SCREEN) {
+                mHud.getPathBar().pushLabel(R.drawable.ic_fs_details, "", new Runnable() {
+                    public void run() {
+                        if (mHud.getAlpha() == 1.0f) {
+                            mHud.swapFullscreenLabel();
+                        }
+                        mHud.setAlpha(1.0f);
+                    }
+                });
+            }
+            break;
+        case STATE_MEDIA_SETS:
+            mTimeElapsedSinceStackViewReady = 0.0f;
+            if (feed != null && feedUnchanged == false) {
+                feed.restorePreviousClusteringState();
+                feed.expandMediaSet(Shared.INVALID);
+                performLayout = false;
+            }
+            disableLocationFiltering();
+            mInputProcessor.clearSelection();
+            layoutInterface.mNumRows = numMaxRows - 1;
+            layoutInterface.mSpacingX = (int) (100 * Gallery.PIXEL_DENSITY);
+            layoutInterface.mSpacingY = (int) (70 * Gallery.PIXEL_DENSITY * yStretch);
+            if (mInAlbum) {
+                if (mState == STATE_FULL_SCREEN) {
+                    mHud.getPathBar().popLabel();
+                }
+                mHud.getPathBar().popLabel();
+                mInAlbum = false;
+            }
+            break;
+        }
+        mState = state;
+        mHud.onGridStateChanged();
+        if (performLayout && mFeedAboutToChange == false) {
+            onLayout(Shared.INVALID, Shared.INVALID, oldLayout);
+        }
+        if (state != STATE_FULL_SCREEN) {
+            mCamera.moveYTo(0);
+            mCamera.moveZTo(0);
+        }
+    }
+
+    protected void enableLocationFiltering(String label) {
+        if (mLocationFilter == false) {
+            mLocationFilter = true;
+            mHud.getPathBar().pushLabel(R.drawable.icon_location_small, label, new Runnable() {
+                public void run() {
+                    if (mHud.getAlpha() == 1.0f) {
+                        if (mState == STATE_FULL_SCREEN) {
+                            mInputProcessor.clearSelection();
+                            setState(STATE_GRID_VIEW);
+                        } else {
+                            disableLocationFiltering();
+                        }
+                    } else {
+                        mHud.setAlpha(1.0f);
+                    }
+                }
+            });
+        }
+    }
+
+    protected void disableLocationFiltering() {
+        if (mLocationFilter) {
+            mLocationFilter = false;
+            mMediaFeed.removeFilter();
+            mHud.getPathBar().popLabel();
+        }
+    }
+
+    boolean goBack() {
+        if (mFeedAboutToChange) {
+            return false;
+        }
+        int state = mState;
+        if (mInputProcessor.getSelectedSlot() == Shared.INVALID) {
+            if (mLocationFilter) {
+                disableLocationFiltering();
+                setState(STATE_TIMELINE);
+                return true;
+            }
+        }
+        switch (state) {
+        case STATE_GRID_VIEW:
+            setState(STATE_MEDIA_SETS);
+            break;
+        case STATE_TIMELINE:
+            setState(STATE_GRID_VIEW);
+            break;
+        case STATE_FULL_SCREEN:
+            setState(STATE_GRID_VIEW);
+            mInputProcessor.clearSelection();
+            break;
+        default:
+            return false;
+        }
+        return true;
+    }
+
+    public void endSlideshow() {
+        mSlideshowMode = false;
+        if (mWakeLock != null) {
+            if (mWakeLock.isHeld()) {
+                mWakeLock.release();
+            }
+            mWakeLock = null;
+        }
+        mHud.setAlpha(1.0f);
+    }
+
+    @Override
+    public void onSensorChanged(RenderView view, SensorEvent event) {
+        mInputProcessor.onSensorChanged(view, event, mState);
+    }
+
+    public DataSource getDataSource() {
+        if (mMediaFeed != null)
+            return mMediaFeed.getDataSource();
+        return null;
+    }
+
+    public void setDataSource(DataSource dataSource) {
+        MediaFeed feed = mMediaFeed;
+        if (feed != null) {
+            feed.shutdown();
+            mDisplayList.clear();
+            mBackground.clear();
+        }
+        mMediaFeed = new MediaFeed(mContext, dataSource, this);
+        mMediaFeed.start();
+    }
+
+    public IndexRange getVisibleRange() {
+        return mVisibleRange;
+    }
+
+    public IndexRange getBufferedVisibleRange() {
+        return mBufferedVisibleRange;
+    }
+
+    public IndexRange getCompleteRange() {
+        return mCompleteRange;
+    }
+
+    private int hitTest(Vector3f worldPos, int itemWidth, int itemHeight) {
+        int retVal = Shared.INVALID;
+        int firstSlotIndex = 0;
+        int lastSlotIndex = 0;
+        IndexRange rangeToUse = mVisibleRange;
+        synchronized (rangeToUse) {
+            firstSlotIndex = rangeToUse.begin;
+            lastSlotIndex = rangeToUse.end;
+        }
+        Pool<Vector3f> pool = mTempVec;
+        float itemWidthBy2 = itemWidth * 0.5f;
+        float itemHeightBy2 = itemHeight * 0.5f;
+        Vector3f position = pool.create();
+        Vector3f deltaAnchorPosition = pool.create();
+        try {
+            deltaAnchorPosition.set(mDeltaAnchorPosition);
+            for (int i = firstSlotIndex; i <= lastSlotIndex; ++i) {
+                GridCameraManager.getSlotPositionForSlotIndex(i, mCamera, mLayoutInterface, deltaAnchorPosition, position);
+                if (FloatUtils.boundsContainsPoint(position.x - itemWidthBy2, position.x + itemWidthBy2,
+                        position.y - itemHeightBy2, position.y + itemHeightBy2, worldPos.x, worldPos.y)) {
+                    retVal = i;
+                    break;
+                }
+            }
+        } finally {
+            pool.delete(deltaAnchorPosition);
+            pool.delete(position);
+        }
+        return retVal;
+    }
+
+    void centerCameraForSlot(int slotIndex, float baseConvergence) {
+        float imageTheta = 0.0f;
+        DisplayItem displayItem = getDisplayItemForSlotId(slotIndex);
+        if (displayItem != null) {
+            imageTheta = displayItem.getImageTheta();
+        }
+        mCameraManager.centerCameraForSlot(mLayoutInterface, slotIndex, baseConvergence, mDeltaAnchorPositionUncommited,
+                mInputProcessor.getCurrentSelectedSlot(), mZoomValue, imageTheta, mState);
+    }
+
+    boolean constrainCameraForSlot(int slotIndex) {
+        return mCameraManager.constrainCameraForSlot(mLayoutInterface, slotIndex, mDeltaAnchorPosition, mCurrentFocusItemWidth,
+                mCurrentFocusItemHeight);
+    }
+
+    // Called on render thread before rendering.
+    @Override
+    public boolean update(RenderView view, float timeElapsed) {
+        if (mFeedAboutToChange == false) {
+            mTimeElapsedSinceTransition += timeElapsed;
+            mTimeElapsedSinceGridViewReady += timeElapsed;
+            if (mTimeElapsedSinceGridViewReady >= 1.0f) {
+                mTimeElapsedSinceGridViewReady = 1.0f;
+            }
+            mTimeElapsedSinceStackViewReady += timeElapsed;
+            if (mTimeElapsedSinceStackViewReady >= 1.0f) {
+                mTimeElapsedSinceStackViewReady = 1.0f;
+            }
+        } else {
+            mTimeElapsedSinceTransition = 0;
+        }
+        if (view.elapsedLoadingExpensiveTextures() > 150 || (mMediaFeed != null && mMediaFeed.getWaitingForMediaScanner())) {
+            mHud.getPathBar().setAnimatedIcons(GridDrawables.TEXTURE_SPINNER);
+        } else {
+            mHud.getPathBar().setAnimatedIcons(null);
+        }
+
+        // In that case, we need to commit the respective Display Items when the
+        // feed was updated.
+        GridCamera camera = mCamera;
+        camera.update(timeElapsed);
+        DisplayItem anchorDisplayItem = getAnchorDisplayItem(ANCHOR_CENTER);
+        if (anchorDisplayItem != null && !mHud.getTimeBar().isDragged()) {
+            mHud.getTimeBar().setItem(anchorDisplayItem.mItemRef);
+        }
+        mDisplayList.update(timeElapsed);
+        mInputProcessor.update(timeElapsed);
+        mSelectedAlpha = FloatUtils.animate(mSelectedAlpha, mTargetAlpha, timeElapsed * 0.5f);
+        if (mState == STATE_FULL_SCREEN) {
+            mHud.autoHide(true);
+        } else {
+            mHud.autoHide(false);
+            mHud.setAlpha(1.0f);
+        }
+        GridQuad[] fullscreenQuads = mDrawables.mFullscreenGrid;
+        int numFullScreenQuads = fullscreenQuads.length;
+        for (int i = 0; i < numFullScreenQuads; ++i) {
+            fullscreenQuads[i].update(timeElapsed);
+        }
+        if (mSlideshowMode && mState == STATE_FULL_SCREEN) {
+            mTimeElapsedSinceView += timeElapsed;
+            if (mTimeElapsedSinceView > SLIDESHOW_TRANSITION_TIME) {
+                // time to go to the next slide
+                mTimeElapsedSinceView = 0.0f;
+                changeFocusToNextSlot(0.5f);
+                mCamera.commitMoveInX();
+                mCamera.commitMoveInY();
+            }
+        }
+        if (mState == STATE_MEDIA_SETS || mState == STATE_TIMELINE) {
+            mCamera.moveYTo(-0.1f);
+            mCamera.commitMoveInY();
+        }
+        boolean dirty = mDrawManager.update(timeElapsed);
+        dirty |= mSlideshowMode;
+        dirty |= mFramesDirty > 0;
+        if (mFramesDirty > 0) {
+            --mFramesDirty;
+        }
+        try {
+            if (mMediaFeed != null && (mMediaFeed.getWaitingForMediaScanner())) {
+                // We limit the drawing of the frame so that the MediaScanner thread can do its work
+                Thread.sleep(200);
+            }
+        } catch (InterruptedException e) {
+
+        }
+        if (mDisplayList.getNumAnimatables() != 0 || mCamera.isAnimating()
+                || (mTimeElapsedSinceTransition > 0.0f && mTimeElapsedSinceTransition < 1.0f) || mSelectedAlpha != mTargetAlpha
+                // || (mAnimatedFov != mTargetFov)
+                || dirty)
+            return true;
+        else
+            return false;
+    }
+
+    private void computeVisibleRange() {
+        if (mPerformingLayoutChange)
+            return;
+        if (mDeltaAnchorPosition.equals(mDeltaAnchorPositionUncommited) == false) {
+            mDeltaAnchorPosition.set(mDeltaAnchorPositionUncommited);
+        }
+        mCameraManager.computeVisibleRange(mMediaFeed, mLayoutInterface, mDeltaAnchorPosition, mVisibleRange,
+                mBufferedVisibleRange, mCompleteRange, mState);
+    }
+
+    private void computeVisibleItems() {
+        if (mFeedAboutToChange == true || mPerformingLayoutChange == true) {
+            return;
+        }
+
+        computeVisibleRange();
+        int deltaBegin = mBufferedVisibleRange.begin - mPreviousDataRange.begin;
+        int deltaEnd = mBufferedVisibleRange.end - mPreviousDataRange.end;
+        if (deltaBegin != 0 || deltaEnd != 0) {
+            // The delta has changed, we have to compute the display items again.
+            // We find the intersection range, these slots have not changed at all.
+            int firstVisibleSlotIndex = mBufferedVisibleRange.begin;
+            int lastVisibleSlotIndex = mBufferedVisibleRange.end;
+            mPreviousDataRange.begin = firstVisibleSlotIndex;
+            mPreviousDataRange.end = lastVisibleSlotIndex;
+
+            Pool<Vector3f> pool = mTempVec;
+            Vector3f position = pool.create();
+            Vector3f deltaAnchorPosition = pool.create();
+            try {
+                MediaFeed feed = mMediaFeed;
+                DisplayList displayList = mDisplayList;
+                DisplayItem[] displayItems = mDisplayItems;
+                DisplaySlot[] displaySlots = mDisplaySlots;
+                int numDisplayItems = displayItems.length;
+                int numDisplaySlots = displaySlots.length;
+                ArrayList<MediaItem> visibleItems = mVisibleItems;
+                deltaAnchorPosition.set(mDeltaAnchorPosition);
+                LayoutInterface layout = mLayoutInterface;
+                GridCamera camera = mCamera;
+                for (int i = firstVisibleSlotIndex; i <= lastVisibleSlotIndex; ++i) {
+                    GridCameraManager.getSlotPositionForSlotIndex(i, camera, layout, deltaAnchorPosition, position);
+                    MediaSet set = feed.getSetForSlot(i);
+                    int indexIntoSlots = i - firstVisibleSlotIndex;
+
+                    if (set != null && indexIntoSlots >= 0 && indexIntoSlots < numDisplaySlots) {
+                        ArrayList<MediaItem> items = set.getItems();
+                        displaySlots[indexIntoSlots].setMediaSet(set);
+                        ArrayList<MediaItem> bestItems = mTempList;
+                        if (mTimeElapsedSinceTransition < 1.0f) {
+                            ArrayUtils.computeSortedIntersection(visibleItems, items, MAX_ITEMS_PER_SLOT, bestItems, mTempHash);
+                        }
+
+                        // TODO: Could be problematic with dummy items in set.
+                        int numItemsInSet = set.getNumItems();
+                        int numBestItems = bestItems.size();
+                        int originallyFoundItems = numBestItems;
+                        if (numBestItems < MAX_ITEMS_PER_SLOT) {
+                            int itemsRemaining = MAX_ITEMS_PER_SLOT - numBestItems;
+                            for (int currItemPos = 0; currItemPos < numItemsInSet; currItemPos++) {
+                                MediaItem item = items.get(currItemPos);
+                                if (mTimeElapsedSinceTransition >= 1.0f || !bestItems.contains(item)) {
+                                    bestItems.add(item);
+                                    if (--itemsRemaining == 0) {
+                                        break;
+                                    }
+                                }
+                            }
+                        }
+                        numBestItems = bestItems.size();
+                        int baseIndex = (i - firstVisibleSlotIndex) * MAX_ITEMS_PER_SLOT;
+                        for (int j = 0; j < numBestItems; ++j) {
+                            if (baseIndex + j >= numDisplayItems) {
+                                break;
+                            }
+                            if (j >= numItemsInSet) {
+                                displayItems[baseIndex + j] = null;
+                            } else {
+                                MediaItem item = bestItems.get(j);
+                                if (item != null) {
+                                    DisplayItem displayItem = displayList.get(item);
+                                    if (mState == STATE_FULL_SCREEN
+                                            || (mState == STATE_GRID_VIEW && (mTimeElapsedSinceTransition > 1.0f || j >= originallyFoundItems))) {
+                                        displayItem.set(position, j, false);
+                                        displayItem.commit();
+                                    } else {
+                                        displayList.setPositionAndStackIndex(displayItem, position, j, true);
+                                    }
+                                    displayItems[baseIndex + j] = displayItem;
+                                }
+                            }
+                        }
+                        for (int j = numBestItems; j < MAX_ITEMS_PER_SLOT; ++j) {
+                            displayItems[baseIndex + j] = null;
+                        }
+                        bestItems.clear();
+                    }
+                }
+                if (mFeedChanged) {
+                    mFeedChanged = false;
+                    if (mInputProcessor != null && mState == STATE_FULL_SCREEN) {
+                        int currentSelectedSlot = mInputProcessor.getCurrentSelectedSlot();
+                        if (currentSelectedSlot > mCompleteRange.end)
+                            currentSelectedSlot = mCompleteRange.end;
+                        mInputProcessor.setCurrentSelectedSlot(currentSelectedSlot);
+                    }
+                    if (mState == STATE_GRID_VIEW) {
+                        MediaSet expandedSet = mMediaFeed.getExpandedMediaSet();
+                        if (expandedSet != null) {
+                            if (!mHud.getPathBar().getCurrentLabel().equals(expandedSet.mNoCountTitleString)) {
+                                mHud.getPathBar().changeLabel(expandedSet.mNoCountTitleString);
+                            }
+                        }
+                    }
+                    if (mRequestFocusContentUri != null) {
+                        // We have to find the item that has this contentUri
+                        if (mState == STATE_FULL_SCREEN) {
+                            int numSlots = mCompleteRange.end;
+                            for (int i = 0; i < numSlots; ++i) {
+                                MediaSet set = feed.getSetForSlot(i);
+                                ArrayList<MediaItem> items = set.getItems();
+                                int numItems = items.size();
+                                for (int j = 0; j < numItems; ++j) {
+                                    if (items.get(j).mContentUri.equals(mRequestFocusContentUri)) {
+                                        mInputProcessor.setCurrentSelectedSlot(i);
+                                        break;
+                                    }
+                                }
+                            }
+                        }
+                        mRequestFocusContentUri = null;
+                    }
+                }
+            } finally {
+                pool.delete(position);
+                pool.delete(deltaAnchorPosition);
+            }
+            // We keep upto 400 thumbnails in memory.
+            int numThumbnailsToKeepInMemory = (mState == STATE_MEDIA_SETS || mState == STATE_TIMELINE) ? 100 : 400;
+            int startMemoryRange = (mBufferedVisibleRange.begin / numThumbnailsToKeepInMemory) * numThumbnailsToKeepInMemory;
+            if (mStartMemoryRange != startMemoryRange) {
+                mStartMemoryRange = startMemoryRange;
+                clearUnusedThumbnails();
+            }
+        }
+    }
+
+    @Override
+    public void handleLowMemory() {
+        clearUnusedThumbnails();
+        mDrawables.mStringTextureTable.clear();
+        mBackground.clearCache();
+    }
+
+    // This method can be potentially expensive
+    public void clearUnusedThumbnails() {
+        mDisplayList.clearExcept(mDisplayItems);
+    }
+
+    @Override
+    public void onSurfaceCreated(RenderView view, GL11 gl) {
+        mDisplayList.clear();
+        mHud.clear();
+        mHud.reset();
+        mDrawables.mStringTextureTable.clear();
+        mDrawables.onSurfaceCreated(view, gl);
+    }
+
+    @Override
+    public void onSurfaceChanged(RenderView view, int width, int height) {
+        mCamera.viewportChanged(width, height, mCamera.mItemWidth, mCamera.mItemHeight);
+        view.setFov(mCamera.mFov);
+        setState(mState);
+    }
+
+    // Renders the node in a given pass.
+    public void renderOpaque(RenderView view, GL11 gl) {
+        GridCamera camera = mCamera;
+        int selectedSlotIndex = mInputProcessor.getCurrentSelectedSlot();
+        computeVisibleItems();
+
+        gl.glMatrixMode(GL11.GL_MODELVIEW);
+        gl.glLoadIdentity();
+        GLU.gluLookAt(gl, -camera.mEyeX, -camera.mEyeY, -camera.mEyeZ, -camera.mLookAtX, -camera.mLookAtY, -camera.mLookAtZ,
+                camera.mUpX, camera.mUpY, camera.mUpZ);
+        view.setAlpha(1.0f);
+        if (mSelectedAlpha != 1.0f) {
+            gl.glEnable(GL11.GL_BLEND);
+            gl.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
+            view.setAlpha(mSelectedAlpha);
+        }
+        if (selectedSlotIndex != Shared.INVALID) {
+            mTargetAlpha = 0.0f;
+        } else {
+            mTargetAlpha = 1.0f;
+        }
+        mDrawManager.prepareDraw(mBufferedVisibleRange, mVisibleRange, selectedSlotIndex, mInputProcessor.getCurrentFocusSlot(),
+                mInputProcessor.isFocusItemPressed());
+        if (mSelectedAlpha != 0.0f) {
+            mDrawManager.drawThumbnails(view, gl, mState);
+        }
+        gl.glDisable(GL11.GL_BLEND);
+        // We draw the selected slotIndex.
+        if (selectedSlotIndex != Shared.INVALID) {
+            mDrawManager.drawFocusItems(view, gl, mZoomValue, mSlideshowMode, mTimeElapsedSinceView);
+            mCurrentFocusItemWidth = mDrawManager.getFocusQuadWidth();
+            mCurrentFocusItemHeight = mDrawManager.getFocusQuadHeight();
+        }
+        view.setAlpha(mSelectedAlpha);
+    }
+
+    public void renderBlended(RenderView view, GL11 gl) {
+        // We draw the placeholder for all visible slots.
+        if (mHud != null && mDrawManager != null) {
+            mDrawManager.drawBlendedComponents(view, gl, mSelectedAlpha, mState, mHud.getMode(), mTimeElapsedSinceStackViewReady,
+                    mTimeElapsedSinceGridViewReady, mBucketList, mMediaFeed.getWaitingForMediaScanner() || mFeedAboutToChange
+                            || mMediaFeed.isLoading());
+        }
+    }
+
+    public synchronized void onLayout(int newAnchorSlotIndex, int currentAnchorSlotIndex, LayoutInterface oldLayout) {
+        if (mPerformingLayoutChange || !mDeltaAnchorPosition.equals(mDeltaAnchorPositionUncommited)) {
+            return;
+        }
+
+        mTimeElapsedSinceTransition = 0.0f;
+        mPerformingLayoutChange = true;
+        LayoutInterface layout = mLayoutInterface;
+        if (oldLayout == null) {
+            oldLayout = mPrevLayoutInterface;
+        }
+        GridCamera camera = mCamera;
+        if (currentAnchorSlotIndex == Shared.INVALID) {
+            currentAnchorSlotIndex = getAnchorSlotIndex(ANCHOR_CENTER);
+            if (mCurrentExpandedSlot != Shared.INVALID) {
+                currentAnchorSlotIndex = mCurrentExpandedSlot;
+            }
+            int selectedSlotIndex = mInputProcessor.getCurrentSelectedSlot();
+            if (selectedSlotIndex != Shared.INVALID) {
+                currentAnchorSlotIndex = selectedSlotIndex;
+            }
+        }
+        if (newAnchorSlotIndex == Shared.INVALID) {
+            newAnchorSlotIndex = currentAnchorSlotIndex;
+        }
+        int itemHeight = camera.mItemHeight;
+        int itemWidth = camera.mItemWidth;
+        Pool<Vector3f> pool = mTempVec;
+        Vector3f deltaAnchorPosition = pool.create();
+        Vector3f currentSlotPosition = pool.create();
+        try {
+            deltaAnchorPosition.set(0, 0, 0);
+            if (currentAnchorSlotIndex != Shared.INVALID && newAnchorSlotIndex != Shared.INVALID) {
+                layout.getPositionForSlotIndex(newAnchorSlotIndex, itemWidth, itemHeight, deltaAnchorPosition);
+                oldLayout.getPositionForSlotIndex(currentAnchorSlotIndex, itemWidth, itemHeight, currentSlotPosition);
+                currentSlotPosition.subtract(mDeltaAnchorPosition);
+                deltaAnchorPosition.subtract(currentSlotPosition);
+                deltaAnchorPosition.y = 0;
+                deltaAnchorPosition.z = 0;
+            }
+            mDeltaAnchorPositionUncommited.set(deltaAnchorPosition);
+        } finally {
+            pool.delete(deltaAnchorPosition);
+            pool.delete(currentSlotPosition);
+        }
+        centerCameraForSlot(newAnchorSlotIndex, 1.0f);
+        mCurrentExpandedSlot = Shared.INVALID;
+        
+        // Force recompute of visible items and their positions.
+        ((GridLayoutInterface) oldLayout).mNumRows = ((GridLayoutInterface) layout).mNumRows;
+        ((GridLayoutInterface) oldLayout).mSpacingX = ((GridLayoutInterface) layout).mSpacingX;
+        ((GridLayoutInterface) oldLayout).mSpacingY = ((GridLayoutInterface) layout).mSpacingY;
+        forceRecomputeVisibleRange();
+        mPerformingLayoutChange = false;
+    }
+
+    private void forceRecomputeVisibleRange() {
+        mPreviousDataRange.begin = Shared.INVALID;
+        mPreviousDataRange.end = Shared.INVALID;
+        if (mView != null) {
+            mView.requestRender();
+        }
+    }
+
+    // called on background thread
+    public synchronized void onFeedChanged(MediaFeed feed, boolean needsLayout) {
+        if (!needsLayout) {
+            mFeedChanged = true;
+            forceRecomputeVisibleRange();
+            if (mState == STATE_GRID_VIEW || mState == STATE_FULL_SCREEN)
+                mHud.setFeed(feed, mState, needsLayout);
+            return;
+        }
+
+        while (mPerformingLayoutChange == true) {
+            Thread.yield();
+        }
+        if (mState == STATE_GRID_VIEW) {
+            if (mHud != null) {
+                MediaSet set = feed.getCurrentSet();
+                if (set != null && !mLocationFilter)
+                    mHud.getPathBar().changeLabel(set.mNoCountTitleString);
+            }
+        }
+        DisplayItem[] displayItems = mDisplayItems;
+        int firstBufferedVisibleSlotIndex = mBufferedVisibleRange.begin;
+        int lastBufferedVisibleSlotIndex = mBufferedVisibleRange.end;
+        int currentlyVisibleSlotIndex = getAnchorSlotIndex(ANCHOR_CENTER);
+        if (mCurrentExpandedSlot != Shared.INVALID) {
+            currentlyVisibleSlotIndex = mCurrentExpandedSlot;
+        }
+        MediaItem anchorItem = null;
+        ArrayList<MediaItem> visibleItems = mVisibleItems;
+        visibleItems.clear();
+        visibleItems.ensureCapacity(lastBufferedVisibleSlotIndex - firstBufferedVisibleSlotIndex);
+        if (currentlyVisibleSlotIndex != Shared.INVALID && currentlyVisibleSlotIndex >= firstBufferedVisibleSlotIndex
+                && currentlyVisibleSlotIndex <= lastBufferedVisibleSlotIndex) {
+            int baseIndex = (currentlyVisibleSlotIndex - firstBufferedVisibleSlotIndex) * MAX_ITEMS_PER_SLOT;
+            for (int i = 0; i < MAX_ITEMS_PER_SLOT; ++i) {
+                DisplayItem displayItem = displayItems[baseIndex + i];
+                if (displayItem != null) {
+                    if (anchorItem == null) {
+                        anchorItem = displayItem.mItemRef;
+                    }
+                    visibleItems.add(displayItem.mItemRef);
+                }
+            }
+        }
+        // We want to add items from the middle.
+        int numItems = lastBufferedVisibleSlotIndex - firstBufferedVisibleSlotIndex + 1;
+        int midPoint = (lastBufferedVisibleSlotIndex - firstBufferedVisibleSlotIndex) / 2;
+        int length = displayItems.length;
+        for (int i = 0; i < numItems; ++i) {
+            int index = midPoint + Shared.midPointIterator(i);
+            int indexIntoDisplayItem = (index - firstBufferedVisibleSlotIndex) * MAX_ITEMS_PER_SLOT;
+            if (indexIntoDisplayItem >= 0 && indexIntoDisplayItem < length) {
+                for (int j = 0; j < MAX_ITEMS_PER_SLOT; ++j) {
+                    DisplayItem displayItem = displayItems[indexIntoDisplayItem + j];
+                    if (displayItem != null) {
+                        MediaItem item = displayItem.mItemRef;
+                        if (item != anchorItem) {
+                            visibleItems.add(item);
+                        }
+                    }
+                }
+            }
+        }
+        int newSlotIndex = Shared.INVALID;
+        if (anchorItem != null) {
+            // We try to find the anchor item in the new feed.
+            int numSlots = feed.getNumSlots();
+            for (int i = 0; i < numSlots; ++i) {
+                MediaSet set = feed.getSetForSlot(i);
+                if (set != null && set.containsItem(anchorItem)) {
+                    newSlotIndex = i;
+                    break;
+                }
+            }
+        }
+
+        if (anchorItem != null && newSlotIndex == Shared.INVALID) {
+            int numSlots = feed.getNumSlots();
+            MediaSet parentSet = anchorItem.mParentMediaSet;
+            for (int i = 0; i < numSlots; ++i) {
+                MediaSet set = feed.getSetForSlot(i);
+                if (set != null && set.mId == parentSet.mId) {
+                    newSlotIndex = i;
+                    break;
+                }
+            }
+        }
+        
+        // We must create a new display store now since the data has changed.
+        if (newSlotIndex != Shared.INVALID) {
+            if (mState == STATE_MEDIA_SETS) {
+                mDisplayList.clearExcept(displayItems);
+            }
+            onLayout(newSlotIndex, currentlyVisibleSlotIndex, null);
+        } else {
+            forceRecomputeVisibleRange();
+        }
+        mCurrentExpandedSlot = Shared.INVALID;
+        mFeedAboutToChange = false;
+        mFeedChanged = true;
+        if (feed != null) {
+            mHud.setFeed(feed, mState, needsLayout);
+        }
+        if (mView != null) {
+            mView.requestRender();
+        }
+    }
+
+    public DisplayItem getAnchorDisplayItem(int type) {
+        int slotIndex = getAnchorSlotIndex(type);
+        return mDisplayItems[(slotIndex - mBufferedVisibleRange.begin) * MAX_ITEMS_PER_SLOT];
+    }
+
+    public float getScrollPosition() {
+        return (mCamera.mLookAtX * mCamera.mScale + mDeltaAnchorPosition.x); // in
+        // pixels
+    }
+
+    public DisplayItem getDisplayItemForScrollPosition(float posX) {
+        Pool<Vector3f> pool = mTempVecAlt;
+        MediaFeed feed = mMediaFeed;
+        int itemWidth = mCamera.mItemWidth;
+        int itemHeight = mCamera.mItemHeight;
+        GridLayoutInterface gridInterface = (GridLayoutInterface) mLayoutInterface;
+        float absolutePosX = posX;
+        int left = (int) ((absolutePosX / itemWidth) * gridInterface.mNumRows);
+        int right = feed == null ? 0 : (int) (feed.getNumSlots());
+        int retSlot = left;
+        Vector3f position = pool.create();
+        try {
+            for (int i = left; i < right; ++i) {
+                gridInterface.getPositionForSlotIndex(i, itemWidth, itemHeight, position);
+                retSlot = i;
+                if (position.x >= absolutePosX) {
+                    break;
+                }
+            }
+        } finally {
+            pool.delete(position);
+        }
+        if (mFeedAboutToChange) {
+            return null;
+        }
+        right = feed == null ? 0 : feed.getNumSlots();
+        if (right == 0) {
+            return null;
+        }
+
+        if (retSlot >= right)
+            retSlot = right - 1;
+        MediaSet set = feed.getSetForSlot(retSlot);
+        if (set != null) {
+            ArrayList<MediaItem> items = set.getItems();
+            if (items != null && set.getNumItems() > 0) {
+                return (mDisplayList.get(items.get(0)));
+            }
+        }
+        return null;
+    }
+
+    // Returns the top left-most item.
+    public int getAnchorSlotIndex(int anchorType) {
+        int retVal = 0;
+        switch (anchorType) {
+        case ANCHOR_LEFT:
+            retVal = mVisibleRange.begin;
+            break;
+        case ANCHOR_RIGHT:
+            retVal = mVisibleRange.end;
+            break;
+        case ANCHOR_CENTER:
+            retVal = (mVisibleRange.begin + mVisibleRange.end) / 2;
+            break;
+        }
+        return retVal;
+    }
+
+    DisplayItem getDisplayItemForSlotId(int slotId) {
+        int index = slotId - mBufferedVisibleRange.begin;
+        if (index >= 0 && slotId <= mBufferedVisibleRange.end) {
+            return mDisplayItems[index * MAX_ITEMS_PER_SLOT];
+        }
+        return null;
+    }
+
+    boolean changeFocusToNextSlot(float convergence) {
+        int currentSelectedSlot = mInputProcessor.getCurrentSelectedSlot();
+        boolean retVal = changeFocusToSlot(currentSelectedSlot + 1, convergence);
+        if (mInputProcessor.getCurrentSelectedSlot() == currentSelectedSlot) {
+            endSlideshow();
+            mHud.setAlpha(1.0f);
+        }
+        return retVal;
+    }
+
+    boolean changeFocusToSlot(int slotId, float convergence) {
+        mZoomValue = 1.0f;
+        int index = slotId - mBufferedVisibleRange.begin;
+        if (index >= 0 && slotId <= mBufferedVisibleRange.end) {
+            DisplayItem displayItem = mDisplayItems[index * MAX_ITEMS_PER_SLOT];
+            if (displayItem != null) {
+                MediaItem item = displayItem.mItemRef;
+                mHud.fullscreenSelectionChanged(item, slotId + 1, mCompleteRange.end + 1);
+                if (slotId != Shared.INVALID && slotId <= mCompleteRange.end) {
+                    mInputProcessor.setCurrentFocusSlot(slotId);
+                    centerCameraForSlot(slotId, convergence);
+                    return true;
+                } else {
+                    centerCameraForSlot(mInputProcessor.getCurrentSelectedSlot(), convergence);
+                    return false;
+                }
+            }
+        }
+        return false;
+    }
+
+    boolean changeFocusToPreviousSlot(float convergence) {
+        return changeFocusToSlot(mInputProcessor.getCurrentSelectedSlot() - 1, convergence);
+    }
+
+    public ArrayList<MediaBucket> getSelectedBuckets() {
+        return mBucketList.get();
+    }
+
+    public void selectAll() {
+        if (mState != STATE_FULL_SCREEN) {
+            int numSlots = mCompleteRange.end + 1;
+            for (int i = 0; i < numSlots; ++i) {
+                addSlotToSelectedItems(i, false, false);
+            }
+            updateCountOfSelectedItems();
+        } else {
+            addSlotToSelectedItems(mInputProcessor.getCurrentFocusSlot(), false, true);
+        }
+    }
+
+    public void deselectOrCancelSelectMode() {
+        if (mBucketList.size() == 0) {
+            mHud.cancelSelection();
+        } else {
+            mBucketList.clear();
+            updateCountOfSelectedItems();
+        }
+    }
+
+    public void deselectAll() {
+        mHud.cancelSelection();
+        mBucketList.clear();
+        updateCountOfSelectedItems();
+    }
+
+    public void deleteSelection() {
+        // Delete the selection and exit selection mode.
+        mMediaFeed.performOperation(MediaFeed.OPERATION_DELETE, getSelectedBuckets(), null);
+        deselectAll();
+
+        // If the current set is now empty, return to the parent set.
+        if (mCompleteRange.isEmpty()) {
+            goBack(); // TODO(venkat): This does not work most of the time, can you take a look?
+        }
+    }
+
+    void addSlotToSelectedItems(int slotId, boolean removeIfAlreadyAdded, boolean updateCount) {
+        if (mFeedAboutToChange == false) {
+            MediaFeed feed = mMediaFeed;
+            mBucketList.add(slotId, feed, removeIfAlreadyAdded);
+            if (updateCount) {
+                updateCountOfSelectedItems();
+                if (mBucketList.size() == 0)
+                    deselectAll();
+            }
+        }
+    }
+
+    private void updateCountOfSelectedItems() {
+        mHud.updateNumItemsSelected(mBucketList.size());
+    }
+
+    public int getMetadataSlotIndexForScreenPosition(int posX, int posY) {
+        return getSlotForScreenPosition(posX, posY, mCamera.mItemWidth + (int) (100 * Gallery.PIXEL_DENSITY), mCamera.mItemHeight
+                + (int) (100 * Gallery.PIXEL_DENSITY));
+    }
+
+    public int getSlotIndexForScreenPosition(int posX, int posY) {
+        return getSlotForScreenPosition(posX, posY, mCamera.mItemWidth, mCamera.mItemHeight);
+    }
+
+    private int getSlotForScreenPosition(int posX, int posY, int itemWidth, int itemHeight) {
+        Pool<Vector3f> pool = mTempVec;
+        int retVal = 0;
+        Vector3f worldPos = pool.create();
+        try {
+            GridCamera camera = mCamera;
+            camera.convertToCameraSpace(posX, posY, 0, worldPos);
+            // slots are expressed in pixels as well
+            worldPos.x *= camera.mScale;
+            worldPos.y *= camera.mScale;
+            // we ignore z
+            retVal = hitTest(worldPos, itemWidth, itemHeight);
+        } finally {
+            pool.delete(worldPos);
+        }
+        return retVal;
+    }
+
+    public boolean tapGesture(int slotIndex, boolean metadata) {
+        MediaFeed feed = mMediaFeed;
+        if (!feed.isClustered()) {
+            // It is not clustering.
+            if (!feed.hasExpandedMediaSet()) {
+                if (feed.canExpandSet(slotIndex)) {
+                    mCurrentExpandedSlot = slotIndex;
+                    feed.expandMediaSet(slotIndex);
+                    setState(STATE_GRID_VIEW);
+                }
+                return false;
+            } else {
+                return true;
+            }
+        } else {
+            // Select a cluster, and recompute a new cluster within this cluster.
+            mCurrentExpandedSlot = slotIndex;
+            goBack();
+            if (metadata) {
+                DisplaySlot slot = mDisplaySlots[slotIndex - mBufferedVisibleRange.begin];
+                if (slot.hasValidLocation()) {
+                    MediaSet set = slot.getMediaSet();
+                    if (set.mReverseGeocodedLocation != null) {
+                        enableLocationFiltering(set.mReverseGeocodedLocation);
+                    }
+                    feed.setFilter(new LocationMediaFilter(set.mMinLatLatitude, set.mMinLonLongitude, set.mMaxLatLatitude, set.mMaxLonLongitude));
+                }
+            }
+            return false;
+        }
+    }
+
+    public void onTimeChanged(TimeBar timebar) {
+        if (mFeedAboutToChange) {
+            return;
+        }
+        // TODO lot of optimization possible here
+        MediaItem item = timebar.getItem();
+        MediaFeed feed = mMediaFeed;
+        int numSlots = feed.getNumSlots();
+        for (int i = 0; i < numSlots; ++i) {
+            MediaSet set = feed.getSetForSlot(i);
+            if (set == null) {
+                return;
+            }
+            ArrayList<MediaItem> items = set.getItems();
+            if (items == null || set.getNumItems() == 0) {
+                return;
+            }
+            if (items.contains(item)) {
+                centerCameraForSlot(i, 1.0f);
+                break;
+            }
+        }
+    }
+
+    public void onFeedAboutToChange(MediaFeed feed) {
+        mFeedAboutToChange = true;
+        mTimeElapsedSinceTransition = 0;
+    }
+
+    public void startSlideshow() {
+        endSlideshow();
+        mSlideshowMode = true;
+        mZoomValue = 1.0f;
+        centerCameraForSlot(mInputProcessor.getCurrentSelectedSlot(), 1.0f);
+        mTimeElapsedSinceView = SLIDESHOW_TRANSITION_TIME - 1.0f;
+        mHud.setAlpha(0);
+        PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "GridView.Slideshow");
+        mWakeLock.acquire();
+    }
+
+    public void enterSelectionMode() {
+        mSlideshowMode = false;
+        mHud.enterSelectionMode();
+        int currentSlot = mInputProcessor.getCurrentSelectedSlot();
+        if (currentSlot == Shared.INVALID) {
+            currentSlot = mInputProcessor.getCurrentFocusSlot();
+        }
+        addSlotToSelectedItems(currentSlot, false, true);
+    }
+
+    private float getFillScreenZoomValue() {
+        return GridCameraManager.getFillScreenZoomValue(mCamera, mTempVec, mCurrentFocusItemWidth, mCurrentFocusItemHeight);
+    }
+
+    public void zoomInToSelectedItem() {
+        mSlideshowMode = false;
+        float potentialZoomValue = getFillScreenZoomValue();
+        if (mZoomValue < potentialZoomValue) {
+            mZoomValue = potentialZoomValue;
+        } else {
+            mZoomValue *= 3.0f;
+        }
+        if (mZoomValue > 6.0f) {
+            mZoomValue = 6.0f;
+        }
+        mHud.setAlpha(1.0f);
+        centerCameraForSlot(mInputProcessor.getCurrentSelectedSlot(), 1.0f);
+    }
+
+    public void zoomOutFromSelectedItem() {
+        mSlideshowMode = false;
+        if (mZoomValue == getFillScreenZoomValue()) {
+            mZoomValue = 1.0f;
+        } else {
+            mZoomValue /= 3.0f;
+        }
+        if (mZoomValue < 1.0f) {
+            mZoomValue = 1.0f;
+        }
+        mHud.setAlpha(1.0f);
+        centerCameraForSlot(mInputProcessor.getCurrentSelectedSlot(), 1.0f);
+    }
+
+    public void rotateSelectedItems(float f) {
+        MediaBucketList bucketList = mBucketList;
+        ArrayList<MediaBucket> mediaBuckets = bucketList.get();
+        DisplayList displayList = mDisplayList;
+        int numBuckets = mediaBuckets.size();
+        for (int i = 0; i < numBuckets; ++i) {
+            MediaBucket bucket = mediaBuckets.get(i);
+            ArrayList<MediaItem> mediaItems = bucket.mediaItems;
+            if (mediaItems != null) {
+                int numMediaItems = mediaItems.size();
+                for (int j = 0; j < numMediaItems; ++j) {
+                    MediaItem item = mediaItems.get(j);
+                    DisplayItem displayItem = displayList.get(item);
+                    displayItem.rotateImageBy(f);
+                    displayList.addToAnimatables(displayItem);
+                }
+            }
+        }
+        if (mState == STATE_FULL_SCREEN) {
+            centerCameraForSlot(mInputProcessor.getCurrentSelectedSlot(), 1.0f);
+        }
+        mMediaFeed.performOperation(MediaFeed.OPERATION_ROTATE, mediaBuckets, new Float(f));
+        // we recreate these displayitems from the cache
+    }
+
+    public void cropSelectedItem() {
+
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        return mInputProcessor.onTouchEvent(event);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (mInputProcessor != null)
+            return mInputProcessor.onKeyDown(keyCode, event, mState);
+        return false;
+    }
+
+    public boolean inSlideShowMode() {
+        return mSlideshowMode;
+    }
+
+    public boolean noDeleteMode() {
+        return mNoDeleteMode || (mMediaFeed != null && mMediaFeed.isSingleImageMode());
+    }
+
+    public float getZoomValue() {
+        return mZoomValue;
+    }
+
+    public boolean feedAboutToChange() {
+        return mFeedAboutToChange;
+    }
+
+    public boolean isInAlbumMode() {
+        return mInAlbum;
+    }
+
+    public Vector3f getDeltaAnchorPosition() {
+        return mDeltaAnchorPosition;
+    }
+
+    public int getExpandedSlot() {
+        return mCurrentExpandedSlot;
+    }
+
+    public GridLayoutInterface getLayoutInterface() {
+        return (GridLayoutInterface) mLayoutInterface;
+    }
+
+    public void setZoomValue(float f) {
+        mZoomValue = f;
+        centerCameraForSlot(mInputProcessor.getCurrentSelectedSlot(), 1.0f);
+    }
+
+    public void setPickIntent(boolean b) {
+        mPickIntent = b;
+        mHud.getPathBar().popLabel();
+        mHud.getPathBar().pushLabel(R.drawable.icon_location_small, mContext.getResources().getString(R.string.pick),
+                new Runnable() {
+                    public void run() {
+                        if (mHud.getAlpha() == 1.0f) {
+                            if (!mFeedAboutToChange) {
+                                setState(STATE_MEDIA_SETS);
+                            }
+                        } else {
+                            mHud.setAlpha(1.0f);
+                        }
+                    }
+                });
+    }
+
+    public boolean getPickIntent() {
+        return mPickIntent;
+    }
+
+    public void setViewIntent(boolean b, final String setName) {
+        mViewIntent = b;
+        if (b) {
+            mMediaFeed.expandMediaSet(0);
+            setState(STATE_GRID_VIEW);
+            // We need to make sure we haven't pushed the same label twice
+            if (mHud.getPathBar().getNumLevels() == 1) {
+                mHud.getPathBar().pushLabel(R.drawable.icon_folder_small, setName, new Runnable() {
+                    public void run() {
+                        if (mFeedAboutToChange) {
+                            return;
+                        }
+                        if (mHud.getAlpha() == 1.0f) {
+                        disableLocationFiltering();
+                        mInputProcessor.clearSelection();
+                        setState(STATE_GRID_VIEW);
+                        } else {
+                            mHud.setAlpha(1.0f);
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    public boolean getViewIntent() {
+        return mViewIntent;
+    }
+
+    public void setSingleImage(boolean noDeleteMode) {
+        mNoDeleteMode = noDeleteMode;
+        mInputProcessor.setCurrentSelectedSlot(0);
+    }
+
+    public MediaFeed getFeed() {
+        return mMediaFeed;
+    }
+
+    public void markDirty(int numFrames) {
+        mFramesDirty = numFrames;
+    }
+
+    public void focusItem(String contentUri) {
+        mRequestFocusContentUri = contentUri;
+        mMediaFeed.updateListener(false);
+    }
+
+}
diff --git a/src/com/cooliris/media/GridLayoutInterface.java b/src/com/cooliris/media/GridLayoutInterface.java
new file mode 100644
index 0000000..6e8affe
--- /dev/null
+++ b/src/com/cooliris/media/GridLayoutInterface.java
@@ -0,0 +1,21 @@
+package com.cooliris.media;
+
+public final class GridLayoutInterface extends LayoutInterface {
+    GridLayoutInterface(int numRows) {
+        mNumRows = numRows;
+        mSpacingX = (int)(20 * Gallery.PIXEL_DENSITY);
+        mSpacingY = (int)(40 * Gallery.PIXEL_DENSITY);
+    }
+
+    public void getPositionForSlotIndex(int slotIndex, int itemWidth, int itemHeight, Vector3f outPosition) {
+        outPosition.x = (slotIndex / mNumRows) * (itemWidth + mSpacingX);
+        outPosition.y = (slotIndex % mNumRows) * (itemHeight + mSpacingY);
+        int maxY = (mNumRows - 1) * (itemHeight + mSpacingY);
+        outPosition.y -= (maxY >> 1);
+        outPosition.z = 0;
+    }
+
+    public int mNumRows;
+    public int mSpacingX;
+    public int mSpacingY;
+}
diff --git a/src/com/cooliris/media/GridQuad.java b/src/com/cooliris/media/GridQuad.java
new file mode 100644
index 0000000..326e48e
--- /dev/null
+++ b/src/com/cooliris/media/GridQuad.java
@@ -0,0 +1,394 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 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.
+ */
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.CharBuffer;
+import java.nio.FloatBuffer;
+
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+
+/**
+ * A 2D rectangular mesh. Can be drawn textured or untextured. This version is modified from the original Grid.java (found in the
+ * SpriteText package in the APIDemos Android sample) to support hardware vertex buffers.
+ */
+
+final class GridQuad {
+    private FloatBuffer mVertexBuffer;
+    private FloatBuffer mOverlayTexCoordBuffer;
+    private CharBuffer mIndexBuffer;
+
+    private int mW;
+    private int mH;
+    private static final int INDEX_COUNT = 4;
+    private static final int ORIENTATION_COUNT = 360;
+    private int mVertBufferIndex;
+    private int mIndexBufferIndex;
+    private int mOverlayTextureCoordBufferIndex;
+    private boolean mDynamicVBO;
+    private float mU;
+    private float mV;
+    private float mAnimU;
+    private float mAnimV;
+    private float mWidth;
+    private float mHeight;
+    private float mAnimWidth;
+    private float mAnimHeight;
+    private boolean mQuadChanged;
+    private float mDefaultAspectRatio;
+    private int mBaseTextureCoordBufferIndex;
+    private FloatBuffer mBaseTexCoordBuffer;
+    private final boolean mOrientedQuad;
+    private MatrixStack mMatrix;
+    private float[] mCoordsIn = new float[4];
+    private float[] mCoordsOut = new float[4];
+
+    public static GridQuad createGridQuad(float width, float height, float xOffset, float yOffset, float uExtents, float vExtents,
+            boolean generateOrientedQuads) {
+        //generateOrientedQuads = false;
+        GridQuad grid = new GridQuad(generateOrientedQuads);
+        grid.mWidth = width;
+        grid.mHeight = height;
+        grid.mAnimWidth = width;
+        grid.mAnimHeight = height;
+        grid.mDefaultAspectRatio = width / height;
+        float widthBy2 = width * 0.5f;
+        float heightBy2 = height * 0.5f;
+        final float v = vExtents;
+        final float u = uExtents;
+        if (!generateOrientedQuads) {
+            grid.set(0, 0, -widthBy2 + xOffset, -heightBy2 + yOffset, 0.0f, u, v);
+            grid.set(1, 0, widthBy2 + xOffset, -heightBy2 + yOffset, 0.0f, 0.0f, v);
+            grid.set(0, 1, -widthBy2 + xOffset, heightBy2 + yOffset, 0.0f, u, 0.0f);
+            grid.set(1, 1, widthBy2 + xOffset, heightBy2 + yOffset, 0.0f, 0.0f, 0.0f);
+        } else {
+            for (int i = 0; i < ORIENTATION_COUNT; ++i) {
+                grid.set(0, 0, -widthBy2 + xOffset, -heightBy2 + yOffset, 0.0f, u, v, true, i);
+                grid.set(1, 0, widthBy2 + xOffset, -heightBy2 + yOffset, 0.0f, 0.0f, v, true, i);
+                grid.set(0, 1, -widthBy2 + xOffset, heightBy2 + yOffset, 0.0f, u, 0.0f, true, i);
+                grid.set(1, 1, widthBy2 + xOffset, heightBy2 + yOffset, 0.0f, 0.0f, 0.0f, true, i);
+            }
+        }
+        grid.mU = uExtents;
+        grid.mV = uExtents;
+        return grid;
+    }
+
+    public GridQuad(boolean generateOrientedQuads) {
+        mOrientedQuad = generateOrientedQuads;
+        if (mOrientedQuad) {
+            mMatrix = new MatrixStack();
+            mMatrix.glLoadIdentity();
+        }
+        int vertsAcross = 2;
+        int vertsDown = 2;
+        mW = vertsAcross;
+        mH = vertsDown;
+        int size = vertsAcross * vertsDown;
+        final int FLOAT_SIZE = 4;
+        final int CHAR_SIZE = 2;
+        final int orientationCount = (!generateOrientedQuads) ? 1 : ORIENTATION_COUNT;
+        mVertexBuffer = ByteBuffer.allocateDirect(FLOAT_SIZE * size * 3 * orientationCount).order(ByteOrder.nativeOrder())
+                .asFloatBuffer();
+        mOverlayTexCoordBuffer = ByteBuffer.allocateDirect(FLOAT_SIZE * size * 2 * orientationCount).order(ByteOrder.nativeOrder())
+                .asFloatBuffer();
+        mBaseTexCoordBuffer = ByteBuffer.allocateDirect(FLOAT_SIZE * size * 2 * orientationCount).order(ByteOrder.nativeOrder())
+                .asFloatBuffer();
+
+        int indexCount = INDEX_COUNT; // using tristrips
+        mIndexBuffer = ByteBuffer.allocateDirect(CHAR_SIZE * indexCount * orientationCount).order(ByteOrder.nativeOrder())
+                .asCharBuffer();
+
+        /*
+         * Initialize triangle list mesh.
+         * 
+         * [0]-----[ 1] ... | / | | / | | / | [w]-----[w+1] ... | |
+         */
+        CharBuffer buffer = mIndexBuffer;
+        for (int i = 0; i < INDEX_COUNT * orientationCount; ++i) {
+            buffer.put(i, (char) i);
+        }
+        mVertBufferIndex = 0;
+    }
+
+    public void setDynamic(boolean dynamic) {
+        mDynamicVBO = dynamic;
+        if (mOrientedQuad) {
+            throw new UnsupportedOperationException("Dynamic Quads can't have orientations");
+        }
+    }
+
+    public float getWidth() {
+        return mWidth;
+    }
+
+    public float getHeight() {
+        return mHeight;
+    }
+
+    public void update(float timeElapsed) {
+        mAnimWidth = FloatUtils.animate(mAnimWidth, mWidth, timeElapsed);
+        mAnimHeight = FloatUtils.animate(mAnimHeight, mHeight, timeElapsed);
+        mAnimU = FloatUtils.animate(mAnimU, mU, timeElapsed);
+        mAnimV = FloatUtils.animate(mAnimV, mV, timeElapsed);
+        recomputeQuad();
+    }
+
+    public void commit() {
+        mAnimWidth = mWidth;
+        mAnimHeight = mHeight;
+        mAnimU = mU;
+        mAnimV = mV;
+    }
+
+    public void recomputeQuad() {
+        mVertexBuffer.clear();
+        mBaseTexCoordBuffer.clear();
+        float widthBy2 = mAnimWidth * 0.5f;
+        float heightBy2 = mAnimHeight * 0.5f;
+        float xOffset = 0.0f;
+        float yOffset = 0.0f;
+        float u = mU;
+        float v = mV;
+        set(0, 0, -widthBy2 + xOffset, -heightBy2 + yOffset, 0.0f, u, v, false, 0);
+        set(1, 0, widthBy2 + xOffset, -heightBy2 + yOffset, 0.0f, 0.0f, v, false, 0);
+        set(0, 1, -widthBy2 + xOffset, heightBy2 + yOffset, 0.0f, u, 0.0f, false, 0);
+        set(1, 1, widthBy2 + xOffset, heightBy2 + yOffset, 0.0f, 0.0f, 0.0f, false, 0);
+        mQuadChanged = true;
+    }
+
+    public void resizeQuad(float viewAspect, float u, float v, float imageWidth, float imageHeight) {
+        // given the u,v; we know the aspect ratio of the image
+        // we have to change one of the co-ords depending upon the image and viewport aspect ratio
+        mU = u;
+        mV = v;
+        float imageAspect = imageWidth / imageHeight;
+        float width = mDefaultAspectRatio;
+        float height = 1.0f;
+        if (viewAspect < 1.0f) {
+            height = height * (mDefaultAspectRatio / imageAspect);
+            float maxHeight = width / viewAspect;
+            if (height > maxHeight) {
+                // we need to reduce the width and height proportionately
+                float ratio = height / maxHeight;
+                height /= ratio;
+                width /= ratio;
+            }
+        } else {
+            width = width * (imageAspect / mDefaultAspectRatio);
+            float maxWidth = height * viewAspect;
+            if (width > maxWidth) {
+                float ratio = width / maxWidth;
+                width /= ratio;
+                height /= ratio;
+            }
+        }
+        mWidth = width;
+        mHeight = height;
+        commit();
+        recomputeQuad();
+    }
+
+    public void set(int i, int j, float x, float y, float z, float u, float v) {
+        set(i, j, x, y, z, u, v, true, 0);
+    }
+
+    private void set(int i, int j, float x, float y, float z, float u, float v, boolean modifyOverlay, int orientationId) {
+        if (i < 0 || i >= mW) {
+            throw new IllegalArgumentException("i");
+        }
+        if (j < 0 || j >= mH) {
+            throw new IllegalArgumentException("j");
+        }
+        int index = orientationId * INDEX_COUNT + mW * j + i;
+        int posIndex = index * 3;
+        mVertexBuffer.put(posIndex, x);
+        mVertexBuffer.put(posIndex + 1, y);
+        mVertexBuffer.put(posIndex + 2, z);
+        int baseTexIndex = index * 2;
+        // we can calculate the u,v for the orientation here
+        MatrixStack matrix = mMatrix;
+        if (matrix != null) {
+            orientationId *= 2;
+            matrix.glLoadIdentity();
+            matrix.glTranslatef(0.5f, 0.5f, 0.0f);
+            float itheta = (float) Math.toRadians(orientationId);
+            float sini = (float) Math.sin(itheta);
+            float scale = 1.0f + (sini * sini) * 0.33333333f;
+            scale = 1.0f / scale;
+            matrix.glRotatef(-orientationId, 0.0f, 0.0f, 1.0f);
+            matrix.glScalef(scale, scale, 1.0f);
+            matrix.glTranslatef(-0.5f + (float) (sini * 0.125f / scale), -0.5f
+                    + (float) (Math.abs(Math.sin(itheta * 0.5f) * 0.25f)), 0.0f);
+            // now we have the desired matrix
+            // populate s,t,r,q
+            // http://glprogramming.com/red/chapter09.html
+            mCoordsIn[0] = u;
+            mCoordsIn[1] = v;
+            mCoordsIn[2] = 0.0f;
+            mCoordsIn[3] = 1.0f;
+            matrix.apply(mCoordsIn, mCoordsOut);
+            u = mCoordsOut[0] / mCoordsOut[3];
+            v = mCoordsOut[1] / mCoordsOut[3];
+        }
+        mBaseTexCoordBuffer.put(baseTexIndex, u);
+        mBaseTexCoordBuffer.put(baseTexIndex + 1, v);
+        if (modifyOverlay) {
+            int texIndex = index * 2;
+            mOverlayTexCoordBuffer.put(texIndex, u);
+            mOverlayTexCoordBuffer.put(texIndex + 1, v);
+        }
+    }
+
+    public void bindArrays(GL10 gl) {
+        GL11 gl11 = (GL11) gl;
+        // draw using hardware buffers
+        gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mVertBufferIndex);
+        gl11.glVertexPointer(3, GL11.GL_FLOAT, 0, 0);
+
+        gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mOverlayTextureCoordBufferIndex);
+        if (mDynamicVBO && mQuadChanged) {
+            final int texCoordSize = mOverlayTexCoordBuffer.capacity() * 4;
+            mOverlayTexCoordBuffer.position(0);
+            gl11.glBufferData(GL11.GL_ARRAY_BUFFER, texCoordSize, mOverlayTexCoordBuffer, GL11.GL_DYNAMIC_DRAW);
+        }
+        gl11.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+        gl11.glClientActiveTexture(GL11.GL_TEXTURE1);
+        if (mDynamicVBO && mQuadChanged) {
+            final int texCoordSize = mBaseTexCoordBuffer.capacity() * 4;
+            mBaseTexCoordBuffer.position(0);
+            gl11.glBufferData(GL11.GL_ARRAY_BUFFER, texCoordSize, mBaseTexCoordBuffer, GL11.GL_DYNAMIC_DRAW);
+        }
+        gl11.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+        gl11.glClientActiveTexture(GL11.GL_TEXTURE0);
+        if (mDynamicVBO && mQuadChanged) {
+            mQuadChanged = false;
+            gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mVertBufferIndex);
+            final int vertexSize = mVertexBuffer.capacity() * 4;
+            mVertexBuffer.position(0);
+            gl11.glBufferData(GL11.GL_ARRAY_BUFFER, vertexSize, mVertexBuffer, GL11.GL_DYNAMIC_DRAW);
+        }
+        gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, mIndexBufferIndex);
+    }
+
+    public static final void draw(GL11 gl11, float orientationDegrees) {
+        // don't call this method unless bindArrays was called
+        int orientation = (int) Shared.normalizePositive(orientationDegrees);
+        gl11.glDrawElements(GL11.GL_TRIANGLE_STRIP, INDEX_COUNT, GL11.GL_UNSIGNED_SHORT, orientation * INDEX_COUNT);
+    }
+
+    public void unbindArrays(GL10 gl) {
+        GL11 gl11 = (GL11) gl;
+        gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0);
+        gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0);
+    }
+
+    public boolean usingHardwareBuffers() {
+        return mVertBufferIndex != 0;
+    }
+
+    /**
+     * When the OpenGL ES device is lost, GL handles become invalidated. In that case, we just want to "forget" the old handles
+     * (without explicitly deleting them) and make new ones.
+     */
+    public void forgetHardwareBuffers() {
+        mVertBufferIndex = 0;
+        mIndexBufferIndex = 0;
+        mOverlayTextureCoordBufferIndex = 0;
+    }
+
+    /**
+     * Deletes the hardware buffers allocated by this object (if any).
+     */
+    public void freeHardwareBuffers(GL10 gl) {
+        if (mVertBufferIndex != 0) {
+            if (gl instanceof GL11) {
+                GL11 gl11 = (GL11) gl;
+                int[] buffer = new int[1];
+                buffer[0] = mVertBufferIndex;
+                gl11.glDeleteBuffers(1, buffer, 0);
+
+                buffer[0] = mOverlayTextureCoordBufferIndex;
+                gl11.glDeleteBuffers(1, buffer, 0);
+
+                buffer[0] = mIndexBufferIndex;
+                gl11.glDeleteBuffers(1, buffer, 0);
+            }
+
+            forgetHardwareBuffers();
+        }
+    }
+
+    /**
+     * Allocates hardware buffers on the graphics card and fills them with data if a buffer has not already been previously
+     * allocated. Note that this function uses the GL_OES_vertex_buffer_object extension, which is not guaranteed to be supported on
+     * every device.
+     * 
+     * @param gl
+     *            A pointer to the OpenGL ES context.
+     */
+
+    public void generateHardwareBuffers(GL10 gl) {
+        if (mVertBufferIndex == 0) {
+            if (gl instanceof GL11) {
+                GL11 gl11 = (GL11) gl;
+                int[] buffer = new int[1];
+
+                // Allocate and fill the vertex buffer.
+                gl11.glGenBuffers(1, buffer, 0);
+                mVertBufferIndex = buffer[0];
+                gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mVertBufferIndex);
+                final int vertexSize = mVertexBuffer.capacity() * 4;
+                int bufferType = (mDynamicVBO) ? GL11.GL_DYNAMIC_DRAW : GL11.GL_STATIC_DRAW;
+                mVertexBuffer.position(0);
+                gl11.glBufferData(GL11.GL_ARRAY_BUFFER, vertexSize, mVertexBuffer, bufferType);
+
+                // Allocate and fill the texture coordinate buffer.
+                gl11.glGenBuffers(1, buffer, 0);
+                mOverlayTextureCoordBufferIndex = buffer[0];
+                gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mOverlayTextureCoordBufferIndex);
+                final int texCoordSize = mOverlayTexCoordBuffer.capacity() * 4;
+                mOverlayTexCoordBuffer.position(0);
+                gl11.glBufferData(GL11.GL_ARRAY_BUFFER, texCoordSize, mOverlayTexCoordBuffer, bufferType);
+
+                gl11.glGenBuffers(1, buffer, 0);
+                mBaseTextureCoordBufferIndex = buffer[0];
+                gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBaseTextureCoordBufferIndex);
+                mBaseTexCoordBuffer.position(0);
+                gl11.glBufferData(GL11.GL_ARRAY_BUFFER, texCoordSize, mBaseTexCoordBuffer, bufferType);
+
+                // Unbind the array buffer.
+                gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0);
+
+                // Allocate and fill the index buffer.
+                gl11.glGenBuffers(1, buffer, 0);
+                mIndexBufferIndex = buffer[0];
+                gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, mIndexBufferIndex);
+                // A char is 2 bytes.
+                final int indexSize = mIndexBuffer.capacity() * 2;
+                mIndexBuffer.position(0);
+                gl11.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER, indexSize, mIndexBuffer, GL11.GL_STATIC_DRAW);
+
+                // Unbind the element array buffer.
+                gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0);
+            }
+        }
+    }
+
+}
diff --git a/src/com/cooliris/media/GridQuadFrame.java b/src/com/cooliris/media/GridQuadFrame.java
new file mode 100644
index 0000000..1f566bc
--- /dev/null
+++ b/src/com/cooliris/media/GridQuadFrame.java
@@ -0,0 +1,264 @@
+package com.cooliris.media;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.CharBuffer;
+import java.nio.FloatBuffer;
+
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+
+final class GridQuadFrame {
+    private FloatBuffer mVertexBuffer;
+    private FloatBuffer mTexCoordBuffer;
+    private FloatBuffer mSecTexCoordBuffer;
+    private CharBuffer mIndexBuffer;
+
+    private int mW;
+    private int mH;
+    
+    // This has 8 quads, 16 vertices, 22 triangles (for tristrip).
+    // We have more triangles because we have to make degenerate triangles to use tri-strips
+    public static final int INDEX_COUNT = 25;
+    private int mVertBufferIndex;
+    private int mIndexBufferIndex;
+    private int mTextureCoordBufferIndex;
+    private int mSecTextureCoordBufferIndex;
+
+    public static GridQuadFrame createFrame(float width, float height, int itemWidth, int itemHeight) {
+        GridQuadFrame frame = new GridQuadFrame();
+        final float textureSize = 64.0f;
+        final float numPixelsYOriginShift = 7;
+        final float inset = 6;
+        final float ratio = 1.0f / (float) itemHeight;
+        final float frameXThickness = 0.5f * textureSize * ratio;
+        final float frameYThickness = 0.5f * textureSize * ratio;
+        final float frameX = width * 0.5f + frameXThickness * 0.5f - inset * ratio;
+        float frameY = height * 0.5f + frameYThickness * 0.5f + (inset - 1) * ratio;
+        final float originX = 0.0f;
+        final float originY = numPixelsYOriginShift * ratio;
+
+        frame.set(0, 0, -frameX + originX, -frameY + originY, 0, 1.0f, 1.0f);
+        frame.set(1, 0, -frameX + originX + frameXThickness, -frameY + originY, 0, 0.5f, 1.0f);
+        frame.set(2, 0, frameX - frameXThickness + originX, -frameY + originY, 0, 0.5f, 1.0f);
+        frame.set(3, 0, frameX + originX, -frameY + originY, 0, 0.0f, 1.0f);
+
+        frameY -= frameYThickness;
+
+        frame.set(0, 1, -frameX + originX, -frameY + originY, 0, 1.0f, 0.5f);
+        frame.set(1, 1, -frameX + frameXThickness + originX, -frameY + originY, 0, 0.5f, 0.5f);
+        frame.set(2, 1, frameX - frameXThickness + originX, -frameY + originY, 0, 0.5f, 0.5f);
+        frame.set(3, 1, frameX + originX, -frameY + originY, 0, 0.0f, 0.5f);
+
+        frameY = height * 0.5f - frameYThickness;
+
+        frame.set(0, 2, -frameX + originX, frameY + originY, 0, 1.0f, 0.5f);
+        frame.set(1, 2, -frameX + frameXThickness + originX, frameY + originY, 0, 0.5f, 0.5f);
+        frame.set(2, 2, frameX - frameXThickness + originX, frameY + originY, 0, 0.5f, 0.5f);
+        frame.set(3, 2, frameX + originX, frameY + originY, 0, 0.0f, 0.5f);
+
+        frameY += frameYThickness;
+
+        frame.set(0, 3, -frameX + originX, frameY + originY, 0, 1.0f, 0.0f);
+        frame.set(1, 3, -frameX + frameXThickness + originX, frameY + originY, 0, 0.5f, 0.0f);
+        frame.set(2, 3, frameX - frameXThickness + originX, frameY + originY, 0, 0.5f, 0.0f);
+        frame.set(3, 3, frameX + originX, frameY + originY, 0, 0.0f, 0.0f);
+
+        return frame;
+    }
+
+    public GridQuadFrame() {
+        int vertsAcross = 4;
+        int vertsDown = 4;
+        mW = vertsAcross;
+        mH = vertsDown;
+        int size = vertsAcross * vertsDown;
+        final int FLOAT_SIZE = 4;
+        final int CHAR_SIZE = 2;
+        mVertexBuffer = ByteBuffer.allocateDirect(FLOAT_SIZE * size * 3).order(ByteOrder.nativeOrder()).asFloatBuffer();
+        mTexCoordBuffer = ByteBuffer.allocateDirect(FLOAT_SIZE * size * 2).order(ByteOrder.nativeOrder()).asFloatBuffer();
+        mSecTexCoordBuffer = ByteBuffer.allocateDirect(FLOAT_SIZE * size * 2).order(ByteOrder.nativeOrder()).asFloatBuffer();
+
+        int indexCount = INDEX_COUNT; // using tristrips
+        mIndexBuffer = ByteBuffer.allocateDirect(CHAR_SIZE * indexCount).order(ByteOrder.nativeOrder()).asCharBuffer();
+
+        /*
+         * Initialize triangle list mesh.
+         * 
+         * [0]---[1]---------[2]---[3] ... | / | / | / | [4]---[5]---------[6]---[7] | / | | /| | / | | / | | / | | / |
+         * [8]---[9]---------[10]---[11] | / | \ | \ | [12]--[13]--------[14]---[15]
+         */
+        CharBuffer buffer = mIndexBuffer;
+        buffer.put(0, (char) 0);
+        buffer.put(1, (char) 4);
+        buffer.put(2, (char) 1);
+        buffer.put(3, (char) 5);
+        buffer.put(4, (char) 2);
+        buffer.put(5, (char) 6);
+        buffer.put(6, (char) 3);
+        buffer.put(7, (char) 7);
+        buffer.put(8, (char) 11);
+        buffer.put(9, (char) 6);
+        buffer.put(10, (char) 10);
+        buffer.put(11, (char) 14);
+        buffer.put(12, (char) 11);
+        buffer.put(13, (char) 15);
+        buffer.put(14, (char) 15);
+        buffer.put(15, (char) 14);
+        buffer.put(16, (char) 14);
+        buffer.put(17, (char) 10);
+        buffer.put(18, (char) 13);
+        buffer.put(19, (char) 9);
+        buffer.put(20, (char) 12);
+        buffer.put(21, (char) 8);
+        buffer.put(22, (char) 4);
+        buffer.put(23, (char) 9);
+        buffer.put(24, (char) 5);
+        mVertBufferIndex = 0;
+    }
+
+    void set(int i, int j, float x, float y, float z, float u, float v) {
+        if (i < 0 || i >= mW) {
+            throw new IllegalArgumentException("i");
+        }
+        if (j < 0 || j >= mH) {
+            throw new IllegalArgumentException("j");
+        }
+
+        int index = mW * j + i;
+
+        int posIndex = index * 3;
+        mVertexBuffer.put(posIndex, x);
+        mVertexBuffer.put(posIndex + 1, y);
+        mVertexBuffer.put(posIndex + 2, z);
+
+        int texIndex = index * 2;
+        mTexCoordBuffer.put(texIndex, u);
+        mTexCoordBuffer.put(texIndex + 1, v);
+        
+        int secTexIndex = index * 2;
+        mSecTexCoordBuffer.put(secTexIndex, u);
+        mSecTexCoordBuffer.put(secTexIndex + 1, v);
+    }
+
+    public void bindArrays(GL10 gl) {
+        GL11 gl11 = (GL11) gl;
+        // draw using hardware buffers
+        gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mVertBufferIndex);
+        gl11.glVertexPointer(3, GL11.GL_FLOAT, 0, 0);
+
+        gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mTextureCoordBufferIndex);
+        gl11.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+        gl11.glClientActiveTexture(GL11.GL_TEXTURE1);
+        gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mSecTextureCoordBufferIndex);
+        gl11.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+        gl11.glClientActiveTexture(GL11.GL_TEXTURE0);
+        gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, mIndexBufferIndex);
+    }
+
+    public static final void draw(GL11 gl11) {
+        // Don't call this method unless bindArrays was called.
+        gl11.glDrawElements(GL11.GL_TRIANGLE_STRIP, INDEX_COUNT, GL11.GL_UNSIGNED_SHORT, 0);
+    }
+
+    public void unbindArrays(GL10 gl) {
+        GL11 gl11 = (GL11) gl;
+        gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0);
+        gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0);
+    }
+
+    public boolean usingHardwareBuffers() {
+        return mVertBufferIndex != 0;
+    }
+
+    /**
+     * When the OpenGL ES device is lost, GL handles become invalidated. In that case, we just want to "forget" the old handles
+     * (without explicitly deleting them) and make new ones.
+     */
+    public void forgetHardwareBuffers() {
+        mVertBufferIndex = 0;
+        mIndexBufferIndex = 0;
+        mTextureCoordBufferIndex = 0;
+        mSecTextureCoordBufferIndex = 0;
+    }
+
+    /**
+     * Deletes the hardware buffers allocated by this object (if any).
+     */
+    public void freeHardwareBuffers(GL10 gl) {
+        if (mVertBufferIndex != 0) {
+            if (gl instanceof GL11) {
+                GL11 gl11 = (GL11) gl;
+                int[] buffer = new int[1];
+                buffer[0] = mVertBufferIndex;
+                gl11.glDeleteBuffers(1, buffer, 0);
+
+                buffer[0] = mTextureCoordBufferIndex;
+                gl11.glDeleteBuffers(1, buffer, 0);
+                
+                buffer[0] = mSecTextureCoordBufferIndex;
+                gl11.glDeleteBuffers(1, buffer, 0);
+
+                buffer[0] = mIndexBufferIndex;
+                gl11.glDeleteBuffers(1, buffer, 0);
+            }
+            forgetHardwareBuffers();
+        }
+    }
+
+    /**
+     * Allocates hardware buffers on the graphics card and fills them with data if a buffer has not already been previously
+     * allocated. Note that this function uses the GL_OES_vertex_buffer_object extension, which is not guaranteed to be supported on
+     * every device.
+     * 
+     * @param gl
+     *            A pointer to the OpenGL ES context.
+     */
+    public void generateHardwareBuffers(GL10 gl) {
+        if (mVertBufferIndex == 0) {
+            if (gl instanceof GL11) {
+                GL11 gl11 = (GL11) gl;
+                int[] buffer = new int[1];
+
+                // Allocate and fill the vertex buffer.
+                gl11.glGenBuffers(1, buffer, 0);
+                mVertBufferIndex = buffer[0];
+                gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mVertBufferIndex);
+                mVertexBuffer.position(0);
+                final int vertexSize = mVertexBuffer.capacity() * 4;
+                gl11.glBufferData(GL11.GL_ARRAY_BUFFER, vertexSize, mVertexBuffer, GL11.GL_STATIC_DRAW);
+
+                // Allocate and fill the texture coordinate buffer.
+                gl11.glGenBuffers(1, buffer, 0);
+                mTextureCoordBufferIndex = buffer[0];
+                gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mTextureCoordBufferIndex);
+                final int texCoordSize = mTexCoordBuffer.capacity() * 4;
+                mTexCoordBuffer.position(0);
+                gl11.glBufferData(GL11.GL_ARRAY_BUFFER, texCoordSize, mTexCoordBuffer, GL11.GL_STATIC_DRAW);
+                
+                // Allocate and fill the secondary texture coordinate buffer.
+                gl11.glGenBuffers(1, buffer, 0);
+                mSecTextureCoordBufferIndex = buffer[0];
+                gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mSecTextureCoordBufferIndex);
+                final int secTexCoordSize = mSecTexCoordBuffer.capacity() * 4;
+                mSecTexCoordBuffer.position(0);
+                gl11.glBufferData(GL11.GL_ARRAY_BUFFER, secTexCoordSize, mSecTexCoordBuffer, GL11.GL_STATIC_DRAW);
+
+                // Unbind the array buffer.
+                gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0);
+
+                // Allocate and fill the index buffer.
+                gl11.glGenBuffers(1, buffer, 0);
+                mIndexBufferIndex = buffer[0];
+                gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, mIndexBufferIndex);
+                // A char is 2 bytes.
+                final int indexSize = mIndexBuffer.capacity() * 2;
+                mIndexBuffer.position(0);
+                gl11.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER, indexSize, mIndexBuffer, GL11.GL_STATIC_DRAW);
+
+                // Unbind the element array buffer.
+                gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0);
+            }
+        }
+    }
+}
diff --git a/src/com/cooliris/media/GridQuadMesh.java b/src/com/cooliris/media/GridQuadMesh.java
new file mode 100644
index 0000000..c4790a6
--- /dev/null
+++ b/src/com/cooliris/media/GridQuadMesh.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.cooliris.media;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.CharBuffer;
+import java.nio.FloatBuffer;
+
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+
+/**
+ * A 2D rectangular mesh. Can be drawn textured or untextured. This version is modified from the original Grid.java (found in the
+ * SpriteText package in the APIDemos Android sample) to support hardware vertex buffers.
+ */
+final class GridQuadMesh {
+    private FloatBuffer mVertexBuffer;
+    private FloatBuffer mTexCoordBuffer;
+    private CharBuffer mIndexBuffer;
+
+    private int mW;
+    private int mH;
+    private int mIndexCount;
+    private int mVertBufferIndex;
+    private int mIndexBufferIndex;
+    private int mTextureCoordBufferIndex;
+
+    public GridQuadMesh(int vertsAcross, int vertsDown) {
+        if (vertsAcross < 0 || vertsAcross >= 65536) {
+            throw new IllegalArgumentException("vertsAcross");
+        }
+        if (vertsDown < 0 || vertsDown >= 65536) {
+            throw new IllegalArgumentException("vertsDown");
+        }
+        if (vertsAcross * vertsDown >= 65536) {
+            throw new IllegalArgumentException("vertsAcross * vertsDown >= 65536");
+        }
+
+        mW = vertsAcross;
+        mH = vertsDown;
+        int size = vertsAcross * vertsDown;
+        final int FLOAT_SIZE = 4;
+        final int CHAR_SIZE = 2;
+        mVertexBuffer = ByteBuffer.allocateDirect(FLOAT_SIZE * size * 3).order(ByteOrder.nativeOrder()).asFloatBuffer();
+        mTexCoordBuffer = ByteBuffer.allocateDirect(FLOAT_SIZE * size * 2).order(ByteOrder.nativeOrder()).asFloatBuffer();
+
+        int quadW = mW - 1;
+        int quadH = mH - 1;
+        int quadCount = quadW * quadH;
+        int indexCount = quadCount * 6;
+        mIndexCount = indexCount;
+        mIndexBuffer = ByteBuffer.allocateDirect(CHAR_SIZE * indexCount).order(ByteOrder.nativeOrder()).asCharBuffer();
+
+        /*
+         * Initialize triangle list mesh.
+         * 
+         * [0]-----[ 1] ... | / | | / | | / | [w]-----[w+1] ... | |
+         */
+
+        {
+            int i = 0;
+            for (int y = 0; y < quadH; y++) {
+                for (int x = 0; x < quadW; x++) {
+                    char a = (char) (y * mW + x);
+                    char b = (char) (y * mW + x + 1);
+                    char c = (char) ((y + 1) * mW + x);
+                    char d = (char) ((y + 1) * mW + x + 1);
+
+                    mIndexBuffer.put(i++, a);
+                    mIndexBuffer.put(i++, b);
+                    mIndexBuffer.put(i++, c);
+
+                    mIndexBuffer.put(i++, b);
+                    mIndexBuffer.put(i++, c);
+                    mIndexBuffer.put(i++, d);
+                }
+            }
+        }
+
+        mVertBufferIndex = 0;
+    }
+
+    void set(int i, int j, float x, float y, float z, float u, float v) {
+        if (i < 0 || i >= mW) {
+            throw new IllegalArgumentException("i");
+        }
+        if (j < 0 || j >= mH) {
+            throw new IllegalArgumentException("j");
+        }
+
+        int index = mW * j + i;
+
+        int posIndex = index * 3;
+        mVertexBuffer.put(posIndex, x);
+        mVertexBuffer.put(posIndex + 1, y);
+        mVertexBuffer.put(posIndex + 2, z);
+
+        int texIndex = index * 2;
+        mTexCoordBuffer.put(texIndex, u);
+        mTexCoordBuffer.put(texIndex + 1, v);
+    }
+
+    public void draw(GL10 gl, boolean useTexture) {
+        if (mVertBufferIndex == 0) {
+            gl.glVertexPointer(3, GL11.GL_FLOAT, 0, mVertexBuffer);
+
+            if (useTexture) {
+                gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, mTexCoordBuffer);
+            }
+
+            gl.glDrawElements(GL11.GL_TRIANGLES, mIndexCount, GL11.GL_UNSIGNED_SHORT, mIndexBuffer);
+        } else {
+            GL11 gl11 = (GL11) gl;
+            // draw using hardware buffers
+            gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mVertBufferIndex);
+            gl11.glVertexPointer(3, GL11.GL_FLOAT, 0, 0);
+
+            gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mTextureCoordBufferIndex);
+            gl11.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+
+            gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, mIndexBufferIndex);
+            gl11.glDrawElements(GL11.GL_TRIANGLES, mIndexCount, GL11.GL_UNSIGNED_SHORT, 0);
+
+            gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0);
+            gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0);
+
+        }
+    }
+
+    public boolean usingHardwareBuffers() {
+        return mVertBufferIndex != 0;
+    }
+
+    /**
+     * When the OpenGL ES device is lost, GL handles become invalidated. In that case, we just want to "forget" the old handles
+     * (without explicitly deleting them) and make new ones.
+     */
+    public void forgetHardwareBuffers() {
+        mVertBufferIndex = 0;
+        mIndexBufferIndex = 0;
+        mTextureCoordBufferIndex = 0;
+    }
+
+    /**
+     * Deletes the hardware buffers allocated by this object (if any).
+     */
+    public void freeHardwareBuffers(GL10 gl) {
+        if (mVertBufferIndex != 0) {
+            if (gl instanceof GL11) {
+                GL11 gl11 = (GL11) gl;
+                int[] buffer = new int[1];
+                buffer[0] = mVertBufferIndex;
+                gl11.glDeleteBuffers(1, buffer, 0);
+
+                buffer[0] = mTextureCoordBufferIndex;
+                gl11.glDeleteBuffers(1, buffer, 0);
+
+                buffer[0] = mIndexBufferIndex;
+                gl11.glDeleteBuffers(1, buffer, 0);
+            }
+
+            forgetHardwareBuffers();
+        }
+    }
+
+    /**
+     * Allocates hardware buffers on the graphics card and fills them with data if a buffer has not already been previously
+     * allocated. Note that this function uses the GL_OES_vertex_buffer_object extension, which is not guaranteed to be supported on
+     * every device.
+     * 
+     * @param gl
+     *            A pointer to the OpenGL ES context.
+     */
+    public void generateHardwareBuffers(GL10 gl) {
+        if (mVertBufferIndex == 0) {
+            if (gl instanceof GL11) {
+                GL11 gl11 = (GL11) gl;
+                int[] buffer = new int[1];
+
+                // Allocate and fill the vertex buffer.
+                gl11.glGenBuffers(1, buffer, 0);
+                mVertBufferIndex = buffer[0];
+                gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mVertBufferIndex);
+                final int vertexSize = mVertexBuffer.capacity() * 4;
+                mVertexBuffer.position(0);
+                gl11.glBufferData(GL11.GL_ARRAY_BUFFER, vertexSize, mVertexBuffer, GL11.GL_STATIC_DRAW);
+
+                // Allocate and fill the texture coordinate buffer.
+                gl11.glGenBuffers(1, buffer, 0);
+                mTextureCoordBufferIndex = buffer[0];
+                gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, mTextureCoordBufferIndex);
+                final int texCoordSize = mTexCoordBuffer.capacity() * 4;
+                mTexCoordBuffer.position(0);
+                gl11.glBufferData(GL11.GL_ARRAY_BUFFER, texCoordSize, mTexCoordBuffer, GL11.GL_STATIC_DRAW);
+
+                // Unbind the array buffer.
+                gl11.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0);
+
+                // Allocate and fill the index buffer.
+                gl11.glGenBuffers(1, buffer, 0);
+                mIndexBufferIndex = buffer[0];
+                gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, mIndexBufferIndex);
+                // A char is 2 bytes.
+                final int indexSize = mIndexBuffer.capacity() * 2;
+                mIndexBuffer.position(0);
+                gl11.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER, indexSize, mIndexBuffer, GL11.GL_STATIC_DRAW);
+
+                // Unbind the element array buffer.
+                gl11.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0);
+            }
+        }
+    }
+
+}
diff --git a/src/com/cooliris/media/HighlightView.java b/src/com/cooliris/media/HighlightView.java
new file mode 100644
index 0000000..d65060c
--- /dev/null
+++ b/src/com/cooliris/media/HighlightView.java
@@ -0,0 +1,412 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 2007 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.
+ */
+
+
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+// This class is used by CropImage to display a highlighted cropping rectangle
+// overlayed with the image. There are two coordinate spaces in use. One is
+// image, another is screen. computeLayout() uses mMatrix to map from image
+// space to screen space.
+class HighlightView {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "HighlightView";
+    View mContext;  // The View displaying the image.
+
+    public static final int GROW_NONE        = (1 << 0);
+    public static final int GROW_LEFT_EDGE   = (1 << 1);
+    public static final int GROW_RIGHT_EDGE  = (1 << 2);
+    public static final int GROW_TOP_EDGE    = (1 << 3);
+    public static final int GROW_BOTTOM_EDGE = (1 << 4);
+    public static final int MOVE             = (1 << 5);
+
+    public HighlightView(View ctx) {
+        mContext = ctx;
+    }
+
+    private void init() {
+        android.content.res.Resources resources = mContext.getResources();
+        mResizeDrawableWidth =
+                resources.getDrawable(R.drawable.camera_crop_width);
+        mResizeDrawableHeight =
+                resources.getDrawable(R.drawable.camera_crop_height);
+        mResizeDrawableDiagonal =
+                resources.getDrawable(R.drawable.indicator_autocrop);
+    }
+
+    boolean mIsFocused;
+    boolean mHidden;
+
+    public boolean hasFocus() {
+        return mIsFocused;
+    }
+
+    public void setFocus(boolean f) {
+        mIsFocused = f;
+    }
+
+    public void setHidden(boolean hidden) {
+        mHidden = hidden;
+    }
+
+    protected void draw(Canvas canvas) {
+        if (mHidden) {
+            return;
+        }
+        canvas.save();
+        Path path = new Path();
+        if (!hasFocus()) {
+            mOutlinePaint.setColor(0xFF000000);
+            canvas.drawRect(mDrawRect, mOutlinePaint);
+        } else {
+            Rect viewDrawingRect = new Rect();
+            mContext.getDrawingRect(viewDrawingRect);
+            if (mCircle) {
+                float width  = mDrawRect.width();
+                float height = mDrawRect.height();
+                path.addCircle(mDrawRect.left + (width  / 2),
+                               mDrawRect.top + (height / 2),
+                               width / 2,
+                               Path.Direction.CW);
+                mOutlinePaint.setColor(0xFFEF04D6);
+            } else {
+                path.addRect(new RectF(mDrawRect), Path.Direction.CW);
+                mOutlinePaint.setColor(0xFFFF8A00);
+            }
+            canvas.clipPath(path, Region.Op.DIFFERENCE);
+            canvas.drawRect(viewDrawingRect,
+                    hasFocus() ? mFocusPaint : mNoFocusPaint);
+
+            canvas.restore();
+            canvas.drawPath(path, mOutlinePaint);
+
+            if (mMode == ModifyMode.Grow) {
+                if (mCircle) {
+                    int width  = mResizeDrawableDiagonal.getIntrinsicWidth();
+                    int height = mResizeDrawableDiagonal.getIntrinsicHeight();
+
+                    int d  = (int) Math.round(Math.cos(/*45deg*/Math.PI / 4D)
+                            * (mDrawRect.width() / 2D));
+                    int x  = mDrawRect.left
+                            + (mDrawRect.width() / 2) + d - width / 2;
+                    int y  = mDrawRect.top
+                            + (mDrawRect.height() / 2) - d - height / 2;
+                    mResizeDrawableDiagonal.setBounds(x, y,
+                            x + mResizeDrawableDiagonal.getIntrinsicWidth(),
+                            y + mResizeDrawableDiagonal.getIntrinsicHeight());
+                    mResizeDrawableDiagonal.draw(canvas);
+                } else {
+                    int left    = mDrawRect.left   + 1;
+                    int right   = mDrawRect.right  + 1;
+                    int top     = mDrawRect.top    + 4;
+                    int bottom  = mDrawRect.bottom + 3;
+
+                    int widthWidth   =
+                            mResizeDrawableWidth.getIntrinsicWidth() / 2;
+                    int widthHeight  =
+                            mResizeDrawableWidth.getIntrinsicHeight() / 2;
+                    int heightHeight =
+                            mResizeDrawableHeight.getIntrinsicHeight() / 2;
+                    int heightWidth  =
+                            mResizeDrawableHeight.getIntrinsicWidth() / 2;
+
+                    int xMiddle = mDrawRect.left
+                            + ((mDrawRect.right  - mDrawRect.left) / 2);
+                    int yMiddle = mDrawRect.top
+                            + ((mDrawRect.bottom - mDrawRect.top) / 2);
+
+                    mResizeDrawableWidth.setBounds(left - widthWidth,
+                                                   yMiddle - widthHeight,
+                                                   left + widthWidth,
+                                                   yMiddle + widthHeight);
+                    mResizeDrawableWidth.draw(canvas);
+
+                    mResizeDrawableWidth.setBounds(right - widthWidth,
+                                                   yMiddle - widthHeight,
+                                                   right + widthWidth,
+                                                   yMiddle + widthHeight);
+                    mResizeDrawableWidth.draw(canvas);
+
+                    mResizeDrawableHeight.setBounds(xMiddle - heightWidth,
+                                                    top - heightHeight,
+                                                    xMiddle + heightWidth,
+                                                    top + heightHeight);
+                    mResizeDrawableHeight.draw(canvas);
+
+                    mResizeDrawableHeight.setBounds(xMiddle - heightWidth,
+                                                    bottom - heightHeight,
+                                                    xMiddle + heightWidth,
+                                                    bottom + heightHeight);
+                    mResizeDrawableHeight.draw(canvas);
+                }
+            }
+        }
+    }
+
+    public void setMode(ModifyMode mode) {
+        if (mode != mMode) {
+            mMode = mode;
+            mContext.invalidate();
+        }
+    }
+
+    // Determines which edges are hit by touching at (x, y).
+    public int getHit(float x, float y) {
+        Rect r = computeLayout();
+        final float hysteresis = 20F;
+        int retval = GROW_NONE;
+
+        if (mCircle) {
+            float distX = x - r.centerX();
+            float distY = y - r.centerY();
+            int distanceFromCenter =
+                    (int) Math.sqrt(distX * distX + distY * distY);
+            int radius  = mDrawRect.width() / 2;
+            int delta = distanceFromCenter - radius;
+            if (Math.abs(delta) <= hysteresis) {
+                if (Math.abs(distY) > Math.abs(distX)) {
+                    if (distY < 0) {
+                        retval = GROW_TOP_EDGE;
+                    } else {
+                        retval = GROW_BOTTOM_EDGE;
+                    }
+                } else {
+                    if (distX < 0) {
+                        retval = GROW_LEFT_EDGE;
+                    } else {
+                        retval = GROW_RIGHT_EDGE;
+                    }
+                }
+            } else if (distanceFromCenter < radius) {
+                retval = MOVE;
+            } else {
+                retval = GROW_NONE;
+            }
+        } else {
+            // verticalCheck makes sure the position is between the top and
+            // the bottom edge (with some tolerance). Similar for horizCheck.
+            boolean verticalCheck = (y >= r.top - hysteresis)
+                    && (y < r.bottom + hysteresis);
+            boolean horizCheck = (x >= r.left - hysteresis)
+                    && (x < r.right + hysteresis);
+
+            // Check whether the position is near some edge(s).
+            if ((Math.abs(r.left - x)     < hysteresis)  &&  verticalCheck) {
+                retval |= GROW_LEFT_EDGE;
+            }
+            if ((Math.abs(r.right - x)    < hysteresis)  &&  verticalCheck) {
+                retval |= GROW_RIGHT_EDGE;
+            }
+            if ((Math.abs(r.top - y)      < hysteresis)  &&  horizCheck) {
+                retval |= GROW_TOP_EDGE;
+            }
+            if ((Math.abs(r.bottom - y)   < hysteresis)  &&  horizCheck) {
+                retval |= GROW_BOTTOM_EDGE;
+            }
+
+            // Not near any edge but inside the rectangle: move.
+            if (retval == GROW_NONE && r.contains((int) x, (int) y)) {
+                retval = MOVE;
+            }
+        }
+        return retval;
+    }
+
+    // Handles motion (dx, dy) in screen space.
+    // The "edge" parameter specifies which edges the user is dragging.
+    void handleMotion(int edge, float dx, float dy) {
+        Rect r = computeLayout();
+        if (edge == GROW_NONE) {
+            return;
+        } else if (edge == MOVE) {
+            // Convert to image space before sending to moveBy().
+            moveBy(dx * (mCropRect.width() / r.width()),
+                   dy * (mCropRect.height() / r.height()));
+        } else {
+            if (((GROW_LEFT_EDGE | GROW_RIGHT_EDGE) & edge) == 0) {
+                dx = 0;
+            }
+
+            if (((GROW_TOP_EDGE | GROW_BOTTOM_EDGE) & edge) == 0) {
+                dy = 0;
+            }
+
+            // Convert to image space before sending to growBy().
+            float xDelta = dx * (mCropRect.width() / r.width());
+            float yDelta = dy * (mCropRect.height() / r.height());
+            growBy((((edge & GROW_LEFT_EDGE) != 0) ? -1 : 1) * xDelta,
+                    (((edge & GROW_TOP_EDGE) != 0) ? -1 : 1) * yDelta);
+        }
+    }
+
+    // Grows the cropping rectange by (dx, dy) in image space.
+    void moveBy(float dx, float dy) {
+        Rect invalRect = new Rect(mDrawRect);
+
+        mCropRect.offset(dx, dy);
+
+        // Put the cropping rectangle inside image rectangle.
+        mCropRect.offset(
+                Math.max(0, mImageRect.left - mCropRect.left),
+                Math.max(0, mImageRect.top  - mCropRect.top));
+
+        mCropRect.offset(
+                Math.min(0, mImageRect.right  - mCropRect.right),
+                Math.min(0, mImageRect.bottom - mCropRect.bottom));
+
+        mDrawRect = computeLayout();
+        invalRect.union(mDrawRect);
+        invalRect.inset(-10, -10);
+        mContext.invalidate(invalRect);
+    }
+
+    // Grows the cropping rectange by (dx, dy) in image space.
+    void growBy(float dx, float dy) {
+        if (mMaintainAspectRatio) {
+            if (dx != 0) {
+                dy = dx / mInitialAspectRatio;
+            } else if (dy != 0) {
+                dx = dy * mInitialAspectRatio;
+            }
+        }
+
+        // Don't let the cropping rectangle grow too fast.
+        // Grow at most half of the difference between the image rectangle and
+        // the cropping rectangle.
+        RectF r = new RectF(mCropRect);
+        if (dx > 0F && r.width() + 2 * dx > mImageRect.width()) {
+            float adjustment = (mImageRect.width() - r.width()) / 2F;
+            dx = adjustment;
+            if (mMaintainAspectRatio) {
+                dy = dx / mInitialAspectRatio;
+            }
+        }
+        if (dy > 0F && r.height() + 2 * dy > mImageRect.height()) {
+            float adjustment = (mImageRect.height() - r.height()) / 2F;
+            dy = adjustment;
+            if (mMaintainAspectRatio) {
+                dx = dy * mInitialAspectRatio;
+            }
+        }
+
+        r.inset(-dx, -dy);
+
+        // Don't let the cropping rectangle shrink too fast.
+        final float widthCap = 25F;
+        if (r.width() < widthCap) {
+            r.inset(-(widthCap - r.width()) / 2F, 0F);
+        }
+        float heightCap = mMaintainAspectRatio
+                ? (widthCap / mInitialAspectRatio)
+                : widthCap;
+        if (r.height() < heightCap) {
+            r.inset(0F, -(heightCap - r.height()) / 2F);
+        }
+
+        // Put the cropping rectangle inside the image rectangle.
+        if (r.left < mImageRect.left) {
+            r.offset(mImageRect.left - r.left, 0F);
+        } else if (r.right > mImageRect.right) {
+            r.offset(-(r.right - mImageRect.right), 0);
+        }
+        if (r.top < mImageRect.top) {
+            r.offset(0F, mImageRect.top - r.top);
+        } else if (r.bottom > mImageRect.bottom) {
+            r.offset(0F, -(r.bottom - mImageRect.bottom));
+        }
+
+        mCropRect.set(r);
+        mDrawRect = computeLayout();
+        mContext.invalidate();
+    }
+
+    // Returns the cropping rectangle in image space.
+    public Rect getCropRect() {
+        return new Rect((int) mCropRect.left, (int) mCropRect.top,
+                        (int) mCropRect.right, (int) mCropRect.bottom);
+    }
+
+    // Maps the cropping rectangle from image space to screen space.
+    private Rect computeLayout() {
+        RectF r = new RectF(mCropRect.left, mCropRect.top,
+                            mCropRect.right, mCropRect.bottom);
+        mMatrix.mapRect(r);
+        return new Rect(Math.round(r.left), Math.round(r.top),
+                        Math.round(r.right), Math.round(r.bottom));
+    }
+
+    public void invalidate() {
+        mDrawRect = computeLayout();
+    }
+
+    public void setup(Matrix m, Rect imageRect, RectF cropRect, boolean circle,
+                      boolean maintainAspectRatio) {
+        if (circle) {
+            maintainAspectRatio = true;
+        }
+        mMatrix = new Matrix(m);
+
+        mCropRect = cropRect;
+        mImageRect = new RectF(imageRect);
+        mMaintainAspectRatio = maintainAspectRatio;
+        mCircle = circle;
+
+        mInitialAspectRatio = mCropRect.width() / mCropRect.height();
+        mDrawRect = computeLayout();
+
+        mFocusPaint.setARGB(125, 50, 50, 50);
+        mNoFocusPaint.setARGB(125, 50, 50, 50);
+        mOutlinePaint.setStrokeWidth(3F);
+        mOutlinePaint.setStyle(Paint.Style.STROKE);
+        mOutlinePaint.setAntiAlias(true);
+
+        mMode = ModifyMode.None;
+        init();
+    }
+
+    enum ModifyMode { None, Move, Grow }
+
+    private ModifyMode mMode = ModifyMode.None;
+
+    Rect mDrawRect;  // in screen space
+    private RectF mImageRect;  // in image space
+    RectF mCropRect;  // in image space
+    Matrix mMatrix;
+
+    private boolean mMaintainAspectRatio = false;
+    private float mInitialAspectRatio;
+    private boolean mCircle = false;
+
+    private Drawable mResizeDrawableWidth;
+    private Drawable mResizeDrawableHeight;
+    private Drawable mResizeDrawableDiagonal;
+
+    private final Paint mFocusPaint = new Paint();
+    private final Paint mNoFocusPaint = new Paint();
+    private final Paint mOutlinePaint = new Paint();
+}
+
diff --git a/src/com/cooliris/media/HudLayer.java b/src/com/cooliris/media/HudLayer.java
new file mode 100644
index 0000000..bff92f5
--- /dev/null
+++ b/src/com/cooliris/media/HudLayer.java
@@ -0,0 +1,768 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.microedition.khronos.opengles.GL11;
+
+import android.app.AlertDialog;
+import android.content.ComponentName;
+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.Resources;
+import android.net.Uri;
+import android.util.FloatMath;
+import android.view.MotionEvent;
+
+import com.cooliris.media.MenuBar.Menu;
+import com.cooliris.media.PopupMenu.Option;
+
+public final class HudLayer extends Layer {
+    public static final int MODE_NORMAL = 0;
+    public static final int MODE_SELECT = 1;
+
+    private final Context mContext;
+    private GridLayer mGridLayer;
+    private final ImageButton mTopRightButton = new ImageButton();
+    private final ImageButton mZoomInButton = new ImageButton();
+    private final ImageButton mZoomOutButton = new ImageButton();
+    private final PathBarLayer mPathBar;
+    private final TimeBar mTimeBar;
+    private MenuBar.Menu[] mNormalBottomMenu = null;
+    private MenuBar.Menu[] mSingleViewIntentBottomMenu = null;
+    private final MenuBar mSelectionMenuBottom;
+    private final MenuBar mSelectionMenuTop;
+    private final MenuBar mFullscreenMenu;
+    private final LoadingLayer mLoadingLayer = new LoadingLayer();
+    private RenderView mView = null;
+
+    private int mMode = MODE_NORMAL;
+
+    // Camera button - launches the camera intent when pressed.
+    private static final int CAMERA_BUTTON_ICON = R.drawable.btn_camera;
+    private static final int CAMERA_BUTTON_ICON_PRESSED = R.drawable.btn_camera_pressed;
+    private static final int ZOOM_IN_ICON = R.drawable.btn_hud_zoom_in_normal;
+    private static final int ZOOM_IN_ICON_PRESSED = R.drawable.btn_hud_zoom_in_pressed;
+    private static final int ZOOM_OUT_ICON = R.drawable.btn_hud_zoom_out_normal;
+    private static final int ZOOM_OUT_ICON_PRESSED = R.drawable.btn_hud_zoom_out_pressed;
+
+    private final Runnable mCameraButtonAction = new Runnable() {
+        public void run() {
+            // Launch the camera intent.
+            Intent intent = new Intent();
+            intent.setClassName("com.android.camera", "com.android.camera.Camera");
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            mContext.startActivity(intent);
+        }
+    };
+
+    // Grid mode button - switches the media browser to grid mode.
+    private static final int GRID_MODE_ICON = R.drawable.mode_stack;
+    private static final int GRID_MODE_PRESSED_ICON = R.drawable.mode_stack;
+
+    private final Runnable mZoomInButtonAction = new Runnable() {
+        public void run() {
+            mGridLayer.zoomInToSelectedItem();
+            mGridLayer.markDirty(1);
+        }
+    };
+
+    private final Runnable mZoomOutButtonAction = new Runnable() {
+        public void run() {
+            mGridLayer.zoomOutFromSelectedItem();
+            mGridLayer.markDirty(1);
+        }
+    };
+
+    private final Runnable mGridModeButtonAction = new Runnable() {
+        public void run() {
+            mGridLayer.setState(GridLayer.STATE_GRID_VIEW);
+        }
+    };
+
+    /**
+     * Stack mode button - switches the media browser to grid mode.
+     */
+    private static final int STACK_MODE_ICON = R.drawable.mode_grid;
+    private static final int STACK_MODE_PRESSED_ICON = R.drawable.mode_grid;
+    private final Runnable mStackModeButtonAction = new Runnable() {
+        public void run() {
+            mGridLayer.setState(GridLayer.STATE_TIMELINE);
+        }
+    };
+    private float mAlpha;
+    private float mAnimAlpha;
+    private boolean mAutoHide;
+    private float mTimeElapsedSinceFullOpacity;
+    private String mCachedCaption;
+    private String mCachedPosition;
+    private String mCachedCurrentLabel;
+
+    HudLayer(Context context) {
+        mAlpha = 1.0f;
+        mContext = context;
+        mTimeBar = new TimeBar(context);
+        mPathBar = new PathBarLayer();
+        mTopRightButton.setSize((int) (100 * Gallery.PIXEL_DENSITY), (int) (94 * Gallery.PIXEL_DENSITY));
+
+        mZoomInButton.setSize(43 * Gallery.PIXEL_DENSITY, 43 * Gallery.PIXEL_DENSITY);
+        mZoomOutButton.setSize(43 * Gallery.PIXEL_DENSITY, 43 * Gallery.PIXEL_DENSITY);
+        mZoomInButton.setImages(ZOOM_IN_ICON, ZOOM_IN_ICON_PRESSED);
+        mZoomInButton.setAction(mZoomInButtonAction);
+        mZoomOutButton.setImages(ZOOM_OUT_ICON, ZOOM_OUT_ICON_PRESSED);
+        mZoomOutButton.setAction(mZoomOutButtonAction);
+
+        // The Share submenu is populated dynamically when opened.
+        Resources resources = context.getResources();
+        PopupMenu.Option[] deleteOptions = {
+                new PopupMenu.Option(mContext.getResources().getString(R.string.confirm_delete), resources
+                        .getDrawable(R.drawable.icon_delete), new Runnable() {
+                    public void run() {
+                        deleteSelection();
+                    }
+                }),
+                new PopupMenu.Option(mContext.getResources().getString(R.string.cancel), resources
+                        .getDrawable(R.drawable.icon_cancel), new Runnable() {
+                    public void run() {
+
+                    }
+                }), };
+        mSelectionMenuBottom = new MenuBar(context);
+
+        MenuBar.Menu shareMenu = new MenuBar.Menu.Builder(mContext.getResources().getString(R.string.share)).icon(
+                R.drawable.icon_share).onSelect(new Runnable() {
+            public void run() {
+                updateShareMenu();
+            }
+        }).build();
+
+        MenuBar.Menu deleteMenu = new MenuBar.Menu.Builder(mContext.getResources().getString(R.string.delete)).icon(
+                R.drawable.icon_delete).options(deleteOptions).build();
+
+        MenuBar.Menu moreMenu = new MenuBar.Menu.Builder(mContext.getResources().getString(R.string.more)).icon(
+                R.drawable.icon_more).onSelect(new Runnable() {
+            public void run() {
+                buildMoreOptions();
+            }
+        }).build();
+
+        mNormalBottomMenu = new MenuBar.Menu[] { shareMenu, deleteMenu, moreMenu };
+        mSingleViewIntentBottomMenu = new MenuBar.Menu[] { shareMenu, moreMenu };
+
+        mSelectionMenuBottom.setMenus(mNormalBottomMenu);
+        mSelectionMenuTop = new MenuBar(context);
+        mSelectionMenuTop.setMenus(new MenuBar.Menu[] {
+                new MenuBar.Menu.Builder(mContext.getResources().getString(R.string.select_all)).onSelect(new Runnable() {
+                    public void run() {
+                        mGridLayer.selectAll();
+                    }
+                }).build(), new MenuBar.Menu.Builder("").build(),
+                new MenuBar.Menu.Builder(mContext.getResources().getString(R.string.deselect_all)).onSelect(new Runnable() {
+                    public void run() {
+                        mGridLayer.deselectOrCancelSelectMode();
+                    }
+                }).build() });
+        mFullscreenMenu = new MenuBar(context);
+        mFullscreenMenu.setMenus(new MenuBar.Menu[] {
+                new MenuBar.Menu.Builder(mContext.getResources().getString(R.string.slideshow)).icon(R.drawable.icon_play)
+                        .onSingleTapUp(new Runnable() {
+                            public void run() {
+                                if (getAlpha() == 1.0f)
+                                    mGridLayer.startSlideshow();
+                                else
+                                    setAlpha(1.0f);
+                            }
+                        }).build(), /* new MenuBar.Menu.Builder("").build(), */
+                new MenuBar.Menu.Builder(mContext.getResources().getString(R.string.menu)).icon(R.drawable.icon_more)
+                        .onSingleTapUp(new Runnable() {
+                            public void run() {
+                                if (getAlpha() == 1.0f)
+                                    mGridLayer.enterSelectionMode();
+                                else
+                                    setAlpha(1.0f);
+                            }
+                        }).build() });
+    }
+
+    private void buildMoreOptions() {
+        ArrayList<MediaBucket> buckets = mGridLayer.getSelectedBuckets();
+
+        int numBuckets = buckets.size();
+        boolean albumMode = false;
+        boolean singleItem = false;
+        boolean isPicasa = false;
+        int mediaType = MediaItem.MEDIA_TYPE_IMAGE;
+        if (numBuckets > 1) {
+            albumMode = true;
+        }
+        if (numBuckets == 1) {
+            MediaBucket bucket = buckets.get(0);
+            MediaSet mediaSet = bucket.mediaSet;
+            if (mediaSet == null) {
+                return;
+            }
+            isPicasa = mediaSet.mPicasaAlbumId != Shared.INVALID;
+            if (bucket.mediaItems == null || bucket.mediaItems.size() == 0) {
+                albumMode = true;
+            } else {
+                ArrayList<MediaItem> items = bucket.mediaItems;
+                int numItems = items.size();
+                mediaType = items.get(0).getMediaType();
+                if (numItems == 1) {
+                    singleItem = true;
+                } else {
+                    for (int i = 1; i < numItems; ++i) {
+                        if (items.get(0).getMediaType() != mediaType) {
+                            albumMode = true;
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        Option[] optionAll = new Option[] { new PopupMenu.Option(mContext.getResources().getString(R.string.details), mContext
+                .getResources().getDrawable(R.drawable.ic_menu_view_details), new Runnable() {
+            public void run() {
+                ArrayList<MediaBucket> buckets = mGridLayer.getSelectedBuckets();
+                final AlertDialog.Builder builder = new AlertDialog.Builder((Gallery) mContext);
+                builder.setTitle(mContext.getResources().getString(R.string.details));
+                boolean foundDataToDisplay = true;
+
+                if (buckets == null) {
+                    foundDataToDisplay = false;
+                } else {
+                    CharSequence[] strings = DetailMode.populateDetailModeStrings(mContext, buckets);
+                    if (strings == null) {
+                        foundDataToDisplay = false;
+                    } else {
+                        builder.setItems(strings, null);
+                    }
+                }
+
+                mGridLayer.deselectAll();
+                if (foundDataToDisplay) {
+                    builder.setNeutralButton(R.string.details_ok, null);
+                    ((Gallery) mContext).getHandler().post(new Runnable() {
+                        public void run() {
+                            builder.show();
+                        }
+                    });
+                }
+            }
+        }) };
+
+        Option[] optionSingle = new Option[] { new PopupMenu.Option(mContext.getResources().getString(R.string.show_on_map),
+                mContext.getResources().getDrawable(R.drawable.ic_menu_mapmode), new Runnable() {
+                    public void run() {
+                        ArrayList<MediaBucket> buckets = mGridLayer.getSelectedBuckets();
+                        MediaItem item = MediaBucketList.getFirstItemSelection(buckets);
+                        if (item == null) {
+                            return;
+                        }
+                        mGridLayer.deselectAll();
+                        Util.openMaps(mContext, item.mLatitude, item.mLongitude);
+                    }
+                }), };
+
+        Option[] optionImageMultiple = new Option[] {
+                new PopupMenu.Option(mContext.getResources().getString(R.string.rotate_left), mContext.getResources().getDrawable(
+                        R.drawable.ic_menu_rotate_left), new Runnable() {
+                    public void run() {
+                        mGridLayer.rotateSelectedItems(-90.0f);
+                    }
+                }),
+                new PopupMenu.Option(mContext.getResources().getString(R.string.rotate_right), mContext.getResources().getDrawable(
+                        R.drawable.ic_menu_rotate_right), new Runnable() {
+                    public void run() {
+                        mGridLayer.rotateSelectedItems(90.0f);
+                    }
+                }), };
+
+        if (isPicasa) {
+            optionImageMultiple = new Option[] {};
+        }
+        Option[] optionImageSingle;
+        if (isPicasa) {
+            optionImageSingle = new Option[] { new PopupMenu.Option(mContext.getResources().getString(R.string.set_as_wallpaper),
+                    mContext.getResources().getDrawable(R.drawable.ic_menu_set_as), new Runnable() {
+                        public void run() {
+                            ArrayList<MediaBucket> buckets = mGridLayer.getSelectedBuckets();
+                            MediaItem item = MediaBucketList.getFirstItemSelection(buckets);
+                            if (item == null) {
+                                return;
+                            }
+                            mGridLayer.deselectAll();
+                            if (item.mParentMediaSet.mPicasaAlbumId != Shared.INVALID) {
+                                final Intent intent = new Intent("android.intent.action.ATTACH_DATA");
+                                intent.setClassName("com.cooliris.media", "com.cooliris.media.Photographs");
+                                intent.setData(Uri.parse(item.mContentUri));
+                                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                                ((Gallery) mContext).startActivityForResult(intent, 0);
+                            }
+                        }
+                    }) };
+        } else {
+            optionImageSingle = new Option[] {
+                    new PopupMenu.Option((isPicasa) ? mContext.getResources().getString(R.string.set_as_wallpaper) : mContext
+                            .getResources().getString(R.string.set_as), mContext.getResources().getDrawable(
+                            R.drawable.ic_menu_set_as), new Runnable() {
+                        public void run() {
+                            ArrayList<MediaBucket> buckets = mGridLayer.getSelectedBuckets();
+                            MediaItem item = MediaBucketList.getFirstItemSelection(buckets);
+                            if (item == null) {
+                                return;
+                            }
+                            mGridLayer.deselectAll();
+                            if (item.mParentMediaSet.mPicasaAlbumId != Shared.INVALID) {
+                                final Intent intent = new Intent("android.intent.action.ATTACH_DATA");
+                                intent.setClassName("com.cooliris.media", "com.cooliris.media.Photographs");
+                                intent.setData(Uri.parse(item.mContentUri));
+                                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                                ((Gallery) mContext).startActivityForResult(intent, 0);
+                            } else {
+                                Intent intent = Util.createSetAsIntent(Uri.parse(item.mContentUri), item.mMimeType);
+                                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                                ((Gallery) mContext).startActivity(Intent.createChooser(intent, ((Gallery) mContext)
+                                        .getText(R.string.set_image)));
+                            }
+                        }
+                    }),
+                    new PopupMenu.Option(mContext.getResources().getString(R.string.crop), mContext.getResources().getDrawable(
+                            R.drawable.ic_menu_crop), new Runnable() {
+                        public void run() {
+                            ArrayList<MediaBucket> buckets = mGridLayer.getSelectedBuckets();
+                            MediaItem item = MediaBucketList.getFirstItemSelection(buckets);
+                            if (item == null) {
+                                return;
+                            }
+                            mGridLayer.deselectAll();
+                            final Intent intent = new Intent("com.android.camera.action.CROP");
+                            intent.setClassName("com.cooliris.media", "com.cooliris.media.CropImage");
+                            intent.setData(Uri.parse(item.mContentUri));
+                            ((Gallery) mContext).startActivityForResult(intent, Gallery.CROP_MSG_INTERNAL);
+                        }
+                    }) };
+        }
+        Option[] options = optionAll;
+        if (!albumMode) {
+            if (!singleItem) {
+                if (mediaType == MediaItem.MEDIA_TYPE_IMAGE)
+                    options = concat(options, optionImageMultiple);
+            } else {
+                MediaItem item = MediaBucketList.getFirstItemSelection(buckets);
+                if (item.mLatitude != 0.0f && item.mLongitude != 0.0f) {
+                    options = concat(options, optionSingle);
+                }
+                if (mediaType == MediaItem.MEDIA_TYPE_IMAGE) {
+                    options = concat(options, optionImageSingle);
+                    options = concat(options, optionImageMultiple);
+                }
+            }
+        }
+
+        // We are assuming that the more menu is the last item in the menu array.
+        int lastIndex = mSelectionMenuBottom.getMenus().length - 1;
+        mSelectionMenuBottom.getMenus()[lastIndex].options = options;
+    }
+
+    private static final Option[] concat(Option[] A, Option[] B) {
+        Option[] C = (Option[]) new Option[A.length + B.length];
+        System.arraycopy(A, 0, C, 0, A.length);
+        System.arraycopy(B, 0, C, A.length, B.length);
+        return C;
+    }
+
+    public void updateNumItemsSelected(int numItems) {
+        String items = " " + ((numItems == 1) ? mContext.getString(R.string.item) : mContext.getString(R.string.items));
+        Menu menu = new MenuBar.Menu.Builder(numItems + items).config(MenuBar.MENU_TITLE_STYLE_TEXT).build();
+        mSelectionMenuTop.updateMenu(menu, 1);
+    }
+
+    protected void deleteSelection() {
+        mGridLayer.deleteSelection();
+    }
+
+    void setGridLayer(GridLayer layer) {
+        mGridLayer = layer;
+    }
+
+    int getMode() {
+        return mMode;
+    }
+
+    void setMode(int mode) {
+        if (mMode != mode) {
+            mMode = mode;
+            updateViews();
+        }
+    }
+
+    @Override
+    protected void onSizeChanged() {
+        final float width = mWidth;
+        final float height = mHeight;
+        closeSelectionMenu();
+
+        mTimeBar.setPosition(0f, height - TimeBar.HEIGHT * Gallery.PIXEL_DENSITY);
+        mTimeBar.setSize(width, TimeBar.HEIGHT * Gallery.PIXEL_DENSITY);
+        mSelectionMenuTop.setPosition(0f, 0);
+        mSelectionMenuTop.setSize(width, MenuBar.HEIGHT * Gallery.PIXEL_DENSITY);
+        mSelectionMenuBottom.setPosition(0f, height - MenuBar.HEIGHT * Gallery.PIXEL_DENSITY);
+        mSelectionMenuBottom.setSize(width, MenuBar.HEIGHT * Gallery.PIXEL_DENSITY);
+
+        mFullscreenMenu.setPosition(0f, height - MenuBar.HEIGHT * Gallery.PIXEL_DENSITY);
+        mFullscreenMenu.setSize(width, MenuBar.HEIGHT * Gallery.PIXEL_DENSITY);
+
+        mPathBar.setPosition(0f, -4f * Gallery.PIXEL_DENSITY);
+        computeSizeForPathbar();
+
+        mTopRightButton.setPosition(width - mTopRightButton.getWidth(), 0f);
+        mZoomInButton.setPosition(width - mZoomInButton.getWidth(), 0f);
+        mZoomOutButton.setPosition(width - mZoomInButton.getWidth(), mZoomInButton.getHeight());
+    }
+
+    private void computeSizeForPathbar() {
+        float pathBarWidth = mWidth
+                - ((mGridLayer.getState() == GridLayer.STATE_FULL_SCREEN) ? 32 * Gallery.PIXEL_DENSITY
+                        : 120 * Gallery.PIXEL_DENSITY);
+        mPathBar.setSize(pathBarWidth, FloatMath.ceil(39 * Gallery.PIXEL_DENSITY));
+        mPathBar.recomputeComponents();
+    }
+
+    public void setFeed(MediaFeed feed, int state, boolean needsLayout) {
+        mTimeBar.setFeed(feed, state, needsLayout);
+    }
+
+    public void onGridStateChanged() {
+        updateViews();
+    }
+
+    private void updateViews() {
+        final int state = mGridLayer.getState();
+
+        // Show the selection menu in selection mode.
+        final boolean selectionMode = mMode == MODE_SELECT;
+        final boolean fullscreenMode = state == GridLayer.STATE_FULL_SCREEN;
+        final boolean stackMode = state == GridLayer.STATE_MEDIA_SETS || state == GridLayer.STATE_TIMELINE;
+        mSelectionMenuTop.setHidden(!selectionMode || fullscreenMode);
+        mSelectionMenuBottom.setHidden(!selectionMode);
+        mFullscreenMenu.setHidden(!fullscreenMode || selectionMode);
+        mZoomInButton.setHidden(mFullscreenMenu.isHidden());
+        mZoomOutButton.setHidden(mFullscreenMenu.isHidden());
+
+        // Show the time bar in stack and grid states, except in selection mode.
+        mTimeBar.setHidden(fullscreenMode || selectionMode || stackMode);
+        // mTimeBar.setHidden(selectionMode || (state != GridLayer.STATE_TIMELINE && state != GridLayer.STATE_GRID_VIEW));
+
+        // Hide the path bar and top-right button in selection mode.
+        mPathBar.setHidden(selectionMode);
+        mTopRightButton.setHidden(selectionMode || fullscreenMode);
+        computeSizeForPathbar();
+
+        // Configure the top-right button.
+        int image = 0;
+        int pressedImage = 0;
+        Runnable action = null;
+        final ImageButton topRightButton = mTopRightButton;
+        int height = (int) (94 * Gallery.PIXEL_DENSITY);
+        switch (state) {
+        case GridLayer.STATE_MEDIA_SETS:
+            image = CAMERA_BUTTON_ICON;
+            pressedImage = CAMERA_BUTTON_ICON_PRESSED;
+            action = mCameraButtonAction;
+            break;
+        case GridLayer.STATE_GRID_VIEW:
+            height /= 2;
+            image = STACK_MODE_ICON;
+            pressedImage = STACK_MODE_PRESSED_ICON;
+            action = mStackModeButtonAction;
+            break;
+        case GridLayer.STATE_TIMELINE:
+            image = GRID_MODE_ICON;
+            pressedImage = GRID_MODE_PRESSED_ICON;
+            action = mGridModeButtonAction;
+            break;
+        default:
+            break;
+        }
+        topRightButton.setSize((int) (100 * Gallery.PIXEL_DENSITY), height);
+        topRightButton.setImages(image, pressedImage);
+        topRightButton.setAction(action);
+    }
+
+    public TimeBar getTimeBar() {
+        return mTimeBar;
+    }
+
+    public PathBarLayer getPathBar() {
+        return mPathBar;
+    }
+
+    public GridLayer getGridLayer() {
+        return mGridLayer;
+    }
+
+    @Override
+    public boolean update(RenderView view, float frameInterval) {
+        float factor = 1.0f;
+        if (mAlpha == 1.0f) {
+            // Speed up the animation when it becomes visible.
+            factor = 4.0f;
+        }
+        mAnimAlpha = FloatUtils.animate(mAnimAlpha, mAlpha, frameInterval * factor);
+        boolean timeElapsedSinceFullOpacity_Reset = mTimeElapsedSinceFullOpacity == 0.0f;
+
+        if (mAutoHide) {
+            if (mAlpha == 1.0f && mMode != MODE_SELECT) {
+                mTimeElapsedSinceFullOpacity += frameInterval;
+                if (mTimeElapsedSinceFullOpacity > 5.0f)
+                    setAlpha(0);
+            }
+        }
+        if (mAnimAlpha != mAlpha || (mTimeElapsedSinceFullOpacity < 5.0f && !timeElapsedSinceFullOpacity_Reset))
+            return true;
+
+        return false;
+    }
+
+    public void renderOpaque(RenderView view, GL11 gl) {
+
+    }
+
+    public void renderBlended(RenderView view, GL11 gl) {
+        view.setAlpha(mAnimAlpha);
+    }
+
+    public void setAlpha(float alpha) {
+        float oldAlpha = mAlpha;
+        mAlpha = alpha;
+        if (oldAlpha != alpha) {
+            if (mView != null)
+                mView.requestRender();
+        }
+        if (alpha == 1.0f) {
+            mTimeElapsedSinceFullOpacity = 0.0f;
+        }
+    }
+
+    public float getAlpha() {
+        return mAlpha;
+    }
+
+    public void setTimeBarTime(long time) {
+        // mTimeBar.setTime(time);
+    }
+
+    @Override
+    public void generate(RenderView view, RenderView.Lists lists) {
+        lists.opaqueList.add(this);
+        lists.blendedList.add(this);
+        lists.hitTestList.add(this);
+        lists.updateList.add(this);
+        mTopRightButton.generate(view, lists);
+        mZoomInButton.generate(view, lists);
+        mZoomOutButton.generate(view, lists);
+        mTimeBar.generate(view, lists);
+        mSelectionMenuTop.generate(view, lists);
+        mSelectionMenuBottom.generate(view, lists);
+        mFullscreenMenu.generate(view, lists);
+        mPathBar.generate(view, lists);
+        //mLoadingLayer.generate(view, lists);
+        mView = view;
+    }
+
+    @Override
+    public boolean containsPoint(float x, float y) {
+        return false;
+    }
+
+    public void cancelSelection() {
+        mSelectionMenuBottom.close();
+        closeSelectionMenu();
+        setMode(MODE_NORMAL);
+    }
+
+    public void closeSelectionMenu() {
+        mSelectionMenuBottom.close();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mMode == MODE_SELECT) {
+            /*
+             * setMode(MODE_NORMAL); ArrayList<MediaBucket> displayBuckets = mGridLayer.getSelectedBuckets(); // use this list, and
+             * then clear the items return true;
+             */
+            return false;
+        } else {
+            return false;
+        }
+    }
+
+    public boolean isLoaded() {
+        return mLoadingLayer.isLoaded();
+    }
+
+    void reset() {
+        mLoadingLayer.reset();
+    }
+
+    public void fullscreenSelectionChanged(MediaItem item, int index, int count) {
+        // request = new ReverseGeocoder.Request();
+        // request.firstLatitude = request.secondLatitude = item.latitude;
+        // request.firstLongitude = request.secondLongitude = item.longitude;
+        // mGeo.enqueue(request);
+        if (item == null)
+            return;
+        String location = index + "/" + count;
+        mCachedCaption = item.mCaption;
+        mCachedPosition = location;
+        mCachedCurrentLabel = location;
+        mPathBar.changeLabel(location);
+        // String displayString = DateFormat.format("h:mmaa MMM dd yyyy", item.dateTaken).toString();
+        // Menu menu = new
+        // MenuBar.Menu.Builder(displayString).StringTexture.Config(MenuBar.MENU_TITLE_STYLE_TEXT).resizeToAccomodate()
+        // .build();
+        // mFullscreenMenu.updateMenu(menu, 1);
+    }
+
+    private void updateShareMenu() {
+        // Get the first selected item. Wire this up to multiple-item intents when we move
+        // to Eclair.
+        ArrayList<MediaBucket> selection = mGridLayer.getSelectedBuckets();
+        ArrayList<Uri> uris = new ArrayList<Uri>();
+        String mimeType = null;
+        if (!selection.isEmpty()) {
+            int mediaType = Shared.INVALID;
+            int numBuckets = selection.size();
+            for (int j = 0; j < numBuckets; ++j) {
+                MediaBucket bucket = selection.get(j);
+                ArrayList<MediaItem> items = null;
+                int numItems = 0;
+                if (bucket.mediaItems != null && !bucket.mediaItems.isEmpty()) {
+                    items = bucket.mediaItems;
+                    numItems = items.size();
+                } else if (bucket.mediaSet != null) {
+                    // We need to delete the entire bucket.
+                    items = bucket.mediaSet.getItems();
+                    numItems = bucket.mediaSet.getNumItems();
+                }
+                for (int i = 0; i < numItems; ++i) {
+                    MediaItem item = items.get(i);
+                    if (mimeType == null) {
+                        mimeType = item.mMimeType;
+                        mediaType = item.getMediaType();
+                        MediaSet parentMediaSet = item.mParentMediaSet;
+                        if (parentMediaSet != null && parentMediaSet.mPicasaAlbumId != Shared.INVALID) {
+                            // This will go away once http uri's are supported for all media types.
+                            // This ensures that just the link is shared as a text
+                            mimeType = "text/plain";
+                        }
+                    }
+                    if (mediaType == item.getMediaType()) {
+                        // add this uri
+                        if (item.mContentUri != null) {
+                            Uri uri = Uri.parse(item.mContentUri);
+                            uris.add(uri);
+                        }
+                    }
+                }
+            }
+        }
+        PopupMenu.Option[] options = null;
+        if (uris.size() != 0) {
+            final Intent intent = new Intent();
+            if (mimeType == null)
+                mimeType = "image/jpeg";
+            if (mimeType.contains("text")) {
+                // We need to share this as a text string.
+                intent.setAction(Intent.ACTION_SEND);
+                intent.setType(mimeType);
+
+                // Create a newline-separated list of URLs.
+                StringBuilder builder = new StringBuilder();
+                for (int i = 0, size = uris.size(); i < size; ++i) {
+                    builder.append(uris.get(i));
+                    if (i != size - 1) {
+                        builder.append('\n');
+                    }
+                }
+                intent.putExtra(Intent.EXTRA_TEXT, builder.toString());
+            } else {
+                if (uris.size() == 1) {
+                    intent.setAction(Intent.ACTION_SEND);
+                    intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
+                } else {
+                    intent.setAction(Intent.ACTION_SEND_MULTIPLE);
+                    intent.putExtra(Intent.EXTRA_STREAM, uris);
+                }
+                intent.setType(mimeType);
+            }
+            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+            // Query the system for matching activities.
+            PackageManager packageManager = mContext.getPackageManager();
+            List<ResolveInfo> activities = packageManager.queryIntentActivities(intent, 0);
+            int numActivities = activities.size();
+            options = new PopupMenu.Option[numActivities];
+            for (int i = 0; i != numActivities; ++i) {
+                final ResolveInfo info = activities.get(i);
+                String label = info.loadLabel(packageManager).toString();
+                options[i] = new PopupMenu.Option(label, info.loadIcon(packageManager), new Runnable() {
+                    public void run() {
+                        startResolvedActivity(intent, info);
+                    }
+                });
+            }
+        }
+        mSelectionMenuBottom.getMenus()[0].options = options;
+    }
+
+    private void startResolvedActivity(Intent intent, ResolveInfo info) {
+        final Intent resolvedIntent = new Intent(intent);
+        ActivityInfo ai = info.activityInfo;
+        resolvedIntent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));
+        ((Gallery) mContext).getHandler().post(new Runnable() {
+            public void run() {
+                mContext.startActivity(resolvedIntent);
+            }
+        });
+    }
+
+    public void autoHide(boolean hide) {
+        mAutoHide = hide;
+    }
+
+    public void swapFullscreenLabel() {
+        mCachedCurrentLabel = (mCachedCurrentLabel == mCachedCaption || mCachedCaption == null) ? mCachedPosition : mCachedCaption;
+        mPathBar.changeLabel(mCachedCurrentLabel);
+    }
+
+    public void clear() {
+
+    }
+
+    public void shutDown() {
+        mGridLayer = null;
+    }
+
+    public void enterSelectionMode() {
+        setAlpha(1.0f);
+        setMode(HudLayer.MODE_SELECT);
+
+        // if we are in single view mode, show the bottom menu without the delete button.
+        if (mGridLayer.noDeleteMode()) {
+            mSelectionMenuBottom.setMenus(mSingleViewIntentBottomMenu);
+        } else {
+            mSelectionMenuBottom.setMenus(mNormalBottomMenu);
+        }
+    }
+
+    public Layer getMenuBar() {
+        return mFullscreenMenu;
+    }
+}
diff --git a/src/com/cooliris/media/IconTitleDrawable.java b/src/com/cooliris/media/IconTitleDrawable.java
new file mode 100644
index 0000000..723bb3e
--- /dev/null
+++ b/src/com/cooliris/media/IconTitleDrawable.java
@@ -0,0 +1,108 @@
+package com.cooliris.media;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.text.TextUtils;
+
+public final class IconTitleDrawable extends Drawable {
+    private final String mTitle;
+    private final int mTitleWidth;
+    private final Drawable mIcon;
+    private final Config mConfig;
+    private StaticLayout mTitleLayout = null;
+    private int mTitleY;
+
+    public IconTitleDrawable(String title, Drawable icon, Config config) {
+        mTitle = title;
+        mTitleWidth = (int) StaticLayout.getDesiredWidth(mTitle, config.mPaint);
+        mIcon = icon;
+        mConfig = config;
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        Config config = mConfig;
+        return config.mTitleLeft + mTitleWidth + 15;
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        Config config = mConfig;
+        return Math.max(config.mIconSize, config.mPaint.getFontMetricsInt(null));
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        // Paint test = new Paint();
+        // test.setColor(0xff0000ff);
+        // canvas.drawRect(getBounds(), test);
+
+        Drawable icon = mIcon;
+        if (icon != null) {
+            icon.draw(canvas);
+        }
+        Rect bounds = getBounds();
+        int x = bounds.left + mConfig.mTitleLeft;
+        int y = mTitleY;
+        canvas.translate(x, y);
+        mTitleLayout.draw(canvas);
+        canvas.translate(-x, -y);
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter cf) {
+    }
+
+    @Override
+    protected void onBoundsChange(Rect bounds) {
+        // Position the icon.
+        int left = bounds.left;
+        int top = bounds.top;
+        int right = bounds.right;
+        int height = bounds.bottom - top;
+        Config config = mConfig;
+        int iconLeft = left + config.mIconLeft;
+        int iconSize = config.mIconSize;
+        Drawable icon = mIcon;
+        if (icon != null) {
+            int iconY = top + (height - iconSize) / 2;
+            icon.setBounds(iconLeft, iconY, iconLeft + iconSize, top + iconSize);
+        }
+
+        // Layout the text.
+        int outerWidth = right - config.mTitleLeft;
+        String title = mTitle;
+        mTitleLayout = new StaticLayout(title, 0, title.length(), config.mPaint, outerWidth, Layout.Alignment.ALIGN_NORMAL, 1, 0,
+                true, TextUtils.TruncateAt.MIDDLE, outerWidth);
+        mTitleY = top + (height - mTitleLayout.getHeight()) / 2;
+    }
+
+    public static final class Config {
+        private final int mIconLeft;
+        private final int mTitleLeft;
+        private final int mIconSize;
+        private final TextPaint mPaint;
+
+        public Config(int iconSpan, int iconSize, TextPaint paint) {
+            mIconLeft = (iconSpan - iconSize) / 2;
+            mTitleLeft = iconSpan;
+            mIconSize = iconSize;
+            mPaint = paint;
+        }
+    }
+}
diff --git a/src/com/cooliris/media/ImageButton.java b/src/com/cooliris/media/ImageButton.java
new file mode 100644
index 0000000..52537d2
--- /dev/null
+++ b/src/com/cooliris/media/ImageButton.java
@@ -0,0 +1,126 @@
+package com.cooliris.media;
+
+import javax.microedition.khronos.opengles.GL11;
+
+import com.cooliris.media.R;
+import com.cooliris.media.RenderView.Lists;
+
+import android.os.SystemClock;
+import android.view.MotionEvent;
+
+public final class ImageButton extends Layer {
+    private static final float TRACKING_MARGIN = 30.0f;
+
+    // Images for the normal and pressed states.
+    private int mImage = 0;
+    private int mPressedImage = 0;
+
+    // The action to be invoked when a single tap occurs on the button.
+    private Runnable mAction = null;
+
+    // Animation state for button crossfades.
+    private final FloatAnim mFade = new FloatAnim(1f);
+    private int mCurrentImage = 0;
+    private int mPreviousImage = 0;
+    private boolean mPressed = false;
+
+    private final int mTransparent = R.drawable.transparent;
+
+    public void setImages(int image, int pressedImage) {
+        mImage = image;
+        mPressedImage = pressedImage;
+        if (!mPressed) {
+            setImage(image, true);
+        }
+    }
+
+    public final void setAction(Runnable action) {
+        mAction = action;
+    }
+
+    private boolean containsPoint(float x, float y, boolean addTrackingMargin) {
+        if (mImage != 0) {
+            float minX = mX;
+            float minY = mY;
+            float maxX = minX + mWidth;
+            float maxY = minY + mHeight;
+            if (addTrackingMargin) {
+                minX -= TRACKING_MARGIN;
+                minY -= TRACKING_MARGIN;
+                maxX += TRACKING_MARGIN;
+                maxY += TRACKING_MARGIN;
+            }
+            return x >= minX && y >= minY && x < maxX && y < maxY;
+        }
+        return false;
+    }
+
+    @Override
+    public void generate(RenderView view, Lists lists) {
+        lists.updateList.add(this);
+        lists.blendedList.add(this);
+        lists.hitTestList.add(this);
+    }
+
+    @Override
+    public void renderBlended(RenderView view, GL11 gl) {
+        // Get the value of the animation.
+        final float ratio = mFade.getValue(view.getFrameTime());
+        Texture currentImage = view.getResource(mCurrentImage);
+        Texture previousImage = view.getResource(mPreviousImage);
+        Texture transparent = view.getResource(mTransparent);
+        if (currentImage == null) {
+            currentImage = transparent;
+        }
+        if (previousImage == null) {
+            previousImage = transparent;
+        }
+        if (ratio >= 0.99f) {
+            view.draw2D(currentImage, mX, mY);
+        } else {
+            view.drawMixed2D(previousImage, currentImage, ratio, mX, mY, 0f, currentImage.getWidth(), currentImage.getHeight());
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent e) {
+        final int action = e.getAction();
+        switch (action) {
+        case MotionEvent.ACTION_DOWN:
+        case MotionEvent.ACTION_MOVE:
+            final boolean hit = containsPoint(e.getX(), e.getY(), true);
+            mPressed = hit;
+            if (hit) {
+                setImage(mPressedImage, false);
+            } else {
+                setImage(mImage, true);
+            }
+            break;
+        case MotionEvent.ACTION_UP:
+            if (mPressed) {
+                if (mAction != null)
+                    mAction.run();
+            }
+        case MotionEvent.ACTION_CANCEL:
+            mPressed = false;
+            setImage(mImage, true);
+            break;
+        default:
+            break;
+        }
+        return true;
+    }
+
+    private void setImage(int image, boolean animate) {
+        if (mCurrentImage != image) {
+            if (animate) {
+                mFade.setValue(0f);
+                mFade.animateValue(1f, 0.25f, SystemClock.uptimeMillis()); // TODO: use frame clock.
+                mPreviousImage = mCurrentImage;
+            } else {
+                mFade.setValue(1f);
+            }
+            mCurrentImage = image;
+        }
+    }
+}
diff --git a/src/com/cooliris/media/ImageManager.java b/src/com/cooliris/media/ImageManager.java
new file mode 100644
index 0000000..da194f7
--- /dev/null
+++ b/src/com/cooliris/media/ImageManager.java
@@ -0,0 +1,323 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 2007 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.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.ExecutionException;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.util.Log;
+
+/**
+ * ImageManager is used to retrieve and store images
+ * in the media content provider.
+ */
+public class ImageManager {
+    private static final String TAG = "ImageManager";
+
+    private static final Uri STORAGE_URI = Images.Media.EXTERNAL_CONTENT_URI;
+
+    /**
+     * Enumerate type for the location of the images in gallery.
+     */
+    public static enum DataLocation { NONE, INTERNAL, EXTERNAL, ALL }
+
+    public static final Bitmap DEFAULT_THUMBNAIL =
+            Bitmap.createBitmap(32, 32, Bitmap.Config.RGB_565);
+    public static final Bitmap NO_IMAGE_BITMAP =
+            Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565);
+
+    public static final int SORT_ASCENDING = 1;
+    public static final int SORT_DESCENDING = 2;
+
+    public static final int INCLUDE_IMAGES = (1 << 0);
+    public static final int INCLUDE_DRM_IMAGES = (1 << 1);
+    public static final int INCLUDE_VIDEOS = (1 << 2);
+
+    public static final String CAMERA_IMAGE_BUCKET_NAME =
+            Environment.getExternalStorageDirectory().toString()
+            + "/DCIM/Camera";
+    public static final String CAMERA_IMAGE_BUCKET_ID =
+            getBucketId(CAMERA_IMAGE_BUCKET_NAME);
+
+    /**
+     * Matches code in MediaProvider.computeBucketValues. Should be a common
+     * function.
+     */
+    public static String getBucketId(String path) {
+        return String.valueOf(path.toLowerCase().hashCode());
+    }
+
+    /**
+     * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
+     * imported. This is a temporary fix for bug#1655552.
+     */
+    public static void ensureOSXCompatibleFolder() {
+        File nnnAAAAA = new File(
+            Environment.getExternalStorageDirectory().toString()
+            + "/DCIM/100ANDRO");
+        if ((!nnnAAAAA.exists()) && (!nnnAAAAA.mkdir())) {
+            Log.e(TAG, "create NNNAAAAA file: " + nnnAAAAA.getPath()
+                    + " failed");
+        }
+    }
+
+    public static int roundOrientation(int orientationInput) {
+        int orientation = orientationInput;
+        if (orientation == -1) {
+            orientation = 0;
+        }
+
+        orientation = orientation % 360;
+        int retVal;
+        if (orientation < (0 * 90) + 45) {
+            retVal = 0;
+        } else if (orientation < (1 * 90) + 45) {
+            retVal = 90;
+        } else if (orientation < (2 * 90) + 45) {
+            retVal = 180;
+        } else if (orientation < (3 * 90) + 45) {
+            retVal = 270;
+        } else {
+            retVal = 0;
+        }
+
+        return retVal;
+    }
+
+    /**
+     * @return true if the mimetype is an image mimetype.
+     */
+    public static boolean isImageMimeType(String mimeType) {
+        return mimeType.startsWith("image/");
+    }
+
+    /**
+     * @return true if the mimetype is a video mimetype.
+     */
+    public static boolean isVideoMimeType(String mimeType) {
+        return mimeType.startsWith("video/");
+    }
+
+    public static void setImageSize(ContentResolver cr, Uri uri, long size) {
+        ContentValues values = new ContentValues();
+        values.put(Images.Media.SIZE, size);
+        cr.update(uri, values, null, null);
+    }
+
+    public static Uri addImage(ContentResolver cr, String title,
+            long dateTaken, double latitude, double longitude,
+            int orientation, String directory, String filename) {
+
+        ContentValues values = new ContentValues(7);
+        values.put(Images.Media.TITLE, title);
+
+        // That filename is what will be handed to Gmail when a user shares a
+        // photo. Gmail gets the name of the picture attachment from the
+        // "DISPLAY_NAME" field.
+        values.put(Images.Media.DISPLAY_NAME, filename);
+        values.put(Images.Media.DATE_TAKEN, dateTaken);
+        values.put(Images.Media.MIME_TYPE, "image/jpeg");
+        values.put(Images.Media.ORIENTATION, orientation);
+
+        values.put(Images.Media.LATITUDE, latitude);
+        values.put(Images.Media.LONGITUDE, longitude);
+                
+        if (directory != null && filename != null) {
+            String value = directory + "/" + filename;
+            values.put(Images.Media.DATA, value);
+        }
+
+        return cr.insert(STORAGE_URI, values);
+    }
+
+
+    private static class AddImageCancelable extends BaseCancelable<Void> {
+        private final Uri mUri;
+        private final ContentResolver mCr;
+        private final byte [] mJpegData;
+
+        public AddImageCancelable(Uri uri, ContentResolver cr,
+                int orientation, Bitmap source, byte[] jpegData) {
+            if (source == null && jpegData == null || uri == null) {
+                throw new IllegalArgumentException("source cannot be null");
+            }
+            mUri = uri;
+            mCr = cr;
+            mJpegData = jpegData;
+        }
+
+        @Override
+        protected Void execute() throws InterruptedException,
+                ExecutionException {
+            boolean complete = false;
+            try {
+                String[] projection = new String[] {
+                        ImageColumns._ID,
+                        ImageColumns.MINI_THUMB_MAGIC};
+                Cursor c = mCr.query(mUri, projection, null, null, null);
+                try {
+                    c.moveToPosition(0);
+                } finally {
+                    c.close();
+                }
+                ContentValues values = new ContentValues();
+                values.put(ImageColumns.MINI_THUMB_MAGIC, 0);
+                mCr.update(mUri, values, null, null);
+                OutputStream outputStream = null;
+                try {
+                    outputStream = mCr.openOutputStream(mUri);
+                    if (outputStream != null) {
+                        outputStream.write(mJpegData);
+                    }
+                } catch (IOException ex) {
+                    // TODO: report error to caller
+                    Log.e(TAG, "Cannot open file: " + mUri, ex);
+                } finally {
+                    Util.closeSilently(outputStream);
+                }
+                complete = true;
+                return null;
+            } finally {
+                if (!complete) {
+                    try {
+                        mCr.delete(mUri, null, null);
+                    } catch (Throwable t) {
+                        // ignore it while clean up.
+                    }
+                }
+            }
+        }
+    }
+
+    public static Cancelable<Void> storeImage(
+            Uri uri, ContentResolver cr, int orientation,
+            Bitmap source, byte [] jpegData) {
+        return new AddImageCancelable(
+                uri, cr, orientation, source, jpegData);
+    }
+
+    static boolean isSingleImageMode(String uriString) {
+        return !uriString.startsWith(
+                MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
+                && !uriString.startsWith(
+                MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString());
+    }
+
+    private static boolean checkFsWritable() {
+        // Create a temporary file to see whether a volume is really writeable.
+        // It's important not to put it in the root directory which may have a
+        // limit on the number of files.
+        String directoryName =
+                Environment.getExternalStorageDirectory().toString() + "/DCIM";
+        File directory = new File(directoryName);
+        if (!directory.isDirectory()) {
+            if (!directory.mkdirs()) {
+                return false;
+            }
+        }
+        File f = new File(directoryName, ".probe");
+        try {
+            // Remove stale file if any
+            if (f.exists()) {
+                f.delete();
+            }
+            if (!f.createNewFile()) {
+                return false;
+            }
+            f.delete();
+            return true;
+        } catch (IOException ex) {
+            return false;
+        }
+    }
+    
+    public static boolean quickHasStorage() {
+        return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
+    }
+
+    public static boolean hasStorage() {
+        return hasStorage(true);
+    }
+
+    public static boolean hasStorage(boolean requireWriteAccess) {
+        String state = Environment.getExternalStorageState();
+        if (Environment.MEDIA_MOUNTED.equals(state)) {
+            if (requireWriteAccess) {
+                boolean writable = checkFsWritable();
+                return writable;
+            } else {
+                return true;
+            }
+        } else if (!requireWriteAccess
+                && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
+            return true;
+        }
+        return false;
+    }
+
+    private static Cursor query(ContentResolver resolver, Uri uri,
+            String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        try {
+            if (resolver == null) {
+                return null;
+            }
+            return resolver.query(
+                    uri, projection, selection, selectionArgs, sortOrder);
+         } catch (UnsupportedOperationException ex) {
+            return null;
+        }
+
+    }
+
+    public static boolean isMediaScannerScanning(ContentResolver cr) {
+        boolean result = false;
+        Cursor cursor = query(cr, MediaStore.getMediaScannerUri(),
+                new String [] {MediaStore.MEDIA_SCANNER_VOLUME},
+                null, null, null);
+        if (cursor != null) {
+            if (cursor.getCount() == 1) {
+                cursor.moveToFirst();
+                result = "external".equals(cursor.getString(0));
+            }
+            cursor.close();
+        }
+        return result;
+    }
+
+    public static String getLastImageThumbPath() {
+        return Environment.getExternalStorageDirectory().toString() +
+               "/DCIM/.thumbnails/image_last_thumb";
+    }
+
+    public static String getLastVideoThumbPath() {
+        return Environment.getExternalStorageDirectory().toString() +
+               "/DCIM/.thumbnails/video_last_thumb";
+    }
+}
+
diff --git a/src/com/cooliris/media/ImageViewTouchBase.java b/src/com/cooliris/media/ImageViewTouchBase.java
new file mode 100644
index 0000000..19a6ac1
--- /dev/null
+++ b/src/com/cooliris/media/ImageViewTouchBase.java
@@ -0,0 +1,389 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 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.
+ */
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.ImageView;
+
+abstract class ImageViewTouchBase extends ImageView {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "ImageViewTouchBase";
+
+    // This is the base transformation which is used to show the image
+    // initially.  The current computation for this shows the image in
+    // it's entirety, letterboxing as needed.  One could choose to
+    // show the image as cropped instead.
+    //
+    // This matrix is recomputed when we go from the thumbnail image to
+    // the full size image.
+    protected Matrix mBaseMatrix = new Matrix();
+
+    // This is the supplementary transformation which reflects what
+    // the user has done in terms of zooming and panning.
+    //
+    // This matrix remains the same when we go from the thumbnail image
+    // to the full size image.
+    protected Matrix mSuppMatrix = new Matrix();
+
+    // This is the final matrix which is computed as the concatentation
+    // of the base matrix and the supplementary matrix.
+    private final Matrix mDisplayMatrix = new Matrix();
+
+    // Temporary buffer used for getting the values out of a matrix.
+    private final float[] mMatrixValues = new float[9];
+
+    // The current bitmap being displayed.
+    final protected RotateBitmap mBitmapDisplayed = new RotateBitmap(null);
+
+    int mThisWidth = -1, mThisHeight = -1;
+
+    float mMaxZoom;
+
+    // ImageViewTouchBase will pass a Bitmap to the Recycler if it has finished
+    // its use of that Bitmap.
+    public interface Recycler {
+        public void recycle(Bitmap b);
+    }
+
+    public void setRecycler(Recycler r) {
+        mRecycler = r;
+    }
+
+    private Recycler mRecycler;
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top,
+                            int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        mThisWidth = right - left;
+        mThisHeight = bottom - top;
+        Runnable r = mOnLayoutRunnable;
+        if (r != null) {
+            mOnLayoutRunnable = null;
+            r.run();
+        }
+        if (mBitmapDisplayed.getBitmap() != null) {
+            getProperBaseMatrix(mBitmapDisplayed, mBaseMatrix);
+            setImageMatrix(getImageViewMatrix());
+        }
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_BACK && getScale() > 1.0f) {
+            // If we're zoomed in, pressing Back jumps out to show the entire
+            // image, otherwise Back returns the user to the gallery.
+            zoomTo(1.0f);
+            return true;
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    protected Handler mHandler = new Handler();
+
+    protected int mLastXTouchPos;
+    protected int mLastYTouchPos;
+
+    @Override
+    public void setImageBitmap(Bitmap bitmap) {
+        setImageBitmap(bitmap, 0);
+    }
+
+    private void setImageBitmap(Bitmap bitmap, int rotation) {
+        super.setImageBitmap(bitmap);
+        Drawable d = getDrawable();
+        if (d != null) {
+            d.setDither(true);
+        }
+
+        Bitmap old = mBitmapDisplayed.getBitmap();
+        mBitmapDisplayed.setBitmap(bitmap);
+        mBitmapDisplayed.setRotation(rotation);
+
+        if (old != null && old != bitmap && mRecycler != null) {
+            mRecycler.recycle(old);
+        }
+    }
+
+    public void clear() {
+        setImageBitmapResetBase(null, true);
+    }
+
+    private Runnable mOnLayoutRunnable = null;
+
+    // This function changes bitmap, reset base matrix according to the size
+    // of the bitmap, and optionally reset the supplementary matrix.
+    public void setImageBitmapResetBase(final Bitmap bitmap,
+            final boolean resetSupp) {
+        setImageRotateBitmapResetBase(new RotateBitmap(bitmap), resetSupp);
+    }
+
+    public void setImageRotateBitmapResetBase(final RotateBitmap bitmap,
+            final boolean resetSupp) {
+        final int viewWidth = getWidth();
+
+        if (viewWidth <= 0)  {
+            mOnLayoutRunnable = new Runnable() {
+                public void run() {
+                    setImageRotateBitmapResetBase(bitmap, resetSupp);
+                }
+            };
+            return;
+        }
+
+        if (bitmap.getBitmap() != null) {
+            getProperBaseMatrix(bitmap, mBaseMatrix);
+            setImageBitmap(bitmap.getBitmap(), bitmap.getRotation());
+        } else {
+            mBaseMatrix.reset();
+            setImageBitmap(null);
+        }
+
+        if (resetSupp) {
+            mSuppMatrix.reset();
+        }
+        setImageMatrix(getImageViewMatrix());
+        mMaxZoom = maxZoom();
+    }
+
+    // Center as much as possible in one or both axis.  Centering is
+    // defined as follows:  if the image is scaled down below the
+    // view's dimensions then center it (literally).  If the image
+    // is scaled larger than the view and is translated out of view
+    // then translate it back into view (i.e. eliminate black bars).
+    protected void center(boolean horizontal, boolean vertical) {
+        if (mBitmapDisplayed.getBitmap() == null) {
+            return;
+        }
+
+        Matrix m = getImageViewMatrix();
+
+        RectF rect = new RectF(0, 0,
+                mBitmapDisplayed.getBitmap().getWidth(),
+                mBitmapDisplayed.getBitmap().getHeight());
+
+        m.mapRect(rect);
+
+        float height = rect.height();
+        float width  = rect.width();
+
+        float deltaX = 0, deltaY = 0;
+
+        if (vertical) {
+            int viewHeight = getHeight();
+            if (height < viewHeight) {
+                deltaY = (viewHeight - height) / 2 - rect.top;
+            } else if (rect.top > 0) {
+                deltaY = -rect.top;
+            } else if (rect.bottom < viewHeight) {
+                deltaY = getHeight() - rect.bottom;
+            }
+        }
+
+        if (horizontal) {
+            int viewWidth = getWidth();
+            if (width < viewWidth) {
+                deltaX = (viewWidth - width) / 2 - rect.left;
+            } else if (rect.left > 0) {
+                deltaX = -rect.left;
+            } else if (rect.right < viewWidth) {
+                deltaX = viewWidth - rect.right;
+            }
+        }
+
+        postTranslate(deltaX, deltaY);
+        setImageMatrix(getImageViewMatrix());
+    }
+
+    public ImageViewTouchBase(Context context) {
+        super(context);
+        init();
+    }
+
+    public ImageViewTouchBase(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    private void init() {
+        setScaleType(ImageView.ScaleType.MATRIX);
+    }
+
+    protected float getValue(Matrix matrix, int whichValue) {
+        matrix.getValues(mMatrixValues);
+        return mMatrixValues[whichValue];
+    }
+
+    // Get the scale factor out of the matrix.
+    protected float getScale(Matrix matrix) {
+        return getValue(matrix, Matrix.MSCALE_X);
+    }
+
+    protected float getScale() {
+        return getScale(mSuppMatrix);
+    }
+
+    // Setup the base matrix so that the image is centered and scaled properly.
+    private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix) {
+        float viewWidth = getWidth();
+        float viewHeight = getHeight();
+
+        float w = bitmap.getWidth();
+        float h = bitmap.getHeight();
+        matrix.reset();
+
+        // We limit up-scaling to 2x otherwise the result may look bad if it's
+        // a small icon.
+        float widthScale = Math.min(viewWidth / w, 2.0f);
+        float heightScale = Math.min(viewHeight / h, 2.0f);
+        float scale = Math.min(widthScale, heightScale);
+
+        matrix.postConcat(bitmap.getRotateMatrix());
+        matrix.postScale(scale, scale);
+
+        matrix.postTranslate(
+                (viewWidth  - w * scale) / 2F,
+                (viewHeight - h * scale) / 2F);
+    }
+
+    // Combine the base matrix and the supp matrix to make the final matrix.
+    protected Matrix getImageViewMatrix() {
+        // The final matrix is computed as the concatentation of the base matrix
+        // and the supplementary matrix.
+        mDisplayMatrix.set(mBaseMatrix);
+        mDisplayMatrix.postConcat(mSuppMatrix);
+        return mDisplayMatrix;
+    }
+
+    static final float SCALE_RATE = 1.25F;
+
+    // Sets the maximum zoom, which is a scale relative to the base matrix. It
+    // is calculated to show the image at 400% zoom regardless of screen or
+    // image orientation. If in the future we decode the full 3 megapixel image,
+    // rather than the current 1024x768, this should be changed down to 200%.
+    protected float maxZoom() {
+        if (mBitmapDisplayed.getBitmap() == null) {
+            return 1F;
+        }
+
+        float fw = (float) mBitmapDisplayed.getWidth()  / (float) mThisWidth;
+        float fh = (float) mBitmapDisplayed.getHeight() / (float) mThisHeight;
+        float max = Math.max(fw, fh) * 4;
+        return max;
+    }
+
+    protected void zoomTo(float scale, float centerX, float centerY) {
+        if (scale > mMaxZoom) {
+            scale = mMaxZoom;
+        }
+
+        float oldScale = getScale();
+        float deltaScale = scale / oldScale;
+
+        mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
+        setImageMatrix(getImageViewMatrix());
+        center(true, true);
+    }
+
+    protected void zoomTo(final float scale, final float centerX,
+                          final float centerY, final float durationMs) {
+        final float incrementPerMs = (scale - getScale()) / durationMs;
+        final float oldScale = getScale();
+        final long startTime = System.currentTimeMillis();
+
+        mHandler.post(new Runnable() {
+            public void run() {
+                long now = System.currentTimeMillis();
+                float currentMs = Math.min(durationMs, now - startTime);
+                float target = oldScale + (incrementPerMs * currentMs);
+                zoomTo(target, centerX, centerY);
+
+                if (currentMs < durationMs) {
+                    mHandler.post(this);
+                }
+            }
+        });
+    }
+
+    protected void zoomTo(float scale) {
+        float cx = getWidth() / 2F;
+        float cy = getHeight() / 2F;
+
+        zoomTo(scale, cx, cy);
+    }
+
+    protected void zoomIn() {
+        zoomIn(SCALE_RATE);
+    }
+
+    protected void zoomOut() {
+        zoomOut(SCALE_RATE);
+    }
+
+    protected void zoomIn(float rate) {
+        if (getScale() >= mMaxZoom) {
+            return;     // Don't let the user zoom into the molecular level.
+        }
+        if (mBitmapDisplayed.getBitmap() == null) {
+            return;
+        }
+
+        float cx = getWidth() / 2F;
+        float cy = getHeight() / 2F;
+
+        mSuppMatrix.postScale(rate, rate, cx, cy);
+        setImageMatrix(getImageViewMatrix());
+    }
+
+    protected void zoomOut(float rate) {
+        if (mBitmapDisplayed.getBitmap() == null) {
+            return;
+        }
+
+        float cx = getWidth() / 2F;
+        float cy = getHeight() / 2F;
+
+        // Zoom out to at most 1x.
+        Matrix tmp = new Matrix(mSuppMatrix);
+        tmp.postScale(1F / rate, 1F / rate, cx, cy);
+
+        if (getScale(tmp) < 1F) {
+            mSuppMatrix.setScale(1F, 1F, cx, cy);
+        } else {
+            mSuppMatrix.postScale(1F / rate, 1F / rate, cx, cy);
+        }
+        setImageMatrix(getImageViewMatrix());
+        center(true, true);
+    }
+
+    protected void postTranslate(float dx, float dy) {
+        mSuppMatrix.postTranslate(dx, dy);
+    }
+
+    protected void panBy(float dx, float dy) {
+        postTranslate(dx, dy);
+        setImageMatrix(getImageViewMatrix());
+    }
+}
diff --git a/src/com/cooliris/media/IndexRange.java b/src/com/cooliris/media/IndexRange.java
new file mode 100644
index 0000000..fe1a3f7
--- /dev/null
+++ b/src/com/cooliris/media/IndexRange.java
@@ -0,0 +1,29 @@
+package com.cooliris.media;
+
+public final class IndexRange {
+    public IndexRange(int beginRange, int endRange) {
+        begin = beginRange;
+        end = endRange;
+    }
+
+    public IndexRange() {
+        begin = 0;
+        end = 0;
+    }
+
+    public void set(int begin, int end) {
+        this.begin = begin;
+        this.end = end;
+    }
+
+    public boolean isEmpty() {
+        return begin == end;
+    }
+
+    public int size() {
+        return end - begin;
+    }
+
+    public int begin;
+    public int end;
+}
diff --git a/src/com/cooliris/media/IndexRangeIterator.java b/src/com/cooliris/media/IndexRangeIterator.java
new file mode 100644
index 0000000..e373c1d
--- /dev/null
+++ b/src/com/cooliris/media/IndexRangeIterator.java
@@ -0,0 +1,57 @@
+package com.cooliris.media;
+
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+
+public class IndexRangeIterator<E> implements Iterator<E> {
+    private final E[] mList;
+    private IndexRange mRange;
+    private int mPos;
+
+    public IndexRangeIterator(E[] list) {
+        super();
+        mList = list;
+        mRange = new IndexRange();
+        mPos = mRange.begin - 1;
+    }
+
+    public void rewind() {
+        mPos = mRange.begin - 1;
+    }
+
+    public void setRange(int begin, int end) {
+        mRange.begin = begin;
+        mRange.end = end;
+        mPos = begin - 1;
+    }
+
+    public int getBegin() {
+        return mRange.begin;
+    }
+
+    public int getEnd() {
+        return mRange.end;
+    }
+
+    public boolean hasNext() {
+        int pos = mPos + 1;
+        return (pos < mList.length && pos < mRange.end);
+    }
+
+    public int getCurrentPosition() {
+        return mPos;
+    }
+
+    public E next() {
+        // TODO Auto-generated method stub
+        int pos = mPos + 1;
+        ++mPos;
+        return mList[pos];
+    }
+
+    public void remove() {
+        // TODO Auto-generated method stub
+        throw new ConcurrentModificationException();
+    }
+
+}
diff --git a/src/com/cooliris/media/Layer.java b/src/com/cooliris/media/Layer.java
new file mode 100644
index 0000000..961f439
--- /dev/null
+++ b/src/com/cooliris/media/Layer.java
@@ -0,0 +1,84 @@
+package com.cooliris.media;
+
+import javax.microedition.khronos.opengles.GL11;
+
+import android.view.MotionEvent;
+
+public abstract class Layer {
+    float mX = 0f;
+    float mY = 0f;
+    float mWidth = 0;
+    float mHeight = 0;
+    boolean mHidden = false;
+
+    public final float getX() {
+        return mX;
+    }
+
+    public final float getY() {
+        return mY;
+    }
+
+    public final void setPosition(float x, float y) {
+        mX = x;
+        mY = y;
+    }
+
+    public final float getWidth() {
+        return mWidth;
+    }
+
+    public final float getHeight() {
+        return mHeight;
+    }
+
+    public final void setSize(float width, float height) {
+        if (mWidth != width || mHeight != height) {
+            mWidth = width;
+            mHeight = height;
+            onSizeChanged();
+        }
+    }
+
+    public boolean isHidden() {
+        return mHidden;
+    }
+
+    public void setHidden(boolean hidden) {
+        if (mHidden != hidden) {
+            mHidden = hidden;
+            onHiddenChanged();
+        }
+    }
+
+    public abstract void generate(RenderView view, RenderView.Lists lists);
+
+    // Returns true if something is animating.
+    public boolean update(RenderView view, float frameInterval) {
+        return false;
+    }
+
+    public void renderOpaque(RenderView view, GL11 gl) {
+    }
+
+    public void renderBlended(RenderView view, GL11 gl) {
+    }
+
+    public boolean onTouchEvent(MotionEvent event) {
+        return false;
+    }
+
+    // Allows subclasses to further constrain the hit test defined by layer bounds.
+    public boolean containsPoint(float x, float y) {
+        return true;
+    }
+
+    protected void onSurfaceCreated(RenderView view, GL11 gl) {
+    }
+
+    protected void onSizeChanged() {
+    }
+
+    protected void onHiddenChanged() {
+    }
+}
diff --git a/src/com/cooliris/media/LayoutInterface.java b/src/com/cooliris/media/LayoutInterface.java
new file mode 100644
index 0000000..81a847a
--- /dev/null
+++ b/src/com/cooliris/media/LayoutInterface.java
@@ -0,0 +1,9 @@
+package com.cooliris.media;
+
+public abstract class LayoutInterface {
+    public abstract void getPositionForSlotIndex(int displayIndex, int itemWidth, int itemHeight, Vector3f outPosition); // the
+                                                                                                                         // positions
+                                                                                                                         // of the
+                                                                                                                         // individual
+                                                                                                                         // slots
+}
diff --git a/src/com/cooliris/media/LoadingLayer.java b/src/com/cooliris/media/LoadingLayer.java
new file mode 100644
index 0000000..f24cd5c
--- /dev/null
+++ b/src/com/cooliris/media/LoadingLayer.java
@@ -0,0 +1,120 @@
+package com.cooliris.media;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.IntBuffer;
+
+import javax.microedition.khronos.opengles.GL11;
+import com.cooliris.media.R;
+
+import android.os.SystemClock;
+
+public final class LoadingLayer extends Layer {
+    private static final float FADE_INTERVAL = 0.5f;
+    private static final float GRAY_VALUE = 0.1f;
+    private static final int[] PRELOAD_RESOURCES_ASYNC_UNSCALED = {R.drawable.stack_frame, R.drawable.grid_frame,
+            R.drawable.stack_frame_focus, R.drawable.stack_frame_gold, R.drawable.btn_location_filter_unscaled, R.drawable.videooverlay,
+            R.drawable.grid_check_on, R.drawable.grid_check_off, R.drawable.icon_camera_small_unscaled, R.drawable.icon_picasa_small_unscaled};
+
+    private static final int[] PRELOAD_RESOURCES_ASYNC_SCALED = {/*R.drawable.btn_camera_pressed, R.drawable.btn_camera_focus,
+            R.drawable.fullscreen_hud_bg, R.drawable.icon_delete,
+            R.drawable.icon_edit, R.drawable.icon_more, R.drawable.icon_share, R.drawable.selection_bg_upper,
+            R.drawable.selection_menu_bg, R.drawable.selection_menu_bg_pressed, R.drawable.selection_menu_bg_pressed_left,
+            R.drawable.selection_menu_bg_pressed_right, R.drawable.selection_menu_divider, R.drawable.timebar_bg,
+            R.drawable.timebar_knob, R.drawable.timebar_knob_pressed, R.drawable.timebar_prev, R.drawable.timebar_next,
+            R.drawable.mode_grid, R.drawable.mode_stack, R.drawable.icon_camera_small, R.drawable.icon_location_small,
+            R.drawable.icon_picasa_small, R.drawable.icon_folder_small, R.drawable.scroller_new, R.drawable.scroller_pressed_new,
+            R.drawable.btn_camera, R.drawable.btn_play, R.drawable.pathbar_bg, R.drawable.pathbar_cap, R.drawable.pathbar_join,
+            R.drawable.transparent, R.drawable.icon_home_small, R.drawable.ic_fs_details,
+            R.drawable.ic_spinner1, R.drawable.ic_spinner2, R.drawable.ic_spinner3, R.drawable.ic_spinner4, R.drawable.ic_spinner5,
+            R.drawable.ic_spinner6, R.drawable.ic_spinner7, R.drawable.ic_spinner8*/};
+
+    private boolean mLoaded = false;
+    private final FloatAnim mOpacity = new FloatAnim(1f);
+    private IntBuffer mVertexBuffer;
+
+    public LoadingLayer() {
+        // Create vertex buffer for a screen-spanning quad.
+        int dimension = 10000 * 0x10000;
+        int[] vertices = { -dimension, -dimension, 0, dimension, -dimension, 0, -dimension, dimension, 0, dimension, dimension, 0 };
+        ByteBuffer vertexByteBuffer = ByteBuffer.allocateDirect(vertices.length * 4);
+        vertexByteBuffer.order(ByteOrder.nativeOrder());
+        mVertexBuffer = vertexByteBuffer.asIntBuffer();
+        mVertexBuffer.put(vertices);
+        mVertexBuffer.position(0);
+    }
+
+    public boolean isLoaded() {
+        return true;
+    }
+
+    @Override
+    public void generate(RenderView view, RenderView.Lists lists) {
+        // Add to drawing list.
+        lists.blendedList.add(this);
+
+        // Start loading textures.
+        int[] textures = PRELOAD_RESOURCES_ASYNC_UNSCALED;
+        for (int i = 0; i != textures.length; ++i) {
+            view.loadTexture(view.getResource(textures[i], false));
+        }
+        textures = PRELOAD_RESOURCES_ASYNC_SCALED;
+        for (int i = 0; i != textures.length; ++i) {
+            view.loadTexture(view.getResource(textures[i]));
+        }
+    }
+
+    @Override
+    public void renderBlended(RenderView view, GL11 gl) {
+        // Wait for textures to finish loading before fading out.
+        if (!mLoaded) {
+            // Request that the view upload all loaded textures.
+            view.processAllTextures();
+
+            // Determine if all textures have loaded.
+            int[] textures = PRELOAD_RESOURCES_ASYNC_SCALED;
+            boolean complete = true;
+            for (int i = 0; i != textures.length; ++i) {
+                if (view.getResource(textures[i]).mState != Texture.STATE_LOADED) {
+                    complete = false;
+                    break;
+                }
+            }
+            textures = PRELOAD_RESOURCES_ASYNC_UNSCALED;
+            for (int i = 0; i != textures.length; ++i) {
+                if (view.getResource(textures[i], false).mState != Texture.STATE_LOADED) {
+                    complete = false;
+                    break;
+                }
+            }
+            if (complete) {
+                mLoaded = true;
+                mOpacity.animateValue(0f, FADE_INTERVAL, SystemClock.uptimeMillis());
+            }
+        }
+
+        // Draw the loading screen.
+        float alpha = mOpacity.getValue(view.getFrameTime());
+        if (alpha > 0.004f) {
+            float gray = GRAY_VALUE * alpha;
+            gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
+            gl.glColor4f(gray, gray, gray, alpha);
+            gl.glVertexPointer(3, GL11.GL_FIXED, 0, mVertexBuffer);
+            gl.glDisable(GL11.GL_TEXTURE_2D);
+            gl.glDisable(GL11.GL_DEPTH_TEST);
+            gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, 0, 4);
+            gl.glEnable(GL11.GL_DEPTH_TEST);
+            gl.glEnable(GL11.GL_TEXTURE_2D);
+            view.resetColor();
+        } else {
+            // Hide the layer once completely faded out.
+            setHidden(true);
+        }
+    }
+
+    void reset() {
+        mLoaded = false;
+        mOpacity.setValue(1.0f);
+        setHidden(false);
+    }
+}
diff --git a/src/com/cooliris/media/LocalDataSource.java b/src/com/cooliris/media/LocalDataSource.java
new file mode 100644
index 0000000..87fde3b
--- /dev/null
+++ b/src/com/cooliris/media/LocalDataSource.java
@@ -0,0 +1,282 @@
+package com.cooliris.media;
+
+import java.io.File;
+import java.net.URI;
+import java.util.ArrayList;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+import android.util.Log;
+
+import com.cooliris.cache.CacheService;
+
+public final class LocalDataSource implements DataSource {
+    private static final String TAG = "LocalDataSource";
+
+    public static final DiskCache sThumbnailCache = new DiskCache("local-image-thumbs");
+    public static final DiskCache sThumbnailCacheVideo = new DiskCache("local-video-thumbs");
+
+    public static final String CAMERA_STRING = "Camera";
+    public static final String DOWNLOAD_STRING = "download";
+    public static final String CAMERA_BUCKET_NAME = Environment.getExternalStorageDirectory().toString() + "/DCIM/" + CAMERA_STRING;
+    public static final String DOWNLOAD_BUCKET_NAME = Environment.getExternalStorageDirectory().toString() + "/" + DOWNLOAD_STRING;
+    public static final int CAMERA_BUCKET_ID = getBucketId(CAMERA_BUCKET_NAME);
+    public static final int DOWNLOAD_BUCKET_ID = getBucketId(DOWNLOAD_BUCKET_NAME);
+    private boolean mDisableImages;
+    private boolean mDisableVideos;
+
+    /**
+     * Matches code in MediaProvider.computeBucketValues. Should be a common function.
+     */
+    public static int getBucketId(String path) {
+        return (path.toLowerCase().hashCode());
+    }
+
+    private Context mContext;
+    private ContentObserver mImagesObserver;
+    private ContentObserver mVideosObserver;
+
+    public LocalDataSource(Context context) {
+        mContext = context;
+    }
+
+    public void setMimeFilter(boolean disableImages, boolean disableVideos) {
+        mDisableImages = disableImages;
+        mDisableVideos = disableVideos;
+    }
+
+    public void loadMediaSets(final MediaFeed feed) {
+        if (mContext == null) {
+            return;
+        }
+        stopListeners();
+        CacheService.loadMediaSets(feed, this, !mDisableImages, !mDisableVideos);
+        Handler handler = ((Gallery) mContext).getHandler();
+        ContentObserver imagesObserver = new ContentObserver(handler) {
+            public void onChange(boolean selfChange) {
+                if (((Gallery) mContext).isPaused()) {
+                    refresh(feed, CAMERA_BUCKET_ID);
+                    refresh(feed, DOWNLOAD_BUCKET_ID);
+
+                    MediaSet set = feed.getCurrentSet();
+                    if (set != null && set.mPicasaAlbumId == Shared.INVALID) {
+                        refresh(feed, set.mId);
+                    }
+                }
+            }
+        };
+        ContentObserver videosObserver = new ContentObserver(handler) {
+            public void onChange(boolean selfChange) {
+                if (((Gallery) mContext).isPaused()) {
+                    refresh(feed, CAMERA_BUCKET_ID);
+                }
+            }
+        };
+
+        // Start listening. TODO: coalesce update notifications while mediascanner is active.
+        Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
+        Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
+        ContentResolver cr = mContext.getContentResolver();
+        mImagesObserver = imagesObserver;
+        mVideosObserver = videosObserver;
+        cr.registerContentObserver(uriImages, false, mImagesObserver);
+        cr.registerContentObserver(uriVideos, false, mVideosObserver);
+    }
+
+    public void shutdown() {
+    }
+    
+    private void stopListeners() {
+        ContentResolver cr = mContext.getContentResolver();
+        if (mImagesObserver != null) {
+            cr.unregisterContentObserver(mImagesObserver);
+        }
+        if (mVideosObserver != null) {
+            cr.unregisterContentObserver(mVideosObserver);
+        }
+    }
+
+    protected void refresh(MediaFeed feed, long setIdToUse) {
+        if (setIdToUse == Shared.INVALID) {
+            return;
+        }
+        Log.i(TAG, "Refreshing local data source");
+        Gallery.NEEDS_REFRESH = true;
+        if (feed.getMediaSet(setIdToUse) == null) {
+            Log.i(TAG, "We check to see if there are any items with this bucket id in the database.");
+            if (!CacheService.setHasItems(mContext.getContentResolver(), setIdToUse))
+                return;
+            MediaSet mediaSet = feed.addMediaSet(setIdToUse, this);
+            if (setIdToUse == CAMERA_BUCKET_ID) {
+                mediaSet.mName = CAMERA_STRING;
+            } else if (setIdToUse == DOWNLOAD_BUCKET_ID) {
+                mediaSet.mName = DOWNLOAD_STRING;
+            }
+            mediaSet.generateTitle(true);
+            if (!CacheService.isPresentInCache(setIdToUse))
+                CacheService.markDirty(mContext);
+        } else {
+            MediaSet mediaSet = feed.replaceMediaSet(setIdToUse, this);
+            if (setIdToUse == CAMERA_BUCKET_ID) {
+                mediaSet.mName = CAMERA_STRING;
+            } else if (setIdToUse == DOWNLOAD_BUCKET_ID) {
+                mediaSet.mName = DOWNLOAD_STRING;
+            }
+            mediaSet.generateTitle(true);
+            CacheService.markDirty(mContext, setIdToUse);
+        }
+    }
+
+    public void loadItemsForSet(final MediaFeed feed, final MediaSet parentSet, int rangeStart, int rangeEnd) {
+        // Quick load from the cache.
+        if (mContext == null || parentSet == null) {
+            return;
+        }
+        loadMediaItemsIntoMediaFeed(feed, parentSet, rangeStart, rangeEnd);
+    }
+
+    private void loadMediaItemsIntoMediaFeed(final MediaFeed mediaFeed, final MediaSet set, int rangeStart, int rangeEnd) {
+        if (rangeEnd - rangeStart < 0) {
+            return;
+        }
+        CacheService.loadMediaItemsIntoMediaFeed(mediaFeed, set, rangeStart, rangeEnd, !mDisableImages, !mDisableVideos);
+        if (set.mId == CAMERA_BUCKET_ID && set.mNumItemsLoaded > 0) {
+            mediaFeed.moveSetToFront(set);
+        }
+    }
+
+    public boolean performOperation(final int operation, final ArrayList<MediaBucket> mediaBuckets, final Object data) {
+        int numBuckets = mediaBuckets.size();
+        ContentResolver cr = mContext.getContentResolver();
+        switch (operation) {
+        case MediaFeed.OPERATION_DELETE:
+            for (int i = 0; i < numBuckets; ++i) {
+                MediaBucket bucket = mediaBuckets.get(i);
+                MediaSet set = bucket.mediaSet;
+                ArrayList<MediaItem> items = bucket.mediaItems;
+                if (set != null && items == null) {
+                    // Remove the entire bucket.
+                    final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
+                    final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
+                    final String whereImages = Images.ImageColumns.BUCKET_ID + "=" + Long.toString(set.mId);
+                    final String whereVideos = Video.VideoColumns.BUCKET_ID + "=" + Long.toString(set.mId);
+                    cr.delete(uriImages, whereImages, null);
+                    cr.delete(uriVideos, whereVideos, null);
+                    CacheService.markDirty(mContext);
+                }
+                if (set != null && items != null) {
+                    // We need to remove these items from the set.
+                    int numItems = items.size();
+                    for (int j = 0; j < numItems; ++j) {
+                        MediaItem item = items.get(j);
+                        cr.delete(Uri.parse(item.mContentUri), null, null);
+                    }
+                    set.updateNumExpectedItems();
+                    set.generateTitle(true);
+                    CacheService.markDirty(mContext, set.mId);
+                }
+            }
+            break;
+        case MediaFeed.OPERATION_ROTATE:
+            for (int i = 0; i < numBuckets; ++i) {
+                MediaBucket bucket = mediaBuckets.get(i);
+                ArrayList<MediaItem> items = bucket.mediaItems;
+                if (items == null) {
+                    continue;
+                }
+                float angleToRotate = ((Float) data).floatValue();
+                if (angleToRotate == 0) {
+                    return true;
+                }
+                int numItems = items.size();
+                for (int j = 0; j < numItems; ++j) {
+                    rotateItem(items.get(j), angleToRotate);
+                }
+            }
+            break;
+        }
+        return true;
+    }
+
+    private void rotateItem(final MediaItem item, float angleToRotate) {
+        ContentResolver cr = mContext.getContentResolver();
+        try {
+            int currentOrientation = (int) item.mRotation;
+            angleToRotate += currentOrientation;
+            float rotation = Shared.normalizePositive(angleToRotate);
+            String rotationString = Integer.toString((int) rotation);
+
+            // Update the database entry.
+            ContentValues values = new ContentValues();
+            values.put(Images.ImageColumns.ORIENTATION, rotationString);
+            cr.update(Uri.parse(item.mContentUri), values, null, null);
+
+            // Update the file EXIF information.
+            Uri uri = Uri.parse(item.mContentUri);
+            String uriScheme = uri.getScheme();
+            if (uriScheme.equals("file")) {
+                ExifInterface exif = new ExifInterface(uri.getPath());
+                exif.setAttribute(ExifInterface.TAG_ORIENTATION, Integer.toString(Shared.degreesToExifOrientation(rotation)));
+                exif.saveAttributes();
+            }
+
+            // Invalidate the cache entry.
+            CacheService.markDirty(mContext, item.mParentMediaSet.mId);
+
+            // Update the object representation of the item.
+            item.mRotation = rotation;
+        } catch (Exception e) {
+            // System.out.println("Apparently not a JPEG");
+        }
+    }
+
+    public DiskCache getThumbnailCache() {
+        return sThumbnailCache;
+    }
+
+    public static MediaItem createMediaItemFromUri(Context context, Uri target) {
+        MediaItem item = null;
+        long id = ContentUris.parseId(target);
+        ContentResolver cr = context.getContentResolver();
+        String whereClause = Images.ImageColumns._ID + "=" + Long.toString(id);
+        Cursor cursor = cr.query(Images.Media.EXTERNAL_CONTENT_URI, CacheService.PROJECTION_IMAGES, whereClause, null, null);
+        if (cursor != null) {
+            if (cursor.moveToFirst()) {
+                item = new MediaItem();
+                CacheService.populateMediaItemFromCursor(item, cr, cursor, Images.Media.EXTERNAL_CONTENT_URI.toString() + "/");
+            }
+            cursor.close();
+            cursor = null;
+        }
+        return item;
+    }
+
+    public static MediaItem createMediaItemFromFileUri(Context context, String fileUri) {
+        MediaItem item = null;
+        String filepath = new File(URI.create(fileUri)).toString();
+        ContentResolver cr = context.getContentResolver();
+        long bucketId = SingleDataSource.parseBucketIdFromFileUri(fileUri);
+        String whereClause = Images.ImageColumns.BUCKET_ID + "=" + bucketId + " AND " + Images.ImageColumns.DATA + "='" + filepath
+                + "'";
+        Cursor cursor = cr.query(Images.Media.EXTERNAL_CONTENT_URI, CacheService.PROJECTION_IMAGES, whereClause, null, null);
+        if (cursor != null) {
+            if (cursor.moveToFirst()) {
+                item = new MediaItem();
+                CacheService.populateMediaItemFromCursor(item, cr, cursor, Images.Media.EXTERNAL_CONTENT_URI.toString() + "/");
+            }
+            cursor.close();
+            cursor = null;
+        }
+        return item;
+    }
+}
diff --git a/src/com/cooliris/media/LocationMediaFilter.java b/src/com/cooliris/media/LocationMediaFilter.java
new file mode 100644
index 0000000..5a5847e
--- /dev/null
+++ b/src/com/cooliris/media/LocationMediaFilter.java
@@ -0,0 +1,71 @@
+package com.cooliris.media;
+
+public class LocationMediaFilter extends MediaFilter {
+    private double mRadius;
+    private double mCenterLat;
+    private double mCenterLon;
+    public static final int EARTH_RADIUS_METERS = 6378137;
+    public static final int LAT_MIN = -90;
+    public static final int LAT_MAX = 90;
+    public static final int LON_MIN = -180;
+    public static final int LON_MAX = 180;
+
+    LocationMediaFilter(double centerLatitude, double centerLongitude, double thresholdRadius) {
+        mCenterLat = centerLatitude;
+        mCenterLon = centerLongitude;
+        mRadius = thresholdRadius;
+    }
+
+    LocationMediaFilter(double latitude1, double longitude1, double latitude2, double longitude2) {
+        mCenterLat = centerLat(latitude1, latitude2);
+        mCenterLon = centerLon(longitude1, longitude2);
+        mRadius = distanceBetween(latitude1, longitude1, latitude2, longitude2);
+    }
+
+    public static final double centerLat(double lat1, double lat2) {
+        return (centerOfAngles(lat1, lat2, LAT_MAX));
+    }
+
+    public static final double centerLon(double lon1, double lon2) {
+        return (centerOfAngles(lon1, lon2, LON_MAX));
+    }
+
+    private static final double centerOfAngles(double ang1, double ang2, int wrapAroundThreshold) {
+        boolean wrapAround = false;
+        if (Math.abs(ang1 - ang2) > wrapAroundThreshold) {
+            wrapAround = true;
+        }
+        double center = (ang1 + ang2) * 0.5;
+        if (wrapAround) {
+            center += wrapAroundThreshold;
+            center %= wrapAroundThreshold;
+        }
+        return center;
+    }
+
+    public static double distanceBetween(double lat1, double lon1, double lat2, double lon2) {
+        double dLat = Math.toRadians(lat2 - lat1);
+        double dLon = Math.toRadians(lon2 - lon1);
+        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
+                   * Math.sin(dLon / 2) * Math.sin(dLon / 2);
+        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+        return EARTH_RADIUS_METERS * c;
+    }
+
+    public static final double toKm(double meter) {
+        return meter / 1000;
+    }
+
+    public static final double toMile(double meter) {
+        return meter / 1609;
+    }
+
+    @Override
+    public boolean pass(MediaItem item) {
+        double radius = distanceBetween(mCenterLat, mCenterLon, item.mLatitude, item.mLongitude);
+        if (radius <= mRadius) {
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/src/com/cooliris/media/LongSparseArray.java b/src/com/cooliris/media/LongSparseArray.java
new file mode 100644
index 0000000..7145487
--- /dev/null
+++ b/src/com/cooliris/media/LongSparseArray.java
@@ -0,0 +1,451 @@
+/*
+ * Copyright (C) 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.
+ * 
+ * NOTE(aws): Adapted into the project from the repository since this is a non-public class.
+ */
+
+package com.cooliris.media;
+
+import java.lang.reflect.Array;
+
+import android.util.Log;
+
+/**
+ * SparseArrays map longs to Objects. Unlike a normal array of Objects, there can be gaps in the indices. It is intended to be more
+ * efficient than using a HashMap to map Longs to Objects.
+ * 
+ * @hide
+ */
+public final class LongSparseArray<E> {
+    private static final Object DELETED = new Object();
+    private boolean mGarbage = false;
+
+    /**
+     * Creates a new SparseArray containing no mappings.
+     */
+    public LongSparseArray() {
+        this(10);
+    }
+
+    /**
+     * Creates a new SparseArray containing no mappings that will not require any additional memory allocation to store the
+     * specified number of mappings.
+     */
+    public LongSparseArray(int initialCapacity) {
+        initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);
+
+        mKeys = new long[initialCapacity];
+        mValues = new Object[initialCapacity];
+        mSize = 0;
+    }
+
+    /**
+     * Gets the Object mapped from the specified key, or <code>null</code> if no such mapping has been made.
+     */
+    public E get(long key) {
+        return get(key, null);
+    }
+
+    /**
+     * Gets the Object mapped from the specified key, or the specified Object if no such mapping has been made.
+     */
+    @SuppressWarnings("unchecked")
+    public E get(long key, E valueIfKeyNotFound) {
+        int i = binarySearch(mKeys, 0, mSize, key);
+
+        if (i < 0 || mValues[i] == DELETED) {
+            return valueIfKeyNotFound;
+        } else {
+            return (E) mValues[i];
+        }
+    }
+
+    /**
+     * Removes the mapping from the specified key, if there was any.
+     */
+    public void delete(long key) {
+        int i = binarySearch(mKeys, 0, mSize, key);
+
+        if (i >= 0) {
+            if (mValues[i] != DELETED) {
+                mValues[i] = DELETED;
+                mGarbage = true;
+            }
+        }
+    }
+
+    /**
+     * Alias for {@link #delete(long)}.
+     */
+    public void remove(long key) {
+        delete(key);
+    }
+
+    private void gc() {
+        // Log.e("SparseArray", "gc start with " + mSize);
+
+        int n = mSize;
+        int o = 0;
+        long[] keys = mKeys;
+        Object[] values = mValues;
+
+        for (int i = 0; i < n; i++) {
+            Object val = values[i];
+
+            if (val != DELETED) {
+                if (i != o) {
+                    keys[o] = keys[i];
+                    values[o] = val;
+                }
+
+                o++;
+            }
+        }
+
+        mGarbage = false;
+        mSize = o;
+
+        // Log.e("SparseArray", "gc end with " + mSize);
+    }
+
+    /**
+     * Adds a mapping from the specified key to the specified value, replacing the previous mapping from the specified key if there
+     * was one.
+     */
+    public void put(long key, E value) {
+        int i = binarySearch(mKeys, 0, mSize, key);
+
+        if (i >= 0) {
+            mValues[i] = value;
+        } else {
+            i = ~i;
+
+            if (i < mSize && mValues[i] == DELETED) {
+                mKeys[i] = key;
+                mValues[i] = value;
+                return;
+            }
+
+            if (mGarbage && mSize >= mKeys.length) {
+                gc();
+
+                // Search again because indices may have changed.
+                i = ~binarySearch(mKeys, 0, mSize, key);
+            }
+
+            if (mSize >= mKeys.length) {
+                int n = ArrayUtils.idealIntArraySize(mSize + 1);
+
+                long[] nkeys = new long[n];
+                Object[] nvalues = new Object[n];
+
+                // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
+                System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+                System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+                mKeys = nkeys;
+                mValues = nvalues;
+            }
+
+            if (mSize - i != 0) {
+                // Log.e("SparseArray", "move " + (mSize - i));
+                System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+                System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+            }
+
+            mKeys[i] = key;
+            mValues[i] = value;
+            mSize++;
+        }
+    }
+
+    /**
+     * Returns the number of key-value mappings that this SparseArray currently stores.
+     */
+    public int size() {
+        if (mGarbage) {
+            gc();
+        }
+
+        return mSize;
+    }
+
+    /**
+     * Given an index in the range <code>0...size()-1</code>, returns the key from the <code>index</code>th key-value mapping that
+     * this SparseArray stores.
+     */
+    public long keyAt(int index) {
+        if (mGarbage) {
+            gc();
+        }
+
+        return mKeys[index];
+    }
+
+    /**
+     * Given an index in the range <code>0...size()-1</code>, returns the value from the <code>index</code>th key-value mapping that
+     * this SparseArray stores.
+     */
+    @SuppressWarnings("unchecked")
+    public E valueAt(int index) {
+        if (mGarbage) {
+            gc();
+        }
+
+        return (E) mValues[index];
+    }
+
+    /**
+     * Given an index in the range <code>0...size()-1</code>, sets a new value for the <code>index</code>th key-value mapping that
+     * this SparseArray stores.
+     */
+    public void setValueAt(int index, E value) {
+        if (mGarbage) {
+            gc();
+        }
+
+        mValues[index] = value;
+    }
+
+    /**
+     * Returns the index for which {@link #keyAt} would return the specified key, or a negative number if the specified key is not
+     * mapped.
+     */
+    public int indexOfKey(long key) {
+        if (mGarbage) {
+            gc();
+        }
+
+        return binarySearch(mKeys, 0, mSize, key);
+    }
+
+    /**
+     * Returns an index for which {@link #valueAt} would return the specified key, or a negative number if no keys map to the
+     * specified value. Beware that this is a linear search, unlike lookups by key, and that multiple keys can map to the same value
+     * and this will find only one of them.
+     */
+    public int indexOfValue(E value) {
+        if (mGarbage) {
+            gc();
+        }
+
+        for (int i = 0; i < mSize; i++)
+            if (mValues[i] == value)
+                return i;
+
+        return -1;
+    }
+
+    /**
+     * Removes all key-value mappings from this SparseArray.
+     */
+    public void clear() {
+        int n = mSize;
+        Object[] values = mValues;
+
+        for (int i = 0; i < n; i++) {
+            values[i] = null;
+        }
+
+        mSize = 0;
+        mGarbage = false;
+    }
+
+    /**
+     * Puts a key/value pair into the array, optimizing for the case where the key is greater than all existing keys in the array.
+     */
+    public void append(long key, E value) {
+        if (mSize != 0 && key <= mKeys[mSize - 1]) {
+            put(key, value);
+            return;
+        }
+
+        if (mGarbage && mSize >= mKeys.length) {
+            gc();
+        }
+
+        int pos = mSize;
+        if (pos >= mKeys.length) {
+            int n = ArrayUtils.idealIntArraySize(pos + 1);
+
+            long[] nkeys = new long[n];
+            Object[] nvalues = new Object[n];
+
+            // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
+            System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+            System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+            mKeys = nkeys;
+            mValues = nvalues;
+        }
+
+        mKeys[pos] = key;
+        mValues[pos] = value;
+        mSize = pos + 1;
+    }
+
+    private static int binarySearch(long[] a, int start, int len, long key) {
+        int high = start + len, low = start - 1, guess;
+
+        while (high - low > 1) {
+            guess = (high + low) / 2;
+
+            if (a[guess] < key)
+                low = guess;
+            else
+                high = guess;
+        }
+
+        if (high == start + len)
+            return ~(start + len);
+        else if (a[high] == key)
+            return high;
+        else
+            return ~high;
+    }
+
+    @SuppressWarnings("unused")
+    private void checkIntegrity() {
+        for (int i = 1; i < mSize; i++) {
+            if (mKeys[i] <= mKeys[i - 1]) {
+                for (int j = 0; j < mSize; j++) {
+                    Log.e("FAIL", j + ": " + mKeys[j] + " -> " + mValues[j]);
+                }
+
+                throw new RuntimeException();
+            }
+        }
+    }
+
+    private long[] mKeys;
+    private Object[] mValues;
+    private int mSize;
+
+    public static final class ArrayUtils {
+        private static Object[] EMPTY = new Object[0];
+        private static final int CACHE_SIZE = 73;
+        private static Object[] sCache = new Object[CACHE_SIZE];
+
+        private ArrayUtils() { /* cannot be instantiated */
+        }
+
+        public static int idealByteArraySize(int need) {
+            for (int i = 4; i < 32; i++)
+                if (need <= (1 << i) - 12)
+                    return (1 << i) - 12;
+
+            return need;
+        }
+
+        public static int idealBooleanArraySize(int need) {
+            return idealByteArraySize(need);
+        }
+
+        public static int idealShortArraySize(int need) {
+            return idealByteArraySize(need * 2) / 2;
+        }
+
+        public static int idealCharArraySize(int need) {
+            return idealByteArraySize(need * 2) / 2;
+        }
+
+        public static int idealIntArraySize(int need) {
+            return idealByteArraySize(need * 4) / 4;
+        }
+
+        public static int idealFloatArraySize(int need) {
+            return idealByteArraySize(need * 4) / 4;
+        }
+
+        public static int idealObjectArraySize(int need) {
+            return idealByteArraySize(need * 4) / 4;
+        }
+
+        public static int idealLongArraySize(int need) {
+            return idealByteArraySize(need * 8) / 8;
+        }
+
+        /**
+         * Checks if the beginnings of two byte arrays are equal.
+         * 
+         * @param array1
+         *            the first byte array
+         * @param array2
+         *            the second byte array
+         * @param length
+         *            the number of bytes to check
+         * @return true if they're equal, false otherwise
+         */
+        public static boolean equals(byte[] array1, byte[] array2, int length) {
+            if (array1 == array2) {
+                return true;
+            }
+            if (array1 == null || array2 == null || array1.length < length || array2.length < length) {
+                return false;
+            }
+            for (int i = 0; i < length; i++) {
+                if (array1[i] != array2[i]) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * Returns an empty array of the specified type. The intent is that it will return the same empty array every time to avoid
+         * reallocation, although this is not guaranteed.
+         */
+        @SuppressWarnings("unchecked")
+        public static <T> T[] emptyArray(Class<T> kind) {
+            if (kind == Object.class) {
+                return (T[]) EMPTY;
+            }
+
+            int bucket = ((System.identityHashCode(kind) / 8) & 0x7FFFFFFF) % CACHE_SIZE;
+            Object cache = sCache[bucket];
+
+            if (cache == null || cache.getClass().getComponentType() != kind) {
+                cache = Array.newInstance(kind, 0);
+                sCache[bucket] = cache;
+
+                // Log.e("cache", "new empty " + kind.getName() + " at " + bucket);
+            }
+
+            return (T[]) cache;
+        }
+
+        /**
+         * Checks that value is present as at least one of the elements of the array.
+         * 
+         * @param array
+         *            the array to check in
+         * @param value
+         *            the value to check for
+         * @return true if the value is present in the array
+         */
+        public static <T> boolean contains(T[] array, T value) {
+            for (T element : array) {
+                if (element == null) {
+                    if (value == null)
+                        return true;
+                } else {
+                    if (value != null && element.equals(value))
+                        return true;
+                }
+            }
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/cooliris/media/MatrixStack.java b/src/com/cooliris/media/MatrixStack.java
new file mode 100644
index 0000000..69d7907
--- /dev/null
+++ b/src/com/cooliris/media/MatrixStack.java
@@ -0,0 +1,184 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 2007 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.
+ */
+
+import android.opengl.Matrix;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+
+/**
+ * A matrix stack, similar to OpenGL ES's internal matrix stack.
+ */
+public class MatrixStack {
+    public MatrixStack() {
+        commonInit(DEFAULT_MAX_DEPTH);
+    }
+
+    public MatrixStack(int maxDepth) {
+        commonInit(maxDepth);
+    }
+
+    private void commonInit(int maxDepth) {
+        mMatrix = new float[maxDepth * MATRIX_SIZE];
+        mTemp = new float[MATRIX_SIZE * 2];
+        glLoadIdentity();
+    }
+    
+    public void apply(float[] mCoordsIn, float[] mCoordsOut) {
+        Matrix.multiplyMV(mCoordsOut, 0, mMatrix, mTop, mCoordsIn, 0);
+    }
+
+    public void glFrustumf(float left, float right, float bottom, float top,
+            float near, float far) {
+        Matrix.frustumM(mMatrix, mTop, left, right, bottom, top, near, far);
+    }
+
+    public void glFrustumx(int left, int right, int bottom, int top, int near,
+            int far) {
+        glFrustumf(fixedToFloat(left),fixedToFloat(right),
+                fixedToFloat(bottom), fixedToFloat(top),
+                fixedToFloat(near), fixedToFloat(far));
+    }
+
+    public void glLoadIdentity() {
+        Matrix.setIdentityM(mMatrix, mTop);
+    }
+
+    public void glLoadMatrixf(float[] m, int offset) {
+        System.arraycopy(m, offset, mMatrix, mTop, MATRIX_SIZE);
+    }
+
+    public void glLoadMatrixf(FloatBuffer m) {
+        m.get(mMatrix, mTop, MATRIX_SIZE);
+    }
+
+    public void glLoadMatrixx(int[] m, int offset) {
+        for(int i = 0; i < MATRIX_SIZE; i++) {
+            mMatrix[mTop + i] = fixedToFloat(m[offset + i]);
+        }
+    }
+
+    public void glLoadMatrixx(IntBuffer m) {
+        for(int i = 0; i < MATRIX_SIZE; i++) {
+            mMatrix[mTop + i] = fixedToFloat(m.get());
+        }
+    }
+
+    public void glMultMatrixf(float[] m, int offset) {
+        System.arraycopy(mMatrix, mTop, mTemp, 0, MATRIX_SIZE);
+        Matrix.multiplyMM(mMatrix, mTop, mTemp, 0, m, offset);
+    }
+
+    public void glMultMatrixf(FloatBuffer m) {
+        m.get(mTemp, MATRIX_SIZE, MATRIX_SIZE);
+        glMultMatrixf(mTemp, MATRIX_SIZE);
+    }
+
+    public void glMultMatrixx(int[] m, int offset) {
+        for(int i = 0; i < MATRIX_SIZE; i++) {
+            mTemp[MATRIX_SIZE + i] = fixedToFloat(m[offset + i]);
+        }
+        glMultMatrixf(mTemp, MATRIX_SIZE);
+    }
+
+    public void glMultMatrixx(IntBuffer m) {
+        for(int i = 0; i < MATRIX_SIZE; i++) {
+            mTemp[MATRIX_SIZE + i] = fixedToFloat(m.get());
+        }
+        glMultMatrixf(mTemp, MATRIX_SIZE);
+    }
+
+    public void glOrthof(float left, float right, float bottom, float top,
+            float near, float far) {
+        Matrix.orthoM(mMatrix, mTop, left, right, bottom, top, near, far);
+    }
+
+    public void glOrthox(int left, int right, int bottom, int top, int near,
+            int far) {
+        glOrthof(fixedToFloat(left), fixedToFloat(right),
+                fixedToFloat(bottom), fixedToFloat(top),
+                fixedToFloat(near), fixedToFloat(far));
+    }
+
+    public void glPopMatrix() {
+        preflight_adjust(-1);
+        adjust(-1);
+    }
+
+    public void glPushMatrix() {
+        preflight_adjust(1);
+        System.arraycopy(mMatrix, mTop, mMatrix, mTop + MATRIX_SIZE,
+                MATRIX_SIZE);
+        adjust(1);
+    }
+
+    public void glRotatef(float angle, float x, float y, float z) {
+        Matrix.setRotateM(mTemp, 0, angle, x, y, z);
+        System.arraycopy(mMatrix, mTop, mTemp, MATRIX_SIZE, MATRIX_SIZE);
+        Matrix.multiplyMM(mMatrix, mTop, mTemp, MATRIX_SIZE, mTemp, 0);
+    }
+
+    public void glRotatex(int angle, int x, int y, int z) {
+        glRotatef(angle, fixedToFloat(x), fixedToFloat(y), fixedToFloat(z));
+    }
+
+    public void glScalef(float x, float y, float z) {
+        Matrix.scaleM(mMatrix, mTop, x, y, z);
+    }
+
+    public void glScalex(int x, int y, int z) {
+        glScalef(fixedToFloat(x), fixedToFloat(y), fixedToFloat(z));
+    }
+
+    public void glTranslatef(float x, float y, float z) {
+        Matrix.translateM(mMatrix, mTop, x, y, z);
+    }
+
+    public void glTranslatex(int x, int y, int z) {
+        glTranslatef(fixedToFloat(x), fixedToFloat(y), fixedToFloat(z));
+    }
+
+    public void getMatrix(float[] dest, int offset) {
+        System.arraycopy(mMatrix, mTop, dest, offset, MATRIX_SIZE);
+    }
+
+    private float fixedToFloat(int x) {
+        return x * (1.0f / 65536.0f);
+    }
+
+    private void preflight_adjust(int dir) {
+        int newTop = mTop + dir * MATRIX_SIZE;
+        if (newTop < 0) {
+            throw new IllegalArgumentException("stack underflow");
+        }
+        if (newTop + MATRIX_SIZE > mMatrix.length) {
+            throw new IllegalArgumentException("stack overflow");
+        }
+    }
+
+    private void adjust(int dir) {
+        mTop += dir * MATRIX_SIZE;
+    }
+
+    private final static int DEFAULT_MAX_DEPTH = 32;
+    private final static int MATRIX_SIZE = 16;
+    private float[] mMatrix;
+    private int mTop;
+    private float[] mTemp;
+}
+
diff --git a/src/com/cooliris/media/MediaBucket.java b/src/com/cooliris/media/MediaBucket.java
new file mode 100644
index 0000000..90df05a
--- /dev/null
+++ b/src/com/cooliris/media/MediaBucket.java
@@ -0,0 +1,8 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+
+public class MediaBucket {
+    public MediaSet mediaSet;
+    public ArrayList<MediaItem> mediaItems;
+}
diff --git a/src/com/cooliris/media/MediaBucketList.java b/src/com/cooliris/media/MediaBucketList.java
new file mode 100644
index 0000000..3e0f568
--- /dev/null
+++ b/src/com/cooliris/media/MediaBucketList.java
@@ -0,0 +1,256 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public final class MediaBucketList {
+    private static final Boolean TRUE = new Boolean(true);
+    private static final Boolean FALSE = new Boolean(false);
+
+    private ArrayList<MediaBucket> mBuckets = new ArrayList<MediaBucket>(1024);
+    private boolean mDirtyCount;
+    private boolean mDirtyAcceleratedLookup;
+    private int mCount;
+    private HashMap<MediaItem, Boolean> mCachedItems = new HashMap<MediaItem, Boolean>(1024);
+
+    // If only albums are selected, a bucket contains mediaSets.
+    // If items are selected, a bucket contains mediaSets and mediaItems.
+
+    // Returns the first item selection (ignoring items within set selections).
+    public static MediaItem getFirstItemSelection(ArrayList<MediaBucket> buckets) {
+        MediaItem item = null;
+        if (buckets != null) {
+            int numBuckets = buckets.size();
+            for (int i = 0; i < numBuckets; i++) {
+                MediaBucket bucket = buckets.get(0);
+                if (bucket != null && !isSetSelection(bucket)) {
+                    ArrayList<MediaItem> items = bucket.mediaItems;
+                    if (items != null && items.size() > 0) {
+                        item = items.get(0);
+                        break;
+                    }
+                }
+            }
+        }
+        return item;
+    }
+
+    // Returns the first set selection (ignoring sets corresponding to item selections).
+    public static MediaSet getFirstSetSelection(ArrayList<MediaBucket> buckets) {
+        MediaSet set = null;
+        if (buckets != null) {
+            int numBuckets = buckets.size();
+            for (int i = 0; i < numBuckets; i++) {
+                MediaBucket bucket = buckets.get(0);
+                if (bucket != null && isSetSelection(bucket)) {
+                    set = bucket.mediaSet;
+                }
+            }
+        }
+        return set;
+    }
+
+    public ArrayList<MediaBucket> get() {
+        return mBuckets;
+    }
+
+    public int size() {
+        if (mDirtyCount) {
+            ArrayList<MediaBucket> buckets = mBuckets;
+            int numBuckets = buckets.size();
+            int count = 0;
+            for (int i = 0; i < numBuckets; ++i) {
+                MediaBucket bucket = buckets.get(i);
+                int numItems = 0;
+                if (bucket.mediaItems == null && bucket.mediaSet != null) {
+                    numItems = bucket.mediaSet.getNumItems();
+                    // This selection reflects the bucket itself, and not the items inside the bucket (which is 0).
+                    if (numItems == 0) {
+                        numItems = 1;
+                    }
+                } else if (bucket.mediaItems != null && bucket.mediaItems != null) {
+                    numItems = bucket.mediaItems.size();
+                }
+                count += numItems;
+            }
+            mCount = count;
+            mDirtyCount = false;
+        }
+        return mCount;
+    }
+
+    public void add(int slotId, MediaFeed feed, boolean removeIfAlreadyAdded) {
+        if (slotId == Shared.INVALID) {
+            return;
+        }
+        setDirty();
+        final ArrayList<MediaBucket> selectedBuckets = mBuckets;
+        final int numSelectedBuckets = selectedBuckets.size();
+        MediaSet mediaSetToAdd = null;
+        ArrayList<MediaItem> selectedItems = null;
+        MediaBucket bucket = null;
+        final boolean hasExpandedMediaSet = feed.hasExpandedMediaSet();
+        if (!hasExpandedMediaSet) {
+            ArrayList<MediaSet> mediaSets = feed.getMediaSets();
+            if (slotId >= mediaSets.size()) {
+                return;
+            }
+            mediaSetToAdd = mediaSets.get(slotId);
+        } else {
+            int numSlots = feed.getNumSlots();
+            if (slotId < numSlots) {
+                MediaSet set = feed.getSetForSlot(slotId);
+                if (set != null) {
+                    ArrayList<MediaItem> items = set.getItems();
+                    if (set.getNumItems() > 0) {
+                        mediaSetToAdd = items.get(0).mParentMediaSet;
+                    }
+                }
+            }
+        }
+
+        // Search for the bucket for this media set
+        for (int i = 0; i < numSelectedBuckets; ++i) {
+            final MediaBucket bucketCompare = selectedBuckets.get(i);
+            if (bucketCompare.mediaSet == mediaSetToAdd) {
+                // We found the MediaSet.
+                if (!hasExpandedMediaSet) {
+                    // Remove this bucket from the list since this bucket was already selected.
+                    if (removeIfAlreadyAdded) {
+                        selectedBuckets.remove(bucketCompare);
+                    }
+                    return;
+                } else {
+                    bucket = bucketCompare;
+                    break;
+                }
+            }
+        }
+        if (bucket == null) {
+            // Did not find the media bucket.
+            bucket = new MediaBucket();
+            bucket.mediaSet = mediaSetToAdd;
+            bucket.mediaItems = selectedItems;
+            selectedBuckets.add(bucket);
+        }
+        if (hasExpandedMediaSet) {
+            int numSlots = feed.getNumSlots();
+            if (slotId < numSlots) {
+                MediaSet set = feed.getSetForSlot(slotId);
+                if (set != null) {
+                    ArrayList<MediaItem> items = set.getItems();
+                    int numItems = set.getNumItems();
+                    selectedItems = bucket.mediaItems;
+                    if (selectedItems == null) {
+                        selectedItems = new ArrayList<MediaItem>(numItems);
+                        bucket.mediaItems = selectedItems;
+                    }
+                    for (int i = 0; i < numItems; ++i) {
+                        MediaItem item = items.get(i);
+                        // We see if this item has already been added.
+                        int numPresentItems = selectedItems.size();
+                        boolean foundIndex = false;
+                        for (int j = 0; j < numPresentItems; ++j) {
+                            if (selectedItems.get(j) == item) {
+                                // This index was already present, we need to remove it.
+                                foundIndex = true;
+                                if (removeIfAlreadyAdded) {
+                                    selectedItems.remove(j);
+                                }
+                                break;
+                            }
+                        }
+                        if (foundIndex == false) {
+                            selectedItems.add(item);
+                        }
+                    }
+                }
+            }
+        }
+        setDirty();
+    }
+
+    public boolean find(MediaItem item) {
+        HashMap<MediaItem, Boolean> cachedItems = mCachedItems;
+        if (mDirtyAcceleratedLookup) {
+            cachedItems.clear();
+            mDirtyAcceleratedLookup = false;
+        }
+        Boolean itemAdded = cachedItems.get(item);
+        if (itemAdded == null) {
+            ArrayList<MediaBucket> selectedBuckets = mBuckets;
+            int numSelectedBuckets = selectedBuckets.size();
+            for (int i = 0; i < numSelectedBuckets; ++i) {
+                MediaBucket bucket = selectedBuckets.get(i);
+                ArrayList<MediaItem> mediaItems = bucket.mediaItems;
+                if (mediaItems == null) {
+                    MediaSet parentMediaSet = item.mParentMediaSet;
+                    if (parentMediaSet != null && parentMediaSet.equals(bucket.mediaSet)) {
+                        cachedItems.put(item, TRUE);
+                        return true;
+                    }
+                } else {
+                    int numMediaItems = mediaItems.size();
+                    for (int j = 0; j < numMediaItems; ++j) {
+                        MediaItem itemCompare = mediaItems.get(j);
+                        if (itemCompare == item) {
+                            cachedItems.put(item, TRUE);
+                            return true;
+                        }
+                    }
+                }
+            }
+            cachedItems.put(item, FALSE);
+            return false;
+        } else {
+            return itemAdded.booleanValue();
+        }
+    }
+
+    public void clear() {
+        mBuckets.clear();
+        setDirty();
+    }
+
+    private void setDirty() {
+        mDirtyCount = true;
+        mDirtyAcceleratedLookup = true;
+    }
+
+    // Assumption: No item and set selection combinations.
+    protected static boolean isSetSelection(ArrayList<MediaBucket> buckets) {
+        if (buckets != null) {
+            int numBuckets = buckets.size();
+            if (numBuckets == 0) {
+                return false;
+            } else if (numBuckets == 1) {
+                return isSetSelection(buckets.get(0));
+            } else {
+                // If there are multiple sets, must be a set selection.
+                return true;
+            }
+        }
+        return false;
+    }
+
+    protected static boolean isSetSelection(MediaBucket bucket) {
+        return (bucket.mediaSet != null && bucket.mediaItems == null) ? true : false;
+    }
+
+    // Assumption: If multiple items are selected, they must all be in the first bucket.
+    protected static boolean isMultipleItemSelection(ArrayList<MediaBucket> buckets) {
+        if (buckets != null) {
+            int numBuckets = buckets.size();
+            if (numBuckets == 0) {
+                return false;
+            } else {
+                return isMultipleSetSelection(buckets.get(0));
+            }
+        }
+        return false;
+    }
+
+    protected static boolean isMultipleSetSelection(MediaBucket bucket) {
+        return (bucket.mediaItems != null && bucket.mediaItems.size() > 1) ? true : false;
+    }
+}
diff --git a/src/com/cooliris/media/MediaClustering.java b/src/com/cooliris/media/MediaClustering.java
new file mode 100644
index 0000000..fa45a98
--- /dev/null
+++ b/src/com/cooliris/media/MediaClustering.java
@@ -0,0 +1,357 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.content.Context;
+
+import android.content.res.Resources;
+
+/**
+ * Implementation of an agglomerative based clustering where all items within a certain time cutoff are grouped into the
+ * same cluster. Small adjacent clusters are merged and large individual clusters are considered for splitting.
+ * 
+ * TODO: Limitation: Can deal with items not being added incrementally to the end of the current date range
+ * but effectively assumes this is the case for efficient performance.
+ */
+
+public final class MediaClustering {
+    // If 2 items are greater than 25 miles apart, they will be in different clusters.
+    private static final int GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES = 25;
+
+    // Do not want to split based on anything under 1 min.
+    private static final long MIN_CLUSTER_SPLIT_TIME_IN_MS = 60000L;
+
+    // Disregard a cluster split time of anything over 2 hours.
+    private static final long MAX_CLUSTER_SPLIT_TIME_IN_MS = 7200000L;
+
+    // Try and get around 9 clusters (best-effort for the common case).
+    private static final int NUM_CLUSTERS_TARGETED = 9;
+
+    // Try and merge 2 clusters if they are both smaller than min cluster size.
+    // The min cluster size can range from 8 to 15.
+    private static final int MIN_MIN_CLUSTER_SIZE = 8;
+    private static final int MAX_MIN_CLUSTER_SIZE = 15;
+
+    // Try and split a cluster if it is bigger than max cluster size.
+    // The max cluster size can range from 20 to 50.
+    private static final int MIN_MAX_CLUSTER_SIZE = 20;
+    private static final int MAX_MAX_CLUSTER_SIZE = 50;
+
+    // Initially put 2 items in the same cluster as long as they are within
+    // 3 cluster frequencies of each other.
+    private static int CLUSTER_SPLIT_MULTIPLIER = 3;
+
+    // The minimum change factor in the time between items to consider a partition.
+    // Example: (Item 3 - Item 2) / (Item 2 - Item 1).
+    private static final int MIN_PARTITION_CHANGE_FACTOR = 2;
+
+    // Make the cluster split time of a large cluster half that of a regular cluster.
+    private static final int PARTITION_CLUSTER_SPLIT_TIME_FACTOR = 2;
+
+    private ArrayList<Cluster> mClusters;
+    private Cluster mCurrCluster;
+    private boolean mIsPicassaAlbum = false;
+    private long mClusterSplitTime = (MIN_CLUSTER_SPLIT_TIME_IN_MS + MAX_CLUSTER_SPLIT_TIME_IN_MS) / 2;
+    private long mLargeClusterSplitTime = mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR;
+    private int mMinClusterSize = (MIN_MIN_CLUSTER_SIZE + MAX_MIN_CLUSTER_SIZE) / 2;
+    private int mMaxClusterSize = (MIN_MAX_CLUSTER_SIZE + MAX_MAX_CLUSTER_SIZE) / 2;
+
+    MediaClustering(boolean isPicassaAlbum) {
+        mClusters = new ArrayList<Cluster>();
+        mIsPicassaAlbum = isPicassaAlbum;
+        mCurrCluster = new Cluster(mIsPicassaAlbum);
+    }
+
+    public void clear() {
+        int numClusters = mClusters.size();
+        for (int i = 0; i < numClusters; i++) {
+            Cluster cluster = mClusters.get(i);
+            cluster.clear();
+        }
+        if (mCurrCluster != null) {
+            mCurrCluster.clear();
+        }
+    }
+
+    public void setTimeRange(long timeRange, int numItems) {
+        if (numItems != 0) {
+            int meanItemsPerCluster = numItems / NUM_CLUSTERS_TARGETED;
+            // Heuristic to get min and max cluster size - half and double the desired items per cluster.
+            mMinClusterSize = meanItemsPerCluster / 2;
+            mMaxClusterSize = meanItemsPerCluster * 2;
+            mClusterSplitTime = timeRange / numItems * CLUSTER_SPLIT_MULTIPLIER;
+        }
+        mClusterSplitTime = Shared.clamp(mClusterSplitTime, MIN_CLUSTER_SPLIT_TIME_IN_MS, MAX_CLUSTER_SPLIT_TIME_IN_MS);
+        mLargeClusterSplitTime = mClusterSplitTime / PARTITION_CLUSTER_SPLIT_TIME_FACTOR;
+        mMinClusterSize = Shared.clamp(mMinClusterSize, MIN_MIN_CLUSTER_SIZE, MAX_MIN_CLUSTER_SIZE);
+        mMaxClusterSize = Shared.clamp(mMaxClusterSize, MIN_MAX_CLUSTER_SIZE, MAX_MAX_CLUSTER_SIZE);
+    }
+
+    public void addItemForClustering(MediaItem mediaItem) {
+        compute(mediaItem, false);
+    }
+
+    public void removeItemFromClustering(MediaItem mediaItem) {
+        // Find the cluster that contains this item.
+        if (mCurrCluster.removeItem(mediaItem)) {
+            return;
+        }
+        int numClusters = mClusters.size();
+        for (int i = 0; i < numClusters; i++) {
+            Cluster cluster = mClusters.get(i);
+            if (cluster.removeItem(mediaItem)) {
+                if (cluster.mNumItemsLoaded == 0) {
+                    mClusters.remove(cluster);
+                }
+                return;
+            }
+        }
+    }
+
+    public void compute(MediaItem currentItem, boolean processAllItems) {
+        if (currentItem != null) {
+            int numClusters = mClusters.size();
+            int numCurrClusterItems = mCurrCluster.mNumItemsLoaded;
+            boolean geographicallySeparateItem = false;
+            boolean itemAddedToCurrentCluster = false;
+
+            // Determine if this item should go in the current cluster or be the start of a new cluster.
+            if (numCurrClusterItems == 0) {
+                mCurrCluster.addItem(currentItem);
+            } else {
+                MediaItem prevItem = mCurrCluster.getLastItem();
+                if (timeDistance(prevItem, currentItem) < mClusterSplitTime) {
+                    mCurrCluster.addItem(currentItem);
+                    itemAddedToCurrentCluster = true;
+                } else if (isGeographicallySeparated(prevItem, currentItem)) {
+                    mClusters.add(mCurrCluster);
+                    geographicallySeparateItem = true;
+                } else if (numCurrClusterItems > mMaxClusterSize) {
+                    splitAndAddCurrentCluster();
+                } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) {
+                    mergeAndAddCurrentCluster();
+                } else {
+                    mClusters.add(mCurrCluster);
+                }
+
+                // Creating a new cluster and adding the current item to it.
+                if (!itemAddedToCurrentCluster) {
+                    mCurrCluster = new Cluster(mIsPicassaAlbum);
+                    if (geographicallySeparateItem) {
+                        mCurrCluster.mGeographicallySeparatedFromPrevCluster = true;
+                    }
+                    mCurrCluster.addItem(currentItem);
+                }
+            }
+        }
+
+        if (processAllItems && mCurrCluster.mNumItemsLoaded > 0) {
+            int numClusters = mClusters.size();
+            int numCurrClusterItems = mCurrCluster.mNumItemsLoaded;
+
+            // The last cluster may potentially be too big or too small.
+            if (numCurrClusterItems > mMaxClusterSize) {
+                splitAndAddCurrentCluster();
+            } else if (numClusters > 0 && numCurrClusterItems < mMinClusterSize && !mCurrCluster.mGeographicallySeparatedFromPrevCluster) {
+                mergeAndAddCurrentCluster();
+            } else {
+                mClusters.add(mCurrCluster);
+            }
+            mCurrCluster = new Cluster(mIsPicassaAlbum);
+        }
+    }
+
+    private void splitAndAddCurrentCluster() {
+        ArrayList<MediaItem> currClusterItems = mCurrCluster.getItems();
+        int numCurrClusterItems = mCurrCluster.mNumItemsLoaded;
+        int secondPartitionStartIndex = getPartitionIndexForCurrentCluster();
+        if (secondPartitionStartIndex != -1) {
+            Cluster partitionedCluster = new Cluster(mIsPicassaAlbum);
+            for (int j = 0; j < secondPartitionStartIndex; j++) {
+                partitionedCluster.addItem(currClusterItems.get(j));
+            }
+            mClusters.add(partitionedCluster);
+            partitionedCluster = new Cluster(mIsPicassaAlbum);
+            for (int j = secondPartitionStartIndex; j < numCurrClusterItems; j++) {
+                partitionedCluster.addItem(currClusterItems.get(j));
+            }
+            mClusters.add(partitionedCluster);
+        } else {
+            mClusters.add(mCurrCluster);
+        }        
+    }
+
+    private int getPartitionIndexForCurrentCluster() {
+        int partitionIndex = -1;
+        float largestChange = MIN_PARTITION_CHANGE_FACTOR;
+        ArrayList<MediaItem> currClusterItems = mCurrCluster.getItems();
+        int numCurrClusterItems = mCurrCluster.mNumItemsLoaded;
+        int minClusterSize = mMinClusterSize;
+
+        // Could be slightly more efficient here but this code seems cleaner.
+        if (numCurrClusterItems > minClusterSize + 1) {
+            for (int i = minClusterSize; i < numCurrClusterItems - minClusterSize; i++) {
+                MediaItem prevItem = currClusterItems.get(i - 1);
+                MediaItem currItem = currClusterItems.get(i);
+                MediaItem nextItem = currClusterItems.get(i + 1);
+    
+                if (prevItem.isDateTakenValid() && currItem.isDateModifiedValid() && nextItem.isDateModifiedValid()) {
+                    long diff1 = Math.abs(nextItem.mDateTakenInMs - currItem.mDateTakenInMs);
+                    long diff2 = Math.abs(currItem.mDateTakenInMs - prevItem.mDateTakenInMs);
+                    float change = Math.max(diff1 / (diff2 + 0.01f), diff2 / (diff1 + 0.01f));
+                    if (change > largestChange) {
+                        if (timeDistance(currItem, prevItem) > mLargeClusterSplitTime) {
+                            partitionIndex = i;
+                            largestChange = change;
+                        } else if (timeDistance(nextItem, currItem) > mLargeClusterSplitTime) {
+                            partitionIndex = i + 1;
+                            largestChange = change;
+                        }
+                    }
+                }
+            }
+        }
+        return partitionIndex;
+    }
+
+    private void mergeAndAddCurrentCluster() {
+        int numClusters = mClusters.size();
+        Cluster prevCluster = mClusters.get(numClusters - 1);
+        ArrayList<MediaItem> currClusterItems = mCurrCluster.getItems();
+        int numCurrClusterItems = mCurrCluster.mNumItemsLoaded;
+        if (prevCluster.mNumItemsLoaded < mMinClusterSize) {
+            for (int i = 0; i < numCurrClusterItems; i++) {
+                prevCluster.addItem(currClusterItems.get(i));
+            }
+            mClusters.set(numClusters - 1, prevCluster);
+        } else {
+            mClusters.add(mCurrCluster);
+        }
+    }
+
+    public synchronized ArrayList<Cluster> getClusters() {
+        int numCurrClusterItems = mCurrCluster.mNumItemsLoaded;
+        if (numCurrClusterItems == 0) {
+            return mClusters;
+        }
+        ArrayList<Cluster> mergedClusters = new ArrayList<Cluster>();
+        mergedClusters.addAll(mClusters);
+        if (numCurrClusterItems > 0) {
+            mergedClusters.add(mCurrCluster);
+        }
+        return mergedClusters;
+    }
+
+    public ArrayList<Cluster> getClustersForDisplay() {
+        return mClusters;
+    }
+
+    public static final class Cluster extends MediaSet {
+        private boolean mGeographicallySeparatedFromPrevCluster = false;
+        private boolean mClusterChanged = false;
+        private boolean mIsPicassaAlbum = false;
+        private static final String MMDDYY_FORMAT = "MMddyy";
+
+        public Cluster(boolean isPicassaAlbum) {
+            mIsPicassaAlbum = isPicassaAlbum;
+        }
+
+        public void generateCaption(Context context) {
+            if (mClusterChanged) {
+                Resources resources = context.getResources();
+
+                long minTimestamp = -1L;
+                long maxTimestamp = -1L;
+                if (areTimestampsAvailable()) {
+                    minTimestamp = mMinTimestamp;
+                    maxTimestamp = mMaxTimestamp;
+                } else if (areAddedTimestampsAvailable()) {
+                    minTimestamp = mMinAddedTimestamp;
+                    maxTimestamp = mMaxAddedTimestamp;
+                }
+
+                if (minTimestamp != -1L) {
+                    if (mIsPicassaAlbum) {
+                        minTimestamp -= Gallery.CURRENT_TIME_ZONE.getOffset(minTimestamp);
+                        maxTimestamp -= Gallery.CURRENT_TIME_ZONE.getOffset(maxTimestamp);
+                    }
+                    String minDay = DateFormat.format(MMDDYY_FORMAT, minTimestamp).toString();
+                    String maxDay = DateFormat.format(MMDDYY_FORMAT, maxTimestamp).toString();
+
+                    if (minDay.substring(4).equals(maxDay.substring(4))) {
+                        // The items are from the same year - show at least as much granularity as abbrev_all allows.
+                        mName = DateUtils.formatDateRange(context, minTimestamp, maxTimestamp, DateUtils.FORMAT_ABBREV_ALL);
+
+                        // Get a more granular date range string if the min and max timestamp are on the same day and from the current year.
+                        if (minDay.equals(maxDay)) {
+                            int flags = DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE;
+                            // Contains the year only if the date does not correspond to the current year.
+                            String dateRangeWithOptionalYear = DateUtils.formatDateTime(context, minTimestamp, flags);
+                            String dateRangeWithYear = DateUtils.formatDateTime(context, minTimestamp, flags | DateUtils.FORMAT_SHOW_YEAR);
+                            if (!dateRangeWithOptionalYear.equals(dateRangeWithYear)) {
+                                // This means both dates are from the same year - show the time.
+                                // Not enough room to display the time range. Pick the mid-point.
+                                long midTimestamp = (minTimestamp + maxTimestamp) / 2;
+                                mName = DateUtils.formatDateRange(context, midTimestamp, midTimestamp, DateUtils.FORMAT_SHOW_TIME | flags);
+                            }
+                        }
+                    } else {
+                        // The items are not from the same year - only show month and year.
+                        int flags = DateUtils.FORMAT_NO_MONTH_DAY | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_DATE;
+                        mName = DateUtils.formatDateRange(context, minTimestamp, maxTimestamp, flags);
+                    }
+                } else {
+                    mName = resources.getString(R.string.date_unknown);
+                }
+                updateNumExpectedItems();
+                generateTitle(false);
+                mClusterChanged = false;
+            }
+        }
+
+        public void addItem(MediaItem item) {
+            super.addItem(item);
+            mClusterChanged = true;
+        }
+
+        public boolean removeItem(MediaItem item) {
+            if (super.removeItem(item)) {
+                mClusterChanged = true;
+                return true;
+            }
+            return false;
+        }
+
+        public MediaItem getLastItem() {
+            final ArrayList<MediaItem> items = super.getItems();
+            if (items == null || mNumItemsLoaded == 0) {
+                return null;
+            } else {
+                return items.get(mNumItemsLoaded - 1);
+            }
+        }
+    }
+
+    // Returns the time interval between the two items in milliseconds.
+    public static long timeDistance(MediaItem a, MediaItem b) {
+        if (a == null || b == null) {
+            return 0;
+        }
+        return Math.abs(a.mDateTakenInMs - b.mDateTakenInMs);
+    }
+
+    // Returns true if a, b are sufficiently geographically separated.
+    private static boolean isGeographicallySeparated(MediaItem a, MediaItem b) {
+        // If a or b are null, a or b have the default latitude, longitude values or are close enough, return false.
+        if (a != null && b != null && a.isLatLongValid() && b.isLatLongValid()) {
+            int distance = (int) (LocationMediaFilter.toMile(LocationMediaFilter.distanceBetween(a.mLatitude, a.mLongitude,
+                    b.mLatitude, b.mLongitude)) + 0.5);
+            if (distance > GEOGRAPHIC_DISTANCE_CUTOFF_IN_MILES) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/com/cooliris/media/MediaFeed.java b/src/com/cooliris/media/MediaFeed.java
new file mode 100644
index 0000000..6ad5801
--- /dev/null
+++ b/src/com/cooliris/media/MediaFeed.java
@@ -0,0 +1,696 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import android.content.Context;
+import android.view.Gravity;
+import android.widget.Toast;
+import android.os.Process;
+
+import com.cooliris.media.MediaClustering.Cluster;
+
+public final class MediaFeed implements Runnable {
+    public static final int OPERATION_DELETE = 0;
+    public static final int OPERATION_ROTATE = 1;
+    public static final int OPERATION_CROP = 2;
+
+    private static final int NUM_ITEMS_LOOKAHEAD = 60;
+
+    private IndexRange mVisibleRange = new IndexRange();
+    private IndexRange mBufferedRange = new IndexRange();
+    private ArrayList<MediaSet> mMediaSets = new ArrayList<MediaSet>();
+    private Listener mListener;
+    private DataSource mDataSource;
+    private boolean mListenerNeedsUpdate = false;
+    private MediaSet mSingleWrapper = new MediaSet();
+    private boolean mInClusteringMode = false;
+    private HashMap<MediaSet, MediaClustering> mClusterSets = new HashMap<MediaSet, MediaClustering>(32);
+    private int mExpandedMediaSetIndex = Shared.INVALID;
+    private MediaFilter mMediaFilter;
+    private MediaSet mMediaFilteredSet;
+    private Context mContext;
+    private Thread mDataSourceThread = null;
+    private Thread mAlbumSourceThread = null;
+    private boolean mListenerNeedsLayout;
+    private boolean mWaitingForMediaScanner;
+    private boolean mSingleImageMode;
+    private boolean mLoading;
+
+    public interface Listener {
+        public abstract void onFeedAboutToChange(MediaFeed feed);
+
+        public abstract void onFeedChanged(MediaFeed feed, boolean needsLayout);
+    }
+
+    public MediaFeed(Context context, DataSource dataSource, Listener listener) {
+        mContext = context;
+        mListener = listener;
+        mDataSource = dataSource;
+        mSingleWrapper.setNumExpectedItems(1);
+        mLoading = true;
+    }
+    
+    public void shutdown() {
+        if (mDataSourceThread != null) {
+            mDataSource.shutdown();
+            mDataSourceThread.interrupt();
+            mDataSourceThread = null;
+        }
+        if (mAlbumSourceThread != null) {
+            mAlbumSourceThread.interrupt();
+            mAlbumSourceThread = null;
+        }
+        int numSets = mMediaSets.size();
+        for (int i = 0; i < numSets; ++i) {
+            MediaSet set = mMediaSets.get(i);
+            set.clear();
+        }
+        synchronized (mMediaSets) {
+            mMediaSets.clear();
+        }
+        int numClusters = mClusterSets.size();
+        for (int i = 0; i < numClusters; ++i) {
+            MediaClustering mc = mClusterSets.get(i);
+            if (mc != null) {
+                mc.clear();
+            }
+        }
+        mClusterSets.clear();
+        mListener = null;
+        mDataSource = null;
+        mSingleWrapper = null;
+    }
+
+    public void setVisibleRange(int begin, int end) {
+        mVisibleRange.begin = begin;
+        mVisibleRange.end = end;
+        int numItems = 96;
+        int numItemsBy2 = numItems / 2;
+        int numItemsBy4 = numItems / 4;
+        mBufferedRange.begin = (begin / numItemsBy2) * numItemsBy2 - numItemsBy4;
+        mBufferedRange.end = mBufferedRange.begin + numItems;
+    }
+
+    public void setFilter(MediaFilter filter) {
+        mMediaFilter = filter;
+        mMediaFilteredSet = null;
+        if (mListener != null) {
+            mListener.onFeedAboutToChange(this);
+        }
+    }
+
+    public void removeFilter() {
+        mMediaFilter = null;
+        mMediaFilteredSet = null;
+        if (mListener != null) {
+            mListener.onFeedAboutToChange(this);
+            updateListener(true);
+        }
+    }
+
+    public ArrayList<MediaSet> getMediaSets() {
+        return mMediaSets;
+    }
+
+    public synchronized MediaSet getMediaSet(final long setId) {
+        if (setId != Shared.INVALID) {
+            int mMediaSetsSize = mMediaSets.size();
+            for (int i = 0; i < mMediaSetsSize; i++) {
+                if (mMediaSets.get(i).mId == setId) {
+                    return mMediaSets.get(i);
+                }
+            }
+        }
+        return null;
+    }
+
+    public MediaSet getFilteredSet() {
+        return mMediaFilteredSet;
+    }
+
+    public MediaSet addMediaSet(final long setId, DataSource dataSource) {
+        MediaSet mediaSet = new MediaSet(dataSource);
+        mediaSet.mId = setId;
+        mMediaSets.add(mediaSet);
+        return mediaSet;
+    }
+
+    public DataSource getDataSource() {
+        return mDataSource;
+    }
+
+    public MediaClustering getClustering() {
+        if (mExpandedMediaSetIndex != Shared.INVALID && mExpandedMediaSetIndex < mMediaSets.size()) {
+            return mClusterSets.get(mMediaSets.get(mExpandedMediaSetIndex));
+        }
+        return null;
+    }
+
+    public ArrayList<Cluster> getClustersForSet(final MediaSet set) {
+        ArrayList<Cluster> clusters = null;
+        if (mClusterSets != null && mClusterSets.containsKey(set)) {
+            MediaClustering mediaClustering = mClusterSets.get(set);
+            if (mediaClustering != null) {
+                clusters = mediaClustering.getClusters();
+            }
+        }
+        return clusters;
+    }
+
+    public void addItemToMediaSet(MediaItem item, MediaSet mediaSet) {
+        item.mParentMediaSet = mediaSet;
+        mediaSet.addItem(item);
+        synchronized (this) {
+            if (item.mClusteringState == MediaItem.NOT_CLUSTERED) {
+                MediaClustering clustering = mClusterSets.get(mediaSet);
+                if (clustering == null) {
+                    clustering = new MediaClustering(mediaSet.isPicassaAlbum());
+                    mClusterSets.put(mediaSet, clustering);
+                }
+                clustering.setTimeRange(mediaSet.mMaxTimestamp - mediaSet.mMinTimestamp, mediaSet.getNumExpectedItems());
+                clustering.addItemForClustering(item);
+                item.mClusteringState = MediaItem.CLUSTERED;
+            }
+        }
+    }
+
+    public void performOperation(final int operation, final ArrayList<MediaBucket> mediaBuckets, final Object data) {
+        int numBuckets = mediaBuckets.size();
+        final ArrayList<MediaBucket> copyMediaBuckets = new ArrayList<MediaBucket>(numBuckets);
+        for (int i = 0; i < numBuckets; ++i) {
+            copyMediaBuckets.add(mediaBuckets.get(i));
+        }
+        if (operation == OPERATION_DELETE && mListener != null) {
+            mListener.onFeedAboutToChange(this);
+
+        }
+        Thread operationThread = new Thread(new Runnable() {
+            public void run() {
+                ArrayList<MediaBucket> mediaBuckets = copyMediaBuckets;
+                if (operation == OPERATION_DELETE) {
+                    int numBuckets = mediaBuckets.size();
+                    for (int i = 0; i < numBuckets; ++i) {
+                        MediaBucket bucket = mediaBuckets.get(i);
+                        MediaSet set = bucket.mediaSet;
+                        ArrayList<MediaItem> items = bucket.mediaItems;
+                        if (set != null && items == null) {
+                            // Remove the entire bucket.
+                            removeMediaSet(set);
+                        } else if (set != null && items != null) {
+                            // We need to remove these items from the set.
+                            int numItems = items.size();
+                            // We also need to delete the items from the cluster.
+                            MediaClustering clustering = mClusterSets.get(set);
+                            for (int j = 0; j < numItems; ++j) {
+                                MediaItem item = items.get(j);
+                                removeItemFromMediaSet(item, set);
+                                if (clustering != null) {
+                                    clustering.removeItemFromClustering(item);
+                                }
+                            }
+                            set.updateNumExpectedItems();
+                            set.generateTitle(true);
+                        }
+                    }
+                    updateListener(true);
+                    if (mDataSource != null) {
+                        mDataSource.performOperation(OPERATION_DELETE, mediaBuckets, null);
+                    }
+                } else {
+                    mDataSource.performOperation(operation, mediaBuckets, data);
+                }
+            }
+        });
+        operationThread.setName("Operation " + operation);
+        operationThread.start();
+    }
+
+    public void removeMediaSet(MediaSet set) {
+        mMediaSets.remove(set);
+    }
+
+    private void removeItemFromMediaSet(MediaItem item, MediaSet mediaSet) {
+        mediaSet.removeItem(item);
+        synchronized (this) {
+            MediaClustering clustering = mClusterSets.get(mediaSet);
+            if (clustering != null) {
+                clustering.removeItemFromClustering(item);
+            }
+        }
+    }
+
+    public void updateListener(boolean needsLayout) {
+        mListenerNeedsUpdate = true;
+        mListenerNeedsLayout = needsLayout;
+    }
+
+    public int getNumSlots() {
+        int currentMediaSetIndex = mExpandedMediaSetIndex;
+        ArrayList<MediaSet> mediaSets = mMediaSets;
+        int mediaSetsSize = mediaSets.size();
+
+        if (mInClusteringMode == false) {
+            if (currentMediaSetIndex == Shared.INVALID || currentMediaSetIndex >= mediaSetsSize) {
+                return mediaSetsSize;
+            } else {
+                MediaSet setToUse = (mMediaFilteredSet == null) ? mediaSets.get(currentMediaSetIndex) : mMediaFilteredSet;
+                return setToUse.getNumItems();
+            }
+        } else if (currentMediaSetIndex != Shared.INVALID && currentMediaSetIndex < mediaSetsSize) {
+            MediaSet set = mediaSets.get(currentMediaSetIndex);
+            MediaClustering clustering = mClusterSets.get(set);
+            if (clustering != null) {
+                return clustering.getClustersForDisplay().size();
+            }
+        }
+        return 0;
+    }
+
+    public MediaSet getSetForSlot(int slotIndex) {
+        if (slotIndex < 0) {
+            return null;
+        }
+
+        ArrayList<MediaSet> mediaSets = mMediaSets;
+        int mediaSetsSize = mediaSets.size();
+        int currentMediaSetIndex = mExpandedMediaSetIndex;
+
+        if (mInClusteringMode == false) {
+            if (currentMediaSetIndex == Shared.INVALID || currentMediaSetIndex >= mediaSetsSize) {
+                if (slotIndex >= mediaSetsSize) {
+                    return null;
+                }
+                return mMediaSets.get(slotIndex);
+            }
+            if (mSingleWrapper.getNumItems() == 0) {
+                mSingleWrapper.addItem(null);
+            }
+            MediaSet setToUse = (mMediaFilteredSet == null) ? mMediaSets.get(currentMediaSetIndex) : mMediaFilteredSet;
+            ArrayList<MediaItem> items = setToUse.getItems();
+            if (slotIndex >= setToUse.getNumItems()) {
+                return null;
+            }
+            mSingleWrapper.getItems().set(0, items.get(slotIndex));
+            return mSingleWrapper;
+        } else if (currentMediaSetIndex != Shared.INVALID && currentMediaSetIndex < mediaSetsSize) {
+            MediaSet set = mediaSets.get(currentMediaSetIndex);
+            MediaClustering clustering = mClusterSets.get(set);
+            if (clustering != null) {
+                ArrayList<MediaClustering.Cluster> clusters = clustering.getClustersForDisplay();
+                if (clusters.size() > slotIndex) {
+                    MediaClustering.Cluster cluster = clusters.get(slotIndex);
+                    cluster.generateCaption(mContext);
+                    return cluster;
+                }
+            }
+        }
+        return null;
+    }
+
+    public boolean getWaitingForMediaScanner() {
+        return mWaitingForMediaScanner;
+    }
+    
+    public boolean isLoading() {
+        return mLoading;
+    }
+
+    public void start() {
+        final MediaFeed feed = this;
+        mLoading = true;
+        mAlbumSourceThread = new Thread(new Runnable() {
+            public void run() {
+                if (mContext == null)
+                    return;
+                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+                DataSource dataSource = mDataSource;
+                // We must wait while the SD card is mounted or the MediaScanner is running.
+                mWaitingForMediaScanner = false;
+                while (ImageManager.isMediaScannerScanning(mContext.getContentResolver())) {
+                    // MediaScanner is still running, wait
+                    mWaitingForMediaScanner = true;
+                    try {
+                        if (mContext == null)
+                            return;
+                        showToast(mContext.getResources().getString(R.string.initializing), Toast.LENGTH_LONG);
+                        Thread.sleep(6000);
+                    } catch (InterruptedException e) {
+
+                    }
+                }
+                if (mWaitingForMediaScanner) {
+                    showToast(mContext.getResources().getString(R.string.loading_new), Toast.LENGTH_LONG);
+                }
+                mWaitingForMediaScanner = false;
+                if (dataSource != null) {
+                    dataSource.loadMediaSets(feed);
+                }
+                mLoading = false;
+            }
+        });
+        mAlbumSourceThread.setName("MediaSets");
+        mAlbumSourceThread.start();
+        mDataSourceThread = new Thread(this);
+        mDataSourceThread.setName("MediaFeed");
+        mDataSourceThread.start();
+    }
+
+    private void showToast(final String string, final int duration) {
+        showToast(string, duration, false);
+    }
+
+    private void showToast(final String string, final int duration, final boolean centered) {
+        if (mContext != null && !((Gallery)mContext).isPaused()) {
+            ((Gallery) mContext).getHandler().post(new Runnable() {
+                public void run() {
+                    if (mContext != null) {
+                        Toast toast = Toast.makeText(mContext, string, duration);
+                        if (centered) {
+                            toast.setGravity(Gravity.CENTER, 0, 0);
+                        }
+                        toast.show();
+                    }
+                }
+            });
+        }
+    }
+
+    public void run() {
+        DataSource dataSource = mDataSource;
+        int sleepMs = 100;
+        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+        if (dataSource != null) {
+            while (Thread.interrupted() == false) {
+                if (mListenerNeedsUpdate) {
+                    mListenerNeedsUpdate = false;
+                    if (mListener != null)
+                        mListener.onFeedChanged(this, mListenerNeedsLayout);
+                    try {
+                        Thread.sleep(sleepMs);
+                    } catch (InterruptedException e) {
+                        return;
+                    }
+                } else {
+                    try {
+                        Thread.sleep(sleepMs);
+                    } catch (InterruptedException e) {
+                        return;
+                    }
+                }
+                sleepMs = 100;
+                ArrayList<MediaSet> mediaSets = mMediaSets;
+                synchronized (mediaSets) {
+                    int expandedSetIndex = mExpandedMediaSetIndex;
+                    if (expandedSetIndex >= mMediaSets.size()) {
+                        expandedSetIndex = Shared.INVALID;
+                    }
+                    if (expandedSetIndex == Shared.INVALID) {
+                        // We purge the sets outside this visibleRange.
+                        int numSets = mMediaSets.size();
+                        IndexRange visibleRange = mVisibleRange;
+                        IndexRange bufferedRange = mBufferedRange;
+                        boolean scanMediaSets = true;
+                        for (int i = 0; i < numSets; ++i) {
+                            if (i >= visibleRange.begin && i <= visibleRange.end && scanMediaSets) {
+                                MediaSet set = mediaSets.get(i);
+                                int numItemsLoaded = set.mNumItemsLoaded;
+                                if (!set.setContainsValidItems()) {
+                                    mediaSets.remove(set);
+                                    if (mListener != null) {
+                                        mListener.onFeedChanged(this, false);
+                                    }
+                                    break;
+                                }
+                                if (numItemsLoaded < set.getNumExpectedItems() && numItemsLoaded < 8) {
+                                    dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
+                                    if (set.getNumExpectedItems() == 0) {
+                                        mediaSets.remove(set);
+                                        break;
+                                    }
+                                    if (mListener != null) {
+                                        mListener.onFeedChanged(this, false);
+                                    }
+                                    sleepMs = 100;
+                                    scanMediaSets = false;
+                                }
+                            }
+                        }
+                        numSets = mMediaSets.size();
+                        for (int i = 0; i < numSets; ++i) {
+                            MediaSet set = mediaSets.get(i);
+                            if (i >= bufferedRange.begin && i <= bufferedRange.end) {
+                                if (scanMediaSets) {
+                                    int numItemsLoaded = set.mNumItemsLoaded;
+                                    if (numItemsLoaded < set.getNumExpectedItems() && numItemsLoaded < 8) {
+                                        dataSource.loadItemsForSet(this, set, numItemsLoaded, 8);
+                                        if (set.getNumExpectedItems() == 0) {
+                                            mediaSets.remove(set);
+                                            break;
+                                        }
+                                        if (mListener != null) {
+                                            mListener.onFeedChanged(this, false);
+                                        }
+                                        sleepMs = 100;
+                                        scanMediaSets = false;
+                                    }
+                                }
+                            } else if (i < bufferedRange.begin || i > bufferedRange.end){
+                                // Purge this set to its initial status.
+                                MediaClustering clustering = mClusterSets.get(set);
+                                if (clustering != null) {
+                                    clustering.clear();
+                                    mClusterSets.remove(set);
+                                }
+                                if (set.getNumItems() != 0)
+                                    set.clear();
+                            }
+                        }
+                    }
+                    if (expandedSetIndex != Shared.INVALID) {
+                        int numSets = mMediaSets.size();
+                        for (int i = 0; i < numSets; ++i) {
+                            // Purge other sets.
+                            if (i != expandedSetIndex) {
+                                MediaSet set = mediaSets.get(i);
+                                MediaClustering clustering = mClusterSets.get(set);
+                                if (clustering != null) {
+                                    clustering.clear();
+                                    mClusterSets.remove(set);
+                                }
+                                if (set.getNumItems() != 0)
+                                    set.clear();
+                            }
+                        }
+                        // Make sure all the items are loaded for the album.
+                        int numItemsLoaded = mediaSets.get(expandedSetIndex).mNumItemsLoaded;
+                        int requestedItems = mVisibleRange.end;
+                        // requestedItems count changes in clustering mode.
+                        if (mInClusteringMode && mClusterSets != null) {
+                            requestedItems = 0;
+                            MediaClustering clustering = mClusterSets.get(mediaSets.get(expandedSetIndex));
+                            ArrayList<Cluster> clusters = clustering.getClustersForDisplay();
+                            int numClusters = clusters.size();
+                            for (int i = 0; i < numClusters; i++) {
+                                requestedItems += clusters.get(i).getNumExpectedItems();
+                            }
+                        }
+                        MediaSet set = mediaSets.get(expandedSetIndex);
+                        if (numItemsLoaded < set.getNumExpectedItems()) {
+                            // TODO(Venkat) Why are we doing 4th param calculations like this?
+                            dataSource.loadItemsForSet(this, set, numItemsLoaded, (requestedItems / NUM_ITEMS_LOOKAHEAD)
+                                    * NUM_ITEMS_LOOKAHEAD + NUM_ITEMS_LOOKAHEAD);
+                            if (set.getNumExpectedItems() == 0) {
+                                mediaSets.remove(set);
+                                mListener.onFeedChanged(this, false);
+                            }
+                            if (numItemsLoaded != set.mNumItemsLoaded && mListener != null) {
+                                mListener.onFeedChanged(this, false);
+                            }
+                        }
+                    }
+                    MediaFilter filter = mMediaFilter;
+                    if (filter != null && mMediaFilteredSet == null) {
+                        if (expandedSetIndex != Shared.INVALID) {
+                            MediaSet set = mediaSets.get(expandedSetIndex);
+                            ArrayList<MediaItem> items = set.getItems();
+                            int numItems = set.getNumItems();
+                            MediaSet filteredSet = new MediaSet();
+                            filteredSet.setNumExpectedItems(numItems);
+                            mMediaFilteredSet = filteredSet;
+                            for (int i = 0; i < numItems; ++i) {
+                                MediaItem item = items.get(i);
+                                if (filter.pass(item)) {
+                                    filteredSet.addItem(item);
+                                }
+                            }
+                            filteredSet.updateNumExpectedItems();
+                            filteredSet.generateTitle(true);
+                        }
+                        updateListener(true);
+                    }
+                }
+            }
+        }
+    }
+
+    public void expandMediaSet(int mediaSetIndex) {
+        // We need to check if this slot can be focused or not.
+        if (mListener != null) {
+            mListener.onFeedAboutToChange(this);
+        }
+        if (mExpandedMediaSetIndex > 0 && mediaSetIndex == Shared.INVALID) {
+            // We are collapsing a previously expanded media set
+            if (mediaSetIndex < mMediaSets.size() && mExpandedMediaSetIndex >= 0) {
+                MediaSet set = mMediaSets.get(mExpandedMediaSetIndex);
+                if (set.getNumItems() == 0) {
+                    set.clear();
+                }
+            }
+        }
+        mExpandedMediaSetIndex = mediaSetIndex;
+        if (mediaSetIndex < mMediaSets.size() && mediaSetIndex >= 0) {
+            // Notify Picasa that the user entered the album.
+            // MediaSet set = mMediaSets.get(mediaSetIndex);
+            // PicasaService.requestSync(mContext, PicasaService.TYPE_ALBUM_PHOTOS, set.mPicasaAlbumId);
+        }
+        updateListener(true);
+    }
+
+    public boolean canExpandSet(int slotIndex) {
+        int mediaSetIndex = slotIndex;
+        if (mediaSetIndex < mMediaSets.size() && mediaSetIndex >= 0) {
+            MediaSet set = mMediaSets.get(mediaSetIndex);
+            if (set.getNumItems() > 0) {
+                MediaItem item = set.getItems().get(0);
+                if (item.mId == Shared.INVALID) {
+                    return false;
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean hasExpandedMediaSet() {
+        return (mExpandedMediaSetIndex != Shared.INVALID);
+    }
+
+    public boolean restorePreviousClusteringState() {
+        boolean retVal = disableClusteringIfNecessary();
+        if (retVal) {
+            if (mListener != null) {
+                mListener.onFeedAboutToChange(this);
+            }
+            updateListener(true);
+        }
+        return retVal;
+    }
+
+    private boolean disableClusteringIfNecessary() {
+        if (mInClusteringMode) {
+            // Disable clustering.
+            mInClusteringMode = false;
+            return true;
+        }
+        return false;
+    }
+
+    public boolean isClustered() {
+        return mInClusteringMode;
+    }
+
+    public MediaSet getCurrentSet() {
+        if (mExpandedMediaSetIndex != Shared.INVALID && mExpandedMediaSetIndex < mMediaSets.size()) {
+            return mMediaSets.get(mExpandedMediaSetIndex);
+        }
+        return null;
+    }
+
+    public void performClustering() {
+        if (mListener != null) {
+            mListener.onFeedAboutToChange(this);
+        }
+        MediaSet setToUse = null;
+        if (mExpandedMediaSetIndex != Shared.INVALID || mExpandedMediaSetIndex < mMediaSets.size()) {
+            setToUse = mMediaSets.get(mExpandedMediaSetIndex);
+        }
+        if (setToUse != null) {
+            MediaClustering clustering = null;
+            synchronized (this) {
+                // Make sure the computation is completed to the end.
+                clustering = mClusterSets.get(setToUse);
+                if (clustering != null) {
+                    clustering.compute(null, true);
+                } else {
+                    return;
+                }
+            }
+            mInClusteringMode = true;
+            updateListener(true);
+        }
+    }
+
+    public void moveSetToFront(MediaSet mediaSet) {
+        ArrayList<MediaSet> mediaSets = mMediaSets;
+        int numSets = mediaSets.size();
+        if (numSets == 0) {
+            mediaSets.add(mediaSet);
+            return;
+        }
+        MediaSet setToFind = mediaSets.get(0);
+        if (setToFind == mediaSet) {
+            return;
+        }
+        mediaSets.set(0, mediaSet);
+        int indexToSwapTill = -1;
+        for (int i = 1; i < numSets; ++i) {
+            MediaSet set = mediaSets.get(i);
+            if (set == mediaSet) {
+                mediaSets.set(i, setToFind);
+                indexToSwapTill = i;
+                break;
+            }
+        }
+        if (indexToSwapTill != Shared.INVALID) {
+            for (int i = indexToSwapTill; i > 1; --i) {
+                MediaSet setEnd = mediaSets.get(i);
+                MediaSet setPrev = mediaSets.get(i - 1);
+                mediaSets.set(i, setPrev);
+                mediaSets.set(i - 1, setEnd);
+            }
+        }
+    }
+
+    public MediaSet replaceMediaSet(long setId, DataSource dataSource) {
+        MediaSet mediaSet = new MediaSet(dataSource);
+        mediaSet.mId = setId;
+        ArrayList<MediaSet> mediaSets = mMediaSets;
+        int numSets = mediaSets.size();
+        for (int i = 0; i < numSets; ++i) {
+            if (mediaSets.get(i).mId == setId) {
+                MediaSet thisSet = mediaSets.get(i);
+                mediaSet.mName = thisSet.mName;
+                mediaSets.set(i, mediaSet);
+                break;
+            }
+        }
+        return mediaSet;
+    }
+    
+    public void setSingleImageMode(boolean singleImageMode) {
+        mSingleImageMode = singleImageMode;
+    }
+
+    public boolean isSingleImageMode() {
+        return mSingleImageMode;
+    }
+
+    public MediaSet getExpandedMediaSet() {
+        if (mExpandedMediaSetIndex == Shared.INVALID)
+            return null;
+        if (mExpandedMediaSetIndex >= mMediaSets.size())
+            return null;
+        return mMediaSets.get(mExpandedMediaSetIndex);
+    }
+}
diff --git a/src/com/cooliris/media/MediaFilter.java b/src/com/cooliris/media/MediaFilter.java
new file mode 100644
index 0000000..67b881a
--- /dev/null
+++ b/src/com/cooliris/media/MediaFilter.java
@@ -0,0 +1,5 @@
+package com.cooliris.media;
+
+public abstract class MediaFilter {
+    public abstract boolean pass(MediaItem item);
+}
diff --git a/src/com/cooliris/media/MediaItem.java b/src/com/cooliris/media/MediaItem.java
new file mode 100644
index 0000000..828a037
--- /dev/null
+++ b/src/com/cooliris/media/MediaItem.java
@@ -0,0 +1,134 @@
+package com.cooliris.media;
+
+public final class MediaItem {
+    public static final int MEDIA_TYPE_IMAGE = 0;
+    public static final int MEDIA_TYPE_VIDEO = 1;
+    // Approximately the year 1975 in milliseconds and seconds. Serves as a min cutoff for bad times.
+    public static final long MIN_VALID_DATE_IN_MS = 157680000000L;
+    public static final long MIN_VALID_DATE_IN_SEC = 157680000L;
+    // Approximately the year 2035 in milliseconds ans seconds. Serves as a max cutoff for bad time.
+    public static final long MAX_VALID_DATE_IN_MS = 2049840000000L;
+    public static final long MAX_VALID_DATE_IN_SEC = 2049840000L;
+
+    // mId is not a unique identifier of the item mId is initialized to -1 in some cases.
+    public static final String ID = new String("id");
+    public long mId;
+
+    public String mGuid;
+    public String mCaption;
+    public String mEditUri;
+    public String mContentUri;
+    public String mThumbnailUri;
+    public String mScreennailUri;
+    public String mMicroThumbnailUri;
+    public String mWeblink;
+    public String mMimeType;
+    private String mDisplayMimeType;
+    private int mMediaType = -1;
+    public String mRole;
+    public String mDescription;
+
+    // Location-based properties of the item.
+    public double mLatitude;
+    public double mLongitude;
+    public String mReverseGeocodedLocation;
+
+    public long mDateTakenInMs = 0;
+    public boolean mTriedRetrievingExifDateTaken = false;
+    public long mDateModifiedInSec = 0;
+    public long mDateAddedInSec = 0;
+    public int mDurationInSec;
+
+    public int mPrimingState = 0;
+    public static final int NOT_PRIMED = 0;
+    public static final int STARTED_PRIMING = 1;
+    public static final int PRIMED = 2;
+
+    public int mClusteringState = 0;
+    public static final int NOT_CLUSTERED = 0;
+    public static final int CLUSTERED = 1;
+
+    public float mRotation;
+
+    public long mThumbnailId;
+    public int mThumbnailFocusX;
+    public int mThumbnailFocusY;
+
+    public String mFilePath;
+
+    public MediaSet mParentMediaSet;
+
+    public MediaItem() {
+        mCaption = "";
+    }
+
+    public boolean isWellFormed() {
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return mCaption;
+    }
+
+    public boolean isLatLongValid() {
+        return (mLatitude != 0.0 || mLongitude != 0.0);
+    }
+
+    // Serves as a sanity check cutoff for bad exif information.
+    public boolean isDateTakenValid() {
+        return (mDateTakenInMs > MIN_VALID_DATE_IN_MS && mDateTakenInMs < MAX_VALID_DATE_IN_MS);
+    }
+
+    public boolean isDateModifiedValid() {
+        return (mDateModifiedInSec > MIN_VALID_DATE_IN_SEC && mDateModifiedInSec < MAX_VALID_DATE_IN_SEC);
+    }
+
+    public boolean isDateAddedValid() {
+        return (mDateAddedInSec > MIN_VALID_DATE_IN_SEC && mDateAddedInSec < MAX_VALID_DATE_IN_SEC);
+    }
+
+    public boolean isPicassaItem() {
+        return (mParentMediaSet != null && mParentMediaSet.isPicassaAlbum());
+    }
+
+    public int getMediaType() {
+        if (mMediaType == -1) {
+            // Default to image if mMimetype is null or not video.
+            mMediaType = (mMimeType != null && mMimeType.startsWith("video/")) ? MediaItem.MEDIA_TYPE_VIDEO : MediaItem.MEDIA_TYPE_IMAGE;
+        }
+        return mMediaType;
+    }
+
+    public void setMediaType(int mediaType) {
+        mMediaType = mediaType;
+    }
+
+    public String getDisplayMimeType() {
+        if (mDisplayMimeType == null && mMimeType != null) {
+            int slashPos = mMimeType.indexOf('/');
+            if (slashPos != -1 && slashPos + 1 < mMimeType.length()) {
+                mDisplayMimeType = mMimeType.substring(slashPos + 1).toUpperCase();                
+            } else {
+                mDisplayMimeType = mMimeType.toUpperCase();
+            }
+        }
+        return (mDisplayMimeType == null) ? "" : mDisplayMimeType;
+    }
+
+    public void setDisplayMimeType(final String displayMimeType) {
+        mDisplayMimeType = displayMimeType;
+    }
+
+    public String getReverseGeocodedLocation(ReverseGeocoder reverseGeocoder) {
+        if (mReverseGeocodedLocation != null) {
+            return mReverseGeocodedLocation;
+        }
+        if (reverseGeocoder == null || !isLatLongValid()) {
+            return null;
+        }
+        // Get the 2 most granular details available.
+        mReverseGeocodedLocation = reverseGeocoder.getReverseGeocodedLocation(mLatitude, mLongitude, 2);
+        return mReverseGeocodedLocation;
+    }
+}
diff --git a/src/com/cooliris/media/MediaItemTexture.java b/src/com/cooliris/media/MediaItemTexture.java
new file mode 100644
index 0000000..917efb7
--- /dev/null
+++ b/src/com/cooliris/media/MediaItemTexture.java
@@ -0,0 +1,185 @@
+package com.cooliris.media;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.os.Process;
+
+import com.cooliris.cache.CacheService;
+
+public final class MediaItemTexture extends Texture {
+    public static final int MAX_FACES = 1;
+    private static final String TAG = "MediaItemTexture";
+    private static final int CACHE_HEADER_SIZE = 12;
+
+    private final Config mConfig;
+    private final MediaItem mItem;
+    private Context mContext;
+    private boolean mIsRetrying;
+
+    public static final class Config {
+        public int thumbnailWidth;
+        public int thumbnailHeight;
+    }
+
+    public MediaItemTexture(Context context, Config config, MediaItem item) {
+        mConfig = config;
+        mContext = context;
+        mItem = item;
+    }
+    
+    @Override
+    public boolean isUncachedVideo() {
+        if (isCached())
+            return false;
+        if (mItem.mParentMediaSet == null || mItem.mMimeType == null)
+            return false;
+        if (mItem.mParentMediaSet.mPicasaAlbumId == Shared.INVALID && mItem.mMimeType.contains("video")) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean isCached() {
+        final Config config = mConfig;
+        final MediaItem item = mItem;
+        DiskCache cache = null;
+        MediaSet parentMediaSet = item.mParentMediaSet;
+        if (config != null && parentMediaSet != null && parentMediaSet.mDataSource != null) {
+            cache = parentMediaSet.mDataSource.getThumbnailCache();
+            if (cache == LocalDataSource.sThumbnailCache) {
+                if (item.mMimeType.contains("video")) {
+                    cache = LocalDataSource.sThumbnailCacheVideo;
+                }
+            } 
+        }
+        if (cache == null) {
+            return false;
+        }
+        synchronized (cache) {
+            long id = parentMediaSet.mPicasaAlbumId == Shared.INVALID ? Utils.Crc64Long(item.mFilePath) : item.mId;
+            return cache.isDataAvailable(id, item.mDateModifiedInSec * 1000);
+        }
+    }
+
+    protected Bitmap load(RenderView view) {
+
+        final Config config = mConfig;
+        final MediaItem item = mItem;
+
+        // Special case for non-MediaStore content URIs, do not cache the thumbnail.
+        String uriString = item.mContentUri;
+        if (uriString != null) {
+            Uri uri = Uri.parse(uriString);
+            if (uri.getScheme().equals("content") && !uri.getAuthority().equals("media")) {
+                try {
+                    return UriTexture.createFromUri(mContext, item.mThumbnailUri, 128, 128, 0, null);
+                } catch (IOException e) {
+                    return null;
+                } catch (URISyntaxException e) {
+                    return null;
+                }
+            }
+        }
+
+        // Look up the thumbnail in the disk cache.
+        if (config == null) {
+            Bitmap retVal = null;
+            try {
+                if (mItem.getMediaType() == MediaItem.MEDIA_TYPE_IMAGE) {
+                    Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
+                    try {
+                        retVal = UriTexture.createFromUri(mContext, mItem.mContentUri, UriTexture.MAX_RESOLUTION,
+                                UriTexture.MAX_RESOLUTION, Utils.Crc64Long(item.mFilePath), null);
+                    } catch (IOException e) {
+                        ;
+                    } catch (URISyntaxException e) {
+                        ;
+                    }
+                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+                } else {
+                    Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
+                    new Thread() {
+                        public void run() {
+                            try {
+                            Thread.sleep(5000);
+                            } catch (InterruptedException e) {
+                                ;
+                            }
+                            MediaStore.Video.Thumbnails.cancelThumbnailRequest(mContext.getContentResolver(), mItem.mId);
+                        }
+                    }.start();
+                    retVal = MediaStore.Video.Thumbnails.getThumbnail(mContext.getContentResolver(), mItem.mId,
+                            MediaStore.Video.Thumbnails.MINI_KIND, null);
+                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+                }
+            } catch (OutOfMemoryError e) {
+                Log.i(TAG, "Bitmap creation fail, outofmemory");
+                view.handleLowMemory();
+                try {
+                    if (!mIsRetrying) {
+                        Thread.sleep(1000);
+                        mIsRetrying = true;
+                        retVal = load(view);
+                    }
+                } catch (InterruptedException eInterrupted) {
+
+                }
+            }
+            return retVal;
+        } else {
+            byte[] data = null;
+            MediaSet parentMediaSet = item.mParentMediaSet;
+            if (parentMediaSet != null && parentMediaSet.mPicasaAlbumId != Shared.INVALID) {
+                DiskCache thumbnailCache = parentMediaSet.mDataSource.getThumbnailCache();
+                data = thumbnailCache.get(item.mId, 0);
+                if (data == null) {
+                    // We need to generate the cache.
+                    try {
+                        Bitmap retVal = UriTexture.createFromUri(mContext, item.mThumbnailUri, 256, 256, 0, null);
+                        data = CacheService.writeBitmapToCache(thumbnailCache, item.mId, item.mId, retVal, config.thumbnailWidth,
+                                config.thumbnailHeight);
+                    } catch (IOException e) {
+                        return null;
+                    } catch (URISyntaxException e) {
+                        return null;
+                    }
+                }
+            } else {
+                data = CacheService.queryThumbnail(mContext, Utils.Crc64Long(item.mFilePath), item.mId,
+                        item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO, item.mDateModifiedInSec * 1000);
+            }
+            if (data != null) {
+                try {
+                    // Parse record header.
+                    final ByteArrayInputStream cacheInput = new ByteArrayInputStream(data);
+                    final DataInputStream dataInput = new DataInputStream(cacheInput);
+                    item.mThumbnailId = dataInput.readLong();
+                    item.mThumbnailFocusX = dataInput.readShort();
+                    item.mThumbnailFocusY = dataInput.readShort();
+                    // Decode the thumbnail.
+                    final BitmapFactory.Options options = new BitmapFactory.Options();
+                    options.inDither = true;
+                    options.inScaled = false;
+                    options.inPreferredConfig = Bitmap.Config.RGB_565;
+                    final Bitmap bitmap = BitmapFactory.decodeByteArray(data, CACHE_HEADER_SIZE, data.length - CACHE_HEADER_SIZE,
+                            options);
+                    return bitmap;
+                } catch (IOException e) {
+                    // Fall through to regenerate the cached thumbnail.
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/com/cooliris/media/MediaSet.java b/src/com/cooliris/media/MediaSet.java
new file mode 100644
index 0000000..a40a8fc
--- /dev/null
+++ b/src/com/cooliris/media/MediaSet.java
@@ -0,0 +1,274 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+
+public class MediaSet {
+    public static final int TYPE_SMART = 0;
+    public static final int TYPE_FOLDER = 1;
+    public static final int TYPE_USERDEFINED = 2;
+
+    public long mId;
+    public String mName;
+
+    public boolean mHasImages;
+    public boolean mHasVideos;
+
+    // The type of the media set. A smart media set is an automatically generated media set. For example, the most recently
+    // viewed items media set is a media set that gets populated by the contents of a folder. A user defined media set
+    // is a set that is made by the user. This would typically correspond to media items belonging to an event.
+    public int mType;
+
+    // The min and max date taken and added at timestamps.
+    public long mMinTimestamp = Long.MAX_VALUE;
+    public long mMaxTimestamp = 0;
+    public long mMinAddedTimestamp = Long.MAX_VALUE;
+    public long mMaxAddedTimestamp = 0;
+
+    // The latitude and longitude of the min latitude point.
+    public double mMinLatLatitude = LocationMediaFilter.LAT_MAX;
+    public double mMinLatLongitude;
+    // The latitude and longitude of the max latitude point.
+    public double mMaxLatLatitude = LocationMediaFilter.LAT_MIN;
+    public double mMaxLatLongitude;
+    // The latitude and longitude of the min longitude point.
+    public double mMinLonLatitude;
+    public double mMinLonLongitude = LocationMediaFilter.LON_MAX;
+    // The latitude and longitude of the max longitude point.
+    public double mMaxLonLatitude;
+    public double mMaxLonLongitude = LocationMediaFilter.LON_MIN;
+
+    // Reverse geocoding the latitude, longitude and getting an address or location.
+    public String mReverseGeocodedLocation;
+    // Set to true if at least one item in the set has a valid latitude and longitude.
+    public boolean mLatLongDetermined = false;
+    public boolean mReverseGeocodedLocationComputed = false;
+    public boolean mReverseGeocodedLocationRequestMade = false;
+
+    public String mTitleString;
+    public String mTruncTitleString;
+    public String mNoCountTitleString;
+
+    public String mEditUri = null;
+    public long mPicasaAlbumId = Shared.INVALID;
+
+    public DataSource mDataSource;
+    public boolean mSyncPending = false;
+
+    private ArrayList<MediaItem> mItems;
+    public int mNumItemsLoaded = 0;
+    // mNumExpectedItems is preset to how many items are expected to be in the set as it is used to visually
+    // display the number of items in the set and we don't want this display to keep changing as items get loaded.
+    private int mNumExpectedItems = 0;
+    private boolean mNumExpectedItemsCountAccurate = false;
+
+    public MediaSet() {
+        this(null);
+    }
+
+    public MediaSet(DataSource dataSource) {
+        mItems = new ArrayList<MediaItem>(16);
+        mDataSource = dataSource;
+        // TODO(Venkat): Can we move away from this dummy item setup?
+        MediaItem item = new MediaItem();
+        item.mId = Shared.INVALID;
+        item.mParentMediaSet = this;
+        mItems.add(item);
+        mNumExpectedItems = 16;
+    }
+
+    /**
+     * @return underlying ArrayList of MediaItems. Use only for iteration (read operations) on the ArrayList.
+     */
+    public ArrayList<MediaItem> getItems() {
+        return mItems;
+    }
+
+    public void setNumExpectedItems(int numExpectedItems) {
+        mItems.ensureCapacity(numExpectedItems);
+        mNumExpectedItems = numExpectedItems;
+        mNumExpectedItemsCountAccurate = true;
+    }
+
+    public int getNumExpectedItems() {
+        return mNumExpectedItems;
+    }
+    
+    public boolean setContainsValidItems() {
+        if (mNumExpectedItems == 0)
+            return false;
+        return true;
+    }
+
+    public void updateNumExpectedItems() {
+        mNumExpectedItems = mNumItemsLoaded;
+        mNumExpectedItemsCountAccurate = true;
+    }
+
+    public int getNumItems() {
+        return mItems.size();
+    }
+
+    public void clear() {
+        mItems.clear();
+        // TODO(Venkat): Can we move away from this dummy item setup?
+        MediaItem item = new MediaItem();
+        item.mId = Shared.INVALID;
+        item.mParentMediaSet = this;
+        mItems.add(item);
+        mNumExpectedItems = 16;
+        mNumExpectedItemsCountAccurate = false;
+        mNumItemsLoaded = 0;
+    }
+
+    /**
+     * Generates the label for the MediaSet.
+     */
+    public void generateTitle(final boolean truncateTitle) {
+        if (mName == null) {
+            mName = "";
+        }
+        String size = (mNumExpectedItemsCountAccurate) ? "  (" + mNumExpectedItems + ")" : "";
+        mTitleString = mName + size;
+        if (truncateTitle) {
+            int length = mName.length();
+            mTruncTitleString = (length > 16) ? mName.substring(0, 12) + "É" + mName.substring(length - 4, length) + size : mName + size;
+            mNoCountTitleString = mName;
+        } else {
+            mTruncTitleString = mTitleString;
+        }
+    }
+
+    /**
+     * Adds a MediaItem to this set, and increments the load count. Additionally, it also recomputes
+     * the location bounds and time range of the media set.
+     */
+    public void addItem(final MediaItem item) {
+        // Important to not set the parentMediaSet in here as temporary MediaSet's are occasionally
+        // created and we do not want the MediaItem updated as a result of that.
+        if (mItems.size() == 0) {
+            mItems.add(item);
+        } else if (mItems.get(0).mId == -1L) {
+            mItems.set(0, item);
+        } else {
+            mItems.add(item);
+        }
+        if (item == null) {
+            return;
+        }
+        if (item.mId != Shared.INVALID) {
+            ++mNumItemsLoaded;
+        }
+        if (item.isDateTakenValid()) {
+            long dateTaken = item.mDateTakenInMs;
+            if (dateTaken < mMinTimestamp) {
+                mMinTimestamp = dateTaken;
+            }
+            if (dateTaken > mMaxTimestamp) {
+                mMaxTimestamp = dateTaken;
+            }
+        } else if (item.isDateAddedValid()) {
+            long dateTaken = item.mDateAddedInSec * 1000;
+            if (dateTaken < mMinAddedTimestamp) {
+                mMinAddedTimestamp = dateTaken;
+            }
+            if (dateTaken > mMaxAddedTimestamp) {
+                mMaxAddedTimestamp = dateTaken;
+            }            
+        }
+
+        // Determining the latitude longitude bounds of the set and setting the location string.
+        if (!item.isLatLongValid()) {
+            return;
+        }
+        double itemLatitude = item.mLatitude;
+        double itemLongitude = item.mLongitude;
+        if (mMinLatLatitude > itemLatitude) {
+            mMinLatLatitude = itemLatitude;
+            mMinLatLongitude = itemLongitude;
+            mLatLongDetermined = true;
+        }
+        if (mMaxLatLatitude < itemLatitude) {
+            mMaxLatLatitude = itemLatitude;
+            mMaxLatLongitude = itemLongitude;
+            mLatLongDetermined = true;
+        }
+        if (mMinLonLongitude > itemLongitude) {
+            mMinLonLatitude = itemLatitude;
+            mMinLonLongitude = itemLongitude;
+            mLatLongDetermined = true;
+        }
+        if (mMaxLonLongitude < itemLongitude) {
+            mMaxLonLatitude = itemLatitude;
+            mMaxLonLongitude = itemLongitude;
+            mLatLongDetermined = true;
+        }
+    }
+
+    /**
+     * Removes a MediaItem if present in the MediaSet.
+     * 
+     * @return true if the item was removed, false if removal failed or item was not present in the set.
+     */
+    public boolean removeItem(final MediaItem itemToRemove) {
+        if (mItems.remove(itemToRemove)) {
+            --mNumExpectedItems;
+            --mNumItemsLoaded;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @return true if this MediaSet contains the argument MediaItem.
+     */
+    public boolean containsItem(final MediaItem item) {
+        return ArrayUtils.contains(mItems, item);
+    }
+
+    /**
+     * @return true if the title string is truncated.
+     */
+    public boolean isTruncated() {
+        return (mTitleString != null && !mTitleString.equals(mTruncTitleString));
+    }
+
+    /**
+     * @return true if timestamps are available for this set.
+     */
+    public boolean areTimestampsAvailable() {
+        return (mMinTimestamp < Long.MAX_VALUE && mMaxTimestamp > 0);
+    }
+
+    /**
+     * @return true if the added timestamps are available for this set.
+     */
+    public boolean areAddedTimestampsAvailable() {
+        return (mMinAddedTimestamp < Long.MAX_VALUE && mMaxAddedTimestamp > 0);
+    }
+
+    /**
+     * @return true if this set of items corresponds to Picassa items.
+     */
+    public boolean isPicassaSet() {
+        // 2 cases:-
+        // 1. This set is just a Picassa Album, and all its items are therefore from Picassa.
+        // 2. This set is a random collection of items and each item is a Picassa item.
+        if (isPicassaAlbum()) {
+            return true;
+        } 
+        int numItems = mItems.size();
+        for (int i = 0; i < numItems; i++) {
+            if (!mItems.get(i).isPicassaItem()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * @return true if this set is a Picassa album.
+     */
+    public boolean isPicassaAlbum() {
+        return (mPicasaAlbumId != Shared.INVALID);
+    }
+}
diff --git a/src/com/cooliris/media/MenuBar.java b/src/com/cooliris/media/MenuBar.java
new file mode 100644
index 0000000..45bdd8f
--- /dev/null
+++ b/src/com/cooliris/media/MenuBar.java
@@ -0,0 +1,500 @@
+package com.cooliris.media;
+
+import java.util.HashMap;
+
+import javax.microedition.khronos.opengles.GL11;
+
+import android.content.Context;
+import android.view.MotionEvent;
+
+public final class MenuBar extends Layer implements PopupMenu.Listener {
+    public static final int HEIGHT = 45;
+
+    public static final StringTexture.Config MENU_TITLE_STYLE_TEXT = new StringTexture.Config();
+    private static final StringTexture.Config MENU_TITLE_STYLE = new StringTexture.Config();
+    private static final int MENU_HIGHLIGHT_EDGE_WIDTH = 21;
+    private static final int MENU_HIGHLIGHT_EDGE_INSET = 9;
+    private static final long LONG_PRESS_THRESHOLD_MS = 350;
+    private static final int HIT_TEST_MARGIN = 15;
+
+    static {
+        MENU_TITLE_STYLE.fontSize = 17 * Gallery.PIXEL_DENSITY;
+        MENU_TITLE_STYLE.sizeMode = StringTexture.Config.SIZE_EXACT;
+        MENU_TITLE_STYLE.overflowMode = StringTexture.Config.OVERFLOW_FADE;
+        
+        MENU_TITLE_STYLE_TEXT.fontSize = 15 * Gallery.PIXEL_DENSITY;
+        MENU_TITLE_STYLE_TEXT.xalignment = StringTexture.Config.ALIGN_HCENTER;
+        MENU_TITLE_STYLE_TEXT.sizeMode = StringTexture.Config.SIZE_EXACT;
+        MENU_TITLE_STYLE_TEXT.overflowMode = StringTexture.Config.OVERFLOW_FADE;
+    }
+
+    private boolean mNeedsLayout = false;
+    private Menu[] mMenus = {};
+    private int mTouchMenu = -1;
+    private int mTouchMenuItem = -1;
+    private boolean mTouchActive = false;
+    private boolean mTouchOverMenu = false;
+    private final PopupMenu mSubmenu;
+    private static final int BACKGROUND = R.drawable.selection_menu_bg;
+    private static final int SEPERATOR = R.drawable.selection_menu_divider;
+    private static final int MENU_HIGHLIGHT_LEFT = R.drawable.selection_menu_bg_pressed_left;
+    private static final int MENU_HIGHLIGHT_MIDDLE = R.drawable.selection_menu_bg_pressed;
+    private static final int MENU_HIGHLIGHT_RIGHT = R.drawable.selection_menu_bg_pressed_right;
+    private final HashMap<String, Texture> mTextureMap = new HashMap<String, Texture>();
+    private GL11 mGL;
+
+    private boolean mSecondTouch;
+
+    public MenuBar(Context context) {
+        mSubmenu = new PopupMenu(context);
+        mSubmenu.setListener(this);
+    }
+
+    public Menu[] getMenus() {
+        return mMenus;
+    }
+
+    public void setMenus(Menu[] menus) {
+        mMenus = menus;
+        mNeedsLayout = true;
+    }
+
+    public void updateMenu(Menu menu, int index) {
+        mMenus[index] = menu;
+        mNeedsLayout = true;
+    }
+
+    @Override
+    protected void onHiddenChanged() {
+        if (mHidden) {
+            mSubmenu.close(false);
+        }
+    }
+    
+    @Override
+    protected void onSizeChanged() {
+        mNeedsLayout = true;
+    }
+    
+    @Override
+    public void generate(RenderView view, RenderView.Lists lists) {
+        lists.blendedList.add(this);
+        lists.hitTestList.add(this);
+        lists.systemList.add(this);
+        lists.updateList.add(this);
+        mSubmenu.generate(view, lists);
+    }
+
+    @Override
+    public void renderBlended(RenderView view, GL11 gl) {
+        // Layout if needed.
+        if (mNeedsLayout) {
+            layoutMenus();
+            mNeedsLayout = false;
+        }
+        if (mGL != gl) {
+        	mTextureMap.clear();
+        	mGL = gl;
+        }
+        
+        // Draw the background.
+        Texture background = view.getResource(BACKGROUND);
+        int backgroundHeight = background.getHeight();
+        int menuHeight = (int)(HEIGHT * Gallery.PIXEL_DENSITY + 0.5f);
+        int extra = background.getHeight() - menuHeight;
+        view.draw2D(background, mX, mY - extra, mWidth, backgroundHeight);
+
+        // Draw the separators.
+        Menu[] menus = mMenus;
+        int numMenus = menus.length;
+        int y = (int) mY;
+        if (view.bind(view.getResource(SEPERATOR))) {
+            for (int i = 1; i < numMenus; ++i) {
+                view.draw2D(menus[i].x, y, 0, 1, menuHeight);
+            }
+        }
+
+        // Draw the selection / focus highlight.
+        int touchMenu = mTouchMenu;
+        if (canDrawHighlight()) {
+            drawHighlight(view, gl, touchMenu);
+        }
+        
+        // Draw labels.
+        float height = mHeight;
+        for (int i = 0; i != numMenus; ++i) {
+            // Draw the icon and title.
+            Menu menu = menus[i];
+            ResourceTexture icon = view.getResource(menu.icon);
+            
+            StringTexture titleTexture = (StringTexture) mTextureMap.get(menu.title);
+            if (titleTexture == null) {
+            	titleTexture = new StringTexture(menu.title, menu.config, menu.titleWidth, MENU_TITLE_STYLE.height);
+                view.loadTexture(titleTexture);
+                menu.titleTexture = titleTexture;
+                mTextureMap.put(menu.title, titleTexture);
+            }
+            int iconWidth = icon != null ? icon.getWidth() : 0;
+            int width = iconWidth + menu.titleWidth;
+            int offset = (menu.mWidth - width) / 2;
+            if (icon != null) {
+                float iconY = y + (height - icon.getHeight()) / 2;
+                view.draw2D(icon, menu.x + offset, iconY);
+            }
+            float titleY = y + (height - MENU_TITLE_STYLE.height) / 2 + 1;
+            //Log.i("MENUBAR", "Drawing label (" + title.getWidth() + ", " + title.getHeight() + ", " + title.mNormalizedWidth + ")");
+            view.draw2D(titleTexture, menu.x + offset + iconWidth, titleY);
+        }
+    }
+    
+    private void drawHighlight(RenderView view, GL11 gl, int touchMenu) {
+        Texture highlightLeft = view.getResource(MENU_HIGHLIGHT_LEFT);
+        Texture highlightMiddle = view.getResource(MENU_HIGHLIGHT_MIDDLE);
+        Texture highlightRight = view.getResource(MENU_HIGHLIGHT_RIGHT);
+
+        int height = highlightLeft.getHeight();
+        int extra = height - (int)(HEIGHT * Gallery.PIXEL_DENSITY);
+        Menu menu = mMenus[touchMenu];
+        int x = menu.x + (int)(MENU_HIGHLIGHT_EDGE_INSET * Gallery.PIXEL_DENSITY);
+        int width = menu.mWidth - (int)((MENU_HIGHLIGHT_EDGE_INSET * 2) * Gallery.PIXEL_DENSITY);
+        int y = (int) mY - extra;
+
+        // Draw left edge.
+        view.draw2D(highlightLeft, x - MENU_HIGHLIGHT_EDGE_WIDTH * Gallery.PIXEL_DENSITY, y, MENU_HIGHLIGHT_EDGE_WIDTH * Gallery.PIXEL_DENSITY, height);
+
+        // Draw middle.
+        view.draw2D(highlightMiddle, x, y, width, height);
+
+        // Draw right edge.
+        view.draw2D(highlightRight, x + width, y, MENU_HIGHLIGHT_EDGE_WIDTH * Gallery.PIXEL_DENSITY, height);
+    }
+
+    private int hitTestMenu(int x, int y) {
+        if (y > mY - HIT_TEST_MARGIN * Gallery.PIXEL_DENSITY) {
+            Menu[] menus = mMenus;
+            for (int i = menus.length - 1; i >= 0; --i) {
+                if (x > menus[i].x) {
+                    if (menus[i].onSelect != null || menus[i].options != null || menus[i].onSingleTapUp != null) {
+                        return i;
+                    } else {
+                        return -1;
+                    }
+                }
+            }
+        }
+        return -1;
+    }
+
+    private void selectMenu(int index) {
+        int oldIndex = mTouchMenu;
+        if (oldIndex != index) {
+            // Notify on deselect.
+            Menu[] menus = mMenus;
+            if (oldIndex != -1) {
+                Menu oldMenu = menus[oldIndex];
+                if (oldMenu.onDeselect != null) {
+                    oldMenu.onDeselect.run();
+                }
+            }
+
+            // Select the new menu.
+            mTouchMenu = index;
+            mTouchMenuItem = -1;
+
+            // Show the submenu for the selected menu if one is provided.
+            PopupMenu submenu = mSubmenu;
+            boolean didShow = false;
+            if (index != -1) {
+                // Notify on select.
+                Menu menu = mMenus[index];
+                if (menu.onSelect != null) {
+                    menu.onSelect.run();
+                }
+
+                // Show the popup menu if options are provided.
+                PopupMenu.Option[] options = menu.options;
+                if (options != null) {
+                    int x = (int) mX + menu.x + menu.mWidth / 2;
+                    int y = (int) mY;
+                    didShow = true;
+                    submenu.setOptions(options);
+                    submenu.showAtPoint(x, y, (int)mWidth, (int)mHeight);
+                }
+            }
+            if (!didShow) {
+                submenu.close(true);
+            }
+        }
+    }
+
+    public void close() {
+        int oldIndex = mTouchMenu;
+        if (oldIndex != -1) {
+            // Notify on deselect.
+            Menu[] menus = mMenus;
+            if (oldIndex != -1) {
+                Menu oldMenu = menus[oldIndex];
+                if (oldMenu.onDeselect != null) {
+                    oldMenu.onDeselect.run();
+                }
+            }
+            oldIndex = -1;
+        }
+        selectMenu(-1);
+        if (mSubmenu != null)
+            mSubmenu.close(false);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        int x = (int) event.getX();
+        int y = (int) event.getY();
+        int hit = hitTestMenu(x, y);
+        int action = event.getAction();
+        switch (action) {
+        case MotionEvent.ACTION_DOWN:
+            mTouchActive = true;
+            if (mTouchMenu == hit) {
+                mSecondTouch = true;
+            } else {
+                mSecondTouch = false;
+            }
+        case MotionEvent.ACTION_MOVE:
+            // Determine which menu the touch is over.
+            if (hit != -1) {
+                // Select the menu and invoke the action.
+                selectMenu(hit);
+                mTouchOverMenu = true;
+            } else {
+                // Forward events outside the menubar to the active popup menu.
+                mTouchOverMenu = false;
+            }
+            mSubmenu.onTouchEvent(event);
+            break;
+        case MotionEvent.ACTION_UP:
+            if (mTouchMenu == hit && mSecondTouch) {
+                mSubmenu.close(true);
+                mTouchMenu = -1;
+                break;
+            }
+            // Forward event to submenu.
+            mSubmenu.onTouchEvent(event);
+
+            // Leave the submenu open if the touch ends on the menu button in less than
+            // a time threshold.
+            long elapsed = event.getEventTime() - event.getDownTime();
+            if (hit != -1) {
+                // Notify on single tap.
+                Menu menu = mMenus[hit];
+                if (menu.onSingleTapUp != null) {
+                    menu.onSingleTapUp.run();
+                }
+                if (menu.options == null)
+                    selectMenu(-1);
+            } else if (elapsed > LONG_PRESS_THRESHOLD_MS) {
+                selectMenu(-1);
+            }
+            break;
+
+        case MotionEvent.ACTION_CANCEL:
+            // Always deselect if canceled.
+            selectMenu(-1);
+            break;
+        }
+        return true;
+    }
+
+    private boolean canDrawHighlight() {
+        return mTouchMenu != -1 && mTouchMenuItem == -1 && (!mTouchActive || mTouchOverMenu);
+    }
+
+    private void layoutMenus() {
+    	mTextureMap.clear();
+    	
+        Menu[] menus = mMenus;
+        int numMenus = menus.length;
+        // we do the best attempt to fit the menu items and resize them
+        // also, it tries to minimize different sized menu items
+        // it finds the maximum width for a set of menu items, and checks whether that width
+        // can be used for all the cells, else, it goes to the next maximum width, so on and
+        // so forth
+        if (numMenus != 0) {
+            float viewWidth = mWidth;
+            int occupiedWidth = 0;
+            int previousMaxWidth = Integer.MAX_VALUE;
+            int totalDesiredWidth = 0;
+            
+            for (int i = 0; i < numMenus; i++) {
+                totalDesiredWidth += menus[i].computeRequiredWidth();
+            }
+            
+            if (totalDesiredWidth > viewWidth) {
+            	// Just split the menus up by available size / nr of menus.
+            	int widthPerMenu = (int) Math.floor(viewWidth / numMenus);
+            	int x = 0;
+            	
+            	for (int i = 0; i < numMenus; i++) {
+            		Menu menu = menus[i];
+            		menu.x = x;
+            		menu.mWidth = widthPerMenu;
+            		menu.titleWidth = widthPerMenu - (20 + (menu.icon != 0 ? 45 : 0)); //TODO factor out padding etc
+
+            		// fix up rounding errors by adding the last pixel to the last menu. 
+            		if (i == numMenus - 1) {
+            			menu.mWidth = (int) viewWidth - x;
+            		}
+            		x += widthPerMenu;
+            		
+            	}
+            } else {
+                boolean foundANewMaxWidth = true;
+                int menusProcessed = 0;
+            	
+            	while (foundANewMaxWidth && menusProcessed < numMenus) {
+	                foundANewMaxWidth = false;
+	                int maxWidth = 0;
+	                for (int i = 0; i < numMenus; ++i) {
+	                    int width = menus[i].computeRequiredWidth();
+	                    if (width > maxWidth && width < previousMaxWidth) {
+	                        foundANewMaxWidth = true;
+	                        maxWidth = width;
+	                    }
+	                }
+	                // can all the menus have this width
+	                int cumulativeWidth = maxWidth * (numMenus - menusProcessed) + occupiedWidth;
+	                if (cumulativeWidth < viewWidth || !foundANewMaxWidth || menusProcessed == numMenus - 1) {
+	                    float delta = (viewWidth - cumulativeWidth) / numMenus;
+	                    if (delta < 0) {
+	                        delta = 0;
+	                    }
+	                    int x = 0;
+	                    for (int i = 0; i < numMenus; ++i) {
+	                        Menu menu = menus[i];
+	                        menu.x = x;
+	                        float width = menus[i].computeRequiredWidth();
+	                        if (width < maxWidth) {
+	                            width = maxWidth + delta;
+	                        } else {
+	                            width += delta;
+	                        }
+	                        menu.mWidth = (int)width;
+	                        menu.titleWidth = StringTexture.computeTextWidthForConfig(menu.title, menu.config);  //(int)menus[i].title.computeTextWidth();
+	                        x += width;
+	                    }
+	                    break;
+	                } else {
+	                    ++menusProcessed;
+	                    previousMaxWidth = maxWidth;
+	                    occupiedWidth += maxWidth;
+	                }
+	            }
+            }
+        }
+    }
+
+    public static final class Menu {
+        public final String title;
+        public StringTexture titleTexture = null;
+        public int titleWidth = 0;
+        public final StringTexture.Config config;
+        public final int icon;
+        public final Runnable onSelect;
+        public final Runnable onDeselect;
+        public final Runnable onSingleTapUp;
+        public final boolean resizeToAccomodate;
+        public PopupMenu.Option[] options;
+        private int x;
+        private int mWidth;
+        private static final float ICON_WIDTH = 45.0f;
+        
+        public static final class Builder {
+            private final String title;
+            private StringTexture.Config config;
+            private int icon = 0;
+            private Runnable onSelect = null;
+            private Runnable onDeselect = null;
+            private Runnable onSingleTapUp = null;
+            private PopupMenu.Option[] options = null;
+            private boolean resizeToAccomodate;
+
+            public Builder(String title) {
+                this.title = title;
+                config = MENU_TITLE_STYLE;
+            }
+
+            public Builder config(StringTexture.Config config) {
+                this.config = config;
+                return this;
+            }
+
+            public Builder resizeToAccomodate() {
+                this.resizeToAccomodate = true;
+                return this;
+            }
+
+            public Builder icon(int icon) {
+                this.icon = icon;
+                return this;
+            }
+
+            public Builder onSelect(Runnable onSelect) {
+                this.onSelect = onSelect;
+                return this;
+            }
+
+            public Builder onDeselect(Runnable onDeselect) {
+                this.onDeselect = onDeselect;
+                return this;
+            }
+
+            public Builder onSingleTapUp(Runnable onSingleTapUp) {
+                this.onSingleTapUp = onSingleTapUp;
+                return this;
+            }
+
+            public Builder options(PopupMenu.Option[] options) {
+                this.options = options;
+                return this;
+            }
+
+            public Menu build() {
+                return new Menu(this);
+            }
+        }
+
+        private Menu(Builder builder) {
+            config = builder.config;
+            title = builder.title;  //new StringTexture(builder.title, config);
+            icon = builder.icon;
+            onSelect = builder.onSelect;
+            onDeselect = builder.onDeselect;
+            onSingleTapUp = builder.onSingleTapUp;
+            options = builder.options;
+            resizeToAccomodate = builder.resizeToAccomodate;
+        }
+
+        public int computeRequiredWidth() {
+            int width = 0;
+            if (icon != 0) {
+                width += (ICON_WIDTH); // * Gallery.PIXEL_DENSITY);
+            }
+            if (title != null) {
+                width += StringTexture.computeTextWidthForConfig(title, config);//title.computeTextWidth();
+            }
+            // pad it
+            width += 20;
+            if (width < HEIGHT)
+                width = HEIGHT;
+            return width;
+        }
+        
+    }
+
+    public void onSelectionChanged(PopupMenu menu, int selectedIndex) {
+        mTouchMenuItem = selectedIndex;
+    }
+
+    public void onSelectionClicked(PopupMenu menu, int selectedIndex) {
+        selectMenu(-1);
+    }
+}
diff --git a/src/com/cooliris/media/MonitoredActivity.java b/src/com/cooliris/media/MonitoredActivity.java
new file mode 100644
index 0000000..423b211
--- /dev/null
+++ b/src/com/cooliris/media/MonitoredActivity.java
@@ -0,0 +1,99 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 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.
+ */
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+
+public class MonitoredActivity extends Activity {
+
+    private final ArrayList<LifeCycleListener> mListeners =
+            new ArrayList<LifeCycleListener>();
+
+    public static interface LifeCycleListener {
+        public void onActivityCreated(MonitoredActivity activity);
+        public void onActivityDestroyed(MonitoredActivity activity);
+        public void onActivityPaused(MonitoredActivity activity);
+        public void onActivityResumed(MonitoredActivity activity);
+        public void onActivityStarted(MonitoredActivity activity);
+        public void onActivityStopped(MonitoredActivity activity);
+    }
+
+    public static class LifeCycleAdapter implements LifeCycleListener {
+        public void onActivityCreated(MonitoredActivity activity) {
+        }
+
+        public void onActivityDestroyed(MonitoredActivity activity) {
+        }
+
+        public void onActivityPaused(MonitoredActivity activity) {
+        }
+
+        public void onActivityResumed(MonitoredActivity activity) {
+        }
+
+        public void onActivityStarted(MonitoredActivity activity) {
+        }
+
+        public void onActivityStopped(MonitoredActivity activity) {
+        }
+    }
+
+    public void addLifeCycleListener(LifeCycleListener listener) {
+        if (mListeners.contains(listener)) return;
+        mListeners.add(listener);
+    }
+
+    public void removeLifeCycleListener(LifeCycleListener listener) {
+        mListeners.remove(listener);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        for (LifeCycleListener listener : mListeners) {
+            listener.onActivityCreated(this);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        for (LifeCycleListener listener : mListeners) {
+            listener.onActivityDestroyed(this);
+        }
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        for (LifeCycleListener listener : mListeners) {
+            listener.onActivityStarted(this);
+        }
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        for (LifeCycleListener listener : mListeners) {
+            listener.onActivityStopped(this);
+        }
+    }
+}
+
diff --git a/src/com/cooliris/media/MovieView.java b/src/com/cooliris/media/MovieView.java
new file mode 100644
index 0000000..b4d4cd9
--- /dev/null
+++ b/src/com/cooliris/media/MovieView.java
@@ -0,0 +1,75 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 2007 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.
+ */
+
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.view.View;
+
+/**
+ * This activity plays a video from a specified URI.
+ */
+public class MovieView extends Activity  {
+    @SuppressWarnings("unused")
+    private static final String TAG = "MovieView";
+
+    private MovieViewControl mControl;
+    private boolean mFinishOnCompletion;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setContentView(R.layout.movie_view);
+        View rootView = findViewById(R.id.root);
+        Intent intent = getIntent();
+        mControl = new MovieViewControl(rootView, this, intent.getData()) {
+            @Override
+            public void onCompletion() {
+                if (mFinishOnCompletion) {
+                    finish();
+                }
+            }
+        };
+        if (intent.hasExtra(MediaStore.EXTRA_SCREEN_ORIENTATION)) {
+            int orientation = intent.getIntExtra(
+                    MediaStore.EXTRA_SCREEN_ORIENTATION,
+                    ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+            if (orientation != getRequestedOrientation()) {
+                setRequestedOrientation(orientation);
+            }
+        }
+        mFinishOnCompletion = intent.getBooleanExtra(
+                MediaStore.EXTRA_FINISH_ON_COMPLETION, true);
+    }
+
+    @Override
+    public void onPause() {
+        mControl.onPause();
+        super.onPause();
+    }
+
+    @Override
+    public void onResume() {
+        mControl.onResume();
+        super.onResume();
+    }
+}
+
diff --git a/src/com/cooliris/media/MovieViewControl.java b/src/com/cooliris/media/MovieViewControl.java
new file mode 100644
index 0000000..3f9161d
--- /dev/null
+++ b/src/com/cooliris/media/MovieViewControl.java
@@ -0,0 +1,256 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 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.
+ */
+
+
+import android.app.AlertDialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Video;
+import android.view.View;
+import android.widget.MediaController;
+import android.widget.VideoView;
+
+public class MovieViewControl implements MediaPlayer.OnErrorListener,
+        MediaPlayer.OnCompletionListener {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "MovieViewControl";
+
+    private static final int ONE_MINUTE = 60 * 1000;
+    private static final int TWO_MINUTES = 2 * ONE_MINUTE;
+    private static final int FIVE_MINUTES = 5 * ONE_MINUTE;
+
+    // Copied from MediaPlaybackService in the Music Player app. Should be
+    // public, but isn't.
+    private static final String SERVICECMD =
+            "com.android.music.musicservicecommand";
+    private static final String CMDNAME = "command";
+    private static final String CMDPAUSE = "pause";
+
+    private final VideoView mVideoView;
+    private final View mProgressView;
+    private final Uri mUri;
+    private final ContentResolver mContentResolver;
+
+    // State maintained for proper onPause/OnResume behaviour.
+    private int mPositionWhenPaused = -1;
+    private boolean mWasPlayingWhenPaused = false;
+
+    Handler mHandler = new Handler();
+
+    Runnable mPlayingChecker = new Runnable() {
+        public void run() {
+            if (mVideoView.isPlaying()) {
+                mProgressView.setVisibility(View.GONE);
+            } else {
+                mHandler.postDelayed(mPlayingChecker, 250);
+            }
+        }
+    };
+    
+    public static String formatDuration(final Context context,
+            int durationMs) {
+        int duration = durationMs / 1000;
+        int h = duration / 3600;
+        int m = (duration - h * 3600) / 60;
+        int s = duration - (h * 3600 + m * 60);
+        String durationValue;
+        if (h == 0) {
+            durationValue = String.format(
+                    context.getString(R.string.details_ms), m, s);
+        } else {
+            durationValue = String.format(
+                    context.getString(R.string.details_hms), h, m, s);
+        }
+        return durationValue;
+    }
+
+    public MovieViewControl(View rootView, Context context, Uri videoUri) {
+        mContentResolver = context.getContentResolver();
+        mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
+        mProgressView = rootView.findViewById(R.id.progress_indicator);
+
+        mUri = videoUri;
+
+        // For streams that we expect to be slow to start up, show a
+        // progress spinner until playback starts.
+        String scheme = mUri.getScheme();
+        if ("http".equalsIgnoreCase(scheme)
+                || "rtsp".equalsIgnoreCase(scheme)) {
+            mHandler.postDelayed(mPlayingChecker, 250);
+        } else {
+            mProgressView.setVisibility(View.GONE);
+        }
+
+        mVideoView.setOnErrorListener(this);
+        mVideoView.setOnCompletionListener(this);
+        mVideoView.setVideoURI(mUri);
+        mVideoView.setMediaController(new MediaController(context));
+
+        // make the video view handle keys for seeking and pausing
+        mVideoView.requestFocus();
+
+        Intent i = new Intent(SERVICECMD);
+        i.putExtra(CMDNAME, CMDPAUSE);
+        context.sendBroadcast(i);
+
+        final Integer bookmark = getBookmark();
+        if (bookmark != null) {
+            AlertDialog.Builder builder = new AlertDialog.Builder(context);
+            builder.setTitle(R.string.resume_playing_title);
+            builder.setMessage(String.format(
+                    context.getString(R.string.resume_playing_message),
+                    formatDuration(context, bookmark)));
+            builder.setOnCancelListener(new OnCancelListener() {
+                public void onCancel(DialogInterface dialog) {
+                    onCompletion();
+                }});
+            builder.setPositiveButton(R.string.resume_playing_resume,
+                    new OnClickListener() {
+                public void onClick(DialogInterface dialog, int which) {
+                    mVideoView.seekTo(bookmark);
+                    mVideoView.start();
+                }});
+            builder.setNegativeButton(R.string.resume_playing_restart,
+                    new OnClickListener() {
+                public void onClick(DialogInterface dialog, int which) {
+                    mVideoView.start();
+                }});
+            builder.show();
+        } else {
+            mVideoView.start();
+        }
+    }
+
+    private static boolean uriSupportsBookmarks(Uri uri) {
+        String scheme = uri.getScheme();
+        String authority = uri.getAuthority();
+        return ("content".equalsIgnoreCase(scheme)
+                && MediaStore.AUTHORITY.equalsIgnoreCase(authority));
+    }
+
+    private Integer getBookmark() {
+        if (!uriSupportsBookmarks(mUri)) {
+            return null;
+        }
+
+        String[] projection = new String[] {
+                Video.VideoColumns.DURATION,
+                Video.VideoColumns.BOOKMARK};
+
+        try {
+            Cursor cursor = mContentResolver.query(
+                    mUri, projection, null, null, null);
+            if (cursor != null) {
+                try {
+                    if (cursor.moveToFirst()) {
+                        int duration = getCursorInteger(cursor, 0);
+                        int bookmark = getCursorInteger(cursor, 1);
+                        if ((bookmark < TWO_MINUTES)
+                                || (duration < FIVE_MINUTES)
+                                || (bookmark > (duration - ONE_MINUTE))) {
+                            return null;
+                        }
+                        return Integer.valueOf(bookmark);
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        } catch (SQLiteException e) {
+            // ignore
+        }
+
+        return null;
+    }
+
+    private static int getCursorInteger(Cursor cursor, int index) {
+        try {
+            return cursor.getInt(index);
+        } catch (SQLiteException e) {
+            return 0;
+        } catch (NumberFormatException e) {
+            return 0;
+        }
+
+    }
+
+    private void setBookmark(int bookmark) {
+        if (!uriSupportsBookmarks(mUri)) {
+            return;
+        }
+
+        ContentValues values = new ContentValues();
+        values.put(Video.VideoColumns.BOOKMARK, Integer.toString(bookmark));
+        try {
+            mContentResolver.update(mUri, values, null, null);
+        } catch (SecurityException ex) {
+            // Ignore, can happen if we try to set the bookmark on a read-only
+            // resource such as a video attached to GMail.
+        } catch (SQLiteException e) {
+            // ignore. can happen if the content doesn't support a bookmark
+            // column.
+        } catch (UnsupportedOperationException e) {
+            // ignore. can happen if the external volume is already detached.
+        }
+    }
+
+    public void onPause() {
+        mHandler.removeCallbacksAndMessages(null);
+        setBookmark(mVideoView.getCurrentPosition());
+
+        mPositionWhenPaused = mVideoView.getCurrentPosition();
+        mWasPlayingWhenPaused = mVideoView.isPlaying();
+        mVideoView.stopPlayback();
+    }
+
+    public void onResume() {
+        if (mPositionWhenPaused >= 0) {
+            mVideoView.setVideoURI(mUri);
+            mVideoView.seekTo(mPositionWhenPaused);
+            if (mWasPlayingWhenPaused) {
+                mVideoView.start();
+            }
+        }
+    }
+
+    public boolean onError(MediaPlayer player, int arg1, int arg2) {
+        mHandler.removeCallbacksAndMessages(null);
+        mProgressView.setVisibility(View.GONE);
+        return false;
+    }
+
+    public void onCompletion(MediaPlayer mp) {
+        onCompletion();
+    }
+
+    public void onCompletion() {
+    }
+}
+
diff --git a/src/com/cooliris/media/PagedFeed.java b/src/com/cooliris/media/PagedFeed.java
new file mode 100644
index 0000000..08b373a
--- /dev/null
+++ b/src/com/cooliris/media/PagedFeed.java
@@ -0,0 +1,9 @@
+package com.cooliris.media;
+
+public abstract class PagedFeed<E> extends VirtualFeed<E> {
+
+    @Override
+    public void setLoadingRange(int begin, int end) {
+    }
+
+}
diff --git a/src/com/cooliris/media/Pair.java b/src/com/cooliris/media/Pair.java
new file mode 100644
index 0000000..2f96886
--- /dev/null
+++ b/src/com/cooliris/media/Pair.java
@@ -0,0 +1,54 @@
+package com.cooliris.media;
+
+/**
+ * A pair of objects. Mostly auto-generated by Eclipse.
+ */
+public class Pair<S, T> {
+    public final S first;
+    public final T second;
+
+    public Pair(S first, T second) {
+        this.first = first;
+        this.second = second;
+    }
+
+    public static <S, T> Pair<S, T> create(S first, T second) {
+        return new Pair<S, T>(first, second);
+    }
+
+    @Override
+    public int hashCode() {
+        final int PRIME = 31;
+        int result = 1;
+        result = PRIME * result + ((first == null) ? 0 : first.hashCode());
+        result = PRIME * result + ((second == null) ? 0 : second.hashCode());
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Pair<S, T> other = (Pair) obj;
+        if (first == null && other.first != null) {
+            return false;
+        } else if (!first.equals(other.first)) {
+            return false;
+        }
+        if (second == null && other.second != null) {
+            return false;
+        } else if (!second.equals(other.second)) {
+            return false;
+        }
+        return true;
+    }
+
+}
diff --git a/src/com/cooliris/media/PathBarLayer.java b/src/com/cooliris/media/PathBarLayer.java
new file mode 100644
index 0000000..e9be7ff
--- /dev/null
+++ b/src/com/cooliris/media/PathBarLayer.java
@@ -0,0 +1,263 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+
+import javax.microedition.khronos.opengles.GL11;
+
+import android.graphics.Typeface;
+import android.view.MotionEvent;
+
+public final class PathBarLayer extends Layer {
+    private static final StringTexture.Config sPathFormat = new StringTexture.Config();
+    private final ArrayList<Component> mComponents = new ArrayList<Component>();
+    private static final int FILL = R.drawable.pathbar_bg;
+    private static final int JOIN = R.drawable.pathbar_join;
+    private static final int CAP = R.drawable.pathbar_cap;
+    private Component mTouchItem = null;
+
+    static {
+        sPathFormat.fontSize = 18f * Gallery.PIXEL_DENSITY;
+    }
+
+    public PathBarLayer() {
+    }
+
+    public void pushLabel(int icon, String label, Runnable action) {
+        mComponents.add(new Component(icon, label, action, 0));
+        recomputeComponents();
+    }
+    
+    public void setAnimatedIcons(final int[] icons) {
+        final int numComponents = mComponents.size();
+        for (int i = 0; i < numComponents; ++i) {
+            final Component component = mComponents.get(i);
+            if (component != null) {
+                if (component.animatedIcons != null) {
+                    component.animatedIcons = null;
+                }
+                if (i == numComponents - 1) {
+                    component.animatedIcons = icons;
+                }
+            }
+        }
+    }
+
+    public void changeLabel(String label) {
+        if (label == null || label.length() == 0)
+            return;
+        Component component = popLabel();
+        if (component != null) {
+            pushLabel(component.icon, label, component.action);
+        }
+    }
+    
+    public String getCurrentLabel() {
+        final ArrayList<Component> components = mComponents;
+        int lastIndex = components.size() - 1;
+        if (lastIndex < 0) {
+            return "";
+        }
+        Component retVal = components.get(lastIndex);
+        return retVal.origString;
+    }
+
+    public Component popLabel() {
+        final ArrayList<Component> components = mComponents;
+        int lastIndex = components.size() - 1;
+        if (lastIndex < 0) {
+            return null;
+        }
+        Component retVal = components.get(lastIndex);
+        components.remove(lastIndex);
+        return retVal;
+    }
+
+    private static final class Component {
+        public String origString;
+        public int icon;
+        public Runnable action;
+        public StringTexture texture;
+        public float width;
+        public float animWidth;
+        public float x;
+        public int[] animatedIcons;
+        public float timeElapsed;
+        private static final float ICON_WIDTH = 38.0f;
+
+        Component(int icon, String label, Runnable action, float widthLeft) {
+            this.action = action;
+            origString = label;
+            this.icon = icon;
+            computeLabel(widthLeft);
+        }
+
+        public final void computeLabel(float widthLeft) {
+            Typeface typeface = sPathFormat.bold ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT;
+            String label = "";
+            if (origString != null) {
+                label = origString.substring(0, StringTexture.lengthToFit(sPathFormat.fontSize, widthLeft, typeface, origString));
+                if (label.length() != origString.length()) {
+                    label += "É";
+                }
+            }
+            this.texture = new StringTexture(label, sPathFormat);
+        }
+
+        public final boolean update(float timeElapsed) {
+            this.timeElapsed += timeElapsed;
+            if (animWidth == 0.0f) {
+                animWidth = width;
+            }
+            animWidth = FloatUtils.animate(animWidth, width, timeElapsed);
+            if (animatedIcons != null && animatedIcons.length > 1)
+                return true;
+            if (animWidth == width) {
+                return false;
+            } else {
+                return true;
+            }
+        }
+
+        public float getIconWidth() {
+            return ICON_WIDTH * Gallery.PIXEL_DENSITY;
+        }
+    }
+
+    @Override
+    public void generate(RenderView view, RenderView.Lists lists) {
+        lists.blendedList.add(this);
+        lists.hitTestList.add(this);
+        lists.updateList.add(this);
+    }
+
+    private void layout() {
+        int numComponents = mComponents.size();
+        for (int i = 0; i < numComponents; ++i) {
+            Component component = mComponents.get(i);
+            float iconWidth = (component.icon == 0) ? 0 : component.getIconWidth();
+            if (iconWidth == 0) {
+                iconWidth = 8 * Gallery.PIXEL_DENSITY;
+            }
+            float offset = 5 * Gallery.PIXEL_DENSITY;
+            float thisComponentWidth = (i != numComponents - 1) ? iconWidth + offset : component.texture.computeTextWidth()
+                    + iconWidth + offset;
+            component.width = thisComponentWidth;
+        }
+    }
+
+    @Override
+    public boolean update(RenderView view, float timeElapsed) {
+        layout();
+        boolean retVal = false;
+        int numComponents = mComponents.size();
+        for (int i = 0; i < numComponents; i++) {
+            Component component = mComponents.get(i);
+            retVal |= component.update(timeElapsed);
+        }
+        return retVal;
+    }
+
+    @Override
+    public void renderBlended(RenderView view, GL11 gl) {
+        // Draw components.
+        final Texture fill = view.getResource(FILL);
+        final Texture join = view.getResource(JOIN);
+        final Texture cap = view.getResource(CAP);
+        final float y = mY + 3;
+        int x = (int) (3 * Gallery.PIXEL_DENSITY);
+        float height = mHeight;
+        int numComponents = mComponents.size();
+        for (int i = 0; i < numComponents; ++i) {
+            Component component = mComponents.get(i);
+            component.x = x;
+            // Draw the left join if not the first component, and the fill.
+            // TODO: Draw the pressed background for mTouchItem.
+            final int width = (int) component.animWidth;
+            if (i != 0) {
+                view.draw2D(join, x - join.getWidth(), y);
+                if (view.bind(fill)) {
+                    view.draw2D(x, y, 0f, width, height);
+                }
+            } else if (view.bind(fill)) {
+                view.draw2D(0f, y, 0f, x + width, height);
+            }
+
+            if (i == numComponents - 1) {
+                // Draw the cap on the right edge.
+                view.draw2D(cap, x + width, y);
+            }
+            float xOffset = 5 * Gallery.PIXEL_DENSITY;
+            // Draw the label.
+            final int[] icons = component.animatedIcons;
+            
+            // Cycles animated icons.
+            final int iconId = (icons != null && icons.length > 0) ? icons[(int) (component.timeElapsed * 20.0f)
+                    % icons.length] : component.icon;
+            final Texture icon = view.getResource(iconId);
+            if (icon != null) {
+                view.loadTexture(icon);
+                view.draw2D(icon, x + xOffset, y - 2 * Gallery.PIXEL_DENSITY);
+            }
+            if (i == numComponents - 1) {
+                final StringTexture texture = component.texture;
+                view.loadTexture(texture);
+                float iconWidth = component.getIconWidth();
+                if (texture.computeTextWidth() <= (width - iconWidth)) {
+                    float textOffset = (iconWidth == 0) ? 8 * Gallery.PIXEL_DENSITY : iconWidth;
+                    view.draw2D(texture, x + textOffset, y + 5);
+                }
+            }
+            x += (int) (width + (21 * Gallery.PIXEL_DENSITY + 0.5f));
+        }
+    }
+
+    private Component hitTestItems(float x, float y) {
+        if (y >= mY && y < mY + mHeight) {
+            int numComponents = mComponents.size();
+            for (int i = 0; i < numComponents; i++) {
+                final Component component = mComponents.get(i);
+                float componentx = component.x;
+                if (x >= componentx && x < componentx + component.width) {
+                    return component;
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        final float x = event.getX();
+        final float y = event.getY();
+        switch (event.getAction()) {
+        case MotionEvent.ACTION_DOWN:
+            mTouchItem = hitTestItems(x, y);
+            break;
+        case MotionEvent.ACTION_MOVE:
+            break;
+        case MotionEvent.ACTION_UP:
+            if (mTouchItem != null) {
+                mTouchItem.action.run();
+            }
+        case MotionEvent.ACTION_CANCEL:
+            mTouchItem = null;
+            break;
+        }
+        return true;
+    }
+
+    public void recomputeComponents() {
+        float width = mWidth;
+        width -= 20f * Gallery.PIXEL_DENSITY;
+        int numComponents = mComponents.size();
+        for (int i = 0; i < numComponents; i++) {
+            Component component = mComponents.get(i);
+            width -= (component.getIconWidth() + 20.0f * Gallery.PIXEL_DENSITY);
+            component.computeLabel(width);
+        }
+    }
+
+    public int getNumLevels() {
+        return mComponents.size();
+    }
+}
diff --git a/src/com/cooliris/media/PhotoAppWidgetBind.java b/src/com/cooliris/media/PhotoAppWidgetBind.java
new file mode 100644
index 0000000..82eb238
--- /dev/null
+++ b/src/com/cooliris/media/PhotoAppWidgetBind.java
@@ -0,0 +1,77 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 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.
+ */
+
+import com.cooliris.media.PhotoAppWidgetProvider.PhotoDatabaseHelper;
+
+import android.app.Activity;
+import android.appwidget.AppWidgetManager;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import java.util.ArrayList;
+
+public class PhotoAppWidgetBind extends Activity {
+    private static final String TAG = "PhotoAppWidgetBind";
+    private static final String EXTRA_APPWIDGET_BITMAPS =
+            "com.android.camera.appwidgetbitmaps";
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        finish();
+
+        // The caller has requested that we bind a given bitmap to a specific
+        // appWidgetId, which probably is happening during a Launcher upgrade.
+        // This is dangerous because the caller could set bitmaps on
+        // appWidgetIds they don't own, so we guard this call at the manifest
+        // level by requiring the BIND_APPWIDGET permission.
+
+        final Intent intent = getIntent();
+        final Bundle extras = intent.getExtras();
+
+        final int[] appWidgetIds =
+                extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
+        final ArrayList<Bitmap> bitmaps =
+                extras.getParcelableArrayList(EXTRA_APPWIDGET_BITMAPS);
+
+        if (appWidgetIds == null || bitmaps == null
+                || appWidgetIds.length != bitmaps.size()) {
+            Log.e(TAG, "Problem parsing photo widget bind request");
+            return;
+        }
+
+        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
+        PhotoDatabaseHelper helper = new PhotoDatabaseHelper(this);
+        for (int i = 0; i < appWidgetIds.length; i++) {
+            // Store the cropped photo in our database
+            int appWidgetId = appWidgetIds[i];
+            helper.setPhoto(appWidgetId, bitmaps.get(i));
+
+            // Push newly updated widget to surface
+            RemoteViews views =
+                    PhotoAppWidgetProvider.buildUpdate(this, appWidgetId,
+                    helper);
+            appWidgetManager.updateAppWidget(new int[] { appWidgetId }, views);
+        }
+        helper.close();
+    }
+}
+
diff --git a/src/com/cooliris/media/PhotoAppWidgetConfigure.java b/src/com/cooliris/media/PhotoAppWidgetConfigure.java
new file mode 100644
index 0000000..cc0c1c3
--- /dev/null
+++ b/src/com/cooliris/media/PhotoAppWidgetConfigure.java
@@ -0,0 +1,97 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 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.
+ */
+
+import com.cooliris.media.PhotoAppWidgetProvider.PhotoDatabaseHelper;
+
+import android.app.Activity;
+import android.appwidget.AppWidgetManager;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.widget.RemoteViews;
+
+public class PhotoAppWidgetConfigure extends Activity {
+
+    @SuppressWarnings("unused")
+    private static final String TAG = "PhotoAppWidgetConfigure";
+    static final int REQUEST_GET_PHOTO = 2;
+
+    int mAppWidgetId = -1;
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        // Someone is requesting that we configure the given mAppWidgetId, which
+        // means we prompt the user to pick and crop a photo.
+
+        mAppWidgetId = getIntent().getIntExtra(
+                AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
+        if (mAppWidgetId == -1) {
+            setResult(Activity.RESULT_CANCELED);
+            finish();
+        }
+
+        // TODO: get these values from constants somewhere
+        // TODO: Adjust the PhotoFrame's image size to avoid on the fly scaling
+        Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+        intent.setType("image/*");
+        intent.putExtra("crop", "true");
+        intent.putExtra("aspectX", 1);
+        intent.putExtra("aspectY", 1);
+        intent.putExtra("outputX", 192);
+        intent.putExtra("outputY", 192);
+        intent.putExtra("noFaceDetection", true);
+        intent.putExtra("return-data", true);
+
+        startActivityForResult(intent, REQUEST_GET_PHOTO);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode,
+                                    Intent data) {
+        if (resultCode == RESULT_OK && mAppWidgetId != -1) {
+            // Store the cropped photo in our database
+            Bitmap bitmap = (Bitmap) data.getParcelableExtra("data");
+
+            PhotoDatabaseHelper helper = new PhotoDatabaseHelper(this);
+            if (helper.setPhoto(mAppWidgetId, bitmap)) {
+                resultCode = Activity.RESULT_OK;
+
+                // Push newly updated widget to surface
+                RemoteViews views = PhotoAppWidgetProvider.buildUpdate(this,
+                        mAppWidgetId, helper);
+                AppWidgetManager appWidgetManager =
+                        AppWidgetManager.getInstance(this);
+                appWidgetManager.updateAppWidget(new int[] {mAppWidgetId},
+                                                 views);
+            }
+            helper.close();
+        } else {
+            resultCode = Activity.RESULT_CANCELED;
+        }
+
+        // Make sure we pass back the original mAppWidgetId
+        Intent resultValue = new Intent();
+        resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
+        setResult(resultCode, resultValue);
+        finish();
+    }
+
+}
+
diff --git a/src/com/cooliris/media/PhotoAppWidgetProvider.java b/src/com/cooliris/media/PhotoAppWidgetProvider.java
new file mode 100644
index 0000000..4acd6b8
--- /dev/null
+++ b/src/com/cooliris/media/PhotoAppWidgetProvider.java
@@ -0,0 +1,210 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 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.
+ */
+
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Simple widget to show a user-selected picture.
+ */
+public class PhotoAppWidgetProvider extends AppWidgetProvider {
+    private static final String TAG = "PhotoAppWidgetProvider";
+    private static final boolean LOGD = true;
+
+    @Override
+    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
+                         int[] appWidgetIds) {
+        // Update each requested appWidgetId with its unique photo
+        PhotoDatabaseHelper helper = new PhotoDatabaseHelper(context);
+        for (int appWidgetId : appWidgetIds) {
+            int[] specificAppWidget = new int[] { appWidgetId };
+            RemoteViews views = buildUpdate(context, appWidgetId, helper);
+            if (LOGD) {
+                Log.d(TAG, "sending out views=" + views
+                        + " for id=" + appWidgetId);
+            }
+            appWidgetManager.updateAppWidget(specificAppWidget, views);
+        }
+        helper.close();
+    }
+
+    @Override
+    public void onDeleted(Context context, int[] appWidgetIds) {
+        // Clean deleted photos out of our database
+        PhotoDatabaseHelper helper = new PhotoDatabaseHelper(context);
+        for (int appWidgetId : appWidgetIds) {
+            helper.deletePhoto(appWidgetId);
+        }
+        helper.close();
+    }
+
+    /**
+     * Load photo for given widget and build {@link RemoteViews} for it.
+     */
+    static RemoteViews buildUpdate(Context context, int appWidgetId,
+                                   PhotoDatabaseHelper helper) {
+        RemoteViews views = null;
+        Bitmap bitmap = helper.getPhoto(appWidgetId);
+        if (bitmap != null) {
+            views = new RemoteViews(context.getPackageName(),
+                                    R.layout.photo_frame);
+            views.setImageViewBitmap(R.id.photo, bitmap);
+        }
+        return views;
+    }
+
+    static class PhotoDatabaseHelper extends SQLiteOpenHelper {
+        private static final String DATABASE_NAME = "launcher.db";
+
+        private static final int DATABASE_VERSION = 2;
+
+        static final String TABLE_PHOTOS = "photos";
+        static final String FIELD_APPWIDGET_ID = "appWidgetId";
+        static final String FIELD_PHOTO_BLOB = "photoBlob";
+
+        PhotoDatabaseHelper(Context context) {
+            super(context, DATABASE_NAME, null, DATABASE_VERSION);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE " + TABLE_PHOTOS + " (" +
+                    FIELD_APPWIDGET_ID + " INTEGER PRIMARY KEY," +
+                    FIELD_PHOTO_BLOB + " BLOB" +
+                    ");");
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion,
+                              int newVersion) {
+            int version = oldVersion;
+
+            if (version != DATABASE_VERSION) {
+                Log.w(TAG, "Destroying all old data.");
+                db.execSQL("DROP TABLE IF EXISTS " + TABLE_PHOTOS);
+                onCreate(db);
+            }
+        }
+
+        /**
+         * Store the given bitmap in this database for the given appWidgetId.
+         */
+        public boolean setPhoto(int appWidgetId, Bitmap bitmap) {
+            boolean success = false;
+            try {
+                // Try go guesstimate how much space the icon will take when
+                // serialized to avoid unnecessary allocations/copies during
+                // the write.
+                int size = bitmap.getWidth() * bitmap.getHeight() * 4;
+                ByteArrayOutputStream out = new ByteArrayOutputStream(size);
+                bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
+                out.flush();
+                out.close();
+
+                ContentValues values = new ContentValues();
+                values.put(PhotoDatabaseHelper.FIELD_APPWIDGET_ID, appWidgetId);
+                values.put(PhotoDatabaseHelper.FIELD_PHOTO_BLOB,
+                           out.toByteArray());
+
+                SQLiteDatabase db = getWritableDatabase();
+                db.insertOrThrow(PhotoDatabaseHelper.TABLE_PHOTOS, null,
+                                 values);
+
+                success = true;
+            } catch (SQLiteException e) {
+                Log.e(TAG, "Could not open database", e);
+            } catch (IOException e) {
+                Log.e(TAG, "Could not serialize photo", e);
+            }
+            if (LOGD) {
+                Log.d(TAG, "setPhoto success=" + success);
+            }
+            return success;
+        }
+
+        static final String[] PHOTOS_PROJECTION = {
+            FIELD_PHOTO_BLOB,
+        };
+
+        static final int INDEX_PHOTO_BLOB = 0;
+
+        /**
+         * Inflate and return a bitmap for the given appWidgetId.
+         */
+        public Bitmap getPhoto(int appWidgetId) {
+            Cursor c = null;
+            Bitmap bitmap = null;
+            try {
+                SQLiteDatabase db = getReadableDatabase();
+                String selection = String.format("%s=%d", FIELD_APPWIDGET_ID,
+                                                 appWidgetId);
+                c = db.query(TABLE_PHOTOS, PHOTOS_PROJECTION, selection, null,
+                        null, null, null, null);
+
+                if (c != null && LOGD) {
+                    Log.d(TAG, "getPhoto query count=" + c.getCount());
+                }
+
+                if (c != null && c.moveToFirst()) {
+                    byte[] data = c.getBlob(INDEX_PHOTO_BLOB);
+                    if (data != null) {
+                        bitmap = BitmapFactory.decodeByteArray(data, 0,
+                                data.length);
+                    }
+                }
+            } catch (SQLiteException e) {
+                Log.e(TAG, "Could not load photo from database", e);
+            } finally {
+                if (c != null) {
+                    c.close();
+                }
+            }
+            return bitmap;
+        }
+
+        /**
+         * Remove any bitmap associated with the given appWidgetId.
+         */
+        public void deletePhoto(int appWidgetId) {
+            try {
+                SQLiteDatabase db = getWritableDatabase();
+                String whereClause = String.format("%s=%d", FIELD_APPWIDGET_ID,
+                                                   appWidgetId);
+                db.delete(TABLE_PHOTOS, whereClause, null);
+            } catch (SQLiteException e) {
+                Log.e(TAG, "Could not delete photo from database", e);
+            }
+        }
+    }
+
+}
+
+
diff --git a/src/com/cooliris/media/Photographs.java b/src/com/cooliris/media/Photographs.java
new file mode 100644
index 0000000..ce61c67
--- /dev/null
+++ b/src/com/cooliris/media/Photographs.java
@@ -0,0 +1,208 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 2007 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.
+ */
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Wallpaper picker for the camera application. This just redirects to the
+ * standard pick action.
+ */
+public class Photographs extends Activity {
+    private static final String LOG_TAG = "Wallpaper";
+    static final int PHOTO_PICKED = 1;
+    static final int CROP_DONE = 2;
+
+    static final int SHOW_PROGRESS = 0;
+    static final int FINISH = 1;
+
+    static final String DO_LAUNCH_ICICLE = "do_launch";
+    static final String TEMP_FILE_PATH_ICICLE = "temp_file_path";
+
+    private ProgressDialog mProgressDialog = null;
+    private boolean mDoLaunch = true;
+    private File mTempFile;
+
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case SHOW_PROGRESS: {
+                    CharSequence c = getText(R.string.wallpaper);
+                    mProgressDialog = ProgressDialog.show(Photographs.this,
+                            "", c, true, false);
+                    break;
+                }
+                case FINISH: {
+                    closeProgressDialog();
+                    setResult(RESULT_OK);
+                    finish();
+                    break;
+                }
+            }
+        }
+    };
+
+    static class SetWallpaperThread extends Thread {
+        private final Bitmap mBitmap;
+        private final Handler mHandler;
+        private final Context mContext;
+        private final File mFile;
+
+        public SetWallpaperThread(Bitmap bitmap, Handler handler,
+                                  Context context, File file) {
+            mBitmap = bitmap;
+            mHandler = handler;
+            mContext = context;
+            mFile = file;
+        }
+
+        @Override
+        public void run() {
+            try {
+                mContext.setWallpaper(mBitmap);
+            } catch (IOException e) {
+                Log.e(LOG_TAG, "Failed to set wallpaper.", e);
+            } finally {
+                mHandler.sendEmptyMessage(FINISH);
+                mFile.delete();
+            }
+        }
+    }
+
+    private synchronized void closeProgressDialog() {
+        if (mProgressDialog != null) {
+            mProgressDialog.dismiss();
+            mProgressDialog = null;
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        if (icicle != null) {
+            mDoLaunch = icicle.getBoolean(DO_LAUNCH_ICICLE);
+            mTempFile = new File(icicle.getString(TEMP_FILE_PATH_ICICLE));
+        }
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle icicle) {
+        icicle.putBoolean(DO_LAUNCH_ICICLE, mDoLaunch);
+        icicle.putString(TEMP_FILE_PATH_ICICLE, mTempFile.getAbsolutePath());
+    }
+
+    @Override
+    protected void onPause() {
+        closeProgressDialog();
+        super.onPause();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        if (!mDoLaunch) {
+            return;
+        }
+        Uri imageToUse = getIntent().getData();
+        if (imageToUse != null) {
+            Intent intent = new Intent();
+            intent.setClassName("com.cooliris.media",
+                                "com.cooliris.media.CropImage");
+            intent.setData(imageToUse);
+            formatIntent(intent);
+            startActivityForResult(intent, CROP_DONE);
+        } else {
+            Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+            intent.setType("image/*");
+            intent.putExtra("crop", "true");
+            formatIntent(intent);
+            startActivityForResult(intent, PHOTO_PICKED);
+        }
+    }
+
+    protected void formatIntent(Intent intent) {
+        // TODO: A temporary file is NOT necessary
+        // The CropImage intent should be able to set the wallpaper directly
+        // without writing to a file, which we then need to read here to write
+        // it again as the final wallpaper, this is silly
+        mTempFile = getFileStreamPath("temp-wallpaper");
+        mTempFile.getParentFile().mkdirs();
+
+        int width = getWallpaperDesiredMinimumWidth();
+        int height = getWallpaperDesiredMinimumHeight();
+        intent.putExtra("outputX",         width);
+        intent.putExtra("outputY",         height);
+        intent.putExtra("aspectX",         width);
+        intent.putExtra("aspectY",         height);
+        intent.putExtra("scale",           true);
+        intent.putExtra("noFaceDetection", true);
+        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mTempFile));
+        intent.putExtra("outputFormat", Bitmap.CompressFormat.PNG.name());
+        // TODO: we should have an extra called "setWallpaper" to ask CropImage
+        // to set the cropped image as a wallpaper directly. This means the
+        // SetWallpaperThread should be moved out of this class to CropImage
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode,
+                                    Intent data) {
+        if ((requestCode == PHOTO_PICKED || requestCode == CROP_DONE)
+                && (resultCode == RESULT_OK) && (data != null)) {
+            try {
+                InputStream s = new FileInputStream(mTempFile);
+                try {
+                    Bitmap bitmap = BitmapFactory.decodeStream(s);
+                    if (bitmap == null) {
+                        Log.e(LOG_TAG, "Failed to set wallpaper. "
+                                       + "Couldn't get bitmap for path "
+                                       + mTempFile);
+                    } else {
+                        mHandler.sendEmptyMessage(SHOW_PROGRESS);
+                        new SetWallpaperThread(
+                                bitmap, mHandler, this, mTempFile).start();
+                    }
+                    mDoLaunch = false;
+                } finally {
+                    Util.closeSilently(s);
+                }
+            } catch (FileNotFoundException ex) {
+                Log.e(LOG_TAG, "file not found: " + mTempFile, ex);
+            }
+        } else {
+            setResult(RESULT_CANCELED);
+            finish();
+        }
+    }
+}
diff --git a/src/com/cooliris/media/PicasaDataSource.java b/src/com/cooliris/media/PicasaDataSource.java
new file mode 100644
index 0000000..633d9a3
--- /dev/null
+++ b/src/com/cooliris/media/PicasaDataSource.java
@@ -0,0 +1,238 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.cooliris.picasa.AlbumEntry;
+import com.cooliris.picasa.Entry;
+import com.cooliris.picasa.EntrySchema;
+import com.cooliris.picasa.PicasaApi;
+import com.cooliris.picasa.PicasaContentProvider;
+import com.cooliris.picasa.PicasaService;
+
+public final class PicasaDataSource implements DataSource {
+    private static final String TAG = "PicasaDataSource";
+    public static final DiskCache sThumbnailCache = new DiskCache("picasa-thumbs");
+    private static final String DEFAULT_BUCKET_SORT_ORDER = AlbumEntry.Columns.USER + ", " + AlbumEntry.Columns.DATE_PUBLISHED
+            + " DESC";
+
+    private ContentProviderClient mProviderClient;
+    private final Context mContext;
+    private ContentObserver mAlbumObserver;
+
+    public PicasaDataSource(Context context) {
+        mContext = context;
+    }
+
+    public void loadMediaSets(final MediaFeed feed) {
+        if (mProviderClient == null) {
+            mProviderClient = mContext.getContentResolver().acquireContentProviderClient(PicasaContentProvider.AUTHORITY);
+        }
+        // Force permission dialog to be displayed if necessary. TODO: remove this after signed by Google.
+        PicasaApi.getAccounts(mContext);
+        
+        // Ensure that users are up to date. TODO: also listen for accounts changed broadcast.
+        PicasaService.requestSync(mContext, PicasaService.TYPE_USERS_ALBUMS, 0);
+        Handler handler = ((Gallery)mContext).getHandler();
+        ContentObserver albumObserver = new ContentObserver(handler) {
+            public void onChange(boolean selfChange) {
+                loadMediaSetsIntoFeed(feed, true);
+            }
+        };
+        mAlbumObserver = albumObserver;
+        loadMediaSetsIntoFeed(feed, true);
+        
+        // Start listening.
+        ContentResolver cr = mContext.getContentResolver();
+        cr.registerContentObserver(PicasaContentProvider.ALBUMS_URI, false, mAlbumObserver);
+        cr.registerContentObserver(PicasaContentProvider.PHOTOS_URI, false, mAlbumObserver);
+    }
+ 
+    public void shutdown() {
+        if (mAlbumObserver != null) {
+            ContentResolver cr = mContext.getContentResolver();
+            cr.unregisterContentObserver(mAlbumObserver);
+        }
+    }
+
+    public void loadItemsForSet(final MediaFeed feed, final MediaSet parentSet, int rangeStart, int rangeEnd) {
+        if (parentSet == null) {
+            return;
+        } else {
+            // Return a list of items within an album.
+            addItemsToFeed(feed, parentSet, rangeStart, rangeEnd);
+        }
+    }
+
+    protected void loadMediaSetsIntoFeed(final MediaFeed feed, boolean sync) {
+        ContentProviderClient client = mProviderClient;
+        if (client == null)
+            return;
+        try {
+            EntrySchema albumSchema = AlbumEntry.SCHEMA;
+            Cursor cursor = client.query(PicasaContentProvider.ALBUMS_URI, albumSchema.getProjection(), null, null,
+                    DEFAULT_BUCKET_SORT_ORDER);
+            AlbumEntry album = new AlbumEntry();
+            MediaSet mediaSet;
+            if (cursor.moveToFirst()) {
+                int numAlbums = cursor.getCount();
+                ArrayList<MediaSet> picasaSets = new ArrayList<MediaSet>(numAlbums);
+                do {
+                    albumSchema.cursorToObject(cursor, album);
+                    mediaSet = feed.getMediaSet(album.id);
+                    if (mediaSet == null) {
+                        mediaSet = feed.addMediaSet(album.id, this);
+                        mediaSet.mName = album.title;
+                        mediaSet.mEditUri = album.editUri;
+                        mediaSet.generateTitle(true);
+                    } else {
+                        mediaSet.setNumExpectedItems(album.numPhotos);
+                    }
+                    mediaSet.mPicasaAlbumId = album.id;
+                    mediaSet.mSyncPending = album.photosDirty;
+                    picasaSets.add(mediaSet);
+                } while (cursor.moveToNext());
+            }
+            cursor.close();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error occurred loading albums");
+        }
+    }
+
+    private void addItemsToFeed(MediaFeed feed, MediaSet set, int start, int end) {
+        ContentProviderClient client = mProviderClient;
+        Cursor cursor = null;
+        try {
+            // Query photos in the album.
+            EntrySchema photosSchema = PhotoProjection.SCHEMA;
+            String whereInAlbum = "album_id = " + Long.toString(set.mId);
+            cursor = client.query(PicasaContentProvider.PHOTOS_URI, photosSchema.getProjection(), whereInAlbum, null, null);
+            PhotoProjection photo = new PhotoProjection();
+            int count = cursor.getCount();
+            if (count < end) {
+                end = count;
+            }
+            set.setNumExpectedItems(count);
+            set.generateTitle(true);
+            // Move to the next unread item.
+            int newIndex = start + 1;
+            if (newIndex > count || !cursor.move(newIndex)) {
+                end = 0;
+                cursor.close();
+                set.updateNumExpectedItems();
+                set.generateTitle(true);
+                return;
+            }
+            if (set.mNumItemsLoaded == 0) {
+                photosSchema.cursorToObject(cursor, photo);
+                set.mMinTimestamp = photo.dateTaken;
+                cursor.moveToLast();
+                photosSchema.cursorToObject(cursor, photo);
+                set.mMinTimestamp = photo.dateTaken;
+                cursor.moveToFirst();
+            }
+            for (int i = 0; i < end; ++i) {
+                photosSchema.cursorToObject(cursor, photo);
+                MediaItem item = new MediaItem();
+                item.mId = photo.id;
+                item.mEditUri = photo.editUri;
+                item.mMimeType = photo.contentType;
+                item.mDateTakenInMs = photo.dateTaken;
+                item.mLatitude = photo.latitude;
+                item.mLongitude = photo.longitude;
+                item.mThumbnailUri = photo.thumbnailUrl;
+                item.mScreennailUri = photo.screennailUrl;
+                item.mContentUri = photo.contentUrl;
+                item.mCaption = photo.title;
+                item.mWeblink = photo.htmlPageUrl;
+                item.mDescription = photo.summary;
+                item.mFilePath = item.mContentUri;
+                feed.addItemToMediaSet(item, set);
+                if (!cursor.moveToNext()) {
+                    break;
+                }
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Error occurred loading photos for album " + set.mId);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    public void prime(final MediaItem item) {
+    }
+
+    public boolean performOperation(final int operation, final ArrayList<MediaBucket> mediaBuckets, final Object data) {
+        try {
+            if (operation == MediaFeed.OPERATION_DELETE) {
+                ContentProviderClient client = mProviderClient;
+                for (int i = 0, numBuckets = mediaBuckets.size(); i != numBuckets; ++i) {
+                    MediaBucket bucket = mediaBuckets.get(i);
+                    ArrayList<MediaItem> items = bucket.mediaItems;
+                    if (items == null) {
+                        // Delete an album.
+                        String albumUri = PicasaContentProvider.ALBUMS_URI + "/" + bucket.mediaSet.mId;
+                        client.delete(Uri.parse(albumUri), null, null);
+                    } else {
+                        // Delete a set of photos.
+                        for (int j = 0, numItems = items.size(); j != numItems; ++j) {
+                            MediaItem item = items.get(j);
+                            if (item != null) {
+                                Log.i(TAG, "Deleting picasa photo " + item.mContentUri);
+                                String itemUri = PicasaContentProvider.PHOTOS_URI + "/" + item.mId; 
+                                client.delete(Uri.parse(itemUri), null, null);
+                            }
+                        }
+                    }
+                }
+            }
+            return true;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public DiskCache getThumbnailCache() {
+        return sThumbnailCache;
+    }
+
+    /**
+     * The projection of PhotoEntry needed by the data source.
+     */
+    private static final class PhotoProjection extends Entry {
+        public static final EntrySchema SCHEMA = new EntrySchema(PhotoProjection.class);
+        @Column("edit_uri")
+        public String editUri;
+        @Column("title")
+        public String title;
+        @Column("summary")
+        public String summary;
+        @Column("date_taken")
+        public long dateTaken;
+        @Column("latitude")
+        public double latitude;
+        @Column("longitude")
+        public double longitude;
+        @Column("thumbnail_url")
+        public String thumbnailUrl;
+        @Column("screennail_url")
+        public String screennailUrl;
+        @Column("content_url")
+        public String contentUrl;
+        @Column("content_type")
+        public String contentType;
+        @Column("html_page_url")
+        public String htmlPageUrl;
+    }
+}
diff --git a/src/com/cooliris/media/Pool.java b/src/com/cooliris/media/Pool.java
new file mode 100644
index 0000000..c0f581d
--- /dev/null
+++ b/src/com/cooliris/media/Pool.java
@@ -0,0 +1,29 @@
+package com.cooliris.media;
+
+public final class Pool<E extends Object> {
+    private final E[] mFreeList;
+    private int mFreeListIndex;
+
+    public Pool(E[] objects) {
+        mFreeList = objects;
+        mFreeListIndex = objects.length;
+    }
+
+    public E create() {
+        int index = --mFreeListIndex;
+        if (index >= 0 && index < mFreeList.length) {
+            E object = mFreeList[index];
+            mFreeList[index] = null;
+            return object;
+        }
+        return null;
+    }
+
+    public void delete(E object) {
+        int index = mFreeListIndex;
+        if (index >= 0 && index < mFreeList.length) {
+            mFreeList[index] = object;
+            mFreeListIndex++;
+        }
+    }
+}
diff --git a/src/com/cooliris/media/PopupMenu.java b/src/com/cooliris/media/PopupMenu.java
new file mode 100644
index 0000000..f200061
--- /dev/null
+++ b/src/com/cooliris/media/PopupMenu.java
@@ -0,0 +1,320 @@
+package com.cooliris.media;
+
+import javax.microedition.khronos.opengles.GL11;
+
+import com.cooliris.media.R;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.NinePatch;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
+import android.text.TextPaint;
+import android.view.MotionEvent;
+
+public final class PopupMenu extends Layer {
+    private static final int POPUP_TRIANGLE_EXTRA_HEIGHT = 14;
+    private static final int POPUP_TRIANGLE_X_MARGIN = 16;
+    private static final int POPUP_Y_OFFSET = 20;
+    private static final Paint SRC_PAINT = new Paint();
+    private static final int PADDING_LEFT = 10 + 5;
+    private static final int PADDING_TOP = 10 + 3;
+    private static final int PADDING_RIGHT = 10 + 5;
+    private static final int PADDING_BOTTOM = 30 + 10;
+    private static final int ICON_TITLE_MIN_WIDTH = 100;
+    private static final IconTitleDrawable.Config ICON_TITLE_CONFIG;
+
+    private final PopupTexture mPopupTexture;
+    private Listener mListener = null;
+    private Option[] mOptions = {};
+    private boolean mNeedsLayout = false;
+    private boolean mShow = false;
+    private final FloatAnim mShowAnim = new FloatAnim(0f);
+    private int mRowHeight = 36;
+    private int mSelectedItem = -1;
+
+    static {
+        TextPaint paint = new TextPaint();
+        paint.setTextSize(17f * Gallery.PIXEL_DENSITY);
+        paint.setColor(0xffffffff);
+        paint.setAntiAlias(true);
+        ICON_TITLE_CONFIG = new IconTitleDrawable.Config((int)(45 * Gallery.PIXEL_DENSITY), (int)(34 * Gallery.PIXEL_DENSITY), paint);
+        SRC_PAINT.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+    }
+
+    public PopupMenu(Context context) {
+        mPopupTexture = new PopupTexture(context);
+        setHidden(true);
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public void setOptions(Option[] options) {
+        close(false);
+        mOptions = options;
+        mNeedsLayout = true;
+    }
+
+    public void showAtPoint(int pointX, int pointY, int outerWidth, int outerHeight) {
+        // Compute layout if needed.
+        if (mNeedsLayout) {
+            layout();
+        }
+        // Try to center the popup over the target point.
+        int width = (int)mWidth;
+        int height = (int)mHeight;
+        int widthOver2 = width / 2;
+        int x = pointX - widthOver2;
+        int y = pointY + POPUP_Y_OFFSET - height;
+        int clampedX = Shared.clamp(x, 0, outerWidth - width);
+        int triangleWidthOver2 = mPopupTexture.mTriangleBottom.getWidth() / 2;
+        mPopupTexture.mTriangleX = Shared.clamp(widthOver2 + (x - clampedX) - triangleWidthOver2, POPUP_TRIANGLE_X_MARGIN, width
+                - POPUP_TRIANGLE_X_MARGIN * 2);
+        mPopupTexture.setNeedsDraw();
+        setPosition(clampedX, y);
+
+        // Fade in the menu if it is not already visible, otherwise snap to the new location.
+        // if (!mShow) {
+        mShow = true;
+        setHidden(false);
+        mShowAnim.setValue(0);
+        mShowAnim.animateValue(1f, 0.4f, SystemClock.uptimeMillis());
+        // }
+    }
+
+    public void close(boolean fadeOut) {
+        if (mShow) {
+            if (fadeOut) {
+                mShowAnim.animateValue(0, 0.3f, SystemClock.uptimeMillis());
+            } else {
+                mShowAnim.setValue(0);
+            }
+            mShow = false;
+            mSelectedItem = -1;
+        }
+        
+    }
+
+    @Override
+    public void generate(RenderView view, RenderView.Lists lists) {
+        lists.blendedList.add(this);
+        lists.hitTestList.add(this);
+        lists.systemList.add(this);
+        lists.updateList.add(this);
+    }
+
+    @Override
+    protected void onSizeChanged() {
+        super.onSizeChanged();
+        mPopupTexture.setSize((int)mWidth, (int)mHeight);
+    }
+
+    @Override
+    protected void onSurfaceCreated(RenderView view, GL11 gl) {
+        close(false);
+        mPopupTexture.resetTexture();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        int hit = hitTestOptions((int) event.getX(), (int) event.getY());
+        switch (event.getAction()) {
+        case MotionEvent.ACTION_DOWN:
+        case MotionEvent.ACTION_MOVE:
+            setSelectedItem(hit);
+            break;
+        case MotionEvent.ACTION_UP:
+            if (hit != -1 && mSelectedItem == hit) {
+                mOptions[hit].mAction.run();
+                if (mListener != null) {
+                    mListener.onSelectionClicked(this, hit);
+                }
+            }
+        case MotionEvent.ACTION_CANCEL:
+            setSelectedItem(-1);
+            break;
+        }
+        return true;
+    }
+
+    private void setSelectedItem(int hit) {
+        if (mSelectedItem != hit) {
+            mSelectedItem = hit;
+            mPopupTexture.setNeedsDraw();
+            if (mListener != null) {
+                mListener.onSelectionChanged(this, hit);
+            }
+        }
+    }
+    
+    @Override
+    public boolean update(RenderView view, float timeElapsed) {
+        return (mShowAnim.getTimeRemaining(SystemClock.uptimeMillis()) > 0);
+     }
+
+    @Override
+    public void renderBlended(RenderView view, GL11 gl) {
+        // Hide the layer if the close animation is complete.
+        float showRatio = mShowAnim.getValue(SystemClock.uptimeMillis());
+        boolean show = mShow;
+        if (showRatio < 0.003f && !show) {
+            setHidden(true);
+        }
+
+        // Draw the selection menu with the show animation.
+        int x = (int) mX;
+        int y = (int) mY;
+
+        if (show && showRatio < 1f) {
+            // Animate the scale as well for the open animation.
+            float scale;
+            float split = 0.7f;
+            if (showRatio < split) {
+                scale = 0.8f + 0.3f * showRatio / split;
+            } else {
+                scale = 1f + ((1f - showRatio) / (1f - split)) * 0.1f;
+            }
+            mPopupTexture.drawWithEffect(view, gl, x, y, 0.5f, 0.65f, showRatio, scale);
+        } else {
+            if (showRatio < 1f) {
+                view.setAlpha(showRatio);
+            }
+            mPopupTexture.draw(view, gl, x, y);
+            if (showRatio < 1f) {
+                view.resetColor();
+            }
+        }
+    }
+
+    private void layout() {
+        // Mark as not needing layout.
+        mNeedsLayout = false;
+
+        // Measure the menu options.
+        Option[] options = mOptions;
+        int numOptions = options.length;
+        int maxWidth = (int)(ICON_TITLE_MIN_WIDTH * Gallery.PIXEL_DENSITY);
+        for (int i = 0; i != numOptions; ++i) {
+            Option option = options[i];
+            IconTitleDrawable drawable = option.mDrawable;
+            if (drawable == null) {
+                drawable = new IconTitleDrawable(option.mTitle, option.mIcon, ICON_TITLE_CONFIG);
+                option.mDrawable = drawable;
+            }
+            int width = drawable.getIntrinsicWidth();
+            if (width > maxWidth) {
+                maxWidth = width;
+            }
+        }
+
+        // Layout the menu options.
+        int rowHeight = (int)(mRowHeight * Gallery.PIXEL_DENSITY);
+        int left = (int)(PADDING_LEFT * Gallery.PIXEL_DENSITY);
+        int top = (int)(PADDING_TOP * Gallery.PIXEL_DENSITY);
+        int right = left + maxWidth;
+        for (int i = 0; i != numOptions; ++i) {
+            Option option = options[i];
+            IconTitleDrawable drawable = option.mDrawable;
+            option.mBottom = top + rowHeight;
+            drawable.setBounds(left, top, right, option.mBottom);
+            top += rowHeight;
+        }
+
+        // Resize the popup menu.
+        setSize(right + PADDING_RIGHT * Gallery.PIXEL_DENSITY, top + PADDING_BOTTOM * Gallery.PIXEL_DENSITY);
+        
+    }
+
+    private int hitTestOptions(int x, int y) {
+        Option[] options = mOptions;
+        int numOptions = options.length;
+        x -= mX;
+        y -= mY;
+        if (numOptions != 0 && x >= 0 && x < mWidth && y >= 0) {
+            for (int i = 0; i != numOptions; ++i) {
+                if (y < options[i].mBottom) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
+    public interface Listener {
+        void onSelectionChanged(PopupMenu menu, int selectedIndex);
+
+        void onSelectionClicked(PopupMenu menu, int selectedIndex);
+    }
+
+    public static final class Option {
+        private final String mTitle;
+        private final Drawable mIcon;
+        private final Runnable mAction;
+        private IconTitleDrawable mDrawable = null;
+        private int mBottom;
+
+        public Option(String title, Drawable icon, Runnable action) {
+            mTitle = title;
+            mIcon = icon;
+            mAction = action;
+        }
+    }
+
+    private final class PopupTexture extends CanvasTexture {
+        private final NinePatch mBackground;
+        private final NinePatch mHighlightSelected;
+        private final Bitmap mTriangleBottom;
+        private final Rect mBackgroundRect = new Rect();
+        private int mTriangleX = 0;
+
+        public PopupTexture(Context context) {
+            super(Bitmap.Config.ARGB_8888);
+            Resources resources = context.getResources();
+            Bitmap background = BitmapFactory.decodeResource(resources, R.drawable.popup);
+            mBackground = new NinePatch(background, background.getNinePatchChunk(), null);
+            Bitmap highlightSelected = BitmapFactory.decodeResource(resources, R.drawable.popup_option_selected);
+            mHighlightSelected = new NinePatch(highlightSelected, highlightSelected.getNinePatchChunk(), null);
+            mTriangleBottom = BitmapFactory.decodeResource(resources, R.drawable.popup_triangle_bottom);
+        }
+
+        @Override
+        protected void onSizeChanged() {
+            mBackgroundRect.set(0, 0, getWidth(), getHeight() - (int)(POPUP_TRIANGLE_EXTRA_HEIGHT * Gallery.PIXEL_DENSITY));
+        }
+
+        @Override
+        protected void renderCanvas(Canvas canvas, Bitmap backing, int width, int height) {
+            // Draw the background.
+            backing.eraseColor(0);
+            mBackground.draw(canvas, mBackgroundRect, SRC_PAINT);
+
+            // Stamp the popup triangle over the appropriate region ignoring alpha.
+            Bitmap triangle = mTriangleBottom;
+            canvas.drawBitmap(triangle, mTriangleX, height - triangle.getHeight() - 1, SRC_PAINT);
+
+            // Draw the selection / focus highlight.
+            Option[] options = mOptions;
+            int selectedItem = mSelectedItem;
+            if (selectedItem != -1) {
+                Option option = options[selectedItem];
+                mHighlightSelected.draw(canvas, option.mDrawable.getBounds());
+            }
+
+            // Draw icons and titles.
+            int numOptions = options.length;
+            for (int i = 0; i != numOptions; ++i) {
+                options[i].mDrawable.draw(canvas);
+            }
+        }
+
+    }
+}
diff --git a/src/com/cooliris/media/RenderView.java b/src/com/cooliris/media/RenderView.java
new file mode 100644
index 0000000..9349bd0
--- /dev/null
+++ b/src/com/cooliris/media/RenderView.java
@@ -0,0 +1,1001 @@
+package com.cooliris.media;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.opengl.GLU;
+import android.opengl.GLUtils;
+import android.opengl.GLSurfaceView;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+
+public final class RenderView extends GLSurfaceView implements GLSurfaceView.Renderer, SensorEventListener {
+    private static final String TAG = "RenderView";
+    private static final int NUM_TEXTURE_LOAD_THREADS = 4;
+    private static final int MAX_LOADING_COUNT = 8;
+
+    private static final int EVENT_NONE = 0;
+    // private static final int EVENT_TOUCH = 1;
+    private static final int EVENT_KEY = 2;
+    private static final int EVENT_FOCUS = 3;
+
+    private SensorManager mSensorManager;
+
+    private GL11 mGL = null;
+    private int mViewWidth = 0;
+    private int mViewHeight = 0;
+
+    private RootLayer mRootLayer = null;
+    private boolean mListsDirty = false;
+    private final Lists mLists = new Lists();
+
+    private Layer mTouchEventTarget = null;
+
+    private int mCurrentEventType = EVENT_NONE;
+    private KeyEvent mCurrentKeyEvent = null;
+    private boolean mCurrentKeyEventResult = false;
+    private volatile boolean mPendingSensorEvent = false;
+
+    private int mLoadingCount = 0;
+    private final Deque<Texture> mLoadInputQueue = new Deque<Texture>();
+    private final Deque<Texture> mLoadInputQueueCached = new Deque<Texture>();
+    private final Deque<Texture> mLoadInputQueueVideo = new Deque<Texture>();
+    private final Deque<Texture> mLoadOutputQueue = new Deque<Texture>();
+    private final Deque<MotionEvent> mTouchEventQueue = new Deque<MotionEvent>();
+    private final DirectLinkedList<TextureReference> mActiveTextureList = new DirectLinkedList<TextureReference>();
+    private final ReferenceQueue<Texture> mUnreferencedTextureQueue = new ReferenceQueue<Texture>();
+    private final TextureLoadThread[] mTextureLoadThreads = new TextureLoadThread[NUM_TEXTURE_LOAD_THREADS];
+    private TextureLoadThread mCachedTextureLoadThread = null;
+    private TextureLoadThread mVideoTextureLoadThread = null;
+
+    // Frame time in milliseconds and delta since last frame in seconds. Uses SystemClock.getUptimeMillis().
+    private long mFrameTime = 0;
+    private float mFrameInterval = 0.0f;
+    private float mAlpha;
+
+    private long mLoadingExpensiveTexturesStartTime = 0;
+    private final SparseArray<ResourceTexture> sCacheScaled = new SparseArray<ResourceTexture>();
+    private final SparseArray<ResourceTexture> sCacheUnscaled = new SparseArray<ResourceTexture>();
+
+    private boolean mFirstDraw;
+
+    // Weak reference to a texture that stores the associated texture ID.
+    private static final class TextureReference extends WeakReference<Texture> {
+        public TextureReference(Texture texture, GL11 gl, ReferenceQueue<Texture> referenceQueue, int textureId) {
+            super(texture, referenceQueue);
+            this.textureId = textureId;
+            this.gl = gl;
+        }
+
+        public final int textureId;
+        public final GL11 gl;
+        public final DirectLinkedList.Entry<TextureReference> activeListEntry = new DirectLinkedList.Entry<TextureReference>(this);
+    }
+
+    public static final class Lists {
+        public final ArrayList<Layer> updateList = new ArrayList<Layer>();
+        public final ArrayList<Layer> opaqueList = new ArrayList<Layer>();
+        public final ArrayList<Layer> blendedList = new ArrayList<Layer>();
+        public final ArrayList<Layer> hitTestList = new ArrayList<Layer>();
+        public final ArrayList<Layer> systemList = new ArrayList<Layer>();
+
+        void clear() {
+            updateList.clear();
+            opaqueList.clear();
+            blendedList.clear();
+            hitTestList.clear();
+            systemList.clear();
+        }
+    }
+
+    public RenderView(Context context) {
+        super(context);
+        setBackgroundDrawable(null);
+        setFocusable(true);
+        setEGLConfigChooser(true);
+        setRenderer(this);
+        mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+
+        for (int i = 0; i != NUM_TEXTURE_LOAD_THREADS; ++i) {
+            TextureLoadThread thread = new TextureLoadThread();
+            if (i == 0) {
+                mCachedTextureLoadThread = thread;
+            }
+            if (i == 1) {
+                mVideoTextureLoadThread = thread;
+            }
+            mTextureLoadThreads[i] = thread;
+            thread.start();
+        }
+    }
+
+    public void setRootLayer(RootLayer layer) {
+        if (mRootLayer != layer) {
+            mRootLayer = layer;
+            mListsDirty = true;
+            if (layer != null) {
+                mRootLayer.setSize(mViewWidth, mViewHeight);
+            }
+        }
+    }
+
+    public ResourceTexture getResource(int resourceId) {
+        return getResourceInternal(resourceId, true);
+    }
+
+    public ResourceTexture getResource(int resourceId, boolean scaled) {
+        return getResourceInternal(resourceId, scaled);
+    }
+
+    private ResourceTexture getResourceInternal(int resourceId, boolean scaled) {
+        final SparseArray<ResourceTexture> cache = (scaled) ? sCacheScaled : sCacheUnscaled;
+        ResourceTexture texture = cache.get(resourceId);
+        if (texture == null && resourceId != 0) {
+            texture = new ResourceTexture(resourceId, scaled);
+            cache.put(resourceId, texture);
+        }
+        return texture;
+    }
+
+    public void clearCache() {
+        clearTextureArray(sCacheScaled);
+        clearTextureArray(sCacheUnscaled);
+    }
+
+    private void clearTextureArray(SparseArray<ResourceTexture> array) {
+        /*
+         * final int size = array.size(); for (int i = 0; i < size; ++i) { ResourceTexture texture = array.get(array.keyAt(i)); if
+         * (texture != null) { texture.clear(); } }
+         */
+        array.clear();
+    }
+
+    /** Render API */
+
+    public long getFrameTime() {
+        return mFrameTime;
+    }
+
+    public float getFrameInterval() {
+        return mFrameInterval;
+    }
+
+    public void prime(Texture texture, boolean highPriority) {
+        if (texture != null && texture.mState == Texture.STATE_UNLOADED && (highPriority || mLoadingCount < MAX_LOADING_COUNT)) {
+            queueLoad(texture, highPriority);
+        }
+    }
+
+    public void loadTexture(Texture texture) {
+        if (texture != null) {
+            switch (texture.mState) {
+            case Texture.STATE_UNLOADED:
+            case Texture.STATE_QUEUED:
+                int[] textureId = new int[1];
+                texture.mState = Texture.STATE_LOADING;
+                loadTextureAsync(texture);
+                uploadTexture(texture, textureId);
+                break;
+            }
+        }
+    }
+
+    private void loadTextureAsync(Texture texture) {
+        try {
+            Bitmap bitmap = texture.load(this);
+            if (bitmap != null) {
+                bitmap = Utils.resizeBitmap(bitmap, 1024);
+                int width = bitmap.getWidth();
+                int height = bitmap.getHeight();
+                texture.mWidth = width;
+                texture.mHeight = height;
+                // Create a padded bitmap if the natural size is not a power of 2.
+                if (!Shared.isPowerOf2(width) || !Shared.isPowerOf2(height)) {
+                    int paddedWidth = Shared.nextPowerOf2(width);
+                    int paddedHeight = Shared.nextPowerOf2(height);
+                    Bitmap.Config config = bitmap.getConfig();
+                    if (config == null)
+                        config = Bitmap.Config.RGB_565;
+                    if (width * height >= 512 * 512)
+                        config = Bitmap.Config.RGB_565;
+                    Bitmap padded = Bitmap.createBitmap(paddedWidth, paddedHeight, config);
+                    Canvas canvas = new Canvas(padded);
+                    canvas.drawBitmap(bitmap, 0, 0, null);
+                    bitmap.recycle();
+                    bitmap = padded;
+                    // Store normalized width and height for use in texture coordinates.
+                    texture.mNormalizedWidth = (float) width / (float) paddedWidth;
+                    texture.mNormalizedHeight = (float) height / (float) paddedHeight;
+                } else {
+                    texture.mNormalizedWidth = 1.0f;
+                    texture.mNormalizedHeight = 1.0f;
+                }
+            }
+            texture.mBitmap = bitmap;
+        } catch (Exception e) {
+            texture.mBitmap = null;
+        } catch (OutOfMemoryError eMem) {
+            Log.i(TAG, "Bitmap power of 2 creation fail, outofmemory");
+            handleLowMemory();
+        }
+    }
+
+    public boolean bind(Texture texture) {
+        if (texture != null) {
+            switch (texture.mState) {
+            case Texture.STATE_UNLOADED:
+                if (texture.getClass().equals(ResourceTexture.class)) {
+                    loadTexture(texture);
+                    return true;
+                }
+                if (mLoadingCount < MAX_LOADING_COUNT) {
+                    queueLoad(texture, false);
+                }
+                break;
+            case Texture.STATE_LOADED:
+                mGL.glBindTexture(GL11.GL_TEXTURE_2D, texture.mId);
+                return true;
+            default:
+                break;
+            }
+        }
+        return false;
+    }
+
+    public void setAlpha(float alpha) {
+        GL11 gl = mGL;
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
+        gl.glColor4f(alpha, alpha, alpha, alpha);
+        mAlpha = alpha;
+    }
+
+    public float getAlpha() {
+        return mAlpha;
+    }
+
+    public void setColor(float red, float green, float blue, float alpha) {
+        GL11 gl = mGL;
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
+        gl.glColor4f(red, green, blue, alpha);
+        mAlpha = alpha;
+    }
+
+    public void resetColor() {
+        GL11 gl = mGL;
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE);
+        gl.glColor4f(1, 1, 1, 1);
+    }
+
+    public boolean isLoadingExpensiveTextures() {
+        return mLoadingExpensiveTexturesStartTime != 0;
+    }
+
+    public long elapsedLoadingExpensiveTextures() {
+        long startTime = mLoadingExpensiveTexturesStartTime;
+        if (startTime != 0) {
+            return SystemClock.uptimeMillis() - startTime;
+        } else {
+            return -1;
+        }
+    }
+
+    private void queueLoad(final Texture texture, boolean highPriority) {
+        // Allow the texture to defer queuing.
+        if (!texture.shouldQueue()) {
+            return;
+        }
+
+        // Change the texture state to loading.
+        texture.mState = Texture.STATE_LOADING;
+
+        // Push the texture onto the load input queue.
+        Deque<Texture> inputQueue = (texture.isCached()) ? mLoadInputQueueCached : mLoadInputQueue;
+        inputQueue = (texture.isUncachedVideo()) ? mLoadInputQueueVideo : mLoadInputQueue;
+        synchronized (inputQueue) {
+            if (highPriority) {
+                inputQueue.addFirst(texture);
+            } else {
+                inputQueue.addLast(texture);
+            }
+            inputQueue.notify();
+        }
+        ++mLoadingCount;
+    }
+
+    public void draw2D(Texture texture, float x, float y) {
+        if (bind(texture)) {
+            final float width = texture.getWidth();
+            final float height = texture.getHeight();
+            ((GL11Ext) mGL).glDrawTexfOES(x, mViewHeight - y - height, 0f, width, height);
+        }
+    }
+
+    public void draw2D(Texture texture, float x, float y, float width, float height) {
+        if (bind(texture)) {
+            ((GL11Ext) mGL).glDrawTexfOES(x, mViewHeight - y - height, 0f, width, height);
+        }
+    }
+
+    public void draw2D(Texture texture, int x, int y, int width, int height) {
+        if (bind(texture)) {
+            ((GL11Ext) mGL).glDrawTexiOES(x, (int) (mViewHeight - y - height), 0, width, height);
+        }
+    }
+
+    public void draw2D(float x, float y, float z, float width, float height) {
+        ((GL11Ext) mGL).glDrawTexfOES(x, mViewHeight - y - height, z, width, height);
+    }
+
+    public boolean bindMixed(Texture from, Texture to, float ratio) {
+        // Bind "from" and "to" to TEXTURE0 and TEXTURE1, respectively.
+        final GL11 gl = mGL;
+        boolean bind = true;
+        bind &= bind(from);
+        gl.glActiveTexture(GL11.GL_TEXTURE1);
+        bind &= bind(to);
+        if (!bind) {
+            return false;
+        }
+
+        // Enable TEXTURE1.
+        gl.glEnable(GL11.GL_TEXTURE_2D);
+
+        // Interpolate the RGB and alpha values between both textures.
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_COMBINE);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);
+
+        // Specify the interpolation factor via the alpha component of GL_TEXTURE_ENV_COLOR.
+        final float[] color = { 1f, 1f, 1f, ratio };
+        gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, color, 0);
+
+        // Wire up the interpolation factor for RGB.
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
+
+        // Wire up the interpolation factor for alpha.
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
+        return true;
+    }
+
+    public void unbindMixed() {
+        // Disable TEXTURE1.
+        final GL11 gl = mGL;
+        gl.glDisable(GL11.GL_TEXTURE_2D);
+
+        // Switch back to the default texture unit.
+        gl.glActiveTexture(GL11.GL_TEXTURE0);
+    }
+
+    public void drawMixed2D(Texture from, Texture to, float ratio, float x, float y, float z, float width, float height) {
+        final GL11 gl = mGL;
+
+        // Bind "from" and "to" to TEXTURE0 and TEXTURE1, respectively.
+        if (bind(from)) {
+            gl.glActiveTexture(GL11.GL_TEXTURE1);
+            if (bind(to)) {
+                // Enable TEXTURE1.
+                gl.glEnable(GL11.GL_TEXTURE_2D);
+
+                // Interpolate the RGB and alpha values between both textures.
+                gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_COMBINE);
+                gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
+                gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);
+
+                // Specify the interpolation factor via the alpha component of GL_TEXTURE_ENV_COLOR.
+                final float[] color = { 1f, 1f, 1f, ratio };
+                gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, color, 0);
+
+                // Wire up the interpolation factor for RGB.
+                gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
+                gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
+
+                // Wire up the interpolation factor for alpha.
+                gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
+                gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
+
+                // Draw the combined texture.
+                ((GL11Ext) gl).glDrawTexfOES(x, mViewHeight - y - height, z, width, height);
+
+                // Disable TEXTURE1.
+                gl.glDisable(GL11.GL_TEXTURE_2D);
+            }
+
+            // Switch back to the default texture unit.
+            gl.glActiveTexture(GL11.GL_TEXTURE0);
+        }
+    }
+
+    public void processAllTextures() {
+        processTextures(true);
+    }
+
+    final static int[] textureId = new int[1];
+
+    /** Uploads at most one texture to GL. */
+    private void processTextures(boolean processAll) {
+        // Destroy any textures that are no longer referenced.
+        GL11 gl = mGL;
+        TextureReference textureReference;
+        while ((textureReference = (TextureReference) mUnreferencedTextureQueue.poll()) != null) {
+            textureId[0] = textureReference.textureId;
+            GL11 glOld = textureReference.gl;
+            if (glOld == gl) {
+                gl.glDeleteTextures(1, textureId, 0);
+            }
+            mActiveTextureList.remove(textureReference.activeListEntry);
+        }
+        Deque<Texture> outputQueue = mLoadOutputQueue;
+        Texture texture;
+        do {
+            // Upload loaded textures to the GPU one frame at a time.
+            synchronized (outputQueue) {
+                texture = outputQueue.pollFirst();
+            }
+            if (texture != null) {
+                // Extract the bitmap from the texture.
+                uploadTexture(texture, textureId);
+
+                // Decrement the loading count.
+                --mLoadingCount;
+            } else {
+                break;
+            }
+        } while (processAll);
+    }
+
+    private void uploadTexture(Texture texture, int[] textureId) {
+        Bitmap bitmap = texture.mBitmap;
+        GL11 gl = mGL;
+        if (bitmap != null) {
+            final int width = texture.mWidth;
+            final int height = texture.mHeight;
+
+            // Define a vertically flipped crop rectangle for OES_draw_texture.
+            int[] cropRect = { 0, height, width, -height };
+
+            // Handle texture upload failures
+            int numTextureFails = 0;
+            boolean textureFail = false;
+            do {
+                textureFail = false;
+                // Upload the bitmap to a new texture.
+                gl.glGenTextures(1, textureId, 0);
+                gl.glBindTexture(GL11.GL_TEXTURE_2D, textureId[0]);
+                gl.glTexParameteriv(GL11.GL_TEXTURE_2D, GL11Ext.GL_TEXTURE_CROP_RECT_OES, cropRect, 0);
+                gl.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
+                gl.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
+                gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+                gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+                GLUtils.texImage2D(GL11.GL_TEXTURE_2D, 0, bitmap, 0);
+                int glError = gl.glGetError();
+                if (glError == GL11.GL_OUT_OF_MEMORY) {
+                    Log.i(TAG, "Texture creation fail, glError " + glError + " retry id " + numTextureFails);
+                    ++numTextureFails;
+                    textureFail = true;
+                    handleLowMemory();
+                    // TODO: Retry logic
+                    numTextureFails = 3;
+                }
+            } while (textureFail && numTextureFails < 3);
+            bitmap.recycle();
+
+            // Update texture state.
+            texture.mBitmap = null;
+            texture.mId = textureId[0];
+            texture.mState = Texture.STATE_LOADED;
+
+            // Add to the active list.
+            final TextureReference textureRef = new TextureReference(texture, gl, mUnreferencedTextureQueue, textureId[0]);
+            mActiveTextureList.add(textureRef.activeListEntry);
+            requestRender();
+        } else {
+            texture.mState = Texture.STATE_ERROR;
+        }
+
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        Sensor sensorAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+        Sensor sensorOrientation = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+        if (sensorAccelerometer != null) {
+            mSensorManager.registerListener(this, sensorAccelerometer, SensorManager.SENSOR_DELAY_UI);
+        }
+        if (sensorOrientation != null) {
+            mSensorManager.registerListener(this, sensorOrientation, SensorManager.SENSOR_DELAY_UI);
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        Log.i(TAG, "OnPause RenderView " + this);
+        mSensorManager.unregisterListener(this);
+    }
+
+    /** Renders a frame of the UI. */
+    // @Override
+    public void onDrawFrame(GL10 gl1) {
+        GL11 gl = (GL11) gl1;
+        if (!mFirstDraw) {
+            Log.i(TAG, "First Draw");
+        }
+        mFirstDraw = true;
+        // setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+        // Rebuild the display lists if the render tree has changed.
+        if (mListsDirty) {
+            updateLists();
+        }
+
+        boolean wasLoadingExpensiveTextures = isLoadingExpensiveTextures();
+        boolean loadingExpensiveTextures = false;
+        int numTextureThreads = mTextureLoadThreads.length;
+        for (int i = 2; i < numTextureThreads; ++i) {
+            if (mTextureLoadThreads[i].mIsLoading) {
+                loadingExpensiveTextures = true;
+                break;
+            }
+        }
+        if (loadingExpensiveTextures != wasLoadingExpensiveTextures) {
+            mLoadingExpensiveTexturesStartTime = loadingExpensiveTextures ? SystemClock.uptimeMillis() : 0;
+        }
+
+        // Upload new textures.
+        processTextures(false);
+
+        // Update the current time and frame time interval.
+        final long now = SystemClock.uptimeMillis();
+        final float dt = 0.001f * Math.min(50, now - mFrameTime);
+        mFrameInterval = dt;
+        mFrameTime = now;
+
+        // Dispatch the current touch event.
+        processCurrentEvent();
+        processTouchEvent();
+        // Run the update pass.
+        final Lists lists = mLists;
+        synchronized (lists) {
+            final ArrayList<Layer> updateList = lists.updateList;
+            boolean isDirty = false;
+            for (int i = 0, size = updateList.size(); i != size; ++i) {
+                boolean retVal = updateList.get(i).update(this, mFrameInterval);
+                isDirty |= retVal;
+            }
+            if (isDirty) {
+                requestRender();
+            }
+
+            // Clear the depth buffer.
+            gl.glClear(GL11.GL_DEPTH_BUFFER_BIT);
+            gl.glEnable(GL11.GL_SCISSOR_TEST);
+            gl.glScissor(0, 0, getWidth(), getHeight());
+
+            // Run the opaque pass.
+            gl.glDisable(GL11.GL_BLEND);
+            final ArrayList<Layer> opaqueList = lists.opaqueList;
+            for (int i = opaqueList.size() - 1; i >= 0; --i) {
+                final Layer layer = opaqueList.get(i);
+                if (!layer.mHidden) {
+                    layer.renderOpaque(this, gl);
+                }
+            }
+
+            // Run the blended pass.
+            gl.glEnable(GL11.GL_BLEND);
+            final ArrayList<Layer> blendedList = lists.blendedList;
+            for (int i = 0, size = blendedList.size(); i != size; ++i) {
+                final Layer layer = blendedList.get(i);
+                if (!layer.mHidden) {
+                    layer.renderBlended(this, gl);
+                }
+            }
+        }
+        int priority = Process.getThreadPriority(Process.myTid());
+        if (priority != Process.THREAD_PRIORITY_URGENT_DISPLAY) {
+            Log.e(TAG, "The display thread should never be lowered to priority level " + priority);
+        }
+    }
+
+    private void processCurrentEvent() {
+        final int type = mCurrentEventType;
+        switch (type) {
+        case EVENT_KEY:
+            processKeyEvent();
+            break;
+        case EVENT_FOCUS:
+            processFocusEvent();
+            break;
+        default:
+            break;
+        }
+        synchronized (this) {
+            mCurrentEventType = EVENT_NONE;
+            this.notify();
+        }
+    }
+
+    private void processTouchEvent() {
+        MotionEvent event = null;
+        do {
+            // We look at the touch event queue and process one event at a time
+            synchronized (mTouchEventQueue) {
+                event = mTouchEventQueue.pollFirst();
+            }
+            if (event == null)
+                return;
+
+            // Detect the hit layer.
+            final int action = event.getAction();
+            Layer target;
+            if (action == MotionEvent.ACTION_DOWN) {
+                target = hitTest(event.getX(), event.getY());
+                mTouchEventTarget = target;
+            } else {
+                target = mTouchEventTarget;
+            }
+
+            // Dispatch event to the hit layer.
+            if (target != null) {
+                target.onTouchEvent(event);
+            }
+
+            // Clear the hit layer.
+            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+                mTouchEventTarget = null;
+            }
+            event.recycle();
+        } while (event != null);
+        synchronized (this) {
+            this.notify();
+        }
+    }
+
+    private void processKeyEvent() {
+        // Get the event.
+        final KeyEvent event = mCurrentKeyEvent;
+        boolean result = false;
+        mCurrentKeyEvent = null;
+
+        // Dispatch the event to the root layer.
+        if (mRootLayer != null) {
+            if (event.getAction() == KeyEvent.ACTION_DOWN) {
+                result = mRootLayer.onKeyDown(event.getKeyCode(), event);
+            } else {
+                result = mRootLayer.onKeyUp(event.getKeyCode(), event);
+            }
+        }
+        mCurrentKeyEventResult = result;
+    }
+
+    private void processFocusEvent() {
+        // Get event information.
+        if (mRootLayer != null) {
+
+        }
+    }
+
+    private Layer hitTest(float x, float y) {
+        final ArrayList<Layer> hitTestList = mLists.hitTestList;
+        for (int i = hitTestList.size() - 1; i >= 0; --i) {
+            final Layer layer = hitTestList.get(i);
+            if (layer != null && !layer.mHidden) {
+                final float layerX = layer.mX;
+                final float layerY = layer.mY;
+                if (x >= layerX && y >= layerY && x < layerX + layer.mWidth && y < layerY + layer.mHeight
+                        && layer.containsPoint(x, y)) {
+                    return layer;
+                }
+            }
+        }
+        return null;
+    }
+
+    private void updateLists() {
+        if (mRootLayer != null) {
+            synchronized (mLists) {
+                mLists.clear();
+                mRootLayer.generate(this, mLists);
+            }
+        }
+    }
+
+    /** Called when the OpenGL surface is recreated without destroying the context. */
+    public void onSurfaceChanged(GL10 gl1, int width, int height) {
+        GL11 gl = (GL11) gl1;
+        mFirstDraw = false;
+        mViewWidth = width;
+        mViewHeight = height;
+        if (mRootLayer != null) {
+            mRootLayer.setSize(width, height);
+        }
+
+        // Set the viewport and projection matrix.
+        final float zNear = 0.1f;
+        final float zFar = 100.0f;
+        gl.glViewport(0, 0, width, height);
+        gl.glMatrixMode(GL11.GL_PROJECTION);
+        gl.glLoadIdentity();
+        GLU.gluPerspective(gl, 45.0f, (float) width / height, zNear, zFar);
+        if (mRootLayer != null) {
+            mRootLayer.onSurfaceChanged(this, width, height);
+        }
+        gl.glMatrixMode(GL11.GL_MODELVIEW);
+    }
+
+    public void setFov(float fov) {
+        GL11 gl = mGL;
+        gl.glMatrixMode(GL11.GL_PROJECTION);
+        gl.glLoadIdentity();
+        final float zNear = 0.1f;
+        final float zFar = 100.0f;
+        GLU.gluPerspective(gl, fov, (float) getWidth() / getHeight(), zNear, zFar);
+        gl.glMatrixMode(GL11.GL_MODELVIEW);
+    }
+
+    /** Called when the context is created, possibly after automatic destruction. */
+    public void onSurfaceCreated(GL10 gl1, EGLConfig config) {
+        // Clear the resource texture cache.
+        clearCache();
+
+        // Log.i(TAG, "Surface Created for " + this);
+        GL11 gl = (GL11) gl1;
+        if (mGL == null) {
+            mGL = gl;
+        } else {
+            // The GL Object has changed.
+            Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl);
+            mGL = gl;
+        }
+        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+        // Increase the priority of the render thread.
+        Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);
+
+        // Disable unused state.
+        gl.glEnable(GL11.GL_DITHER);
+        gl.glDisable(GL11.GL_LIGHTING);
+
+        // Set global state.
+        gl.glHint(GL11.GL_PERSPECTIVE_CORRECTION_HINT, GL11.GL_NICEST);
+
+        // Enable textures.
+        gl.glEnable(GL11.GL_TEXTURE_2D);
+        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE);
+
+        // Set up state for multitexture operations. Since multitexture is currently used
+        // only for layered crossfades the needed state can be factored out into one-time
+        // initialization. This section may need to be folded into drawMixed2D() if multitexture
+        // is used for other effects.
+
+        // Enable Vertex Arrays
+        gl.glEnableClientState(GL11.GL_VERTEX_ARRAY);
+        gl.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY);
+        gl.glClientActiveTexture(GL11.GL_TEXTURE1);
+        gl.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY);
+        gl.glClientActiveTexture(GL11.GL_TEXTURE0);
+
+        // Enable depth test.
+        gl.glEnable(GL11.GL_DEPTH_TEST);
+        gl.glDepthFunc(GL11.GL_LEQUAL);
+
+        // Set the blend function for premultiplied alpha.
+        gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);
+
+        // Set the background color.
+        gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+        gl.glClear(GL11.GL_COLOR_BUFFER_BIT);
+
+        // Start reloading textures if the context was automatically destroyed.
+        if (!mActiveTextureList.isEmpty()) {
+            DirectLinkedList.Entry<TextureReference> iter = mActiveTextureList.getHead();
+            while (iter != null) {
+                final Texture texture = iter.value.get();
+                if (texture != null) {
+                    texture.mState = Texture.STATE_UNLOADED;
+                }
+                iter = iter.next;
+            }
+        }
+        mActiveTextureList.clear();
+        if (mRootLayer != null) {
+            mRootLayer.onSurfaceCreated(this, gl);
+        }
+        synchronized (mLists) {
+            ArrayList<Layer> systemList = mLists.systemList;
+            for (int i = systemList.size() - 1; i >= 0; --i) {
+                systemList.get(i).onSurfaceCreated(this, gl);
+            }
+        }
+    }
+
+    /** Indicates that the accuracy of a sensor value has changed. */
+    public void onAccuracyChanged(Sensor sensor, int accuracy) {
+    }
+
+    /** Indicates that a sensor value has changed. */
+    public void onSensorChanged(SensorEvent event) {
+        final int type = event.sensor.getType();
+        if ((!mPendingSensorEvent && type == Sensor.TYPE_ACCELEROMETER) || type == Sensor.TYPE_ORIENTATION) {
+            final SensorEvent e = event;
+            if (mRootLayer != null)
+                mRootLayer.onSensorChanged(RenderView.this, e);
+            if (type == Sensor.TYPE_ORIENTATION) {
+                requestRender();
+            }
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // Ignore events received before the surface is created to avoid
+        // deadlocking with GLSurfaceView's needToWait().
+        if (mGL == null) {
+            return false;
+        }
+        // Wait for the render thread to process this event.
+        synchronized (mTouchEventQueue) {
+            MotionEvent eventCopy = MotionEvent.obtain(event);
+            mTouchEventQueue.addLast(eventCopy);
+            requestRender();
+        }
+        return true;
+    }
+
+    @Override
+    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+
+        // Ignore events received before the surface is created to avoid
+        // deadlocking with GLSurfaceView's needToWait().
+        /*
+         * if (mGL == null) { return; }
+         * 
+         * // Wait for the render thread to process this event. try { synchronized (this) { mCurrentFocusEventGain = gainFocus;
+         * mCurrentFocusEventDirection = direction; mCurrentEventType = EVENT_FOCUS; do { wait(); } while (mCurrentEventType !=
+         * EVENT_NONE); } } catch (InterruptedException e) { // Stop waiting for the render thread if interrupted. }
+         */
+        requestRender();
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        // Ignore events received before the surface is created to avoid
+        // deadlocking with GLSurfaceView's needToWait().
+        if (mGL == null) {
+            return false;
+        }
+
+        // Wait for the render thread to process this event.
+        try {
+            synchronized (this) {
+                mCurrentKeyEvent = event;
+                mCurrentEventType = EVENT_KEY;
+                requestRender();
+                long timeout = SystemClock.uptimeMillis() + 50;
+                do {
+                    wait(50);
+                } while (mCurrentEventType != EVENT_NONE && SystemClock.uptimeMillis() < timeout);
+            }
+        } catch (InterruptedException e) {
+            // Stop waiting for the render thread if interrupted.
+        }
+
+        // Key events are handled on the main thread.
+        boolean retVal = false;
+        if (!mCurrentKeyEventResult) {
+            retVal = super.onKeyDown(keyCode, event);
+        } else {
+            retVal = true;
+        }
+        requestRender();
+        return retVal;
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+        super.surfaceDestroyed(holder);
+        // Log.i(TAG, "Surface destroyed for " + this);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        // Log.i(TAG, "Attaching to window for " + this);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        // Log.i(TAG, "Detaching from Window for " + this);
+    }
+
+    private final class TextureLoadThread extends Thread {
+        public boolean mIsLoading;
+
+        public TextureLoadThread() {
+            super("TextureLoad");
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+        }
+
+        public void run() {
+            RenderView view = RenderView.this;
+            Deque<Texture> inputQueue = (mCachedTextureLoadThread == this) ? view.mLoadInputQueueCached : view.mLoadInputQueue;
+            inputQueue = (mVideoTextureLoadThread == this) ? view.mLoadInputQueueVideo : view.mLoadInputQueue;
+            Deque<Texture> outputQueue = view.mLoadOutputQueue;
+            try {
+                for (;;) {
+                    // Pop the next texture from the input queue.
+                    Texture texture = null;
+                    synchronized (inputQueue) {
+                        while ((texture = inputQueue.pollFirst()) == null) {
+                            inputQueue.wait();
+                        }
+                    }
+                    if (mCachedTextureLoadThread != this)
+                        mIsLoading = true;
+                    // Load the texture bitmap.
+                    load(texture);
+                    mIsLoading = false;
+
+                    // Push the texture onto the output queue.
+                    synchronized (outputQueue) {
+                        outputQueue.addLast(texture);
+                    }
+                }
+            } catch (InterruptedException e) {
+                // Terminate the thread.
+            }
+        }
+
+        private void load(Texture texture) {
+            // Generate the texture bitmap.
+            RenderView view = RenderView.this;
+            view.loadTextureAsync(texture);
+            view.requestRender();
+        }
+    }
+
+    public void shutdown() {
+        // stop all the threads
+        for (int i = 0; i < NUM_TEXTURE_LOAD_THREADS; ++i) {
+            mTextureLoadThreads[i].interrupt();
+        }
+        ArrayUtils.clear(mTextureLoadThreads);
+        mRootLayer = null;
+        synchronized (mLists) {
+            mLists.clear();
+        }
+    }
+
+    public void handleLowMemory() {
+        Log.i(TAG, "Handling low memory condition");
+        if (mRootLayer != null) {
+            mRootLayer.handleLowMemory();
+        }
+    }
+
+    public Lists getLists() {
+        return mLists;
+    }
+}
diff --git a/src/com/cooliris/media/ResourceTexture.java b/src/com/cooliris/media/ResourceTexture.java
new file mode 100644
index 0000000..a7ee734
--- /dev/null
+++ b/src/com/cooliris/media/ResourceTexture.java
@@ -0,0 +1,49 @@
+package com.cooliris.media;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+public final class ResourceTexture extends Texture {
+    private final int mResourceId;
+    private final boolean mScaled;
+
+    @Override
+    public boolean isCached() {
+        return true;
+    }
+
+    public ResourceTexture(int resourceId, boolean scaled) {
+        mResourceId = resourceId;
+        mScaled = scaled;
+    }
+
+    @Override
+    protected Bitmap load(RenderView view) {
+        // Load a bitmap from the resource.
+        Bitmap bitmap = null;
+        if (mScaled) {
+            BitmapFactory.Options options = new BitmapFactory.Options();
+            options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+            bitmap = BitmapFactory.decodeResource(view.getResources(), mResourceId, options);
+        } else {
+            InputStream inputStream = view.getResources().openRawResource(mResourceId);
+            if (inputStream != null) {
+                try {
+                    BitmapFactory.Options options = new BitmapFactory.Options();
+                    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+                    bitmap = BitmapFactory.decodeStream(inputStream, null, options);
+                } catch (Exception e) {
+                } finally {
+                    try {
+                        inputStream.close();
+                    } catch (IOException e) { /* ignore */
+                    }
+                }
+            }
+        }
+        return bitmap;
+    }
+}
diff --git a/src/com/cooliris/media/ReverseGeocoder.java b/src/com/cooliris/media/ReverseGeocoder.java
new file mode 100644
index 0000000..646a329
--- /dev/null
+++ b/src/com/cooliris/media/ReverseGeocoder.java
@@ -0,0 +1,362 @@
+package com.cooliris.media;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+import android.content.Context;
+import android.location.Address;
+import android.location.Geocoder;
+import android.os.Process;
+
+public final class ReverseGeocoder extends Thread {
+    private static final int MAX_COUNTRY_NAME_LENGTH = 8;
+    // If two points are within 50 miles of each other, use "Around Palo Alto, CA" or "Around Mountain View, CA".
+    // instead of directly jumping to the next level and saying "California, US".
+    private static final int MAX_LOCALITY_MILE_RANGE = 50;
+
+    private static final String TAG = "ReverseGeocoder";
+
+    private final Geocoder mGeocoder;
+    private final Context mContext;
+    private final Deque<MediaSet> mQueue = new Deque<MediaSet>();
+    private final DiskCache mGeoCache = new DiskCache("geocoder-cache");
+
+    public ReverseGeocoder(Context context) {
+        super(TAG);
+        mContext = context;
+        mGeocoder = new Geocoder(mContext);
+        // Loading the addresses in the GeoCache.
+        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+        start();
+    }
+
+    public void enqueue(MediaSet set) {
+        Deque<MediaSet> inQueue = mQueue;
+        synchronized (inQueue) {
+            inQueue.addLast(set);
+            inQueue.notify();
+        }
+    }
+
+    @Override
+    public void run() {
+        Deque<MediaSet> queue = mQueue;
+        try {
+            for (;;) {
+                // Wait for the next request.
+                MediaSet set;
+                synchronized (queue) {
+                    while ((set = queue.pollFirst()) == null) {
+                        queue.wait();
+                    }
+                }
+                // Process the request.
+                process(set);
+            }
+        } catch (InterruptedException e) {
+            // Terminate the thread.
+        }
+    }
+
+    public void flushCache() {
+        mGeoCache.flush();
+    }
+
+    public void shutdown() {
+        flushCache();
+        mGeoCache.close();
+        this.interrupt();
+    }
+
+    private boolean process(final MediaSet set) {
+        if (!set.mLatLongDetermined) {
+            // No latitude, longitude information available.
+            set.mReverseGeocodedLocationComputed = true;
+            return false;
+        }
+        set.mReverseGeocodedLocation = computeMostGranularCommonLocation(set);
+        set.mReverseGeocodedLocationComputed = true;
+        return true;
+    }
+
+    protected String computeMostGranularCommonLocation(final MediaSet set) {
+        // The overall min and max latitudes and longitudes of the set.
+        double setMinLatitude = set.mMinLatLatitude;
+        double setMinLongitude = set.mMinLonLongitude;
+        double setMaxLatitude = set.mMaxLatLatitude;
+        double setMaxLongitude = set.mMaxLonLongitude;
+        Address addr1 = lookupAddress(setMinLatitude, setMinLongitude);
+        Address addr2 = lookupAddress(setMaxLatitude, setMaxLongitude);
+        if (addr1 == null || addr2 == null) {
+            return null;
+        }
+
+        // Look at the first line of the address.
+        String closestCommonLocation = valueIfEqual(addr1.getAddressLine(0), addr2.getAddressLine(0));
+        if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
+            return closestCommonLocation;
+        }
+
+        // Compare thoroughfare (street address) next.
+        closestCommonLocation = valueIfEqual(addr1.getThoroughfare(), addr2.getThoroughfare());
+        if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
+            return closestCommonLocation;
+        }
+
+        // Feature names can sometimes be useful like "Golden Gate Bridge" but can also be
+        // degenerate for street address (like just the house number).
+        closestCommonLocation = valueIfEqual(addr1.getFeatureName(), addr2.getFeatureName());
+        if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
+            try {
+                Integer.parseInt(closestCommonLocation);
+                closestCommonLocation = null;
+            } catch (final NumberFormatException nfe) {
+                // The feature name is not an integer, allow and continue.
+                return closestCommonLocation;
+            }
+        }
+
+        // Compare the locality.
+        closestCommonLocation = valueIfEqual(addr1.getLocality(), addr2.getLocality());
+        if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
+            String adminArea = addr1.getAdminArea();
+            if (adminArea != null && adminArea.length() > 0) {
+                closestCommonLocation += ", " + adminArea;
+            }
+            return closestCommonLocation;
+        }
+
+        // Just choose one of the localities if within a 50 mile radius.
+        int distance = (int) LocationMediaFilter.toMile(
+                LocationMediaFilter.distanceBetween(setMinLatitude, setMinLongitude, setMaxLatitude, setMaxLongitude));
+        if (distance < MAX_LOCALITY_MILE_RANGE) {
+            // Try each of the points and just return the first one to have a valid address.
+            Address minLatAddress = lookupAddress(setMinLatitude, set.mMinLatLongitude);
+            closestCommonLocation = getLocalityAdminForAddress(minLatAddress, true);
+            if (closestCommonLocation != null) {
+                return closestCommonLocation;
+            }
+            Address minLonAddress = lookupAddress(set.mMinLonLatitude, setMinLongitude);
+            closestCommonLocation = getLocalityAdminForAddress(minLonAddress, true);
+            if (closestCommonLocation != null) {
+                return closestCommonLocation;
+            }
+            Address maxLatAddress = lookupAddress(setMaxLatitude, set.mMaxLatLongitude);
+            closestCommonLocation = getLocalityAdminForAddress(maxLatAddress, true);
+            if (closestCommonLocation != null) {
+                return closestCommonLocation;
+            }
+            Address maxLonAddress = lookupAddress(set.mMaxLonLatitude, setMaxLongitude);
+            closestCommonLocation = getLocalityAdminForAddress(maxLonAddress, true);
+            if (closestCommonLocation != null) {
+                return closestCommonLocation;
+            }
+        }
+
+        // Check the administrative area.
+        closestCommonLocation = valueIfEqual(addr1.getAdminArea(), addr2.getAdminArea());
+        if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
+            String countryCode = addr1.getCountryCode();
+            if (countryCode != null && countryCode.length() > 0) {
+                closestCommonLocation += ", " + countryCode;
+            }
+            return closestCommonLocation;
+        }
+
+        // Check the country codes.
+        closestCommonLocation = valueIfEqual(addr1.getCountryCode(), addr2.getCountryCode());
+        if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
+            return closestCommonLocation;
+        }
+        // There is no intersection, let's choose a nicer name.
+        String addr1Country = addr1.getCountryName();
+        String addr2Country = addr2.getCountryName();
+        if (addr1Country.length() > MAX_COUNTRY_NAME_LENGTH || addr2Country.length() > MAX_COUNTRY_NAME_LENGTH) {
+            closestCommonLocation = addr1.getCountryCode() + " - " + addr2.getCountryCode();
+        } else {
+            closestCommonLocation = addr1Country + " - " + addr2Country;
+        }
+        return closestCommonLocation;
+    }
+
+    protected String getReverseGeocodedLocation(final double latitude, final double longitude, final int desiredNumDetails) {
+        String location = null;
+        int numDetails = 0;
+        Address addr = lookupAddress(latitude, longitude);
+
+        if (addr != null) {
+            // Look at the first line of the address, thorough fare and feature name in order and pick one.
+            location = addr.getAddressLine(0);
+            if (location != null && !("null".equals(location))) {
+                numDetails++;
+            } else {
+                location = addr.getThoroughfare();
+                if (location != null && !("null".equals(location))) {
+                    numDetails++;
+                } else {
+                    location = addr.getFeatureName();
+                    if (location != null && !("null".equals(location))) {
+                        numDetails++;
+                    }
+                }
+            }
+    
+            if (numDetails == desiredNumDetails) {
+                return location;
+            }
+    
+            String locality = addr.getLocality();
+            if (locality != null && !("null".equals(locality))) {
+                if (location != null && location.length() > 0) {
+                    location += ", " + locality;
+                } else {
+                    location = locality;
+                }
+                numDetails++;
+            }
+    
+            if (numDetails == desiredNumDetails) {
+                return location;
+            }
+    
+            String adminArea = addr.getAdminArea();
+            if (adminArea != null && !("null".equals(adminArea))) {
+                if (location != null && location.length() > 0) {
+                    location += ", " + adminArea;
+                } else {
+                    location = adminArea;
+                }
+                numDetails++;
+            }
+
+            if (numDetails == desiredNumDetails) {
+                return location;
+            }
+
+            String countryCode = addr.getCountryCode();
+            if (countryCode != null && !("null".equals(countryCode))) {
+                if (location != null && location.length() > 0) {
+                    location += ", " + countryCode;
+                } else {
+                    location = addr.getCountryName();
+                }
+            }
+        }
+
+        return location;
+    }
+
+    private String getLocalityAdminForAddress(final Address addr, final boolean approxLocation) {
+        if (addr == null)
+            return "";
+        String localityAdminStr = addr.getLocality();
+        if (localityAdminStr != null && !("null".equals(localityAdminStr))) {
+            if (approxLocation) {
+                // TODO: Uncomment these lines as soon as we may translations for R.string.around.
+                // localityAdminStr = mContext.getResources().getString(R.string.around) + " " + localityAdminStr;
+            }
+            String adminArea = addr.getAdminArea();
+            if (adminArea != null && adminArea.length() > 0) {
+                localityAdminStr += ", " + adminArea;
+            }
+            return localityAdminStr;
+        }
+        return null;
+    }
+
+    private Address lookupAddress(final double latitude, final double longitude) {
+        try {
+            long locationKey = (long) (((latitude + LocationMediaFilter.LAT_MAX) * 2 * LocationMediaFilter.LAT_MAX + (longitude + LocationMediaFilter.LON_MAX)) * LocationMediaFilter.EARTH_RADIUS_METERS);
+            byte[] cachedLocation = mGeoCache.get(locationKey, 0);
+            Address address = null;
+            if (cachedLocation == null || cachedLocation.length == 0) {
+                List<Address> addresses = mGeocoder.getFromLocation(latitude, longitude, 1);
+                if (!addresses.isEmpty()) {
+                    address = addresses.get(0);
+                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
+                    DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256));
+                    Locale locale = address.getLocale();
+                    Utils.writeUTF(dos, locale.getLanguage());
+                    Utils.writeUTF(dos, locale.getCountry());
+                    Utils.writeUTF(dos, locale.getVariant());
+
+                    Utils.writeUTF(dos, address.getThoroughfare());
+                    int numAddressLines = address.getMaxAddressLineIndex();
+                    dos.writeInt(numAddressLines);
+                    for (int i = 0; i < numAddressLines; ++i) {
+                        Utils.writeUTF(dos, address.getAddressLine(i));
+                    }
+                    Utils.writeUTF(dos, address.getFeatureName());
+                    Utils.writeUTF(dos, address.getLocality());
+                    Utils.writeUTF(dos, address.getAdminArea());
+                    Utils.writeUTF(dos, address.getSubAdminArea());
+
+                    Utils.writeUTF(dos, address.getCountryName());
+                    Utils.writeUTF(dos, address.getCountryCode());
+                    Utils.writeUTF(dos, address.getPostalCode());
+                    Utils.writeUTF(dos, address.getPhone());
+                    Utils.writeUTF(dos, address.getUrl());
+
+                    dos.flush();
+                    mGeoCache.put(locationKey, bos.toByteArray());
+                    dos.close();
+                }
+            } else {
+                // Parsing the address from the byte stream.
+                DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(cachedLocation), 256));
+                String language = Utils.readUTF(dis);
+                String country = Utils.readUTF(dis);
+                String variant = Utils.readUTF(dis);
+                Locale locale = null;
+                if (language != null) {
+                    if (country == null) {
+                        locale = new Locale(language);
+                    } else if (variant == null) {
+                        locale = new Locale(country, language);
+                    } else {
+                        locale = new Locale(country, language, variant);
+                    }
+                }
+                if (!locale.getLanguage().equals(Locale.getDefault().getLanguage())) {
+                    mGeoCache.delete(locationKey);
+                    dis.close();
+                    return lookupAddress(latitude, longitude);
+                }
+                address = new Address(locale);
+
+                address.setThoroughfare(Utils.readUTF(dis));
+                int numAddressLines = dis.readInt();
+                for (int i = 0; i < numAddressLines; ++i) {
+                    address.setAddressLine(i, Utils.readUTF(dis));
+                }
+                address.setFeatureName(Utils.readUTF(dis));
+                address.setLocality(Utils.readUTF(dis));
+                address.setAdminArea(Utils.readUTF(dis));
+                address.setSubAdminArea(Utils.readUTF(dis));
+
+                address.setCountryName(Utils.readUTF(dis));
+                address.setCountryCode(Utils.readUTF(dis));
+                address.setPostalCode(Utils.readUTF(dis));
+                address.setPhone(Utils.readUTF(dis));
+                address.setUrl(Utils.readUTF(dis));
+                dis.close();
+            }
+            return address;
+
+        } catch (IOException e) {
+            // Ignore.
+        }
+        return null;
+    }
+
+    private String valueIfEqual(String a, String b) {
+        return (a != null && b != null && a.equalsIgnoreCase(b)) ? a : null;
+    }
+}
diff --git a/src/com/cooliris/media/RootLayer.java b/src/com/cooliris/media/RootLayer.java
new file mode 100644
index 0000000..7debd1a
--- /dev/null
+++ b/src/com/cooliris/media/RootLayer.java
@@ -0,0 +1,31 @@
+package com.cooliris.media;
+
+import javax.microedition.khronos.opengles.GL11;
+import android.hardware.SensorEvent;
+import android.view.KeyEvent;
+
+public abstract class RootLayer extends Layer {
+    public void onOrientationChanged(int orientation) {
+    }
+
+    public void onSurfaceCreated(RenderView renderView, GL11 gl) {
+    }
+
+    public void onSurfaceChanged(RenderView view, int width, int height) {
+    }
+
+    public void onSensorChanged(RenderView view, SensorEvent e) {
+    }
+
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return false;
+    }
+
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return false;
+    }
+    
+    public void handleLowMemory() {
+     
+    }
+ }
diff --git a/src/com/cooliris/media/RotateBitmap.java b/src/com/cooliris/media/RotateBitmap.java
new file mode 100644
index 0000000..45df573
--- /dev/null
+++ b/src/com/cooliris/media/RotateBitmap.java
@@ -0,0 +1,96 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 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.
+ */
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+
+public class RotateBitmap {
+    public static final String TAG = "RotateBitmap";
+    private Bitmap mBitmap;
+    private int mRotation;
+
+    public RotateBitmap(Bitmap bitmap) {
+        mBitmap = bitmap;
+        mRotation = 0;
+    }
+
+    public RotateBitmap(Bitmap bitmap, int rotation) {
+        mBitmap = bitmap;
+        mRotation = rotation % 360;
+    }
+
+    public void setRotation(int rotation) {
+        mRotation = rotation;
+    }
+
+    public int getRotation() {
+        return mRotation;
+    }
+
+    public Bitmap getBitmap() {
+        return mBitmap;
+    }
+
+    public void setBitmap(Bitmap bitmap) {
+        mBitmap = bitmap;
+    }
+
+    public Matrix getRotateMatrix() {
+        // By default this is an identity matrix.
+        Matrix matrix = new Matrix();
+        if (mRotation != 0) {
+            // We want to do the rotation at origin, but since the bounding
+            // rectangle will be changed after rotation, so the delta values
+            // are based on old & new width/height respectively.
+            int cx = mBitmap.getWidth() / 2;
+            int cy = mBitmap.getHeight() / 2;
+            matrix.preTranslate(-cx, -cy);
+            matrix.postRotate(mRotation);
+            matrix.postTranslate(getWidth() / 2, getHeight() / 2);
+        }
+        return matrix;
+    }
+
+    public boolean isOrientationChanged() {
+        return (mRotation / 90) % 2 != 0;
+    }
+
+    public int getHeight() {
+        if (isOrientationChanged()) {
+            return mBitmap.getWidth();
+        } else {
+            return mBitmap.getHeight();
+        }
+    }
+
+    public int getWidth() {
+        if (isOrientationChanged()) {
+            return mBitmap.getHeight();
+        } else {
+            return mBitmap.getWidth();
+        }
+    }
+
+    public void recycle() {
+        if (mBitmap != null) {
+            mBitmap.recycle();
+            mBitmap = null;
+        }
+    }
+}
+
diff --git a/src/com/cooliris/media/SelectionMenu.java b/src/com/cooliris/media/SelectionMenu.java
new file mode 100644
index 0000000..5695ea3
--- /dev/null
+++ b/src/com/cooliris/media/SelectionMenu.java
@@ -0,0 +1,326 @@
+package com.cooliris.media;
+
+//
+//import java.util.ArrayList;
+//import java.util.List;
+//
+//import javax.microedition.khronos.opengles.GL11;
+//
+//import android.content.ActivityNotFoundException;
+//import android.content.Context;
+//import android.content.Intent;
+//import android.content.pm.PackageManager;
+//import android.content.pm.ResolveInfo;
+//import android.net.Uri;
+//import android.util.FloatMath;
+//import android.util.Log;
+//import android.view.MotionEvent;
+//import android.widget.Toast;
+//
+//public final class SelectionMenu extends Layer {
+//    public static final int HEIGHT = 58;
+//
+//    private static final int BUTTON_SHARE = 0;
+//    private static final int BUTTON_DELETE = 1;
+//    private static final int BUTTON_MORE = 2;
+//    private static final int BUTTON_SELECT_ALL = 3;
+//    private static final int BUTTON_DESELECT_ALL = 4;
+//
+//    private static final int[] BUTTON_LABELS = { R.string.Share, R.string.Delete,
+//                                                R.string.More };
+//    private static final int[] BUTTON_ICONS = { R.drawable.icon_share,
+//                                               R.drawable.icon_delete, R.drawable.icon_more };
+//    private static final float BUTTON_ICON_SIZE = 34f;
+//
+//    protected static final String TAG = "SelectionMenu";
+//
+//    private final Context mContext;
+//    private final HudLayer mHud;
+//    private final Texture mLowerBackground = ResourceTexture.get(R.drawable.selection_lower_bg);
+//    private final ResourceTexture mDividerImage = ResourceTexture
+//                    .get(R.drawable.selection_menu_divider);
+//    private final ResourceTexture mSelectionLeft = ResourceTexture
+//                    .get(R.drawable.selection_menu_bg_pressed_left);
+//    private final ResourceTexture mSelectionFill = ResourceTexture
+//                    .get(R.drawable.selection_menu_bg_pressed);
+//    private final ResourceTexture mSelectionRight = ResourceTexture
+//                    .get(R.drawable.selection_menu_bg_pressed_right);
+//    private final Button[] mButtons = new Button[BUTTON_LABELS.length];
+//    private float mDividerSpacing = 0f;
+//    private boolean mDrawButtonIcons = true;
+//    private int mTouchButton = -1;
+//    private boolean mTouchOver = false;
+//    private final PopupMenu mPopupMenu;
+//
+//    SelectionMenu(HudLayer hud, Context context) {
+//        mContext = context;
+//        mHud = hud;
+//        mPopupMenu = new PopupMenu(context);
+//
+//        // Prepare format for the small labels.
+//        StringTexture.Config smallLabelFormat = new StringTexture.Config();
+//        smallLabelFormat.fontSize = 14;
+//
+//        // Prepare format for the large labels.
+//        StringTexture.Config largeLabelFormat = new StringTexture.Config();
+//        largeLabelFormat.fontSize = 20f;
+//
+//        // Create icon and label textures.
+//        for (int i = 0; i < BUTTON_LABELS.length; ++i) {
+//            Button button = new Button();
+//            button.icon = ResourceTexture.get(BUTTON_ICONS[i]);
+//            button.smallLabel = new SimpleStringTexture(context.getString(BUTTON_LABELS[i]),
+//                                                        smallLabelFormat);
+//            button.largeLabel = new SimpleStringTexture(context.getString(BUTTON_LABELS[i]),
+//                                                        largeLabelFormat);
+//            mButtons[i] = button;
+//        }
+//    }
+//
+//    private void layout() {
+//        // Perform layout with icons and text labels.
+//        final Button[] buttons = mButtons;
+//        final float width = mWidth;
+//        final float buttonWidth = width / buttons.length;
+//        float dx = 0f;
+//        boolean overflow = false;
+//
+//        mDividerSpacing = buttonWidth;
+//
+//        // Attempt layout with icons + labels.
+//        for (int i = 0; i != buttons.length; ++i) {
+//            final Button button = buttons[i];
+//            final float labelWidth = button.largeLabel.getWidth();
+//            final float iconLabelWidth = BUTTON_ICON_SIZE + labelWidth;
+//            if (iconLabelWidth > buttonWidth) {
+//                overflow = true;
+//                break;
+//            }
+//            button.x = dx + FloatMath.floor(0.5f * (buttonWidth - iconLabelWidth));
+//            button.centerX = dx + FloatMath.floor(0.5f * buttonWidth);
+//            dx += buttonWidth;
+//        }
+//
+//        /*
+//        // In the case of overflow layout without icons.
+//        if (overflow) {
+//            dx = 0f;
+//            for (int i = 0; i != buttons.length; ++i) {
+//                final Button button = buttons[i];
+//                final float labelWidth = button.largeLabel.getWidth();
+//                button.x = dx + FloatMath.floor(0.5f * (buttonWidth - labelWidth));
+//                dx += buttonWidth;
+//            }
+//        }
+//        mDrawButtonIcons = !overflow;*/
+//        mDrawButtonIcons = true;
+//        
+//        // Layout the popup menu.
+//        mPopupMenu.setSize(230, 200);
+//    }
+//
+//    @Override
+//    public void renderBlended(RenderView view, GL11 gl) {
+//        // TODO: only recompute layout when things have changed.
+//        layout();
+//
+//        if (view.bind(mLowerBackground)) {
+//            final float imageHeight = mLowerBackground.getHeight();
+//            view.draw2D(0f, mY, 0, mWidth, imageHeight);
+//        }
+//
+//        // Draw the selection highlight if needed.
+//        if (mTouchOver) {
+//            final float SELECTION_FILL_INSET = 10f;
+//            final float SELECTION_CAP_WIDTH = 21f;
+//            final int touchButton = mTouchButton;
+//            final float x = mDividerSpacing * touchButton + SELECTION_FILL_INSET;
+//            final float y = mY;
+//            final float insetWidth = mDividerSpacing + 1f - SELECTION_FILL_INSET -
+//                                     SELECTION_FILL_INSET;
+//            final float height = mSelectionFill.getHeight();
+//            if (view.bind(mSelectionLeft)) {
+//                view.draw2D(x - SELECTION_CAP_WIDTH, y, 0f, SELECTION_CAP_WIDTH, height);
+//            }
+//            if (view.bind(mSelectionFill)) {
+//                view.draw2D(x, y, 0f, insetWidth, height);
+//            }
+//            if (view.bind(mSelectionRight)) {
+//                view.draw2D(x + insetWidth, y, 0f, SELECTION_CAP_WIDTH, height);
+//            }
+//        }
+//
+//        // Draw the button icon + labels.
+//        final float labelY = mY + 20f;
+//        final Button[] buttons = mButtons;
+//        for (int i = 0; i < buttons.length; ++i) {
+//            final Button button = buttons[i];
+//            float x = button.x;
+//            if (mDrawButtonIcons && view.bind(button.icon)) {
+//                view.draw2D(x, labelY, 0f, BUTTON_ICON_SIZE, BUTTON_ICON_SIZE);
+//                x += BUTTON_ICON_SIZE - 2f;
+//            }
+//            if (view.bind(button.largeLabel)) {
+//                view.draw2D(x, labelY, 0f, button.largeLabel.getWidth(), button.largeLabel
+//                                .getHeight());
+//            }
+//        }
+//
+//        // Draw the inter-button dividers.
+//        final float dividerY = mY + 13f;
+//        float x = mDividerSpacing;
+//        if (view.bind(mDividerImage)) {
+//            for (int i = 1; i < buttons.length; ++i) {
+//                view.draw2D(x, dividerY, 0f, 1f, 60f);
+//                x += mDividerSpacing;
+//            }
+//        }
+//    }
+//
+//    private int hitTestButtons(float x) {
+//        final Button[] buttons = mButtons;
+//        for (int i = buttons.length - 1; i >= 0; --i) {
+//            if (buttons[i].x < x) {
+//                return i;
+//            }
+//        }
+//        return -1;
+//    }
+//
+//    @Override
+//    public boolean onTouchEvent(MotionEvent event) {
+//        final float x = event.getX();
+//        switch (event.getAction()) {
+//            case MotionEvent.ACTION_DOWN:
+//                mTouchButton = hitTestButtons(x);
+//                mTouchOver = mTouchButton != -1;
+//                break;
+//            case MotionEvent.ACTION_MOVE:
+//                mTouchOver = hitTestButtons(x) == mTouchButton;
+//                break;
+//            case MotionEvent.ACTION_UP:
+//            case MotionEvent.ACTION_CANCEL:
+//                performAction(mTouchButton, event.getX(), event.getY());
+//                mTouchButton = -1;
+//                mTouchOver = false;
+//                break;
+//        }
+//        return true;
+//    }
+//
+//    private void performAction(final int button, float touchX, float touchY) {
+//        // Gather the selection.
+//        ArrayList<MediaBucket> selection = mHud.getGridLayer().getSelectedBuckets();
+//
+//        // Get the first item from the selection. TODO: support multiple items in Eclair.
+//        MediaItem item = null;
+//        if (!selection.isEmpty()) {
+//            MediaBucket bucket = selection.get(0);
+//            if (bucket.mediaItems != null && !bucket.mediaItems.isEmpty()) {
+//                item = bucket.mediaItems.get(0);
+//            }
+//        }
+//
+//        // Perform the action on the selection.
+//        if (item != null) {
+//            Gallery gallery = (Gallery)mContext; // TODO: provide better context.
+//            final GridLayer gridLayer = mHud.getGridLayer();
+//            final MediaItem sharedItem = item;
+//            if (sharedItem.mimetype == null) {
+//                Toast.makeText(mContext, "Error: mime type is null", Toast.LENGTH_SHORT).show();
+//            }
+//            if (sharedItem.contentUri == null) {
+//                Toast.makeText(mContext, "Error: content URI is null", Toast.LENGTH_SHORT).show();
+//            }
+//            if (button == BUTTON_SELECT_ALL) {
+//                gridLayer.selectAll();
+//            } else if (button == BUTTON_DESELECT_ALL) {
+//                mHud.cancelSelection();
+//            } else {
+//                // Get the target button and popup focus location.
+//                Button targetButton = mButtons[button];
+//                int popupX = (int)targetButton.centerX;
+//                int popupY = (int)mY;
+//                
+//                // Configure the popup depending on the button pressed.
+//                PopupMenu menu = mPopupMenu;
+//                menu.clear();
+//                switch (button) {
+//                    case BUTTON_SHARE:
+//                        /*
+//                        PackageManager packageManager = mContext.getPackageManager();
+//                        List<ResolveInfo> activities = packageManager.queryIntentActivities(
+//                                                               intent,
+//                                                               PackageManager.MATCH_DEFAULT_ONLY);
+//                        for (ResolveInfo activity: activities) {
+//                            activity.icon
+//                            PopupMenu.Option option = new PopupMenu.Option(activit, name, action)
+//                        }
+//                        menu.add(new PopupMenu.Option());*/
+//                        mHud.cancelSelection();
+//                        break;
+//                    case BUTTON_DELETE:
+//                        
+//                    case BUTTON_MORE:
+//                }
+//                
+//                if (button != BUTTON_SHARE) {
+//                    mPopupMenu.showAtPoint(popupX, popupY, (int)mHud.getWidth(), (int)mHud.getHeight());
+//                }
+//                
+//                gallery.getHandler().post(new Runnable() {
+//                    public void run() {
+//                        switch (button) {
+//                            case BUTTON_SHARE:
+//                                Intent intent = new Intent();
+//                                intent.setAction(Intent.ACTION_SEND);
+//                                intent.setType(sharedItem.mimetype);
+//                                intent.putExtra(Intent.EXTRA_STREAM, Uri
+//                                                .parse(sharedItem.contentUri));
+//                                try {
+//                                    mContext.startActivity(Intent.createChooser(intent,
+//                                                                                "Share via"));
+//                                } catch (ActivityNotFoundException e) {
+//                                    Toast.makeText(mContext, "Unable to share",
+//                                                   Toast.LENGTH_SHORT).show();
+//                                }
+//
+//                                break;
+//                            case BUTTON_DELETE:
+//                                gridLayer.deleteSelection();
+//                            case BUTTON_MORE:
+//                                // Show options for Set As, Details, Slideshow.
+//                            default:
+//                                break;
+//                        }
+//                    }
+//                });
+//            }
+//        }
+//
+//        // Exit selection mode.
+//        //mHud.cancelSelection();
+//    }
+//
+//    @Override
+//    public void generate(RenderView view, RenderView.Lists lists) {
+//        lists.blendedList.add(this);
+//        lists.hitTestList.add(this);
+//        mPopupMenu.generate(view, lists);
+//    }
+//    
+//    @Override
+//    protected void onHiddenChanged() {
+//        if (mHidden) {
+//            mPopupMenu.close();
+//        }
+//    }
+//
+//    private static final class Button {
+//        public Texture icon;
+//        public SimpleStringTexture smallLabel;
+//        public SimpleStringTexture largeLabel;
+//        public float x;
+//        public float centerX;
+//    }
+// }
diff --git a/src/com/cooliris/media/Shared.java b/src/com/cooliris/media/Shared.java
new file mode 100644
index 0000000..83527ef
--- /dev/null
+++ b/src/com/cooliris/media/Shared.java
@@ -0,0 +1,116 @@
+package com.cooliris.media;
+
+import android.graphics.Color;
+import android.media.ExifInterface;
+
+public final class Shared {
+    // Constants.
+    public static final int INVALID = -1;
+    public static final int INFINITY = Integer.MAX_VALUE;
+
+    public static int argb(float a, float r, float g, float b) {
+        return Color.argb((int) (a * 255f), (int) (r * 255f), (int) (g * 255f), (int) (b * 255f));
+    }
+
+    public static boolean isPowerOf2(int n) {
+        return (n & -n) == n;
+    }
+    
+    /**
+     * @param i : running variable
+     * @return 0, +1, -1, +2, -2, +3, -3 ..
+     */
+    public static int midPointIterator(int i) {
+        if (i != 0) {
+            int tick = ((i-1)/2) + 1;
+            int pass = ((i-1)%2 == 0) ? 1 : -1;
+            return tick * pass;
+        }
+        return 0;
+    }
+
+    public static int nextPowerOf2(int n) {
+        n -= 1;
+        n |= n >>> 16;
+        n |= n >>> 8;
+        n |= n >>> 4;
+        n |= n >>> 2;
+        n |= n >>> 1;
+        return n + 1;
+    }
+
+    public static int prevPowerOf2(int n) {
+        if (isPowerOf2(n)) {
+            return nextPowerOf2(n);
+        } else {
+            return nextPowerOf2(n) - 1;
+        }
+    }
+
+    public static int clamp(int value, int min, int max) {
+        if (value < min) {
+            value = min;
+        } else if (value > max) {
+            value = max;
+        }
+        return value;
+    }
+    
+	public static long clamp(long value, long min, long max) {
+		if (value < min) {
+            value = min;
+        } else if (value > max) {
+            value = max;
+        }
+        return value;
+	}
+
+    public static float scaleToFit(float srcWidth, float srcHeight, float outerWidth, float outerHeight, boolean clipToFit) {
+        float scaleX = outerWidth / srcWidth;
+        float scaleY = outerHeight / srcHeight;
+        return (clipToFit ? scaleX > scaleY : scaleX < scaleY) ? scaleX : scaleY;
+    }
+
+    // Returns an angle between 0 and 360 degrees independent of the input angle.
+    public static float normalizePositive(float angleToRotate) {
+        if (angleToRotate == 0.0f) {
+            return 0.0f;
+        }
+        float nf = (angleToRotate / 360.0f);
+        int n = 0;
+        if (angleToRotate < 0) {
+            n = (int) (nf - 1.0f);
+        } else if (angleToRotate > 360) {
+            n = (int) (nf);
+        }
+        angleToRotate -= (n * 360.0f);
+        if (angleToRotate == 360.0f) {
+            angleToRotate = 0;
+        }
+        return angleToRotate;
+    }
+
+    public static int degreesToExifOrientation(float normalizedAngle) {
+        if (normalizedAngle == 0.0f) {
+            return ExifInterface.ORIENTATION_NORMAL;
+        } else if (normalizedAngle == 90.0f) {
+            return ExifInterface.ORIENTATION_ROTATE_90;
+        } else if (normalizedAngle == 180.0f) {
+            return ExifInterface.ORIENTATION_ROTATE_180;
+        } else if (normalizedAngle == 270.0f) {
+            return ExifInterface.ORIENTATION_ROTATE_270;
+        }
+        return ExifInterface.ORIENTATION_NORMAL;
+    }
+    
+    public static float exifOrientationToDegrees(int exifOrientation) {
+    	if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) {
+    	    return 90;
+    	} else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_180) {
+    	    return 180;
+    	} else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
+    	    return 270;
+    	}
+    	return 0;
+    }
+}
diff --git a/src/com/cooliris/media/SimpleStringTexture.java b/src/com/cooliris/media/SimpleStringTexture.java
new file mode 100644
index 0000000..98634e8
--- /dev/null
+++ b/src/com/cooliris/media/SimpleStringTexture.java
@@ -0,0 +1,69 @@
+package com.cooliris.media;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+
+public final class SimpleStringTexture extends Texture {
+
+    private final String mString;
+    private final StringTexture.Config mConfig;
+    private float mBaselineHeight = 0.0f;
+
+    SimpleStringTexture(String string, StringTexture.Config config) {
+        mString = string;
+        mConfig = config;
+    }
+
+    public float getBaselineHeight() {
+        return mBaselineHeight;
+    }
+    
+    @Override
+    public boolean isCached() {
+        return true;
+    }
+
+    @Override
+    protected Bitmap load(RenderView view) {
+        // Configure paint.
+        StringTexture.Config config = mConfig;
+        Paint paint = new Paint();
+        paint.setAntiAlias(true);
+        paint.setColor(Shared.argb(config.a, config.r, config.g, config.b));
+        paint.setShadowLayer(config.shadowRadius, 0f, 0f, Color.BLACK);
+        paint.setTypeface(config.bold ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
+        paint.setTextSize(config.fontSize);
+        paint.setUnderlineText(config.underline);
+        paint.setStrikeThruText(config.strikeThrough);
+        if (config.italic) {
+            paint.setTextSkewX(-0.25f);
+        }
+
+        // Measure string.
+        String string = mString;
+        Rect bounds = new Rect();
+        paint.getTextBounds(string, 0, string.length(), bounds);
+
+        // Get font metrics.
+        Paint.FontMetricsInt metrics = paint.getFontMetricsInt();
+        int height = metrics.bottom - metrics.top;
+
+        // Draw string into bitmap with a 1px margin for anti-aliasing.
+        // Ensure baseline alignment with other strings of the same size.
+        int padding = 1 + config.shadowRadius;
+        Bitmap bitmap = Bitmap
+                .createBitmap(bounds.width() + padding + padding, height + padding + padding, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmap);
+        canvas.translate(padding, padding - metrics.ascent);
+        canvas.drawText(string, 0, 0, paint);
+
+        mBaselineHeight = padding + metrics.bottom;
+
+        return bitmap;
+    }
+
+}
diff --git a/src/com/cooliris/media/SingleDataSource.java b/src/com/cooliris/media/SingleDataSource.java
new file mode 100644
index 0000000..6d8e0d4
--- /dev/null
+++ b/src/com/cooliris/media/SingleDataSource.java
@@ -0,0 +1,316 @@
+package com.cooliris.media;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+import android.util.Log;
+
+import com.cooliris.cache.CacheService;
+
+// TODO: Merge SingleDataSource and LocalDataSource into one type if possible
+
+public class SingleDataSource implements DataSource {
+    private static final String TAG = "SingleDataSource";
+    public final String mUri;
+    public final String mBucketId;
+    public boolean mDone;
+    public final boolean mSlideshow;
+    public final boolean mSingleUri;
+    public final boolean mAllItems;
+    public final DiskCache mDiskCache;
+    private Context mContext;
+
+    public SingleDataSource(final Context context, final String uri, final boolean slideshow) {
+        this.mUri = uri;
+        mContext = context;
+        String bucketId = Uri.parse(uri).getQueryParameter("bucketId");
+        if (bucketId != null && bucketId.length() > 0) {
+            mBucketId = bucketId;
+        } else {
+            mBucketId = null;
+        }
+        if (mBucketId == null) {
+            if (uri.equals(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())) {
+                mAllItems = true;
+            } else {
+                mAllItems = false;
+            }
+        } else {
+            mAllItems = false;
+        }
+        this.mSlideshow = slideshow;
+        mSingleUri = isSingleImageMode(uri) && mBucketId == null;
+        mDone = false;
+        mDiskCache = mUri.startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) || mUri.startsWith("file://") ? LocalDataSource.sThumbnailCache
+                : null;
+    }
+
+    public void shutdown() {
+
+    }
+
+    public static boolean isSingleImageMode(String uriString) {
+        return !uriString.equals(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
+                && !uriString.equals(MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString());
+    }
+
+    public DiskCache getThumbnailCache() {
+        return mDiskCache;
+    }
+
+    public void loadItemsForSet(MediaFeed feed, MediaSet parentSet, int rangeStart, int rangeEnd) {
+        if (parentSet.mNumItemsLoaded > 0 && mDone) {
+            return;
+        }
+        if (mSingleUri && !mDone) {
+            MediaItem item = new MediaItem();
+            item.mId = 0;
+            item.mFilePath = "";
+            item.setMediaType((isImage(mUri)) ? MediaItem.MEDIA_TYPE_IMAGE : MediaItem.MEDIA_TYPE_VIDEO);
+            if (mUri.startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())) {
+                MediaItem newItem = LocalDataSource.createMediaItemFromUri(mContext, Uri.parse(mUri));
+                if (newItem != null) {
+                    item = newItem;
+                    String fileUri = new File(item.mFilePath).toURI().toString();
+                    parentSet.mName = Utils.getBucketNameFromUri(Uri.parse(fileUri));
+                    parentSet.mId = parseBucketIdFromFileUri(fileUri);
+                    parentSet.generateTitle(true);
+                }
+            } else if (mUri.startsWith("file://")) {
+                MediaItem newItem = LocalDataSource.createMediaItemFromFileUri(mContext, mUri);
+                if (newItem != null)
+                    item = newItem;
+            } else {
+                item.mContentUri = mUri;
+                item.mThumbnailUri = mUri;
+                item.mScreennailUri = mUri;
+                feed.setSingleImageMode(true);
+            }
+            if (item != null) {
+                feed.addItemToMediaSet(item, parentSet);
+                // Parse EXIF orientation if a local file.
+                if (mUri.startsWith("file://")) {
+                    try {
+                        ExifInterface exif = new ExifInterface(Uri.parse(mUri).getPath());
+                        item.mRotation = Shared.exifOrientationToDegrees(exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
+                                ExifInterface.ORIENTATION_NORMAL));
+                    } catch (IOException e) {
+                        Log.i(TAG, "Error reading Exif information, probably not a jpeg.");
+                    }
+                }
+                // Try and get the date taken for this item.
+                long dateTaken = CacheService.fetchDateTaken(item);
+                if (dateTaken != -1L) {
+                    item.mDateTakenInMs = dateTaken;
+                }
+                CacheService.loadMediaItemsIntoMediaFeed(feed, parentSet, rangeStart, rangeEnd, true, false);
+                ArrayList<MediaItem> items = parentSet.getItems();
+                int numItems = items.size();
+                for (int i = 1; i < numItems; ++i) {
+                    MediaItem thisItem = items.get(i);
+                    String filePath = Uri.fromFile(new File(thisItem.mFilePath)).toString();
+                    if (item.mId == thisItem.mId || ((item.mContentUri != null && thisItem.mContentUri != null) && (item.mContentUri.equals(thisItem.mContentUri)
+                            || item.mContentUri.equals(filePath)))) {
+                        items.remove(thisItem);
+                        --parentSet.mNumItemsLoaded;
+                        break;
+                    }
+                }
+            }
+            parentSet.updateNumExpectedItems();
+            parentSet.generateTitle(true);
+        } else if (mUri.equals(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())) {
+            final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
+            final ContentResolver cr = mContext.getContentResolver();
+            String where = null;
+            Cursor cursor = cr.query(uriImages, CacheService.PROJECTION_IMAGES, where, null, null);
+            if (cursor != null && cursor.moveToFirst()) {
+                parentSet.setNumExpectedItems(cursor.getCount());
+                do {
+                    if (Thread.interrupted()) {
+                        return;
+                    }
+                    final MediaItem item = new MediaItem();
+                    CacheService.populateMediaItemFromCursor(item, cr, cursor, CacheService.BASE_CONTENT_STRING_IMAGES);
+                    feed.addItemToMediaSet(item, parentSet);
+                } while (cursor.moveToNext());
+                if (cursor != null) {
+                    cursor.close();
+                    cursor = null;
+                }
+                parentSet.updateNumExpectedItems();
+                parentSet.generateTitle(true);
+            }
+        } else {
+            CacheService.loadMediaItemsIntoMediaFeed(feed, parentSet, rangeStart, rangeEnd, true, true);
+        }
+        mDone = true;
+    }
+
+    public static long parseBucketIdFromFileUri(String uriString) {
+        // This is a local folder.
+        final Uri uri = Uri.parse(uriString);
+        final List<String> paths = uri.getPathSegments();
+        final int numPaths = paths.size() - 1;
+        StringBuffer pathBuilder = new StringBuffer(Environment.getExternalStorageDirectory().toString());
+        if (numPaths > 1)
+            pathBuilder.append("/");
+        for (int i = 0; i < numPaths; ++i) {
+            String path = paths.get(i);
+            if (!"file".equals(path) && !"sdcard".equals(path)) {
+                pathBuilder.append(path);
+                if (i != numPaths - 1) {
+                    pathBuilder.append("/");
+                }
+            }
+        }
+        return LocalDataSource.getBucketId(pathBuilder.toString());
+    }
+
+    private static boolean isImage(String uriString) {
+        return !uriString.startsWith(MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString());
+    }
+
+    private static long parseIdFromContentUri(String uri) {
+        try {
+            long id = ContentUris.parseId(Uri.parse(uri));
+            return id;
+        } catch (Exception e) {
+            return 0;
+        }
+    }
+
+    public void loadMediaSets(MediaFeed feed) {
+        MediaSet set = null; // Dummy set.
+        boolean loadOtherSets = true;
+        if (mSingleUri) {
+            String name = Utils.getBucketNameFromUri(Uri.parse(mUri));
+            long id = getBucketId(mUri);
+            set = feed.addMediaSet(id, this);
+            set.mName = name;
+            set.setNumExpectedItems(2);
+            set.generateTitle(true);
+            set.mPicasaAlbumId = Shared.INVALID;
+            if (this.getThumbnailCache() != LocalDataSource.sThumbnailCache) {
+                loadOtherSets = false;
+            }
+        } else if (mBucketId == null) {
+            // All the buckets.
+            set = feed.addMediaSet(0, this); // Create dummy set.
+            set.mName = Utils.getBucketNameFromUri(Uri.parse(mUri));
+            set.mId = LocalDataSource.getBucketId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString() + "/" + set.mName);
+            set.setNumExpectedItems(1);
+            set.generateTitle(true);
+            set.mPicasaAlbumId = Shared.INVALID;
+        } else {
+            CacheService.loadMediaSet(feed, this, Long.parseLong(mBucketId));
+            ArrayList<MediaSet> sets = feed.getMediaSets();
+            if (sets.size() > 0)
+                set = sets.get(0);
+        }
+        // We also load the other MediaSets
+        if (!mAllItems && set != null && loadOtherSets) {
+            if (!CacheService.isPresentInCache(set.mId)) {
+                CacheService.markDirty(mContext);
+            }
+            CacheService.loadMediaSets(feed, this, true, false);
+        }
+    }
+
+    private long getBucketId(String uriString) {
+        if (uriString.startsWith("content://.")) {
+            return parseIdFromContentUri(uriString);
+        } else {
+            return parseBucketIdFromFileUri(uriString);
+        }
+    }
+
+    public boolean performOperation(int operation, ArrayList<MediaBucket> mediaBuckets, Object data) {
+        int numBuckets = mediaBuckets.size();
+        ContentResolver cr = mContext.getContentResolver();
+        switch (operation) {
+        case MediaFeed.OPERATION_DELETE:
+            // TODO: Refactor this against LocalDataSource.performOperation.
+            for (int i = 0; i < numBuckets; ++i) {
+                MediaBucket bucket = mediaBuckets.get(i);
+                MediaSet set = bucket.mediaSet;
+                ArrayList<MediaItem> items = bucket.mediaItems;
+                if (set != null && items == null) {
+                    // TODO bulk delete
+                    // remove the entire bucket
+                    final Uri uriImages = Images.Media.EXTERNAL_CONTENT_URI;
+                    final Uri uriVideos = Video.Media.EXTERNAL_CONTENT_URI;
+                    final String whereImages = Images.ImageColumns.BUCKET_ID + "=" + Long.toString(set.mId);
+                    final String whereVideos = Video.VideoColumns.BUCKET_ID + "=" + Long.toString(set.mId);
+                    cr.delete(uriImages, whereImages, null);
+                    cr.delete(uriVideos, whereVideos, null);
+                    CacheService.markDirty(mContext);
+                }
+                if (set != null && items != null) {
+                    // We need to remove these items from the set.
+                    int numItems = items.size();
+                    for (int j = 0; j < numItems; ++j) {
+                        MediaItem item = items.get(j);
+                        cr.delete(Uri.parse(item.mContentUri), null, null);
+                    }
+                    set.updateNumExpectedItems();
+                    set.generateTitle(true);
+                    CacheService.markDirty(mContext, set.mId);
+                }
+            }
+            break;
+        case MediaFeed.OPERATION_ROTATE:
+            for (int i = 0; i < numBuckets; ++i) {
+                MediaBucket bucket = mediaBuckets.get(i);
+                ArrayList<MediaItem> items = bucket.mediaItems;
+                if (items == null) {
+                    continue;
+                }
+                float angleToRotate = ((Float) data).floatValue();
+                if (angleToRotate == 0) {
+                    return true;
+                }
+                int numItems = items.size();
+                for (int j = 0; j < numItems; ++j) {
+                    rotateItem(items.get(j), angleToRotate);
+                }
+            }
+            break;
+        }
+        return true;
+    }
+
+    private void rotateItem(final MediaItem item, float angleToRotate) {
+        try {
+            int currentOrientation = (int) item.mRotation;
+            angleToRotate += currentOrientation;
+            float rotation = Shared.normalizePositive(angleToRotate);
+
+            // Update the file EXIF information.
+            Uri uri = Uri.parse(item.mContentUri);
+            String uriScheme = uri.getScheme();
+            if (uriScheme.equals("file")) {
+                ExifInterface exif = new ExifInterface(uri.getPath());
+                exif.setAttribute(ExifInterface.TAG_ORIENTATION, Integer.toString(Shared.degreesToExifOrientation(rotation)));
+                exif.saveAttributes();
+            }
+
+            // Update the object representation of the item.
+            item.mRotation = rotation;
+        } catch (Exception e) {
+        }
+    }
+
+}
diff --git a/src/com/cooliris/media/SortCursor.java b/src/com/cooliris/media/SortCursor.java
new file mode 100644
index 0000000..e77a139
--- /dev/null
+++ b/src/com/cooliris/media/SortCursor.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2007 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.cooliris.media;
+
+/*
+ *  Added changes to support numeric comparisons, and also expose the current
+ *  cursor being used
+ */
+
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.util.Log;
+
+/**
+ * A variant of MergeCursor that sorts the cursors being merged. If decent performance is ever obtained, it can be put back under
+ * android.database.
+ */
+public class SortCursor extends AbstractCursor {
+    private static final String TAG = "SortCursor";
+    private Cursor mCursor; // updated in onMove
+    private Cursor[] mCursors;
+    private int[] mSortColumns;
+    private final int ROWCACHESIZE = 64;
+    private int mRowNumCache[] = new int[ROWCACHESIZE];
+    private int mCursorCache[] = new int[ROWCACHESIZE];
+    private int mCurRowNumCache[][];
+    private int mLastCacheHit = -1;
+    private int mType;
+    private boolean mAscending;
+    public static final int TYPE_STRING = 0;
+    public static final int TYPE_NUMERIC = 1;
+
+    private DataSetObserver mObserver = new DataSetObserver() {
+        @Override
+        public void onChanged() {
+            // Reset our position so the optimizations in move-related code
+            // don't screw us over
+            mPos = -1;
+        }
+
+        @Override
+        public void onInvalidated() {
+            mPos = -1;
+        }
+    };
+    private int mCursorIndex;
+
+    public SortCursor(Cursor[] cursors, String sortcolumn, int type, boolean ascending) {
+        mAscending = ascending;
+        mCursors = cursors;
+        mType = type;
+        int length = mCursors.length;
+        mSortColumns = new int[length];
+        for (int i = 0; i < length; i++) {
+            if (mCursors[i] == null) {
+                continue;
+            }
+            // Register ourself as a data set observer
+            mCursors[i].registerDataSetObserver(mObserver);
+            mCursors[i].moveToFirst();
+            // We don't catch the exception.
+            mSortColumns[i] = mCursors[i].getColumnIndexOrThrow(sortcolumn);
+        }
+        mCursor = null;
+        if (type == TYPE_STRING) {
+            String smallest = "";
+            for (int j = 0; j < length; j++) {
+                if (mCursors[j] == null || mCursors[j].isAfterLast())
+                    continue;
+                String current = mCursors[j].getString(mSortColumns[j]);
+                if (mCursor == null || current == null || current.compareToIgnoreCase(smallest) < 0) {
+                    smallest = current;
+                    mCursor = mCursors[j];
+                    mCursorIndex = j;
+                }
+            }
+        } else {
+            long smallest = (ascending) ? Long.MAX_VALUE : Long.MIN_VALUE;
+            for (int j = 0; j < length; j++) {
+                if (mCursors[j] == null || mCursors[j].isAfterLast()) {
+                    continue;
+                }
+                long current = mCursors[j].getLong(mSortColumns[j]);
+                boolean comparison = (ascending) ? (current < smallest) : (current > smallest);
+                if (mCursor == null || comparison) {
+                    smallest = current;
+                    mCursor = mCursors[j];
+                    mCursorIndex = j;
+                }
+            }
+        }
+
+        for (int i = mRowNumCache.length - 1; i >= 0; i--) {
+            mRowNumCache[i] = -2;
+        }
+        mCurRowNumCache = new int[ROWCACHESIZE][length];
+    }
+
+    @Override
+    public int getCount() {
+        int count = 0;
+        int length = mCursors.length;
+        for (int i = 0; i < length; i++) {
+            if (mCursors[i] != null) {
+                count += mCursors[i].getCount();
+            }
+        }
+        return count;
+    }
+
+    @Override
+    public boolean onMove(int oldPosition, int newPosition) {
+        if (oldPosition == newPosition)
+            return true;
+
+        /*
+         * Find the right cursor Because the client of this cursor (the listadapter/view) tends to jump around in the cursor
+         * somewhat, a simple cache strategy is used to avoid having to search all cursors from the start. TODO: investigate
+         * strategies for optimizing random access and reverse-order access.
+         */
+
+        int cache_entry = newPosition % ROWCACHESIZE;
+
+        if (mRowNumCache[cache_entry] == newPosition) {
+            int which = mCursorCache[cache_entry];
+            mCursor = mCursors[which];
+            mCursorIndex = which;
+            if (mCursor == null) {
+                Log.w(TAG, "onMove: cache results in a null cursor.");
+                return false;
+            }
+            mCursor.moveToPosition(mCurRowNumCache[cache_entry][which]);
+            mLastCacheHit = cache_entry;
+            return true;
+        }
+
+        mCursor = null;
+        int length = mCursors.length;
+
+        if (mLastCacheHit >= 0) {
+            for (int i = 0; i < length; i++) {
+                if (mCursors[i] == null)
+                    continue;
+                mCursors[i].moveToPosition(mCurRowNumCache[mLastCacheHit][i]);
+            }
+        }
+
+        if (newPosition < oldPosition || oldPosition == -1) {
+            for (int i = 0; i < length; i++) {
+                if (mCursors[i] == null)
+                    continue;
+                mCursors[i].moveToFirst();
+            }
+            oldPosition = 0;
+        }
+        if (oldPosition < 0) {
+            oldPosition = 0;
+        }
+
+        // search forward to the new position
+        int smallestIdx = -1;
+        if (mType == TYPE_STRING) {
+            for (int i = oldPosition; i <= newPosition; i++) {
+                String smallest = "";
+                smallestIdx = -1;
+                for (int j = 0; j < length; j++) {
+                    if (mCursors[j] == null || mCursors[j].isAfterLast()) {
+                        continue;
+                    }
+                    String current = mCursors[j].getString(mSortColumns[j]);
+                    if (smallestIdx < 0 || current == null || current.compareToIgnoreCase(smallest) < 0) {
+                        smallest = current;
+                        smallestIdx = j;
+                    }
+                }
+                if (i == newPosition) {
+                    break;
+                }
+                if (mCursors[smallestIdx] != null) {
+                    mCursors[smallestIdx].moveToNext();
+                }
+            }
+        } else {
+            for (int i = oldPosition; i <= newPosition; i++) {
+                long smallest = (mAscending) ? Long.MAX_VALUE : Long.MIN_VALUE;
+                smallestIdx = -1;
+                for (int j = 0; j < length; j++) {
+                    if (mCursors[j] == null || mCursors[j].isAfterLast()) {
+                        continue;
+                    }
+                    long current = mCursors[j].getLong(mSortColumns[j]);
+                    boolean comparison = (mAscending) ? current < smallest : current > smallest;
+                    if (smallestIdx < 0 || comparison) {
+                        smallest = current;
+                        smallestIdx = j;
+                    }
+                }
+                if (i == newPosition) {
+                    break;
+                }
+                if (mCursors[smallestIdx] != null) {
+                    mCursors[smallestIdx].moveToNext();
+                }
+            }
+        }
+        mCursor = mCursors[smallestIdx];
+        mCursorIndex = smallestIdx;
+        mRowNumCache[cache_entry] = newPosition;
+        mCursorCache[cache_entry] = smallestIdx;
+        for (int i = 0; i < length; i++) {
+            if (mCursors[i] != null) {
+                mCurRowNumCache[cache_entry][i] = mCursors[i].getPosition();
+            }
+        }
+        mLastCacheHit = -1;
+        return true;
+    }
+
+    @Override
+    public String getString(int column) {
+        return mCursor.getString(column);
+    }
+
+    @Override
+    public short getShort(int column) {
+        return mCursor.getShort(column);
+    }
+
+    @Override
+    public int getInt(int column) {
+        return mCursor.getInt(column);
+    }
+
+    @Override
+    public long getLong(int column) {
+        return mCursor.getLong(column);
+    }
+
+    @Override
+    public float getFloat(int column) {
+        return mCursor.getFloat(column);
+    }
+
+    @Override
+    public double getDouble(int column) {
+        return mCursor.getDouble(column);
+    }
+
+    @Override
+    public boolean isNull(int column) {
+        return mCursor.isNull(column);
+    }
+
+    @Override
+    public byte[] getBlob(int column) {
+        return mCursor.getBlob(column);
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        if (mCursor != null) {
+            return mCursor.getColumnNames();
+        } else {
+            // All of the cursors may be empty, but they can still return
+            // this information.
+            int length = mCursors.length;
+            for (int i = 0; i < length; i++) {
+                if (mCursors[i] != null) {
+                    return mCursors[i].getColumnNames();
+                }
+            }
+            throw new IllegalStateException("No cursor that can return names");
+        }
+    }
+
+    @Override
+    public void deactivate() {
+        int length = mCursors.length;
+        for (int i = 0; i < length; i++) {
+            if (mCursors[i] == null)
+                continue;
+            mCursors[i].deactivate();
+        }
+    }
+
+    @Override
+    public void close() {
+        int length = mCursors.length;
+        for (int i = 0; i < length; i++) {
+            if (mCursors[i] == null)
+                continue;
+            mCursors[i].close();
+        }
+    }
+
+    @Override
+    public void registerDataSetObserver(DataSetObserver observer) {
+        int length = mCursors.length;
+        for (int i = 0; i < length; i++) {
+            if (mCursors[i] != null) {
+                mCursors[i].registerDataSetObserver(observer);
+            }
+        }
+    }
+
+    @Override
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        int length = mCursors.length;
+        for (int i = 0; i < length; i++) {
+            if (mCursors[i] != null) {
+                mCursors[i].unregisterDataSetObserver(observer);
+            }
+        }
+    }
+
+    @Override
+    public boolean requery() {
+        int length = mCursors.length;
+        for (int i = 0; i < length; i++) {
+            if (mCursors[i] == null)
+                continue;
+
+            if (mCursors[i].requery() == false) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    public int getCurrentCursorIndex() {
+        return mCursorIndex;
+    }
+}
diff --git a/src/com/cooliris/media/StringTexture.java b/src/com/cooliris/media/StringTexture.java
new file mode 100644
index 0000000..e9bc662
--- /dev/null
+++ b/src/com/cooliris/media/StringTexture.java
@@ -0,0 +1,252 @@
+package com.cooliris.media;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.Typeface;
+import android.graphics.Paint.Align;
+import android.util.FloatMath;
+
+public final class StringTexture extends Texture {
+    private String mString;
+    private Config mConfig;
+    private Paint mPaint;
+    private int mBaselineHeight;
+
+    private static final Paint sPaint = new Paint();
+
+    public static int computeTextWidthForConfig(String string, Config config) {
+    	return computeTextWidthForConfig(config.fontSize, config.bold ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT, string);
+    }
+    
+    public static int computeTextWidthForConfig(float textSize, Typeface typeface, String string) {
+        Paint paint = sPaint;
+        synchronized (paint) {
+            paint.setAntiAlias(true);
+            paint.setTypeface(typeface);
+            paint.setTextSize(textSize);
+            // 10 pixel buffer to compensate for the shade at the end.
+            return (int)(10.0f * Gallery.PIXEL_DENSITY) + (int)FloatMath.ceil(paint.measureText(string));
+        }
+    }
+
+    public static int lengthToFit(float textSize, float maxWidth, Typeface typeface, String string) {
+        if (maxWidth <= 0)
+            return 0;
+        Paint paint = sPaint;
+        paint.setAntiAlias(true);
+        paint.setTypeface(typeface);
+        paint.setTextSize(textSize);
+        int length = string.length();
+        float retVal = paint.measureText(string);
+        if (retVal <= maxWidth)
+            return length;
+        else {
+            while (retVal > maxWidth) {
+                --length;
+                retVal = paint.measureText(string, 0, length - 1);
+            }
+            return length;
+        }
+    }
+
+    public StringTexture(String string) {
+        mString = string;
+        mConfig = Config.DEFAULT_CONFIG_SCALED;
+    }
+
+    public StringTexture(String string, Config config) {
+        this(string, config, config.width, config.height);
+    }
+    
+    public StringTexture(String string, Config config, int width, int height) {
+        mString = string;
+        mConfig = config;
+        mWidth = width;
+        mHeight = height;
+    }
+
+    public float computeTextWidth() {
+        Paint paint = computePaint();
+        if (paint != null) {
+            if (mString != null)
+                return paint.measureText(mString);
+            else
+                return 0;
+        } else {
+            return 0;
+        }
+    }
+    
+    @Override
+    public boolean isCached() {
+        return true;
+    }
+
+    public float getBaselineHeight() {
+        return mBaselineHeight;
+    }
+
+    protected Paint computePaint() {
+        if (mPaint != null)
+            return mPaint;
+        Paint paint = new Paint();
+        mPaint = paint;
+        paint.setAntiAlias(true);
+        Config config = mConfig;
+        int alpha = (int) (config.a * 255);
+        int red = (int) (config.r * 255);
+        int green = (int) (config.g * 255);
+        int blue = (int) (config.b * 255);
+        int color = Color.argb(alpha, red, green, blue);
+        paint.setColor(color);
+        paint.setShadowLayer(config.shadowRadius, 0, 0, Color.BLACK);
+        paint.setUnderlineText(config.underline);
+        paint.setTypeface(config.bold ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
+        paint.setStrikeThruText(config.strikeThrough);
+        // int originX = 0;
+        if (config.xalignment == Config.ALIGN_LEFT) {
+            paint.setTextAlign(Align.LEFT);
+        } else if (config.xalignment == Config.ALIGN_RIGHT) {
+            paint.setTextAlign(Align.RIGHT);
+            // originX = (int)config.width;
+        } else {
+            paint.setTextAlign(Align.CENTER);
+            // originX = (int)config.height;
+        }
+        if (config.italic)
+            paint.setTextSkewX(-0.25f);
+        String stringToDraw = mString;
+        paint.setTextSize(config.fontSize);
+        if (config.sizeMode == Config.SIZE_TEXT_TO_BOUNDS) {
+            // we have to compute the fontsize appropriately
+            while (true) {
+                // we measure the textwidth
+                float currentTextSize = paint.getTextSize();
+                float measuredTextWidth = 0;
+                measuredTextWidth = paint.measureText(stringToDraw);
+                if (measuredTextWidth < mWidth)
+                    break;
+                paint.setTextSize(currentTextSize - 1.0f);
+                if (currentTextSize <= 6.0f)
+                    break;
+            }
+        }
+        return paint;
+    }
+
+    @Override
+    protected Bitmap load(RenderView view) {
+        if (mString == null)
+            return null;
+        Paint paint = computePaint();
+        String stringToDraw = mString;
+        Config config = mConfig;
+        Bitmap.Config bmConfig = Bitmap.Config.ARGB_4444;
+        Paint.FontMetricsInt metrics = paint.getFontMetricsInt();
+        // 1 pixel for anti-aliasing
+        int padding = 1 + config.shadowRadius;
+        int ascent = metrics.ascent - padding;
+        int descent = metrics.descent + padding;
+        int backWidth = mWidth;
+        int backHeight = mHeight;
+        
+        String string = mString;
+        Rect bounds = new Rect();
+        paint.getTextBounds(string, 0, string.length(), bounds);
+        
+        if (config.sizeMode == Config.SIZE_BOUNDS_TO_TEXT) {
+            // do something else
+            backWidth = bounds.width() + 2 * padding;
+            int height = descent - ascent;
+            backHeight = height + padding;
+        } 
+        if (backWidth <= 0 || backHeight <= 0)
+            return null;
+        Bitmap bitmap = Bitmap.createBitmap(backWidth, backHeight, bmConfig);
+        Canvas canvas = new Canvas(bitmap);
+        // for top
+        int x = (config.xalignment == Config.ALIGN_LEFT) ? padding
+                : (config.xalignment == Config.ALIGN_RIGHT ? backWidth - padding : backWidth / 2);
+        int y = (config.yalignment == Config.ALIGN_TOP) ? -metrics.top + padding
+                : ((config.yalignment == Config.ALIGN_BOTTOM) ? (backHeight - descent)
+                        : ((int) backHeight - (descent + ascent)) / 2);
+        // bitmap.eraseColor(0xff00ff00);
+        canvas.drawText(stringToDraw, x, y, paint);
+        
+        if (bounds.width() > backWidth && config.overflowMode == Config.OVERFLOW_FADE) {
+            // Fade the right edge of the string if the text overflows. TODO: BIDI text should fade on the left.
+            float gradientLeft = backWidth - Config.FADE_WIDTH;
+            LinearGradient gradient = new LinearGradient(gradientLeft, 0, backWidth, 0, 0xffffffff,
+                    0x00ffffff, Shader.TileMode.CLAMP);
+            paint = new Paint();
+            paint.setShader(gradient);
+            paint.setDither(true);
+            paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
+            canvas.drawRect(gradientLeft, 0, backWidth, backHeight, paint);
+        }
+        
+        mBaselineHeight = padding + metrics.bottom;
+        return bitmap;
+    }
+    
+    public static final class Config {
+        public static final int SIZE_EXACT = 0;
+        public static final int SIZE_TEXT_TO_BOUNDS = 1;
+        public static final int SIZE_BOUNDS_TO_TEXT = 2;
+        
+        public static final int OVERFLOW_CLIP = 0;
+        public static final int OVERFLOW_ELLIPSIZE = 1;
+        public static final int OVERFLOW_FADE = 2;
+        
+        public static final int ALIGN_HCENTER = 0;
+        public static final int ALIGN_LEFT = 1;
+        public static final int ALIGN_RIGHT = 2;
+        public static final int ALIGN_TOP = 3;
+        public static final int ALIGN_BOTTOM = 4;
+        public static final int ALIGN_VCENTER = 5;
+        
+        private static final int FADE_WIDTH = 30;
+        
+        public static final Config DEFAULT_CONFIG_SCALED = new Config();
+        public static final Config DEFAULT_CONFIG_TRUNCATED = new Config(SIZE_TEXT_TO_BOUNDS);
+
+        public float fontSize = 20f;
+        public float r = 1f;
+        public float g = 1f;
+        public float b = 1f;
+        public float a = 1f;
+        public int shadowRadius = 4 * (int)Gallery.PIXEL_DENSITY;
+        public boolean underline = false;
+        public boolean bold = false;
+        public boolean italic = false;
+        public boolean strikeThrough = false;
+        public int width = 256;  // TODO: there is no good default for this, require explicit specification.
+        public int height = 32;
+        public int xalignment = ALIGN_LEFT;
+        public int yalignment = ALIGN_VCENTER;
+        public int sizeMode = SIZE_BOUNDS_TO_TEXT;
+        public int overflowMode = OVERFLOW_FADE;
+
+        public Config() {
+        }
+
+        public Config(int sizeMode) {
+            this.sizeMode = sizeMode;
+        }
+
+        public Config(float fontSize, int width, int height) {
+            this.fontSize = fontSize;
+            this.width = width;
+            this.height = height;
+            this.sizeMode = SIZE_EXACT;
+        }
+    };
+
+}
diff --git a/src/com/cooliris/media/Texture.java b/src/com/cooliris/media/Texture.java
new file mode 100644
index 0000000..422b90d
--- /dev/null
+++ b/src/com/cooliris/media/Texture.java
@@ -0,0 +1,73 @@
+package com.cooliris.media;
+
+import android.graphics.Bitmap;
+
+public abstract class Texture {
+
+    public static final int STATE_UNLOADED = 0;
+    public static final int STATE_QUEUED = 1;
+    public static final int STATE_LOADING = 2;
+    public static final int STATE_LOADED = 3;
+    public static final int STATE_ERROR = 4;
+
+    int mState = STATE_UNLOADED;
+    int mId;
+    int mWidth;
+    int mHeight;
+    float mNormalizedWidth;
+    float mNormalizedHeight;
+    Bitmap mBitmap;
+
+    public boolean isCached() {
+        return false;
+    }
+    
+    public final void clear() {
+        mId = 0;
+        mState = STATE_UNLOADED;
+        mWidth = 0;
+        mHeight = 0;
+        mNormalizedWidth = 0;
+        mNormalizedHeight = 0;
+        if (mBitmap != null) {
+            mBitmap.recycle();
+            mBitmap = null;
+        }
+    }
+    
+    public final boolean isLoaded() {
+        return mState == STATE_LOADED;
+    }
+
+    public final int getState() {
+        return mState;
+    }
+
+    public final int getWidth() {
+        return mWidth;
+    }
+
+    public final int getHeight() {
+        return mHeight;
+    }
+
+    public final float getNormalizedWidth() {
+        return mNormalizedWidth;
+    }
+
+    public final float getNormalizedHeight() {
+        return mNormalizedHeight;
+    }
+
+    /** If this returns true, the texture will be enqueued. */
+    protected boolean shouldQueue() {
+        return true;
+    }
+
+    /** Returns a bitmap, or null if an error occurs. */
+    protected abstract Bitmap load(RenderView view);
+
+    public boolean isUncachedVideo() {
+        return false;
+    }
+}
diff --git a/src/com/cooliris/media/TimeBar.java b/src/com/cooliris/media/TimeBar.java
new file mode 100644
index 0000000..ec3015f
--- /dev/null
+++ b/src/com/cooliris/media/TimeBar.java
@@ -0,0 +1,543 @@
+package com.cooliris.media;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+
+import javax.microedition.khronos.opengles.GL11;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.NinePatch;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import com.cooliris.media.RenderView.Lists;
+
+public final class TimeBar extends Layer implements MediaFeed.Listener {
+    public static final int HEIGHT = 48;
+    private static final int MARKER_SPACING_PIXELS = 50;
+    private static final float AUTO_SCROLL_MARGIN = 100f;
+    private static final Paint SRC_PAINT = new Paint();
+    private final Context mContext;
+    private Listener mListener = null;
+    private MediaFeed mFeed = null;
+    private float mTotalWidth = 0f;
+    private float mPosition = 0f;
+    private float mPositionAnim = 0f;
+    private float mScroll = 0f;
+    private float mScrollAnim = 0f;
+    private boolean mInDrag = false;
+    private float mDragX = 0f;
+
+    private ArrayList<Marker> mMarkers = new ArrayList<Marker>();
+    private ArrayList<Marker> mMarkersCopy = new ArrayList<Marker>();
+
+    private static final int KNOB = R.drawable.scroller_new;
+    private static final int KNOB_PRESSED = R.drawable.scroller_pressed_new;
+    private final StringTexture.Config mMonthYearFormat = new StringTexture.Config();
+    private final StringTexture.Config mDayFormat = new StringTexture.Config();
+    private final SparseArray<StringTexture> mYearLabels = new SparseArray<StringTexture>();
+    private final StringTexture mDateUnknown;
+    private final StringTexture[] mMonthLabels = new StringTexture[12];
+    private final StringTexture[] mDayLabels = new StringTexture[32];
+    private final StringTexture[] mOpaqueDayLabels = new StringTexture[32];
+    private final StringTexture mDot = new StringTexture("¥");
+    private final HashMap<MediaItem, Marker> mTracker = new HashMap<MediaItem, Marker>(1024);
+    private int mState;
+    private float mTextAlpha = 0.0f;
+    private float mAnimTextAlpha = 0.0f;
+    private boolean mShowTime;
+    private NinePatch mBackground;
+    private Rect mBackgroundRect;
+    private BitmapTexture mBackgroundTexture;
+
+    public interface Listener {
+        public void onTimeChanged(TimeBar timebar);
+    }
+
+    TimeBar(Context context) {
+        // Save the context.
+        mContext = context;
+
+        // Setup formatting for text labels.
+        mMonthYearFormat.fontSize = 17f * Gallery.PIXEL_DENSITY;
+        mMonthYearFormat.bold = true;
+        mMonthYearFormat.a = 0.85f;
+        mDayFormat.fontSize = 17f * Gallery.PIXEL_DENSITY;
+        mDayFormat.a = 0.61f;
+
+        // Create textures for month names.
+        String[] months = mContext.getResources().getStringArray(R.array.months_abbreviated);
+        for (int i = 0; i < months.length; ++i) {
+            mMonthLabels[i] = new StringTexture(months[i], mMonthYearFormat);
+        }
+
+        for (int i = 0; i <= 31; ++i) {
+            mDayLabels[i] = new StringTexture(Integer.toString(i), mDayFormat);
+            mOpaqueDayLabels[i] = new StringTexture(Integer.toString(i), mMonthYearFormat);
+        }
+        mDateUnknown = new StringTexture(context.getResources().getString(R.string.date_unknown), mMonthYearFormat);
+        Bitmap background = BitmapFactory.decodeResource(context.getResources(), R.drawable.popup);
+        mBackground = new NinePatch(background, background.getNinePatchChunk(), null);
+        mBackgroundRect = new Rect();
+        SRC_PAINT.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public void setFeed(MediaFeed feed, int state, boolean needsLayout) {
+        mFeed = feed;
+        mState = state;
+        layout();
+        if (needsLayout) {
+            mPosition = 0;
+            mScroll = getScrollForPosition(mPosition);
+        }
+    }
+
+    @Override
+    protected void onSizeChanged() {
+        mScroll = getScrollForPosition(mPosition);
+    }
+
+    // public long getTime() {
+    // final float x = mPosition * mTotalWidth;
+    // for (int i = 0, size = mMarkers.size(); i < size; ++i) {
+    // Marker marker = mMarkers.get(i);
+    // if (x <= marker.x) {
+    // return marker.time;
+    // }
+    // }
+    // return 0;
+    // }
+    //
+    // public void setTime(long time) {
+    // synchronized (mMarkers) {
+    // for (int i = 0, size = mMarkers.size(); i < size; ++i) {
+    // Marker marker = mMarkers.get(i);
+    // if (time <= marker.time) {
+    // mPosition = Math.max(0.0f, Math.min(1.0f, marker.x / mTotalWidth));
+    // mScroll = getScrollForPosition(mPosition);
+    // break;
+    // }
+    // }
+    // }
+    // }
+
+    public MediaItem getItem() {
+        synchronized (mMarkers) {
+            // x is between 0 and 1.0f
+            int numMarkers = mMarkers.size();
+            if (numMarkers == 0)
+                return null;
+            int index = (int) (mPosition * (numMarkers));
+            if (index >= numMarkers)
+                index = numMarkers - 1;
+            Marker marker = mMarkers.get(index);
+            if (marker != null) {
+                // we have to find the index of the media item depending upon
+                // the value of mPosition
+                float deltaBetweenMarkers = 1.0f / numMarkers;
+                float increment = mPosition - index * deltaBetweenMarkers;
+                // if (increment > deltaBetweenMarkers)
+                // increment = deltaBetweenMarkers;
+                // if (increment < 0)
+                // increment = 0;
+                ArrayList<MediaItem> items = marker.items;
+                int numItems = items.size();
+                if (numItems == 0)
+                    return null;
+                int itemIndex = (int) ((numItems) * increment / deltaBetweenMarkers);
+                if (itemIndex >= numItems)
+                    itemIndex = numItems - 1;
+                return marker.items.get(itemIndex);
+            }
+        }
+        return null;
+    }
+
+    private Marker getAnchorMarker() {
+        synchronized (mMarkers) {
+            // x is between 0 and 1.0f
+            int numMarkers = mMarkers.size();
+            if (numMarkers == 0)
+                return null;
+            int index = (int) (mPosition * (numMarkers));
+            if (index >= numMarkers)
+                index = numMarkers - 1;
+            Marker marker = mMarkers.get(index);
+            return marker;
+        }
+    }
+
+    public void setItem(MediaItem item) {
+        Marker marker = mTracker.get(item);
+        if (marker != null) {
+            float markerX = (mTotalWidth == 0.0f) ? 0.0f : marker.x / mTotalWidth;
+            mPosition = Math.max(0.0f, Math.min(1.0f, markerX));
+            mScroll = getScrollForPosition(mPosition);
+        }
+    }
+
+    private void layout() {
+        if (mFeed != null) {
+            // Clear existing markers.
+            mTracker.clear();
+            synchronized (mMarkers) {
+                mMarkers.clear();
+            }
+            float scrollX = mScroll;
+            // Place markers for every time interval that intersects one of the
+            // clusters.
+            // Markers for a full month would be for example: Jan 5 10 15 20 25
+            // 30.
+            MediaFeed feed = mFeed;
+            int lastYear = -1;
+            int lastMonth = -1;
+            int lastDayBlock = -1;
+            float dx = 0f;
+            int increment = 12;
+            MediaSet set = null;
+            mShowTime = true;
+            if (mState == GridLayer.STATE_GRID_VIEW) {
+                set = feed.getFilteredSet();
+                if (set == null) {
+                    set = feed.getCurrentSet();
+                }
+            } else {
+                increment = 2;
+                if (!feed.hasExpandedMediaSet()) {
+                    mShowTime = false;
+                }
+                set = new MediaSet();
+                int numSlots = feed.getNumSlots();
+                for (int i = 0; i < numSlots; ++i) {
+                    MediaSet slotSet = feed.getSetForSlot(i);
+                    if (slotSet != null) {
+                        ArrayList<MediaItem> slotSetItems = slotSet.getItems();
+                        if (slotSetItems != null && slotSet.getNumItems() > 0) {
+                            MediaItem item = slotSetItems.get(0);
+                            if (item != null) {
+                                set.addItem(item);
+                            }
+                        }
+                    }
+                }
+            }
+            if (set != null) {
+                GregorianCalendar time = new GregorianCalendar();
+                ArrayList<MediaItem> items = set.getItems();
+                if (items != null) {
+                    int j = 0;
+                    while (j < set.getNumItems()) {
+                        final MediaItem item = items.get(j);
+                        if (item == null)
+                            continue;
+                        time.setTimeInMillis(item.mDateTakenInMs);
+                        // Detect year rollovers.
+                        final int year = time.get(Calendar.YEAR);
+                        if (year != lastYear) {
+                            lastYear = year;
+                            lastMonth = -1;
+                            lastDayBlock = -1;
+                        }
+                        Marker marker = null;
+                        // Detect month rollovers and emit a month marker.
+                        final int month = time.get(Calendar.MONTH);
+                        final int dayBlock = time.get(Calendar.DATE);
+                        if (month != lastMonth) {
+                            lastMonth = month;
+                            lastDayBlock = -1;
+                            marker = new Marker(mMonthLabels[month], dx, time.getTimeInMillis(), year, month, dayBlock,
+                                    Marker.TYPE_MONTH, increment);
+                            dx = addMarker(marker);
+                        } else if (dayBlock != lastDayBlock) {
+                            lastDayBlock = dayBlock;
+                            if (dayBlock != 0) {
+                                marker = new Marker(mDayLabels[dayBlock], dx, time.getTimeInMillis(), year, month, dayBlock,
+                                        Marker.TYPE_DAY, increment);
+                                dx = addMarker(marker);
+                            }
+                        } else {
+                            marker = new Marker(mDot, dx, time.getTimeInMillis(), year, month, dayBlock, Marker.TYPE_DOT, increment);
+                            dx = addMarker(marker);
+                        }
+                        for (int k = 0; k < increment; ++k) {
+                            int index = k + j;
+                            if (index < 0)
+                                continue;
+                            if (index >= items.size())
+                                break;
+                            if (index == items.size() - 1 && k != 0)
+                                break;
+                            MediaItem thisItem = items.get(index);
+                            marker.items.add(thisItem);
+                            mTracker.put(thisItem, marker);
+                        }
+                        if (j == items.size() - 1)
+                            break;
+                        j += increment;
+                        if (j >= items.size() - 1)
+                            j = items.size() - 1;
+                    }
+                }
+                mTotalWidth = dx - MARKER_SPACING_PIXELS * Gallery.PIXEL_DENSITY;
+            }
+            mPosition = getPositionForScroll(scrollX);
+            mPositionAnim = mPosition;
+            synchronized (mMarkersCopy) {
+                int numMarkers = mMarkers.size();
+                mMarkersCopy.clear();
+                mMarkersCopy.ensureCapacity(numMarkers);
+                for (int i = 0; i < numMarkers; ++i) {
+                    mMarkersCopy.add(mMarkers.get(i));
+                }
+            }
+        }
+    }
+
+    private float addMarker(Marker marker) {
+        mMarkers.add(marker);
+        return marker.x + MARKER_SPACING_PIXELS * Gallery.PIXEL_DENSITY;
+    }
+
+    /*
+     * private float getKnobXForPosition(float position) { return position * (mTotalWidth - mKnob.getWidth()); }
+     * 
+     * private float getPositionForKnobX(float knobX) { return Math.max(0f, Math.min(1f, knobX / (mTotalWidth - mKnob.getWidth())));
+     * }
+     * 
+     * private float getScrollForPosition(float position) { return position * (mTotalWidth - mWidth);// - (1f - 2f * position) *
+     * MARKER_SPACING_PIXELS; }
+     */
+
+    private float getScrollForPosition(float position) {
+        // Map position [0, 1] to scroll [-visibleWidth/2, totalWidth -
+        // visibleWidth/2].
+        // This has the effect of centering the scroll knob on screen.
+        float halfWidth = mWidth * 0.5f;
+        float positionInv = 1f - position;
+        float centered = positionInv * -halfWidth + position * (mTotalWidth - halfWidth);
+        return centered;
+    }
+
+    private float getPositionForScroll(float scroll) {
+        float halfWidth = mWidth * 0.5f;
+        if (mTotalWidth == 0)
+            return 0;
+        return ((scroll + halfWidth) / (mTotalWidth));
+    }
+
+    private float getKnobXForPosition(float position) {
+        return position * mTotalWidth;
+    }
+
+    private float getPositionForKnobX(float knobX) {
+        float normKnobX = (mTotalWidth == 0) ? 0 : knobX / mTotalWidth;
+        return Math.max(0f, Math.min(1f, normKnobX));
+    }
+
+    @Override
+    public boolean update(RenderView view, float dt) {
+        // Update animations.
+        final float ratio = Math.min(1f, 10f * dt);
+        final float invRatio = 1f - ratio;
+        mPositionAnim = ratio * mPosition + invRatio * mPositionAnim;
+        mScrollAnim = ratio * mScroll + invRatio * mScrollAnim;
+        // Handle autoscroll.
+        if (mInDrag) {
+            final float x = getKnobXForPosition(mPosition) - mScrollAnim;
+            float velocity;
+            float autoScrollMargin = AUTO_SCROLL_MARGIN * Gallery.PIXEL_DENSITY;
+            if (x < autoScrollMargin) {
+                velocity = -(float) Math.pow((1f - x / autoScrollMargin), 2);
+            } else if (x > mWidth - autoScrollMargin) {
+                velocity = (float) Math.pow(1f - (mWidth - x) / autoScrollMargin, 2);
+            } else {
+                velocity = 0;
+            }
+            mScroll += velocity * 400f * dt;
+            mPosition = getPositionForKnobX(mDragX + mScroll);
+            mTextAlpha = 1.0f;
+        } else {
+            mTextAlpha = 0.0f;
+        }
+        mAnimTextAlpha = FloatUtils.animate(mAnimTextAlpha, mTextAlpha, dt);
+        return mAnimTextAlpha != mTextAlpha;
+    }
+
+    @Override
+    public void renderBlended(RenderView view, GL11 gl) {
+        final float originX = mX;
+        final float originY = mY;
+        final float scrollOffset = mScrollAnim;
+        final float scrolledOriginX = originX - scrollOffset;
+        final float position = mPositionAnim;
+        final int knobId = mInDrag ? KNOB_PRESSED : KNOB;
+        final Texture knob = view.getResource(knobId);
+        // Draw the scroller knob.
+        if (!mShowTime) {
+            if (view.bind(knob)) {
+                final float knobWidth = knob.getWidth();
+                view.draw2D(scrolledOriginX + getKnobXForPosition(position) - knobWidth * 0.5f, originY, 0f, knobWidth, knob
+                        .getHeight());
+            }
+        } else {
+            if (view.bind(knob)) {
+                final float knobWidth = knob.getWidth();
+                final float knobHeight = knob.getHeight();
+                view.draw2D(scrolledOriginX + getKnobXForPosition(position) - knobWidth * 0.5f, view.getHeight() - knobHeight, 0f,
+                        knobWidth, knobHeight);
+            }
+            // we draw the current time on top of the knob
+            if (mInDrag || mAnimTextAlpha != 0.0f) {
+                Marker anchor = getAnchorMarker();
+                if (anchor != null) {
+                    Texture month = mMonthLabels[anchor.month];
+                    Texture day = mOpaqueDayLabels[anchor.day];
+                    Texture year = getYearLabel(anchor.year);
+                    boolean validDate = true;
+                    if (anchor.year <= 1970) {
+                        month = mDateUnknown;
+                        day = null;
+                        year = null;
+                        validDate = false;
+                    }
+                    view.loadTexture(month);
+                    if (validDate) {
+                        view.loadTexture(day);
+                        view.loadTexture(year);
+                    }
+                    int numPixelsBufferX = 70;
+                    float expectedWidth = month.getWidth()
+                            + ((validDate) ? (day.getWidth() + year.getWidth() + 10 * Gallery.PIXEL_DENSITY) : 0);
+                    if ((expectedWidth + numPixelsBufferX * Gallery.PIXEL_DENSITY) != mBackgroundRect.right) {
+                        mBackgroundRect.right = (int) (expectedWidth + numPixelsBufferX * Gallery.PIXEL_DENSITY);
+                        mBackgroundRect.bottom = (int) (month.getHeight() + 20 * Gallery.PIXEL_DENSITY);
+                        try {
+                            Bitmap bitmap = Bitmap.createBitmap(mBackgroundRect.right, mBackgroundRect.bottom,
+                                    Bitmap.Config.ARGB_8888);
+                            Canvas canvas = new Canvas();
+                            canvas.setBitmap(bitmap);
+                            mBackground.draw(canvas, mBackgroundRect, SRC_PAINT);
+                            mBackgroundTexture = new BitmapTexture(bitmap);
+                            view.loadTexture(mBackgroundTexture);
+                            bitmap.recycle();
+                        } catch (OutOfMemoryError e) {
+                            // Do nothing.
+                        }
+                    }
+                    gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
+                    gl.glColor4f(mAnimTextAlpha, mAnimTextAlpha, mAnimTextAlpha, mAnimTextAlpha);
+                    float x = (view.getWidth() - expectedWidth - numPixelsBufferX * Gallery.PIXEL_DENSITY) / 2;
+                    float y = (view.getHeight() - 10 * Gallery.PIXEL_DENSITY) * 0.5f;
+                    if (mBackgroundTexture != null) {
+                        view.draw2D(mBackgroundTexture, x, y);
+                    }
+                    y = view.getHeight() * 0.5f;
+                    x = (view.getWidth() - expectedWidth) / 2;
+                    view.draw2D(month, x, y);
+                    if (validDate) {
+                        x += month.getWidth() + 3 * Gallery.PIXEL_DENSITY;
+                        view.draw2D(day, x, y);
+                        x += day.getWidth() + 7 * Gallery.PIXEL_DENSITY;
+                        view.draw2D(year, x, y);
+                    }
+                    if (mAnimTextAlpha != 1f) {
+                        gl.glColor4f(1f, 1f, 1f, 1f);
+                    }
+                    gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE);
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // Set position on touch movement.
+        mDragX = event.getX();
+        mPosition = getPositionForKnobX(mDragX + mScroll);
+
+        // Notify the listener.
+        if (mListener != null) {
+            mListener.onTimeChanged(this);
+        }
+
+        // Update state when touch begins and ends.
+        switch (event.getAction()) {
+        case MotionEvent.ACTION_DOWN:
+            mInDrag = true;
+            break;
+        case MotionEvent.ACTION_UP:
+        case MotionEvent.ACTION_CANCEL:
+            // mScroll = getScrollForPosition(mPosition);
+            mInDrag = false;
+
+            // Clamp to the nearest marker.
+            setItem(getItem());
+        default:
+            break;
+        }
+
+        return true;
+    }
+
+    public void onFeedChanged(MediaFeed feed, boolean needsLayout) {
+        layout();
+    }
+
+    private static final class Marker {
+        Marker(StringTexture texture, float x, long time, int year, int month, int day, int type, int expectedCapacity) {
+            this.x = x;
+            this.year = year;
+            this.month = month;
+            this.day = day;
+            this.items = new ArrayList<MediaItem>(expectedCapacity);
+        }
+
+        public static final int TYPE_MONTH = 1;
+        public static final int TYPE_DAY = 2;
+        public static final int TYPE_DOT = 3;
+        public ArrayList<MediaItem> items;
+        public final float x;
+        public final int year;
+        public final int month;
+        public final int day;
+    }
+
+    @Override
+    public void generate(RenderView view, Lists lists) {
+        lists.updateList.add(this);
+        lists.blendedList.add(this);
+        lists.hitTestList.add(this);
+    }
+
+    public void onFeedAboutToChange(MediaFeed feed) {
+        // nothing needs to be done
+        return;
+    }
+
+    private StringTexture getYearLabel(int year) {
+        if (year <= 1970)
+            return mDot;
+        StringTexture label = mYearLabels.get(year);
+        if (label == null) {
+            label = new StringTexture(Integer.toString(year), mMonthYearFormat);
+            mYearLabels.put(year, label);
+        }
+        return label;
+    }
+
+    public boolean isDragged() {
+        return mInDrag;
+    }
+}
diff --git a/src/com/cooliris/media/UriTexture.java b/src/com/cooliris/media/UriTexture.java
new file mode 100644
index 0000000..3a8f216
--- /dev/null
+++ b/src/com/cooliris/media/UriTexture.java
@@ -0,0 +1,309 @@
+package com.cooliris.media;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.params.HttpClientParams;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.*;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+
+import com.cooliris.cache.CacheService;
+
+public class UriTexture extends Texture {
+    public static final int MAX_RESOLUTION = 1024;
+    private static final String TAG = "UriTexture";
+    protected String mUri;
+    protected long mCacheId;
+    private static final int MAX_RESOLUTION_A = MAX_RESOLUTION;
+    private static final int MAX_RESOLUTION_B = MAX_RESOLUTION;
+    public static final String URI_CACHE = CacheService.getCachePath("hires-image-cache");
+    private static final String USER_AGENT = "Cooliris-ImageDownload";
+    private static final int CONNECTION_TIMEOUT = 20000; // ms.
+    public static final HttpParams HTTP_PARAMS;
+    public static final SchemeRegistry SCHEME_REGISTRY;
+    static {
+        // Prepare HTTP parameters.
+        HttpParams params = new BasicHttpParams();
+        HttpConnectionParams.setStaleCheckingEnabled(params, false);
+        HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
+        HttpConnectionParams.setSoTimeout(params, CONNECTION_TIMEOUT);
+        HttpClientParams.setRedirecting(params, true);
+        HttpProtocolParams.setUserAgent(params, USER_AGENT);
+        HTTP_PARAMS = params;
+
+        // Register HTTP protocol.
+        SCHEME_REGISTRY = new SchemeRegistry();
+        SCHEME_REGISTRY.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
+    }
+
+    private SingleClientConnManager mConnectionManager;
+
+    static {
+        File uri_cache = new File(URI_CACHE);
+        uri_cache.mkdirs();
+    }
+
+    public UriTexture(String imageUri) {
+        mUri = imageUri;
+    }
+
+    public void setCacheId(long id) {
+        mCacheId = id;
+    }
+
+    public static final Bitmap createFromUri(Context context, String uri, int maxResolutionX, int maxResolutionY, long cacheId,
+            ClientConnectionManager connectionManager) throws IOException, URISyntaxException, OutOfMemoryError {
+        final BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inScaled = false;
+        options.inPreferredConfig = Bitmap.Config.RGB_565;
+        options.inDither = true;
+        long crc64 = 0;
+        Bitmap bitmap = null;
+        if (uri.startsWith(ContentResolver.SCHEME_CONTENT)) {
+            // We need the filepath for the given content uri
+            crc64 = cacheId;
+        } else {
+            crc64 = Utils.Crc64Long(uri);
+        }
+        bitmap = createFromCache(crc64, maxResolutionX);
+        if (bitmap != null) {
+            return bitmap;
+        }
+        boolean local = false;
+        int sampleSize = 1;
+        if (uri.startsWith(ContentResolver.SCHEME_CONTENT)) {
+            local = true;
+
+            // Load the bitmap from a local file.
+            options.inJustDecodeBounds = true;
+            BufferedInputStream bufferedInput = new BufferedInputStream(context.getContentResolver()
+                    .openInputStream(Uri.parse(uri)), 16384);
+            bufferedInput.mark(Integer.MAX_VALUE);
+            bitmap = BitmapFactory.decodeStream(bufferedInput, null, options);
+            int width = options.outWidth;
+            int height = options.outHeight;
+            float maxResX = maxResolutionY;
+            if (width > height) {
+                maxResX = maxResolutionX;
+            }
+            float maxResY = (maxResX == maxResolutionX) ? maxResolutionY : maxResolutionX;
+            int ratioX = (int) Math.ceil((float) width / maxResX);
+            int ratioY = (int) Math.ceil((float) height / maxResY);
+            int ratio = Math.max(ratioX, ratioY);
+            ratio = Shared.nextPowerOf2(ratio);
+            sampleSize = ratio;
+            options.inDither = true;
+            options.inJustDecodeBounds = false;
+            options.inSampleSize = ratio;
+            Thread timeoutThread = new Thread("BitmapTimeoutThread") {
+                public void run() {
+                    try {
+                        Thread.sleep(6000);
+                        options.requestCancelDecode();
+                    } catch (InterruptedException e) {
+
+                    }
+                }
+            };
+            timeoutThread.start();
+            bufferedInput.close();
+            bufferedInput = new BufferedInputStream(context.getContentResolver().openInputStream(Uri.parse(uri)), 16384);
+            bitmap = BitmapFactory.decodeStream(bufferedInput, null, options);
+            bufferedInput.close();
+        } else {
+            // Load the bitmap from a remote URL.
+            try {
+                InputStream contentInput = null;
+                if (connectionManager == null) {
+                    final URL url = new URI(uri).toURL();
+                    final URLConnection conn = url.openConnection();
+                    conn.connect();
+                    contentInput = conn.getInputStream();
+                } else {
+                    // We create a cancelable http request from the client
+                    final DefaultHttpClient mHttpClient = new DefaultHttpClient(connectionManager, HTTP_PARAMS);
+                    HttpUriRequest request = new HttpGet(uri);
+                    // Execute the HTTP request.
+                    HttpResponse httpResponse = null;
+                    try {
+                        httpResponse = mHttpClient.execute(request);
+                    } catch (IOException e) {
+                        Log.w(TAG, "Request failed: " + request.getURI());
+                        throw e;
+                    }
+                    HttpEntity entity = httpResponse.getEntity();
+                    if (entity != null) {
+                        // Wrap the entity input stream in a GZIP decoder if necessary.
+                        contentInput = entity.getContent();
+                    }
+                }
+                if (contentInput != null) {
+                    final BufferedInputStream bufferedInput = new BufferedInputStream(contentInput, 4096);
+                    bitmap = BitmapFactory.decodeStream(bufferedInput, null, options);
+                    bufferedInput.close();
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Error loading image from uri " + uri);
+            }
+        }
+        if (sampleSize > 1 || !local) {
+            writeToCache(crc64, bitmap, maxResolutionX);
+        }
+        return bitmap;
+    }
+
+    @Override
+    protected Bitmap load(RenderView view) {
+        Bitmap bitmap = null;
+        try {
+            if (mUri.startsWith("http://")) {
+                if (!isCached(Utils.Crc64Long(mUri), MAX_RESOLUTION_A)) {
+                    mConnectionManager = new SingleClientConnManager(HTTP_PARAMS, SCHEME_REGISTRY);
+                }
+            }
+            bitmap = createFromUri(view.getContext(), mUri, MAX_RESOLUTION_A, MAX_RESOLUTION_B, mCacheId, mConnectionManager);
+        } catch (Exception e2) {
+            Log.e(TAG, "Unable to load image from URI " + mUri);
+            e2.printStackTrace();
+        }
+        return bitmap;
+    }
+
+    public static final String createFilePathFromCrc64(long crc64, int maxResolution) {
+        return URI_CACHE + crc64 + "_" + maxResolution + ".cache";
+    }
+
+    public static boolean isCached(long crc64, int maxResolution) {
+        String file = null;
+        final BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inScaled = false;
+        options.inPreferredConfig = Bitmap.Config.RGB_565;
+        options.inDither = true;
+        if (crc64 != 0) {
+            file = createFilePathFromCrc64(crc64, maxResolution);
+            try {
+                new FileInputStream(file);
+                return true;
+            } catch (FileNotFoundException e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    public static Bitmap createFromCache(long crc64, int maxResolution) {
+        try {
+            String file = null;
+            Bitmap bitmap = null;
+            final BitmapFactory.Options options = new BitmapFactory.Options();
+            options.inScaled = false;
+            options.inPreferredConfig = Bitmap.Config.RGB_565;
+            options.inDither = true;
+            if (crc64 != 0) {
+                file = createFilePathFromCrc64(crc64, maxResolution);
+                bitmap = BitmapFactory.decodeFile(file, options);
+            }
+            return bitmap;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+    
+    public static String writeHttpDataInDirectory(Context context, String uri, String path) {
+        long crc64 = Utils.Crc64Long(uri);
+        if (!isCached(crc64, 1024)) {
+            Bitmap bitmap;
+            try {
+                bitmap = UriTexture.createFromUri(context, uri, 1024, 1024, crc64, null);
+            } catch (OutOfMemoryError e) {
+                return null;
+            } catch (IOException e) {
+                return null;
+            } catch (URISyntaxException e) {
+                return null;
+            }
+            bitmap.recycle();
+        }
+        String fileString = createFilePathFromCrc64(crc64, 1024);
+        try {
+            File file = new File(fileString);
+            if (file.exists()) {
+                // We write a copy of this file
+                String newPath = path + (path.endsWith("/") ? "" : "/") + crc64 + ".jpg";
+                File newFile = new File(newPath);
+                Utils.Copy(file, newFile);
+                return newPath;
+            }
+            return null;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    public static void writeToCache(long crc64, Bitmap bitmap, int maxResolution) {
+        String file = createFilePathFromCrc64(crc64, maxResolution);
+        if (bitmap != null && file != null && crc64 != 0) {
+            try {
+                File fileC = new File(file);
+                fileC.createNewFile();
+                final FileOutputStream fos = new FileOutputStream(fileC);
+                final BufferedOutputStream bos = new BufferedOutputStream(fos, 16384);
+                bitmap.compress(Bitmap.CompressFormat.JPEG, 80, bos);
+                bos.flush();
+                bos.close();
+                fos.close();
+            } catch (Exception e) {
+
+            }
+        }
+    }
+    
+    public static void invalidateCache(long crc64, int maxResolution) {
+        String file = createFilePathFromCrc64(crc64, maxResolution);
+        if (file != null && crc64 != 0) {
+            try {
+                File fileC = new File(file);
+                fileC.delete();
+            } catch (Exception e) {
+
+            }
+        }
+
+    }
+
+    @Override
+    public void finalize() {
+        if (mConnectionManager != null) {
+            mConnectionManager.shutdown();
+        }
+    }
+}
diff --git a/src/com/cooliris/media/Util.java b/src/com/cooliris/media/Util.java
new file mode 100644
index 0000000..8089c30
--- /dev/null
+++ b/src/com/cooliris/media/Util.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.cooliris.media;
+
+
+import android.app.ProgressDialog;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import java.io.Closeable;
+
+/**
+ * Collection of utility functions used in this package.
+ */
+public class Util {
+    private static final String TAG = "db.Util";
+    private static final String MAPS_PACKAGE_NAME =
+            "com.google.android.apps.maps";
+    private static final String MAPS_CLASS_NAME =
+            "com.google.android.maps.MapsActivity";
+
+
+    private Util() {
+    }
+
+    // Rotates the bitmap by the specified degree.
+    // If a new bitmap is created, the original bitmap is recycled.
+    public static Bitmap rotate(Bitmap b, int degrees) {
+        if (degrees != 0 && b != null) {
+            Matrix m = new Matrix();
+            m.setRotate(degrees,
+                    (float) b.getWidth() / 2, (float) b.getHeight() / 2);
+            try {
+                Bitmap b2 = Bitmap.createBitmap(
+                        b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+                if (b != b2) {
+                    b.recycle();
+                    b = b2;
+                }
+            } catch (OutOfMemoryError ex) {
+                // We have no memory to rotate. Return the original bitmap.
+            }
+        }
+        return b;
+    }
+
+    public static Bitmap transform(Matrix scaler,
+                                   Bitmap source,
+                                   int targetWidth,
+                                   int targetHeight,
+                                   boolean scaleUp) {
+        int deltaX = source.getWidth() - targetWidth;
+        int deltaY = source.getHeight() - targetHeight;
+        if (!scaleUp && (deltaX < 0 || deltaY < 0)) {
+            /*
+             * In this case the bitmap is smaller, at least in one dimension,
+             * than the target.  Transform it by placing as much of the image
+             * as possible into the target and leaving the top/bottom or
+             * left/right (or both) black.
+             */
+            Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight,
+                    Bitmap.Config.ARGB_8888);
+            Canvas c = new Canvas(b2);
+
+            int deltaXHalf = Math.max(0, deltaX / 2);
+            int deltaYHalf = Math.max(0, deltaY / 2);
+            Rect src = new Rect(
+                    deltaXHalf,
+                    deltaYHalf,
+                    deltaXHalf + Math.min(targetWidth, source.getWidth()),
+                    deltaYHalf + Math.min(targetHeight, source.getHeight()));
+            int dstX = (targetWidth  - src.width())  / 2;
+            int dstY = (targetHeight - src.height()) / 2;
+            Rect dst = new Rect(
+                    dstX,
+                    dstY,
+                    targetWidth - dstX,
+                    targetHeight - dstY);
+            c.drawBitmap(source, src, dst, null);
+            return b2;
+        }
+        float bitmapWidthF = source.getWidth();
+        float bitmapHeightF = source.getHeight();
+
+        float bitmapAspect = bitmapWidthF / bitmapHeightF;
+        float viewAspect   = (float) targetWidth / targetHeight;
+
+        if (bitmapAspect > viewAspect) {
+            float scale = targetHeight / bitmapHeightF;
+            if (scale < .9F || scale > 1F) {
+                scaler.setScale(scale, scale);
+            } else {
+                scaler = null;
+            }
+        } else {
+            float scale = targetWidth / bitmapWidthF;
+            if (scale < .9F || scale > 1F) {
+                scaler.setScale(scale, scale);
+            } else {
+                scaler = null;
+            }
+        }
+
+        Bitmap b1;
+        if (scaler != null) {
+            // this is used for minithumb and crop, so we want to filter here.
+            b1 = Bitmap.createBitmap(source, 0, 0,
+                    source.getWidth(), source.getHeight(), scaler, true);
+        } else {
+            b1 = source;
+        }
+
+        int dx1 = Math.max(0, b1.getWidth() - targetWidth);
+        int dy1 = Math.max(0, b1.getHeight() - targetHeight);
+
+        Bitmap b2 = Bitmap.createBitmap(
+                b1,
+                dx1 / 2,
+                dy1 / 2,
+                targetWidth,
+                targetHeight);
+
+        if (b1 != source) {
+            b1.recycle();
+        }
+
+        return b2;
+    }
+
+    /**
+     * Creates a centered bitmap of the desired size. Recycles the input.
+     * @param source
+     */
+    public static Bitmap extractMiniThumb(
+            Bitmap source, int width, int height) {
+        return Util.extractMiniThumb(source, width, height, true);
+    }
+
+    public static Bitmap extractMiniThumb(
+            Bitmap source, int width, int height, boolean recycle) {
+        if (source == null) {
+            return null;
+        }
+
+        float scale;
+        if (source.getWidth() < source.getHeight()) {
+            scale = width / (float) source.getWidth();
+        } else {
+            scale = height / (float) source.getHeight();
+        }
+        Matrix matrix = new Matrix();
+        matrix.setScale(scale, scale);
+        Bitmap miniThumbnail = transform(matrix, source, width, height, false);
+
+        if (recycle && miniThumbnail != source) {
+            source.recycle();
+        }
+        return miniThumbnail;
+    }
+
+
+    public static <T>  int indexOf(T [] array, T s) {
+        for (int i = 0; i < array.length; i++) {
+            if (array[i].equals(s)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    public static void closeSilently(Closeable c) {
+        if (c == null) return;
+        try {
+            c.close();
+        } catch (Throwable t) {
+            // do nothing
+        }
+    }
+
+    public static void closeSilently(ParcelFileDescriptor c) {
+        if (c == null) return;
+        try {
+            c.close();
+        } catch (Throwable t) {
+            // do nothing
+        }
+    }
+
+    public static void Assert(boolean cond) {
+        if (!cond) {
+            throw new AssertionError();
+        }
+    }
+
+    public static boolean equals(String a, String b) {
+        // return true if both string are null or the content equals
+        return a == b || a.equals(b);
+    }
+
+    private static class BackgroundJob
+            extends MonitoredActivity.LifeCycleAdapter implements Runnable {
+
+        private final MonitoredActivity mActivity;
+        private final ProgressDialog mDialog;
+        private final Runnable mJob;
+        private final Handler mHandler;
+        private final Runnable mCleanupRunner = new Runnable() {
+            public void run() {
+                mActivity.removeLifeCycleListener(BackgroundJob.this);
+                if (mDialog.getWindow() != null) mDialog.dismiss();
+            }
+        };
+
+        public BackgroundJob(MonitoredActivity activity, Runnable job,
+                ProgressDialog dialog, Handler handler) {
+            mActivity = activity;
+            mDialog = dialog;
+            mJob = job;
+            mActivity.addLifeCycleListener(this);
+            mHandler = handler;
+        }
+
+        public void run() {
+            try {
+                mJob.run();
+            } finally {
+                mHandler.post(mCleanupRunner);
+            }
+        }
+
+
+        @Override
+        public void onActivityDestroyed(MonitoredActivity activity) {
+            // We get here only when the onDestroyed being called before
+            // the mCleanupRunner. So, run it now and remove it from the queue
+            mCleanupRunner.run();
+            mHandler.removeCallbacks(mCleanupRunner);
+        }
+
+        @Override
+        public void onActivityStopped(MonitoredActivity activity) {
+            mDialog.hide();
+        }
+
+        @Override
+        public void onActivityStarted(MonitoredActivity activity) {
+            mDialog.show();
+        }
+    }
+
+    public static void startBackgroundJob(MonitoredActivity activity,
+            String title, String message, Runnable job, Handler handler) {
+        // Make the progress dialog uncancelable, so that we can gurantee
+        // the thread will be done before the activity getting destroyed.
+        ProgressDialog dialog = ProgressDialog.show(
+                activity, title, message, true, false);
+        new Thread(new BackgroundJob(activity, job, dialog, handler)).start();
+    }
+
+    // Returns an intent which is used for "set as" menu items.
+    public static Intent createSetAsIntent(Uri uri, String mimeType) {
+    	// Infer MIME type if missing for file URLs.
+    	if (uri.getScheme().equals("file")) {
+    		String path = uri.getPath();
+    		int lastDotIndex = path.lastIndexOf('.');
+    		if (lastDotIndex != -1) {
+    			mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+    					uri.getPath().substring(lastDotIndex + 1).toLowerCase());
+    		}
+    	}
+    	
+        Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
+        intent.setDataAndType(uri, mimeType);
+        intent.putExtra("mimeType", mimeType);
+        return intent;
+    }
+
+    // Opens Maps application for a map with given latlong. There is a bug
+    // which crashes the Browser when opening this kind of URL. So, we open
+    // it in GMM instead. For those platforms which have no GMM installed,
+    // the default Maps application will be chosen.
+
+    
+    public static void openMaps(Context context, double latitude, double longitude) {
+        try {
+            // Try to open the GMM first
+
+            // We don't use "geo:latitude,longitude" because it only centers
+            // the MapView to the specified location, but we need a marker
+            // for further operations (routing to/from).
+            // The q=(lat, lng) syntax is suggested by geo-team.
+            String url = String.format(
+                    "http://maps.google.com/maps?f=q&q=(%s,%s)", latitude, longitude);
+            ComponentName compName =
+                    new ComponentName(MAPS_PACKAGE_NAME, MAPS_CLASS_NAME);
+            Intent mapsIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url))
+                    .setComponent(compName);
+            context.startActivity(mapsIntent);
+        } catch (ActivityNotFoundException e) {
+            // Use the "geo intent" if no GMM is installed
+            Log.e(TAG, "GMM activity not found!", e);
+            String url = String.format("geo:%s,%s", latitude, longitude);
+            Intent mapsIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+            context.startActivity(mapsIntent);
+        }
+    }
+}
diff --git a/src/com/cooliris/media/Utils.java b/src/com/cooliris/media/Utils.java
new file mode 100644
index 0000000..da115a7
--- /dev/null
+++ b/src/com/cooliris/media/Utils.java
@@ -0,0 +1,166 @@
+package com.cooliris.media;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.widget.Toast;
+
+public class Utils {
+    public static void playVideo(final Context context, final MediaItem item) {
+        // this is a video
+        ((Gallery) context).getHandler().post(new Runnable() {
+            public void run() {
+                try {
+                    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(item.mContentUri));
+                    intent.setDataAndType(Uri.parse(item.mContentUri), item.mMimeType);
+                    context.startActivity(intent);
+                } catch (ActivityNotFoundException e) {
+                    Toast.makeText(context, context.getResources().getString(R.string.video_err), Toast.LENGTH_SHORT).show();
+                }
+            }
+        });
+    }
+
+    public static final void writeUTF(DataOutputStream dos, String string) throws IOException {
+        if (string == null) {
+            dos.writeUTF(new String());
+        } else {
+            dos.writeUTF(string);
+        }
+    }
+
+    public static final String readUTF(DataInputStream dis) throws IOException {
+        String retVal = dis.readUTF();
+        if (retVal.length() == 0)
+            return null;
+        return retVal;
+    }
+
+    public static final Bitmap resizeBitmap(Bitmap bitmap, int maxSize) {
+        int srcWidth = bitmap.getWidth();
+        int srcHeight = bitmap.getHeight();
+        int width = maxSize;
+        int height = maxSize;
+        boolean needsResize = false;
+        if (srcWidth > srcHeight) {
+            if (srcWidth > maxSize) {
+                needsResize = true;
+                height = ((maxSize * srcHeight) / srcWidth);
+            }
+        } else {
+            if (srcHeight > maxSize) {
+                needsResize = true;
+                width = ((maxSize * srcWidth) / srcHeight);
+            }
+        }
+        if (needsResize) {
+            Bitmap retVal = Bitmap.createScaledBitmap(bitmap, width, height, true);
+            return retVal;
+        } else {
+            return bitmap;
+        }
+    }
+
+    private static final long POLY64REV = 0x95AC9329AC4BC9B5L;
+    private static final long INITIALCRC = 0xFFFFFFFFFFFFFFFFL;
+
+    private static boolean init = false;
+    private static long[] CRCTable = new long[256];
+
+    /**
+     * A function thats returns a 64-bit crc for string
+     * 
+     * @param in
+     *            : input string
+     * @return 64-bit crc value
+     */
+    public static final long Crc64Long(String in) {
+        if (in == null || in.length() == 0) {
+            return 0;
+        }
+        // http://bioinf.cs.ucl.ac.uk/downloads/crc64/crc64.c
+        long crc = INITIALCRC, part;
+        if (!init) {
+            for (int i = 0; i < 256; i++) {
+                part = i;
+                for (int j = 0; j < 8; j++) {
+                    int value = ((int) part & 1);
+                    if (value != 0)
+                        part = (part >> 1) ^ POLY64REV;
+                    else
+                        part >>= 1;
+                }
+                CRCTable[i] = part;
+            }
+            init = true;
+        }
+        int length = in.length();
+        for (int k = 0; k < length; ++k) {
+            char c = in.charAt(k);
+            crc = CRCTable[(((int) crc) ^ c) & 0xff] ^ (crc >> 8);
+        }
+        return crc;
+    }
+
+    /**
+     * A function that returns a human readable hex string of a Crx64
+     * 
+     * @param in
+     *            : input string
+     * @return hex string of the 64-bit CRC value
+     */
+    public static final String Crc64(String in) {
+        if (in == null)
+            return null;
+        long crc = Crc64Long(in);
+        /*
+         * The output is done in two parts to avoid problems with architecture-dependent word order
+         */
+        int low = ((int) crc) & 0xffffffff;
+        int high = ((int) (crc >> 32)) & 0xffffffff;
+        String outVal = Integer.toHexString(high) + Integer.toHexString(low);
+        return outVal;
+    }
+
+    public static String getBucketNameFromUri(Uri uri) {
+        String string = "";
+        if (string == null || string.length() == 0) {
+            List<String> paths = uri.getPathSegments();
+            int numPaths = paths.size();
+            if (numPaths > 1) {
+                string = paths.get(paths.size() - 2);
+            }
+            if (string == null)
+                string = "";
+        }
+        return string;
+    }
+    
+    // Copies src file to dst file.
+    // If the dst file does not exist, it is created
+    public static void Copy(File src, File dst) throws IOException {
+        InputStream in = new FileInputStream(src);
+        OutputStream out = new FileOutputStream(dst);
+    
+        // Transfer bytes from in to out
+        byte[] buf = new byte[1024];
+        int len;
+        while ((len = in.read(buf)) > 0) {
+            out.write(buf, 0, len);
+        }
+        in.close();
+        out.close();
+    }
+}
diff --git a/src/com/cooliris/media/Vector3f.java b/src/com/cooliris/media/Vector3f.java
new file mode 100644
index 0000000..855821c
--- /dev/null
+++ b/src/com/cooliris/media/Vector3f.java
@@ -0,0 +1,56 @@
+package com.cooliris.media;
+
+public final class Vector3f {
+    public float x;
+    public float y;
+    public float z;
+
+    public Vector3f() {
+
+    }
+
+    public Vector3f(float x, float y, float z) {
+        set(x, y, z);
+    }
+
+    public Vector3f(Vector3f vector) {
+        x = vector.x;
+        y = vector.y;
+        z = vector.z;
+    }
+
+    public void set(Vector3f vector) {
+        x = vector.x;
+        y = vector.y;
+        z = vector.z;
+    }
+
+    public void set(float x, float y, float z) {
+        this.x = x;
+        this.y = y;
+        this.z = z;
+    }
+
+    public void add(Vector3f vector) {
+        x += vector.x;
+        y += vector.y;
+        z += vector.z;
+    }
+
+    public void subtract(Vector3f vector) {
+        x -= vector.x;
+        y -= vector.y;
+        z -= vector.z;
+    }
+
+    public boolean equals(Vector3f vector) {
+        if (x == vector.x && y == vector.y && z == vector.z)
+            return true;
+        return false;
+    }
+    
+    @Override
+    public String toString() {
+        return (new String("(" + x + ", " + y + ", " + z + ")" ));
+    }
+}
diff --git a/src/com/cooliris/media/VirtualFeed.java b/src/com/cooliris/media/VirtualFeed.java
new file mode 100644
index 0000000..aaea41c
--- /dev/null
+++ b/src/com/cooliris/media/VirtualFeed.java
@@ -0,0 +1,50 @@
+package com.cooliris.media;
+
+public abstract class VirtualFeed<E> {
+    protected int mLoadedBegin = 0;
+    protected int mLoadedEnd = 0;
+    protected int mLoadingBegin = 0;
+    protected int mLoadingEnd = 0;
+    protected int mLoadableBegin = Integer.MIN_VALUE;
+    protected int mLoadableEnd = Integer.MAX_VALUE;
+    protected final Deque<E> mElements = new Deque<E>();
+
+    public VirtualFeed() {
+    }
+
+    public abstract void setLoadingRange(int begin, int end);
+
+    public final E get(int index) {
+
+        return null;
+
+    }
+
+    public final int getLoadedBegin() {
+        return mLoadedBegin;
+    }
+
+    public final int getLoadedEnd() {
+        return mLoadedEnd;
+    }
+
+    public final int getLoadingBegin() {
+        return mLoadingBegin;
+    }
+
+    public final int getLoadingEnd() {
+        return mLoadingEnd;
+    }
+
+    public final int getLoadableBegin() {
+        return mLoadableBegin;
+    }
+
+    public final int getLoadableEnd() {
+        return mLoadableEnd;
+    }
+
+    public interface RangeListener<E> {
+        void onRangeUpdated(VirtualFeed<E> array);
+    }
+}
diff --git a/src/com/cooliris/media/Wallpaper.java b/src/com/cooliris/media/Wallpaper.java
new file mode 100644
index 0000000..0b631e3
--- /dev/null
+++ b/src/com/cooliris/media/Wallpaper.java
@@ -0,0 +1,208 @@
+package com.cooliris.media;
+
+/*
+ * Copyright (C) 2007 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.
+ */
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Wallpaper picker for the camera application. This just redirects to the
+ * standard pick action.
+ */
+public class Wallpaper extends Activity {
+    private static final String LOG_TAG = "Wallpaper";
+    static final int PHOTO_PICKED = 1;
+    static final int CROP_DONE = 2;
+
+    static final int SHOW_PROGRESS = 0;
+    static final int FINISH = 1;
+
+    static final String DO_LAUNCH_ICICLE = "do_launch";
+    static final String TEMP_FILE_PATH_ICICLE = "temp_file_path";
+
+    private ProgressDialog mProgressDialog = null;
+    private boolean mDoLaunch = true;
+    private File mTempFile;
+
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case SHOW_PROGRESS: {
+                    CharSequence c = getText(R.string.wallpaper);
+                    mProgressDialog = ProgressDialog.show(Wallpaper.this,
+                            "", c, true, false);
+                    break;
+                }
+                case FINISH: {
+                    closeProgressDialog();
+                    setResult(RESULT_OK);
+                    finish();
+                    break;
+                }
+            }
+        }
+    };
+
+    static class SetWallpaperThread extends Thread {
+        private final Bitmap mBitmap;
+        private final Handler mHandler;
+        private final Context mContext;
+        private final File mFile;
+
+        public SetWallpaperThread(Bitmap bitmap, Handler handler,
+                                  Context context, File file) {
+            mBitmap = bitmap;
+            mHandler = handler;
+            mContext = context;
+            mFile = file;
+        }
+
+        @Override
+        public void run() {
+            try {
+                mContext.setWallpaper(mBitmap);
+            } catch (IOException e) {
+                Log.e(LOG_TAG, "Failed to set wallpaper.", e);
+            } finally {
+                mHandler.sendEmptyMessage(FINISH);
+                mFile.delete();
+            }
+        }
+    }
+
+    private synchronized void closeProgressDialog() {
+        if (mProgressDialog != null) {
+            mProgressDialog.dismiss();
+            mProgressDialog = null;
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        if (icicle != null) {
+            mDoLaunch = icicle.getBoolean(DO_LAUNCH_ICICLE);
+            mTempFile = new File(icicle.getString(TEMP_FILE_PATH_ICICLE));
+        }
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle icicle) {
+        icicle.putBoolean(DO_LAUNCH_ICICLE, mDoLaunch);
+        icicle.putString(TEMP_FILE_PATH_ICICLE, mTempFile.getAbsolutePath());
+    }
+
+    @Override
+    protected void onPause() {
+        closeProgressDialog();
+        super.onPause();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        if (!mDoLaunch) {
+            return;
+        }
+        Uri imageToUse = getIntent().getData();
+        if (imageToUse != null) {
+            Intent intent = new Intent();
+            intent.setClassName("com.cooliris.media",
+                                "com.cooliris.media.CropImage");
+            intent.setData(imageToUse);
+            formatIntent(intent);
+            startActivityForResult(intent, CROP_DONE);
+        } else {
+            Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+            intent.setType("image/*");
+            intent.putExtra("crop", "true");
+            formatIntent(intent);
+            startActivityForResult(intent, PHOTO_PICKED);
+        }
+    }
+
+    protected void formatIntent(Intent intent) {
+        // TODO: A temporary file is NOT necessary
+        // The CropImage intent should be able to set the wallpaper directly
+        // without writing to a file, which we then need to read here to write
+        // it again as the final wallpaper, this is silly
+        mTempFile = getFileStreamPath("temp-wallpaper");
+        mTempFile.getParentFile().mkdirs();
+
+        int width = getWallpaperDesiredMinimumWidth();
+        int height = getWallpaperDesiredMinimumHeight();
+        intent.putExtra("outputX",         width);
+        intent.putExtra("outputY",         height);
+        intent.putExtra("aspectX",         width);
+        intent.putExtra("aspectY",         height);
+        intent.putExtra("scale",           true);
+        intent.putExtra("noFaceDetection", true);
+        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mTempFile));
+        intent.putExtra("outputFormat", Bitmap.CompressFormat.PNG.name());
+        // TODO: we should have an extra called "setWallpaper" to ask CropImage
+        // to set the cropped image as a wallpaper directly. This means the
+        // SetWallpaperThread should be moved out of this class to CropImage
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode,
+                                    Intent data) {
+        if ((requestCode == PHOTO_PICKED || requestCode == CROP_DONE)
+                && (resultCode == RESULT_OK) && (data != null)) {
+            try {
+                InputStream s = new FileInputStream(mTempFile);
+                try {
+                    Bitmap bitmap = BitmapFactory.decodeStream(s);
+                    if (bitmap == null) {
+                        Log.e(LOG_TAG, "Failed to set wallpaper. "
+                                       + "Couldn't get bitmap for path "
+                                       + mTempFile);
+                    } else {
+                        mHandler.sendEmptyMessage(SHOW_PROGRESS);
+                        new SetWallpaperThread(
+                                bitmap, mHandler, this, mTempFile).start();
+                    }
+                    mDoLaunch = false;
+                } finally {
+                    Util.closeSilently(s);
+                }
+            } catch (FileNotFoundException ex) {
+                Log.e(LOG_TAG, "file not found: " + mTempFile, ex);
+            }
+        } else {
+            setResult(RESULT_CANCELED);
+            finish();
+        }
+    }
+}
diff --git a/src/com/cooliris/picasa/AlbumEntry.java b/src/com/cooliris/picasa/AlbumEntry.java
new file mode 100644
index 0000000..04091a3
--- /dev/null
+++ b/src/com/cooliris/picasa/AlbumEntry.java
@@ -0,0 +1,209 @@
+package com.cooliris.picasa;
+
+import org.xml.sax.Attributes;
+
+/**
+ * This class models the album entry kind in the Picasa GData API.
+ */
+@Entry.Table("albums")
+public final class AlbumEntry extends Entry {
+    public static final EntrySchema SCHEMA = new EntrySchema(AlbumEntry.class);
+
+    /**
+     * The user account that is the sync source for this entry. Must be set before insert/update.
+     */
+    @Column(Columns.SYNC_ACCOUNT)
+    public String syncAccount;
+    
+    /**
+     * The ETag for the album/photos GData feed.
+     */
+    @Column(Columns.PHOTOS_ETAG)
+	public String photosEtag = null;
+    
+    /**
+     * True if the contents of the album need to be synchronized. Must be set before insert/update.
+     */
+    @Column(Columns.PHOTOS_DIRTY)
+    public boolean photosDirty;
+    
+    /**
+     * The "edit" URI of the album.
+     */
+    @Column(Columns.EDIT_URI)
+    public String editUri;
+    
+    /**
+     * The album owner.
+     */
+    @Column(Columns.USER)
+    public String user;
+
+    /**
+     * The title of the album.
+     */
+    @Column(value=Columns.TITLE)
+    public String title;
+    
+    /**
+     * A short summary of the contents of the album.
+     */
+    @Column(value=Columns.SUMMARY)
+    public String summary;
+    
+    /**
+     * The date the album was created.
+     */
+    @Column(Columns.DATE_PUBLISHED)
+    public long datePublished;
+    
+    /**
+     * The date the album was last updated.
+     */
+    @Column(Columns.DATE_UPDATED)
+    public long dateUpdated;
+    
+    /**
+     * The date the album entry was last edited. May be more recent than dateUpdated.
+     */
+    @Column(Columns.DATE_EDITED)
+    public long dateEdited;
+    
+    /**
+     * The number of photos in the album.
+     */
+    @Column(Columns.NUM_PHOTOS)
+    public int numPhotos;
+    
+    /**
+     * The number of bytes of storage that this album uses.
+     */
+    @Column(Columns.BYTES_USED)
+    public long bytesUsed;
+    
+    /**
+     * The user-specified location associated with the album.
+     */
+    @Column(Columns.LOCATION_STRING)
+    public String locationString;
+    
+    /**
+     * The thumbnail URL associated with the album.
+     */
+    @Column(Columns.THUMBNAIL_URL)
+    public String thumbnailUrl;
+    
+    /**
+     * A link to the HTML page associated with the album.
+     */
+    @Column(Columns.HTML_PAGE_URL)
+    public String htmlPageUrl;
+        
+    /**
+     * Column names specific to album entries.
+     */
+    public static final class Columns extends PicasaApi.Columns {
+        public static final String PHOTOS_ETAG = "photos_etag";
+		public static final String USER = "user";
+        public static final String BYTES_USED = "bytes_used";
+        public static final String NUM_PHOTOS = "num_photos";
+        public static final String LOCATION_STRING = "location_string";
+        public static final String PHOTOS_DIRTY = "photos_dirty";
+    }
+    
+    /**
+     * Resets values to defaults for object reuse.
+     */
+    @Override
+    public void clear() {
+        super.clear();
+        syncAccount = null;
+        photosDirty = false;
+        editUri = null;
+        user = null;
+        title = null;
+        summary = null;
+        datePublished = 0;
+        dateUpdated = 0;
+        dateEdited = 0;
+        numPhotos = 0;
+        bytesUsed = 0;
+        locationString = null;
+        thumbnailUrl = null;
+        htmlPageUrl = null;
+    }
+    
+    /**
+     * Sets the property value corresponding to the given XML element, if applicable.
+     */
+    @Override
+    public void setPropertyFromXml(String uri, String localName, Attributes attrs, String content) {
+        char localNameChar = localName.charAt(0);
+        if (uri.equals(GDataParser.GPHOTO_NAMESPACE)) {
+            switch (localNameChar) {
+                case 'i':
+                    if (localName.equals("id")) {
+                        id = Long.parseLong(content);
+                    }
+                    break;
+                case 'u':
+                    if (localName.equals("user")) {
+                        user = content;
+                    }
+                    break;
+                case 'n':
+                    if (localName.equals("numphotos")) {
+                        numPhotos = Integer.parseInt(content);
+                    }
+                    break;
+                case 'b':
+                    if (localName.equals("bytesUsed")) {
+                        bytesUsed = Long.parseLong(content);
+                    }
+                    break;
+            }
+        } else if (uri.equals(GDataParser.ATOM_NAMESPACE)) {
+            switch (localNameChar) {
+                case 't':
+                    if (localName.equals("title")) {
+                        title = content;
+                    }
+                    break;
+                case 's':
+                    if (localName.equals("summary")) {
+                        summary = content;
+                    }
+                    break;
+                case 'p':
+                    if (localName.equals("published")) {
+                        datePublished = GDataParser.parseAtomTimestamp(content);
+                    }
+                    break;
+                case 'u':
+                    if (localName.equals("updated")) {
+                        dateUpdated = GDataParser.parseAtomTimestamp(content);
+                    }
+                    break;
+                case 'l':
+                    if (localName.equals("link")) {
+                        String rel = attrs.getValue("", "rel");
+                        String href = attrs.getValue("", "href");
+                        if (rel.equals("alternate") && attrs.getValue("", "type").equals("text/html")) {
+                            htmlPageUrl = href;
+                        } else if (rel.equals("edit")) {
+                            editUri = href;
+                        }
+                    }
+                    break;
+            }
+        } else if (uri.equals(GDataParser.APP_NAMESPACE)) {
+            if (localName.equals("edited")) {
+                dateEdited = GDataParser.parseAtomTimestamp(content);
+            }
+        } else if (uri.equals(GDataParser.MEDIA_RSS_NAMESPACE)) {
+            if (localName == "thumbnail") {
+                thumbnailUrl = attrs.getValue("", "url");
+            }
+        }
+    }
+}
diff --git a/src/com/cooliris/picasa/Entry.java b/src/com/cooliris/picasa/Entry.java
new file mode 100644
index 0000000..59ce7d7
--- /dev/null
+++ b/src/com/cooliris/picasa/Entry.java
@@ -0,0 +1,38 @@
+package com.cooliris.picasa;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.xml.sax.Attributes;
+
+public abstract class Entry {
+    public static final String[] ID_PROJECTION = {"_id"};
+    
+    // The primary key of the entry. 
+    @Column("_id")
+    public long id = 0;
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.TYPE)
+    public @interface Table {
+        String value();
+    }
+    
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.FIELD)
+    public @interface Column {
+        String value();
+        boolean indexed() default false;
+        boolean fullText() default false;
+    }
+
+    public void clear() {
+        id = 0;
+    }
+    
+    public void setPropertyFromXml(String uri, String localName, Attributes attrs, String content) {
+        throw new UnsupportedOperationException("Entry class does not support XML parsing");
+    }
+}
diff --git a/src/com/cooliris/picasa/EntrySchema.java b/src/com/cooliris/picasa/EntrySchema.java
new file mode 100644
index 0000000..02e5fde
--- /dev/null
+++ b/src/com/cooliris/picasa/EntrySchema.java
@@ -0,0 +1,419 @@
+package com.cooliris.picasa;
+
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+public final class EntrySchema {
+    public static final int TYPE_STRING = 0;
+    public static final int TYPE_BOOLEAN = 1;
+    public static final int TYPE_SHORT = 2;
+    public static final int TYPE_INT = 3;
+    public static final int TYPE_LONG = 4;
+    public static final int TYPE_FLOAT = 5;
+    public static final int TYPE_DOUBLE = 6;
+    public static final int TYPE_BLOB = 7;
+    public static final String SQLITE_TYPES[] = { "TEXT", "INTEGER", "INTEGER", "INTEGER",
+                                                 "INTEGER", "REAL", "REAL", "NONE" };
+
+    private static final String TAG = "SchemaInfo";
+    private static final String FULL_TEXT_INDEX_SUFFIX = "_fulltext";
+
+    private final String mTableName;
+    private final ColumnInfo[] mColumnInfo;
+    private final String[] mProjection;
+    private final boolean mHasFullTextIndex;
+
+    public EntrySchema(Class<? extends Entry> clazz) {
+        // Get table and column metadata from reflection.
+        ColumnInfo[] columns = parseColumnInfo(clazz);
+        mTableName = parseTableName(clazz);
+        mColumnInfo = columns;
+
+        // Cache the list of projection columns and check for full-text columns.
+        String[] projection = {};
+        boolean hasFullTextIndex = false;
+        if (columns != null) {
+            projection = new String[columns.length];
+            for (int i = 0; i != columns.length; ++i) {
+                ColumnInfo column = columns[i];
+                projection[i] = column.name;
+                if (column.fullText) {
+                    hasFullTextIndex = true;
+                }
+            }
+        }
+        mProjection = projection;
+        mHasFullTextIndex = hasFullTextIndex;
+    }
+
+    public String getTableName() {
+        return mTableName;
+    }
+
+    public ColumnInfo[] getColumnInfo() {
+        return mColumnInfo;
+    }
+
+    public String[] getProjection() {
+        return mProjection;
+    }
+
+    private void logExecSql(SQLiteDatabase db, String sql) {
+        //Log.i(TAG, sql);
+        db.execSQL(sql);
+    }
+
+    public void cursorToObject(Cursor cursor, Entry object) {
+        try {
+            ColumnInfo[] columns = mColumnInfo;
+            for (int i = 0, size = columns.length; i != size; ++i) {
+                ColumnInfo column = columns[i];
+                int columnIndex = column.projectionIndex;
+                Field field = column.field;
+                switch (column.type) {
+                    case TYPE_STRING:
+                        field.set(object, cursor.getString(columnIndex));
+                        break;
+                    case TYPE_BOOLEAN:
+                        field.setBoolean(object, cursor.getShort(columnIndex) == 1);
+                        break;
+                    case TYPE_SHORT:
+                        field.setShort(object, cursor.getShort(columnIndex));
+                        break;
+                    case TYPE_INT:
+                        field.setInt(object, cursor.getInt(columnIndex));
+                        break;
+                    case TYPE_LONG:
+                        field.setLong(object, cursor.getLong(columnIndex));
+                        break;
+                    case TYPE_FLOAT:
+                        field.setFloat(object, cursor.getFloat(columnIndex));
+                        break;
+                    case TYPE_DOUBLE:
+                        field.setDouble(object, cursor.getDouble(columnIndex));
+                        break;
+                    case TYPE_BLOB:
+                        field.set(object, cursor.getBlob(columnIndex));
+                        break;
+                }
+            }
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "SchemaInfo.setFromCursor: object not of the right type");
+        } catch (IllegalAccessException e) {
+            Log.e(TAG, "SchemaInfo.setFromCursor: field not accessible");
+        }
+    }
+
+    public void objectToValues(Entry object, ContentValues values) {
+        try {
+            ColumnInfo[] columns = mColumnInfo;
+            for (int i = 0, size = columns.length; i != size; ++i) {
+                ColumnInfo column = columns[i];
+                String columnName = column.name;
+                Field field = column.field;
+                switch (column.type) {
+                    case TYPE_STRING:
+                        values.put(columnName, (String)field.get(object));
+                        break;
+                    case TYPE_BOOLEAN:
+                        values.put(columnName, field.getBoolean(object));
+                        break;
+                    case TYPE_SHORT:
+                        values.put(columnName, field.getShort(object));
+                        break;
+                    case TYPE_INT:
+                        values.put(columnName, field.getInt(object));
+                        break;
+                    case TYPE_LONG:
+                        values.put(columnName, field.getLong(object));
+                        break;
+                    case TYPE_FLOAT:
+                        values.put(columnName, field.getFloat(object));
+                        break;
+                    case TYPE_DOUBLE:
+                        values.put(columnName, field.getDouble(object));
+                        break;
+                    case TYPE_BLOB:
+                        values.put(columnName, (byte[])field.get(object));
+                        break;
+                }
+            }
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "SchemaInfo.setFromCursor: object not of the right type");
+        } catch (IllegalAccessException e) {
+            Log.e(TAG, "SchemaInfo.setFromCursor: field not accessible");
+        }
+    }
+
+    public Cursor queryAll(SQLiteDatabase db) {
+        return db.query(mTableName, mProjection, null, null, null, null, null);
+    }
+
+    public boolean queryWithId(SQLiteDatabase db, long id, Entry entry) {
+        Cursor cursor = db.query(mTableName, mProjection, "_id=?",
+                                 new String[] { Long.toString(id) }, null, null, null);
+        boolean success = false;
+        if (cursor.moveToFirst()) {
+            cursorToObject(cursor, entry);
+            success = true;
+        }
+        cursor.close();
+        return success;
+    }
+
+    public long insertOrReplace(SQLiteDatabase db, Entry entry) {
+        ContentValues values = new ContentValues();
+        objectToValues(entry, values);
+        if (entry.id == 0) {
+            Log.i(TAG, "removing id before insert");
+            values.remove("_id");
+        }
+        long id = db.replace(mTableName, "_id", values);
+        entry.id = id;
+        return id;
+    }
+
+    public boolean deleteWithId(SQLiteDatabase db, long id) {
+        return db.delete(mTableName, "_id=?", new String[] { Long.toString(id) }) == 1;
+    }
+
+    public void createTables(SQLiteDatabase db) {
+        // Wrapped class must have a @Table.Definition.
+        String tableName = mTableName;
+        if (tableName == null) {
+            return;
+        }
+
+        // Add the CREATE TABLE statement for the main table.
+        StringBuilder sql = new StringBuilder("CREATE TABLE ");
+        sql.append(tableName);
+        sql.append(" (_id INTEGER PRIMARY KEY");
+        ColumnInfo[] columns = mColumnInfo;
+        int numColumns = columns.length;
+        for (int i = 0; i != numColumns; ++i) {
+            ColumnInfo column = columns[i];
+            if (!column.isId()) {
+                sql.append(',');
+                sql.append(column.name);
+                sql.append(' ');
+                sql.append(SQLITE_TYPES[column.type]);
+                if (column.extraSql != null) {
+                    sql.append(' ');
+                    sql.append(column.extraSql);
+                }
+            }
+        }
+        sql.append(");");
+        logExecSql(db, sql.toString());
+        sql.setLength(0);
+
+        // Create indexes for all indexed columns.
+        for (int i = 0; i != numColumns; ++i) {
+            // Create an index on the indexed columns.
+            ColumnInfo column = columns[i];
+            if (column.indexed) {
+                sql.append("CREATE INDEX ");
+                sql.append(tableName);
+                sql.append("_index_");
+                sql.append(column.name);
+                sql.append(" ON ");
+                sql.append(tableName);
+                sql.append(" (");
+                sql.append(column.name);
+                sql.append(");");
+                logExecSql(db, sql.toString());
+                sql.setLength(0);
+            }
+        }
+
+        if (mHasFullTextIndex) {
+            // Add an FTS virtual table if using full-text search.
+            String ftsTableName = tableName + FULL_TEXT_INDEX_SUFFIX;
+            sql.append("CREATE VIRTUAL TABLE ");
+            sql.append(ftsTableName);
+            sql.append(" USING FTS3 (_id INTEGER PRIMARY KEY");
+            for (int i = 0; i != numColumns; ++i) {
+                ColumnInfo column = columns[i];
+                if (column.fullText) {
+                    // Add the column to the FTS table.
+                    String columnName = column.name;
+                    sql.append(',');
+                    sql.append(columnName);
+                    sql.append(" TEXT");
+                }
+            }
+            sql.append(");");
+            logExecSql(db, sql.toString());
+            sql.setLength(0);
+
+            // Build an insert statement that will automatically keep the FTS table in sync.
+            StringBuilder insertSql = new StringBuilder("INSERT OR REPLACE INTO ");
+            insertSql.append(ftsTableName);
+            insertSql.append(" (_id");
+            for (int i = 0; i != numColumns; ++i) {
+                ColumnInfo column = columns[i];
+                if (column.fullText) {
+                    insertSql.append(',');
+                    insertSql.append(column.name);
+                }
+            }
+            insertSql.append(") VALUES (new._id");
+            for (int i = 0; i != numColumns; ++i) {
+                ColumnInfo column = columns[i];
+                if (column.fullText) {
+                    insertSql.append(",new.");
+                    insertSql.append(column.name);
+                }
+            }
+            insertSql.append(");");
+            String insertSqlString = insertSql.toString();
+
+            // Add an insert trigger.
+            sql.append("CREATE TRIGGER ");
+            sql.append(tableName);
+            sql.append("_insert_trigger AFTER INSERT ON ");
+            sql.append(tableName);
+            sql.append(" FOR EACH ROW BEGIN ");
+            sql.append(insertSqlString);
+            sql.append("END;");
+            logExecSql(db, sql.toString());
+            sql.setLength(0);
+
+            // Add an update trigger.
+            sql.append("CREATE TRIGGER ");
+            sql.append(tableName);
+            sql.append("_update_trigger AFTER UPDATE ON ");
+            sql.append(tableName);
+            sql.append(" FOR EACH ROW BEGIN ");
+            sql.append(insertSqlString);
+            sql.append("END;");
+            logExecSql(db, sql.toString());
+            sql.setLength(0);
+
+            // Add a delete trigger.
+            sql.append("CREATE TRIGGER ");
+            sql.append(tableName);
+            sql.append("_delete_trigger AFTER DELETE ON ");
+            sql.append(tableName);
+            sql.append(" FOR EACH ROW BEGIN DELETE FROM ");
+            sql.append(ftsTableName);
+            sql.append(" WHERE _id = old._id; END;");
+            logExecSql(db, sql.toString());
+            sql.setLength(0);
+        }
+    }
+
+    public void dropTables(SQLiteDatabase db) {
+        String tableName = mTableName;
+        StringBuilder sql = new StringBuilder("DROP TABLE IF EXISTS ");
+        sql.append(tableName);
+        sql.append(';');
+        logExecSql(db, sql.toString());
+        sql.setLength(0);
+
+        if (mHasFullTextIndex) {
+            sql.append("DROP TABLE IF EXISTS ");
+            sql.append(tableName);
+            sql.append(FULL_TEXT_INDEX_SUFFIX);
+            sql.append(';');
+            logExecSql(db, sql.toString());
+        }
+
+    }
+
+    public void deleteAll(SQLiteDatabase db) {
+        StringBuilder sql = new StringBuilder("DELETE FROM ");
+        sql.append(mTableName);
+        sql.append(";");
+        logExecSql(db, sql.toString());
+    }
+
+    private String parseTableName(Class<? extends Object> clazz) {
+        // Check for a table annotation.
+        Entry.Table table = clazz.getAnnotation(Entry.Table.class);
+        if (table == null) {
+            return null;
+        }
+
+        // Return the table name.
+        return table.value();
+    }
+
+    private ColumnInfo[] parseColumnInfo(Class<? extends Object> clazz) {
+        // Gather metadata from each annotated field.
+        ArrayList<ColumnInfo> columns = new ArrayList<ColumnInfo>();
+        Field[] fields = clazz.getFields();
+        for (int i = 0; i != fields.length; ++i) {
+            // Get column metadata from the annotation.
+            Field field = fields[i];
+            Entry.Column info = ((AnnotatedElement)field).getAnnotation(Entry.Column.class);
+            if (info == null) {
+                continue;
+            }
+
+            // Determine the field type.
+            int type;
+            Class<?> fieldType = field.getType();
+            if (fieldType == String.class) {
+                type = TYPE_STRING;
+            } else if (fieldType == boolean.class) {
+                type = TYPE_BOOLEAN;
+            } else if (fieldType == short.class) {
+                type = TYPE_SHORT;
+            } else if (fieldType == int.class) {
+                type = TYPE_INT;
+            } else if (fieldType == long.class) {
+                type = TYPE_LONG;
+            } else if (fieldType == float.class) {
+                type = TYPE_FLOAT;
+            } else if (fieldType == double.class) {
+                type = TYPE_DOUBLE;
+            } else if (fieldType == byte[].class) {
+                type = TYPE_BLOB;
+            } else {
+                throw new IllegalArgumentException("Unsupported field type for column: " +
+                                                   fieldType.getName());
+            }
+
+            // Add the column to the array.
+            int index = columns.size();
+            columns.add(new ColumnInfo(info.value(), type, info.indexed(), info.fullText(), field,
+                                       index));
+        }
+
+        // Return a list.
+        ColumnInfo[] columnList = new ColumnInfo[columns.size()];
+        columns.toArray(columnList);
+        return columnList;
+    }
+
+    public static final class ColumnInfo {
+        public final String name;
+        public final int type;
+        public final boolean indexed;
+        public final boolean fullText;
+        public final String extraSql = "";
+        public final Field field;
+        public final int projectionIndex;
+
+        public ColumnInfo(String name, int type, boolean indexed, boolean fullText, Field field,
+                          int projectionIndex) {
+            this.name = name.toLowerCase();
+            this.type = type;
+            this.indexed = indexed;
+            this.fullText = fullText;
+            this.field = field;
+            this.projectionIndex = projectionIndex;
+        }
+
+        public boolean isId() {
+            return name == "_id";
+        }
+    }
+}
diff --git a/src/com/cooliris/picasa/GDataClient.java b/src/com/cooliris/picasa/GDataClient.java
new file mode 100644
index 0000000..d07d12d
--- /dev/null
+++ b/src/com/cooliris/picasa/GDataClient.java
@@ -0,0 +1,197 @@
+package com.cooliris.picasa;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.params.HttpClientParams;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+public final class GDataClient {
+    private static final String TAG = "GDataClient";
+    private static final String USER_AGENT = "Cooliris-GData/1.0; gzip";
+    private static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";
+    private static final String IF_MATCH = "If-Match";
+    private static final int CONNECTION_TIMEOUT = 20000; // ms.
+    private static final int MIN_GZIP_SIZE = 512;
+    public static final HttpParams HTTP_PARAMS;
+    public static final ThreadSafeClientConnManager HTTP_CONNECTION_MANAGER;
+
+    private final DefaultHttpClient mHttpClient = new DefaultHttpClient(HTTP_CONNECTION_MANAGER,
+                                                                        HTTP_PARAMS);
+    private String mAuthToken;
+
+    static {
+        // Prepare HTTP parameters.
+        HttpParams params = new BasicHttpParams();
+        HttpConnectionParams.setStaleCheckingEnabled(params, false);
+        HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
+        HttpConnectionParams.setSoTimeout(params, CONNECTION_TIMEOUT);
+        HttpClientParams.setRedirecting(params, true);
+        HttpProtocolParams.setUserAgent(params, USER_AGENT);
+        HTTP_PARAMS = params;
+
+        // Register HTTP protocol.
+        SchemeRegistry schemeRegistry = new SchemeRegistry();
+        schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
+
+        // Create the connection manager.
+        HTTP_CONNECTION_MANAGER = new ThreadSafeClientConnManager(params, schemeRegistry);
+    }
+
+    public static final class Operation {
+        public String inOutEtag;
+        public int outStatus;
+        public InputStream outBody;
+    }
+
+    public void setAuthToken(String authToken) {
+        mAuthToken = authToken;
+    }
+    
+    public void get(String feedUrl, Operation operation) throws IOException {
+        callMethod(new HttpGet(feedUrl), operation);
+    }
+
+    public void post(String feedUrl, byte[] data, String contentType, Operation operation)
+            throws IOException {
+        ByteArrayEntity entity = getCompressedEntity(data);
+        entity.setContentType(contentType);
+        HttpPost post = new HttpPost(feedUrl);
+        post.setEntity(entity);
+        callMethod(post, operation);
+    }
+
+    public void put(String feedUrl, byte[] data, String contentType, Operation operation)
+            throws IOException {
+        ByteArrayEntity entity = getCompressedEntity(data);
+        entity.setContentType(contentType);
+        HttpPost post = new HttpPost(feedUrl);
+        post.setHeader(X_HTTP_METHOD_OVERRIDE, "PUT");
+        post.setEntity(entity);
+        callMethod(post, operation);
+    }
+
+    public void putStream(String feedUrl, InputStream stream, String contentType,
+                          Operation operation) throws IOException {
+        InputStreamEntity entity = new InputStreamEntity(stream, -1);
+        entity.setContentType(contentType);
+        HttpPost post = new HttpPost(feedUrl);
+        post.setHeader(X_HTTP_METHOD_OVERRIDE, "PUT");
+        post.setEntity(entity);
+        callMethod(post, operation);
+    }
+
+    public void delete(String feedUrl, Operation operation) throws IOException {
+        HttpPost post = new HttpPost(feedUrl);
+        String etag = operation.inOutEtag;
+        post.setHeader(X_HTTP_METHOD_OVERRIDE, "DELETE");
+        post.setHeader(IF_MATCH, etag != null ? etag : "*");
+        callMethod(post, operation);
+    }
+
+    private void callMethod(HttpUriRequest request, Operation operation)
+            throws IOException {
+        // Specify GData protocol version 2.0.
+        request.addHeader("GData-Version", "2");
+
+        // Indicate support for gzip-compressed responses.
+        request.addHeader("Accept-Encoding", "gzip");
+
+        // Specify authorization token if provided.
+        String authToken = mAuthToken;
+        if (!TextUtils.isEmpty(authToken)) {
+            request.addHeader("Authorization", "GoogleLogin auth=" + authToken);
+        }
+
+        // Specify the ETag of a prior response, if available.
+        String etag = operation.inOutEtag;
+        if (etag != null) {
+            request.addHeader("If-None-Match", etag);
+        }
+
+        // Execute the HTTP request.
+        HttpResponse httpResponse = null;
+        try {
+            httpResponse = mHttpClient.execute(request);
+        } catch (IOException e) {
+            Log.w(TAG, "Request failed: " + request.getURI());
+            throw e;
+        }
+
+        // Get the status code and response body.
+        int status = httpResponse.getStatusLine().getStatusCode();
+        InputStream stream = null;
+        HttpEntity entity = httpResponse.getEntity();
+        if (entity != null) {
+            // Wrap the entity input stream in a GZIP decoder if necessary.
+            stream = entity.getContent();
+            if (stream != null) {
+                Header header = entity.getContentEncoding();
+                if (header != null) {
+                    if (header.getValue().contains("gzip")) {
+                        stream = new GZIPInputStream(stream);
+                    }
+                }
+            }
+        }
+        
+        // Return the stream if successful.
+        Header etagHeader = httpResponse.getFirstHeader("ETag");
+        operation.outStatus = status;
+        operation.inOutEtag = etagHeader != null ? etagHeader.getValue() : null;
+        operation.outBody = stream;
+    }
+
+    private ByteArrayEntity getCompressedEntity(byte[] data) throws IOException {
+        ByteArrayEntity entity;
+        if (data.length >= MIN_GZIP_SIZE) {
+            ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(data.length / 2);
+            GZIPOutputStream gzipOutput = new GZIPOutputStream(byteOutput);
+            gzipOutput.write(data);
+            gzipOutput.close();
+            entity = new ByteArrayEntity(byteOutput.toByteArray());
+        } else {
+            entity = new ByteArrayEntity(data);
+        }
+        return entity;
+    }
+
+    public static String inputStreamToString(InputStream stream) {
+        if (stream != null) {
+            try {
+                ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+                byte[] buffer = new byte[4096];
+                int bytesRead;
+                while ((bytesRead = stream.read(buffer)) != -1) {
+                    bytes.write(buffer, 0, bytesRead);
+                }
+                return new String(bytes.toString());
+            } catch (IOException e) {
+                // Fall through and return null.
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/com/cooliris/picasa/GDataParser.java b/src/com/cooliris/picasa/GDataParser.java
new file mode 100644
index 0000000..47132bc
--- /dev/null
+++ b/src/com/cooliris/picasa/GDataParser.java
@@ -0,0 +1,158 @@
+package com.cooliris.picasa;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+import android.text.format.Time;
+
+public final class GDataParser implements ContentHandler {
+    public static final String APP_NAMESPACE = "http://www.w3.org/2007/app";
+    public static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom";
+    public static final String GD_NAMESPACE = "http://schemas.google.com/g/2005";
+    public static final String GPHOTO_NAMESPACE = "http://schemas.google.com/photos/2007";
+    public static final String MEDIA_RSS_NAMESPACE = "http://search.yahoo.com/mrss/";
+    public static final String GML_NAMESPACE = "http://www.opengis.net/gml";
+    private static final String FEED_ELEMENT = "feed";
+    private static final String ENTRY_ELEMENT = "entry";
+  
+    private static final int STATE_DOCUMENT = 0;
+    private static final int STATE_FEED = 1;
+    private static final int STATE_ENTRY = 2;
+    
+    private static final int NUM_LEVELS = 5;
+    
+    private Entry mEntry = null;
+    private EntryHandler mHandler = null;
+    private int mState = STATE_DOCUMENT;
+    private int mLevel = 0;
+    private String[] mUri = new String[NUM_LEVELS];
+    private String[] mName = new String[NUM_LEVELS];
+    private AttributesImpl[] mAttributes = new AttributesImpl[NUM_LEVELS];
+    private final StringBuilder mValue = new StringBuilder(128);
+    
+    public interface EntryHandler {
+        void handleEntry(Entry entry);
+    }
+    
+    public GDataParser() {
+        AttributesImpl[] attributes = mAttributes;
+        for (int i = 0; i != NUM_LEVELS; ++i) {
+            attributes[i] = new AttributesImpl();
+        }
+    }
+    
+    public void setEntry(Entry entry) {
+        mEntry = entry;
+    }
+    
+    public void setHandler(EntryHandler handler) {
+        mHandler = handler;
+    }
+    
+    public static long parseAtomTimestamp(String timestamp) {
+        Time time = new Time();
+        time.parse3339(timestamp);
+        return time.toMillis(true);
+    }
+    
+    public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException {
+        switch (mState) {
+            case STATE_DOCUMENT:
+                // Expect an atom:feed element.
+                if (uri.equals(ATOM_NAMESPACE) && localName.equals(FEED_ELEMENT)) {
+                    mState = STATE_FEED;
+                } else {
+                    throw new SAXException();
+                }
+                break;
+            case STATE_FEED:
+                // Expect a feed property element or an atom:entry element.
+                if (uri.equals(ATOM_NAMESPACE) && localName.equals(ENTRY_ELEMENT)) {
+                    mState = STATE_ENTRY;
+                    mEntry.clear();
+                } else {
+                    startProperty(uri, localName, attrs);
+                }
+                break;
+            case STATE_ENTRY:
+                startProperty(uri, localName, attrs);
+                break;
+        }
+    }
+    
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        if (mLevel > 0) {
+            // Handle property exit.
+            endProperty();
+        } else {        
+            // Handle state exit.
+            switch (mState) {
+                case STATE_DOCUMENT:
+                    throw new SAXException();
+                case STATE_FEED:
+                    mState = STATE_DOCUMENT;
+                    break;
+                case STATE_ENTRY:
+                    mState = STATE_FEED;
+                    mHandler.handleEntry(mEntry);
+                    break;
+            }
+        }
+    }
+    
+    private void startProperty(String uri, String localName, Attributes attrs) {
+        // Push element information onto the property stack.
+        int level = mLevel + 1;
+        mLevel = level;
+        mValue.setLength(0);
+        mUri[level] = uri;
+        mName[level] = localName;
+        mAttributes[level].setAttributes(attrs);
+    }
+
+    private void endProperty() {
+        // Apply property to the entry, then pop the stack.
+        int level = mLevel;
+        mEntry.setPropertyFromXml(mUri[level], mName[level], mAttributes[level], mValue.toString());
+        mLevel = level - 1;
+    }
+
+    public void characters(char[] ch, int start, int length) throws SAXException {
+        mValue.append(ch, start, length);
+    }
+
+    public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
+        // Ignored.
+    }
+
+    public void processingInstruction(String target, String data) throws SAXException {
+        // Ignored.
+    }
+
+    public void setDocumentLocator(Locator locator) {
+        // Ignored.
+    }
+
+    public void skippedEntity(String name) throws SAXException {
+        // Ignored. 
+    }
+
+    public void startDocument() throws SAXException {
+        // Ignored.
+    }
+
+    public void endDocument() throws SAXException {
+        // Ignored.
+    }
+
+    public void startPrefixMapping(String prefix, String uri) throws SAXException {
+        // Ignored.
+    }
+    
+    public void endPrefixMapping(String prefix) throws SAXException {
+        // Ignored.
+    }
+}
diff --git a/src/com/cooliris/picasa/PhotoEntry.java b/src/com/cooliris/picasa/PhotoEntry.java
new file mode 100644
index 0000000..99c53b8
--- /dev/null
+++ b/src/com/cooliris/picasa/PhotoEntry.java
@@ -0,0 +1,303 @@
+package com.cooliris.picasa;
+
+import org.xml.sax.Attributes;
+
+/**
+ * This class models the photo entry kind in the Picasa GData API.
+ */
+@Entry.Table("photos")
+public final class PhotoEntry extends Entry {
+    public static final EntrySchema SCHEMA = new EntrySchema(PhotoEntry.class);
+
+    /**
+     * The user account that is the sync source for this entry. Must be set before insert/update.
+     */
+    @Column("sync_account")
+    public String syncAccount;
+
+    /**
+     * The "edit" URI of the photo.
+     */
+    @Column("edit_uri")
+    public String editUri;
+
+    /**
+     * The containing album ID.
+     */
+    @Column(value = "album_id", indexed = true)
+    public long albumId;
+
+    /**
+     * The display index of the photo within the album. Must be set before insert/update.
+     */
+    @Column(value = "display_index", indexed = true)
+    public int displayIndex;
+
+    /**
+     * The title of the photo.
+     */
+    @Column("title")
+    public String title;
+
+    /**
+     * A short summary of the photo.
+     */
+    @Column("summary")
+    public String summary;
+
+    /**
+     * The date the photo was added.
+     */
+    @Column("date_published")
+    public long datePublished;
+
+    /**
+     * The date the photo was last updated.
+     */
+    @Column("date_updated")
+    public long dateUpdated;
+
+    /**
+     * The date the photo entry was last edited. May be more recent than dateUpdated.
+     */
+    @Column("date_edited")
+    public long dateEdited;
+
+    /**
+     * The date the photo was captured as specified in the EXIF data.
+     */
+    @Column("date_taken")
+    public long dateTaken;
+
+    /**
+     * The number of comments associated with the photo.
+     */
+    @Column("comment_count")
+    public int commentCount;
+
+    /**
+     * The width of the photo in pixels.
+     */
+    @Column("width")
+    public int width;
+
+    /**
+     * The height of the photo in pixels.
+     */
+    @Column("height")
+    public int height;
+
+    /**
+     * The rotation of the photo in degrees, if rotation has not already been applied.
+     */
+    @Column("rotation")
+    public int rotation;
+
+    /**
+     * The size of the photo is bytes.
+     */
+    @Column("size")
+    public int size;
+
+    /**
+     * The latitude associated with the photo.
+     */
+    @Column("latitude")
+    public double latitude;
+
+    /**
+     * The longitude associated with the photo.
+     */
+    @Column("longitude")
+    public double longitude;
+
+    /**
+     * The "mini-thumbnail" URL for the photo (currently 144px-cropped).
+     */
+    @Column("thumbnail_url")
+    public String thumbnailUrl;
+
+    /**
+     * The "screennail" URL for the photo (currently 800px).
+     */
+    @Column("screennail_url")
+    public String screennailUrl;
+
+    /**
+     * The "content" URL for the photo (currently 1280px, or a video). The original image URL is not fetched since "imgmax" accepts
+     * one size, used to get this resource.
+     */
+    @Column("content_url")
+    public String contentUrl;
+
+    /**
+     * The MIME type of the content URL.
+     */
+    @Column("content_type")
+    public String contentType;
+
+    /**
+     * A link to the HTML page associated with the album.
+     */
+    @Column("html_page_url")
+    public String htmlPageUrl;
+
+    /**
+     * Resets values to defaults for object reuse.
+     */
+    @Override
+    public void clear() {
+        super.clear();
+        syncAccount = null;
+        editUri = null;
+        albumId = 0;
+        displayIndex = 0;
+        title = null;
+        summary = null;
+        datePublished = 0;
+        dateUpdated = 0;
+        dateEdited = 0;
+        dateTaken = 0;
+        commentCount = 0;
+        width = 0;
+        height = 0;
+        rotation = 0;
+        size = 0;
+        latitude = 0;
+        longitude = 0;
+        thumbnailUrl = null;
+        screennailUrl = null;
+        contentUrl = null;
+        contentType = null;
+        htmlPageUrl = null;
+    }
+
+    /**
+     * Sets the property value corresponding to the given XML element, if applicable.
+     */
+    @Override
+    public void setPropertyFromXml(String uri, String localName, Attributes attrs, String content) {
+        try {
+            char localNameChar = localName.charAt(0);
+            if (uri.equals(GDataParser.GPHOTO_NAMESPACE)) {
+                switch (localNameChar) {
+                case 'i':
+                    if (localName.equals("id")) {
+                        id = Long.parseLong(content);
+                    }
+                    break;
+                case 'a':
+                    if (localName.equals("albumid")) {
+                        albumId = Long.parseLong(content);
+                    }
+                    break;
+                case 't':
+                    if (localName.equals("timestamp")) {
+                        dateTaken = Long.parseLong(content);
+                    }
+                    break;
+                case 'c':
+                    if (localName.equals("commentCount")) {
+                        commentCount = Integer.parseInt(content);
+                    }
+                    break;
+                case 'w':
+                    if (localName.equals("width")) {
+                        width = Integer.parseInt(content);
+                    }
+                    break;
+                case 'h':
+                    if (localName.equals("height")) {
+                        height = Integer.parseInt(content);
+                    }
+                    break;
+                case 'r':
+                    if (localName.equals("rotation")) {
+                        rotation = Integer.parseInt(content);
+                    }
+                    break;
+                case 's':
+                    if (localName.equals("size")) {
+                        size = Integer.parseInt(content);
+                    }
+                    break;
+                case 'l':
+                    if (localName.equals("latitude")) {
+                        latitude = Double.parseDouble(content);
+                    } else if (localName.equals("longitude")) {
+                        longitude = Double.parseDouble(content);
+                    }
+                    break;
+                }
+            } else if (uri.equals(GDataParser.ATOM_NAMESPACE)) {
+                switch (localNameChar) {
+                case 't':
+                    if (localName.equals("title")) {
+                        title = content;
+                    }
+                    break;
+                case 's':
+                    if (localName.equals("summary")) {
+                        summary = content;
+                    }
+                    break;
+                case 'p':
+                    if (localName.equals("published")) {
+                        datePublished = GDataParser.parseAtomTimestamp(content);
+                    }
+                    break;
+                case 'u':
+                    if (localName.equals("updated")) {
+                        dateUpdated = GDataParser.parseAtomTimestamp(content);
+                    }
+                    break;
+                case 'l':
+                    if (localName.equals("link")) {
+                        String rel = attrs.getValue("", "rel");
+                        String href = attrs.getValue("", "href");
+                        if (rel.equals("alternate") && attrs.getValue("", "type").equals("text/html")) {
+                            htmlPageUrl = href;
+                        } else if (rel.equals("edit")) {
+                            editUri = href;
+                        }
+                    }
+                    break;
+                }
+            } else if (uri.equals(GDataParser.APP_NAMESPACE)) {
+                if (localName.equals("edited")) {
+                    dateEdited = GDataParser.parseAtomTimestamp(content);
+                }
+            } else if (uri.equals(GDataParser.MEDIA_RSS_NAMESPACE)) {
+                if (localName.equals("thumbnail")) {
+                    int width = Integer.parseInt(attrs.getValue("", "width"));
+                    int height = Integer.parseInt(attrs.getValue("", "height"));
+                    int dimension = Math.max(width, height);
+                    String url = attrs.getValue("", "url");
+                    if (dimension <= 300) {
+                        thumbnailUrl = url;
+                    } else {
+                        screennailUrl = url;
+                    }
+                } else if (localName.equals("content")) {
+                    // Only replace an existing URL if the MIME type is video.
+                    String type = attrs.getValue("", "type");
+                    if (contentUrl == null || type.startsWith("video/")) {
+                        contentUrl = attrs.getValue("", "url");
+                        contentType = type;
+                    }
+                }
+            } else if (uri.equals(GDataParser.GML_NAMESPACE)) {
+                if (localName.equals("pos")) {
+                    int spaceIndex = content.indexOf(' ');
+                    if (spaceIndex != -1) {
+                        latitude = Double.parseDouble(content.substring(0, spaceIndex));
+                        longitude = Double.parseDouble(content.substring(spaceIndex + 1));
+                    }
+                }
+            }
+        } catch (Exception e) {
+            return;
+        }
+    }
+
+}
diff --git a/src/com/cooliris/picasa/PicasaApi.java b/src/com/cooliris/picasa/PicasaApi.java
new file mode 100644
index 0000000..9e3fd23
--- /dev/null
+++ b/src/com/cooliris/picasa/PicasaApi.java
@@ -0,0 +1,298 @@
+package com.cooliris.picasa;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+import org.apache.http.HttpStatus;
+import org.xml.sax.SAXException;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.app.Activity;
+import android.content.Context;
+import android.content.SyncResult;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Xml;
+
+public final class PicasaApi {
+    public static final int RESULT_OK = 0;
+    public static final int RESULT_NOT_MODIFIED = 1;
+    public static final int RESULT_ERROR = 2;
+
+    private static final String TAG = "PicasaAPI";
+    private static final String BASE_URL = "http://picasaweb.google.com/data/feed/api/";
+    private static final String BASE_QUERY_STRING;
+
+    static {
+        // Build the base query string using screen dimensions.
+        DisplayMetrics metrics = new DisplayMetrics();
+        int maxDimension = Math.max(metrics.widthPixels, metrics.heightPixels);
+        StringBuilder query = new StringBuilder("?imgmax=1024&max-results=1000&thumbsize=");
+        String thumbnailSize = metrics.density <= 1 ? "144u," : "144u,";
+        String screennailSize = maxDimension <= 512 ? "512u" : "800u";
+        query.append(thumbnailSize);
+        query.append(screennailSize);
+        BASE_QUERY_STRING = query.toString() + "&visibility=visible";
+    }
+
+    private final GDataClient mClient;
+    private final GDataClient.Operation mOperation = new GDataClient.Operation();
+    private final GDataParser mParser = new GDataParser();
+    private final AlbumEntry mAlbumInstance = new AlbumEntry();
+    private final PhotoEntry mPhotoInstance = new PhotoEntry();
+    private AuthAccount mAuth;
+
+    public static final class AuthAccount {
+        public final String user;
+        public final String authToken;
+        public final Account account;
+
+        public AuthAccount(String user, String authToken, Account account) {
+            this.user = user;
+            this.authToken = authToken;
+            this.account = account;
+        }
+    }
+
+    public static Account[] getAccounts(Context context) {
+        // Return the list of accounts supporting the Picasa GData service.
+        AccountManager accountManager = AccountManager.get(context);
+        Account[] accounts = {};
+        try {
+            accounts = accountManager.getAccountsByTypeAndFeatures(PicasaService.ACCOUNT_TYPE,
+                    new String[] { PicasaService.FEATURE_SERVICE_NAME }, null, null).getResult();
+        } catch (OperationCanceledException e) {
+        } catch (AuthenticatorException e) {
+        } catch (IOException e) {
+        }
+        return accounts;
+    }
+
+    public static AuthAccount[] getAuthenticatedAccounts(Context context) {
+        AccountManager accountManager = AccountManager.get(context);
+        Account[] accounts = getAccounts(context);
+        int numAccounts = accounts.length;
+
+        ArrayList<AuthAccount> authAccounts = new ArrayList<AuthAccount>(numAccounts);
+        for (int i = 0; i != numAccounts; ++i) {
+            Account account = accounts[i];
+            String authToken;
+            try {
+                // Get the token without user interaction.
+                authToken = accountManager.blockingGetAuthToken(account, PicasaService.SERVICE_NAME, true);
+
+                // TODO: Remove this once the build is signed by Google, since we will always have permission.
+                // This code requests permission from the user explicitly.
+                if (context instanceof Activity) {
+                    Bundle bundle = accountManager.getAuthToken(account, PicasaService.SERVICE_NAME, null, (Activity) context,
+                            null, null).getResult();
+                    authToken = bundle.getString("authtoken");
+                    PicasaService.requestSync(context, PicasaService.TYPE_USERS_ALBUMS, -1);
+                }
+
+                // Add the account information to the list of accounts.
+                if (authToken != null) {
+                    String username = account.name;
+                    if (username.contains("@gmail.") || username.contains("@googlemail.")) {
+                        // Strip the domain from GMail accounts for canonicalization. TODO: is there an official way?
+                        username = username.substring(0, username.indexOf('@'));
+                    }
+                    authAccounts.add(new AuthAccount(username, authToken, account));
+                }
+            } catch (OperationCanceledException e) {
+            } catch (IOException e) {
+            } catch (AuthenticatorException e) {
+            }
+        }
+        AuthAccount[] authArray = new AuthAccount[authAccounts.size()];
+        authAccounts.toArray(authArray);
+        return authArray;
+    }
+
+    public PicasaApi() {
+        mClient = new GDataClient();
+    }
+
+    public void setAuth(AuthAccount auth) {
+        mAuth = auth;
+        synchronized (mClient) {
+            mClient.setAuthToken(auth.authToken);
+        }
+    }
+
+    public int getAlbums(AccountManager accountManager, SyncResult syncResult, UserEntry user, GDataParser.EntryHandler handler) {
+        // Construct the query URL for user albums.
+        StringBuilder builder = new StringBuilder(BASE_URL);
+        builder.append("user/");
+        builder.append(Uri.encode(mAuth.user));
+        builder.append(BASE_QUERY_STRING);
+        builder.append("&kind=album");
+        Log.i(TAG, "getAlbums uri " + builder.toString());
+
+        try {
+            // Send the request.
+            synchronized (mOperation) {
+                GDataClient.Operation operation = mOperation;
+                operation.inOutEtag = user.albumsEtag;
+                boolean retry = false;
+                int numRetries = 1;
+                do {
+                    retry = false;
+                    synchronized (mClient) {
+                        mClient.get(builder.toString(), operation);
+                    }
+                    switch (operation.outStatus) {
+                    case HttpStatus.SC_OK:
+                        break;
+                    case HttpStatus.SC_NOT_MODIFIED:
+                        return RESULT_NOT_MODIFIED;
+                    case HttpStatus.SC_FORBIDDEN:
+                    case HttpStatus.SC_UNAUTHORIZED:
+                        if (!retry) {
+                            accountManager.invalidateAuthToken(PicasaService.ACCOUNT_TYPE, mAuth.authToken);
+                            retry = true;
+                        }
+                        if (numRetries == 0) {
+                            ++syncResult.stats.numAuthExceptions;
+                        }
+                    default:
+                        Log.e(TAG, "getAlbums: unexpected status code " + operation.outStatus + " data: "
+                                + operation.outBody.toString());
+                        ++syncResult.stats.numIoExceptions;
+                        return RESULT_ERROR;
+                    }
+                    --numRetries;
+                } while (retry && numRetries >= 0);
+
+                // Store the new ETag for the user/albums feed.
+                user.albumsEtag = operation.inOutEtag;
+
+                // Parse the response.
+                synchronized (mParser) {
+                    GDataParser parser = mParser;
+                    parser.setEntry(mAlbumInstance);
+                    parser.setHandler(handler);
+                    Xml.parse(operation.outBody, Xml.Encoding.UTF_8, parser);
+                }
+            }
+            return RESULT_OK;
+        } catch (IOException e) {
+            Log.e(TAG, "getAlbums: " + e);
+            ++syncResult.stats.numIoExceptions;
+        } catch (SAXException e) {
+            Log.e(TAG, "getAlbums: " + e);
+            ++syncResult.stats.numParseExceptions;
+        }
+        return RESULT_ERROR;
+    }
+
+    public int getAlbumPhotos(AccountManager accountManager, SyncResult syncResult, AlbumEntry album, GDataParser.EntryHandler handler) {
+        // Construct the query URL for user albums.
+        StringBuilder builder = new StringBuilder(BASE_URL);
+        builder.append("user/");
+        builder.append(Uri.encode(mAuth.user));
+        builder.append("/albumid/");
+        builder.append(album.id);
+        builder.append(BASE_QUERY_STRING);
+        builder.append("&kind=photo");
+        try {
+            // Send the request.
+            synchronized (mOperation) {
+                GDataClient.Operation operation = mOperation;
+                operation.inOutEtag = album.photosEtag;
+                boolean retry = false;
+                int numRetries = 1;
+                do {
+                    retry = false;
+                    synchronized (mClient) {
+                        mClient.get(builder.toString(), operation);
+                    }
+                    switch (operation.outStatus) {
+                    case HttpStatus.SC_OK:
+                        break;
+                    case HttpStatus.SC_NOT_MODIFIED:
+                        return RESULT_NOT_MODIFIED;
+                    case HttpStatus.SC_FORBIDDEN:
+                    case HttpStatus.SC_UNAUTHORIZED:
+                        // We need to reset the authtoken and retry only once.
+                        if (!retry) {
+                            retry = true;
+                            accountManager.invalidateAuthToken(PicasaService.SERVICE_NAME, mAuth.authToken);
+                        }
+                        if (numRetries == 0) {
+                            ++syncResult.stats.numAuthExceptions;
+                        }
+                        break;
+                    default:
+                        Log.e(TAG, "getAlbumPhotos: " + builder.toString() + ", unexpected status code " + operation.outStatus);
+                        ++syncResult.stats.numIoExceptions;
+                        return RESULT_ERROR;
+                    }
+                    --numRetries;
+                } while (retry && numRetries >= 0);
+
+                // Store the new ETag for the album/photos feed.
+                album.photosEtag = operation.inOutEtag;
+
+                // Parse the response.
+                synchronized (mParser) {
+                    GDataParser parser = mParser;
+                    parser.setEntry(mPhotoInstance);
+                    parser.setHandler(handler);
+                    Xml.parse(operation.outBody, Xml.Encoding.UTF_8, parser);
+                }
+            }
+            return RESULT_OK;
+        } catch (IOException e) {
+            Log.e(TAG, "getAlbumPhotos: " + e);
+            ++syncResult.stats.numIoExceptions;
+            e.printStackTrace();
+        } catch (SAXException e) {
+            Log.e(TAG, "getAlbumPhotos: " + e);
+            ++syncResult.stats.numParseExceptions;
+            e.printStackTrace();
+        }
+        return RESULT_ERROR;
+    }
+
+    public int deleteEntry(String editUri) {
+        try {
+            synchronized (mOperation) {
+                GDataClient.Operation operation = mOperation;
+                operation.inOutEtag = null;
+                synchronized (mClient) {
+                    mClient.delete(editUri, operation);
+                }
+                if (operation.outStatus == 200) {
+                    return RESULT_OK;
+                } else {
+                    Log.e(TAG, "deleteEntry: failed with status code " + operation.outStatus);
+                }
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "deleteEntry: " + e);
+        }
+        return RESULT_ERROR;
+    }
+
+    /**
+     * Column names shared by multiple entry kinds.
+     */
+    public static class Columns {
+        public static final String _ID = "_id";
+        public static final String SYNC_ACCOUNT = "sync_account";
+        public static final String EDIT_URI = "edit_uri";
+        public static final String TITLE = "title";
+        public static final String SUMMARY = "summary";
+        public static final String DATE_PUBLISHED = "date_published";
+        public static final String DATE_UPDATED = "date_updated";
+        public static final String DATE_EDITED = "date_edited";
+        public static final String THUMBNAIL_URL = "thumbnail_url";
+        public static final String HTML_PAGE_URL = "html_page_url";
+    }
+}
diff --git a/src/com/cooliris/picasa/PicasaContentProvider.java b/src/com/cooliris/picasa/PicasaContentProvider.java
new file mode 100644
index 0000000..7ef74bc
--- /dev/null
+++ b/src/com/cooliris/picasa/PicasaContentProvider.java
@@ -0,0 +1,567 @@
+package com.cooliris.picasa;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SyncResult;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.util.Log;
+
+public final class PicasaContentProvider extends TableContentProvider {
+    public static final String AUTHORITY = "com.cooliris.picasa.contentprovider";
+    public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
+    public static final Uri PHOTOS_URI = Uri.withAppendedPath(BASE_URI, "photos");
+    public static final Uri ALBUMS_URI = Uri.withAppendedPath(BASE_URI, "albums");
+
+    private static final String TAG = "PicasaContentProvider";
+    private static final String[] ID_EDITED_PROJECTION = { "_id", "date_edited" };
+    private static final String[] ID_EDITED_INDEX_PROJECTION = { "_id", "date_edited", "display_index" };
+    private static final String WHERE_ACCOUNT = "sync_account=?";
+    private static final String WHERE_ALBUM_ID = "album_id=?";
+
+    private final PhotoEntry mPhotoInstance = new PhotoEntry();
+    private final AlbumEntry mAlbumInstance = new AlbumEntry();
+    private SyncContext mSyncContext = null;
+    private Account mActiveAccount;
+    
+    @Override
+    public void attachInfo(Context context, ProviderInfo info) {
+        // Initialize the provider and set the database.
+        super.attachInfo(context, info);
+        setDatabase(new Database(context));
+
+        // Add mappings for each of the exposed tables.
+        addMapping(AUTHORITY, "photos", "vnd.cooliris.picasa.photo", PhotoEntry.SCHEMA);
+        addMapping(AUTHORITY, "albums", "vnd.cooliris.picasa.album", AlbumEntry.SCHEMA);
+
+        // Create the sync context.
+        mSyncContext = new SyncContext();
+    }
+
+    public static final class Database extends SQLiteOpenHelper {
+        public static final String DATABASE_NAME = "picasa.db";
+        public static final int DATABASE_VERSION = 83;
+        public Database(Context context) {
+            super(context, DATABASE_NAME, null, DATABASE_VERSION);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            PhotoEntry.SCHEMA.createTables(db);
+            AlbumEntry.SCHEMA.createTables(db);
+            UserEntry.SCHEMA.createTables(db);
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            // No new versions yet, if we are asked to upgrade we just reset everything.
+            PhotoEntry.SCHEMA.dropTables(db);
+            AlbumEntry.SCHEMA.dropTables(db);
+            UserEntry.SCHEMA.dropTables(db);
+            onCreate(db);
+        }
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        // Ensure that the URI is well-formed. We currently do not allow WHERE clauses.
+        List<String> path = uri.getPathSegments();
+        if (path.size() != 2 || !uri.getAuthority().equals(AUTHORITY) || selection != null) {
+            return 0;
+        }
+
+        // Get the sync context.
+        SyncContext context = mSyncContext;
+
+        // Determine if the URI refers to an album or photo.
+        String type = path.get(0);
+        long id = Long.parseLong(path.get(1));
+        SQLiteDatabase db = context.db;
+        if (type.equals("photos")) {
+            // Retrieve the photo from the database to get the edit URI.
+            PhotoEntry photo = mPhotoInstance;
+            if (PhotoEntry.SCHEMA.queryWithId(db, id, photo)) {
+                // Send a DELETE request to the API.
+                if (context.login(photo.syncAccount)) {
+                    if (context.api.deleteEntry(photo.editUri) == PicasaApi.RESULT_OK) {
+                        deletePhoto(db, id);
+                        context.photosChanged = true;
+                        return 1;
+                    }
+                }
+            }
+        } else if (type.equals("albums")) {
+            // Retrieve the album from the database to get the edit URI.
+            AlbumEntry album = mAlbumInstance;
+            if (AlbumEntry.SCHEMA.queryWithId(db, id, album)) {
+                // Send a DELETE request to the API.
+                if (context.login(album.syncAccount)) {
+                    if (context.api.deleteEntry(album.editUri) == PicasaApi.RESULT_OK) {
+                        deleteAlbum(db, id);
+                        context.albumsChanged = true;
+                        return 1;
+                    }
+                }
+            }
+        }
+        context.finish();
+        return 0;
+    }
+    
+    public void reloadAccounts() {
+    	mSyncContext.reloadAccounts();
+    }
+ 
+    public void setActiveSyncAccount(Account account) {
+        mActiveAccount = account;
+    }
+    
+    public void syncUsers(SyncResult syncResult) {
+    	syncUsers(mSyncContext, syncResult);
+    }
+
+    public void syncUsersAndAlbums(final boolean syncAlbumPhotos, SyncResult syncResult) {
+        SyncContext context = mSyncContext;
+        
+        // Synchronize users authenticated on the device.
+        UserEntry[] users = syncUsers(context, syncResult);
+        
+        // Synchronize albums for each user.
+        String activeUsername = null;
+        if (mActiveAccount != null) {
+            String username = mActiveAccount.name;
+            if (username.contains("@gmail.")) {
+                username = username.substring(0, username.indexOf('@'));
+            }
+            activeUsername = username;
+        }
+        boolean didSyncActiveUserName = false;
+        for (int i = 0, numUsers = users.length; i != numUsers; ++i) {
+            if (activeUsername != null && !context.accounts[i].user.equals(activeUsername))
+                continue;
+            if (!ContentResolver.getSyncAutomatically(context.accounts[i].account, AUTHORITY)) continue;
+            didSyncActiveUserName = true;
+            context.api.setAuth(context.accounts[i]);
+            syncUserAlbums(context, users[i], syncResult);
+            if (syncAlbumPhotos) {
+                syncUserPhotos(context, users[i].account, syncResult);
+            } else {
+                // // Always sync added albums.
+                // for (Long albumId : context.albumsAdded) {
+                // syncAlbumPhotos(albumId, false);
+                // }
+            }
+        }
+        if (!didSyncActiveUserName) {
+            ++syncResult.stats.numAuthExceptions;
+        }
+        context.finish();
+    }
+
+    public void syncAlbumPhotos(final long albumId, final boolean forceRefresh, SyncResult syncResult) {
+        SyncContext context = mSyncContext;
+        AlbumEntry album = new AlbumEntry();
+        if (AlbumEntry.SCHEMA.queryWithId(context.db, albumId, album)) {
+            if ((album.photosDirty || forceRefresh) && context.login(album.syncAccount)) {
+                if (isSyncEnabled(album.syncAccount, context)) {
+                    syncAlbumPhotos(context, album.syncAccount, album, syncResult);
+                }
+            }
+        }
+        context.finish();
+    }
+    
+    public static boolean isSyncEnabled(String accountName, SyncContext context) {
+        PicasaApi.AuthAccount[] accounts = context.accounts;
+        int numAccounts = accounts.length;
+        for (int i = 0; i < numAccounts; ++i) {
+            PicasaApi.AuthAccount account = accounts[i];
+            if (account.user.equals(accountName)) {
+                return ContentResolver.getSyncAutomatically(account.account, AUTHORITY);
+            }
+        }
+        return true;
+    }
+
+    private UserEntry[] syncUsers(SyncContext context, SyncResult syncResult) {
+        Log.i(TAG, "syncUsers");
+
+        // Get authorized accounts.
+        context.reloadAccounts();
+        PicasaApi.AuthAccount[] accounts = context.accounts;
+        int numUsers = accounts.length;
+        UserEntry[] users = new UserEntry[numUsers];
+
+        // Scan existing accounts.
+        EntrySchema schema = UserEntry.SCHEMA;
+        SQLiteDatabase db = context.db;
+        Cursor cursor = schema.queryAll(db);
+        Log.i(TAG, "#users: " + cursor.getCount());
+        if (cursor.moveToFirst()) {
+            do {
+                // Read the current account.
+                UserEntry entry = new UserEntry();
+                schema.cursorToObject(cursor, entry);
+
+                // Find the corresponding account, or delete the row if it does not exist.
+                int i;
+                for (i = 0; i != numUsers; ++i) {
+                    Log.i(TAG, "Check " + accounts[i].user + " == " + entry.account);
+                    if (accounts[i].user.equals(entry.account)) {
+                        users[i] = entry;
+                        Log.e(TAG, "Updating user " + entry.account);
+                        break;
+                    }
+                }
+                if (i == numUsers) {
+                    Log.e(TAG, "Deleting user " + entry.account);
+                    entry.albumsEtag = null;
+                    deleteUser(db, entry.account);
+                }
+            } while (cursor.moveToNext());
+        } else {
+            // Log.i(TAG, "No users in database yet");
+        }
+        cursor.close();
+
+        // Add new accounts and synchronize user albums if recursive.
+        for (int i = 0; i != numUsers; ++i) {
+            UserEntry entry = users[i];
+            PicasaApi.AuthAccount account = accounts[i];
+            if (entry == null) {
+                entry = new UserEntry();
+                entry.account = account.user;
+                Log.d(TAG, "insert/replace: " + schema.insertOrReplace(db, entry));
+                users[i] = entry;
+                Log.e(TAG, "Inserting user " + entry.account);
+            }
+        }
+        return users;
+    }
+
+    private void syncUserAlbums(final SyncContext context, final UserEntry user, final SyncResult syncResult) {
+        // Query existing album entry (id, dateEdited) sorted by ID.
+        final SQLiteDatabase db = context.db;
+        Cursor cursor = db.query(AlbumEntry.SCHEMA.getTableName(), ID_EDITED_PROJECTION, WHERE_ACCOUNT,
+                new String[] { user.account }, null, null, AlbumEntry.Columns.DATE_EDITED);
+        int localCount = cursor.getCount();
+
+        // Build a sorted index with existing entry timestamps.
+        final EntryMetadata local[] = new EntryMetadata[localCount];
+        for (int i = 0; i != localCount; ++i) {
+            cursor.moveToPosition(i); // TODO: throw exception here if returns false?
+            local[i] = new EntryMetadata(cursor.getLong(0), cursor.getLong(1), 0);
+        }
+        cursor.close();
+        Arrays.sort(local);
+
+        // Merge the truth from the API into the local database.
+        final EntrySchema albumSchema = AlbumEntry.SCHEMA;
+        final EntryMetadata key = new EntryMetadata();
+        final AccountManager accountManager = AccountManager.get(getContext());
+        int result = context.api.getAlbums(accountManager, syncResult, user, new GDataParser.EntryHandler() {
+            public void handleEntry(Entry entry) {
+                AlbumEntry album = (AlbumEntry) entry;
+                long albumId = album.id;
+                key.id = albumId;
+                int index = Arrays.binarySearch(local, key);
+                EntryMetadata metadata = index >= 0 ? local[index] : null;
+                if (metadata == null || metadata.dateEdited < album.dateEdited) {
+                    // Insert / update.
+                    Log.i(TAG, "insert / update album " + album.title);
+                    album.syncAccount = user.account;
+                    album.photosDirty = true;
+                    albumSchema.insertOrReplace(db, album);
+                    if (metadata == null) {
+                        context.albumsAdded.add(albumId);
+                    }
+                    ++syncResult.stats.numUpdates;
+                } else {
+                    // Up-to-date.
+                    // Log.i(TAG, "up-to-date album " + album.title);
+                }
+
+                // Mark item as surviving so it is not deleted.
+                if (metadata != null) {
+                    metadata.survived = true;
+                }
+            }
+        });
+
+        // Return if not modified or on error.
+        switch (result) {
+        case PicasaApi.RESULT_ERROR:
+            ++syncResult.stats.numParseExceptions;
+        case PicasaApi.RESULT_NOT_MODIFIED:
+            return;
+        }
+
+        // Update the user entry with the new ETag.
+        UserEntry.SCHEMA.insertOrReplace(db, user);
+
+        // Delete all entries not present in the API response.
+        for (int i = 0; i != localCount; ++i) {
+            EntryMetadata metadata = local[i];
+            if (!metadata.survived) {
+                deleteAlbum(db, metadata.id);
+                ++syncResult.stats.numDeletes;
+                Log.i(TAG, "delete album " + metadata.id);
+            }
+        }
+
+        // Note that albums changed.
+        context.albumsChanged = true;
+    }
+
+    private void syncUserPhotos(SyncContext context, String account, SyncResult syncResult) {
+        // Synchronize albums with out-of-date photos.
+        SQLiteDatabase db = context.db;
+        Cursor cursor = db.query(AlbumEntry.SCHEMA.getTableName(), Entry.ID_PROJECTION, "sync_account=? AND photos_dirty=1",
+                new String[] { account }, null, null, null);
+        AlbumEntry album = new AlbumEntry();
+        for (int i = 0, count = cursor.getCount(); i != count; ++i) {
+            cursor.moveToPosition(i);
+            if (AlbumEntry.SCHEMA.queryWithId(db, cursor.getLong(0), album)) {
+                syncAlbumPhotos(context, account, album, syncResult);
+            }
+            
+            // Abort if interrupted.
+            if (Thread.interrupted()) {
+                ++syncResult.stats.numIoExceptions;
+                Log.e(TAG, "syncUserPhotos interrupted");
+            }
+        }
+        cursor.close();
+    }
+
+    private void syncAlbumPhotos(SyncContext context, final String account, AlbumEntry album, final SyncResult syncResult) {
+    	Log.i(TAG, "Syncing Picasa album: " + album.title);
+    	
+        // Query existing album entry (id, dateEdited) sorted by ID.
+        final SQLiteDatabase db = context.db;
+        long albumId = album.id;
+        String[] albumIdArgs = { Long.toString(albumId) };
+        Cursor cursor = db.query(PhotoEntry.SCHEMA.getTableName(), ID_EDITED_INDEX_PROJECTION, WHERE_ALBUM_ID, albumIdArgs, null,
+                null, "date_edited");
+        int localCount = cursor.getCount();
+
+        // Build a sorted index with existing entry timestamps and display indexes.
+        final EntryMetadata local[] = new EntryMetadata[localCount];
+        final EntryMetadata key = new EntryMetadata();
+        for (int i = 0; i != localCount; ++i) {
+            cursor.moveToPosition(i); // TODO: throw exception here if returns false?
+            local[i] = new EntryMetadata(cursor.getLong(0), cursor.getLong(1), cursor.getInt(2));
+        }
+        cursor.close();
+        Arrays.sort(local);
+
+        // Merge the truth from the API into the local database.
+        final EntrySchema photoSchema = PhotoEntry.SCHEMA;
+        final int[] displayIndex = { 0 };
+        final AccountManager accountManager = AccountManager.get(getContext());
+        int result = context.api.getAlbumPhotos(accountManager, syncResult, album, new GDataParser.EntryHandler() {
+            public void handleEntry(Entry entry) {
+                PhotoEntry photo = (PhotoEntry) entry;
+                long photoId = photo.id;
+                int newDisplayIndex = displayIndex[0];
+                key.id = photoId;
+                int index = Arrays.binarySearch(local, key);
+                EntryMetadata metadata = index >= 0 ? local[index] : null;
+                if (metadata == null || metadata.dateEdited < photo.dateEdited || metadata.displayIndex != newDisplayIndex) {
+
+                    // Insert / update.
+                    // Log.i(TAG, "insert / update photo " + photo.title);
+                    photo.syncAccount = account;
+                    photo.displayIndex = newDisplayIndex;
+                    photoSchema.insertOrReplace(db, photo);
+                    ++syncResult.stats.numUpdates;
+                } else {
+                    // Up-to-date.
+                    // Log.i(TAG, "up-to-date photo " + photo.title);
+                }
+
+                // Mark item as surviving so it is not deleted.
+                if (metadata != null) {
+                    metadata.survived = true;
+                }
+
+                // Increment the display index.
+                displayIndex[0] = newDisplayIndex + 1;
+            }
+        });
+
+        // Return if not modified or on error.
+        switch (result) {
+        case PicasaApi.RESULT_ERROR:
+            ++syncResult.stats.numParseExceptions;
+            Log.e(TAG, "syncAlbumPhotos error");
+        case PicasaApi.RESULT_NOT_MODIFIED:
+            // Log.e(TAG, "result not modified");
+            return;
+        }
+
+        // Delete all entries not present in the API response.
+        for (int i = 0; i != localCount; ++i) {
+            EntryMetadata metadata = local[i];
+            if (!metadata.survived) {
+                deletePhoto(db, metadata.id);
+                ++syncResult.stats.numDeletes;
+                // Log.i(TAG, "delete photo " + metadata.id);
+            }
+        }
+
+        // Mark album as no longer dirty and store the new ETag.
+        album.photosDirty = false;
+        AlbumEntry.SCHEMA.insertOrReplace(db, album);
+        // Log.i(TAG, "Clearing dirty bit on album " + albumId);
+
+        // Mark that photos changed.
+        // context.photosChanged = true;
+        getContext().getContentResolver().notifyChange(ALBUMS_URI, null);
+        getContext().getContentResolver().notifyChange(PHOTOS_URI, null);
+    }
+
+    private void deleteUser(SQLiteDatabase db, String account) {
+        Log.w(TAG, "deleteUser(" + account + ")");
+
+        // Select albums owned by the user.
+        String albumTableName = AlbumEntry.SCHEMA.getTableName();
+        String[] whereArgs = { account };
+        Cursor cursor = db.query(AlbumEntry.SCHEMA.getTableName(), Entry.ID_PROJECTION, WHERE_ACCOUNT, whereArgs, null, null, null);
+
+        // Delete contained photos for each album.
+        if (cursor.moveToFirst()) {
+            do {
+                deleteAlbumPhotos(db, cursor.getLong(0));
+            } while (cursor.moveToNext());
+        }
+        cursor.close();
+
+        // Delete all albums.
+        db.delete(albumTableName, WHERE_ACCOUNT, whereArgs);
+        
+        // Delete the user entry.
+        db.delete(UserEntry.SCHEMA.getTableName(), "account=?", whereArgs);
+    }
+
+    private void deleteAlbum(SQLiteDatabase db, long albumId) {
+        // Delete contained photos.
+        deleteAlbumPhotos(db, albumId);
+
+        // Delete the album.
+        AlbumEntry.SCHEMA.deleteWithId(db, albumId);
+    }
+
+    private void deleteAlbumPhotos(SQLiteDatabase db, long albumId) {
+        Log.w(TAG, "deleteAlbumPhotos(" + albumId + ")");
+        String photoTableName = PhotoEntry.SCHEMA.getTableName();
+        String[] whereArgs = { Long.toString(albumId) };
+        Cursor cursor = db.query(photoTableName, Entry.ID_PROJECTION, WHERE_ALBUM_ID, whereArgs, null, null, null);
+
+        // Delete cache entry for each photo.
+        if (cursor.moveToFirst()) {
+            do {
+                deletePhotoCache(cursor.getLong(0));
+            } while (cursor.moveToNext());
+        }
+        cursor.close();
+
+        // Delete all photos.
+        db.delete(photoTableName, WHERE_ALBUM_ID, whereArgs);
+    }
+
+    private void deletePhoto(SQLiteDatabase db, long photoId) {
+        PhotoEntry.SCHEMA.deleteWithId(db, photoId);
+        deletePhotoCache(photoId);
+    }
+
+    private void deletePhotoCache(long photoId) {
+        // TODO
+        Log.w(TAG, "deletePhotoCache(" + photoId + ")");
+    }
+
+    private final class SyncContext {
+        // List of all authenticated user accounts.
+        public PicasaApi.AuthAccount[] accounts;
+
+        // A connection to the Picasa API for a specific user account. Initially null.
+        public PicasaApi api = new PicasaApi();
+
+        // A handle to the Picasa databse.
+        public SQLiteDatabase db;
+
+        // List of album IDs that were added during the sync.
+        public final ArrayList<Long> albumsAdded = new ArrayList<Long>();
+
+        // Set to true if albums were changed.
+        public boolean albumsChanged = false;
+
+        // Set to true if photos were changed.
+        public boolean photosChanged = false;
+
+        public SyncContext() {
+            db = mDatabase.getWritableDatabase();
+            reloadAccounts();
+        }
+        
+        public void reloadAccounts() {
+        	accounts = PicasaApi.getAuthenticatedAccounts(getContext());
+        }
+        
+        public void finish() {
+            // Send notifications if needed and reset state.
+            ContentResolver cr = getContext().getContentResolver();
+            if (albumsChanged) {
+                cr.notifyChange(ALBUMS_URI, null);
+            }
+            if (photosChanged) {
+                cr.notifyChange(PHOTOS_URI, null);
+            }
+            albumsChanged = false;
+            photosChanged = false;
+        }
+
+        public boolean login(String user) {
+            for (PicasaApi.AuthAccount auth : accounts) {
+                if (auth.user.equals(user)) {
+                    api.setAuth(auth);
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Minimal metadata gathered during sync.
+     */
+    private static final class EntryMetadata implements Comparable<EntryMetadata> {
+        public long id;
+        public long dateEdited;
+        public int displayIndex;
+        public boolean survived = false;
+
+        public EntryMetadata() {
+        }
+
+        public EntryMetadata(long id, long dateEdited, int displayIndex) {
+            this.id = id;
+            this.dateEdited = dateEdited;
+            this.displayIndex = displayIndex;
+        }
+
+        public int compareTo(EntryMetadata other) {
+            return Long.signum(id - other.id);
+        }
+
+    }
+}
diff --git a/src/com/cooliris/picasa/PicasaReceiver.java b/src/com/cooliris/picasa/PicasaReceiver.java
new file mode 100644
index 0000000..55905fd
--- /dev/null
+++ b/src/com/cooliris/picasa/PicasaReceiver.java
@@ -0,0 +1,17 @@
+package com.cooliris.picasa;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class PicasaReceiver extends BroadcastReceiver {
+
+	private static final String TAG = "PicasaRecevier";
+
+	@Override
+	public void onReceive(Context context, Intent intent) {
+	    Log.e(TAG, "Accounts changed: " + intent);
+	}
+
+}
diff --git a/src/com/cooliris/picasa/PicasaService.java b/src/com/cooliris/picasa/PicasaService.java
new file mode 100644
index 0000000..05c3345
--- /dev/null
+++ b/src/com/cooliris/picasa/PicasaService.java
@@ -0,0 +1,181 @@
+package com.cooliris.picasa;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.app.Service;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Process;
+
+public final class PicasaService extends Service {
+    public static final String ACTION_SYNC = "com.cooliris.picasa.action.SYNC";
+    public static final String ACTION_PERIODIC_SYNC = "com.cooliris.picasa.action.PERIODIC_SYNC";
+    public static final String ACCOUNT_TYPE = "com.google";
+    public static final String SERVICE_NAME = "lh2";
+    public static final String FEATURE_SERVICE_NAME = "service_" + SERVICE_NAME;
+    public static final String KEY_TYPE = "com.cooliris.SYNC_TYPE";
+    public static final String KEY_ID = "com.cooliris.SYNC_ID";
+    public static final int TYPE_USERS = 0;
+    public static final int TYPE_USERS_ALBUMS = 1;
+    public static final int TYPE_ALBUM_PHOTOS = 2;
+
+    private final HandlerThread mSyncThread = new HandlerThread("PicasaSyncThread");
+    private final Handler mSyncHandler;
+    private static final AtomicBoolean sSyncPending = new AtomicBoolean(false);
+
+    public static void requestSync(Context context, int type, long id) {
+    	Bundle extras = new Bundle();
+    	extras.putInt(KEY_TYPE, type);
+    	extras.putLong(KEY_ID, id);
+    	
+    	Account[] accounts = PicasaApi.getAccounts(context);
+    	for (Account account : accounts) {
+    	    ContentResolver.requestSync(account, PicasaContentProvider.AUTHORITY, extras);
+    	}
+    	
+    	//context.startService(new Intent(context, PicasaService.class).putExtras(extras));
+    }
+
+    public PicasaService() {
+        super();
+        mSyncThread.start();
+        mSyncHandler = new Handler(mSyncThread.getLooper());
+        mSyncHandler.post(new Runnable() {
+        	public void run() {
+        		Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+        	}
+        });
+    }
+
+    private static PicasaContentProvider getContentProvider(Context context) {
+        ContentResolver cr = context.getContentResolver();
+        ContentProviderClient client = cr.acquireContentProviderClient(PicasaContentProvider.AUTHORITY);
+        return (PicasaContentProvider) client.getLocalContentProvider();
+    }
+    
+    @Override
+    public int onStartCommand(final Intent intent, int flags, final int startId) {
+		mSyncHandler.post(new Runnable() {
+			public void run() {
+				performSync(PicasaService.this, null, intent.getExtras(), new SyncResult());
+				stopSelf(startId);
+			}
+		});
+		return START_NOT_STICKY;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return new PicasaSyncAdapter(getApplicationContext()).getSyncAdapterBinder();
+    }
+    
+    @Override
+    public void onDestroy() {
+    	mSyncThread.quit();
+    }
+    
+    public static boolean performSync(Context context, Account account, Bundle extras, SyncResult syncResult) {
+    	// Skip if another sync is pending.
+    	if (!sSyncPending.compareAndSet(false, true)) {
+    		return false;
+		}
+    	
+    	// Perform the sync.
+    	performSyncImpl(context, account, extras, syncResult);
+    	
+    	// Mark sync as complete and notify all waiters.
+    	sSyncPending.set(false);
+    	synchronized (sSyncPending) {
+    	    sSyncPending.notifyAll();
+    	}
+    	return true;
+    }
+    
+    public static void waitForPerformSync() {
+        synchronized (sSyncPending) {
+            while (sSyncPending.get()) {
+                try {
+                    // Wait for the sync to complete.
+                    sSyncPending.wait();
+                } catch (InterruptedException e) {
+                    // Stop waiting if interrupted.
+                    break;
+                }
+            }
+        }
+    }
+    
+    private static void performSyncImpl(Context context, Account account, Bundle extras, SyncResult syncResult) {
+    	// Initialize newly added accounts to sync by default.
+    	String authority = PicasaContentProvider.AUTHORITY;
+    	if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) {
+	    	if (account != null && ContentResolver.getIsSyncable(account, authority) < 0) {
+	    		try {
+	    			ContentResolver.setIsSyncable(account, authority, getIsSyncable(context, account) ? 1 : 0);
+	    		} catch (OperationCanceledException e) {
+	    		} catch (IOException e) {
+	    		}
+	    	}
+	    	return;
+    	}
+    	
+    	// Do nothing if sync is disabled for this account. TODO: is this blocked in PicasaContentProvider too?
+    	if (account != null && ContentResolver.getIsSyncable(account, authority) < 0) {
+    	    ++syncResult.stats.numSkippedEntries;
+    		return;
+    	}
+	    	
+    	// Get the type of sync operation and the entity ID, if applicable. Default to synchronize all.
+    	int type = extras.getInt(PicasaService.KEY_TYPE, PicasaService.TYPE_USERS_ALBUMS);
+    	long id = extras.getLong(PicasaService.KEY_ID, -1);
+    	
+    	// Get the content provider instance and reload the list of user accounts.
+    	PicasaContentProvider provider = getContentProvider(context);
+    	provider.reloadAccounts();
+    	
+    	// Restrict sync to either a specific account or all accounts.
+    	provider.setActiveSyncAccount(account);
+    	
+    	// Perform the desired sync operation.
+    	switch (type) {
+    	case PicasaService.TYPE_USERS:
+    		provider.syncUsers(syncResult);
+    		break;
+    	case PicasaService.TYPE_USERS_ALBUMS:
+    		provider.syncUsersAndAlbums(true, syncResult);
+    		break;
+    	case PicasaService.TYPE_ALBUM_PHOTOS:
+    		provider.syncAlbumPhotos(id, true, syncResult);
+    		break;
+    	default:
+    		throw new IllegalArgumentException();
+    	}
+    }
+    
+    private static boolean getIsSyncable(Context context, Account account) throws IOException, OperationCanceledException {
+        try {
+            Account[] picasaAccounts = AccountManager.get(context).getAccountsByTypeAndFeatures(ACCOUNT_TYPE,
+                    new String[] { FEATURE_SERVICE_NAME }, null /* callback */, null /* handler */).getResult();
+            for (Account picasaAccount : picasaAccounts) {
+                if (account.equals(picasaAccount)) {
+                    return true;
+                }
+            }
+            return false;
+        } catch (AuthenticatorException e) {
+            throw new IOException(e.getMessage());
+        }
+    }
+}
diff --git a/src/com/cooliris/picasa/PicasaSyncAdapter.java b/src/com/cooliris/picasa/PicasaSyncAdapter.java
new file mode 100644
index 0000000..8bdfaad
--- /dev/null
+++ b/src/com/cooliris/picasa/PicasaSyncAdapter.java
@@ -0,0 +1,36 @@
+package com.cooliris.picasa;
+
+import android.accounts.Account;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.BroadcastReceiver;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.os.Bundle;
+import android.util.Log;
+
+public class PicasaSyncAdapter extends AbstractThreadedSyncAdapter {
+    private final Context mContext;
+    public final static String TAG = "PicasaSyncAdapter";
+
+    public PicasaSyncAdapter(Context applicationContext) {
+        super(applicationContext, false);
+        mContext = applicationContext;
+    }
+
+    @Override
+    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient providerClient,
+            SyncResult syncResult) {
+        PicasaService.performSync(mContext, account, extras, syncResult);
+        Log.i(TAG, "SyncResult: " + syncResult.toDebugString());
+    }
+
+    public static final class AccountChangeReceiver extends BroadcastReceiver {
+		@Override
+		public void onReceive(Context context, Intent intent) {
+			// TODO: Need to get account list change broadcast.
+		}
+    	
+    }
+}
diff --git a/src/com/cooliris/picasa/TableContentProvider.java b/src/com/cooliris/picasa/TableContentProvider.java
new file mode 100644
index 0000000..32b4b8c
--- /dev/null
+++ b/src/com/cooliris/picasa/TableContentProvider.java
@@ -0,0 +1,202 @@
+package com.cooliris.picasa;
+
+import java.util.ArrayList;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.text.TextUtils;
+
+public class TableContentProvider extends ContentProvider {
+    private static final String NULL_COLUMN_HACK = "_id";
+    protected SQLiteOpenHelper mDatabase = null;
+    private final UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+    private final ArrayList<Mapping> mMappings = new ArrayList<Mapping>();
+
+    public void setDatabase(SQLiteOpenHelper database) {
+        mDatabase = database;
+    }
+    
+    public void addMapping(String authority, String path, String mimeSubtype, EntrySchema table) {
+        // Add the table URI mapping.
+        ArrayList<Mapping> mappings = mMappings;
+        UriMatcher matcher = mUriMatcher;
+        matcher.addURI(authority, path, mappings.size());
+        mappings.add(new Mapping(table, mimeSubtype, false));
+        
+        // Add the row URI mapping.
+        matcher.addURI(authority, path + "/#", mappings.size());
+        mappings.add(new Mapping(table, mimeSubtype, true));
+    }
+    
+    @Override
+    public boolean onCreate() {
+        // The database may not be loaded yet since attachInfo() has not been called, so we cannot
+        // check that the database opened successfully. Returns true optimistically.
+        return true;
+    }
+    
+    @Override
+    public String getType(Uri uri) {
+        // Resolve the URI.
+        int match = mUriMatcher.match(uri);
+        if (match == UriMatcher.NO_MATCH) {
+            throw new IllegalArgumentException("Invalid URI: " + uri);
+        }
+
+        // Combine the standard type with the user-provided subtype.
+        Mapping mapping = mMappings.get(match);
+        String prefix = mapping.hasId ? ContentResolver.CURSOR_ITEM_BASE_TYPE : ContentResolver.CURSOR_DIR_BASE_TYPE;
+        return prefix + "/" + mapping.mimeSubtype;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                        String sortOrder) {
+        // Resolve the URI.
+        int match = mUriMatcher.match(uri);
+        if (match == UriMatcher.NO_MATCH) {
+            throw new IllegalArgumentException("Invalid URI: " + uri);
+        }
+        
+        // Add the ID predicate if needed.
+        Mapping mapping = mMappings.get(match);
+        if (mapping.hasId) {
+            selection = whereWithId(uri, selection);
+        }
+        
+        //System.out.println("QUERY " + uri + " WHERE (" + selection + ")");
+
+        // Run the query.
+        String tableName = mapping.table.getTableName();
+        Cursor cursor = mDatabase.getReadableDatabase().query(tableName, projection, selection, selectionArgs, null, null, sortOrder);
+        cursor.setNotificationUri(getContext().getContentResolver(), uri);
+        return cursor;
+    }
+    
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        // Resolve the URI.
+        int match = mUriMatcher.match(uri);
+        Mapping mapping = match != UriMatcher.NO_MATCH ? mMappings.get(match) : null;
+        if (mapping == null || mapping.hasId) {
+            throw new IllegalArgumentException("Invalid URI: " + uri);
+        }
+
+        // Insert into the database, notify observers, and return the qualified URI.
+        String tableName = mapping.table.getTableName();
+        long rowId = mDatabase.getWritableDatabase().insert(tableName, NULL_COLUMN_HACK, values);
+        if (rowId > 0) {
+            notifyChange(uri);
+            return Uri.withAppendedPath(uri, Long.toString(rowId));
+        } else {
+            throw new SQLException("Failed to insert row at: " + uri);
+        }
+    }
+    
+    @Override
+    public int bulkInsert(Uri uri, ContentValues[] values) {
+        // Resolve the URI.
+        int match = mUriMatcher.match(uri);
+        Mapping mapping = match != UriMatcher.NO_MATCH ? mMappings.get(match) : null;
+        if (mapping == null || mapping.hasId) {
+            throw new IllegalArgumentException("Invalid URI: " + uri);
+        }
+
+        // Insert all rows into the database and notify observers.
+        String tableName = mapping.table.getTableName();
+        SQLiteDatabase database = mDatabase.getWritableDatabase();
+        int numInserted = 0;
+        try {
+            int length = values.length;
+            database.beginTransaction();
+            for (int i = 0; i != length; ++i) {
+                database.insert(tableName, NULL_COLUMN_HACK, values[i]);
+            }
+            database.setTransactionSuccessful();
+            numInserted = length;
+        } finally {
+            database.endTransaction();
+        }
+        notifyChange(uri);
+        return numInserted;
+    }
+    
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        // Resolve the URI.
+        int match = mUriMatcher.match(uri);
+        if (match == UriMatcher.NO_MATCH) {
+            throw new IllegalArgumentException("Invalid URI: " + uri);
+        }
+        
+        // Add the ID predicate if needed.
+        Mapping mapping = mMappings.get(match);
+        if (mapping.hasId) {
+            selection = whereWithId(uri, selection);
+        }
+        
+        // Update the item(s) and broadcast a change notification.
+        SQLiteDatabase db = mDatabase.getWritableDatabase();
+        String tableName = mapping.table.getTableName();
+        int count = db.update(tableName, values, selection, selectionArgs);
+        notifyChange(uri);
+        return count;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        // Resolve the URI.
+        int match = mUriMatcher.match(uri);
+        if (match == UriMatcher.NO_MATCH) {
+            throw new IllegalArgumentException("Invalid URI: " + uri);
+        }
+        
+        // Add the ID predicate if needed.
+        Mapping mapping = mMappings.get(match);
+        if (mapping.hasId) {
+            selection = whereWithId(uri, selection);
+        }
+
+        // Delete the item(s) and broadcast a change notification.
+        SQLiteDatabase db = mDatabase.getWritableDatabase();
+        String tableName = mapping.table.getTableName();
+        int count = db.delete(tableName, selection, selectionArgs);
+        notifyChange(uri);
+        return count;
+    }
+    
+    private final String whereWithId(Uri uri, String selection) {
+        String id = uri.getPathSegments().get(1);
+        StringBuilder where = new StringBuilder("_id=");
+        where.append(id);
+        if (!TextUtils.isEmpty(selection)) {
+            where.append(" AND (");
+            where.append(selection);
+            where.append(')');
+        }
+        return where.toString();
+    }
+    
+    private final void notifyChange(Uri uri) {
+        getContext().getContentResolver().notifyChange(uri, null);
+    }
+    
+    private static final class Mapping {
+        public EntrySchema table;
+        public String mimeSubtype;
+        public boolean hasId;
+
+        public Mapping(EntrySchema table, String mimeSubtype, boolean hasId) {
+            this.table = table;
+            this.mimeSubtype = mimeSubtype;
+            this.hasId = hasId;
+        }
+    }
+}
diff --git a/src/com/cooliris/picasa/UserEntry.java b/src/com/cooliris/picasa/UserEntry.java
new file mode 100644
index 0000000..ac528dc
--- /dev/null
+++ b/src/com/cooliris/picasa/UserEntry.java
@@ -0,0 +1,12 @@
+package com.cooliris.picasa;
+
+@Entry.Table("users")
+public final class UserEntry extends Entry {
+    public static final EntrySchema SCHEMA = new EntrySchema(UserEntry.class);
+
+    @Column("account")
+    public String account;
+    
+    @Column("albums_etag")
+    public String albumsEtag;
+}