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;
+}