OSDN Git Service

Initial Contribution
authorThe Android Open Source Project <initial-contribution@android.com>
Tue, 21 Oct 2008 14:00:00 +0000 (07:00 -0700)
committerThe Android Open Source Project <initial-contribution@android.com>
Tue, 21 Oct 2008 14:00:00 +0000 (07:00 -0700)
120 files changed:
Android.mk [new file with mode: 0644]
AndroidManifest.xml [new file with mode: 0644]
MODULE_LICENSE_APACHE2 [new file with mode: 0644]
NOTICE [new file with mode: 0644]
res/drawable-finger/albumart_mp_unknown.png [new file with mode: 0755]
res/drawable-finger/albumart_mp_unknown_list.png [new file with mode: 0755]
res/drawable-finger/btn_selection_focused.9.png [new file with mode: 0755]
res/drawable-finger/btn_selection_pressed.9.png [new file with mode: 0755]
res/drawable-finger/ic_dialog_alert.png [new file with mode: 0755]
res/drawable-finger/ic_menu_add.png [new file with mode: 0755]
res/drawable-finger/ic_menu_clear_playlist.png [new file with mode: 0755]
res/drawable-finger/ic_menu_delete.png [new file with mode: 0755]
res/drawable-finger/ic_menu_music_library.png [new file with mode: 0755]
res/drawable-finger/ic_menu_party_shuffle.png [new file with mode: 0755]
res/drawable-finger/ic_menu_play_clip.png [new file with mode: 0755]
res/drawable-finger/ic_menu_playback.png [new file with mode: 0755]
res/drawable-finger/ic_menu_save.png [new file with mode: 0755]
res/drawable-finger/ic_menu_search.png [new file with mode: 0755]
res/drawable-finger/ic_menu_set_as_ringtone.png [new file with mode: 0755]
res/drawable-finger/ic_menu_shuffle.png [new file with mode: 0755]
res/drawable-finger/ic_mp_album_playback.png [new file with mode: 0755]
res/drawable-finger/ic_mp_artist_playback.png [new file with mode: 0755]
res/drawable-finger/ic_mp_clear_playlist.png [new file with mode: 0755]
res/drawable-finger/ic_mp_current_playlist_btn.png [new file with mode: 0755]
res/drawable-finger/ic_mp_move.png [new file with mode: 0755]
res/drawable-finger/ic_mp_partyshuffle_on_btn.png [new file with mode: 0755]
res/drawable-finger/ic_mp_playlist_list.png [new file with mode: 0755]
res/drawable-finger/ic_mp_repeat_all_btn.png [new file with mode: 0755]
res/drawable-finger/ic_mp_repeat_off_btn.png [new file with mode: 0755]
res/drawable-finger/ic_mp_repeat_once_btn.png [new file with mode: 0755]
res/drawable-finger/ic_mp_screen_albums.png [new file with mode: 0755]
res/drawable-finger/ic_mp_screen_artists.png [new file with mode: 0755]
res/drawable-finger/ic_mp_screen_playlists.png [new file with mode: 0755]
res/drawable-finger/ic_mp_screen_tracks.png [new file with mode: 0755]
res/drawable-finger/ic_mp_sd_card.png [new file with mode: 0644]
res/drawable-finger/ic_mp_shuffle_off_btn.png [new file with mode: 0755]
res/drawable-finger/ic_mp_shuffle_on_btn.png [new file with mode: 0755]
res/drawable-finger/ic_mp_song_playback.png [new file with mode: 0755]
res/drawable-finger/ic_search_category_music_album.png [new file with mode: 0755]
res/drawable-finger/ic_search_category_music_artist.png [new file with mode: 0755]
res/drawable-finger/ic_search_category_music_song.png [new file with mode: 0755]
res/drawable-finger/ic_slide_keyboard.png [new file with mode: 0755]
res/drawable-finger/indicator_ic_mp_playing_list.png [new file with mode: 0755]
res/drawable-finger/indicator_ic_mp_playing_notifications.png [new file with mode: 0755]
res/drawable-finger/list_selector.xml [new file with mode: 0644]
res/drawable-finger/main_menu_button.xml [new file with mode: 0644]
res/drawable-finger/stat_notify_musicplayer.png [new file with mode: 0755]
res/drawable-land-finger/albumart_mp_unknown.png [new file with mode: 0755]
res/drawable-land-finger/ic_mp_screen_tracks.png [new file with mode: 0644]
res/drawable/album_title_icon.png [new file with mode: 0644]
res/drawable/app_music.png [new file with mode: 0644]
res/drawable/app_video.png [new file with mode: 0644]
res/drawable/midi.png [new file with mode: 0644]
res/drawable/movie.png [new file with mode: 0644]
res/drawable/music.png [new file with mode: 0644]
res/drawable/musicnote.png [new file with mode: 0644]
res/drawable/paused.png [new file with mode: 0644]
res/drawable/person.png [new file with mode: 0644]
res/drawable/person_title_icon.png [new file with mode: 0644]
res/drawable/spacer.png [new file with mode: 0644]
res/drawable/track_icon.png [new file with mode: 0644]
res/layout-finger/audio_player.xml [new file with mode: 0644]
res/layout-finger/audio_player_common.xml [new file with mode: 0644]
res/layout-finger/confirm_delete.xml [new file with mode: 0644]
res/layout-finger/edit_track_list_item.xml [new file with mode: 0644]
res/layout-finger/no_sd_card.xml [new file with mode: 0644]
res/layout-finger/no_sd_card_expanding.xml [new file with mode: 0644]
res/layout-finger/streamstarter.xml [new file with mode: 0644]
res/layout-finger/track_list_item.xml [new file with mode: 0644]
res/layout-finger/track_list_item_child.xml [new file with mode: 0644]
res/layout-finger/track_list_item_common.xml [new file with mode: 0644]
res/layout-finger/track_list_item_group.xml [new file with mode: 0644]
res/layout-finger/weekpicker.xml [new file with mode: 0644]
res/layout-keysexposed/create_playlist.xml [new file with mode: 0644]
res/layout-keyshidden/create_playlist.xml [new file with mode: 0644]
res/layout-land-finger/audio_player.xml [new file with mode: 0644]
res/layout-land-finger/music_library.xml [new file with mode: 0644]
res/layout/function_picker.xml [new file with mode: 0644]
res/layout/media_picker_activity.xml [new file with mode: 0644]
res/layout/movie_view.xml [new file with mode: 0644]
res/layout/music_library.xml [new file with mode: 0644]
res/layout/query_activity.xml [new file with mode: 0644]
res/layout/scanning.xml [new file with mode: 0644]
res/layout/statusbar.xml [new file with mode: 0644]
res/values-cs/strings.xml [new file with mode: 0644]
res/values-de-rDE/strings.xml [new file with mode: 0644]
res/values-en-rGB/strings.xml [new file with mode: 0644]
res/values-es-rUS/strings.xml [new file with mode: 0644]
res/values-finger/strings2.xml [new file with mode: 0644]
res/values-fr-rFR/strings.xml [new file with mode: 0644]
res/values-it-rIT/strings.xml [new file with mode: 0644]
res/values-keysexposed/strings.xml [new file with mode: 0644]
res/values-keyshidden/strings.xml [new file with mode: 0644]
res/values-nl-rNL/strings.xml [new file with mode: 0644]
res/values-zh-rTW/strings.xml [new file with mode: 0644]
res/values/strings.xml [new file with mode: 0644]
res/values/strings2.xml [new file with mode: 0644]
src/com/android/music/AlbumBrowserActivity.java [new file with mode: 0644]
src/com/android/music/AlbumView.java [new file with mode: 0644]
src/com/android/music/ArtistAlbumBrowserActivity.java [new file with mode: 0644]
src/com/android/music/CreatePlaylist.java [new file with mode: 0644]
src/com/android/music/DeleteItems.java [new file with mode: 0644]
src/com/android/music/IMediaPlaybackService.aidl [new file with mode: 0644]
src/com/android/music/MediaButtonIntentReceiver.java [new file with mode: 0644]
src/com/android/music/MediaPickerActivity.java [new file with mode: 0644]
src/com/android/music/MediaPlaybackActivity.java [new file with mode: 0644]
src/com/android/music/MediaPlaybackService.java [new file with mode: 0644]
src/com/android/music/MovieView.java [new file with mode: 0644]
src/com/android/music/MusicBrowserActivity.java [new file with mode: 0644]
src/com/android/music/MusicUtils.java [new file with mode: 0644]
src/com/android/music/PlaylistBrowserActivity.java [new file with mode: 0644]
src/com/android/music/QueryBrowserActivity.java [new file with mode: 0644]
src/com/android/music/RenamePlaylist.java [new file with mode: 0644]
src/com/android/music/RepeatingImageButton.java [new file with mode: 0644]
src/com/android/music/ScanningProgress.java [new file with mode: 0644]
src/com/android/music/StreamStarter.java [new file with mode: 0644]
src/com/android/music/TouchInterceptor.java [new file with mode: 0644]
src/com/android/music/TrackBrowserActivity.java [new file with mode: 0644]
src/com/android/music/VideoBrowserActivity.java [new file with mode: 0644]
src/com/android/music/WeekSelector.java [new file with mode: 0644]

diff --git a/Android.mk b/Android.mk
new file mode 100644 (file)
index 0000000..93feb9d
--- /dev/null
@@ -0,0 +1,14 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := user development
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src) \
+       src/com/android/music/IMediaPlaybackService.aidl
+
+LOCAL_PACKAGE_NAME := Music
+
+include $(BUILD_PACKAGE)
+
+# Use the folloing 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 (file)
index 0000000..518e19d
--- /dev/null
@@ -0,0 +1,220 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.music">
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+
+    <application android:icon="@drawable/app_music"
+        android:label="@string/musicbrowserlabel"
+        android:taskAffinity="android.task.music"
+        android:allowTaskReparenting="true">
+        <activity android:name="MusicBrowserActivity"
+                android:theme="@android:style/Theme.NoTitleBar">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <receiver android:name="MediaButtonIntentReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.MEDIA_BUTTON" />
+            </intent-filter>
+        </receiver>
+        <!-- This is the "current music playing" panel, which has special
+             launch behavior.  We clear its task affinity, so it will not
+             be associated with the main media task and if launched
+             from a notification will not bring the rest of the media app
+             to the foreground.  We make it singleTask so that when others
+             launch it (such as media) we will launch in to our own task.
+             We set clearTaskOnLaunch because the user
+             can go to a playlist from this activity, so if they later return
+             to it we want it back in its initial state.  We exclude from
+             recents since this is accessible through a notification when
+             appropriate. -->
+        <activity android:name="MediaPlaybackActivity"
+                android:theme="@android:style/Theme.NoTitleBar"
+                android:label="@string/mediaplaybacklabel"
+                android:taskAffinity=""
+                android:launchMode="singleTask"
+                android:clearTaskOnLaunch="true"
+                android:excludeFromRecents="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="content"/>
+                <data android:scheme="file"/>
+                <data android:mimeType="audio/*"/>
+                <data android:mimeType="application/ogg"/>
+                <data android:mimeType="application/x-ogg"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="com.android.music.PLAYBACK_VIEWER" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity android:name="StreamStarter" android:theme="@android:style/Theme.Dialog" >
+            <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="audio/mp3"/>
+                <data android:mimeType="audio/x-mp3"/>
+                <data android:mimeType="audio/mpeg"/>
+                <data android:mimeType="audio/mp4"/>
+            </intent-filter>
+        </activity>
+        <activity android:name="ArtistAlbumBrowserActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.PICK" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="vnd.android.cursor.dir/artistalbum"/>
+            </intent-filter>
+        </activity>
+        <activity android:name="AlbumBrowserActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.PICK" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="vnd.android.cursor.dir/album"/>
+            </intent-filter>
+        </activity>
+        <activity android:name="NowPlayingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.PICK" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="vnd.android.cursor.dir/nowplaying"/>
+            </intent-filter>
+        </activity>
+        <activity android:name="TrackBrowserActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.EDIT" />
+                <action android:name="android.intent.action.PICK" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="vnd.android.cursor.dir/track"/>
+            </intent-filter>
+        </activity>
+        <activity android:name="QueryBrowserActivity"
+                android:theme="@android:style/Theme.NoTitleBar">
+            <intent-filter>
+                <action android:name="android.intent.action.SEARCH" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity android:name="PlaylistBrowserActivity" android:label="@string/musicbrowserlabel">
+            <intent-filter>
+                <action android:name="android.intent.action.PICK" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="vnd.android.cursor.dir/playlist"/>
+            </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/playlist"/>
+            </intent-filter>
+        </activity>
+        <activity-alias android:name="PlaylistShortcutActivity"
+            android:targetActivity="PlaylistBrowserActivity"
+            android:label="@string/musicshortcutlabel">
+
+            <intent-filter>
+                <action android:name="android.intent.action.CREATE_SHORTCUT" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+
+        </activity-alias>
+        <activity android:name="VideoBrowserActivity"
+            android:taskAffinity="android.task.video"
+            android:label="@string/videobrowserlabel"
+            android:icon="@drawable/app_video">
+            <intent-filter>
+                <action android:name="android.intent.action.PICK" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="vnd.android.cursor.dir/video"/>
+            </intent-filter>
+<!--
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+-->
+        </activity>
+        <activity android:name="MediaPickerActivity" android:label="@string/mediapickerlabel">
+<!--
+            <intent-filter>
+                <action android:name="android.intent.action.PICK" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="media/*"/>
+                <data android:mimeType="audio/*"/>
+                <data android:mimeType="application/ogg"/>
+                <data android:mimeType="application/x-ogg"/>
+                <data android:mimeType="video/*"/>
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.GET_CONTENT" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.OPENABLE" />
+                <data android:mimeType="media/*"/>
+                <data android:mimeType="audio/*"/>
+                <data android:mimeType="application/ogg"/>
+                <data android:mimeType="application/x-ogg"/>
+                <data android:mimeType="video/*"/>
+            </intent-filter>
+-->
+        </activity>
+        <activity android:name="CreatePlaylist" android:theme="@android:style/Theme.Dialog" />
+        <activity android:name="RenamePlaylist" android:theme="@android:style/Theme.Dialog" />
+        <activity android:name="WeekSelector" android:theme="@android:style/Theme.Dialog" />
+        <activity android:name="DeleteItems" android:theme="@android:style/Theme.Dialog" />
+        <activity android:name="ScanningProgress" android:theme="@android:style/Theme.Dialog" />
+        <activity android:name="MovieView"
+                android:label="@string/movieviewlabel"
+                android:screenOrientation="sensor"
+                android:configChanges="orientation|keyboardHidden"
+                android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
+                android:taskAffinity="android.task.video"
+                android:process=":MovieView" >
+             <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/*" />
+             </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/mp4" />
+                <data android:mimeType="video/3gp" />
+                <data android:mimeType="video/3gpp" />
+                <data android:mimeType="video/3gpp2" />
+             </intent-filter>
+        </activity>
+        <service android:name="MediaPlaybackService" android:exported="true" />
+    </application>
+</manifest>
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/NOTICE b/NOTICE
new file mode 100644 (file)
index 0000000..c5b1efa
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/res/drawable-finger/albumart_mp_unknown.png b/res/drawable-finger/albumart_mp_unknown.png
new file mode 100755 (executable)
index 0000000..ecd5a98
Binary files /dev/null and b/res/drawable-finger/albumart_mp_unknown.png differ
diff --git a/res/drawable-finger/albumart_mp_unknown_list.png b/res/drawable-finger/albumart_mp_unknown_list.png
new file mode 100755 (executable)
index 0000000..1dcf4bd
Binary files /dev/null and b/res/drawable-finger/albumart_mp_unknown_list.png differ
diff --git a/res/drawable-finger/btn_selection_focused.9.png b/res/drawable-finger/btn_selection_focused.9.png
new file mode 100755 (executable)
index 0000000..00342c0
Binary files /dev/null and b/res/drawable-finger/btn_selection_focused.9.png differ
diff --git a/res/drawable-finger/btn_selection_pressed.9.png b/res/drawable-finger/btn_selection_pressed.9.png
new file mode 100755 (executable)
index 0000000..4359903
Binary files /dev/null and b/res/drawable-finger/btn_selection_pressed.9.png differ
diff --git a/res/drawable-finger/ic_dialog_alert.png b/res/drawable-finger/ic_dialog_alert.png
new file mode 100755 (executable)
index 0000000..0a7de04
Binary files /dev/null and b/res/drawable-finger/ic_dialog_alert.png differ
diff --git a/res/drawable-finger/ic_menu_add.png b/res/drawable-finger/ic_menu_add.png
new file mode 100755 (executable)
index 0000000..6752bfd
Binary files /dev/null and b/res/drawable-finger/ic_menu_add.png differ
diff --git a/res/drawable-finger/ic_menu_clear_playlist.png b/res/drawable-finger/ic_menu_clear_playlist.png
new file mode 100755 (executable)
index 0000000..750db62
Binary files /dev/null and b/res/drawable-finger/ic_menu_clear_playlist.png differ
diff --git a/res/drawable-finger/ic_menu_delete.png b/res/drawable-finger/ic_menu_delete.png
new file mode 100755 (executable)
index 0000000..82f9d56
Binary files /dev/null and b/res/drawable-finger/ic_menu_delete.png differ
diff --git a/res/drawable-finger/ic_menu_music_library.png b/res/drawable-finger/ic_menu_music_library.png
new file mode 100755 (executable)
index 0000000..3f1de60
Binary files /dev/null and b/res/drawable-finger/ic_menu_music_library.png differ
diff --git a/res/drawable-finger/ic_menu_party_shuffle.png b/res/drawable-finger/ic_menu_party_shuffle.png
new file mode 100755 (executable)
index 0000000..51c3d02
Binary files /dev/null and b/res/drawable-finger/ic_menu_party_shuffle.png differ
diff --git a/res/drawable-finger/ic_menu_play_clip.png b/res/drawable-finger/ic_menu_play_clip.png
new file mode 100755 (executable)
index 0000000..4669947
Binary files /dev/null and b/res/drawable-finger/ic_menu_play_clip.png differ
diff --git a/res/drawable-finger/ic_menu_playback.png b/res/drawable-finger/ic_menu_playback.png
new file mode 100755 (executable)
index 0000000..22f203a
Binary files /dev/null and b/res/drawable-finger/ic_menu_playback.png differ
diff --git a/res/drawable-finger/ic_menu_save.png b/res/drawable-finger/ic_menu_save.png
new file mode 100755 (executable)
index 0000000..36d50b3
Binary files /dev/null and b/res/drawable-finger/ic_menu_save.png differ
diff --git a/res/drawable-finger/ic_menu_search.png b/res/drawable-finger/ic_menu_search.png
new file mode 100755 (executable)
index 0000000..94446db
Binary files /dev/null and b/res/drawable-finger/ic_menu_search.png differ
diff --git a/res/drawable-finger/ic_menu_set_as_ringtone.png b/res/drawable-finger/ic_menu_set_as_ringtone.png
new file mode 100755 (executable)
index 0000000..d44d7bd
Binary files /dev/null and b/res/drawable-finger/ic_menu_set_as_ringtone.png differ
diff --git a/res/drawable-finger/ic_menu_shuffle.png b/res/drawable-finger/ic_menu_shuffle.png
new file mode 100755 (executable)
index 0000000..cb7009d
Binary files /dev/null and b/res/drawable-finger/ic_menu_shuffle.png differ
diff --git a/res/drawable-finger/ic_mp_album_playback.png b/res/drawable-finger/ic_mp_album_playback.png
new file mode 100755 (executable)
index 0000000..1ab7042
Binary files /dev/null and b/res/drawable-finger/ic_mp_album_playback.png differ
diff --git a/res/drawable-finger/ic_mp_artist_playback.png b/res/drawable-finger/ic_mp_artist_playback.png
new file mode 100755 (executable)
index 0000000..a657c47
Binary files /dev/null and b/res/drawable-finger/ic_mp_artist_playback.png differ
diff --git a/res/drawable-finger/ic_mp_clear_playlist.png b/res/drawable-finger/ic_mp_clear_playlist.png
new file mode 100755 (executable)
index 0000000..5cd1c37
Binary files /dev/null and b/res/drawable-finger/ic_mp_clear_playlist.png differ
diff --git a/res/drawable-finger/ic_mp_current_playlist_btn.png b/res/drawable-finger/ic_mp_current_playlist_btn.png
new file mode 100755 (executable)
index 0000000..0d86e77
Binary files /dev/null and b/res/drawable-finger/ic_mp_current_playlist_btn.png differ
diff --git a/res/drawable-finger/ic_mp_move.png b/res/drawable-finger/ic_mp_move.png
new file mode 100755 (executable)
index 0000000..5104f5d
Binary files /dev/null and b/res/drawable-finger/ic_mp_move.png differ
diff --git a/res/drawable-finger/ic_mp_partyshuffle_on_btn.png b/res/drawable-finger/ic_mp_partyshuffle_on_btn.png
new file mode 100755 (executable)
index 0000000..7d89adb
Binary files /dev/null and b/res/drawable-finger/ic_mp_partyshuffle_on_btn.png differ
diff --git a/res/drawable-finger/ic_mp_playlist_list.png b/res/drawable-finger/ic_mp_playlist_list.png
new file mode 100755 (executable)
index 0000000..2fb4456
Binary files /dev/null and b/res/drawable-finger/ic_mp_playlist_list.png differ
diff --git a/res/drawable-finger/ic_mp_repeat_all_btn.png b/res/drawable-finger/ic_mp_repeat_all_btn.png
new file mode 100755 (executable)
index 0000000..fe043c6
Binary files /dev/null and b/res/drawable-finger/ic_mp_repeat_all_btn.png differ
diff --git a/res/drawable-finger/ic_mp_repeat_off_btn.png b/res/drawable-finger/ic_mp_repeat_off_btn.png
new file mode 100755 (executable)
index 0000000..a7516d0
Binary files /dev/null and b/res/drawable-finger/ic_mp_repeat_off_btn.png differ
diff --git a/res/drawable-finger/ic_mp_repeat_once_btn.png b/res/drawable-finger/ic_mp_repeat_once_btn.png
new file mode 100755 (executable)
index 0000000..8950734
Binary files /dev/null and b/res/drawable-finger/ic_mp_repeat_once_btn.png differ
diff --git a/res/drawable-finger/ic_mp_screen_albums.png b/res/drawable-finger/ic_mp_screen_albums.png
new file mode 100755 (executable)
index 0000000..ad8b96d
Binary files /dev/null and b/res/drawable-finger/ic_mp_screen_albums.png differ
diff --git a/res/drawable-finger/ic_mp_screen_artists.png b/res/drawable-finger/ic_mp_screen_artists.png
new file mode 100755 (executable)
index 0000000..a77e87f
Binary files /dev/null and b/res/drawable-finger/ic_mp_screen_artists.png differ
diff --git a/res/drawable-finger/ic_mp_screen_playlists.png b/res/drawable-finger/ic_mp_screen_playlists.png
new file mode 100755 (executable)
index 0000000..944df81
Binary files /dev/null and b/res/drawable-finger/ic_mp_screen_playlists.png differ
diff --git a/res/drawable-finger/ic_mp_screen_tracks.png b/res/drawable-finger/ic_mp_screen_tracks.png
new file mode 100755 (executable)
index 0000000..f782da4
Binary files /dev/null and b/res/drawable-finger/ic_mp_screen_tracks.png differ
diff --git a/res/drawable-finger/ic_mp_sd_card.png b/res/drawable-finger/ic_mp_sd_card.png
new file mode 100644 (file)
index 0000000..90e5081
Binary files /dev/null and b/res/drawable-finger/ic_mp_sd_card.png differ
diff --git a/res/drawable-finger/ic_mp_shuffle_off_btn.png b/res/drawable-finger/ic_mp_shuffle_off_btn.png
new file mode 100755 (executable)
index 0000000..f515df9
Binary files /dev/null and b/res/drawable-finger/ic_mp_shuffle_off_btn.png differ
diff --git a/res/drawable-finger/ic_mp_shuffle_on_btn.png b/res/drawable-finger/ic_mp_shuffle_on_btn.png
new file mode 100755 (executable)
index 0000000..9c18be6
Binary files /dev/null and b/res/drawable-finger/ic_mp_shuffle_on_btn.png differ
diff --git a/res/drawable-finger/ic_mp_song_playback.png b/res/drawable-finger/ic_mp_song_playback.png
new file mode 100755 (executable)
index 0000000..4846f43
Binary files /dev/null and b/res/drawable-finger/ic_mp_song_playback.png differ
diff --git a/res/drawable-finger/ic_search_category_music_album.png b/res/drawable-finger/ic_search_category_music_album.png
new file mode 100755 (executable)
index 0000000..6cd42bd
Binary files /dev/null and b/res/drawable-finger/ic_search_category_music_album.png differ
diff --git a/res/drawable-finger/ic_search_category_music_artist.png b/res/drawable-finger/ic_search_category_music_artist.png
new file mode 100755 (executable)
index 0000000..6f7de73
Binary files /dev/null and b/res/drawable-finger/ic_search_category_music_artist.png differ
diff --git a/res/drawable-finger/ic_search_category_music_song.png b/res/drawable-finger/ic_search_category_music_song.png
new file mode 100755 (executable)
index 0000000..983e75d
Binary files /dev/null and b/res/drawable-finger/ic_search_category_music_song.png differ
diff --git a/res/drawable-finger/ic_slide_keyboard.png b/res/drawable-finger/ic_slide_keyboard.png
new file mode 100755 (executable)
index 0000000..38a7dbf
Binary files /dev/null and b/res/drawable-finger/ic_slide_keyboard.png differ
diff --git a/res/drawable-finger/indicator_ic_mp_playing_list.png b/res/drawable-finger/indicator_ic_mp_playing_list.png
new file mode 100755 (executable)
index 0000000..4e0b2bd
Binary files /dev/null and b/res/drawable-finger/indicator_ic_mp_playing_list.png differ
diff --git a/res/drawable-finger/indicator_ic_mp_playing_notifications.png b/res/drawable-finger/indicator_ic_mp_playing_notifications.png
new file mode 100755 (executable)
index 0000000..185db2e
Binary files /dev/null and b/res/drawable-finger/indicator_ic_mp_playing_notifications.png differ
diff --git a/res/drawable-finger/list_selector.xml b/res/drawable-finger/list_selector.xml
new file mode 100644 (file)
index 0000000..d8afcb6
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true"
+        android:drawable="@android:color/transparent" />
+    <item android:state_pressed="true" android:state_selected="false"
+        android:drawable="@android:color/transparent" />
+    <item android:state_selected="false"
+        android:drawable="@color/expanding_child_background" />
+</selector>
diff --git a/res/drawable-finger/main_menu_button.xml b/res/drawable-finger/main_menu_button.xml
new file mode 100644 (file)
index 0000000..da90dab
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/btn_selection_pressed" />
+    <item android:state_focused="true" android:state_pressed="false" android:drawable="@drawable/btn_selection_focused" />
+    <item android:drawable="@android:drawable/btn_default" />
+</selector>
+
diff --git a/res/drawable-finger/stat_notify_musicplayer.png b/res/drawable-finger/stat_notify_musicplayer.png
new file mode 100755 (executable)
index 0000000..fd92c18
Binary files /dev/null and b/res/drawable-finger/stat_notify_musicplayer.png differ
diff --git a/res/drawable-land-finger/albumart_mp_unknown.png b/res/drawable-land-finger/albumart_mp_unknown.png
new file mode 100755 (executable)
index 0000000..53748fd
Binary files /dev/null and b/res/drawable-land-finger/albumart_mp_unknown.png differ
diff --git a/res/drawable-land-finger/ic_mp_screen_tracks.png b/res/drawable-land-finger/ic_mp_screen_tracks.png
new file mode 100644 (file)
index 0000000..d425b01
Binary files /dev/null and b/res/drawable-land-finger/ic_mp_screen_tracks.png differ
diff --git a/res/drawable/album_title_icon.png b/res/drawable/album_title_icon.png
new file mode 100644 (file)
index 0000000..ebf936c
Binary files /dev/null and b/res/drawable/album_title_icon.png differ
diff --git a/res/drawable/app_music.png b/res/drawable/app_music.png
new file mode 100644 (file)
index 0000000..0353b91
Binary files /dev/null and b/res/drawable/app_music.png differ
diff --git a/res/drawable/app_video.png b/res/drawable/app_video.png
new file mode 100644 (file)
index 0000000..0c10731
Binary files /dev/null and b/res/drawable/app_video.png differ
diff --git a/res/drawable/midi.png b/res/drawable/midi.png
new file mode 100644 (file)
index 0000000..87f32ac
Binary files /dev/null and b/res/drawable/midi.png differ
diff --git a/res/drawable/movie.png b/res/drawable/movie.png
new file mode 100644 (file)
index 0000000..bed7c78
Binary files /dev/null and b/res/drawable/movie.png differ
diff --git a/res/drawable/music.png b/res/drawable/music.png
new file mode 100644 (file)
index 0000000..2dcd8c3
Binary files /dev/null and b/res/drawable/music.png differ
diff --git a/res/drawable/musicnote.png b/res/drawable/musicnote.png
new file mode 100644 (file)
index 0000000..11b5fdf
Binary files /dev/null and b/res/drawable/musicnote.png differ
diff --git a/res/drawable/paused.png b/res/drawable/paused.png
new file mode 100644 (file)
index 0000000..ad9b031
Binary files /dev/null and b/res/drawable/paused.png differ
diff --git a/res/drawable/person.png b/res/drawable/person.png
new file mode 100644 (file)
index 0000000..58545a5
Binary files /dev/null and b/res/drawable/person.png differ
diff --git a/res/drawable/person_title_icon.png b/res/drawable/person_title_icon.png
new file mode 100644 (file)
index 0000000..58545a5
Binary files /dev/null and b/res/drawable/person_title_icon.png differ
diff --git a/res/drawable/spacer.png b/res/drawable/spacer.png
new file mode 100644 (file)
index 0000000..c26601c
Binary files /dev/null and b/res/drawable/spacer.png differ
diff --git a/res/drawable/track_icon.png b/res/drawable/track_icon.png
new file mode 100644 (file)
index 0000000..11b5fdf
Binary files /dev/null and b/res/drawable/track_icon.png differ
diff --git a/res/layout-finger/audio_player.xml b/res/layout-finger/audio_player.xml
new file mode 100644 (file)
index 0000000..0ae8013
--- /dev/null
@@ -0,0 +1,141 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1"
+        android:orientation="horizontal"
+        android:gravity="center">
+
+        <com.android.music.AlbumView android:id="@+id/album"
+            android:layout_width="220px"
+            android:layout_height="fill_parent"
+            android:paddingLeft="12px"
+            android:paddingRight="6px"
+            android:paddingTop="16px" />
+
+        <LinearLayout
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:orientation="vertical"
+            android:gravity="center_horizontal">
+
+           <ImageButton android:id="@+id/curplaylist"
+                android:src="@drawable/ic_mp_current_playlist_btn"
+                android:layout_width="85px"
+                android:layout_height="54px"
+                android:layout_marginTop="14px" />
+
+           <ImageButton android:id="@+id/shuffle"
+                android:layout_width="85px"
+                android:layout_height="54px"
+                android:layout_marginTop="20px" />
+
+           <ImageButton android:id="@+id/repeat"
+                android:layout_width="85px"
+                android:layout_height="54px"
+                android:layout_marginTop="20px" />
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:baselineAligned="false"
+        android:paddingLeft="11dip"
+        android:paddingTop="4dip"
+        android:paddingBottom="8dip">
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="4dip"
+            android:src="@drawable/ic_mp_artist_playback" />
+
+        <TextView android:id="@+id/artistname"
+            android:textSize="18sp"
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:textStyle="bold"
+            android:layout_gravity="center_vertical"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:baselineAligned="false"
+        android:paddingLeft="11dip"
+        android:paddingTop="4dip"
+        android:paddingBottom="8dip">
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="4dip"
+            android:src="@drawable/ic_mp_album_playback" />
+
+        <TextView android:id="@+id/albumname"
+            android:textSize="14sp"
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:layout_gravity="center_vertical"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:baselineAligned="false"
+        android:paddingLeft="11dip"
+        android:paddingTop="0dip"
+        android:paddingBottom="8dip">
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="4dip"
+            android:src="@drawable/ic_mp_song_playback" />
+
+        <TextView android:id="@+id/trackname"
+            android:textSize="14sp"
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:layout_gravity="center_vertical"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+    </LinearLayout>
+
+    <include layout="@layout/audio_player_common" />
+
+</LinearLayout>
diff --git a/res/layout-finger/audio_player_common.xml b/res/layout-finger/audio_player_common.xml
new file mode 100644 (file)
index 0000000..95320d9
--- /dev/null
@@ -0,0 +1,89 @@
+<?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.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <View 
+        android:layout_width="fill_parent"
+        android:layout_height="1px"
+        android:background="#ffffffff" />
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:background="#ff5a5a5a"
+        android:paddingTop="1px"
+        android:paddingBottom="4px"
+        android:orientation="horizontal">
+
+        <TextView android:id="@+id/currenttime"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:textSize="14sp"
+            android:textStyle="bold"
+            android:shadowColor="#ff000000"
+            android:shadowDx="0"
+            android:shadowDy="0"
+            android:shadowRadius="3"
+            android:layout_gravity="bottom"
+            android:layout_weight="1"
+            android:layout_width="0dip"
+            android:paddingLeft="5px"
+            android:layout_height="wrap_content" />
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_gravity="bottom"
+            android:layout_marginTop="1px"
+            android:layout_marginBottom="2px"
+            android:gravity="center">
+
+            <com.android.music.RepeatingImageButton android:id="@+id/prev" style="@android:style/MediaButton.Previous" />
+
+            <ImageButton android:id="@+id/pause" style="@android:style/MediaButton.Play" />
+
+            <com.android.music.RepeatingImageButton android:id="@+id/next" style="@android:style/MediaButton.Next" />
+
+        </LinearLayout>
+
+        <TextView android:id="@+id/totaltime"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:textSize="14sp"
+            android:textStyle="bold"
+            android:shadowColor="#ff000000"
+            android:shadowDx="0"
+            android:shadowDy="0"
+            android:shadowRadius="3"
+            android:gravity="right"
+            android:paddingRight="5px"
+            android:layout_gravity="bottom"
+            android:layout_weight="1"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content" />
+
+    </LinearLayout>
+
+    <SeekBar android:id="@android:id/progress"
+        android:background="#ff5a5a5a"
+        style="?android:attr/progressBarStyleHorizontal"
+        android:layout_width="fill_parent"
+        android:layout_height="36px"
+        android:paddingLeft="5px"
+        android:paddingRight="5px"
+        android:paddingBottom="4px" />
+
+</merge>
diff --git a/res/layout-finger/confirm_delete.xml b/res/layout-finger/confirm_delete.xml
new file mode 100644 (file)
index 0000000..be8af25
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content">
+
+    <TextView android:id="@+id/prompt"
+        android:layout_width="fill_parent" android:layout_height="wrap_content"
+        android:text="@string/delete_confirm_prompt"
+        android:layout_marginLeft="8dip"
+        android:layout_marginTop="8dip"
+        android:layout_marginBottom="8dip"
+        android:drawableLeft="@drawable/ic_dialog_alert"
+        android:drawablePadding="8dip">
+    </TextView>
+
+    <RelativeLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:padding="6px"
+        android:background="#ffffff" >
+
+        <Button android:id="@+id/delete"
+            android:layout_width="120px" android:layout_height="wrap_content" 
+            android:text="@string/delete_confirm_button_text"
+            android:layout_gravity="center_horizontal"
+            android:layout_alignParentLeft="true" />
+
+        <Button android:id="@+id/cancel"
+            android:layout_width="120px" android:layout_height="wrap_content" 
+            android:text="@string/cancel"
+            android:layout_alignParentRight="true" />
+
+    </RelativeLayout>
+
+</LinearLayout>
+
diff --git a/res/layout-finger/edit_track_list_item.xml b/res/layout-finger/edit_track_list_item.xml
new file mode 100644 (file)
index 0000000..cae865d
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License"); 
+** you may not use this file except in compliance with the License. 
+** You may obtain a copy of the License at 
+**
+**     http://www.apache.org/licenses/LICENSE-2.0 
+**
+** Unless required by applicable law or agreed to in writing, software 
+** distributed under the License is distributed on an "AS IS" BASIS, 
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
+** See the License for the specific language governing permissions and 
+** limitations under the License.
+*/
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="64dip"
+    android:gravity="bottom"
+    android:orientation="vertical"
+    android:baselineAligned="false">
+
+    <ImageView
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:background="@android:drawable/divider_horizontal_dark" />
+
+    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="fill_parent"
+        android:layout_height="64dip"
+        android:gravity="bottom">
+
+        <include layout="@layout/track_list_item_common" />
+
+    </RelativeLayout>
+
+</LinearLayout>
diff --git a/res/layout-finger/no_sd_card.xml b/res/layout-finger/no_sd_card.xml
new file mode 100644 (file)
index 0000000..be9c149
--- /dev/null
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License"); 
+** you may not use this file except in compliance with the License. 
+** You may obtain a copy of the License at 
+**
+**     http://www.apache.org/licenses/LICENSE-2.0 
+**
+** Unless required by applicable law or agreed to in writing, software 
+** distributed under the License is distributed on an "AS IS" BASIS, 
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
+** See the License for the specific language governing permissions and 
+** limitations under the License.
+*/
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical"
+    android:gravity="center_vertical" >
+
+    <ImageView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:background="@drawable/ic_mp_sd_card" />
+
+    <TextView
+        android:id="@+id/sd_message"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:padding="8dip"
+        android:text="@string/sdcard_missing_message"
+        android:textAppearance="?android:attr/textAppearanceMedium" />
+
+    <!-- Need a dummy ListView because this will be placed in a ListActivity -->
+    <ListView
+        android:id="@android:id/list"
+        android:layout_width="0px"
+        android:layout_height="0px"
+        android:visibility="gone" />
+
+</LinearLayout>
diff --git a/res/layout-finger/no_sd_card_expanding.xml b/res/layout-finger/no_sd_card_expanding.xml
new file mode 100644 (file)
index 0000000..2c05875
--- /dev/null
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License"); 
+** you may not use this file except in compliance with the License. 
+** You may obtain a copy of the License at 
+**
+**     http://www.apache.org/licenses/LICENSE-2.0 
+**
+** Unless required by applicable law or agreed to in writing, software 
+** distributed under the License is distributed on an "AS IS" BASIS, 
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
+** See the License for the specific language governing permissions and 
+** limitations under the License.
+*/
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical"
+    android:gravity="center_vertical" >
+
+    <ImageView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:background="@drawable/ic_mp_sd_card" />
+
+    <TextView
+        android:id="@+id/sd_message"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:padding="8dip"
+        android:text="@string/sdcard_missing_message"
+        android:textAppearance="?android:attr/textAppearanceMedium" />
+
+    <!-- Need a dummy ListView because this will be placed in a ListActivity -->
+    <ExpandableListView
+        android:id="@android:id/list"
+        android:layout_width="0px"
+        android:layout_height="0px"
+        android:visibility="gone" />
+
+</LinearLayout>
diff --git a/res/layout-finger/streamstarter.xml b/res/layout-finger/streamstarter.xml
new file mode 100644 (file)
index 0000000..025d889
--- /dev/null
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:padding="10dip">
+
+    <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:id="@+id/streamloading"
+        android:paddingTop="5dip"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:textSize="14sp"
+        android:textColor="#ffffffff" />
+
+</LinearLayout>
+
diff --git a/res/layout-finger/track_list_item.xml b/res/layout-finger/track_list_item.xml
new file mode 100644 (file)
index 0000000..aa2cd5d
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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:layout_width="fill_parent"
+    android:layout_height="64dip"
+    android:gravity="center_vertical"
+    android:ignoreGravity="@+id/icon">
+
+    <include layout="@layout/track_list_item_common" />
+
+</RelativeLayout>
diff --git a/res/layout-finger/track_list_item_child.xml b/res/layout-finger/track_list_item_child.xml
new file mode 100644 (file)
index 0000000..d049ff1
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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:layout_width="fill_parent"
+    android:layout_height="64dip"
+    android:background="@drawable/list_selector"
+    android:paddingLeft="24px">
+
+    <include layout="@layout/track_list_item_common" />
+
+</RelativeLayout>
diff --git a/res/layout-finger/track_list_item_common.xml b/res/layout-finger/track_list_item_common.xml
new file mode 100644 (file)
index 0000000..d3711d0
--- /dev/null
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <ImageView android:id="@+id/icon"
+        android:layout_alignParentLeft="true"
+        android:layout_centerVertical="true"
+        android:layout_marginLeft="4dip"
+        android:layout_width="60px"
+        android:layout_height="60px"/>
+
+    <TextView android:id="@+id/duration" android:textSize="14sp" android:textColor="#ffe0d090"
+        android:paddingLeft="4dip"
+        android:paddingRight="5dip"
+        android:layout_alignParentRight="true"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignBaseline="@+id/line1"
+        android:singleLine="true" />
+
+    <TextView android:id="@+id/line1"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:layout_width="wrap_content"
+        android:paddingLeft="4dip"
+        android:layout_height="wrap_content"
+        android:layout_alignParentTop="true"
+        android:layout_alignWithParentIfMissing="true"
+        android:layout_toRightOf="@id/icon"
+        android:layout_toLeftOf="@id/duration"
+        android:ellipsize="end"
+        android:singleLine="true" />
+
+    <TextView android:id="@+id/line2" android:visibility="visible"
+        android:maxLines="2"
+        android:ellipsize="end"
+        android:paddingLeft="4dip"
+        android:textSize="14sp"
+        android:layout_below="@id/line1"
+        android:layout_alignWithParentIfMissing="true"
+        android:layout_toRightOf="@id/icon"
+        android:layout_toLeftOf="@id/duration"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+    <ImageView android:id="@+id/play_indicator"
+        android:layout_alignParentRight="true"
+        android:layout_alignBottom="@id/line2"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginRight="6dip" />
+
+</merge>
diff --git a/res/layout-finger/track_list_item_group.xml b/res/layout-finger/track_list_item_group.xml
new file mode 100644 (file)
index 0000000..056bfe7
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License"); 
+** you may not use this file except in compliance with the License. 
+** You may obtain a copy of the License at 
+**
+**     http://www.apache.org/licenses/LICENSE-2.0 
+**
+** Unless required by applicable law or agreed to in writing, software 
+** distributed under the License is distributed on an "AS IS" BASIS, 
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
+** See the License for the specific language governing permissions and 
+** limitations under the License.
+*/
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="64dip"
+    android:gravity="center_vertical"
+    android:ignoreGravity="@+id/icon"
+    android:paddingLeft="30px">
+
+    <include layout="@layout/track_list_item_common" />
+
+</RelativeLayout>
diff --git a/res/layout-finger/weekpicker.xml b/res/layout-finger/weekpicker.xml
new file mode 100644 (file)
index 0000000..51260b1
--- /dev/null
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<!-- Layout of time picker-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content">
+
+    <TextView
+        android:text="@string/weekpicker_title"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:layout_gravity="center_horizontal"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+    <!-- weeks -->
+    <com.android.internal.widget.VerticalTextSpinner
+        android:id="@+id/weeks"
+        android:layout_width="120dip"
+        android:layout_height="120dip"
+        android:focusable="true"
+        android:focusableInTouchMode="true"
+        android:layout_gravity="center_horizontal"
+        android:layout_marginTop="6px"
+        android:layout_marginBottom="6px"
+        />
+    
+    <!-- Set button -->
+    <RelativeLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:padding="6px"
+        android:background="#ffffff" >
+
+        <Button android:id="@+id/set"
+            android:layout_width="120px"
+            android:layout_height="wrap_content"
+            android:text="@string/weekpicker_set"
+            android:layout_alignParentRight="true" />
+    </RelativeLayout>
+
+</LinearLayout>
+
diff --git a/res/layout-keysexposed/create_playlist.xml b/res/layout-keysexposed/create_playlist.xml
new file mode 100644 (file)
index 0000000..1bf2252
--- /dev/null
@@ -0,0 +1,59 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content">
+
+    <TextView android:id="@+id/prompt"
+        android:layout_width="fill_parent" android:layout_height="wrap_content"
+        android:text="@string/create_playlist_create_text_prompt"
+        android:layout_marginTop="8dip"
+        android:layout_marginBottom="8dip"
+        android:layout_marginLeft="8dip"
+        android:layout_marginRight="8dip">
+    </TextView>
+
+    <EditText android:id="@+id/playlist"
+        android:layout_width="fill_parent" android:layout_height="wrap_content"
+        android:singleLine="true"
+        android:layout_marginBottom="8dip"
+        android:layout_marginLeft="8dip"
+        android:layout_marginRight="8dip">
+        <requestFocus />
+    </EditText>
+
+    <RelativeLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:padding="6px"
+        android:background="#ffffff" >
+
+        <Button android:id="@+id/create"
+            android:layout_width="120px" android:layout_height="wrap_content" 
+            android:text="@string/create_playlist_create_text"
+            android:layout_alignParentLeft="true" />
+
+        <Button android:id="@+id/cancel"
+            android:layout_width="120px" android:layout_height="wrap_content" 
+            android:text="@string/cancel"
+            android:layout_alignParentRight="true" />
+
+    </RelativeLayout>
+
+</LinearLayout>
+
diff --git a/res/layout-keyshidden/create_playlist.xml b/res/layout-keyshidden/create_playlist.xml
new file mode 100644 (file)
index 0000000..bb5c6ac
--- /dev/null
@@ -0,0 +1,70 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content">
+
+    <LinearLayout
+        android:orientation="horizontal"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="8dip"
+        android:layout_marginTop="8dip"
+        android:layout_marginLeft="8dip"
+        android:layout_marginRight="8dip">
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/ic_slide_keyboard"/>
+
+        <TextView android:id="@+id/prompt"
+            android:layout_width="fill_parent" android:layout_height="wrap_content"
+            android:text="@string/create_playlist_create_text_prompt"
+            android:layout_marginLeft="8dip">
+        </TextView>
+    </LinearLayout>
+
+    <EditText android:id="@+id/playlist"
+        android:layout_width="fill_parent" android:layout_height="wrap_content"
+        android:singleLine="true"
+        android:visibility="gone"
+        android:layout_marginBottom="8dip">
+        <requestFocus />
+    </EditText>
+
+    <RelativeLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:padding="6px"
+        android:background="#ffffff" >
+
+        <Button android:id="@+id/create"
+            android:layout_width="120px" android:layout_height="wrap_content" 
+            android:text="@string/create_playlist_create_text"
+            android:layout_alignParentLeft="true" />
+
+        <Button android:id="@+id/cancel"
+            android:layout_width="120px" android:layout_height="wrap_content" 
+            android:text="@string/cancel"
+            android:layout_alignParentRight="true" />
+
+    </RelativeLayout>
+
+</LinearLayout>
+
diff --git a/res/layout-land-finger/audio_player.xml b/res/layout-land-finger/audio_player.xml
new file mode 100644 (file)
index 0000000..d6baa5e
--- /dev/null
@@ -0,0 +1,145 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical">
+
+
+    <!-- This is the LinearLayout that contains the album art, function buttons and album/artist/track info -->
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1"
+        android:orientation="horizontal">
+
+        <com.android.music.AlbumView android:id="@+id/album"
+            android:layout_width="200px"
+            android:layout_height="204px"
+            android:paddingLeft="20px"
+            android:paddingRight="15px"
+            android:paddingTop="20px" />
+
+        <!-- This is the LinearLayout that contains function buttons and album/artist/track info -->
+        <LinearLayout
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:orientation="vertical">
+
+            <LinearLayout
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:paddingTop="20px" >
+
+               <ImageButton android:id="@+id/curplaylist"
+                    android:src="@drawable/ic_mp_current_playlist_btn"
+                    android:layout_width="82px"
+                    android:layout_height="45px"
+                    android:layout_marginRight="8px" />
+
+               <ImageButton android:id="@+id/shuffle"
+                    android:layout_width="82px"
+                    android:layout_height="45px"
+                    android:layout_marginRight="8px" />
+
+               <ImageButton android:id="@+id/repeat"
+                    android:layout_width="82px"
+                    android:layout_height="45px" />
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:baselineAligned="false"
+                android:paddingTop="8dip"
+                android:paddingBottom="2dip">
+
+                <ImageView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginRight="4dip"
+                    android:src="@drawable/ic_mp_artist_playback" />
+
+                <TextView android:id="@+id/artistname"
+                    android:textSize="18sp"
+                    android:maxLines="1"
+                    android:textStyle="bold"
+                    android:layout_gravity="center_vertical"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content" />
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:baselineAligned="false"
+                android:paddingTop="8dip"
+                android:paddingBottom="2dip">
+
+                <ImageView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginRight="4dip"
+                    android:src="@drawable/ic_mp_album_playback" />
+
+                <TextView android:id="@+id/albumname"
+                    android:textSize="14sp"
+                    android:maxLines="1"
+                    android:textStyle="bold"
+                    android:layout_gravity="center_vertical"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content" />
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:baselineAligned="false"
+                android:paddingTop="4dip"
+                android:paddingBottom="2dip">
+
+                <ImageView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginRight="4dip"
+                    android:src="@drawable/ic_mp_song_playback" />
+
+                <TextView android:id="@+id/trackname"
+                    android:textSize="14sp"
+                    android:maxLines="1"
+                    android:textStyle="bold"
+                    android:layout_gravity="center_vertical"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content" />
+
+            </LinearLayout>
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+    <include layout="@layout/audio_player_common" />
+
+</LinearLayout>
diff --git a/res/layout-land-finger/music_library.xml b/res/layout-land-finger/music_library.xml
new file mode 100644 (file)
index 0000000..c7a436c
--- /dev/null
@@ -0,0 +1,169 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1"
+        android:paddingTop="12px"
+        android:paddingLeft="12px"
+        android:paddingRight="12px"
+        android:orientation="horizontal"
+        >
+
+        <LinearLayout
+            android:layout_weight="1"
+            android:layout_width="0dip"
+            android:layout_height="fill_parent"
+            android:orientation="vertical"
+            >
+
+            <ImageButton
+                android:background="@drawable/main_menu_button"
+                android:id="@+id/browse_button"
+                android:layout_width="fill_parent"
+                android:layout_height="0dip"
+                android:layout_weight="1"
+                android:src="@drawable/ic_mp_screen_artists"
+            />
+
+            <TextView
+                android:textAppearance="?android:attr/textAppearanceLarge"
+                android:layout_gravity="center_horizontal"
+                android:gravity="center"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:maxLines="1" android:scrollHorizontally="true"
+                android:text="@string/browse_menu"
+                android:layout_marginRight="8px"
+            />
+
+            <ImageButton
+                android:background="@drawable/main_menu_button"
+                android:id="@+id/tracks_button"
+                android:layout_width="fill_parent"
+                android:layout_height="0dip"
+                android:layout_weight="1"
+                android:src="@drawable/ic_mp_screen_tracks"
+            />
+
+            <TextView
+                android:textAppearance="?android:attr/textAppearanceLarge"
+                android:layout_gravity="center_horizontal"
+                android:gravity="center"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:maxLines="1" android:scrollHorizontally="true"
+                android:text="@string/tracks_menu"
+                android:layout_marginRight="8px"
+            />
+        </LinearLayout>
+
+        <View
+            android:layout_width="12px"
+            android:layout_height="10px" />
+
+        <LinearLayout
+            android:layout_weight="1"
+            android:layout_width="0dip"
+            android:layout_height="fill_parent"
+            android:orientation="vertical"
+            >
+
+            <ImageButton
+                android:background="@drawable/main_menu_button"
+                android:id="@+id/albums_button"
+                android:layout_width="fill_parent"
+                android:layout_height="0dip"
+                android:layout_weight="1"
+                android:src="@drawable/ic_mp_screen_albums"
+            />
+
+            <TextView
+                android:textAppearance="?android:attr/textAppearanceLarge"
+                android:layout_gravity="center_horizontal"
+                android:gravity="center"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:maxLines="1" android:scrollHorizontally="true"
+                android:text="@string/albums_menu"
+                android:layout_marginLeft="8px"
+            />
+
+            <ImageButton
+                android:background="@drawable/main_menu_button"
+                android:id="@+id/playlists_button"
+                android:layout_width="fill_parent"
+                android:layout_height="0dip"
+                android:layout_weight="1"
+                android:src="@drawable/ic_mp_screen_playlists"
+            />
+            <TextView
+                android:textAppearance="?android:attr/textAppearanceLarge"
+                android:layout_gravity="center_horizontal"
+                android:gravity="center"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:maxLines="1" android:scrollHorizontally="true"
+                android:text="@string/playlists_menu"
+                android:layout_marginLeft="8px"
+            />
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/nowplaying"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:focusable="true"
+        android:background="#ff404040"
+        android:visibility="invisible"
+        android:orientation="horizontal">
+
+        <ImageView android:id="@+id/icon"
+            style="@android:style/MediaButton.Play"
+            android:layout_marginLeft="8dip"
+            android:layout_marginTop="4dip"
+            android:layout_marginRight="6dip"
+            android:layout_marginBottom="4dip" />
+
+        <LinearLayout
+            android:layout_width="0dip"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:orientation="vertical">
+
+            <TextView android:id="@+id/title" android:textAppearance="?android:attr/textAppearanceLarge"
+                android:singleLine="true" android:ellipsize="end"
+                android:layout_marginLeft="3dip"
+                android:layout_width="fill_parent" android:layout_height="wrap_content" />
+            <TextView android:id="@+id/artist" android:textAppearance="?android:attr/textAppearanceMedium"
+                android:singleLine="true" android:ellipsize="end"
+                android:layout_marginLeft="3dip"
+                android:layout_width="fill_parent" android:layout_height="wrap_content" />
+        </LinearLayout>
+    </LinearLayout>   
+</LinearLayout>
+
diff --git a/res/layout/function_picker.xml b/res/layout/function_picker.xml
new file mode 100644 (file)
index 0000000..c3c2bd4
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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="wrap_content">
+
+    <LinearLayout android:id="@+id/buttoncontainer"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+            
+    </LinearLayout>
+</FrameLayout>
diff --git a/res/layout/media_picker_activity.xml b/res/layout/media_picker_activity.xml
new file mode 100644 (file)
index 0000000..5a81285
--- /dev/null
@@ -0,0 +1,26 @@
+<?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.
+-->
+
+<com.android.music.TouchInterceptor xmlns:android="http://schemas.android.com/apk/res/android"
+
+    android:id="@android:id/list"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:textSize="18sp"
+    android:drawSelectorOnTop="false"
+
+/>
+
diff --git a/res/layout/movie_view.xml b/res/layout/movie_view.xml
new file mode 100644 (file)
index 0000000..6344d8b
--- /dev/null
@@ -0,0 +1,48 @@
+<?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: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/music_library.xml b/res/layout/music_library.xml
new file mode 100644 (file)
index 0000000..e413a0e
--- /dev/null
@@ -0,0 +1,169 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1"
+        android:paddingTop="17px"
+        android:paddingLeft="14px"
+        android:paddingRight="14px"
+        android:orientation="horizontal"
+        >
+
+        <LinearLayout
+            android:layout_weight="1"
+            android:layout_width="0dip"
+            android:layout_height="fill_parent"
+            android:orientation="vertical"
+            >
+
+            <ImageButton
+                android:background="@drawable/main_menu_button"
+                android:id="@+id/browse_button"
+                android:layout_width="fill_parent"
+                android:layout_height="0dip"
+                android:layout_weight="1"
+                android:src="@drawable/ic_mp_screen_artists"
+            />
+
+            <TextView
+                android:textAppearance="?android:attr/textAppearanceLarge"
+                android:layout_gravity="center_horizontal"
+                android:gravity="center"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:maxLines="1" android:scrollHorizontally="true"
+                android:text="@string/browse_menu"
+                android:layout_marginBottom="24px"
+            />
+
+            <ImageButton
+                android:background="@drawable/main_menu_button"
+                android:id="@+id/tracks_button"
+                android:layout_width="fill_parent"
+                android:layout_height="0dip"
+                android:layout_weight="1"
+                android:src="@drawable/ic_mp_screen_tracks"
+            />
+
+            <TextView
+                android:textAppearance="?android:attr/textAppearanceLarge"
+                android:layout_gravity="center_horizontal"
+                android:gravity="center"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:maxLines="1" android:scrollHorizontally="true"
+                android:text="@string/tracks_menu"
+                android:layout_marginBottom="24px"
+            />
+        </LinearLayout>
+
+        <View
+            android:layout_width="18px"
+            android:layout_height="10px" />
+
+        <LinearLayout
+            android:layout_weight="1"
+            android:layout_width="0dip"
+            android:layout_height="fill_parent"
+            android:orientation="vertical"
+            >
+
+            <ImageButton
+                android:background="@drawable/main_menu_button"
+                android:id="@+id/albums_button"
+                android:layout_width="fill_parent"
+                android:layout_height="0dip"
+                android:layout_weight="1"
+                android:src="@drawable/ic_mp_screen_albums"
+            />
+
+            <TextView
+                android:textAppearance="?android:attr/textAppearanceLarge"
+                android:layout_gravity="center_horizontal"
+                android:gravity="center"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:maxLines="1" android:scrollHorizontally="true"
+                android:text="@string/albums_menu"
+                android:layout_marginBottom="24px"
+            />
+
+            <ImageButton
+                android:background="@drawable/main_menu_button"
+                android:id="@+id/playlists_button"
+                android:layout_width="fill_parent"
+                android:layout_height="0dip"
+                android:layout_weight="1"
+                android:src="@drawable/ic_mp_screen_playlists"
+            />
+            <TextView
+                android:textAppearance="?android:attr/textAppearanceLarge"
+                android:layout_gravity="center_horizontal"
+                android:gravity="center"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:maxLines="1" android:scrollHorizontally="true"
+                android:text="@string/playlists_menu"
+                android:layout_marginBottom="24px"
+            />
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/nowplaying"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:focusable="true"
+        android:background="#ff404040"
+        android:visibility="invisible"
+        android:orientation="horizontal">
+
+        <ImageView android:id="@+id/icon"
+            style="@android:style/MediaButton.Play"
+            android:layout_marginLeft="8dip"
+            android:layout_marginTop="4dip"
+            android:layout_marginRight="6dip"
+            android:layout_marginBottom="4dip" />
+
+        <LinearLayout
+            android:layout_width="0dip"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:orientation="vertical">
+
+            <TextView android:id="@+id/title" android:textAppearance="?android:attr/textAppearanceLarge"
+                android:singleLine="true" android:ellipsize="end"
+                android:layout_marginLeft="3dip"
+                android:layout_width="fill_parent" android:layout_height="wrap_content" />
+            <TextView android:id="@+id/artist" android:textAppearance="?android:attr/textAppearanceMedium"
+                android:singleLine="true" android:ellipsize="end"
+                android:layout_marginLeft="3dip"
+                android:layout_width="fill_parent" android:layout_height="wrap_content" />
+        </LinearLayout>
+    </LinearLayout>   
+</LinearLayout>
+
diff --git a/res/layout/query_activity.xml b/res/layout/query_activity.xml
new file mode 100644 (file)
index 0000000..de30f53
--- /dev/null
@@ -0,0 +1,28 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical">
+
+    <ListView android:id="@android:id/list"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:drawSelectorOnTop="false">
+    </ListView>
+</LinearLayout>
+
diff --git a/res/layout/scanning.xml b/res/layout/scanning.xml
new file mode 100644 (file)
index 0000000..ff3caa6
--- /dev/null
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:padding="12px">
+
+    <ProgressBar android:id="@+id/spinner"
+        style="?android:attr/progressBarStyleLarge"
+        android:layout_gravity="center"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+    <TextView android:id="@+id/message"
+        android:layout_gravity="center"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:text="@string/scanning">
+    </TextView>
+
+</LinearLayout>
+
diff --git a/res/layout/statusbar.xml b/res/layout/statusbar.xml
new file mode 100644 (file)
index 0000000..b834631
--- /dev/null
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="horizontal">
+
+    <ImageView android:id="@+id/icon"
+        android:padding="4dip"
+        android:gravity="center"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+    </ImageView>
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <TextView android:id="@+id/trackname"
+            android:textAppearance="?android:attr/textAppearanceMediumInverse"
+            android:layout_gravity="left"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+        <TextView android:id="@+id/artistalbum"
+            android:textAppearance="?android:attr/textAppearanceSmallInverse"
+            android:layout_gravity="left"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
new file mode 100644 (file)
index 0000000..b21d7de
--- /dev/null
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <string name="NNNtracksdeleted">Bylo odstraněno <xliff:g id="songs_to_delete">%s</xliff:g> skladeb</string>
+  <string name="NNNtrackstoplaylist">Do seznamu stop bylo přidáno NNN skladeb</string>
+  <string name="Nalbums"><xliff:g id="count">%d</xliff:g> alb</string>
+  <string name="Nsongs"><xliff:g id="count">%d</xliff:g> skladeb</string>
+  <string name="add_to_playlist">Přidat do seznamu stop</string>
+  <string name="albums_menu">Alba</string>
+  <string name="albums_title">Alba</string>
+  <string name="all_title">Všechna média</string>
+  <string name="artists_title">Interpreti</string>
+  <string name="browse_menu">Interpreti</string>
+  <string name="create_playlist_create_text">Uložit</string>
+  <string name="create_playlist_overwrite_text">Přepsat</string>
+  <string name="delete_album_desc">celé album \"<xliff:g id="album">%s</xliff:g>\"</string>
+  <string name="delete_artist_desc">všechny skladby od interpreta <xliff:g id="artist">%s</xliff:g></string>
+  <string name="delete_confirm_button_text">Odstranit</string>
+  <string name="delete_confirm_prompt">Trvale odstranit <xliff:g id="to_delete">%s</xliff:g> z karty SD?</string>
+  <string name="delete_item">Odstranit</string>
+  <string name="delete_playlist_menu">Odstranit</string>
+  <string name="durationformat"><xliff:g id="format">%2$d:%5$02d</xliff:g></string>
+  <string name="edit_playlist_menu">Upravit</string>
+  <string name="emptyplaylist">Vybraný seznam stop je prázdný</string>
+  <string name="goto_playback">Přehrávání</string>
+  <string name="goto_start">Knihovna</string>
+  <string name="mediapickerlabel">Hudba</string>
+  <string name="mediaplaybacklabel">Hudba</string>
+  <string name="movieviewlabel">Filmy</string>
+  <string name="musicbrowserlabel">Hudba</string>
+  <string name="musicsearch">Hudba</string>
+  <string name="musicsearchhint">Interpret, album nebo název skladby</string>
+  <string name="new_playlist">Nový\u2026</string>
+  <string name="new_playlist_name_template">Nový seznam stop <xliff:g id="number">%d</xliff:g></string>
+  <string name="no_albums_title">Žádná alba</string>
+  <string name="no_playlists_title">Žádné seznamy stop</string>
+  <string name="no_tracks_title">Žádné skladby</string>
+  <string name="no_videos_title">Žádná videa</string>
+  <string name="nowplaying_title">Přehrávání</string>
+  <string name="onealbum">1 album</string>
+  <string name="onesong">1 skladba</string>
+  <string name="party_shuffle">Náhodné přehrávání</string>
+  <string name="party_shuffle_off">Náhodné přehrávání vypnuto</string>
+  <string name="partyshuffle_title">Náhodné přehrávání</string>
+  <string name="play_all">Přehrát vše</string>
+  <string name="play_selection">Přehrát</string>
+  <string name="playlists_menu">Seznamy stop</string>
+  <string name="playlists_title">Seznamy stop</string>
+  <string name="recentlyadded">Naposledy přidané</string>
+  <string name="rename_playlist_menu">Přejmenovat</string>
+  <string name="repeat_all_notif">Opakování všech skladeb</string>
+  <string name="repeat_current_notif">Opakování aktuální skladby</string>
+  <string name="repeat_off_notif">Opakování je vypnuto</string>
+  <string name="ringtone_menu">Použít jako vyzv. tón</string>
+  <string name="save_as_playlist">Uložit jako seznam stop</string>
+  <string name="scanning">Prohledávání karty SD\u2026</string>
+  <string name="sdcard_busy_title">Karta SD je zaneprázdněna</string>
+  <string name="sdcard_error_title">Chyba karty SD</string>
+  <string name="sdcard_missing_title">Karta SD chybí</string>
+  <string name="search_title">Hledat</string>
+  <string name="service_start_error_button">OK</string>
+  <string name="service_start_error_msg">Nelze spustit službu přehrávání médií</string>
+  <string name="service_start_error_title">Chyba</string>
+  <string name="shuffle_all">Náhodně vše</string>
+  <string name="shuffle_off_notif">Náhodné přehrávání je vypnuto</string>
+  <string name="shuffle_on_notif">Náhodné přehrávání je zapnuto</string>
+  <string name="tracks_menu">Skladby</string>
+  <string name="tracks_title">Skladby</string>
+  <string name="unknown_album_name">Neznámé album</string>
+  <string name="unknown_artist_name">Neznámý interpret</string>
+  <string name="videobrowserlabel">Videa</string>
+  <string name="videos_title">Videa</string>
+  <string name="weekpicker_set">Hotovo</string>
+  <string name="weekpicker_title">Nastavit čas</string>
+</resources>
diff --git a/res/values-de-rDE/strings.xml b/res/values-de-rDE/strings.xml
new file mode 100644 (file)
index 0000000..61f1e64
--- /dev/null
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <string name="add_to_playlist">Zur Wiedergabeliste hinzufügen</string>
+  <string name="albums_menu">Alben</string>
+  <string name="albums_title">Alben</string>
+  <string name="all_title">Alle Medien</string>
+  <string name="artists_title">Künstler</string>
+  <string name="browse_menu">Interpreten</string>
+  <string name="cancel">Abbrechen</string>
+  <string name="clear_playlist">Wiedergabeliste löschen</string>
+  <string name="create_playlist_create_text">Speichern</string>
+  <string name="create_playlist_overwrite_text">Überschreiben</string>
+  <string name="delete_album_desc">das gesamte Album \"<xliff:g id="album">%s</xliff:g>\"</string>
+  <string name="delete_artist_desc">alle Lieder von <xliff:g id="artist">%s</xliff:g></string>
+  <string name="delete_confirm_button_text">OK</string>
+  <string name="delete_confirm_prompt">\"<xliff:g id="to_delete">%s</xliff:g>\" wird permanent aus der SD-Karte entfernt.</string>
+  <string name="delete_item">Löschen</string>
+  <string name="delete_playlist_menu">Löschen</string>
+  <string name="durationformat">
+                                       <xliff:g id="format">%2$d:%5$02d</xliff:g>
+                               </string>
+  <string name="edit_playlist_menu">Bearbeiten</string>
+  <string name="emptyplaylist">Die ausgewählte Wiedergabeliste ist leer.</string>
+  <string name="goto_playback">Wiedergabe</string>
+  <string name="goto_start">Bibliothek</string>
+  <string name="loading_video">Lädt Video\u2026</string>
+  <string name="mediapickerlabel">Musik</string>
+  <string name="mediaplaybacklabel">Musik</string>
+  <string name="mediasearch">Suche nach %s mit:</string>
+  <string name="mediasearch_amazon">Amazon MP3</string>
+  <string name="mediasearch_google">Google</string>
+  <string name="mediasearch_youtube">YouTube</string>
+  <string name="movieviewlabel">Filme</string>
+  <string name="musicbrowserlabel">Musik</string>
+  <string name="musicsearch">Musik</string>
+  <string name="musicsearchhint">Künstler, Album oder Liedtitel</string>
+  <string name="musicshortcutlabel">Musikwiedergabeliste</string>
+  <string name="new_playlist">Neu</string>
+  <string name="new_playlist_name_template">Neue Wiedergabeliste <xliff:g id="number">%d</xliff:g></string>
+  <string name="no_albums_title">Keine Alben</string>
+  <string name="no_playlists_title">Keine Wiedergabelisten</string>
+  <string name="no_tracks_title">Keine Lieder</string>
+  <string name="no_videos_title">Keine Videos</string>
+  <string name="nowplaying_title">Aktuelle Wiedergabe</string>
+  <string name="onesong">1 Lied</string>
+  <string name="party_shuffle">Partymix</string>
+  <string name="party_shuffle_off">Partymix aus</string>
+  <string name="partyshuffle_title">Partymix</string>
+  <string name="play_all">Alle wiedergeben</string>
+  <string name="play_selection">Wiedergabe</string>
+  <string name="playback_failed">Der Player unterstützt diese Art von Audiodatei nicht."</string>
+  <string name="playlist_deleted_message">Wiedergabeliste gelöscht.</string>
+  <string name="playlist_renamed_message">Wiedergabeliste umbenannt.</string>
+  <string name="playlists_menu">Wiedergabelisten</string>
+  <string name="playlists_title">Wiedergabelisten</string>
+  <string name="queue">Aktuelle Wiedergabeliste</string>
+  <string name="recentlyadded">Zuletzt hinzugefügt</string>
+  <string name="remove_from_playlist">Aus Wiedergabeliste entfernen</string>
+  <string name="rename_playlist_menu">Umbenennen</string>
+  <string name="repeat_all_notif">Alle Lieder wiederholen.</string>
+  <string name="repeat_current_notif">Aktuelles Lied wiederholen.</string>
+  <string name="repeat_off_notif">Wiederholung aus.</string>
+  <string name="ringtone_menu">Als Telefonklingelton verwenden</string>
+  <string name="ringtone_menu_short">Als Klingelton verwenden</string>
+  <string name="ringtone_set">\"%s\" als Klingelton einrichten.</string>
+  <string name="save_as_playlist">Als Wiedergabeliste speichern</string>
+  <string name="scanning">SD-Karte durchsuchen\u2026</string>
+  <string name="sdcard_busy_message">Die SD-Karte wird derzeit benutzt. Um auf die Musik zuzugreifen, gehen Sie nach Startseite &gt; Einstellungen &gt; SD-Karte &amp; Telefonspeicher und heben Sie die Auswahl von \"Für USB-Speicher verwenden\" auf.</string>
+  <string name="sdcard_busy_title">SD-Karte wird benutzt</string>
+  <string name="sdcard_error_message">SD-Karten-Fehler.</string>
+  <string name="sdcard_error_title">SD-Karten-Fehler</string>
+  <string name="sdcard_missing_message">Keine SD-Karte.</string>
+  <string name="sdcard_missing_title">Keine SD-Karte</string>
+  <string name="search_title">Suchen</string>
+  <string name="service_start_error_button">OK</string>
+  <string name="service_start_error_msg">Das Lied konnte leider nicht wiedergegeben werden.</string>
+  <string name="service_start_error_title">Wiedergabeproblem</string>
+  <string name="shuffle_all">Alle zufällig wiedergeben</string>
+  <string name="shuffle_off_notif">Zufällige Wiedergabe aus.</string>
+  <string name="shuffle_on_notif">Zufällige Wiedergabe ein.</string>
+  <string name="streamloadingtext">Verbindet mit <xliff:g id="host">%s</xliff:g></string>
+  <string name="tracks_menu">Lieder</string>
+  <string name="tracks_title">Lieder</string>
+  <string name="unknown_album_name">Unbekanntes Album</string>
+  <string name="unknown_artist_name">Unbekannter Künstler</string>
+  <string name="videobrowserlabel">Videos</string>
+  <string name="videos_title">Videos</string>
+  <string name="weekpicker_set">Fertig</string>
+  <string name="weekpicker_title">Zeit einstellen</string>
+</resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
new file mode 100644 (file)
index 0000000..a6714ea
--- /dev/null
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <string name="add_to_playlist">Add to playlist</string>
+  <string name="albums_menu">Albums</string>
+  <string name="albums_title">Albums</string>
+  <string name="all_title">All media</string>
+  <string name="artists_title">Artists</string>
+  <string name="browse_menu">Artists</string>
+  <string name="cancel">Cancel</string>
+  <string name="clear_playlist">Clear playlist</string>
+  <string name="create_playlist_create_text">Save</string>
+  <string name="create_playlist_overwrite_text">Overwrite</string>
+  <string name="delete_album_desc">the entire album \"<xliff:g id="album">%s</xliff:g>\"</string>
+  <string name="delete_artist_desc">all songs by <xliff:g id="artist">%s</xliff:g></string>
+  <string name="delete_confirm_button_text">OK</string>
+  <string name="delete_confirm_prompt">\"<xliff:g id="to_delete">%s</xliff:g>\" will be permanently deleted from the SD card.</string>
+  <string name="delete_item">Delete</string>
+  <string name="delete_playlist_menu">Delete</string>
+  <string name="durationformat"><xliff:g id="format">%2$d:%5$02d</xliff:g></string>
+  <string name="edit_playlist_menu">Edit</string>
+  <string name="emptyplaylist">Selected playlist is empty.</string>
+  <string name="goto_playback">Playback</string>
+  <string name="goto_start">Library</string>
+  <string name="loading_video">Loading video\u2026</string>
+  <string name="mediapickerlabel">Music</string>
+  <string name="mediaplaybacklabel">Music</string>
+  <string name="movieviewlabel">Movies</string>
+  <string name="musicbrowserlabel">Music</string>
+  <string name="musicsearch">Music</string>
+  <string name="musicsearchhint">Artist, album, or song name</string>
+  <string name="musicshortcutlabel">Music playlist</string>
+  <string name="new_playlist">New\u2026</string>
+  <string name="new_playlist_name_template">New playlist <xliff:g id="number">%d</xliff:g></string>
+  <string name="no_albums_title">No albums</string>
+  <string name="no_playlists_title">No playlists</string>
+  <string name="no_tracks_title">No songs</string>
+  <string name="no_videos_title">No videos</string>
+  <string name="nowplaying_title">Now playing</string>
+  <string name="onesong">1 song</string>
+  <string name="party_shuffle">Party shuffle</string>
+  <string name="party_shuffle_off">Party shuffle off</string>
+  <string name="partyshuffle_title">Party shuffle</string>
+  <string name="play_all">Play all</string>
+  <string name="play_selection">Play</string>
+  <string name="playback_failed">Sorry, the player does not support this type of audio file."</string>
+  <string name="playlist_deleted_message">Playlist deleted.</string>
+  <string name="playlist_renamed_message">Playlist renamed.</string>
+  <string name="playlists_menu">Playlists</string>
+  <string name="playlists_title">Playlists</string>
+  <string name="queue">Current playlist</string>
+  <string name="recentlyadded">Recently added</string>
+  <string name="remove_from_playlist">Remove from playlist</string>
+  <string name="rename_playlist_menu">Rename</string>
+  <string name="repeat_all_notif">Repeating all songs.</string>
+  <string name="repeat_current_notif">Repeating current song.</string>
+  <string name="repeat_off_notif">Repeat is off.</string>
+  <string name="ringtone_menu">Use as phone ringtone</string>
+  <string name="ringtone_menu_short">Use as ringtone</string>
+  <string name="ringtone_set">\"%s\" set as phone ringtone.</string>
+  <string name="save_as_playlist">Save as playlist</string>
+  <string name="scanning">Scanning SD card\u2026</string>
+  <string name="sdcard_busy_title">SD card is busy</string>
+  <string name="sdcard_error_title">SD card error</string>
+  <string name="sdcard_missing_title">No SD card</string>
+  <string name="search_title">Search</string>
+  <string name="service_start_error_button">OK</string>
+  <string name="service_start_error_msg">Sorry, the song could not be played.</string>
+  <string name="service_start_error_title">Playback problem</string>
+  <string name="shuffle_all">Shuffle all</string>
+  <string name="shuffle_off_notif">Shuffle is off.</string>
+  <string name="shuffle_on_notif">Shuffle is on.</string>
+  <string name="tracks_menu">Songs</string>
+  <string name="tracks_title">Songs</string>
+  <string name="unknown_album_name">Unknown album</string>
+  <string name="unknown_artist_name">Unknown artist</string>
+  <string name="videobrowserlabel">Videos</string>
+  <string name="videos_title">Videos</string>
+  <string name="weekpicker_set">Done</string>
+  <string name="weekpicker_title">Set time</string>
+</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
new file mode 100644 (file)
index 0000000..533647a
--- /dev/null
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <string name="add_to_playlist">Añadir a Lista de reproducción</string>
+  <string name="albums_menu">Álbumes</string>
+  <string name="albums_title">Álbumes</string>
+  <string name="all_title">Todos los medios</string>
+  <string name="artists_title">Artistas</string>
+  <string name="browse_menu">Artistas</string>
+  <string name="cancel">Cancelar</string>
+  <string name="clear_playlist">Borrar lista de reproducción</string>
+  <string name="create_playlist_create_text">Guardar</string>
+  <string name="create_playlist_overwrite_text">Sobrescribir</string>
+  <string name="delete_album_desc">todo el álbum \"<xliff:g id="album">%s</xliff:g>\"</string>
+  <string name="delete_artist_desc">todas las canciones de <xliff:g id="artist">%s</xliff:g></string>
+  <string name="delete_confirm_button_text">Aceptar</string>
+  <string name="delete_confirm_prompt">\"<xliff:g id="to_delete">%s</xliff:g>\" se eliminará de forma permanente de la tarjeta SD. </string>
+  <string name="delete_item">Eliminar</string>
+  <string name="delete_playlist_menu">Eliminar</string>
+  <string name="durationformat">
+                                       <xliff:g id="format">%2$d:%5$02d</xliff:g>
+                               </string>
+  <string name="edit_playlist_menu">Editar</string>
+  <string name="emptyplaylist">La lista de reproducción seleccionada está vacía. </string>
+  <string name="goto_playback">Reproducción</string>
+  <string name="goto_start">Biblioteca</string>
+  <string name="loading_video">Cargando vídeo\u2026</string>
+  <string name="mediapickerlabel">Música</string>
+  <string name="mediaplaybacklabel">Música</string>
+  <string name="mediasearch">Buscar %s usando:</string>
+  <string name="mediasearch_amazon">Amazon MP3</string>
+  <string name="mediasearch_google">Google</string>
+  <string name="mediasearch_youtube">YouTube</string>
+  <string name="movieviewlabel">Películas</string>
+  <string name="musicbrowserlabel">Música</string>
+  <string name="musicsearch">Música</string>
+  <string name="musicsearchhint">Artista, álbum o nombre de canción</string>
+  <string name="musicshortcutlabel">Lista de música</string>
+  <string name="new_playlist">Nuevo</string>
+  <string name="new_playlist_name_template">Nueva lista de reproducción <xliff:g id="number">%d</xliff:g></string>
+  <string name="no_albums_title">No hay álbumes</string>
+  <string name="no_playlists_title">No hay listas de reproducción</string>
+  <string name="no_tracks_title">No hay canciones</string>
+  <string name="no_videos_title">No hay vídeos</string>
+  <string name="nowplaying_title">Reproduciendo</string>
+  <string name="onesong">1 canción</string>
+  <string name="party_shuffle">Lista aleatoria</string>
+  <string name="party_shuffle_off">Lista aleatoria desactivada</string>
+  <string name="partyshuffle_title">Lista aleatoria</string>
+  <string name="play_all">Reproducir todo</string>
+  <string name="play_selection">Reproducir</string>
+  <string name="playback_failed">Lo siento, el reproductor no admite este tipo de archivo de audio. "</string>
+  <string name="playlist_deleted_message">Lista de reproducción eliminada.</string>
+  <string name="playlist_renamed_message">Lista de reproducción con un nombre diferente.</string>
+  <string name="playlists_menu">Listas de reproducción</string>
+  <string name="playlists_title">Listas de reproducción</string>
+  <string name="queue">Lista de reproducción actual</string>
+  <string name="recentlyadded">Agregado recientemente</string>
+  <string name="remove_from_playlist">Eliminar de la lista de reproducción</string>
+  <string name="rename_playlist_menu">Cambiar nombre</string>
+  <string name="repeat_all_notif">Repitiendo todas las canciones.</string>
+  <string name="repeat_current_notif">Repitiendo la canción actual</string>
+  <string name="repeat_off_notif">La repetición está desactivada.</string>
+  <string name="ringtone_menu">Utilizar como tono de timbre.</string>
+  <string name="ringtone_menu_short">Utilizar como tono de timbre</string>
+  <string name="ringtone_set">\"%s\" configurado como tono de timbre del teléfono.</string>
+  <string name="save_as_playlist">Guardar como lista de reproducción</string>
+  <string name="scanning">Explorando tarjeta SD\u2026</string>
+  <string name="sdcard_busy_message">La tarjeta SD está ocupada. Para acceder a su música, vaya a Inicio &gt; Configuración &gt; Tarjeta SD y memoria de almacenamiento y desactive la casilla de verificación \"Utilizar para almacenamiento USB\".</string>
+  <string name="sdcard_busy_title">La tarjeta SD está ocupada</string>
+  <string name="sdcard_error_message">Error de la tarjeta SD.</string>
+  <string name="sdcard_error_title">Error de la tarjeta SD</string>
+  <string name="sdcard_missing_message">Ninguna tarjeta SD.</string>
+  <string name="sdcard_missing_title">Ninguna tarjeta SD</string>
+  <string name="search_title">Buscar</string>
+  <string name="service_start_error_button">Aceptar</string>
+  <string name="service_start_error_msg">Lo sentimos, no se ha podido reproducir la canción.</string>
+  <string name="service_start_error_title">Problema de reproducción</string>
+  <string name="shuffle_all">Todo en orden aleatorio</string>
+  <string name="shuffle_off_notif">La opción orden aleatorio está desactivada.</string>
+  <string name="shuffle_on_notif">La opción orden aleatorio está activada.</string>
+  <string name="streamloadingtext">Conectando con <xliff:g id="host">%s</xliff:g></string>
+  <string name="tracks_menu">Canciones</string>
+  <string name="tracks_title">Canciones</string>
+  <string name="unknown_album_name">Álbum desconocido</string>
+  <string name="unknown_artist_name">Artista desconocido</string>
+  <string name="videobrowserlabel">Vídeos</string>
+  <string name="videos_title">Vídeos</string>
+  <string name="weekpicker_set">Listo</string>
+  <string name="weekpicker_title">Establecer hora</string>
+</resources>
diff --git a/res/values-finger/strings2.xml b/res/values-finger/strings2.xml
new file mode 100644 (file)
index 0000000..a90dbfa
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="albumsongseparator">\n</string>
+
+    <string name="notification_artist_album"><xliff:g id="album">%2$s</xliff:g>\n<xliff:g id="artist">%1$s</xliff:g></string>
+</resources>
+
diff --git a/res/values-fr-rFR/strings.xml b/res/values-fr-rFR/strings.xml
new file mode 100644 (file)
index 0000000..f1dfea9
--- /dev/null
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <string name="add_to_playlist">Ajouter à sélection</string>
+  <string name="albums_menu">Albums</string>
+  <string name="albums_title">Albums</string>
+  <string name="all_title">Tous les médias</string>
+  <string name="artists_title">Artistes</string>
+  <string name="browse_menu">Artistes</string>
+  <string name="cancel">Annuler</string>
+  <string name="clear_playlist">Effacer la sélection</string>
+  <string name="create_playlist_create_text">Enregistrer</string>
+  <string name="create_playlist_overwrite_text">Écraser</string>
+  <string name="delete_album_desc">l\'album entier \"<xliff:g id="album">%s</xliff:g>\"</string>
+  <string name="delete_artist_desc">toutes les chansons par <xliff:g id="artist">%s</xliff:g></string>
+  <string name="delete_confirm_button_text">OK</string>
+  <string name="delete_confirm_prompt">\"<xliff:g id="to_delete">%s</xliff:g>\" seront supprimés de manière permanente de la carte SD.</string>
+  <string name="delete_item">Supprimer</string>
+  <string name="delete_playlist_menu">Supprimer</string>
+  <string name="durationformat">
+                                       <xliff:g id="format">%2$d:%5$02d</xliff:g>
+                               </string>
+  <string name="edit_playlist_menu">Modifier</string>
+  <string name="emptyplaylist">La sélection choisie est vide.</string>
+  <string name="goto_playback">Lecture</string>
+  <string name="goto_start">Bibliothèque</string>
+  <string name="loading_video">Chargement de la vidéo\u2026</string>
+  <string name="mediapickerlabel">Musique</string>
+  <string name="mediaplaybacklabel">Musique</string>
+  <string name="mediasearch">Rechercher %s en utilisant :</string>
+  <string name="mediasearch_amazon">Amazon MP3</string>
+  <string name="mediasearch_google">Google</string>
+  <string name="mediasearch_youtube">YouTube</string>
+  <string name="movieviewlabel">Films</string>
+  <string name="musicbrowserlabel">Musique</string>
+  <string name="musicsearch">Musique</string>
+  <string name="musicsearchhint">Artiste, album ou nom de chanson</string>
+  <string name="musicshortcutlabel">Sélection musicale</string>
+  <string name="new_playlist">Nouveau</string>
+  <string name="new_playlist_name_template">Nouvelle sélection <xliff:g id="number">%d</xliff:g></string>
+  <string name="no_albums_title">Aucun album</string>
+  <string name="no_playlists_title">Aucune sélection</string>
+  <string name="no_tracks_title">Aucune chanson</string>
+  <string name="no_videos_title">Aucune vidéo</string>
+  <string name="nowplaying_title">Lecture en cours</string>
+  <string name="onesong">1 chanson</string>
+  <string name="party_shuffle">Lecture aléatoire de fête</string>
+  <string name="party_shuffle_off">Lecture aléatoire de fête désactivée</string>
+  <string name="partyshuffle_title">Lecture aléatoire de fête</string>
+  <string name="play_all">Lire tout</string>
+  <string name="play_selection">Lecture</string>
+  <string name="playback_failed">Désolé, le lecteur ne prend pas en charge ce type de fichier audio."</string>
+  <string name="playlist_deleted_message">Sélection supprimée.</string>
+  <string name="playlist_renamed_message">Sélection renommée.</string>
+  <string name="playlists_menu">Sélections</string>
+  <string name="playlists_title">Sélections</string>
+  <string name="queue">Sélection actuelle</string>
+  <string name="recentlyadded">Récemment ajoutée</string>
+  <string name="remove_from_playlist">Supprimer de la sélection</string>
+  <string name="rename_playlist_menu">Renommer</string>
+  <string name="repeat_all_notif">Répéter toutes les chansons.</string>
+  <string name="repeat_current_notif">Répéter la chanson actuelle.</string>
+  <string name="repeat_off_notif">Répétition désactivée.</string>
+  <string name="ringtone_menu">Utiliser comme sonnerie du téléphone</string>
+  <string name="ringtone_menu_short">Utiliser comme sonnerie</string>
+  <string name="ringtone_set">\"%s\" défini comme sonnerie du téléphone.</string>
+  <string name="save_as_playlist">Enregistrer comme sélection</string>
+  <string name="scanning">Recherche de la carte SD\u2026</string>
+  <string name="sdcard_busy_message">Carte SD occupée. Pour accéder à votre musique, allez à Accueil &gt; Paramètres &gt;  Stockage du téléphone et carte SD et désélectionnez la case \"Utiliser pour le stockage USB\".</string>
+  <string name="sdcard_busy_title">Carte SD occupée</string>
+  <string name="sdcard_error_message">Erreur de carte SD.</string>
+  <string name="sdcard_error_title">Erreur de carte SD</string>
+  <string name="sdcard_missing_message">Pas de carte SD.</string>
+  <string name="sdcard_missing_title">Pas de carte SD</string>
+  <string name="search_title">Recherche</string>
+  <string name="service_start_error_button">OK</string>
+  <string name="service_start_error_msg">Désolé, la chanson n\'a pas pu être lue.</string>
+  <string name="service_start_error_title">Problème de lecture</string>
+  <string name="shuffle_all">Lecture aléatoire de tout</string>
+  <string name="shuffle_off_notif">Lecture aléatoire désactivée.</string>
+  <string name="shuffle_on_notif">Lecture aléatoire activée.</string>
+  <string name="streamloadingtext">Connexion à <xliff:g id="host">%s</xliff:g></string>
+  <string name="tracks_menu">Chansons</string>
+  <string name="tracks_title">Chansons</string>
+  <string name="unknown_album_name">Album inconnu</string>
+  <string name="unknown_artist_name">Artiste inconnu</string>
+  <string name="videobrowserlabel">Vidéos</string>
+  <string name="videos_title">Vidéos</string>
+  <string name="weekpicker_set">Terminé</string>
+  <string name="weekpicker_title">Définir l\'heure</string>
+</resources>
diff --git a/res/values-it-rIT/strings.xml b/res/values-it-rIT/strings.xml
new file mode 100644 (file)
index 0000000..f0a24cf
--- /dev/null
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <string name="add_to_playlist">Aggiungi a elenco di riproduzione</string>
+  <string name="albums_menu">Album</string>
+  <string name="albums_title">Album</string>
+  <string name="all_title">Tutti i supporti</string>
+  <string name="artists_title">Artisti</string>
+  <string name="browse_menu">Artisti</string>
+  <string name="cancel">Annulla</string>
+  <string name="clear_playlist">Cancella elenco di riproduzione</string>
+  <string name="create_playlist_create_text">Salva</string>
+  <string name="create_playlist_overwrite_text">Sovrascrivi</string>
+  <string name="delete_album_desc">l'intero album \"<xliff:g id="album">%s</xliff:g>\"</string>
+  <string name="delete_artist_desc">tutti i brani di <xliff:g id="artist">%s</xliff:g></string>
+  <string name="delete_confirm_button_text">OK</string>
+  <string name="delete_confirm_prompt">\"<xliff:g id="to_delete">%s</xliff:g>\" verrà eliminato in maniera permanente dalla scheda SD.</string>
+  <string name="delete_item">Elimina</string>
+  <string name="delete_playlist_menu">Elimina</string>
+  <string name="durationformat">
+                                       <xliff:g id="format">%2$d:%5$02d</xliff:g>
+                               </string>
+  <string name="edit_playlist_menu">Modifica</string>
+  <string name="emptyplaylist">L'elenco di riproduzione selezionato è vuoto.</string>
+  <string name="goto_playback">Riproduzione</string>
+  <string name="goto_start">Catalogo</string>
+  <string name="loading_video">Caricamento del video in corso\u2026</string>
+  <string name="mediapickerlabel">Musica</string>
+  <string name="mediaplaybacklabel">Musica</string>
+  <string name="mediasearch">Ricerca %s utilizzando:</string>
+  <string name="mediasearch_amazon">Amazon MP3</string>
+  <string name="mediasearch_google">Google</string>
+  <string name="mediasearch_youtube">YouTube</string>
+  <string name="movieviewlabel">Filmati</string>
+  <string name="musicbrowserlabel">Musica</string>
+  <string name="musicsearch">Musica</string>
+  <string name="musicsearchhint">Nome artista, album o brano</string>
+  <string name="musicshortcutlabel">Elenco di riproduzione musicale</string>
+  <string name="new_playlist">Nuovo</string>
+  <string name="new_playlist_name_template">Nuovo elenco di riproduzione <xliff:g id="number">%d</xliff:g></string>
+  <string name="no_albums_title">Nessun album</string>
+  <string name="no_playlists_title">Nessun elenco di riproduzione</string>
+  <string name="no_tracks_title">Nessun brano</string>
+  <string name="no_videos_title">Nessun video</string>
+  <string name="nowplaying_title">In esecuzione</string>
+  <string name="onesong">Brano 1</string>
+  <string name="party_shuffle">Riproduzione casuale parziale</string>
+  <string name="party_shuffle_off">Riproduzione casuale parziale disattivata</string>
+  <string name="partyshuffle_title">Riproduzione casuale parziale</string>
+  <string name="play_all">Riproduci tutto</string>
+  <string name="play_selection">Riproduci</string>
+  <string name="playback_failed">Il lettore non supporta questo tipo di file audio."</string>
+  <string name="playlist_deleted_message">Elenco di riproduzione eliminato.</string>
+  <string name="playlist_renamed_message">Elenco di riproduzione rinominato.</string>
+  <string name="playlists_menu">Elenchi di riproduzione</string>
+  <string name="playlists_title">Elenchi di riproduzione</string>
+  <string name="queue">Elenco di riproduzione corrente</string>
+  <string name="recentlyadded">Aggiunto recentemente</string>
+  <string name="remove_from_playlist">Rimuovi da elenco di riproduzione</string>
+  <string name="rename_playlist_menu">Rinomina</string>
+  <string name="repeat_all_notif">Ripetizione di tutti i brani.</string>
+  <string name="repeat_current_notif">Ripetizione del bano corrente.</string>
+  <string name="repeat_off_notif">Ripetizione disattivata.</string>
+  <string name="ringtone_menu">Utilizzare come suoneria telefonica</string>
+  <string name="ringtone_menu_short">Utilizzare come suoneria</string>
+  <string name="ringtone_set">\"%s\" impostato come suoneria telefonica.</string>
+  <string name="save_as_playlist">Salva come elenco di riproduzione</string>
+  <string name="scanning">Scansione scheda SD\u2026</string>
+  <string name="sdcard_busy_message">Scheda SD occupata. Per accedere alla musica, andare su Home &gt; Impostazioni&gt; Scheda SD e memoria telefono e deselezionare la casella di controllo \"Usa per dispositivo di memorizzazione USB\".</string>
+  <string name="sdcard_busy_title">La scheda SD è occupata</string>
+  <string name="sdcard_error_message">Errore scheda SD.</string>
+  <string name="sdcard_error_title">Errore scheda SD</string>
+  <string name="sdcard_missing_message">Nessuna scheda SD.</string>
+  <string name="sdcard_missing_title">Nessuna scheda SD</string>
+  <string name="search_title">Cerca</string>
+  <string name="service_start_error_button">OK</string>
+  <string name="service_start_error_msg">Impossibile riprodurre il brano.</string>
+  <string name="service_start_error_title">Problema di riproduzione</string>
+  <string name="shuffle_all">Riproduzione casuale di tutto</string>
+  <string name="shuffle_off_notif">La riproduzione casuale è disattivata.</string>
+  <string name="shuffle_on_notif">La riproduzione casuale è attivata.</string>
+  <string name="streamloadingtext">Collegamento a <xliff:g id="host">%s</xliff:g></string>
+  <string name="tracks_menu">Brani</string>
+  <string name="tracks_title">Brani</string>
+  <string name="unknown_album_name">Album sconosciuto</string>
+  <string name="unknown_artist_name">Artista sconosciuto</string>
+  <string name="videobrowserlabel">Video</string>
+  <string name="videos_title">Video</string>
+  <string name="weekpicker_set">Completato</string>
+  <string name="weekpicker_title">Imposta ora</string>
+</resources>
diff --git a/res/values-keysexposed/strings.xml b/res/values-keysexposed/strings.xml
new file mode 100644 (file)
index 0000000..de1eb22
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<resources>
+    <string name="create_playlist_create_text_prompt">Playlist name</string>
+    <string name="rename_playlist_same_prompt">Rename \"%s\" to</string>
+    <string name="rename_playlist_diff_prompt">Rename \"%s\" to</string>
+</resources>
+
diff --git a/res/values-keyshidden/strings.xml b/res/values-keyshidden/strings.xml
new file mode 100644 (file)
index 0000000..cefa0af
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<resources>
+    <string name="create_playlist_create_text_prompt">Open the keyboard to give your new Playlist a name, or select Save to name it \"%s\".</string>
+
+    <!-- When the entered name is the same as the old one, show this -->
+    <string name="rename_playlist_same_prompt">Open the keyboard to give playlist \"%s\" a new name.</string>
+    <!-- When the entered name is different, show this -->
+    <string name="rename_playlist_diff_prompt">Open the keyboard to give playlist \"%s\" a new name, or select Save to name it \"%s\".</string>
+</resources>
+
diff --git a/res/values-nl-rNL/strings.xml b/res/values-nl-rNL/strings.xml
new file mode 100644 (file)
index 0000000..60f1281
--- /dev/null
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <string name="NNNtracksdeleted"><xliff:g id="songs_to_delete">%s</xliff:g> nummers verwijderd</string>
+  <string name="NNNtrackstoplaylist">NNN nummers toegevoegd aan afspeellijst</string>
+  <string name="Nalbums"><xliff:g id="count">%d</xliff:g> albums</string>
+  <string name="Nsongs"><xliff:g id="count">%d</xliff:g> nummers</string>
+  <string name="add_to_playlist">Toevoegen aan speellijst</string>
+  <string name="albums_menu">Albums</string>
+  <string name="albums_title">Albums</string>
+  <string name="all_title">Alle media</string>
+  <string name="artists_title">Artiesten</string>
+  <string name="browse_menu">Artiesten</string>
+  <string name="create_playlist_create_text">Opslaan</string>
+  <string name="create_playlist_overwrite_text">Overschrijven</string>
+  <string name="delete_album_desc">het hele album \"<xliff:g id="album">%s</xliff:g>\"</string>
+  <string name="delete_artist_desc">alle nummers van <xliff:g id="artist">%s</xliff:g></string>
+  <string name="delete_confirm_button_text">Verwijderen</string>
+  <string name="delete_confirm_prompt"><xliff:g id="to_delete">%s</xliff:g> permanent van sd-kaart verwijderen?</string>
+  <string name="delete_item">Verwijderen</string>
+  <string name="delete_playlist_menu">Verwijderen</string>
+  <string name="durationformat"><xliff:g id="format">%2$d:%5$02d</xliff:g></string>
+  <string name="edit_playlist_menu">Bewerken</string>
+  <string name="emptyplaylist">De geselecteerde afspeellijst is leeg</string>
+  <string name="goto_playback">Afspelen</string>
+  <string name="goto_start">Bibliotheek</string>
+  <string name="mediapickerlabel">Muziek</string>
+  <string name="mediaplaybacklabel">Muziek</string>
+  <string name="movieviewlabel">Films</string>
+  <string name="musicbrowserlabel">Muziek</string>
+  <string name="musicsearch">Muziek</string>
+  <string name="musicsearchhint">Artiest, album of naam</string>
+  <string name="new_playlist">Nieuw\u2026</string>
+  <string name="new_playlist_name_template">Nieuwe afspeellijst <xliff:g id="number">%d</xliff:g></string>
+  <string name="no_albums_title">Geen albums</string>
+  <string name="no_playlists_title">Geen afspeellijsten</string>
+  <string name="no_tracks_title">Geen nummers</string>
+  <string name="no_videos_title">Geen video's</string>
+  <string name="nowplaying_title">Afspelen</string>
+  <string name="onealbum">1 album</string>
+  <string name="onesong">1 nummer</string>
+  <string name="party_shuffle">Feestvolgorde</string>
+  <string name="party_shuffle_off">Feestvolgorde uit</string>
+  <string name="partyshuffle_title">Feestvolgorde</string>
+  <string name="play_all">Alles afspelen</string>
+  <string name="play_selection">Afspelen</string>
+  <string name="playlists_menu">Afspeellijst</string>
+  <string name="playlists_title">Afspeellijst</string>
+  <string name="recentlyadded">Recent toegevoegd</string>
+  <string name="rename_playlist_menu">Naam wijzigen</string>
+  <string name="repeat_all_notif">Alle nummers herhalen</string>
+  <string name="repeat_current_notif">Huidig nummer herhalen</string>
+  <string name="repeat_off_notif">Herhalen staat uit</string>
+  <string name="ringtone_menu">Gebruiken als beltoon</string>
+  <string name="save_as_playlist">Opslaan als afspeellijst</string>
+  <string name="scanning">Sd-kaart wordt gescand\u2026</string>
+  <string name="sdcard_busy_title">Sd-kaart is bezet</string>
+  <string name="sdcard_error_title">Sd-kaartfout</string>
+  <string name="sdcard_missing_title">Geen sd-kaart</string>
+  <string name="search_title">Zoeken</string>
+  <string name="service_start_error_button">OK</string>
+  <string name="service_start_error_msg">Kan media-afspeelservice niet starten</string>
+  <string name="service_start_error_title">Fout</string>
+  <string name="shuffle_all">Alles willekeurig afspelen</string>
+  <string name="shuffle_off_notif">Willekeurig afspelen staat uit</string>
+  <string name="shuffle_on_notif">Willekeurig afspelen staat aan</string>
+  <string name="tracks_menu">Nummers</string>
+  <string name="tracks_title">Nummers</string>
+  <string name="unknown_album_name">Onbekend album</string>
+  <string name="unknown_artist_name">Onbekende artiest</string>
+  <string name="videobrowserlabel">Video's</string>
+  <string name="videos_title">Video's</string>
+  <string name="weekpicker_set">Gereed</string>
+  <string name="weekpicker_title">Tijd instellen</string>
+</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
new file mode 100644 (file)
index 0000000..c4973ab
--- /dev/null
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <string name="add_to_playlist">新增到播放清單</string>
+  <string name="albums_menu">專輯</string>
+  <string name="albums_title">專輯</string>
+  <string name="all_title">所有媒體</string>
+  <string name="artists_title">演出者</string>
+  <string name="browse_menu">演出者</string>
+  <string name="cancel">取消</string>
+  <string name="clear_playlist">清除播放清單</string>
+  <string name="create_playlist_create_text">儲存</string>
+  <string name="create_playlist_overwrite_text">覆寫</string>
+  <string name="delete_album_desc">整張專輯 \"<xliff:g id="album">%s</xliff:g>\"</string>
+  <string name="delete_artist_desc"><xliff:g id="artist">%s</xliff:g> 的所有歌曲</string>
+  <string name="delete_confirm_button_text">確定</string>
+  <string name="delete_confirm_prompt">將會永久從 SD 卡中刪除 \"<xliff:g id="to_delete">%s</xliff:g>\"。</string>
+  <string name="delete_item">刪除</string>
+  <string name="delete_playlist_menu">刪除</string>
+  <string name="durationformat">
+                                       <xliff:g id="format">%2$d:%5$02d</xliff:g>
+                               </string>
+  <string name="edit_playlist_menu">編輯</string>
+  <string name="emptyplaylist">選取的播放清單空白。</string>
+  <string name="goto_playback">播放</string>
+  <string name="goto_start">媒體櫃</string>
+  <string name="loading_video">正在載入影片\u2026</string>
+  <string name="mediapickerlabel">音樂</string>
+  <string name="mediaplaybacklabel">音樂</string>
+  <string name="mediasearch">搜尋 %s 的工具:</string>
+  <string name="mediasearch_amazon">Amazon MP3</string>
+  <string name="mediasearch_google">Google</string>
+  <string name="mediasearch_youtube">YouTube</string>
+  <string name="movieviewlabel">電影</string>
+  <string name="musicbrowserlabel">音樂</string>
+  <string name="musicsearch">音樂</string>
+  <string name="musicsearchhint">演出者、專輯或歌曲名稱</string>
+  <string name="musicshortcutlabel">音樂播放清單</string>
+  <string name="new_playlist">新增</string>
+  <string name="new_playlist_name_template">新增播放清單 <xliff:g id="number">%d</xliff:g></string>
+  <string name="no_albums_title">無專輯</string>
+  <string name="no_playlists_title">無播放清單</string>
+  <string name="no_tracks_title">無歌曲</string>
+  <string name="no_videos_title">無影片</string>
+  <string name="nowplaying_title">現正播放</string>
+  <string name="onesong">1 首歌曲</string>
+  <string name="party_shuffle">派對隨機播放</string>
+  <string name="party_shuffle_off">關閉派對隨機播放</string>
+  <string name="partyshuffle_title">派對隨機播放</string>
+  <string name="play_all">全部播放</string>
+  <string name="play_selection">播放</string>
+  <string name="playback_failed">抱歉,播放器不支援此類型的音訊檔。"</string>
+  <string name="playlist_deleted_message">已經刪除播放清單。</string>
+  <string name="playlist_renamed_message">已經重新命名播放清單。</string>
+  <string name="playlists_menu">播放清單</string>
+  <string name="playlists_title">播放清單</string>
+  <string name="queue">目前的播放清單</string>
+  <string name="recentlyadded">最近新增</string>
+  <string name="remove_from_playlist">從播放清單移除</string>
+  <string name="rename_playlist_menu">重新命名</string>
+  <string name="repeat_all_notif">重複所有歌曲。</string>
+  <string name="repeat_current_notif">重複目前歌曲。</string>
+  <string name="repeat_off_notif">關閉重複。</string>
+  <string name="ringtone_menu">設為電話響鈴音調</string>
+  <string name="ringtone_menu_short">設為響鈴音調</string>
+  <string name="ringtone_set">\"%s\" 已經設為響鈴音調。</string>
+  <string name="save_as_playlist">儲存為播放清單</string>
+  <string name="scanning">正在掃描 SD 卡\u2026</string>
+  <string name="sdcard_busy_message">SD 卡忙碌中。若要存取音樂,請移至首頁 &gt; 設定 &gt; SD 卡與電話儲存空間,並清除 \"用於 USB 儲存\" 核取方塊。</string>
+  <string name="sdcard_busy_title">SD 卡忙碌中</string>
+  <string name="sdcard_error_message">SD 卡錯誤。</string>
+  <string name="sdcard_error_title">SD 卡錯誤</string>
+  <string name="sdcard_missing_message">無 SD 卡。</string>
+  <string name="sdcard_missing_title">無 SD 卡</string>
+  <string name="search_title">搜尋</string>
+  <string name="service_start_error_button">確定</string>
+  <string name="service_start_error_msg">抱歉,無法播放此歌曲。</string>
+  <string name="service_start_error_title">播放發生問題</string>
+  <string name="shuffle_all">全部隨機播放</string>
+  <string name="shuffle_off_notif">關閉隨機播放。</string>
+  <string name="shuffle_on_notif">開啟隨機播放。</string>
+  <string name="streamloadingtext">正在連線到 <xliff:g id="host">%s</xliff:g></string>
+  <string name="tracks_menu">歌曲</string>
+  <string name="tracks_title">歌曲</string>
+  <string name="unknown_album_name">無法辨識的專輯</string>
+  <string name="unknown_artist_name">無法辨識的演出者</string>
+  <string name="videobrowserlabel">影片</string>
+  <string name="videos_title">影片</string>
+  <string name="weekpicker_set">完成</string>
+  <string name="weekpicker_title">設定時間</string>
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644 (file)
index 0000000..fed161e
--- /dev/null
@@ -0,0 +1,174 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <string name="onesong">1 song</string>
+    <plurals name="Nsongs">
+        <item quantity="other"><xliff:g id="count">%d</xliff:g> songs</item>
+    </plurals>
+
+    <plurals name="Nalbums">
+        <item quantity="one">1 album</item>
+        <item quantity="other"><xliff:g id="count">%d</xliff:g> albums</item>
+    </plurals>
+    
+    <!--
+    This string is used as the format string in a String.format call,
+    and 5 additional arguments are available for printing:
+    1 - hours
+    2 - minutes
+    3 - minutes%60
+    4 - seconds
+    5 - second%60
+    -->
+    <string name="durationformat"><xliff:g id="format">%2$d:%5$02d</xliff:g></string>
+
+    <!-- Search -->
+    <string name="musicsearch">Music</string>
+    <string name="musicsearchhint">Artist, album, or song name</string>
+
+    <string name="goto_start">Library</string>
+    <string name="goto_playback">Playback</string>
+    <string name="party_shuffle">Party shuffle</string>
+    <string name="party_shuffle_off">Party shuffle off</string>
+    <string name="delete_item">Delete</string>
+    <string name="shuffle_all">Shuffle all</string>
+    <string name="play_all">Play all</string>
+
+    <string name="delete_artist_desc">all songs by <xliff:g id="artist">%s</xliff:g></string>
+    <string name="delete_album_desc">the entire album \"<xliff:g id="album">%s</xliff:g>\"</string>
+    <string name="delete_confirm_prompt">\"<xliff:g id="to_delete">%s</xliff:g>\" will be permanently deleted from the SD card.</string>
+    <string name="delete_confirm_button_text">OK</string>
+    <plurals name="NNNtracksdeleted">
+        <item quantity="one">1 song was deleted.</item>
+        <item quantity="other"><xliff:g id="songs_to_delete">%d</xliff:g> songs were deleted.</item>
+    </plurals>
+
+    <string name="scanning">Scanning SD card\u2026</string>
+
+    <string name="nowplaying_title">Now playing</string>
+    <string name="partyshuffle_title">Party shuffle</string>
+    <string name="artists_title">Artists</string>
+    <string name="albums_menu">Albums</string>
+    <string name="albums_title">Albums</string>
+    <string name="tracks_menu">Songs</string>
+    <string name="tracks_title">Songs</string>
+    <string name="playlists_menu">Playlists</string>
+    <string name="playlists_title">Playlists</string>
+    <string name="videos_title">Videos</string>
+    <string name="all_title">All media</string>
+    <string name="browse_menu">Artists</string>
+    <string name="search_title">Search</string>
+
+    <string name="no_albums_title">No albums</string>
+    <string name="no_tracks_title">No songs</string>
+    <string name="no_videos_title">No videos</string>
+    <string name="no_playlists_title">No playlists</string>
+    <string name="delete_playlist_menu">Delete</string>
+    <string name="edit_playlist_menu">Edit</string>
+    <string name="rename_playlist_menu">Rename</string>
+    <string name="playlist_deleted_message">Playlist deleted.</string>
+    <string name="playlist_renamed_message">Playlist renamed.</string>
+    <string name="recentlyadded">Recently added</string>
+    <string name="sdcard_missing_title">No SD card</string>
+    <string name="sdcard_missing_message">No SD card.</string>
+    <string name="sdcard_busy_title">SD card is busy</string>
+    <string name="sdcard_busy_message">Sorry, your SD card is busy.</string>
+    <string name="sdcard_error_title">SD card error</string>
+    <string name="sdcard_error_message">SD card error.</string>
+    <string name="unknown_artist_name">Unknown artist</string>
+    <string name="unknown_album_name">Unknown album</string>
+
+    <string name="shuffle_on_notif">Shuffle is on.</string>
+    <string name="shuffle_off_notif">Shuffle is off.</string>
+
+    <string name="repeat_off_notif">Repeat is off.</string>
+    <string name="repeat_current_notif">Repeating current song.</string>
+    <string name="repeat_all_notif">Repeating all songs.</string>
+
+    <string name="ringtone_menu">Use as phone ringtone</string>
+    <string name="ringtone_menu_short">Use as ringtone</string>
+    <string name="ringtone_set">\"%s\" set as phone ringtone.</string>
+
+    <string name="play_selection">Play</string>
+    <string name="add_to_playlist">Add to playlist</string>
+    <string name="queue">Current playlist</string>
+    <string name="new_playlist">New</string>
+    <string name="new_playlist_name_template">New playlist <xliff:g id="number">%d</xliff:g></string>
+    <plurals name="NNNtrackstoplaylist">
+        <item quantity="one">1 song added to playlist.</item>
+        <item quantity="other">%d songs added to playlist.</item>
+    </plurals>
+    <string name="emptyplaylist">Selected playlist is empty.</string>
+
+    <string name="create_playlist_create_text">Save</string>
+    <string name="create_playlist_overwrite_text">Overwrite</string>
+
+    <string name="service_start_error_title">Playback problem</string>
+    <string name="service_start_error_msg">Sorry, the song could not be played.</string>
+    <string name="service_start_error_button">OK</string>
+
+    <string-array name="weeklist">
+        <item>"1 week"</item>
+        <item>"2 weeks"</item>
+        <item>"3 weeks"</item>
+        <item>"4 weeks"</item>
+        <item>"5 weeks"</item>
+        <item>"6 weeks"</item>
+        <item>"7 weeks"</item>
+        <item>"8 weeks"</item>
+        <item>"9 weeks"</item>
+        <item>"10 weeks"</item>
+        <item>"11 weeks"</item>
+        <item>"12 weeks"</item>
+    </string-array>
+    <string name="weekpicker_set">Done</string>
+    <string name="weekpicker_title">Set time</string>
+
+    <color name="dragndrop_background">#e0103010</color>
+    <color name="expanding_child_background">#ff404040</color>
+
+    <string name="save_as_playlist">Save as playlist</string>
+    <string name="clear_playlist">Clear playlist</string>
+
+    <!-- Activity labels. These might show up in an activity-picker -->
+    <string name="musicbrowserlabel">Music</string>
+    <string name="musicshortcutlabel">Music playlist</string>
+    <string name="mediaplaybacklabel">Music</string>
+    <string name="videobrowserlabel">Videos</string>
+    <string name="mediapickerlabel">Music</string>
+    <string name="movieviewlabel">Movies</string>
+
+    <string name="playback_failed">Sorry, the player does not support this type of audio file."</string>
+
+    <string name="cancel">Cancel</string>
+
+    <string name="remove_from_playlist">Remove from playlist</string>
+
+    <string name="loading_video">Loading video\u2026</string>
+
+    <string name="streamloadingtext">Connecting to <xliff:g id="host">%s</xliff:g></string>
+
+    <string name="mediasearch">Search for %s using:</string>
+    <string name="mediasearch_amazon">Amazon MP3</string>
+    <string name="mediasearch_google">Google</string>
+    <string name="mediasearch_youtube">YouTube</string>
+</resources>
+
diff --git a/res/values/strings2.xml b/res/values/strings2.xml
new file mode 100644 (file)
index 0000000..9829eee
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<resources>
+    <string name="albumsongseparator">, </string>
+</resources>
+
diff --git a/src/com/android/music/AlbumBrowserActivity.java b/src/com/android/music/AlbumBrowserActivity.java
new file mode 100644 (file)
index 0000000..1a961bc
--- /dev/null
@@ -0,0 +1,519 @@
+/*
+ * 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.android.music;
+
+import java.text.Collator;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.BroadcastReceiver;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.media.MediaFile;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+
+public class AlbumBrowserActivity extends ListActivity
+    implements View.OnCreateContextMenuListener, MusicUtils.Defs
+{
+    private String mCurrentAlbumId;
+    private String mCurrentAlbumName;
+
+    public AlbumBrowserActivity()
+    {
+    }
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle icicle)
+    {
+        if (icicle != null) {
+            mCurrentAlbumId = icicle.getString("selectedalbum");
+            mArtistId = icicle.getString("artist");
+        } else {
+            mArtistId = getIntent().getStringExtra("artist");
+        }
+        super.onCreate(icicle);
+        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+        MusicUtils.bindToService(this);
+
+        IntentFilter f = new IntentFilter();
+        f.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+        f.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
+        f.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+        f.addDataScheme("file");
+        registerReceiver(mScanListener, f);
+
+        init();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outcicle) {
+        // need to store the selected item so we don't lose it in case
+        // of an orientation switch. Otherwise we could lose it while
+        // in the middle of specifying a playlist to add the item to.
+        outcicle.putString("selectedalbum", mCurrentAlbumId);
+        outcicle.putString("artist", mArtistId);
+        super.onSaveInstanceState(outcicle);
+    }
+
+    @Override
+    public void onDestroy() {
+        MusicUtils.unbindFromService(this);
+        if (mAlbumCursor != null) {
+            mAlbumCursor.close();
+        }
+        unregisterReceiver(mScanListener);
+        super.onDestroy();
+    }
+    
+    @Override
+    public void onResume() {
+        super.onResume();
+        IntentFilter f = new IntentFilter();
+        f.addAction(MediaPlaybackService.META_CHANGED);
+        f.addAction(MediaPlaybackService.QUEUE_CHANGED);
+        registerReceiver(mTrackListListener, f);
+        mTrackListListener.onReceive(null, null);
+
+        MusicUtils.setSpinnerState(this);
+    }
+
+    private BroadcastReceiver mTrackListListener = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            getListView().invalidateViews();
+        }
+    };
+    private BroadcastReceiver mScanListener = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            MusicUtils.setSpinnerState(AlbumBrowserActivity.this);
+            mReScanHandler.sendEmptyMessage(0);
+            if (intent.getAction().equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
+                MusicUtils.clearAlbumArtCache();
+            }
+        }
+    };
+    
+    private Handler mReScanHandler = new Handler() {
+        public void handleMessage(Message msg) {
+            init();
+            if (mAlbumCursor == null) {
+                sendEmptyMessageDelayed(0, 1000);
+            }
+        }
+    };
+
+    @Override
+    public void onPause() {
+        unregisterReceiver(mTrackListListener);
+        mReScanHandler.removeCallbacksAndMessages(null);
+        super.onPause();
+    }
+
+    public void init() {
+
+        mAlbumCursor = getAlbumCursor(null);
+
+        /* The UI spec does not call for icons in the title bar
+        if (mArtistId != null) {
+            requestWindowFeature(Window.FEATURE_LEFT_ICON);
+        }
+        */
+        setContentView(R.layout.media_picker_activity);
+        /* The UI spec does not call for icons in the title bar
+        if (mArtistId != null) {
+            // this call needs to happen after setContentView
+            setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, R.drawable.person_title_icon);
+        }
+        */
+
+        if (mAlbumCursor == null) {
+            MusicUtils.displayDatabaseError(this);
+            return;
+        }
+
+        CharSequence fancyName = "";
+        if (mAlbumCursor.getCount() > 0) {
+            mAlbumCursor.moveToFirst();
+            fancyName = mAlbumCursor.getString(3);
+            if (MediaFile.UNKNOWN_STRING.equals(fancyName))
+                fancyName = getText(R.string.unknown_artist_name);
+
+            if (mArtistId != null && fancyName != null)
+                setTitle(fancyName);
+            else
+                setTitle(R.string.albums_title);
+        } else {
+            setTitle(R.string.no_albums_title);
+        }
+
+        // Map Cursor columns to views defined in media_list_item.xml
+        AlbumListAdapter adapter = new AlbumListAdapter(
+                this,
+                R.layout.track_list_item,
+                mAlbumCursor,
+                new String[] {},
+                new int[] {});
+
+        setListAdapter(adapter);
+        ListView lv = getListView();
+        lv.setOnCreateContextMenuListener(this);
+        lv.setTextFilterEnabled(true);
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
+        menu.add(0, PLAY_SELECTION, 0, R.string.play_selection);
+        SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0, R.string.add_to_playlist);
+        MusicUtils.makePlaylistMenu(this, sub);
+        menu.add(0, DELETE_ITEM, 0, R.string.delete_item);
+
+        AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn;
+        mAlbumCursor.moveToPosition(mi.position);
+        mCurrentAlbumId = mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID));
+        mCurrentAlbumName = mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM));
+        menu.setHeaderTitle(mCurrentAlbumName);
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case PLAY_SELECTION: {
+                // play the selected album
+                int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
+                MusicUtils.playAll(this, list, 0);
+                return true;
+            }
+
+            case QUEUE: {
+                int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
+                MusicUtils.addToCurrentPlaylist(this, list);
+                return true;
+            }
+
+            case NEW_PLAYLIST: {
+                Intent intent = new Intent();
+                intent.setClass(this, CreatePlaylist.class);
+                startActivityForResult(intent, NEW_PLAYLIST);
+                return true;
+            }
+
+            case PLAYLIST_SELECTED: {
+                int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
+                int playlist = item.getIntent().getIntExtra("playlist", 0);
+                MusicUtils.addToPlaylist(this, list, playlist);
+                return true;
+            }
+            case DELETE_ITEM: {
+                int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
+                String f = getString(R.string.delete_album_desc); 
+                String desc = String.format(f, mCurrentAlbumName);
+                Bundle b = new Bundle();
+                b.putString("description", desc);
+                b.putIntArray("items", list);
+                Intent intent = new Intent();
+                intent.setClass(this, DeleteItems.class);
+                intent.putExtras(b);
+                startActivityForResult(intent, -1);
+                return true;
+            }
+
+        }
+        return super.onContextItemSelected(item);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+        switch (requestCode) {
+            case SCAN_DONE:
+                if (resultCode == RESULT_CANCELED) {
+                    finish();
+                } else {
+                    init();
+                }
+                break;
+
+            case NEW_PLAYLIST:
+                if (resultCode == RESULT_OK) {
+                    Uri uri = Uri.parse(intent.getAction());
+                    if (uri != null) {
+                        int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
+                        MusicUtils.addToPlaylist(this, list, Integer.parseInt(uri.getLastPathSegment()));
+                    }
+                }
+                break;
+        }
+    }
+
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id)
+    {
+        if (mHasHeader) {
+            position --;
+        }
+        Intent intent = new Intent(Intent.ACTION_PICK);
+        intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
+        intent.putExtra("album", Long.valueOf(id).toString());
+        intent.putExtra("artist", mArtistId);
+        startActivity(intent);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(R.drawable.ic_menu_music_library);
+        menu.add(0, GOTO_PLAYBACK, 0, R.string.goto_playback).setIcon(R.drawable.ic_menu_playback);
+        menu.add(0, SHUFFLE_ALL, 0, R.string.shuffle_all).setIcon(R.drawable.ic_menu_shuffle);
+        return true;
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        menu.findItem(GOTO_PLAYBACK).setVisible(MusicUtils.isMusicLoaded());
+        return super.onPrepareOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        Intent intent;
+        Cursor cursor;
+        switch (item.getItemId()) {
+            case GOTO_START:
+                intent = new Intent();
+                intent.setClass(this, MusicBrowserActivity.class);
+                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                startActivity(intent);
+                return true;
+
+            case GOTO_PLAYBACK:
+                intent = new Intent("com.android.music.PLAYBACK_VIEWER");
+                startActivity(intent);
+                return true;
+
+            case SHUFFLE_ALL:
+                cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                        new String [] { MediaStore.Audio.Media._ID},
+                        MediaStore.Audio.Media.IS_MUSIC + "=1", null,
+                        MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+                if (cursor != null) {
+                    MusicUtils.shuffleAll(this, cursor);
+                    cursor.close();
+                }
+                return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    private Cursor getAlbumCursor(String filterstring) {
+        StringBuilder where = new StringBuilder();
+        where.append(MediaStore.Audio.Albums.ALBUM + " != ''");
+        
+        // Add in the filtering constraints
+        String [] keywords = null;
+        if (filterstring != null) {
+            String [] searchWords = filterstring.split(" ");
+            keywords = new String[searchWords.length];
+            Collator col = Collator.getInstance();
+            col.setStrength(Collator.PRIMARY);
+            for (int i = 0; i < searchWords.length; i++) {
+                keywords[i] = '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%';
+            }
+            for (int i = 0; i < searchWords.length; i++) {
+                where.append(" AND ");
+                where.append(MediaStore.Audio.Media.ARTIST_KEY + "||");
+                where.append(MediaStore.Audio.Media.ALBUM_KEY + " LIKE ?");
+            }
+        }
+
+        String whereclause = where.toString();  
+            
+        String[] cols = new String[] {
+                MediaStore.Audio.Albums._ID,
+                MediaStore.Audio.Albums.ALBUM,
+                MediaStore.Audio.Albums.ALBUM_KEY,
+                MediaStore.Audio.Albums.ARTIST,
+                MediaStore.Audio.Albums.NUMBER_OF_SONGS,
+                MediaStore.Audio.Albums.ALBUM_ART
+        };
+        Cursor ret;
+        if (mArtistId != null) {
+            ret = MusicUtils.query(this,
+                    MediaStore.Audio.Artists.Albums.getContentUri("external", Long.valueOf(mArtistId)),
+                    cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
+        } else {
+            ret = MusicUtils.query(this, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
+                cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
+        }
+        return ret;
+    }
+    
+    class AlbumListAdapter extends SimpleCursorAdapter {
+        
+        private final Drawable mNowPlayingOverlay;
+        private final BitmapDrawable mDefaultAlbumIcon;
+        private final int mAlbumIdx;
+        private final int mArtistIdx;
+        private final int mNumSongsIdx;
+        private final int mAlbumArtIndex;
+        private final Resources mResources;
+        private final StringBuilder mStringBuilder = new StringBuilder();
+        private final String mUnknownAlbum;
+        private final String mUnknownArtist;
+        private final String mAlbumSongSeparator;
+        private final Object[] mFormatArgs = new Object[1];
+
+        class ViewHolder {
+            TextView line1;
+            TextView line2;
+            TextView duration;
+            ImageView play_indicator;
+            ImageView icon;
+        }
+
+        AlbumListAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to) {
+            super(context, layout, cursor, from, to);
+
+            mUnknownAlbum = context.getString(R.string.unknown_album_name);
+            mUnknownArtist = context.getString(R.string.unknown_artist_name);
+            mAlbumSongSeparator = context.getString(R.string.albumsongseparator);
+
+            Resources r = getResources();
+            mNowPlayingOverlay = r.getDrawable(R.drawable.indicator_ic_mp_playing_list);
+
+            Bitmap b = BitmapFactory.decodeResource(r, R.drawable.albumart_mp_unknown_list);
+            mDefaultAlbumIcon = new BitmapDrawable(b);
+            // no filter or dither, it's a lot faster and we can't tell the difference
+            mDefaultAlbumIcon.setFilterBitmap(false);
+            mDefaultAlbumIcon.setDither(false);
+            mAlbumIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM);
+            mArtistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST);
+            mNumSongsIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.NUMBER_OF_SONGS);
+            mAlbumArtIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM_ART);
+            
+            mResources = context.getResources();
+        }
+
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+           View v = super.newView(context, cursor, parent);
+           ViewHolder vh = new ViewHolder();
+           vh.line1 = (TextView) v.findViewById(R.id.line1);
+           vh.line2 = (TextView) v.findViewById(R.id.line2);
+           vh.duration = (TextView) v.findViewById(R.id.duration);
+           vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator);
+           vh.icon = (ImageView) v.findViewById(R.id.icon);
+           vh.icon.setBackgroundDrawable(mDefaultAlbumIcon);
+           vh.icon.setPadding(1, 1, 1, 1);
+           v.setTag(vh);
+           return v;
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            
+            ViewHolder vh = (ViewHolder) view.getTag();
+
+            String name = cursor.getString(mAlbumIdx);
+            String displayname = name;
+            if (MediaFile.UNKNOWN_STRING.equals(name)) {
+                displayname = mUnknownAlbum;
+            }
+            vh.line1.setText(displayname);
+            
+            name = cursor.getString(mArtistIdx);
+            displayname = name;
+            if (MediaFile.UNKNOWN_STRING.equals(name)) {
+                displayname = mUnknownArtist;
+            }
+            StringBuilder builder = mStringBuilder;
+            builder.delete(0, builder.length());
+            builder.append(displayname);
+            builder.append(mAlbumSongSeparator);
+            
+            int numsongs = cursor.getInt(mNumSongsIdx);
+            if (numsongs == 1) {
+                builder.append(context.getString(R.string.onesong));
+            } else {
+                final Object[] args = mFormatArgs;
+                args[0] = numsongs;
+                builder.append(mResources.getQuantityString(R.plurals.Nsongs, numsongs, args));
+            }
+            vh.line2.setText(builder.toString());
+
+            ImageView iv = vh.icon;
+            // We don't actually need the path to the thumbnail file,
+            // we just use it to see if there is album art or not
+            String art = cursor.getString(mAlbumArtIndex);
+            if (art == null || art.length() == 0) {
+                iv.setImageDrawable(null);
+            } else {
+                int artIndex = cursor.getInt(0);
+                Drawable d = MusicUtils.getCachedArtwork(context, artIndex, mDefaultAlbumIcon);
+                iv.setImageDrawable(d);
+            }
+            
+            int currentalbumid = MusicUtils.getCurrentAlbumId();
+            int aid = cursor.getInt(0);
+            iv = vh.play_indicator;
+            if (currentalbumid == aid) {
+                iv.setImageDrawable(mNowPlayingOverlay);
+            } else {
+                iv.setImageDrawable(null);
+            }
+        }
+        @Override
+        public void changeCursor(Cursor cursor) {
+            super.changeCursor(cursor);
+            mAlbumCursor = cursor;
+        }
+        @Override
+        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+            return getAlbumCursor(constraint.toString());
+        }
+    }
+
+    private Cursor mAlbumCursor;
+    private String mArtistId;
+    private boolean mHasHeader = false;
+}
+
diff --git a/src/com/android/music/AlbumView.java b/src/com/android/music/AlbumView.java
new file mode 100644 (file)
index 0000000..55ac234
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.music;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapShader;
+import android.graphics.Camera;
+import android.graphics.Canvas;
+import android.graphics.ComposeShader;
+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.Xfermode;
+import android.util.AttributeSet;
+import android.widget.RelativeLayout;
+
+
+public class AlbumView extends RelativeLayout
+{
+    public AlbumView(Context context)
+    {
+        super(context);
+        setWillNotDraw(false);
+    }
+
+    public AlbumView(Context context, AttributeSet attrs)
+    {
+        super(context, attrs);
+        setWillNotDraw(false);
+    }
+
+    public void setArtwork(Bitmap art)
+    {
+        mScale = 1.00f;
+        cX = 0f;
+        cY = 0f;
+        cR = 0f; // 24f;
+
+        if (art == null) {
+            BitmapFactory.Options opts = new BitmapFactory.Options();
+            opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
+            art = BitmapFactory.decodeResource(getResources(), R.drawable.albumart_mp_unknown, opts);
+        }
+        
+        if (art != null) {
+            mBit = art; // Bitmap.createBitmap(art, 2, 2, art.width()-4, art.height()-4);
+            mCoverPaint = new Paint();
+            //mCoverPaint.setAntiAlias(true);
+            mCoverPaint.setFilterBitmap(true);
+            mCoverPaint.setDither(true);
+
+            BitmapShader sh1 = new BitmapShader(mBit, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
+            LinearGradient sh2 = new LinearGradient(0f, mBit.getHeight(),0f, mBit.getHeight()/3, 0x7f000000, 0x00000000, Shader.TileMode.CLAMP);
+            Xfermode mode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
+            mReflectionShader = new ComposeShader(sh1, sh2, mode );
+
+            mCamera = new Camera();
+            mCamera.rotateY(cR);
+            mCamera.translate(cX,cY,0);
+        }
+    }
+
+    private void drawArtwork(Canvas canvas)
+    {
+        if (mBit == null)
+            return;
+
+        canvas.save();
+
+        mCamera.applyToCanvas(canvas);
+
+        int mywidth = getWidth();
+        float artwidth = mBit.getWidth();
+        float scale = ((float)(mywidth - mPaddingLeft - mPaddingRight))/artwidth * mScale;
+
+        canvas.translate(mPaddingLeft, mPaddingTop);
+
+        canvas.scale(scale, scale);
+        mCoverPaint.setAlpha(255);
+        canvas.drawBitmap(mBit,0f,0f, mCoverPaint);
+        
+        if (false) {
+            // draw the reflection
+            canvas.scale(1, -1, 0, mBit.getHeight());
+            mCoverPaint.setAlpha(64);
+            mCoverPaint.setShader(mReflectionShader);
+            canvas.drawRect(new Rect(0,0,mBit.getWidth(),mBit.getHeight()), mCoverPaint); 
+            mCoverPaint.setShader(null);
+        }
+
+        canvas.restore();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        drawArtwork(canvas);
+    }
+
+    void adjustParams(double scale, double ix, double iy, double cx, double cy, double cr)
+    {
+        mScale += scale;
+        mPaddingLeft += ix;
+        mPaddingTop += iy;
+        cX += cx;
+        cY += cy;
+        cR += cr;
+
+        mCamera.rotateY((float)cr); // little r
+        mCamera.translate((float)cx,(float)cy,0); // little too
+
+        System.out.println("parameters: "
+              + mScale + " "
+              + mPaddingLeft + " "
+              + mPaddingTop + " "
+              + cX + " "
+              + cY + " "
+              + cR + " ");
+        invalidate();
+    }
+
+    private Bitmap mBit;
+    private Paint mCoverPaint;
+    private Camera mCamera;
+    private ComposeShader mReflectionShader;
+
+    float mScale;
+    float cX;
+    float cY;
+    float cR;
+}
diff --git a/src/com/android/music/ArtistAlbumBrowserActivity.java b/src/com/android/music/ArtistAlbumBrowserActivity.java
new file mode 100644 (file)
index 0000000..0ef95f3
--- /dev/null
@@ -0,0 +1,617 @@
+/*
+ * 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.android.music;
+
+import java.text.Collator;
+
+import android.app.ExpandableListActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.BroadcastReceiver;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.media.MediaFile;
+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 android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+import android.widget.ExpandableListView;
+import android.widget.ImageView;
+import android.widget.SimpleCursorTreeAdapter;
+import android.widget.TextView;
+
+
+public class ArtistAlbumBrowserActivity extends ExpandableListActivity
+        implements View.OnCreateContextMenuListener, MusicUtils.Defs
+{
+    private String mCurrentArtistId;
+    private String mCurrentArtistName;
+    private String mCurrentAlbumId;
+    private String mCurrentAlbumName;
+
+    public ArtistAlbumBrowserActivity()
+    {
+    }
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+        if (icicle != null) {
+            mCurrentAlbumId = icicle.getString("selectedalbum");
+            mCurrentAlbumName = icicle.getString("selectedalbumname");
+            mCurrentArtistId = icicle.getString("selectedartist");
+            mCurrentArtistName = icicle.getString("selectedartistname");
+        }
+        MusicUtils.bindToService(this);
+
+        IntentFilter f = new IntentFilter();
+        f.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+        f.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
+        f.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+        f.addDataScheme("file");
+        registerReceiver(mScanListener, f);
+
+        init();
+    }
+    
+    @Override
+    public void onSaveInstanceState(Bundle outcicle) {
+        // need to store the selected item so we don't lose it in case
+        // of an orientation switch. Otherwise we could lose it while
+        // in the middle of specifying a playlist to add the item to.
+        outcicle.putString("selectedalbum", mCurrentAlbumId);
+        outcicle.putString("selectedalbumname", mCurrentAlbumName);
+        outcicle.putString("selectedartist", mCurrentArtistId);
+        outcicle.putString("selectedartistname", mCurrentArtistName);
+        super.onSaveInstanceState(outcicle);
+    }
+
+    @Override
+    public void onDestroy() {
+        MusicUtils.unbindFromService(this);
+        if (mArtistCursor != null) {
+            mArtistCursor.close();
+        }
+        unregisterReceiver(mScanListener);
+        super.onDestroy();
+    }
+    
+    @Override
+    public void onResume() {
+        super.onResume();
+        IntentFilter f = new IntentFilter();
+        f.addAction(MediaPlaybackService.META_CHANGED);
+        f.addAction(MediaPlaybackService.QUEUE_CHANGED);
+        registerReceiver(mTrackListListener, f);
+        mTrackListListener.onReceive(null, null);
+
+        MusicUtils.setSpinnerState(this);
+    }
+
+    private BroadcastReceiver mTrackListListener = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            getExpandableListView().invalidateViews();
+        }
+    };
+    private BroadcastReceiver mScanListener = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            MusicUtils.setSpinnerState(ArtistAlbumBrowserActivity.this);
+            mReScanHandler.sendEmptyMessage(0);
+            if (intent.getAction().equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
+                MusicUtils.clearAlbumArtCache();
+            }
+        }
+    };
+    
+    private Handler mReScanHandler = new Handler() {
+        public void handleMessage(Message msg) {
+            init();
+            if (mArtistCursor == null) {
+                sendEmptyMessageDelayed(0, 1000);
+            }
+        }
+    };
+
+    @Override
+    public void onPause() {
+        unregisterReceiver(mTrackListListener);
+        mReScanHandler.removeCallbacksAndMessages(null);
+        super.onPause();
+    }
+    
+    public void init() {
+
+        mArtistCursor = getArtistCursor(null);
+
+        if (mArtistCursor == null) {
+            MusicUtils.displayDatabaseError(this);
+            return;
+        }
+
+        setContentView(android.R.layout.expandable_list_content);
+
+        // Map Cursor columns to views defined in media_list_item.xml
+        ArtistAlbumListAdapter adapter = new ArtistAlbumListAdapter(
+                this,
+                mArtistCursor,
+                R.layout.track_list_item_group,
+                new String[] {},
+                new int[] {},
+                R.layout.track_list_item_child,
+                new String[] {},
+                new int[] {});
+
+        setTitle(R.string.artists_title);
+        setListAdapter(adapter);
+        ExpandableListView lv = getExpandableListView();
+        lv.setOnCreateContextMenuListener(this);
+        lv.setTextFilterEnabled(true);
+    }
+
+    @Override
+    public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
+
+        mCurrentAlbumId = Long.valueOf(id).toString();
+        
+        Intent intent = new Intent(Intent.ACTION_PICK);
+        intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
+        intent.putExtra("album", mCurrentAlbumId);
+        mArtistCursor.moveToPosition(groupPosition);
+        mCurrentArtistId = Long.valueOf(id).toString();
+        intent.putExtra("artist", mCurrentArtistId);
+        startActivity(intent);
+        return true;
+    }
+    
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(R.drawable.ic_menu_music_library);
+        menu.add(0, GOTO_PLAYBACK, 0, R.string.goto_playback).setIcon(R.drawable.ic_menu_playback);
+        menu.add(0, SHUFFLE_ALL, 0, R.string.shuffle_all).setIcon(R.drawable.ic_menu_shuffle);
+        return true;
+    }
+    
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        menu.findItem(GOTO_PLAYBACK).setVisible(MusicUtils.isMusicLoaded());
+        return super.onPrepareOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        Intent intent;
+        Cursor cursor;
+        switch (item.getItemId()) {
+            case GOTO_START:
+                intent = new Intent();
+                intent.setClass(this, MusicBrowserActivity.class);
+                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                startActivity(intent);
+                return true;
+
+            case GOTO_PLAYBACK:
+                intent = new Intent("com.android.music.PLAYBACK_VIEWER");
+                startActivity(intent);
+                return true;
+                
+            case SHUFFLE_ALL:
+                cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                        new String [] { MediaStore.Audio.Media._ID}, 
+                        MediaStore.Audio.Media.IS_MUSIC + "=1", null,
+                        MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+                if (cursor != null) {
+                    MusicUtils.shuffleAll(this, cursor);
+                    cursor.close();
+                }
+                return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
+        menu.add(0, PLAY_SELECTION, 0, R.string.play_selection);
+        SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0, R.string.add_to_playlist);
+        MusicUtils.makePlaylistMenu(this, sub);
+        menu.add(0, DELETE_ITEM, 0, R.string.delete_item);
+        
+        ExpandableListContextMenuInfo mi = (ExpandableListContextMenuInfo) menuInfoIn;
+        
+        int itemtype = ExpandableListView.getPackedPositionType(mi.packedPosition);
+        int gpos = ExpandableListView.getPackedPositionGroup(mi.packedPosition);
+        int cpos = ExpandableListView.getPackedPositionChild(mi.packedPosition);
+        if (itemtype == ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
+            if (gpos == -1) {
+                // this shouldn't happen
+                Log.d("Artist/Album", "no group");
+                return;
+            }
+            gpos = gpos - getExpandableListView().getHeaderViewsCount();
+            mArtistCursor.moveToPosition(gpos);
+            mCurrentArtistId = mArtistCursor.getString(mArtistCursor.getColumnIndexOrThrow(MediaStore.Audio.Artists._ID));
+            mCurrentArtistName = mArtistCursor.getString(mArtistCursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST));
+            mCurrentAlbumId = null;
+            menu.setHeaderTitle(mCurrentArtistName);
+            return;
+        } else if (itemtype == ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
+            if (cpos == -1) {
+                // this shouldn't happen
+                Log.d("Artist/Album", "no child");
+                return;
+            }
+            Cursor c = (Cursor) getExpandableListAdapter().getChild(gpos, cpos);
+            c.moveToPosition(cpos);
+            mCurrentArtistId = null;
+            mCurrentAlbumId = Long.valueOf(mi.id).toString();
+            mCurrentAlbumName = c.getString(c.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM));
+            menu.setHeaderTitle(mCurrentAlbumName);
+        }
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case PLAY_SELECTION: {
+                // play everything by the selected artist
+                int [] list =
+                    mCurrentArtistId != null ?
+                    MusicUtils.getSongListForArtist(this, Integer.parseInt(mCurrentArtistId))
+                    : MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
+                        
+                MusicUtils.playAll(this, list, 0);
+                return true;
+            }
+
+            case QUEUE: {
+                int [] list =
+                    mCurrentArtistId != null ?
+                    MusicUtils.getSongListForArtist(this, Integer.parseInt(mCurrentArtistId))
+                    : MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
+                MusicUtils.addToCurrentPlaylist(this, list);
+                return true;
+            }
+
+            case NEW_PLAYLIST: {
+                Intent intent = new Intent();
+                intent.setClass(this, CreatePlaylist.class);
+                startActivityForResult(intent, NEW_PLAYLIST);
+                return true;
+            }
+
+            case PLAYLIST_SELECTED: {
+                int [] list =
+                    mCurrentArtistId != null ?
+                    MusicUtils.getSongListForArtist(this, Integer.parseInt(mCurrentArtistId))
+                    : MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
+                int playlist = item.getIntent().getIntExtra("playlist", 0);
+                MusicUtils.addToPlaylist(this, list, playlist);
+                return true;
+            }
+            
+            case DELETE_ITEM: {
+                int [] list;
+                String desc;
+                if (mCurrentArtistId != null) {
+                    list = MusicUtils.getSongListForArtist(this, Integer.parseInt(mCurrentArtistId));
+                    String f = getString(R.string.delete_artist_desc);
+                    desc = String.format(f, mCurrentArtistName);
+                } else {
+                    list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
+                    String f = getString(R.string.delete_album_desc); 
+                    desc = String.format(f, mCurrentAlbumName);
+                }
+                Bundle b = new Bundle();
+                b.putString("description", desc);
+                b.putIntArray("items", list);
+                Intent intent = new Intent();
+                intent.setClass(this, DeleteItems.class);
+                intent.putExtras(b);
+                startActivityForResult(intent, -1);
+                return true;
+            }
+        }
+        return super.onContextItemSelected(item);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+        switch (requestCode) {
+            case SCAN_DONE:
+                if (resultCode == RESULT_CANCELED) {
+                    finish();
+                } else {
+                    init();
+                }
+                break;
+
+            case NEW_PLAYLIST:
+                if (resultCode == RESULT_OK) {
+                    Uri uri = Uri.parse(intent.getAction());
+                    if (uri != null) {
+                        int [] list = null;
+                        if (mCurrentArtistId != null) {
+                            list = MusicUtils.getSongListForArtist(this, Integer.parseInt(mCurrentArtistId));
+                        } else if (mCurrentAlbumId != null) {
+                            list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
+                        }
+                        MusicUtils.addToPlaylist(this, list, Integer.parseInt(uri.getLastPathSegment()));
+                    }
+                }
+                break;
+        }
+    }
+
+    private Cursor getArtistCursor(String filterstring) {
+
+        StringBuilder where = new StringBuilder();
+        where.append(MediaStore.Audio.Artists.ARTIST + " != ''");
+        
+        // Add in the filtering constraints
+        String [] keywords = null;
+        if (filterstring != null) {
+            String [] searchWords = filterstring.split(" ");
+            keywords = new String[searchWords.length];
+            Collator col = Collator.getInstance();
+            col.setStrength(Collator.PRIMARY);
+            for (int i = 0; i < searchWords.length; i++) {
+                keywords[i] = '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%';
+            }
+            for (int i = 0; i < searchWords.length; i++) {
+                where.append(" AND ");
+                where.append(MediaStore.Audio.Media.ARTIST_KEY + " LIKE ?");
+            }
+        }
+
+        String whereclause = where.toString();  
+        String[] cols = new String[] {
+                MediaStore.Audio.Artists._ID,
+                MediaStore.Audio.Artists.ARTIST,
+                MediaStore.Audio.Artists.NUMBER_OF_ALBUMS,
+                MediaStore.Audio.Artists.NUMBER_OF_TRACKS
+        };
+        Cursor ret;
+        ret = MusicUtils.query(this, MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
+                cols, whereclause , keywords, MediaStore.Audio.Artists.ARTIST_KEY);
+        return ret;
+    }
+    
+    class ArtistAlbumListAdapter extends SimpleCursorTreeAdapter {
+        
+        private final Drawable mNowPlayingOverlay;
+        private final BitmapDrawable mDefaultAlbumIcon;
+        private final int mGroupArtistIdIdx;
+        private final int mGroupArtistIdx;
+        private final int mGroupAlbumIdx;
+        private final int mGroupSongIdx;
+        private final Resources mResources;
+        private final String mAlbumSongSeparator;
+        private final String mUnknownAlbum;
+        private final String mUnknownArtist;
+        private final StringBuilder mBuffer = new StringBuilder();
+        private final Object[] mFormatArgs = new Object[1];
+        
+        class ViewHolder {
+            TextView line1;
+            TextView line2;
+            ImageView play_indicator;
+            ImageView icon;
+        }
+
+        ArtistAlbumListAdapter(Context context, Cursor cursor,
+                int glayout, String[] gfrom, int[] gto, 
+                int clayout, String[] cfrom, int[] cto) {
+            super(context, cursor, glayout, gfrom, gto, clayout, cfrom, cto);
+
+            Resources r = getResources();
+            mNowPlayingOverlay = r.getDrawable(R.drawable.indicator_ic_mp_playing_list);
+            mDefaultAlbumIcon = (BitmapDrawable) r.getDrawable(R.drawable.albumart_mp_unknown_list);
+            // no filter or dither, it's a lot faster and we can't tell the difference
+            mDefaultAlbumIcon.setFilterBitmap(false);
+            mDefaultAlbumIcon.setDither(false);
+            mGroupArtistIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists._ID);
+            mGroupArtistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST);
+            mGroupAlbumIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.NUMBER_OF_ALBUMS);
+            mGroupSongIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.NUMBER_OF_TRACKS);
+            
+            mResources = context.getResources();
+            mAlbumSongSeparator = context.getString(R.string.albumsongseparator);
+            mUnknownAlbum = context.getString(R.string.unknown_album_name);
+            mUnknownArtist = context.getString(R.string.unknown_artist_name);
+        }
+
+        @Override
+        public View newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent) {
+            View v = super.newGroupView(context, cursor, isExpanded, parent);
+            ImageView iv = (ImageView) v.findViewById(R.id.icon);
+            ViewGroup.LayoutParams p = iv.getLayoutParams();
+            p.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+            p.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+            ViewHolder vh = new ViewHolder();
+            vh.line1 = (TextView) v.findViewById(R.id.line1);
+            vh.line2 = (TextView) v.findViewById(R.id.line2);
+            vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator);
+            vh.icon = (ImageView) v.findViewById(R.id.icon);
+            vh.icon.setPadding(1, 1, 1, 1);
+            v.setTag(vh);
+            return v;
+        }
+
+        @Override
+        public View newChildView(Context context, Cursor cursor, boolean isLastChild,
+                ViewGroup parent) {
+            View v = super.newChildView(context, cursor, isLastChild, parent);
+            ViewHolder vh = new ViewHolder();
+            vh.line1 = (TextView) v.findViewById(R.id.line1);
+            vh.line2 = (TextView) v.findViewById(R.id.line2);
+            vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator);
+            vh.icon = (ImageView) v.findViewById(R.id.icon);
+            vh.icon.setBackgroundDrawable(mDefaultAlbumIcon);
+            vh.icon.setPadding(1, 1, 1, 1);
+            v.setTag(vh);
+            return v;
+        }
+        
+        @Override
+        public void bindGroupView(View view, Context context, Cursor cursor, boolean isexpanded) {
+
+            ViewHolder vh = (ViewHolder) view.getTag();
+
+            String artist = cursor.getString(mGroupArtistIdx);
+            String displayartist = artist;
+            boolean unknown = MediaFile.UNKNOWN_STRING.equals(artist);
+            if (unknown) {
+                displayartist = mUnknownArtist;
+            }
+            vh.line1.setText(displayartist);
+
+            int numalbums = cursor.getInt(mGroupAlbumIdx);
+            int numsongs = cursor.getInt(mGroupSongIdx);
+            
+            String songs_albums = MusicUtils.makeAlbumsSongsLabel(context,
+                    numalbums, numsongs, unknown);
+            
+            vh.line2.setText(songs_albums);
+            
+            int currentartistid = MusicUtils.getCurrentArtistId();
+            int artistid = cursor.getInt(mGroupArtistIdIdx);
+            if (currentartistid == artistid && !isexpanded) {
+                vh.play_indicator.setImageDrawable(mNowPlayingOverlay);
+            } else {
+                vh.play_indicator.setImageDrawable(null);
+            }
+        }
+
+        @Override
+        public void bindChildView(View view, Context context, Cursor cursor, boolean islast) {
+
+            ViewHolder vh = (ViewHolder) view.getTag();
+
+            String name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM));
+            String displayname = name;
+            if (name.equals(MediaFile.UNKNOWN_STRING)) {
+                displayname = mUnknownAlbum;
+            }
+            vh.line1.setText(displayname);
+
+            int numsongs = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.NUMBER_OF_SONGS));
+            int first = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.FIRST_YEAR));
+            int last = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.LAST_YEAR));
+
+            if (first == 0) {
+                first = last;
+            }
+
+            final StringBuilder builder = mBuffer;
+            builder.delete(0, builder.length());
+            if (numsongs == 1) {
+                builder.append(context.getString(R.string.onesong));
+            } else {
+                final Object[] args = mFormatArgs;
+                args[0] = numsongs;
+                builder.append(mResources.getQuantityString(R.plurals.Nsongs, numsongs, args));
+            }
+            if (first != 0 && last != 0) {
+                builder.append(mAlbumSongSeparator);
+                
+                builder.append(first);
+                if (first != last) {
+                    builder.append('-');
+                    builder.append(last);
+                } else {
+                }
+            }
+            vh.line2.setText(builder.toString());
+            
+            ImageView iv = vh.icon;
+            // We don't actually need the path to the thumbnail file,
+            // we just use it to see if there is album art or not
+            String art = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Albums.ALBUM_ART));
+            if (art == null || art.length() == 0) {
+                iv.setBackgroundDrawable(mDefaultAlbumIcon);
+                iv.setImageDrawable(null);
+            } else {
+                int artIndex = cursor.getInt(0);
+                Drawable d = MusicUtils.getCachedArtwork(context, artIndex, mDefaultAlbumIcon);
+                iv.setImageDrawable(d);
+            }
+
+            int currentalbumid = MusicUtils.getCurrentAlbumId();
+            int aid = cursor.getInt(0);
+            iv = vh.play_indicator;
+            if (currentalbumid == aid) {
+                iv.setImageDrawable(mNowPlayingOverlay);
+            } else {
+                iv.setImageDrawable(null);
+            }
+        }
+
+        
+        @Override
+        protected Cursor getChildrenCursor(Cursor groupCursor) {
+            
+            int id = groupCursor.getInt(groupCursor.getColumnIndexOrThrow(MediaStore.Audio.Artists._ID));
+            
+            String[] cols = new String[] {
+                    MediaStore.Audio.Albums._ID,
+                    MediaStore.Audio.Albums.ALBUM,
+                    MediaStore.Audio.Albums.ARTIST,
+                    MediaStore.Audio.Albums.NUMBER_OF_SONGS,
+                    MediaStore.Audio.Albums.FIRST_YEAR,
+                    MediaStore.Audio.Albums.LAST_YEAR,
+                    MediaStore.Audio.Albums.ALBUM_ART
+            };
+            return MusicUtils.query(ArtistAlbumBrowserActivity.this,
+                    MediaStore.Audio.Artists.Albums.getContentUri("external", id),
+                    cols, null, null, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
+        }
+
+        @Override
+        public void changeCursor(Cursor cursor) {
+            super.changeCursor(cursor);
+            mArtistCursor = cursor;
+        }
+        @Override
+        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+            return getArtistCursor(constraint.toString());
+        }
+
+    }
+    
+    private Cursor mArtistCursor;
+}
+
diff --git a/src/com/android/music/CreatePlaylist.java b/src/com/android/music/CreatePlaylist.java
new file mode 100644 (file)
index 0000000..87b9af5
--- /dev/null
@@ -0,0 +1,179 @@
+/*
+ * 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.android.music;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+public class CreatePlaylist extends Activity
+{
+    private EditText mPlaylist;
+    private TextView mPrompt;
+    private Button mSaveButton;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.create_playlist);
+        getWindow().setLayout(WindowManager.LayoutParams.FILL_PARENT,
+                                    WindowManager.LayoutParams.WRAP_CONTENT);
+
+        mPrompt = (TextView)findViewById(R.id.prompt);
+        mPlaylist = (EditText)findViewById(R.id.playlist);
+        mSaveButton = (Button) findViewById(R.id.create);
+        mSaveButton.setOnClickListener(mOpenClicked);
+
+        ((Button)findViewById(R.id.cancel)).setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                finish();
+            }
+        });
+        
+        String defaultname = icicle != null ? icicle.getString("defaultname") : makePlaylistName();
+        String promptformat = getString(R.string.create_playlist_create_text_prompt);
+        String prompt = String.format(promptformat, defaultname);
+        mPrompt.setText(prompt);
+        mPlaylist.setText(defaultname);
+        mPlaylist.setSelection(defaultname.length());
+        mPlaylist.addTextChangedListener(mTextWatcher);
+    }
+    
+    TextWatcher mTextWatcher = new TextWatcher() {
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+            // don't care about this one
+        }
+        public void onTextChanged(CharSequence s, int start, int before, int count) {
+            // check if playlist with current name exists already, and warn the user if so.
+            if (idForplaylist(mPlaylist.getText().toString()) >= 0) {
+                mSaveButton.setText(R.string.create_playlist_overwrite_text);
+            } else {
+                mSaveButton.setText(R.string.create_playlist_create_text);
+            }
+        };
+        public void afterTextChanged(Editable s) {
+            // don't care about this one
+        }
+    };
+    
+    private int idForplaylist(String name) {
+        Cursor c = MusicUtils.query(this, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+                new String[] { MediaStore.Audio.Playlists._ID },
+                MediaStore.Audio.Playlists.NAME + "=?",
+                new String[] { name },
+                MediaStore.Audio.Playlists.NAME);
+        int id = -1;
+        if (c != null) {
+            c.moveToFirst();
+            if (!c.isAfterLast()) {
+                id = c.getInt(0);
+            }
+        }
+        c.close();
+        return id;
+    }
+    
+    @Override
+    public void onSaveInstanceState(Bundle outcicle) {
+        outcicle.putString("defaultname", mPlaylist.getText().toString());
+    }
+    
+    @Override
+    public void onResume() {
+        super.onResume();
+    }
+
+    private String makePlaylistName() {
+
+        String template = getString(R.string.new_playlist_name_template);
+        int num = 1;
+
+        String[] cols = new String[] {
+                MediaStore.Audio.Playlists.NAME
+        };
+        ContentResolver resolver = getContentResolver();
+        String whereclause = MediaStore.Audio.Playlists.NAME + " != ''";
+        Cursor c = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+            cols, whereclause, null,
+            MediaStore.Audio.Playlists.NAME);
+
+        String suggestedname;
+        suggestedname = String.format(template, num++);
+        
+        // Need to loop until we've made 1 full pass through without finding a match.
+        // Looping more than once shouldn't happen very often, but will happen if
+        // you have playlists named "New Playlist 1"/10/2/3/4/5/6/7/8/9, where
+        // making only one pass would result in "New Playlist 10" being erroneously
+        // picked for the new name.
+        boolean done = false;
+        while (!done) {
+            done = true;
+            c.moveToFirst();
+            while (! c.isAfterLast()) {
+                String playlistname = c.getString(0);
+                if (playlistname.compareToIgnoreCase(suggestedname) == 0) {
+                    suggestedname = String.format(template, num++);
+                    done = false;
+                }
+                c.moveToNext();
+            }
+        }
+        c.close();
+        return suggestedname;
+    }
+    
+    private View.OnClickListener mOpenClicked = new View.OnClickListener() {
+        public void onClick(View v) {
+            String name = mPlaylist.getText().toString();
+            if (name != null && name.length() > 0) {
+                ContentResolver resolver = getContentResolver();
+                int id = idForplaylist(name);
+                Uri uri;
+                if (id >= 0) {
+                    uri = ContentUris.withAppendedId(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, id);
+                    MusicUtils.clearPlaylist(CreatePlaylist.this, id);
+                } else {
+                    ContentValues values = new ContentValues(1);
+                    values.put(MediaStore.Audio.Playlists.NAME, name);
+                    uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, values);
+                }
+                setResult(RESULT_OK, (new Intent()).setAction(uri.toString()));
+                finish();
+            }
+        }
+    };
+}
diff --git a/src/com/android/music/DeleteItems.java b/src/com/android/music/DeleteItems.java
new file mode 100644 (file)
index 0000000..16266cf
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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.android.music;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class DeleteItems extends Activity
+{
+    private TextView mPrompt;
+    private Button mButton;
+    private int [] mItemList;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.confirm_delete);
+        getWindow().setLayout(WindowManager.LayoutParams.FILL_PARENT,
+                                    WindowManager.LayoutParams.WRAP_CONTENT);
+
+        mPrompt = (TextView)findViewById(R.id.prompt);
+        mButton = (Button) findViewById(R.id.delete);
+        mButton.setOnClickListener(mButtonClicked);
+
+        ((Button)findViewById(R.id.cancel)).setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                finish();
+            }
+        });
+
+        Bundle b = getIntent().getExtras();
+        String desc = b.getString("description");
+        mItemList = b.getIntArray("items");
+        
+        String promptformat = getString(R.string.delete_confirm_prompt);
+        String prompt = String.format(promptformat, desc);
+        mPrompt.setText(prompt);
+    }
+    
+    private View.OnClickListener mButtonClicked = new View.OnClickListener() {
+        public void onClick(View v) {
+            // delete the selected item(s)
+            MusicUtils.deleteTracks(DeleteItems.this, mItemList);
+            finish();
+        }
+    };
+}
diff --git a/src/com/android/music/IMediaPlaybackService.aidl b/src/com/android/music/IMediaPlaybackService.aidl
new file mode 100644 (file)
index 0000000..5698cc5
--- /dev/null
@@ -0,0 +1,56 @@
+/* //device/samples/SampleCode/src/com/android/samples/app/RemoteServiceInterface.java
+**
+** Copyright 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.android.music;
+
+import android.graphics.Bitmap;
+
+interface IMediaPlaybackService
+{
+    void openfile(String path);
+    void openfileAsync(String path);
+    void open(in int [] list, int position);
+    int getQueuePosition();
+    boolean isPlaying();
+    void stop();
+    void pause();
+    void play();
+    void prev();
+    void next();
+    long duration();
+    long position();
+    long seek(long pos);
+    String getTrackName();
+    String getAlbumName();
+    int getAlbumId();
+    String getArtistName();
+    int getArtistId();
+    void enqueue(in int [] list, int action);
+    int [] getQueue();
+    void moveQueueItem(int from, int to);
+    void setQueuePosition(int index);
+    String getPath();
+    int getAudioId();
+    void setShuffleMode(int shufflemode);
+    int getShuffleMode();
+    int removeTracks(int first, int last);
+    int removeTrack(int id);
+    void setRepeatMode(int repeatmode);
+    int getRepeatMode();
+    int getMediaMountedCount();
+}
+
diff --git a/src/com/android/music/MediaButtonIntentReceiver.java b/src/com/android/music/MediaButtonIntentReceiver.java
new file mode 100644 (file)
index 0000000..08c8a4a
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * 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.android.music;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.BroadcastReceiver;
+import android.content.SharedPreferences;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.os.Handler;
+import android.os.Message;
+
+/**
+ * 
+ */
+public class MediaButtonIntentReceiver extends BroadcastReceiver {
+
+    private static final int MSG_LONGPRESS_TIMEOUT = 1;
+    private static final int LONG_PRESS_DELAY = 1000;
+
+    private static long mLastClickTime = 0;
+    private static boolean mDown = false;
+    private static boolean mLaunched = false;
+
+    private static Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_LONGPRESS_TIMEOUT:
+                    if (!mLaunched) {
+                        Context context = (Context)msg.obj;
+                        Intent i = new Intent();
+                        i.putExtra("autoshuffle", "true");
+                        i.setClass(context, MusicBrowserActivity.class);
+                        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                        context.startActivity(i);
+                        mLaunched = true;
+                    }
+                    break;
+            }
+        }
+    };
+    
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        KeyEvent event = (KeyEvent)
+                intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+        
+        if (event == null) {
+            return;
+        }
+
+        int keycode = event.getKeyCode();
+        int action = event.getAction();
+        long eventtime = event.getEventTime();
+
+        // single quick press: pause/resume. 
+        // double press: next track
+        // long press: start auto-shuffle mode.
+
+        if (keycode == KeyEvent.KEYCODE_HEADSETHOOK) {
+            if (action == KeyEvent.ACTION_DOWN) {
+                if (!mDown) {
+                    // only if this isn't a repeat event
+                    
+                    // We're not using the original time of the event as the
+                    // base here, because in some cases it can take more than
+                    // one second for us to receive the event, in which case
+                    // we would go immediately to auto shuffle mode, even if
+                    // the user didn't long press.
+                    mHandler.sendMessageDelayed(
+                            mHandler.obtainMessage(MSG_LONGPRESS_TIMEOUT, context),
+                            LONG_PRESS_DELAY);
+
+                    
+                    SharedPreferences pref = context.getSharedPreferences("Music", 
+                            Context.MODE_WORLD_READABLE | Context.MODE_WORLD_WRITEABLE);
+                    String q = pref.getString("queue", "");
+                    // The service may or may not be running, but we need to send it
+                    // a command.
+                    Intent i = new Intent(context, MediaPlaybackService.class);
+                    i.setAction(MediaPlaybackService.SERVICECMD);
+                    if (eventtime - mLastClickTime < 300) {
+                        i.putExtra(MediaPlaybackService.CMDNAME, MediaPlaybackService.CMDNEXT);
+                        context.startService(i);
+                        mLastClickTime = 0;
+                    } else {
+                        i.putExtra(MediaPlaybackService.CMDNAME,
+                                MediaPlaybackService.CMDTOGGLEPAUSE);
+                        context.startService(i);
+                        mLastClickTime = eventtime;
+                    }
+
+                    mLaunched = false;
+                    mDown = true;
+                }
+            } else {
+                mHandler.removeMessages(MSG_LONGPRESS_TIMEOUT);
+                mDown = false;
+            }
+            abortBroadcast();
+        }
+    }
+}
diff --git a/src/com/android/music/MediaPickerActivity.java b/src/com/android/music/MediaPickerActivity.java
new file mode 100644 (file)
index 0000000..8a43523
--- /dev/null
@@ -0,0 +1,291 @@
+/*
+ * 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.android.music;
+
+import com.android.internal.database.SortCursor;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ContentUris;
+import android.database.Cursor;
+import android.media.MediaFile;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.provider.MediaStore;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+public class MediaPickerActivity extends ListActivity implements MusicUtils.Defs
+{
+
+    public MediaPickerActivity()
+    {
+    }
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle icicle)
+    {
+        super.onCreate(icicle);
+
+        mFirstYear = getIntent().getStringExtra("firstyear");
+        mLastYear = getIntent().getStringExtra("lastyear");
+
+        if (mFirstYear == null) {
+            setTitle(R.string.all_title);
+        } else if (mFirstYear.equals(mLastYear)) {
+            setTitle(mFirstYear);
+        } else {
+            setTitle(mFirstYear + "-" + mLastYear);
+        }
+        MusicUtils.bindToService(this);
+        init();
+    }
+
+    @Override
+    public void onDestroy() {
+        MusicUtils.unbindFromService(this);
+        super.onDestroy();
+    }
+
+    public void init() {
+
+        setContentView(R.layout.media_picker_activity);
+
+        MakeCursor();
+        if (null == mCursor || 0 == mCursor.getCount()) {
+            return;
+        }
+
+        PickListAdapter adapter = new PickListAdapter(
+                this,
+                R.layout.track_list_item,
+                mCursor,
+                new String[] {},
+                new int[] {});
+
+        setListAdapter(adapter);
+    }
+
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id)
+    {
+        mCursor.moveToPosition(position);
+        String type = mCursor.getString(mCursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE));
+
+        String action = getIntent().getAction();
+        if (Intent.ACTION_GET_CONTENT.equals(action)) {
+            Uri uri;
+
+            long mediaId;
+            if (type.startsWith("video")) {
+                uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+                mediaId = mCursor.getLong(mCursor.getColumnIndex(MediaStore.Video.Media._ID));
+            } else {
+                uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+                mediaId = mCursor.getLong(mCursor.getColumnIndex(MediaStore.Audio.Media._ID));
+            }
+
+            setResult(RESULT_OK, new Intent().setData(ContentUris.withAppendedId(uri, mediaId)));
+            finish();
+            return;
+        }
+
+        // Need to stop the playbackservice, in case it is busy playing audio
+        // and the user selected a video.
+        if (MusicUtils.sService != null) {
+            try {
+                MusicUtils.sService.stop();
+            } catch (RemoteException ex) {
+            }
+        }
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setDataAndType(ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id), type);
+
+        startActivity(intent);
+    }
+
+    private void MakeCursor() {
+        String[] audiocols = new String[] {
+                MediaStore.Audio.Media._ID,
+                MediaStore.Audio.Media.ARTIST,
+                MediaStore.Audio.Media.ALBUM,
+                MediaStore.Audio.Media.TITLE,
+                MediaStore.Audio.Media.DATA,
+                MediaStore.Audio.Media.MIME_TYPE,
+                MediaStore.Audio.Media.YEAR
+        };
+        String[] videocols = new String[] {
+                MediaStore.Audio.Media._ID,
+                MediaStore.Audio.Media.TITLE,
+                MediaStore.Audio.Media.ARTIST,
+                MediaStore.Audio.Media.ALBUM,
+                MediaStore.Audio.Media.TITLE,
+                MediaStore.Audio.Media.DATA,
+                MediaStore.Audio.Media.MIME_TYPE
+        };
+
+        Cursor[] cs;
+        // Use ArrayList for the moment, since we don't know the size of
+        // Cursor[]. If the length of Corsor[] larger than really used,
+        // a NPE will come up when access the content of Corsor[].
+        ArrayList<Cursor> cList = new ArrayList<Cursor>();
+        Intent intent = getIntent();
+        String type = intent.getType();
+
+        if (mFirstYear != null) {
+            // If mFirstYear is not null, the picker only for audio because
+            // video has no year column.
+            if(type.equals("video/*")) {
+                mCursor = null;
+                return;
+            }
+
+            mWhereClause = MediaStore.Audio.Media.YEAR + ">=" + mFirstYear + " AND " +
+                           MediaStore.Audio.Media.YEAR + "<=" + mLastYear;
+        }
+
+        // If use Cursor[] as before, the Cursor[i] could be null when there is
+        // no video/audio/sdcard. Then a NPE will come up when access the content of the
+        // Array.
+
+        Cursor c;
+        if (type.equals("video/*")) {
+            // Only video.
+            c = MusicUtils.query(this, MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+                    videocols, null , null, mSortOrder);
+            if (c != null) {
+                cList.add(c);
+            }
+        } else {
+            c = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                    audiocols, mWhereClause , null, mSortOrder);
+
+            if (c != null) {
+                cList.add(c);
+            }
+
+            if (mFirstYear == null && intent.getType().equals("media/*")) {
+                // video has no year column
+                c = MusicUtils.query(this, MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+                    videocols, null , null, mSortOrder);
+                if (c != null) {
+                    cList.add(c);
+                }
+            }
+        }
+
+        // Get the ArrayList size.
+        int size = cList.size();
+        if (0 == size) {
+            // If no video/audio/SDCard exist, return.
+            mCursor = null;
+            return;
+        }
+
+        // The size is known now, we're sure each item of Cursor[] is not null.
+        cs = new Cursor[size];
+        cs = cList.toArray(cs);
+        mCursor = new SortCursor(cs, MediaStore.Audio.Media.TITLE);
+    }
+
+    private Cursor mCursor;
+    private String mSortOrder = MediaStore.Audio.Media.TITLE + " COLLATE UNICODE";
+    private String mFirstYear;
+    private String mLastYear;
+    private String mWhereClause;
+
+    class PickListAdapter extends SimpleCursorAdapter {
+        int mTitleIdx;
+        int mArtistIdx;
+        int mAlbumIdx;
+        int mMimeIdx;
+
+        PickListAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to) {
+            super(context, layout, cursor, from, to);
+
+            mTitleIdx = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
+            mArtistIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
+            mAlbumIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM);
+            mMimeIdx = cursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE);
+        }
+        
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+           View v = super.newView(context, cursor, parent);
+           ImageView iv = (ImageView) v.findViewById(R.id.icon);
+           iv.setVisibility(View.VISIBLE);
+           ViewGroup.LayoutParams p = iv.getLayoutParams();
+           p.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+           p.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+
+           TextView tv = (TextView) v.findViewById(R.id.duration);
+           tv.setVisibility(View.GONE);
+           iv = (ImageView) v.findViewById(R.id.play_indicator);
+           iv.setVisibility(View.GONE);
+           
+           return v;
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+
+            TextView tv = (TextView) view.findViewById(R.id.line1);
+            String name = cursor.getString(mTitleIdx);
+            tv.setText(name);
+            
+            tv = (TextView) view.findViewById(R.id.line2);
+            name = cursor.getString(mAlbumIdx);
+            StringBuilder builder = new StringBuilder();
+            if (name == null || name.equals(MediaFile.UNKNOWN_STRING)) {
+                builder.append(context.getString(R.string.unknown_album_name));
+            } else {
+                builder.append(name);
+            }
+            builder.append("\n");
+            name = cursor.getString(mArtistIdx);
+            if (name == null || name.equals(MediaFile.UNKNOWN_STRING)) {
+                builder.append(context.getString(R.string.unknown_artist_name));
+            } else {
+                builder.append(name);
+            }
+            tv.setText(builder.toString());
+
+            String text = cursor.getString(mMimeIdx);
+            ImageView iv = (ImageView) view.findViewById(R.id.icon);;
+            if("audio/midi".equals(text)) {
+                iv.setImageResource(R.drawable.midi);
+            } else if(text != null && (text.startsWith("audio") ||
+                    text.equals("application/ogg") ||
+                    text.equals("application/x-ogg"))) {
+                iv.setImageResource(R.drawable.ic_search_category_music_song);
+            } else if(text != null && text.startsWith("video")) {
+                iv.setImageResource(R.drawable.movie);
+            } else {
+                iv.setImageResource(0);
+            }
+        }
+    }
+}
diff --git a/src/com/android/music/MediaPlaybackActivity.java b/src/com/android/music/MediaPlaybackActivity.java
new file mode 100644 (file)
index 0000000..ea52b03
--- /dev/null
@@ -0,0 +1,1194 @@
+/*
+ * 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.android.music;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.media.MediaFile;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.Window;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.ImageButton;
+import android.widget.ProgressBar;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+
+public class MediaPlaybackActivity extends Activity implements MusicUtils.Defs, View.OnTouchListener
+{
+    private static final int USE_AS_RINGTONE = CHILD_MENU_BASE;
+    
+    private boolean mOneShot = false;
+    private boolean mSeeking = false;
+    private boolean mTrackball;
+    private long mStartSeekPos = 0;
+    private long mLastSeekEventTime;
+    private IMediaPlaybackService mService = null;
+    private RepeatingImageButton mPrevButton;
+    private ImageButton mPauseButton;
+    private RepeatingImageButton mNextButton;
+    private ImageButton mRepeatButton;
+    private ImageButton mShuffleButton;
+    private ImageButton mQueueButton;
+    private Worker mAlbumArtWorker;
+    private AlbumArtHandler mAlbumArtHandler;
+    private Toast mToast;
+    private boolean mRelaunchAfterConfigChange;
+
+    public MediaPlaybackActivity()
+    {
+    }
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle icicle)
+    {
+        super.onCreate(icicle);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+        mAlbumArtWorker = new Worker("album art worker");
+        mAlbumArtHandler = new AlbumArtHandler(mAlbumArtWorker.getLooper());
+
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.audio_player);
+
+        mCurrentTime = (TextView) findViewById(R.id.currenttime);
+        mTotalTime = (TextView) findViewById(R.id.totaltime);
+        mProgress = (ProgressBar) findViewById(android.R.id.progress);
+        mAlbum = (AlbumView) findViewById(R.id.album);
+        mArtistName = (TextView) findViewById(R.id.artistname);
+        mAlbumName = (TextView) findViewById(R.id.albumname);
+        mTrackName = (TextView) findViewById(R.id.trackname);
+
+        View v = (View)mArtistName.getParent(); 
+        v.setOnTouchListener(this);
+        registerForContextMenu(v);
+
+        v = (View)mAlbumName.getParent();
+        v.setOnTouchListener(this);
+        registerForContextMenu(v);
+
+        v = (View)mTrackName.getParent();
+        v.setOnTouchListener(this);
+        registerForContextMenu(v);
+        
+        mPrevButton = (RepeatingImageButton) findViewById(R.id.prev);
+        mPrevButton.setOnClickListener(mPrevListener);
+        mPrevButton.setRepeatListener(mRewListener, 260);
+        mPauseButton = (ImageButton) findViewById(R.id.pause);
+        mPauseButton.requestFocus();
+        mPauseButton.setOnClickListener(mPauseListener);
+        mNextButton = (RepeatingImageButton) findViewById(R.id.next);
+        mNextButton.setOnClickListener(mNextListener);
+        mNextButton.setRepeatListener(mFfwdListener, 260);
+        seekmethod = 1;
+
+        mTrackball = true; /* (See bug 1044348) (getResources().getConfiguration().navigation == 
+            Resources.Configuration.NAVIGATION_TRACKBALL);*/
+        
+        mQueueButton = (ImageButton) findViewById(R.id.curplaylist);
+        mQueueButton.setOnClickListener(mQueueListener);
+        mShuffleButton = ((ImageButton) findViewById(R.id.shuffle));
+        mShuffleButton.setOnClickListener(mShuffleListener);
+        mRepeatButton = ((ImageButton) findViewById(R.id.repeat));
+        mRepeatButton.setOnClickListener(mRepeatListener);
+        
+        if (mProgress instanceof SeekBar) {
+            SeekBar seeker = (SeekBar) mProgress;
+            seeker.setOnSeekBarChangeListener(mSeekListener);
+        }
+        mProgress.setMax(1000);
+        
+        if (icicle != null) {
+            mRelaunchAfterConfigChange = icicle.getBoolean("configchange");
+            mOneShot = icicle.getBoolean("oneshot");
+        } else {
+            mOneShot = getIntent().getBooleanExtra("oneshot", false);
+        }
+    }
+    
+    public boolean onTouch(View v, MotionEvent event) {
+        int action = event.getAction();
+        if (action == MotionEvent.ACTION_DOWN) {
+            v.setBackgroundColor(0xff606060);
+        } else if (action == MotionEvent.ACTION_UP ||
+                action == MotionEvent.ACTION_CANCEL) {
+            v.setBackgroundColor(0);
+        }
+        return false; 
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
+
+        /*
+         * A better way to do this would be to define a new "media search" intent (which
+         * would behave similar to a regular search intent), and have amazon, youtube, the
+         * browser and other suitable apps support it. Then we could just fire off the
+         * intent and let the user choose from the activity picker.
+         */
+        CharSequence title = null;
+        String query = null;
+        CharSequence artist = mArtistName.getText();
+        CharSequence album = mAlbumName.getText();
+        CharSequence song = mTrackName.getText();
+        if (view.equals(mArtistName.getParent()) && artist.length() > 0) {
+            title = artist;
+            query = artist.toString();
+        } else if (view.equals(mAlbumName.getParent()) &&
+                artist.length() > 0 && album.length() > 0) {
+            title = album ;
+            query = artist.toString() + " " + album.toString();
+        } else if (view.equals(mTrackName.getParent()) &&
+                artist.length() > 0 && song.length() > 0) {
+            title = song;
+            query = artist.toString() + " " + song.toString();
+        } else {
+            return;
+        }
+        
+        title = getString(R.string.mediasearch, title);
+        TextView tv = new TextView(this);
+        tv.setText(title);
+        tv.setTextSize(18);
+        tv.setPadding(8, 8, 8, 8);
+        menu.setHeaderView(tv);
+        //menu.setHeaderTitle(title);
+        
+        Intent i = new Intent();
+        i.setAction(Intent.ACTION_SEARCH);
+        i.setClassName("com.amazon.mp3", "com.amazon.mp3.android.client.SearchActivity");
+        i.putExtra("query", query);
+        PackageManager pm = getPackageManager();
+        ActivityInfo ai = i.resolveActivityInfo(pm, 0);
+        if ( ai != null) {
+            menu.add(R.string.mediasearch_amazon).setIntent(i);
+        }
+        
+        i = new Intent();
+        i.setAction(Intent.ACTION_WEB_SEARCH);
+        i.putExtra("query", query);
+        menu.add(R.string.mediasearch_google).setIntent(i);
+
+        i = new Intent();
+        i.setAction(Intent.ACTION_SEARCH);
+        i.setClassName("com.google.android.youtube", "com.google.android.youtube.QueryActivity");
+        i.putExtra("query", query);
+        menu.add(R.string.mediasearch_youtube).setIntent(i);
+        return;
+    }
+
+    private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
+        public void onStartTrackingTouch(SeekBar bar) {
+            mLastSeekEventTime = 0;
+        }
+        public void onProgressChanged(SeekBar bar, int progress, boolean fromtouch) {
+            if (mService == null) return;
+            if (fromtouch) {
+                long now = SystemClock.elapsedRealtime();
+                if ((now - mLastSeekEventTime) > 250) {
+                    mLastSeekEventTime = now;
+                    mPosOverride = mDuration * progress / 1000;
+                    try {
+                        mService.seek(mPosOverride);
+                    } catch (RemoteException ex) {
+                    }
+                }
+            }
+        }
+        public void onStopTrackingTouch(SeekBar bar) {
+            mPosOverride = -1;
+        }
+    };
+    
+    private View.OnClickListener mQueueListener = new View.OnClickListener() {
+        public void onClick(View v) {
+            startActivity(
+                    new Intent(Intent.ACTION_EDIT)
+                    .setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track")
+                    .putExtra("playlist", "nowplaying")
+            );
+        }
+    };
+    
+    private View.OnClickListener mShuffleListener = new View.OnClickListener() {
+        public void onClick(View v) {
+            toggleShuffle();
+        }
+    };
+
+    private View.OnClickListener mRepeatListener = new View.OnClickListener() {
+        public void onClick(View v) {
+            cycleRepeat();
+        }
+    };
+
+    private View.OnClickListener mPauseListener = new View.OnClickListener() {
+        public void onClick(View v) {
+            doPauseResume();
+        }
+    };
+
+    private View.OnClickListener mPrevListener = new View.OnClickListener() {
+        public void onClick(View v) {
+            if (mService == null) return;
+            try {
+                if (mService.position() < 2000) {
+                    mService.prev();
+                } else {
+                    mService.seek(0);
+                    mService.play();
+                }
+            } catch (RemoteException ex) {
+            }
+        }
+    };
+
+    private View.OnClickListener mNextListener = new View.OnClickListener() {
+        public void onClick(View v) {
+            if (mService == null) return;
+            try {
+                mService.next();
+            } catch (RemoteException ex) {
+            }
+        }
+    };
+
+    private RepeatingImageButton.RepeatListener mRewListener =
+        new RepeatingImageButton.RepeatListener() {
+        public void onRepeat(View v, long howlong, int repcnt) {
+            scanBackward(repcnt, howlong);
+        }
+    };
+    
+    private RepeatingImageButton.RepeatListener mFfwdListener =
+        new RepeatingImageButton.RepeatListener() {
+        public void onRepeat(View v, long howlong, int repcnt) {
+            scanForward(repcnt, howlong);
+        }
+    };
+   
+    @Override
+    public void onStop() {
+        paused = true;
+        if (mService != null && mOneShot && getChangingConfigurations() == 0) {
+            try {
+                mService.stop();
+            } catch (RemoteException ex) {
+            }
+        }
+        mHandler.removeMessages(REFRESH);
+        unregisterReceiver(mStatusListener);
+        MusicUtils.unbindFromService(this);
+        super.onStop();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        outState.putBoolean("configchange", getChangingConfigurations() != 0);
+        outState.putBoolean("oneshot", mOneShot);
+        super.onSaveInstanceState(outState);
+    }
+    
+    @Override
+    public void onStart() {
+        super.onStart();
+        paused = false;
+
+        if (false == MusicUtils.bindToService(this, osc)) {
+            // something went wrong
+            mHandler.sendEmptyMessage(QUIT);
+        }
+        
+        IntentFilter f = new IntentFilter();
+        f.addAction(MediaPlaybackService.PLAYSTATE_CHANGED);
+        f.addAction(MediaPlaybackService.META_CHANGED);
+        f.addAction(MediaPlaybackService.PLAYBACK_COMPLETE);
+        registerReceiver(mStatusListener, new IntentFilter(f));
+        updateTrackInfo();
+        long next = refreshNow();
+        queueNextRefresh(next);
+    }
+    
+    @Override
+    public void onNewIntent(Intent intent) {
+        setIntent(intent);
+        mOneShot = intent.getBooleanExtra("oneshot", false);
+    }
+    
+    @Override
+    public void onResume() {
+        super.onResume();
+        updateTrackInfo();
+        setPauseButtonImage();
+    }
+    
+    @Override
+    public void onDestroy()
+    {
+        mAlbumArtWorker.quit();
+        super.onDestroy();
+        //System.out.println("***************** playback activity onDestroy\n");
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        // Don't show the menu items if we got launched by path/filedescriptor, since
+        // those tend to not be in the media database.
+        if (MusicUtils.getCurrentAudioId() >= 0) {
+            if (!mOneShot) {
+                menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(R.drawable.ic_menu_music_library);
+                menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu()
+            }
+            SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0,
+                    R.string.add_to_playlist).setIcon(R.drawable.ic_menu_add);
+            MusicUtils.makePlaylistMenu(this, sub);
+            menu.add(0, USE_AS_RINGTONE, 0, R.string.ringtone_menu_short).setIcon(R.drawable.ic_menu_set_as_ringtone);
+            menu.add(0, DELETE_ITEM, 0, R.string.delete_item).setIcon(R.drawable.ic_menu_delete);
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        MenuItem item = menu.findItem(PARTY_SHUFFLE);
+        if (item != null) {
+            int shuffle = MusicUtils.getCurrentShuffleMode();
+            if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
+                item.setIcon(R.drawable.ic_menu_party_shuffle);
+                item.setTitle(R.string.party_shuffle_off);
+            } else {
+                item.setIcon(R.drawable.ic_menu_party_shuffle);
+                item.setTitle(R.string.party_shuffle);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        Intent intent;
+        try {
+            switch (item.getItemId()) {
+                case GOTO_START:
+                    intent = new Intent();
+                    intent.setClass(this, MusicBrowserActivity.class);
+                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                            | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                    startActivity(intent);
+                    break;
+                case USE_AS_RINGTONE: {
+                    // Set the system setting to make this the current ringtone
+                    if (mService != null) {
+                        MusicUtils.setRingtone(this, mService.getAudioId());
+                    }
+                    return true;
+                }
+                case PARTY_SHUFFLE:
+                    if (mService != null) {
+                        int shuffle = mService.getShuffleMode();
+                        if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
+                            mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
+                        } else {
+                            mService.setShuffleMode(MediaPlaybackService.SHUFFLE_AUTO);
+                        }
+                    }
+                    setShuffleButtonImage();
+                    break;
+                    
+                case NEW_PLAYLIST: {
+                    intent = new Intent();
+                    intent.setClass(this, CreatePlaylist.class);
+                    startActivityForResult(intent, NEW_PLAYLIST);
+                    return true;
+                }
+
+                case PLAYLIST_SELECTED: {
+                    int [] list = new int[1];
+                    list[0] = MusicUtils.getCurrentAudioId();
+                    int playlist = item.getIntent().getIntExtra("playlist", 0);
+                    MusicUtils.addToPlaylist(this, list, playlist);
+                    return true;
+                }
+                
+                case DELETE_ITEM: {
+                    if (mService != null) {
+                        int [] list = new int[1];
+                        list[0] = MusicUtils.getCurrentAudioId();
+                        Bundle b = new Bundle();
+                        b.putString("description", mService.getTrackName());
+                        b.putIntArray("items", list);
+                        intent = new Intent();
+                        intent.setClass(this, DeleteItems.class);
+                        intent.putExtras(b);
+                        startActivityForResult(intent, -1);
+                    }
+                    return true;
+                }
+            }
+        } catch (RemoteException ex) {
+        }
+        return super.onOptionsItemSelected(item);
+    }
+    
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+        if (resultCode != RESULT_OK) {
+            return;
+        }
+        switch (requestCode) {
+            case NEW_PLAYLIST:
+                Uri uri = Uri.parse(intent.getAction());
+                if (uri != null) {
+                    int [] list = new int[1];
+                    list[0] = MusicUtils.getCurrentAudioId();
+                    int playlist = Integer.parseInt(uri.getLastPathSegment());
+                    MusicUtils.addToPlaylist(this, list, playlist);
+                }
+                break;
+        }
+    }
+    private final int keyboard[][] = {
+        {
+            KeyEvent.KEYCODE_Q,
+            KeyEvent.KEYCODE_W,
+            KeyEvent.KEYCODE_E,
+            KeyEvent.KEYCODE_R,
+            KeyEvent.KEYCODE_T,
+            KeyEvent.KEYCODE_Y,
+            KeyEvent.KEYCODE_U,
+            KeyEvent.KEYCODE_I,
+            KeyEvent.KEYCODE_O,
+            KeyEvent.KEYCODE_P,
+        },
+        {
+            KeyEvent.KEYCODE_A,
+            KeyEvent.KEYCODE_S,
+            KeyEvent.KEYCODE_D,
+            KeyEvent.KEYCODE_F,
+            KeyEvent.KEYCODE_G,
+            KeyEvent.KEYCODE_H,
+            KeyEvent.KEYCODE_J,
+            KeyEvent.KEYCODE_K,
+            KeyEvent.KEYCODE_L,
+            KeyEvent.KEYCODE_DEL,
+        },
+        {
+            KeyEvent.KEYCODE_Z,
+            KeyEvent.KEYCODE_X,
+            KeyEvent.KEYCODE_C,
+            KeyEvent.KEYCODE_V,
+            KeyEvent.KEYCODE_B,
+            KeyEvent.KEYCODE_N,
+            KeyEvent.KEYCODE_M,
+            KeyEvent.KEYCODE_COMMA,
+            KeyEvent.KEYCODE_PERIOD,
+            KeyEvent.KEYCODE_ENTER
+        }
+
+    };
+
+    private int lastX;
+    private int lastY;
+
+    private boolean seekMethod1(int keyCode)
+    {
+        for(int x=0;x<10;x++) {
+            for(int y=0;y<3;y++) {
+                if(keyboard[y][x] == keyCode) {
+                    int dir = 0;
+                    // top row
+                    if(x == lastX && y == lastY) dir = 0;
+                    else if (y == 0 && lastY == 0 && x > lastX) dir = 1;
+                    else if (y == 0 && lastY == 0 && x < lastX) dir = -1;
+                    // bottom row
+                    else if (y == 2 && lastY == 2 && x > lastX) dir = -1;
+                    else if (y == 2 && lastY == 2 && x < lastX) dir = 1;
+                    // moving up
+                    else if (y < lastY && x <= 4) dir = 1; 
+                    else if (y < lastY && x >= 5) dir = -1; 
+                    // moving down
+                    else if (y > lastY && x <= 4) dir = -1; 
+                    else if (y > lastY && x >= 5) dir = 1; 
+                    lastX = x;
+                    lastY = y;
+                    try {
+                        mService.seek(mService.position() + dir * 5);
+                    } catch (RemoteException ex) {
+                    }
+                    refreshNow();
+                    return true;
+                }
+            }
+        }
+        lastX = -1;
+        lastY = -1;
+        return false;
+    }
+
+    private boolean seekMethod2(int keyCode)
+    {
+        if (mService == null) return false;
+        for(int i=0;i<10;i++) {
+            if(keyboard[0][i] == keyCode) {
+                int seekpercentage = 100*i/10;
+                try {
+                    mService.seek(mService.duration() * seekpercentage / 100);
+                } catch (RemoteException ex) {
+                }
+                refreshNow();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        try {
+            switch(keyCode)
+            {
+                case KeyEvent.KEYCODE_DPAD_LEFT:
+                    if (mTrackball) {
+                        break;
+                    }
+                    if (mService != null) {
+                        if (!mSeeking && mStartSeekPos >= 0) {
+                            mPauseButton.requestFocus();
+                            if (mStartSeekPos < 1000) {
+                                mService.prev();
+                            } else {
+                                mService.seek(0);
+                            }
+                        } else {
+                            scanBackward(-1, event.getEventTime() - event.getDownTime());
+                            mPauseButton.requestFocus();
+                            mStartSeekPos = -1;
+                        }
+                    }
+                    mSeeking = false;
+                    mPosOverride = -1;
+                    return true;
+                case KeyEvent.KEYCODE_DPAD_RIGHT:
+                    if (mTrackball) {
+                        break;
+                    }
+                    if (mService != null) {
+                        if (!mSeeking && mStartSeekPos >= 0) {
+                            mPauseButton.requestFocus();
+                            mService.next();
+                        } else {
+                            scanForward(-1, event.getEventTime() - event.getDownTime());
+                            mPauseButton.requestFocus();
+                            mStartSeekPos = -1;
+                        }
+                    }
+                    mSeeking = false;
+                    mPosOverride = -1;
+                    return true;
+            }
+        } catch (RemoteException ex) {
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event)
+    {
+        int direction = -1;
+        int repcnt = event.getRepeatCount();
+
+        if((seekmethod==0)?seekMethod1(keyCode):seekMethod2(keyCode))
+            return true;
+
+        switch(keyCode)
+        {
+/*
+            // image scale
+            case KeyEvent.KEYCODE_Q: av.adjustParams(-0.05, 0.0, 0.0, 0.0, 0.0,-1.0); break;
+            case KeyEvent.KEYCODE_E: av.adjustParams( 0.05, 0.0, 0.0, 0.0, 0.0, 1.0); break;
+            // image translate
+            case KeyEvent.KEYCODE_W: av.adjustParams(    0.0, 0.0,-1.0, 0.0, 0.0, 0.0); break;
+            case KeyEvent.KEYCODE_X: av.adjustParams(    0.0, 0.0, 1.0, 0.0, 0.0, 0.0); break;
+            case KeyEvent.KEYCODE_A: av.adjustParams(    0.0,-1.0, 0.0, 0.0, 0.0, 0.0); break;
+            case KeyEvent.KEYCODE_D: av.adjustParams(    0.0, 1.0, 0.0, 0.0, 0.0, 0.0); break;
+            // camera rotation
+            case KeyEvent.KEYCODE_R: av.adjustParams(    0.0, 0.0, 0.0, 0.0, 0.0,-1.0); break;
+            case KeyEvent.KEYCODE_U: av.adjustParams(    0.0, 0.0, 0.0, 0.0, 0.0, 1.0); break;
+            // camera translate
+            case KeyEvent.KEYCODE_Y: av.adjustParams(    0.0, 0.0, 0.0, 0.0,-1.0, 0.0); break;
+            case KeyEvent.KEYCODE_N: av.adjustParams(    0.0, 0.0, 0.0, 0.0, 1.0, 0.0); break;
+            case KeyEvent.KEYCODE_G: av.adjustParams(    0.0, 0.0, 0.0,-1.0, 0.0, 0.0); break;
+            case KeyEvent.KEYCODE_J: av.adjustParams(    0.0, 0.0, 0.0, 1.0, 0.0, 0.0); break;
+
+*/
+
+            case KeyEvent.KEYCODE_SLASH:
+                seekmethod = 1 - seekmethod;
+                return true;
+
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+                if (mTrackball) {
+                    break;
+                }
+                if (!mPrevButton.hasFocus()) {
+                    mPrevButton.requestFocus();
+                }
+                scanBackward(repcnt, event.getEventTime() - event.getDownTime());
+                return true;
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                if (mTrackball) {
+                    break;
+                }
+                if (!mNextButton.hasFocus()) {
+                    mNextButton.requestFocus();
+                }
+                scanForward(repcnt, event.getEventTime() - event.getDownTime());
+                return true;
+
+            case KeyEvent.KEYCODE_S:
+                toggleShuffle();
+                return true;
+
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+            case KeyEvent.KEYCODE_SPACE:
+                doPauseResume();
+                return true;
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+    
+    private void scanBackward(int repcnt, long delta) {
+        if(mService == null) return;
+        try {
+            if(repcnt == 0) {
+                mStartSeekPos = mService.position();
+                mLastSeekEventTime = 0;
+                mSeeking = false;
+            } else {
+                mSeeking = true;
+                if (delta < 5000) {
+                    // seek at 10x speed for the first 5 seconds
+                    delta = delta * 10; 
+                } else {
+                    // seek at 40x after that
+                    delta = 50000 + (delta - 5000) * 40;
+                }
+                long newpos = mStartSeekPos - delta;
+                if (newpos < 0) {
+                    // move to previous track
+                    mService.prev();
+                    long duration = mService.duration();
+                    mStartSeekPos += duration;
+                    newpos += duration;
+                }
+                if (((delta - mLastSeekEventTime) > 250) || repcnt < 0){
+                    mService.seek(newpos);
+                    mLastSeekEventTime = delta;
+                }
+                if (repcnt >= 0) {
+                    mPosOverride = newpos;
+                } else {
+                    mPosOverride = -1;
+                }
+                refreshNow();
+            }
+        } catch (RemoteException ex) {
+        }
+    }
+
+    private void scanForward(int repcnt, long delta) {
+        if(mService == null) return;
+        try {
+            if(repcnt == 0) {
+                mStartSeekPos = mService.position();
+                mLastSeekEventTime = 0;
+                mSeeking = false;
+            } else {
+                mSeeking = true;
+                if (delta < 5000) {
+                    // seek at 10x speed for the first 5 seconds
+                    delta = delta * 10; 
+                } else {
+                    // seek at 40x after that
+                    delta = 50000 + (delta - 5000) * 40;
+                }
+                long newpos = mStartSeekPos + delta;
+                long duration = mService.duration();
+                if (newpos >= duration) {
+                    // move to next track
+                    mService.next();
+                    mStartSeekPos -= duration; // is OK to go negative
+                    newpos -= duration;
+                }
+                if (((delta - mLastSeekEventTime) > 250) || repcnt < 0){
+                    mService.seek(newpos);
+                    mLastSeekEventTime = delta;
+                }
+                if (repcnt >= 0) {
+                    mPosOverride = newpos;
+                } else {
+                    mPosOverride = -1;
+                }
+                refreshNow();
+            }
+        } catch (RemoteException ex) {
+        }
+    }
+    
+    private void doPauseResume() {
+        try {
+            if(mService != null) {
+                if (mService.isPlaying()) {
+                    mService.pause();
+                } else {
+                    mService.play();
+                }
+                refreshNow();
+                setPauseButtonImage();
+            }
+        } catch (RemoteException ex) {
+        }
+    }
+    
+    private void toggleShuffle() {
+        if (mService == null) {
+            return;
+        }
+        try {
+            int shuffle = mService.getShuffleMode();
+            if (shuffle == MediaPlaybackService.SHUFFLE_NONE) {
+                mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL);
+                if (mService.getRepeatMode() == MediaPlaybackService.REPEAT_CURRENT) {
+                    mService.setRepeatMode(MediaPlaybackService.REPEAT_ALL);
+                    setRepeatButtonImage();
+                }
+                showToast(R.string.shuffle_on_notif);
+            } else if (shuffle == MediaPlaybackService.SHUFFLE_NORMAL ||
+                    shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
+                mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
+                showToast(R.string.shuffle_off_notif);
+            } else {
+                Log.e("MediaPlaybackActivity", "Invalid shuffle mode: " + shuffle);
+            }
+            setShuffleButtonImage();
+        } catch (RemoteException ex) {
+        }
+    }
+    
+    private void cycleRepeat() {
+        if (mService == null) {
+            return;
+        }
+        try {
+            int mode = mService.getRepeatMode();
+            if (mode == MediaPlaybackService.REPEAT_NONE) {
+                mService.setRepeatMode(MediaPlaybackService.REPEAT_ALL);
+                showToast(R.string.repeat_all_notif);
+            } else if (mode == MediaPlaybackService.REPEAT_ALL) {
+                mService.setRepeatMode(MediaPlaybackService.REPEAT_CURRENT);
+                if (mService.getShuffleMode() != MediaPlaybackService.SHUFFLE_NONE) {
+                    mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
+                    setShuffleButtonImage();
+                }
+                showToast(R.string.repeat_current_notif);
+            } else {
+                mService.setRepeatMode(MediaPlaybackService.REPEAT_NONE);
+                showToast(R.string.repeat_off_notif);
+            }
+            setRepeatButtonImage();
+        } catch (RemoteException ex) {
+        }
+        
+    }
+    
+    private void showToast(int resid) {
+        if (mToast == null) {
+            mToast = Toast.makeText(this, "", Toast.LENGTH_SHORT);
+        }
+        mToast.setText(resid);
+        mToast.show();
+    }
+
+    private void startPlayback() {
+
+        if(mService == null)
+            return;
+        Intent intent = getIntent();
+        String filename = "";
+        Uri uri = intent.getData();
+        if (uri != null && uri.toString().length() > 0) {
+            // If this is a file:// URI, just use the path directly instead
+            // of going through the open-from-filedescriptor codepath.
+            String scheme = uri.getScheme();
+            if ("file".equals(scheme)) {
+                filename = uri.getPath();
+            } else {
+                filename = uri.toString();
+            }
+            try {
+                mOneShot = true;
+                if (! mRelaunchAfterConfigChange) {
+                    mService.stop();
+                    mService.openfile(filename);
+                    mService.play();
+                }
+            } catch (Exception ex) {
+                Log.d("MediaPlaybackActivity", "couldn't start playback: " + ex);
+            }
+        }
+
+        updateTrackInfo();
+        long next = refreshNow();
+        queueNextRefresh(next);
+    }
+
+    private ServiceConnection osc = new ServiceConnection() {
+            public void onServiceConnected(ComponentName classname, IBinder obj) {
+                mService = IMediaPlaybackService.Stub.asInterface(obj);
+                if (MusicUtils.sService == null) {
+                    MusicUtils.sService = mService;
+                }
+                startPlayback();
+                try {
+                    // Assume something is playing when the service says it is,
+                    // but also if the audio ID is valid but the service is paused.
+                    if (mService.getAudioId() >= 0 || mService.isPlaying() ||
+                            mService.getPath() != null) {
+                        // something is playing now, we're done
+                        if (mOneShot) {
+                            mRepeatButton.setVisibility(View.INVISIBLE);
+                            mShuffleButton.setVisibility(View.INVISIBLE);
+                            mQueueButton.setVisibility(View.INVISIBLE);
+                        } else {
+                            mRepeatButton.setVisibility(View.VISIBLE);
+                            mShuffleButton.setVisibility(View.VISIBLE);
+                            mQueueButton.setVisibility(View.VISIBLE);
+                            setRepeatButtonImage();
+                            setShuffleButtonImage();
+                        }
+                        setPauseButtonImage();
+                        return;
+                    }
+                } catch (RemoteException ex) {
+                }
+                // Service is dead or not playing anything. If we got here as part
+                // of a "play this file" Intent, exit. Otherwise go to the Music
+                // app start screen.
+                if (getIntent().getData() == null) {
+                    Intent intent = new Intent(Intent.ACTION_MAIN);
+                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                    intent.setClass(MediaPlaybackActivity.this, MusicBrowserActivity.class);
+                    startActivity(intent);
+                }
+                finish();
+            }
+            public void onServiceDisconnected(ComponentName classname) {
+            }
+    };
+
+    private void setRepeatButtonImage() {
+        try {
+            switch (mService.getRepeatMode()) {
+                case MediaPlaybackService.REPEAT_ALL:
+                    mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_all_btn);
+                    break;
+                case MediaPlaybackService.REPEAT_CURRENT:
+                    mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_once_btn);
+                    break;
+                default:
+                    mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_off_btn);
+                    break;
+            }
+        } catch (RemoteException ex) {
+        }
+    }
+    
+    private void setShuffleButtonImage() {
+        try {
+            switch (mService.getShuffleMode()) {
+                case MediaPlaybackService.SHUFFLE_NONE:
+                    mShuffleButton.setImageResource(R.drawable.ic_mp_shuffle_off_btn);
+                    break;
+                case MediaPlaybackService.SHUFFLE_AUTO:
+                    mShuffleButton.setImageResource(R.drawable.ic_mp_partyshuffle_on_btn);
+                    break;
+                default:
+                    mShuffleButton.setImageResource(R.drawable.ic_mp_shuffle_on_btn);
+                    break;
+            }
+        } catch (RemoteException ex) {
+        }
+    }
+    
+    private void setPauseButtonImage() {
+        try {
+            if (mService != null && mService.isPlaying()) {
+                mPauseButton.setImageResource(android.R.drawable.ic_media_pause);
+            } else {
+                mPauseButton.setImageResource(android.R.drawable.ic_media_play);
+            }
+        } catch (RemoteException ex) {
+        }
+    }
+    
+    private AlbumView mAlbum;
+    private TextView mCurrentTime;
+    private TextView mTotalTime;
+    private TextView mArtistName;
+    private TextView mAlbumName;
+    private TextView mTrackName;
+    private ProgressBar mProgress;
+    private long mPosOverride = -1;
+    private long mDuration;
+    private int seekmethod;
+    private boolean paused;
+
+    private static final int REFRESH = 1;
+    private static final int QUIT = 2;
+    private static final int GET_ALBUM_ART = 3;
+    private static final int ALBUM_ART_DECODED = 4;
+
+    private void queueNextRefresh(long delay) {
+        if (!paused) {
+            Message msg = mHandler.obtainMessage(REFRESH);
+            mHandler.removeMessages(REFRESH);
+            mHandler.sendMessageDelayed(msg, delay);
+        }
+    }
+
+    private long refreshNow() {
+        if(mService == null)
+            return 500;
+        try {
+            long pos = mPosOverride < 0 ? mService.position() : mPosOverride;
+            long remaining = 1000 - (pos % 1000);
+            if ((pos >= 0) && (mDuration > 0)) {
+                mCurrentTime.setText(MusicUtils.makeTimeString(this, pos / 1000));
+                
+                if (mService.isPlaying()) {
+                    mCurrentTime.setVisibility(View.VISIBLE);
+                } else {
+                    // blink the counter
+                    int vis = mCurrentTime.getVisibility();
+                    mCurrentTime.setVisibility(vis == View.INVISIBLE ? View.VISIBLE : View.INVISIBLE);
+                    remaining = 500;
+                }
+
+                mProgress.setProgress((int) (1000 * pos / mDuration));
+            } else {
+                mCurrentTime.setText("--:--");
+                mProgress.setProgress(1000);
+            }
+            // return the number of milliseconds until the next full second, so
+            // the counter can be updated at just the right time
+            return remaining;
+        } catch (RemoteException ex) {
+        }
+        return 500;
+    }
+    
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case ALBUM_ART_DECODED:
+                    mAlbum.setArtwork((Bitmap)msg.obj);
+                    mAlbum.invalidate();
+                    break;
+
+                case REFRESH:
+                    long next = refreshNow();
+                    queueNextRefresh(next);
+                    break;
+                    
+                case QUIT:
+                    // This can be moved back to onCreate once the bug that prevents
+                    // Dialogs from being started from onCreate/onResume is fixed.
+                    new AlertDialog.Builder(MediaPlaybackActivity.this)
+                            .setTitle(R.string.service_start_error_title)
+                            .setMessage(R.string.service_start_error_msg)
+                            .setPositiveButton(R.string.service_start_error_button,
+                                    new DialogInterface.OnClickListener() {
+                                        public void onClick(DialogInterface dialog, int whichButton) {
+                                            finish();
+                                        }
+                                    })
+                            .setCancelable(false)
+                            .show();
+                    break;
+
+                default:
+                    break;
+            }
+        }
+    };
+
+    private BroadcastReceiver mStatusListener = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(MediaPlaybackService.META_CHANGED)) {
+                // redraw the artist/title info and
+                // set new max for progress bar
+                updateTrackInfo();
+                setPauseButtonImage();
+                queueNextRefresh(1);
+            } else if (action.equals(MediaPlaybackService.PLAYBACK_COMPLETE)) {
+                if (mOneShot) {
+                    finish();
+                } else {
+                    setPauseButtonImage();
+                }
+            } else if (action.equals(MediaPlaybackService.PLAYSTATE_CHANGED)) {
+                setPauseButtonImage();
+            }
+        }
+    };
+
+    private void updateTrackInfo() {
+        if (mService == null) {
+            return;
+        }
+        try {
+            if (mService.getPath() == null) {
+                finish();
+                return;
+            }
+            String artistName = mService.getArtistName();
+            if (MediaFile.UNKNOWN_STRING.equals(artistName)) {
+                artistName = getString(R.string.unknown_artist_name);
+            }
+            mArtistName.setText(artistName);
+            String albumName = mService.getAlbumName();
+            int albumid = mService.getAlbumId();
+            if (MediaFile.UNKNOWN_STRING.equals(albumName)) {
+                albumName = getString(R.string.unknown_album_name);
+                albumid = -1;
+            }
+            mAlbumName.setText(albumName);
+            mTrackName.setText(mService.getTrackName());
+            mAlbumArtHandler.removeMessages(GET_ALBUM_ART);
+            mAlbumArtHandler.obtainMessage(GET_ALBUM_ART, albumid, 0).sendToTarget();
+            mDuration = mService.duration();
+            mTotalTime.setText(MusicUtils.makeTimeString(this, mDuration / 1000));
+        } catch (RemoteException ex) {
+            finish();
+        }
+    }
+    
+    public class AlbumArtHandler extends Handler {
+        private int mAlbumId = -1;
+        
+        public AlbumArtHandler(Looper looper) {
+            super(looper);
+        }
+        public void handleMessage(Message msg)
+        {
+            int albumid = msg.arg1;
+            if (msg.what == GET_ALBUM_ART && (mAlbumId != albumid || albumid < 0)) {
+                // while decoding the new image, show the default album art
+                Message numsg = mHandler.obtainMessage(ALBUM_ART_DECODED, null);
+                mHandler.removeMessages(ALBUM_ART_DECODED);
+                mHandler.sendMessageDelayed(numsg, 300);
+                Bitmap bm = MusicUtils.getArtwork(MediaPlaybackActivity.this, albumid);
+                if (bm == null) {
+                    bm = MusicUtils.getArtwork(MediaPlaybackActivity.this, -1);
+                    albumid = -1;
+                }
+                if (bm != null) {
+                    numsg = mHandler.obtainMessage(ALBUM_ART_DECODED, bm);
+                    mHandler.removeMessages(ALBUM_ART_DECODED);
+                    mHandler.sendMessage(numsg);
+                }
+                mAlbumId = albumid;
+            }
+        }
+    }
+    
+    private class Worker implements Runnable {
+        private final Object mLock = new Object();
+        private Looper mLooper;
+        
+        /**
+         * Creates a worker thread with the given name. The thread
+         * then runs a {@link android.os.Looper}.
+         * @param name A name for the new thread
+         */
+        Worker(String name) {
+            Thread t = new Thread(null, this, name);
+            t.setPriority(Thread.MIN_PRIORITY);
+            t.start();
+            synchronized (mLock) {
+                while (mLooper == null) {
+                    try {
+                        mLock.wait();
+                    } catch (InterruptedException ex) {
+                    }
+                }
+            }
+        }
+        
+        public Looper getLooper() {
+            return mLooper;
+        }
+        
+        public void run() {
+            synchronized (mLock) {
+                Looper.prepare();
+                mLooper = Looper.myLooper();
+                mLock.notifyAll();
+            }
+            Looper.loop();
+        }
+        
+        public void quit() {
+            mLooper.quit();
+        }
+    }
+}
+
diff --git a/src/com/android/music/MediaPlaybackService.java b/src/com/android/music/MediaPlaybackService.java
new file mode 100644 (file)
index 0000000..8008780
--- /dev/null
@@ -0,0 +1,1618 @@
+/*
+ * 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.android.music;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.BroadcastReceiver;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.media.MediaFile;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.os.PowerManager.WakeLock;
+import android.provider.MediaStore;
+import android.provider.Settings;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneStateIntentReceiver;
+import android.util.Log;
+import android.widget.RemoteViews;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.util.Random;
+import java.util.Vector;
+
+/**
+ * Provides "background" audio playback capabilities, allowing the
+ * user to switch between activities without stopping playback.
+ */
+public class MediaPlaybackService extends Service {
+    /** used to specify whether enqueue() should start playing
+     * the new list of files right away, next or once all the currently
+     * queued files have been played
+     */
+    public static final int NOW = 1;
+    public static final int NEXT = 2;
+    public static final int LAST = 3;
+    public static final int PLAYBACKSERVICE_STATUS = 1;
+    
+    public static final int SHUFFLE_NONE = 0;
+    public static final int SHUFFLE_NORMAL = 1;
+    public static final int SHUFFLE_AUTO = 2;
+    
+    public static final int REPEAT_NONE = 0;
+    public static final int REPEAT_CURRENT = 1;
+    public static final int REPEAT_ALL = 2;
+
+    public static final String PLAYSTATE_CHANGED = "com.android.music.playstatechanged";
+    public static final String META_CHANGED = "com.android.music.metachanged";
+    public static final String QUEUE_CHANGED = "com.android.music.queuechanged";
+    public static final String PLAYBACK_COMPLETE = "com.android.music.playbackcomplete";
+    public static final String ASYNC_OPEN_COMPLETE = "com.android.music.asyncopencomplete";
+
+    public static final String SERVICECMD = "com.android.music.musicservicecommand";
+    public static final String CMDNAME = "command";
+    public static final String CMDTOGGLEPAUSE = "togglepause";
+    public static final String CMDPAUSE = "pause";
+    public static final String CMDNEXT = "next";
+    
+    private static final int PHONE_CHANGED = 1;
+    private static final int TRACK_ENDED = 1;
+    private static final int RELEASE_WAKELOCK = 2;
+    private static final int SERVER_DIED = 3;
+    private static final int MAX_HISTORY_SIZE = 10;
+
+    private MultiPlayer mPlayer;
+    private String mFileToPlay;
+    private PhoneStateIntentReceiver mPsir;
+    private int mShuffleMode = SHUFFLE_NONE;
+    private int mRepeatMode = REPEAT_NONE;
+    private int mMediaMountedCount = 0;
+    private int [] mAutoShuffleList = null;
+    private boolean mOneShot;
+    private int [] mPlayList = null;
+    private int mPlayListLen = 0;
+    private Vector<Integer> mHistory = new Vector<Integer>(MAX_HISTORY_SIZE);
+    private Cursor mCursor;
+    private int mPlayPos = -1;
+    private static final String LOGTAG = "MediaPlaybackService";
+    private final Shuffler mRand = new Shuffler();
+    private int mOpenFailedCounter = 0;
+    String[] mCursorCols = new String[] {
+            "audio._id AS _id",
+            MediaStore.Audio.Media.ARTIST,
+            MediaStore.Audio.Media.ALBUM,
+            MediaStore.Audio.Media.TITLE,
+            MediaStore.Audio.Media.DATA,
+            MediaStore.Audio.Media.MIME_TYPE,
+            MediaStore.Audio.Media.ALBUM_ID,
+            MediaStore.Audio.Media.ARTIST_ID
+    };
+    private BroadcastReceiver mUnmountReceiver = null;
+    private WakeLock mWakeLock;
+    private int mServiceStartId = -1;
+    private boolean mServiceInUse = false;
+    private boolean mResumeAfterCall = false;
+    private boolean mWasPlaying = false;
+    
+    private SharedPreferences mPreferences;
+    // We use this to distinguish between different cards when saving/restoring playlists.
+    // This will have to change if we want to support multiple simultaneous cards.
+    private int mCardId;
+    
+    // interval after which we stop the service when idle
+    private static final int IDLE_DELAY = 60000; 
+
+    private Handler mPhoneHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case PHONE_CHANGED:
+                    Phone.State state = mPsir.getPhoneState();
+                    if (state == Phone.State.RINGING) {
+                        AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+                        int ringvolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
+                        if (ringvolume > 0) {
+                            mResumeAfterCall = (isPlaying() || mResumeAfterCall) && (getAudioId() >= 0);
+                            pause();
+                        }
+                    } else if (state == Phone.State.OFFHOOK) {
+                        // pause the music while a conversation is in progress
+                        mResumeAfterCall = (isPlaying() || mResumeAfterCall) && (getAudioId() >= 0);
+                        pause();
+                    } else if (state == Phone.State.IDLE) {
+                        // start playing again
+                        if (mResumeAfterCall) {
+                            // resume playback only if music was playing
+                            // when the call was answered
+                            play();
+                            mResumeAfterCall = false;
+                        }
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+    };
+
+    private Handler mMediaplayerHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case SERVER_DIED:
+                    if (mWasPlaying) {
+                        next(true);
+                    } else {
+                        // the server died when we were idle, so just
+                        // reopen the same song (it will start again
+                        // from the beginning though when the user
+                        // restarts)
+                        openCurrent();
+                    }
+                    break;
+                case TRACK_ENDED:
+                    if (mRepeatMode == REPEAT_CURRENT) {
+                        seek(0);
+                        play();
+                    } else if (!mOneShot) {
+                        next(false);
+                    } else {
+                        notifyChange(PLAYBACK_COMPLETE);
+                    }
+                    break;
+                case RELEASE_WAKELOCK:
+                    mWakeLock.release();
+                    break;
+                default:
+                    break;
+            }
+        }
+    };
+
+    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String cmd = intent.getStringExtra("command");
+            if (CMDNEXT.equals(cmd)) {
+                next(true);
+            } else if (CMDTOGGLEPAUSE.equals(cmd)) {
+                if (isPlaying()) {
+                    pause();
+                } else {
+                    play();
+                }
+            } else if (CMDPAUSE.equals(cmd)) {
+                pause();
+            }
+        }
+    };
+
+    public MediaPlaybackService() {
+        mPsir = new PhoneStateIntentReceiver(this, mPhoneHandler);
+        mPsir.notifyPhoneCallState(PHONE_CHANGED);
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        
+        mPreferences = getSharedPreferences("Music", MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE);
+        mCardId = FileUtils.getFatVolumeId(Environment.getExternalStorageDirectory().getPath());
+        
+        registerExternalStorageListener();
+
+        // Needs to be done in this thread, since otherwise ApplicationContext.getPowerManager() crashes.
+        mPlayer = new MultiPlayer();
+        mPlayer.setHandler(mMediaplayerHandler);
+
+        // Clear leftover notification in case this service previously got killed while playing
+        NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+        nm.cancel(PLAYBACKSERVICE_STATUS);
+        
+        reloadQueue();
+
+        registerReceiver(mIntentReceiver, new IntentFilter(SERVICECMD));
+        mPsir.registerIntent();
+        PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName());
+        mWakeLock.setReferenceCounted(false);
+    }
+
+    @Override
+    public void onDestroy() {
+        unregisterReceiver(mIntentReceiver);
+        if (mUnmountReceiver != null) {
+            unregisterReceiver(mUnmountReceiver);
+            mUnmountReceiver = null;
+        }
+        mPsir.unregisterIntent();
+        mWakeLock.release();
+        super.onDestroy();
+    }
+    
+    private final char hexdigits [] = new char [] {
+            '0', '1', '2', '3',
+            '4', '5', '6', '7',
+            '8', '9', 'a', 'b',
+            'c', 'd', 'e', 'f'
+    };
+
+    private void saveQueue(boolean full) {
+        if (mOneShot) {
+            return;
+        }
+        Editor ed = mPreferences.edit();
+        //long start = System.currentTimeMillis();
+        if (full) {
+            StringBuilder q = new StringBuilder();
+            
+            // The current playlist is saved as a list of "reverse hexadecimal"
+            // numbers, which we can generate faster than normal decimal or
+            // hexadecimal numbers, which in turn allows us to save the playlist
+            // more often without worrying too much about performance.
+            // (saving the full state takes about 40 ms under no-load conditions
+            // on the phone)
+            int len = mPlayListLen;
+            for (int i = 0; i < len; i++) {
+                int n = mPlayList[i];
+                if (n == 0) {
+                    q.append("0;");
+                } else {
+                    while (n != 0) {
+                        int digit = n & 0xf;
+                        n >>= 4;
+                        q.append(hexdigits[digit]);
+                    }
+                    q.append(";");
+                }
+            }
+            //Log.i("@@@@ service", "created queue string in " + (System.currentTimeMillis() - start) + " ms");
+            ed.putString("queue", q.toString());
+            ed.putInt("cardid", mCardId);
+        }
+        ed.putInt("curpos", mPlayPos);
+        if (mPlayer.isInitialized()) {
+            ed.putLong("seekpos", mPlayer.position());
+        }
+        ed.putInt("repeatmode", mRepeatMode);
+        ed.putInt("shufflemode", mShuffleMode);
+        ed.commit();
+  
+        //Log.i("@@@@ service", "saved state in " + (System.currentTimeMillis() - start) + " ms");
+    }
+
+    private void reloadQueue() {
+        String q = null;
+        
+        boolean newstyle = false;
+        int id = mCardId;
+        if (mPreferences.contains("cardid")) {
+            newstyle = true;
+            id = mPreferences.getInt("cardid", ~mCardId);
+        }
+        if (id == mCardId) {
+            // Only restore the saved playlist if the card is still
+            // the same one as when the playlist was saved
+            q = mPreferences.getString("queue", "");
+        }
+        if (q != null && q.length() > 1) {
+            //Log.i("@@@@ service", "loaded queue: " + q);
+            String [] entries = q.split(";");
+            int len = entries.length;
+            ensurePlayListCapacity(len);
+            for (int i = 0; i < len; i++) {
+                if (newstyle) {
+                    String revhex = entries[i];
+                    int n = 0;
+                    for (int j = revhex.length() - 1; j >= 0 ; j--) {
+                        n <<= 4;
+                        char c = revhex.charAt(j);
+                        if (c >= '0' && c <= '9') {
+                            n += (c - '0');
+                        } else if (c >= 'a' && c <= 'f') {
+                            n += (10 + c - 'a');
+                        } else {
+                            // bogus playlist data
+                            len = 0;
+                            break;
+                        }
+                    }
+                    mPlayList[i] = n;
+                } else {
+                    mPlayList[i] = Integer.parseInt(entries[i]);
+                }
+            }
+            mPlayListLen = len;
+
+            int pos = mPreferences.getInt("curpos", 0);
+            if (pos < 0 || pos >= len) {
+                // The saved playlist is bogus, discard it
+                mPlayListLen = 0;
+                return;
+            }
+            mPlayPos = pos;
+            
+            // When reloadQueue is called in response to a card-insertion,
+            // we might not be able to query the media provider right away.
+            // To deal with this, try querying for the current file, and if
+            // that fails, wait a while and try again. If that too fails,
+            // assume there is a problem and don't restore the state.
+            Cursor c = MusicUtils.query(this,
+                        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                        new String [] {"_id"}, "_id=" + mPlayList[mPlayPos] , null, null);
+            if (c == null || c.getCount() == 0) {
+                // wait a bit and try again
+                SystemClock.sleep(3000);
+                c = getContentResolver().query(
+                        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                        mCursorCols, "_id=" + mPlayList[mPlayPos] , null, null);
+            }
+            if (c != null) {
+                c.close();
+            }
+
+            // Make sure we don't auto-skip to the next song, since that
+            // also starts playback. What could happen in that case is:
+            // - music is paused
+            // - go to UMS and delete some files, including the currently playing one
+            // - come back from UMS
+            // (time passes)
+            // - music app is killed for some reason (out of memory)
+            // - music service is restarted, service restores state, doesn't find
+            //   the "current" file, goes to the next and: playback starts on its
+            //   own, potentially at some random inconvenient time.
+            mOpenFailedCounter = 20;
+            openCurrent();
+            if (!mPlayer.isInitialized()) {
+                // couldn't restore the saved state
+                mPlayListLen = 0;
+                return;
+            }
+            
+            long seekpos = mPreferences.getLong("seekpos", 0);
+            seek(seekpos >= 0 && seekpos < duration() ? seekpos : 0);
+            
+            int repmode = mPreferences.getInt("repeatmode", REPEAT_NONE);
+            if (repmode != REPEAT_ALL && repmode != REPEAT_CURRENT) {
+                repmode = REPEAT_NONE;
+            }
+            mRepeatMode = repmode;
+
+            int shufmode = mPreferences.getInt("shufflemode", SHUFFLE_NONE);
+            if (shufmode != SHUFFLE_AUTO && shufmode != SHUFFLE_NORMAL) {
+                shufmode = SHUFFLE_NONE;
+            }
+            if (shufmode == SHUFFLE_AUTO) {
+                if (! makeAutoShuffleList()) {
+                    shufmode = SHUFFLE_NONE;
+                }
+            }
+            mShuffleMode = shufmode;
+        }
+    }
+    
+    @Override
+    public IBinder onBind(Intent intent) {
+        mDelayedStopHandler.removeCallbacksAndMessages(null);
+        mServiceInUse = true;
+        return mBinder;
+    }
+
+    @Override
+    public void onRebind(Intent intent) {
+        mDelayedStopHandler.removeCallbacksAndMessages(null);
+        mServiceInUse = true;
+    }
+
+    @Override
+    public void onStart(Intent intent, int startId) {
+        mServiceStartId = startId;
+        mDelayedStopHandler.removeCallbacksAndMessages(null);
+        String cmd = intent.getStringExtra("command");
+        if (CMDNEXT.equals(cmd)) {
+            next(true);
+        } else if (CMDTOGGLEPAUSE.equals(cmd)) {
+            if (isPlaying()) {
+                pause();
+            } else {
+                play();
+            }
+        } else if (CMDPAUSE.equals(cmd)) {
+            pause();
+        }
+        // make sure the service will shut down on its own if it was
+        // just started but not bound to and nothing is playing
+        mDelayedStopHandler.removeCallbacksAndMessages(null);
+        Message msg = mDelayedStopHandler.obtainMessage();
+        mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
+    }
+    
+    @Override
+    public boolean onUnbind(Intent intent) {
+        mServiceInUse = false;
+
+        // Take a snapshot of the current playlist
+        saveQueue(true);
+
+        if (isPlaying() || mResumeAfterCall) {
+            // something is currently playing, or will be playing once 
+            // an in-progress call ends, so don't stop the service now.
+            return true;
+        }
+        
+        // If there is a playlist but playback is paused, then wait a while
+        // before stopping the service, so that pause/resume isn't slow.
+        // Also delay stopping the service if we're transitioning between tracks.
+        if (mPlayListLen > 0  || mMediaplayerHandler.hasMessages(TRACK_ENDED)) {
+            Message msg = mDelayedStopHandler.obtainMessage();
+            mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
+            return true;
+        }
+        
+        // No active playlist, OK to stop the service right now
+        stopSelf(mServiceStartId);
+        return true;
+    }
+    
+    private Handler mDelayedStopHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            // Check again to make sure nothing is playing right now
+            if (isPlaying() || mResumeAfterCall || mServiceInUse
+                    || mMediaplayerHandler.hasMessages(TRACK_ENDED)) {
+                return;
+            }
+            // save the queue again, because it might have change
+            // since the user exited the music app (because of
+            // party-shuffle or because the play-position changed)
+            saveQueue(true);
+            stopSelf(mServiceStartId);
+        }
+    };
+    
+    /**
+     * Called when we receive a ACTION_MEDIA_EJECT notification.
+     *
+     * @param storagePath path to mount point for the removed media
+     */
+    public void closeExternalStorageFiles(String storagePath) {
+        // stop playback and clean up if the SD card is going to be unmounted.
+        stop(true);
+        notifyChange(QUEUE_CHANGED);
+        notifyChange(META_CHANGED);
+    }
+
+    /**
+     * Registers an intent to listen for ACTION_MEDIA_EJECT notifications.
+     * The intent will call closeExternalStorageFiles() if the external media
+     * is going to be ejected, so applications can clean up any files they have open.
+     */
+    public void registerExternalStorageListener() {
+        if (mUnmountReceiver == null) {
+            mUnmountReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    String action = intent.getAction();
+                    if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
+                        saveQueue(true);
+                        mOneShot = true; // This makes us not save the state again later,
+                                         // which would be wrong because the song ids and
+                                         // card id might not match. 
+                        closeExternalStorageFiles(intent.getData().getPath());
+                    } else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
+                        mMediaMountedCount++;
+                        mCardId = FileUtils.getFatVolumeId(intent.getData().getPath());
+                        reloadQueue();
+                        notifyChange(QUEUE_CHANGED);
+                        notifyChange(META_CHANGED);
+                    }
+                }
+            };
+            IntentFilter iFilter = new IntentFilter();
+            iFilter.addAction(Intent.ACTION_MEDIA_EJECT);
+            iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
+            iFilter.addDataScheme("file");
+            registerReceiver(mUnmountReceiver, iFilter);
+        }
+    }
+
+    /**
+     * Notify the change-receivers that something has changed.
+     * The intent that is sent contains the following data
+     * for the currently playing track:
+     * "id" - Integer: the database row ID
+     * "artist" - String: the name of the artist
+     * "album" - String: the name of the album
+     * "track" - String: the name of the track
+     * The intent has an action that is one of
+     * "com.android.music.metachanged"
+     * "com.android.music.queuechanged",
+     * "com.android.music.playbackcomplete"
+     * "com.android.music.playstatechanged"
+     * respectively indicating that a new track has
+     * started playing, that the playback queue has
+     * changed, that playback has stopped because
+     * the last file in the list has been played,
+     * or that the play-state changed (paused/resumed).
+     */
+    private void notifyChange(String what) {
+        
+        Intent i = new Intent(what);
+        i.putExtra("id", Integer.valueOf(getAudioId()));
+        i.putExtra("artist", getArtistName());
+        i.putExtra("album",getAlbumName());
+        i.putExtra("track", getTrackName());
+        sendBroadcast(i);
+        
+        if (what.equals(QUEUE_CHANGED)) {
+            saveQueue(true);
+        } else {
+            saveQueue(false);
+        }
+    }
+
+    private void ensurePlayListCapacity(int size) {
+        if (mPlayList == null || size > mPlayList.length) {
+            // reallocate at 2x requested size so we don't
+            // need to grow and copy the array for every
+            // insert
+            int [] newlist = new int[size * 2];
+            int len = mPlayListLen;
+            for (int i = 0; i < len; i++) {
+                newlist[i] = mPlayList[i];
+            }
+            mPlayList = newlist;
+        }
+        // FIXME: shrink the array when the needed size is much smaller
+        // than the allocated size
+    }
+    
+    private void addToPlayList(int id) {
+        synchronized(this) {
+            ensurePlayListCapacity(mPlayListLen + 1);
+            mPlayList[mPlayListLen++] = id;
+        }
+    }
+    
+    private void addToPlayList(int [] list, int position) {
+        int addlen = list.length;
+        if (position < 0) { // overwrite
+            mPlayListLen = 0;
+            position = 0;
+        }
+        ensurePlayListCapacity(mPlayListLen + addlen);
+        if (position > mPlayListLen) {
+            position = mPlayListLen;
+        }
+        
+        // move part of list after insertion point
+        int tailsize = mPlayListLen - position;
+        for (int i = tailsize ; i > 0 ; i--) {
+            mPlayList[position + i] = mPlayList[position + i - addlen]; 
+        }
+        
+        // copy list into playlist
+        for (int i = 0; i < addlen; i++) {
+            mPlayList[position + i] = list[i];
+        }
+        mPlayListLen += addlen;
+    }
+    
+    /**
+     * Appends a list of tracks to the current playlist.
+     * If nothing is playing currently, playback will be started at
+     * the first track.
+     * If the action is NOW, playback will switch to the first of
+     * the new tracks immediately.
+     * @param list The list of tracks to append.
+     * @param action NOW, NEXT or LAST
+     */
+    public void enqueue(int [] list, int action) {
+        synchronized(this) {
+            if (action == NEXT && mPlayPos + 1 < mPlayListLen) {
+                addToPlayList(list, mPlayPos + 1);
+                notifyChange(QUEUE_CHANGED);
+            } else {
+                // action == LAST || action == NOW || mPlayPos + 1 == mPlayListLen
+                addToPlayList(list, Integer.MAX_VALUE);
+                notifyChange(QUEUE_CHANGED);
+                if (action == NOW) {
+                    mPlayPos = mPlayListLen - list.length;
+                    openCurrent();
+                    play();
+                    notifyChange(META_CHANGED);
+                    return;
+                }
+            }
+            if (mPlayPos < 0) {
+                mPlayPos = 0;
+                openCurrent();
+                play();
+                notifyChange(META_CHANGED);
+            }
+        }
+    }
+
+    /**
+     * Replaces the current playlist with a new list,
+     * and prepares for starting playback at the specified
+     * position in the list.
+     * @param list The new list of tracks.
+     */
+    public void open(int [] list, int position) {
+        synchronized (this) {
+            if (mShuffleMode == SHUFFLE_AUTO) {
+                mShuffleMode = SHUFFLE_NORMAL;
+            }
+            addToPlayList(list, -1);
+            mPlayPos = position;
+            mHistory.clear();
+
+            openCurrent();
+        }
+    }
+    
+    /**
+     * Moves the item at index1 to index2.
+     * @param index1
+     * @param index2
+     */
+    public void moveQueueItem(int index1, int index2) {
+        synchronized (this) {
+            if (index1 >= mPlayListLen) {
+                index1 = mPlayListLen - 1;
+            }
+            if (index2 >= mPlayListLen) {
+                index2 = mPlayListLen - 1;
+            }
+            if (index1 < index2) {
+                int tmp = mPlayList[index1];
+                for (int i = index1; i < index2; i++) {
+                    mPlayList[i] = mPlayList[i+1];
+                }
+                mPlayList[index2] = tmp;
+                if (mPlayPos == index1) {
+                    mPlayPos = index2;
+                } else if (mPlayPos >= index1 && mPlayPos <= index2) {
+                        mPlayPos--;
+                }
+            } else if (index2 < index1) {
+                int tmp = mPlayList[index1];
+                for (int i = index1; i > index2; i--) {
+                    mPlayList[i] = mPlayList[i-1];
+                }
+                mPlayList[index2] = tmp;
+                if (mPlayPos == index1) {
+                    mPlayPos = index2;
+                } else if (mPlayPos >= index2 && mPlayPos <= index1) {
+                        mPlayPos++;
+                }
+            }
+            notifyChange(QUEUE_CHANGED);
+        }
+    }
+
+    /**
+     * Returns the current play list
+     * @return An array of integers containing the IDs of the tracks in the play list
+     */
+    public int [] getQueue() {
+        synchronized (this) {
+            int len = mPlayListLen;
+            int [] list = new int[len];
+            for (int i = 0; i < len; i++) {
+                list[i] = mPlayList[i];
+            }
+            return list;
+        }
+    }
+
+    private void openCurrent() {
+        synchronized (this) {
+            if (mCursor != null) {
+                mCursor.close();
+                mCursor = null;
+            }
+            if (mPlayListLen == 0) {
+                return;
+            }
+            stop(false);
+
+            String id = String.valueOf(mPlayList[mPlayPos]);
+            
+            mCursor = getContentResolver().query(
+                    MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                    mCursorCols, "_id=" + id , null, null);
+            if (mCursor != null) {
+                mCursor.moveToFirst();
+                open(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + id, false);
+            }
+        }
+    }
+
+    public void openAsync(String path) {
+        synchronized (this) {
+            if (path == null) {
+                return;
+            }
+            
+            mRepeatMode = REPEAT_NONE;
+            ensurePlayListCapacity(1);
+            mPlayListLen = 1;
+            mPlayPos = -1;
+            
+            mFileToPlay = path;
+            mCursor = null;
+            mPlayer.setDataSourceAsync(mFileToPlay);
+            mOneShot = true;
+        }
+    }
+    
+    /**
+     * Opens the specified file and readies it for playback.
+     *
+     * @param path The full path of the file to be opened.
+     * @param oneshot when set to true, playback will stop after this file completes, instead
+     * of moving on to the next track in the list 
+     */
+    public void open(String path, boolean oneshot) {
+        synchronized (this) {
+            if (path == null) {
+                return;
+            }
+            
+            if (oneshot) {
+                mRepeatMode = REPEAT_NONE;
+                ensurePlayListCapacity(1);
+                mPlayListLen = 1;
+                mPlayPos = -1;
+            }
+            
+            // if mCursor is null, try to associate path with a database cursor
+            if (mCursor == null) {
+
+                ContentResolver resolver = getContentResolver();
+                Uri uri;
+                String where;
+                String selectionArgs[];
+                if (path.startsWith("content://media/")) {
+                    uri = Uri.parse(path);
+                    where = null;
+                    selectionArgs = null;
+                } else {
+                   uri = MediaStore.Audio.Media.getContentUriForPath(path);
+                   where = MediaStore.Audio.Media.DATA + "=?";
+                   selectionArgs = new String[] { path };
+                }
+                
+                try {
+                    mCursor = resolver.query(uri, mCursorCols, where, selectionArgs, null);
+                    if  (mCursor != null) {
+                        if (mCursor.getCount() == 0) {
+                            mCursor.close();
+                            mCursor = null;
+                        } else {
+                            mCursor.moveToNext();
+                            ensurePlayListCapacity(1);
+                            mPlayListLen = 1;
+                            mPlayList[0] = mCursor.getInt(0);
+                            mPlayPos = 0;
+                        }
+                    }
+                } catch (UnsupportedOperationException ex) {
+                }
+            }
+            mFileToPlay = path;
+            mPlayer.setDataSource(mFileToPlay);
+            mOneShot = oneshot;
+            if (! mPlayer.isInitialized()) {
+                stop(true);
+                if (mOpenFailedCounter++ < 10 &&  mPlayListLen > 1) {
+                    // beware: this ends up being recursive because next() calls open() again.
+                    next(false);
+                }
+                if (! mPlayer.isInitialized() && mOpenFailedCounter != 0) {
+                    // need to make sure we only shows this once
+                    mOpenFailedCounter = 0;
+                    Toast.makeText(this, R.string.playback_failed, Toast.LENGTH_SHORT).show();
+                }
+            } else {
+                mOpenFailedCounter = 0;
+            }
+        }
+    }
+
+    /**
+     * Starts playback of a previously opened file.
+     */
+    public void play() {
+        if (mPlayer.isInitialized()) {
+            mPlayer.start();
+            setForeground(true);
+            mWasPlaying = true;
+
+            NotificationManager nm = (NotificationManager)
+            getSystemService(Context.NOTIFICATION_SERVICE);
+    
+            RemoteViews views = new RemoteViews(getPackageName(), R.layout.statusbar);
+            views.setImageViewResource(R.id.icon, R.drawable.stat_notify_musicplayer);
+            views.setTextViewText(R.id.trackname, getTrackName());
+            String artist = getArtistName();
+            if (artist == null || artist.equals(MediaFile.UNKNOWN_STRING)) {
+                artist = getString(R.string.unknown_artist_name);
+            }
+            String album = getAlbumName();
+            if (album == null || album.equals(MediaFile.UNKNOWN_STRING)) {
+                album = getString(R.string.unknown_album_name);
+            }
+            
+            views.setTextViewText(R.id.artistalbum,
+                    getString(R.string.notification_artist_album, artist, album)
+                    );
+            
+            Intent statusintent = new Intent("com.android.music.PLAYBACK_VIEWER");
+            statusintent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+            Notification status = new Notification();
+            status.contentView = views;
+            status.flags |= Notification.FLAG_ONGOING_EVENT;
+            status.icon = R.drawable.stat_notify_musicplayer;
+            status.contentIntent = PendingIntent.getActivity(this, 0,
+                    new Intent("com.android.music.PLAYBACK_VIEWER"), 0);
+            nm.notify(PLAYBACKSERVICE_STATUS, status);
+            notifyChange(PLAYSTATE_CHANGED);
+        }
+    }
+
+    private void stop(boolean remove_status_icon) {
+        if (mPlayer.isInitialized()) {
+            mPlayer.stop();
+        }
+        mFileToPlay = null;
+        if (mCursor != null) {
+            mCursor.close();
+            mCursor = null;
+        }
+        if (remove_status_icon) {
+            gotoIdleState();
+        }
+        setForeground(false);
+        mWasPlaying = false;
+    }
+
+    /**
+     * Stops playback.
+     */
+    public void stop() {
+        stop(true);
+    }
+
+    /**
+     * Pauses playback (call play() to resume)
+     */
+    public void pause() {
+        if (isPlaying()) {
+            mPlayer.pause();
+            gotoIdleState();
+            setForeground(false);
+            mWasPlaying = false;
+            notifyChange(PLAYSTATE_CHANGED);
+        }
+    }
+
+    /** Returns whether playback is currently paused
+     *
+     * @return true if playback is paused, false if not
+     */
+    public boolean isPlaying() {
+        if (mPlayer.isInitialized()) {
+            return mPlayer.isPlaying();
+        }
+        return false;
+    }
+
+    /*
+      Desired behavior for prev/next/shuffle:
+
+      - NEXT will move to the next track in the list when not shuffling, and to
+        a track randomly picked from the not-yet-played tracks when shuffling.
+        If all tracks have already been played, pick from the full set, but
+        avoid picking the previously played track if possible.
+      - when shuffling, PREV will go to the previously played track. Hitting PREV
+        again will go to the track played before that, etc. When the start of the
+        history has been reached, PREV is a no-op.
+        When not shuffling, PREV will go to the sequentially previous track (the
+        difference with the shuffle-case is mainly that when not shuffling, the
+        user can back up to tracks that are not in the history).
+
+        Example:
+        When playing an album with 10 tracks from the start, and enabling shuffle
+        while playing track 5, the remaining tracks (6-10) will be shuffled, e.g.
+        the final play order might be 1-2-3-4-5-8-10-6-9-7.
+        When hitting 'prev' 8 times while playing track 7 in this example, the
+        user will go to tracks 9-6-10-8-5-4-3-2. If the user then hits 'next',
+        a random track will be picked again. If at any time user disables shuffling
+        the next/previous track will be picked in sequential order again.
+     */
+
+    public void prev() {
+        synchronized (this) {
+            if (mOneShot) {
+                // we were playing a specific file not part of a playlist, so there is no 'previous'
+                seek(0);
+                play();
+                return;
+            }
+            if (mShuffleMode == SHUFFLE_NORMAL) {
+                // go to previously-played track and remove it from the history
+                int histsize = mHistory.size();
+                if (histsize == 0) {
+                    // prev is a no-op
+                    return;
+                }
+                Integer pos = mHistory.remove(histsize - 1);
+                mPlayPos = pos.intValue();
+            } else {
+                if (mPlayPos > 0) {
+                    mPlayPos--;
+                } else {
+                    mPlayPos = mPlayListLen - 1;
+                }
+            }
+            stop(false);
+            openCurrent();
+            play();
+            notifyChange(META_CHANGED);
+        }
+    }
+
+    public void next(boolean force) {
+        synchronized (this) {
+            if (mOneShot) {
+                // we were playing a specific file not part of a playlist, so there is no 'next'
+                seek(0);
+                play();
+                return;
+            }
+
+            // Store the current file in the history, but keep the history at a
+            // reasonable size
+            mHistory.add(Integer.valueOf(mPlayPos));
+            if (mHistory.size() > MAX_HISTORY_SIZE) {
+                mHistory.removeElementAt(0);
+            }
+
+            if (mShuffleMode == SHUFFLE_NORMAL) {
+                // Pick random next track from the not-yet-played ones
+                // TODO: make it work right after adding/removing items in the queue.
+
+                int numTracks = mPlayListLen;
+                int[] tracks = new int[numTracks];
+                for (int i=0;i < numTracks; i++) {
+                    tracks[i] = i;
+                }
+
+                int numHistory = mHistory.size();
+                int numUnplayed = numTracks;
+                for (int i=0;i < numHistory; i++) {
+                    int idx = mHistory.get(i).intValue();
+                    if (idx < numTracks && tracks[idx] >= 0) {
+                        numUnplayed--;
+                        tracks[idx] = -1;
+                    }
+                }
+
+                // 'numUnplayed' now indicates how many tracks have not yet
+                // been played, and 'tracks' contains the indices of those
+                // tracks.
+                if (numUnplayed <=0) {
+                    // everything's already been played
+                    if (mRepeatMode == REPEAT_ALL || force) {
+                        //pick from full set
+                        numUnplayed = numTracks;
+                        for (int i=0;i < numTracks; i++) {
+                            tracks[i] = i;
+                        }
+                    } else {
+                        // all done
+                        gotoIdleState();
+                        return;
+                    }
+                }
+                int skip = mRand.nextInt(numUnplayed);
+                int cnt = -1;
+                while (true) {
+                    while (tracks[++cnt] < 0)
+                        ;
+                    skip--;
+                    if (skip < 0) {
+                        break;
+                    }
+                }
+                mPlayPos = cnt;
+            } else if (mShuffleMode == SHUFFLE_AUTO) {
+                doAutoShuffleUpdate();
+                mPlayPos++;
+            } else {
+                if (mPlayPos >= mPlayListLen - 1) {
+                    // we're at the end of the list
+                    if (mRepeatMode == REPEAT_NONE && !force) {
+                        // all done
+                        gotoIdleState();
+                        notifyChange(PLAYBACK_COMPLETE);
+                        return;
+                    } else if (mRepeatMode == REPEAT_ALL || force) {
+                        mPlayPos = 0;
+                    }
+                } else {
+                    mPlayPos++;
+                }
+            }
+            stop(false);
+            openCurrent();
+            play();
+            notifyChange(META_CHANGED);
+        }
+    }
+    
+    private void gotoIdleState() {
+        NotificationManager nm =
+            (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+        nm.cancel(PLAYBACKSERVICE_STATUS);
+        mDelayedStopHandler.removeCallbacksAndMessages(null);
+        Message msg = mDelayedStopHandler.obtainMessage();
+        mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
+    }
+    
+    // Make sure there are at least 5 items after the currently playing item
+    // and no more than 10 items before.
+    private void doAutoShuffleUpdate() {
+        // remove old entries
+        if (mPlayPos > 10) {
+            removeTracks(0, mPlayPos - 9);
+        }
+        // add new entries if needed
+        int to_add = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos));
+        if (to_add > 0) {
+            for (int i = 0; i < to_add; i++) {
+                // pick something at random from the list
+                int idx = mRand.nextInt(mAutoShuffleList.length);
+                Integer which = mAutoShuffleList[idx];
+                addToPlayList(which);
+            }
+            notifyChange(QUEUE_CHANGED);
+        }
+    }
+
+    // A simple variation of Random that makes sure that the
+    // value it returns is not equal to the value it returned
+    // previously, unless the interval is 1.
+    private class Shuffler {
+        private int mPrevious;
+        private Random mRandom = new Random();
+        public int nextInt(int interval) {
+            int ret;
+            do {
+                ret = mRandom.nextInt(interval);
+            } while (ret == mPrevious && interval > 1);
+            mPrevious = ret;
+            return ret;
+        }
+    };
+
+    private boolean makeAutoShuffleList() {
+        ContentResolver res = getContentResolver();
+        Cursor c = null;
+        try {
+            c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                    new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
+                    null, null);
+            if (c == null || c.getCount() == 0) {
+                return false;
+            }
+            int len = c.getCount();
+            int[] list = new int[len];
+            for (int i = 0; i < len; i++) {
+                c.moveToNext();
+                list[i] = c.getInt(0);
+            }
+            mAutoShuffleList = list;
+            return true;
+        } catch (RuntimeException ex) {
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Removes the range of tracks specified from the play list. If a file within the range is
+     * the file currently being played, playback will move to the next file after the
+     * range. 
+     * @param first The first file to be removed
+     * @param last The last file to be removed
+     * @return the number of tracks deleted
+     */
+    public int removeTracks(int first, int last) {
+        synchronized (this) {
+            if (last < first) return 0;
+            if (first < 0) first = 0;
+            if (last >= mPlayListLen) last = mPlayListLen - 1;
+
+            boolean gotonext = false;
+            if (first <= mPlayPos && mPlayPos <= last) {
+                mPlayPos = first;
+                gotonext = true;
+            } else if (mPlayPos > last) {
+                mPlayPos -= (last - first + 1);
+            }
+            int num = mPlayListLen - last - 1;
+            for (int i = 0; i < num; i++) {
+                mPlayList[first + i] = mPlayList[last + 1 + i];
+            }
+            mPlayListLen -= last - first + 1;
+            
+            if (gotonext) {
+                if (mPlayListLen == 0) {
+                    stop(true);
+                    mPlayPos = -1;
+                } else {
+                    if (mPlayPos >= mPlayListLen) {
+                        mPlayPos = 0;
+                    }
+                    stop(false);
+                    openCurrent();
+                    play();
+                }
+            }
+            notifyChange(QUEUE_CHANGED);
+            return last - first + 1;
+        }
+    }
+    
+    /**
+     * Removes all instances of the track with the given id
+     * from the playlist.
+     * @param id The id to be removed
+     * @return how many instances of the track were removed
+     */
+    public int removeTrack(int id) {
+        int numremoved = 0;
+        synchronized (this) {
+            for (int i = 0; i < mPlayListLen; i++) {
+                if (mPlayList[i] == id) {
+                    numremoved += removeTracks(i, i);
+                    i--;
+                }
+            }
+        }
+        return numremoved;
+    }
+    
+    public void setShuffleMode(int shufflemode) {
+        synchronized(this) {
+            if (mShuffleMode == shufflemode) {
+                return;
+            }
+            mShuffleMode = shufflemode;
+            if (mShuffleMode == SHUFFLE_AUTO) {
+                if (makeAutoShuffleList()) {
+                    mPlayListLen = 0;
+                    doAutoShuffleUpdate();
+                    mPlayPos = 0;
+                    openCurrent();
+                    play();
+                    notifyChange(META_CHANGED);
+                } else {
+                    // failed to build a list of files to shuffle
+                    mShuffleMode = SHUFFLE_NONE;
+                }
+            }
+        }
+    }
+    public int getShuffleMode() {
+        return mShuffleMode;
+    }
+    
+    public void setRepeatMode(int repeatmode) {
+        synchronized(this) {
+            mRepeatMode = repeatmode;
+        }
+    }
+    public int getRepeatMode() {
+        return mRepeatMode;
+    }
+
+    public int getMediaMountedCount() {
+        return mMediaMountedCount;
+    }
+
+    /**
+     * Returns the path of the currently playing file, or null if
+     * no file is currently playing.
+     */
+    public String getPath() {
+        return mFileToPlay;
+    }
+    
+    /**
+     * Returns the rowid of the currently playing file, or -1 if
+     * no file is currently playing.
+     */
+    public int getAudioId() {
+        synchronized (this) {
+            if (mPlayPos >= 0 && mPlayer.isInitialized()) {
+                return mPlayList[mPlayPos];
+            }
+        }
+        return -1;
+    }
+    
+    /**
+     * Returns the position in the queue 
+     * @return the position in the queue
+     */
+    public int getQueuePosition() {
+        synchronized(this) {
+            return mPlayPos;
+        }
+    }
+    
+    /**
+     * Starts playing the track at the given position in the queue.
+     * @param pos The position in the queue of the track that will be played.
+     */
+    public void setQueuePosition(int pos) {
+        synchronized(this) {
+            stop(false);
+            mPlayPos = pos;
+            openCurrent();
+            play();
+            notifyChange(META_CHANGED);
+        }
+    }
+
+    public String getArtistName() {
+        if (mCursor == null) {
+            return null;
+        }
+        return mCursor.getString(mCursor.getColumnIndex(MediaStore.Audio.Media.ARTIST));
+    }
+    
+    public int getArtistId() {
+        if (mCursor == null) {
+            return -1;
+        }
+        return mCursor.getInt(mCursor.getColumnIndex(MediaStore.Audio.Media.ARTIST_ID));
+    }
+
+    public String getAlbumName() {
+        if (mCursor == null) {
+            return null;
+        }
+        return mCursor.getString(mCursor.getColumnIndex(MediaStore.Audio.Media.ALBUM));
+    }
+
+    public int getAlbumId() {
+        if (mCursor == null) {
+            return -1;
+        }
+        return mCursor.getInt(mCursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID));
+    }
+
+    public String getTrackName() {
+        if (mCursor == null) {
+            return null;
+        }
+        return mCursor.getString(mCursor.getColumnIndex(MediaStore.Audio.Media.TITLE));
+    }
+
+    
+    
+    /**
+     * Returns the duration of the file in milliseconds.
+     * Currently this method returns -1 for the duration of MIDI files.
+     */
+    public long duration() {
+        if (mPlayer.isInitialized()) {
+            return mPlayer.duration();
+        }
+        // TODO: when the MIDI engine supports it, return MIDI duration.
+        return -1;
+    }
+
+    /**
+     * Returns the current playback position in milliseconds
+     */
+    public long position() {
+        if (mPlayer.isInitialized()) {
+            return mPlayer.position();
+        }
+        return -1;
+    }
+
+    /**
+     * Seeks to the position specified.
+     *
+     * @param pos The position to seek to, in milliseconds
+     */
+    public long seek(long pos) {
+        if (mPlayer.isInitialized()) {
+            if (pos < 0) pos = 0;
+            if (pos > mPlayer.duration()) pos = mPlayer.duration();
+            return mPlayer.seek(pos);
+        }
+        return -1;
+    }
+
+    /**
+     * Provides a unified interface for dealing with midi files and
+     * other media files.
+     */
+    private class MultiPlayer {
+        private MediaPlayer mMediaPlayer = new MediaPlayer();
+        private Handler mHandler;
+        private boolean mIsInitialized = false;
+
+        public MultiPlayer() {
+            mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);
+        }
+
+        public void setDataSourceAsync(String path) {
+            try {
+                mMediaPlayer.reset();
+                mMediaPlayer.setDataSource(path);
+                mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+                mMediaPlayer.setOnPreparedListener(preparedlistener);
+                mMediaPlayer.prepareAsync();
+            } catch (IOException ex) {
+                // TODO: notify the user why the file couldn't be opened
+                mIsInitialized = false;
+                return;
+            } catch (IllegalArgumentException ex) {
+                // TODO: notify the user why the file couldn't be opened
+                mIsInitialized = false;
+                return;
+            }
+            mMediaPlayer.setOnCompletionListener(listener);
+            mMediaPlayer.setOnErrorListener(errorListener);
+            
+            mIsInitialized = true;
+        }
+        
+        public void setDataSource(String path) {
+            try {
+                mMediaPlayer.reset();
+                mMediaPlayer.setOnPreparedListener(null);
+                if (path.startsWith("content://")) {
+                    mMediaPlayer.setDataSource(MediaPlaybackService.this, Uri.parse(path));
+                } else {
+                    mMediaPlayer.setDataSource(path);
+                }
+                mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+                mMediaPlayer.prepare();
+            } catch (IOException ex) {
+                // TODO: notify the user why the file couldn't be opened
+                mIsInitialized = false;
+                return;
+            } catch (IllegalArgumentException ex) {
+                // TODO: notify the user why the file couldn't be opened
+                mIsInitialized = false;
+                return;
+            }
+            mMediaPlayer.setOnCompletionListener(listener);
+            mMediaPlayer.setOnErrorListener(errorListener);
+            
+            mIsInitialized = true;
+        }
+        
+        public boolean isInitialized() {
+            return mIsInitialized;
+        }
+
+        public void start() {
+            mMediaPlayer.start();
+        }
+
+        public void stop() {
+            mMediaPlayer.reset();
+            mIsInitialized = false;
+        }
+
+        public void pause() {
+            mMediaPlayer.pause();
+        }
+        
+        public boolean isPlaying() {
+            return mMediaPlayer.isPlaying();
+        }
+
+        public void setHandler(Handler handler) {
+            mHandler = handler;
+        }
+
+        MediaPlayer.OnCompletionListener listener = new MediaPlayer.OnCompletionListener() {
+            public void onCompletion(MediaPlayer mp) {
+                // Acquire a temporary wakelock, since when we return from
+                // this callback the MediaPlayer will release its wakelock
+                // and allow the device to go to sleep.
+                // This temporary wakelock is released when the RELEASE_WAKELOCK
+                // message is processed, but just in case, put a timeout on it.
+                mWakeLock.acquire(30000);
+                mHandler.sendEmptyMessage(TRACK_ENDED);
+                mHandler.sendEmptyMessage(RELEASE_WAKELOCK);
+            }
+        };
+
+        MediaPlayer.OnPreparedListener preparedlistener = new MediaPlayer.OnPreparedListener() {
+            public void onPrepared(MediaPlayer mp) {
+                notifyChange(ASYNC_OPEN_COMPLETE);
+            }
+        };
+        MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() {
+            public boolean onError(MediaPlayer mp, int what, int extra) {
+                switch (what) {
+                case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
+                    mIsInitialized = false;
+                    mMediaPlayer.release();
+                    // Creating a new MediaPlayer and settings its wakemode does not
+                    // require the media service, so it's OK to do this now, while the
+                    // service is still being restarted
+                    mMediaPlayer = new MediaPlayer(); 
+                    mMediaPlayer.setWakeMode(MediaPlaybackService.this, PowerManager.PARTIAL_WAKE_LOCK);
+                    mHandler.sendMessageDelayed(mHandler.obtainMessage(SERVER_DIED), 2000);
+                    return true;
+                default:
+                    break;
+                }
+                return false;
+           }
+        };
+
+        public long duration() {
+            return mMediaPlayer.getDuration();
+        }
+
+        public long position() {
+            return mMediaPlayer.getCurrentPosition();
+        }
+
+        public long seek(long whereto) {
+            mMediaPlayer.seekTo((int) whereto);
+            return whereto;
+        }
+    }
+
+    private final IMediaPlaybackService.Stub mBinder = new IMediaPlaybackService.Stub()
+    {
+        public void openfileAsync(String path)
+        {
+            MediaPlaybackService.this.openAsync(path);
+        }
+        public void openfile(String path)
+        {
+            MediaPlaybackService.this.open(path, true);
+        }
+        public void open(int [] list, int position) {
+            MediaPlaybackService.this.open(list, position);
+        }
+        public int getQueuePosition() {
+            return MediaPlaybackService.this.getQueuePosition();
+        }
+        public void setQueuePosition(int index) {
+            MediaPlaybackService.this.setQueuePosition(index);
+        }
+        public boolean isPlaying() {
+            return MediaPlaybackService.this.isPlaying();
+        }
+        public void stop() {
+            MediaPlaybackService.this.stop();
+        }
+        public void pause() {
+            MediaPlaybackService.this.pause();
+        }
+        public void play() {
+            MediaPlaybackService.this.play();
+        }
+        public void prev() {
+            MediaPlaybackService.this.prev();
+        }
+        public void next() {
+            MediaPlaybackService.this.next(true);
+        }
+        public String getTrackName() {
+            return MediaPlaybackService.this.getTrackName();
+        }
+        public String getAlbumName() {
+            return MediaPlaybackService.this.getAlbumName();
+        }
+        public int getAlbumId() {
+            return MediaPlaybackService.this.getAlbumId();
+        }
+        public String getArtistName() {
+            return MediaPlaybackService.this.getArtistName();
+        }
+        public int getArtistId() {
+            return MediaPlaybackService.this.getArtistId();
+        }
+        public void enqueue(int [] list , int action) {
+            MediaPlaybackService.this.enqueue(list, action);
+        }
+        public int [] getQueue() {
+            return MediaPlaybackService.this.getQueue();
+        }
+        public void moveQueueItem(int from, int to) {
+            MediaPlaybackService.this.moveQueueItem(from, to);
+        }
+        public String getPath() {
+            return MediaPlaybackService.this.getPath();
+        }
+        public int getAudioId() {
+            return MediaPlaybackService.this.getAudioId();
+        }
+        public long position() {
+            return MediaPlaybackService.this.position();
+        }
+        public long duration() {
+            return MediaPlaybackService.this.duration();
+        }
+        public long seek(long pos) {
+            return MediaPlaybackService.this.seek(pos);
+        }
+        public void setShuffleMode(int shufflemode) {
+            MediaPlaybackService.this.setShuffleMode(shufflemode);
+        }
+        public int getShuffleMode() {
+            return MediaPlaybackService.this.getShuffleMode();
+        }
+        public int removeTracks(int first, int last) {
+            return MediaPlaybackService.this.removeTracks(first, last);
+        }
+        public int removeTrack(int id) {
+            return MediaPlaybackService.this.removeTrack(id);
+        }
+        public void setRepeatMode(int repeatmode) {
+            MediaPlaybackService.this.setRepeatMode(repeatmode);
+        }
+        public int getRepeatMode() {
+            return MediaPlaybackService.this.getRepeatMode();
+        }
+        public int getMediaMountedCount() {
+            return MediaPlaybackService.this.getMediaMountedCount();
+        }
+    };
+}
+
diff --git a/src/com/android/music/MovieView.java b/src/com/android/music/MovieView.java
new file mode 100644 (file)
index 0000000..55b1971
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * 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.android.music;
+
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Config;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.MediaController;
+import android.widget.VideoView;
+
+public class MovieView extends Activity implements MediaPlayer.OnErrorListener
+{
+    private static final String TAG = "MovieView";
+
+    private VideoView   mVideoView;
+    private View        mProgressView;
+    public MovieView()
+    {
+    }
+
+    @Override
+    public void onCreate(Bundle icicle)
+    {
+        super.onCreate(icicle);
+        
+        setContentView(R.layout.movie_view);
+
+        mVideoView = (VideoView) findViewById(R.id.surface_view);
+        mProgressView = findViewById(R.id.progress_indicator);
+        Uri uri = getIntent().getData();
+        
+        // For streams that we expect to be slow to start up, show a
+        // progress spinner until playback starts.
+        String scheme = uri.getScheme();
+        if ("http".equalsIgnoreCase(scheme) ||
+                "rtsp".equalsIgnoreCase(scheme)) {
+            mHandler.postDelayed(mPlayingChecker, 250);
+        } else {
+            mProgressView.setVisibility(View.GONE);
+        }
+        
+        mVideoView.setOnErrorListener(this);
+        mVideoView.setVideoURI(uri);
+        mVideoView.setMediaController(new MediaController(this));
+        mVideoView.requestFocus(); // make the video view handle keys for seeking and pausing
+
+        Intent i = new Intent(MediaPlaybackService.SERVICECMD);
+        i.putExtra(MediaPlaybackService.CMDNAME, MediaPlaybackService.CMDPAUSE);
+        sendBroadcast(i);
+        
+        mVideoView.start();
+    }
+    
+    @Override
+    public void onPause() {
+        mHandler.removeCallbacksAndMessages(null);
+        super.onPause();
+    }
+    
+    Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+        }
+    };
+
+    Runnable mPlayingChecker = new Runnable() {
+        public void run() {
+            if (mVideoView.isPlaying()) {
+                mProgressView.setVisibility(View.GONE);
+            } else {
+                mHandler.postDelayed(mPlayingChecker, 250);
+            }
+        }
+    };
+    
+    public boolean onError(MediaPlayer player, int arg1, int arg2) {
+        mHandler.removeCallbacksAndMessages(null);
+        mProgressView.setVisibility(View.GONE);
+        return false;
+    }
+}
diff --git a/src/com/android/music/MusicBrowserActivity.java b/src/com/android/music/MusicBrowserActivity.java
new file mode 100644 (file)
index 0000000..c742f57
--- /dev/null
@@ -0,0 +1,283 @@
+/*
+ * 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.android.music;
+
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.media.MediaFile;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.IBinder;
+import android.provider.MediaStore;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.widget.TextView;
+
+public class MusicBrowserActivity extends Activity
+    implements MusicUtils.Defs, View.OnClickListener {
+    private View mNowPlayingView;
+    private TextView mTitle;
+    private TextView mArtist;
+    private boolean mAutoShuffle = false;
+    private static final int SEARCH_MUSIC = CHILD_MENU_BASE;
+
+    public MusicBrowserActivity() {
+    }
+
+    /**
+     * Called when the activity is first created.
+     */
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+        String shuf = getIntent().getStringExtra("autoshuffle");
+        if ("true".equals(shuf)) {
+            mAutoShuffle = true;
+        }
+        MusicUtils.bindToService(this, new ServiceConnection() {
+            public void onServiceConnected(ComponentName classname, IBinder obj) {
+                updateMenu();
+            }
+
+            public void onServiceDisconnected(ComponentName classname) {
+                updateMenu();
+            }
+        
+        });
+        setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+        init();
+    }
+
+    @Override
+    public void onDestroy() {
+        MusicUtils.unbindFromService(this);
+        super.onDestroy();
+    }
+
+    public void init() {
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.music_library);
+        mNowPlayingView = findViewById(R.id.nowplaying);
+        mTitle = (TextView) mNowPlayingView.findViewById(R.id.title);
+        mArtist = (TextView) mNowPlayingView.findViewById(R.id.artist);
+        
+        findViewById(R.id.browse_button).setOnClickListener(this);
+        findViewById(R.id.albums_button).setOnClickListener(this);
+        findViewById(R.id.tracks_button).setOnClickListener(this);
+        findViewById(R.id.playlists_button).setOnClickListener(this);
+    }
+
+    private void updateMenu() {
+        try {
+            if (MusicUtils.sService != null && MusicUtils.sService.getAudioId() != -1) {
+                makeNowPlayingView();
+                mNowPlayingView.setVisibility(View.VISIBLE);
+                return;
+            }
+        } catch (RemoteException ex) {
+        }
+        mNowPlayingView.setVisibility(View.INVISIBLE);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        IntentFilter f = new IntentFilter();
+        f.addAction(MediaPlaybackService.META_CHANGED);
+        registerReceiver(mStatusListener, new IntentFilter(f));
+        updateMenu();
+        if (mAutoShuffle) {
+            mAutoShuffle = false;
+            doAutoShuffle();
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        unregisterReceiver(mStatusListener);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+         menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu()
+         menu.add(0, SEARCH_MUSIC, 0, R.string.search_title).setIcon(R.drawable.ic_menu_search);
+        return true;
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        MenuItem item = menu.findItem(PARTY_SHUFFLE);
+        if (item != null) {
+            int shuffle = MusicUtils.getCurrentShuffleMode();
+            if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
+                item.setIcon(R.drawable.ic_menu_party_shuffle);
+                item.setTitle(R.string.party_shuffle_off);
+            } else {
+                item.setIcon(R.drawable.ic_menu_party_shuffle);
+                item.setTitle(R.string.party_shuffle);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        Intent intent;
+        try {
+            switch (item.getItemId()) {
+                case PARTY_SHUFFLE:
+                    int shuffle = MusicUtils.sService.getShuffleMode();
+                    if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) {
+                        MusicUtils.sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE);
+                    } else {
+                        MusicUtils.sService.setShuffleMode(MediaPlaybackService.SHUFFLE_AUTO);
+                    }
+                    break;
+                    
+                case SEARCH_MUSIC: {
+                    intent = new Intent(Intent.ACTION_PICK);
+                    intent.setClass(this, QueryBrowserActivity.class);
+                    intent.putExtra(SearchManager.QUERY, "");
+                    startActivity(intent);
+                    return true;
+                }
+            }
+        } catch (RemoteException ex) {
+        }
+        return super.onOptionsItemSelected(item);
+    }
+    
+    
+    // TODO: Activities are requested to call onSearchRequested, and to override
+    // that function in order to insert custom fields (e.g. the search bundle).  startSearch
+    // was not intended to be overridden.
+    @Override
+    public void startSearch(String initialQuery, boolean selectQuery, Bundle appSearchData,
+            boolean globalSearch) {
+        Intent intent = new Intent(Intent.ACTION_PICK);
+        intent.setClass(this, QueryBrowserActivity.class);
+        intent.putExtra(SearchManager.QUERY, initialQuery);
+        startActivity(intent);
+    }
+
+    public void onClick(View v) {
+        Intent intent;
+        switch (v.getId()) {
+            case R.id.browse_button:
+                intent = new Intent(Intent.ACTION_PICK);
+                intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/artistalbum");
+                startActivity(intent);
+                break;
+            case R.id.albums_button:
+                intent = new Intent(Intent.ACTION_PICK);
+                intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/album");
+                startActivity(intent);
+                break;
+            case R.id.tracks_button:
+                intent = new Intent(Intent.ACTION_PICK);
+                intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
+                startActivity(intent);
+                break;
+            case R.id.playlists_button:
+                intent = new Intent(Intent.ACTION_PICK);
+                intent.setDataAndType(Uri.EMPTY, MediaStore.Audio.Playlists.CONTENT_TYPE);
+                startActivity(intent);
+                break;
+            case R.id.nowplaying:
+                intent = new Intent("com.android.music.PLAYBACK_VIEWER");
+                startActivity(intent);
+                break;
+        }
+    }
+
+    private void doAutoShuffle() {
+        bindService((new Intent()).setClass(this, MediaPlaybackService.class), autoshuffle, 0);
+    }
+
+    private ServiceConnection autoshuffle = new ServiceConnection() {
+        public void onServiceConnected(ComponentName classname, IBinder obj) {
+            // we need to be able to bind again, so unbind
+            unbindService(this);
+            IMediaPlaybackService serv = IMediaPlaybackService.Stub.asInterface(obj);
+            if (serv != null) {
+                try {
+                    serv.setShuffleMode(MediaPlaybackService.SHUFFLE_AUTO);
+                    updateMenu();
+                } catch (RemoteException ex) {
+                }
+            }
+        }
+
+        public void onServiceDisconnected(ComponentName classname) {
+        }
+    };
+
+    private void makeNowPlayingView() {
+        try {
+            mTitle.setText(MusicUtils.sService.getTrackName());
+            String artistName = MusicUtils.sService.getArtistName();
+            if (MediaFile.UNKNOWN_STRING.equals(artistName)) {
+                artistName = getString(R.string.unknown_artist_name);
+            }
+            mArtist.setText(artistName);
+            mNowPlayingView.setOnFocusChangeListener(mFocuser);
+            mNowPlayingView.setOnClickListener(this);
+        } catch (RemoteException ex) {
+
+        }
+    }
+
+    View.OnFocusChangeListener mFocuser = new View.OnFocusChangeListener() {
+        Drawable mBack;
+
+        public void onFocusChange(View v, boolean hasFocus) {
+            if (hasFocus) {
+                if (mBack == null) {
+                    mBack = mNowPlayingView.getBackground();
+                }
+                Drawable dr = getResources().getDrawable(android.R.drawable.menuitem_background);
+                dr.setState(new int[] { android.R.attr.state_focused});
+                mNowPlayingView.setBackgroundDrawable(dr);
+            } else {
+                mNowPlayingView.setBackgroundDrawable(mBack);
+            }
+        }
+    };
+
+    private BroadcastReceiver mStatusListener = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            // this receiver is only used for META_CHANGED events
+            updateMenu();
+        }
+    };
+}
+
diff --git a/src/com/android/music/MusicUtils.java b/src/com/android/music/MusicUtils.java
new file mode 100644 (file)
index 0000000..a342c84
--- /dev/null
@@ -0,0 +1,1126 @@
+/*
+ * 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.android.music;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.Locale;
+
+import android.app.Activity;
+import android.app.ExpandableListActivity;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.MediaFile;
+import android.media.MediaScanner;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.provider.MediaStore;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.SubMenu;
+import android.view.Window;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class MusicUtils {
+
+    private static final String TAG = "MusicUtils";
+
+    public interface Defs {
+        public final static int OPEN_URL = 0;
+        public final static int ADD_TO_PLAYLIST = 1;
+        public final static int USE_AS_RINGTONE = 2;
+        public final static int PLAYLIST_SELECTED = 3;
+        public final static int NEW_PLAYLIST = 4;
+        public final static int PLAY_SELECTION = 5;
+        public final static int GOTO_START = 6;
+        public final static int GOTO_PLAYBACK = 7;
+        public final static int PARTY_SHUFFLE = 8;
+        public final static int SHUFFLE_ALL = 9;
+        public final static int DELETE_ITEM = 10;
+        public final static int SCAN_DONE = 11;
+        public final static int CHILD_MENU_BASE = 12;
+        public final static int QUEUE = 13;
+    }
+    
+    public static String makeAlbumsSongsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) {
+        // There are several formats for the albums/songs information:
+        // "1 Song"   - used if there is only 1 song
+        // "N Songs" - used for the "unknown artist" item
+        // "1 Album"/"N Songs" 
+        // "N Album"/"M Songs"
+        // Depending on locale, these may need to be further subdivided
+        
+        StringBuilder songs_albums = new StringBuilder();
+
+        if (numsongs == 1) {
+            songs_albums.append(context.getString(R.string.onesong));
+        } else {
+            Resources r = context.getResources();
+            if (! isUnknown) {
+                String f = r.getQuantityText(R.plurals.Nalbums, numalbums).toString();
+                sFormatBuilder.setLength(0);
+                sFormatter.format(f, Integer.valueOf(numalbums));
+                songs_albums.append(sFormatBuilder);
+                songs_albums.append(context.getString(R.string.albumsongseparator));
+            }
+            String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
+            sFormatBuilder.setLength(0);
+            sFormatter.format(f, Integer.valueOf(numsongs));
+            songs_albums.append(sFormatBuilder);
+        }
+        return songs_albums.toString();
+    }
+    
+    public static IMediaPlaybackService sService = null;
+    private static HashMap<Context, ServiceBinder> sConnectionMap = new HashMap<Context, ServiceBinder>();
+
+    public static boolean bindToService(Context context) {
+        return bindToService(context, null);
+    }
+
+    public static boolean bindToService(Context context, ServiceConnection callback) {
+        context.startService(new Intent(context, MediaPlaybackService.class));
+        ServiceBinder sb = new ServiceBinder(callback);
+        sConnectionMap.put(context, sb);
+        return context.bindService((new Intent()).setClass(context,
+                MediaPlaybackService.class), sb, 0);
+    }
+    
+    public static void unbindFromService(Context context) {
+        ServiceBinder sb = (ServiceBinder) sConnectionMap.remove(context);
+        if (sb == null) {
+            Log.e("MusicUtils", "Trying to unbind for unknown Context");
+            return;
+        }
+        context.unbindService(sb);
+    }
+
+    private static class ServiceBinder implements ServiceConnection {
+        ServiceConnection mCallback;
+        ServiceBinder(ServiceConnection callback) {
+            mCallback = callback;
+        }
+        
+        public void onServiceConnected(ComponentName className, android.os.IBinder service) {
+            sService = IMediaPlaybackService.Stub.asInterface(service);
+            initAlbumArtCache();
+            if (mCallback != null) {
+                mCallback.onServiceConnected(className, service);
+            }
+        }
+        
+        public void onServiceDisconnected(ComponentName className) {
+            if (mCallback != null) {
+                mCallback.onServiceDisconnected(className);
+            }
+            sService = null;
+        }
+    }
+    
+    public static int getCurrentAlbumId() {
+        if (sService != null) {
+            try {
+                return sService.getAlbumId();
+            } catch (RemoteException ex) {
+            }
+        }
+        return -1;
+    }
+
+    public static int getCurrentArtistId() {
+        if (MusicUtils.sService != null) {
+            try {
+                return sService.getArtistId();
+            } catch (RemoteException ex) {
+            }
+        }
+        return -1;
+    }
+
+    public static int getCurrentAudioId() {
+        if (MusicUtils.sService != null) {
+            try {
+                return sService.getAudioId();
+            } catch (RemoteException ex) {
+            }
+        }
+        return -1;
+    }
+    
+    public static int getCurrentShuffleMode() {
+        int mode = MediaPlaybackService.SHUFFLE_NONE;
+        if (sService != null) {
+            try {
+                mode = sService.getShuffleMode();
+            } catch (RemoteException ex) {
+            }
+        }
+        return mode;
+    }
+    
+    /*
+     * Returns true if a file is currently opened for playback (regardless
+     * of whether it's playing or paused).
+     */
+    public static boolean isMusicLoaded() {
+        if (MusicUtils.sService != null) {
+            try {
+                return sService.getPath() != null;
+            } catch (RemoteException ex) {
+            }
+        }
+        return false;
+    }
+
+    private final static int [] sEmptyList = new int[0];
+    
+    public static int [] getSongListForCursor(Cursor cursor) {
+        if (cursor == null) {
+            return sEmptyList;
+        }
+        int len = cursor.getCount();
+        int [] list = new int[len];
+        cursor.moveToFirst();
+        int colidx = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID);
+        if (colidx < 0) {
+            colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
+        }
+        for (int i = 0; i < len; i++) {
+            list[i] = cursor.getInt(colidx);
+            cursor.moveToNext();
+        }
+        return list;
+    }
+
+    public static int [] getSongListForArtist(Context context, int id) {
+        final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
+        String where = MediaStore.Audio.Media.ARTIST_ID + "=" + id + " AND " + 
+        MediaStore.Audio.Media.IS_MUSIC + "=1";
+        Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                ccols, where, null,
+                MediaStore.Audio.Media.ALBUM_KEY + ","  + MediaStore.Audio.Media.TRACK);
+        
+        if (cursor != null) {
+            int [] list = getSongListForCursor(cursor);
+            cursor.close();
+            return list;
+        }
+        return sEmptyList;
+    }
+
+    public static int [] getSongListForAlbum(Context context, int id) {
+        final String[] ccols = new String[] { MediaStore.Audio.Media._ID };
+        String where = MediaStore.Audio.Media.ALBUM_ID + "=" + id + " AND " + 
+                MediaStore.Audio.Media.IS_MUSIC + "=1";
+        Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                ccols, where, null, MediaStore.Audio.Media.TRACK);
+
+        if (cursor != null) {
+            int [] list = getSongListForCursor(cursor);
+            cursor.close();
+            return list;
+        }
+        return sEmptyList;
+    }
+
+    public static int [] getSongListForPlaylist(Context context, long plid) {
+        final String[] ccols = new String[] { MediaStore.Audio.Playlists.Members.AUDIO_ID };
+        Cursor cursor = query(context, MediaStore.Audio.Playlists.Members.getContentUri("external", plid),
+                ccols, null, null, MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
+        
+        if (cursor != null) {
+            int [] list = getSongListForCursor(cursor);
+            cursor.close();
+            return list;
+        }
+        return sEmptyList;
+    }
+    
+    public static void playPlaylist(Context context, long plid) {
+        int [] list = getSongListForPlaylist(context, plid);
+        if (list != null) {
+            playAll(context, list, -1, false);
+        }
+    }
+
+    public static int [] getAllSongs(Context context) {
+        Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                new String[] {MediaStore.Audio.Media._ID}, MediaStore.Audio.Media.IS_MUSIC + "=1",
+                null, null);
+        try {
+            if (c == null || c.getCount() == 0) {
+                return null;
+            }
+            int len = c.getCount();
+            int[] list = new int[len];
+            for (int i = 0; i < len; i++) {
+                c.moveToNext();
+                list[i] = c.getInt(0);
+            }
+
+            return list;
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+    }
+
+    /**
+     * Fills out the given submenu with items for "new playlist" and
+     * any existing playlists. When the user selects an item, the
+     * application will receive PLAYLIST_SELECTED with the Uri of
+     * the selected playlist, NEW_PLAYLIST if a new playlist
+     * should be created, and QUEUE if the "current playlist" was
+     * selected.
+     * @param context The context to use for creating the menu items
+     * @param sub The submenu to add the items to.
+     */
+    public static void makePlaylistMenu(Context context, SubMenu sub) {
+        String[] cols = new String[] {
+                MediaStore.Audio.Playlists._ID,
+                MediaStore.Audio.Playlists.NAME
+        };
+        ContentResolver resolver = context.getContentResolver();
+        if (resolver == null) {
+            System.out.println("resolver = null");
+        } else {
+            String whereclause = MediaStore.Audio.Playlists.NAME + " != ''";
+            Cursor cur = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+                cols, whereclause, null,
+                MediaStore.Audio.Playlists.NAME);
+            sub.clear();
+            sub.add(1, Defs.QUEUE, 0, R.string.queue);
+            sub.add(1, Defs.NEW_PLAYLIST, 0, R.string.new_playlist);
+            if (cur != null && cur.getCount() > 0) {
+                //sub.addSeparator(1, 0);
+                cur.moveToFirst();
+                while (! cur.isAfterLast()) {
+                    Intent intent = new Intent();
+                    intent.putExtra("playlist", cur.getInt(0));
+//                    if (cur.getInt(0) == mLastPlaylistSelected) {
+//                        sub.add(0, MusicBaseActivity.PLAYLIST_SELECTED, cur.getString(1)).setIntent(intent);
+//                    } else {
+                        sub.add(1, Defs.PLAYLIST_SELECTED, 0, cur.getString(1)).setIntent(intent);
+//                    }
+                    cur.moveToNext();
+                }
+            }
+            if (cur != null) {
+                cur.close();
+            }
+        }
+    }
+
+    public static void clearPlaylist(Context context, int plid) {
+        final String[] ccols = new String[] { MediaStore.Audio.Playlists.Members._ID };
+        Cursor cursor = query(context, MediaStore.Audio.Playlists.Members.getContentUri("external", plid),
+                ccols, null, null, MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER);
+        
+        if (cursor == null) {
+            return;
+        }
+        cursor.moveToFirst();
+        while (!cursor.isAfterLast()) {
+            cursor.deleteRow();
+        }
+        cursor.commitUpdates();
+        cursor.close();
+        return;
+    }
+    
+    public static void deleteTracks(Context context, int [] list) {
+        
+        String [] cols = new String [] { MediaStore.Audio.Media._ID, 
+                MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM_ID };
+        StringBuilder where = new StringBuilder();
+        where.append(MediaStore.Audio.Media._ID + " IN (");
+        for (int i = 0; i < list.length; i++) {
+            where.append(list[i]);
+            if (i < list.length - 1) {
+                where.append(",");
+            }
+        }
+        where.append(")");
+        Cursor c = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cols,
+                where.toString(), null, null);
+
+        if (c != null) {
+
+            // step 1: remove selected tracks from the current playlist, as well
+            // as from the album art cache
+            try {
+                c.moveToFirst();
+                while (! c.isAfterLast()) {
+                    // remove from current playlist
+                    int id = c.getInt(0);
+                    sService.removeTrack(id);
+                    // remove from album art cache
+                    int artIndex = c.getInt(2);
+                    synchronized(sArtCache) {
+                        sArtCache.remove(artIndex);
+                    }
+                    c.moveToNext();
+                }
+            } catch (RemoteException ex) {
+            }
+
+            // step 2: remove selected tracks from the database
+            context.getContentResolver().delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, where.toString(), null);
+
+            // step 3: remove files from card
+            c.moveToFirst();
+            while (! c.isAfterLast()) {
+                String name = c.getString(1);
+                File f = new File(name);
+                try {  // File.delete can throw a security exception
+                    if (!f.delete()) {
+                        // I'm not sure if we'd ever get here (deletion would
+                        // have to fail, but no exception thrown)
+                        Log.e("MusicUtils", "Failed to delete file " + name);
+                    }
+                    c.moveToNext();
+                } catch (SecurityException ex) {
+                    c.moveToNext();
+                }
+            }
+            c.commitUpdates();
+            c.close();
+        }
+
+        String message = context.getResources().getQuantityString(
+                R.plurals.NNNtracksdeleted, list.length, Integer.valueOf(list.length));
+        
+        Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+        // We deleted a number of tracks, which could affect any number of things
+        // in the media content domain, so update everything.
+        context.getContentResolver().notifyChange(Uri.parse("content://media"), null);
+    }
+    
+    public static void addToCurrentPlaylist(Context context, int [] list) {
+        if (sService == null) {
+            return;
+        }
+        try {
+            sService.enqueue(list, MediaPlaybackService.LAST);
+            String message = context.getResources().getQuantityString(
+                    R.plurals.NNNtrackstoplaylist, list.length, Integer.valueOf(list.length));
+            Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+        } catch (RemoteException ex) {
+        }
+    }
+    
+    public static void addToPlaylist(Context context, int [] ids, long playlistid) {
+        if (ids == null) {
+            // this shouldn't happen (the menuitems shouldn't be visible
+            // unless the selected item represents something playable
+            Log.e("MusicBase", "ListSelection null");
+        } else {
+            int size = ids.length;
+            ContentValues values [] = new ContentValues[size];
+            ContentResolver resolver = context.getContentResolver();
+            // need to determine the number of items currently in the playlist,
+            // so the play_order field can be maintained.
+            String[] cols = new String[] {
+                    "count(*)"
+            };
+            Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid);
+            Cursor cur = resolver.query(uri, cols, null, null, null);
+            cur.moveToFirst();
+            int base = cur.getInt(0);
+            cur.close();
+
+            for (int i = 0; i < size; i++) {
+                values[i] = new ContentValues();
+                values[i].put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + i));
+                values[i].put(MediaStore.Audio.Playlists.Members.AUDIO_ID, ids[i]);
+            }
+            resolver.bulkInsert(uri, values);
+            String message = context.getResources().getQuantityString(
+                    R.plurals.NNNtrackstoplaylist, size, Integer.valueOf(size));
+            Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+            //mLastPlaylistSelected = playlistid;
+        }
+    }
+
+    public static Cursor query(Context context, Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder) {
+        try {
+            ContentResolver resolver = context.getContentResolver();
+            if (resolver == null) {
+                return null;
+            }
+            return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
+         } catch (UnsupportedOperationException ex) {
+            return null;
+        }
+        
+    }
+    
+    public static boolean isMediaScannerScanning(Context context) {
+        boolean result = false;
+        Uri uri = MediaStore.getMediaScannerUri();
+        Cursor cursor = query(context, 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 void setSpinnerState(Activity a) {
+        if (isMediaScannerScanning(a)) {
+            // start the progress spinner
+            a.getWindow().setFeatureInt(
+                    Window.FEATURE_INDETERMINATE_PROGRESS,
+                    Window.PROGRESS_INDETERMINATE_ON);
+
+            a.getWindow().setFeatureInt(
+                    Window.FEATURE_INDETERMINATE_PROGRESS,
+                    Window.PROGRESS_VISIBILITY_ON);
+        } else {
+            // stop the progress spinner
+            a.getWindow().setFeatureInt(
+                    Window.FEATURE_INDETERMINATE_PROGRESS,
+                    Window.PROGRESS_VISIBILITY_OFF);
+        }
+    }
+    
+    public static void displayDatabaseError(Activity a) {
+        String status = Environment.getExternalStorageState();
+        int title = R.string.sdcard_error_title;
+        int message = R.string.sdcard_error_message;
+        
+        if (status.equals(Environment.MEDIA_SHARED)) {
+            title = R.string.sdcard_busy_title;
+            message = R.string.sdcard_busy_message;
+        } else if (status.equals(Environment.MEDIA_REMOVED)) {
+            title = R.string.sdcard_missing_title;
+            message = R.string.sdcard_missing_message;
+        } else if (status.equals(Environment.MEDIA_MOUNTED)){
+            // The card is mounted, but we didn't get a valid cursor.
+            // This probably means the mediascanner hasn't started scanning the
+            // card yet (there is a small window of time during boot where this
+            // will happen).
+            a.setTitle("");
+            Intent intent = new Intent();
+            intent.setClass(a, ScanningProgress.class);
+            a.startActivityForResult(intent, Defs.SCAN_DONE);
+        } else {
+            Log.d(TAG, "sd card: " + status);
+        }
+
+        a.setTitle(title);
+        if (a instanceof ExpandableListActivity) {
+            a.setContentView(R.layout.no_sd_card_expanding);
+        } else {
+            a.setContentView(R.layout.no_sd_card);
+        }
+        TextView tv = (TextView) a.findViewById(R.id.sd_message);
+        tv.setText(message);
+    }
+
+    static protected Uri getContentURIForPath(String path) {
+        return Uri.fromFile(new File(path));
+    }
+
+    
+    /*  Try to use String.format() as little as possible, because it creates a
+     *  new Formatter every time you call it, which is very inefficient.
+     *  Reusing an existing Formatter more than tripled the speed of
+     *  makeTimeString().
+     *  This Formatter/StringBuilder are also used by makeAlbumSongsLabel()
+     */
+    private static StringBuilder sFormatBuilder = new StringBuilder();
+    private static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
+    private static final Object[] sTimeArgs = new Object[5];
+
+    public static String makeTimeString(Context context, long secs) {
+        String durationformat = context.getString(R.string.durationformat);
+        
+        /* Provide multiple arguments so the format can be changed easily
+         * by modifying the xml.
+         */
+        sFormatBuilder.setLength(0);
+
+        final Object[] timeArgs = sTimeArgs;
+        timeArgs[0] = secs / 3600;
+        timeArgs[1] = secs / 60;
+        timeArgs[2] = (secs / 60) % 60;
+        timeArgs[3] = secs;
+        timeArgs[4] = secs % 60;
+
+        return sFormatter.format(durationformat, timeArgs).toString();
+    }
+    
+    public static void shuffleAll(Context context, Cursor cursor) {
+        playAll(context, cursor, 0, true);
+    }
+
+    public static void playAll(Context context, Cursor cursor) {
+        playAll(context, cursor, 0, false);
+    }
+    
+    public static void playAll(Context context, Cursor cursor, int position) {
+        playAll(context, cursor, position, false);
+    }
+    
+    public static void playAll(Context context, int [] list, int position) {
+        playAll(context, list, position, false);
+    }
+    
+    private static void playAll(Context context, Cursor cursor, int position, boolean force_shuffle) {
+    
+        int [] list = getSongListForCursor(cursor);
+        playAll(context, list, position, force_shuffle);
+    }
+    
+    private static void playAll(Context context, int [] list, int position, boolean force_shuffle) {
+        if (list.length == 0 || sService == null) {
+            Log.d("MusicUtils", "attempt to play empty song list");
+            // Don't try to play empty playlists. Nothing good will come of it.
+            String message = context.getString(R.string.emptyplaylist, list.length);
+            Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+            return;
+        }
+        try {
+            if (force_shuffle) {
+                sService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL);
+            }
+            int curid = sService.getAudioId();
+            int curpos = sService.getQueuePosition();
+            if (position != -1 && curpos == position && curid == list[position]) {
+                // The selected file is the file that's currently playing;
+                // figure out if we need to restart with a new playlist,
+                // or just launch the playback activity.
+                int [] playlist = sService.getQueue();
+                if (Arrays.equals(list, playlist)) {
+                    // we don't need to set a new list, but we should resume playback if needed
+                    sService.play();
+                    return; // the 'finally' block will still run
+                }
+            }
+            if (position < 0) {
+                position = 0;
+            }
+            sService.open(list, position);
+            sService.play();
+        } catch (RemoteException ex) {
+        } finally {
+            Intent intent = new Intent("com.android.music.PLAYBACK_VIEWER")
+                .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+            context.startActivity(intent);
+        }
+    }
+    
+    public static void clearQueue() {
+        try {
+            sService.removeTracks(0, Integer.MAX_VALUE);
+        } catch (RemoteException ex) {
+        }
+    }
+    
+    // A really simple BitmapDrawable-like class, that doesn't do
+    // scaling, dithering or filtering.
+    private static class FastBitmapDrawable extends Drawable {
+        private Bitmap mBitmap;
+        public FastBitmapDrawable(Bitmap b) {
+            mBitmap = b;
+        }
+        @Override
+        public void draw(Canvas canvas) {
+            canvas.drawBitmap(mBitmap, 0, 0, null);
+        }
+        @Override
+        public int getOpacity() {
+            return PixelFormat.OPAQUE;
+        }
+        @Override
+        public void setAlpha(int alpha) {
+        }
+        @Override
+        public void setColorFilter(ColorFilter cf) {
+        }
+    }
+    
+    private static int sArtId = -2;
+    private static byte [] mCachedArt;
+    private static Bitmap mCachedBit = null;
+    private static final BitmapFactory.Options sBitmapOptionsCache = new BitmapFactory.Options();
+    private static final BitmapFactory.Options sBitmapOptions = new BitmapFactory.Options();
+    private static final Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");
+    private static final HashMap<Integer, Drawable> sArtCache = new HashMap<Integer, Drawable>();
+    private static int sArtCacheId = -1;
+    
+    static {
+        // for the cache, 
+        // 565 is faster to decode and display
+        // and we don't want to dither here because the image will be scaled down later
+        sBitmapOptionsCache.inPreferredConfig = Bitmap.Config.RGB_565;
+        sBitmapOptionsCache.inDither = false;
+
+        sBitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
+        sBitmapOptions.inDither = false;
+    }
+
+    public static void initAlbumArtCache() {
+        try {
+            int id = sService.getMediaMountedCount();
+            if (id != sArtCacheId) {
+                clearAlbumArtCache();
+                sArtCacheId = id; 
+            }
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static void clearAlbumArtCache() {
+        synchronized(sArtCache) {
+            sArtCache.clear();
+        }
+    }
+    
+    public static Drawable getCachedArtwork(Context context, int artIndex, BitmapDrawable defaultArtwork) {
+        Drawable d = null;
+        synchronized(sArtCache) {
+            d = sArtCache.get(artIndex);
+        }
+        if (d == null) {
+            d = defaultArtwork;
+            final Bitmap icon = defaultArtwork.getBitmap();
+            int w = icon.getWidth();
+            int h = icon.getHeight();
+            Bitmap b = MusicUtils.getArtworkQuick(context, artIndex, w, h);
+            if (b != null) {
+                d = new FastBitmapDrawable(b);
+                synchronized(sArtCache) {
+                    // the cache may have changed since we checked
+                    Drawable value = sArtCache.get(artIndex);
+                    if (value == null) {
+                        sArtCache.put(artIndex, d);
+                    } else {
+                        d = value;
+                    }
+                }
+            }
+        }
+        return d;
+    }
+
+    // Get album art for specified album. This method will not try to
+    // fall back to getting artwork directly from the file, nor will
+    // it attempt to repair the database.
+    private static Bitmap getArtworkQuick(Context context, int album_id, int w, int h) {
+        // NOTE: There is in fact a 1 pixel frame in the ImageView used to
+        // display this drawable. Take it into account now, so we don't have to
+        // scale later.
+        w -= 2;
+        h -= 2;
+        ContentResolver res = context.getContentResolver();
+        Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
+        if (uri != null) {
+            ParcelFileDescriptor fd = null;
+            try {
+                fd = res.openFileDescriptor(uri, "r");
+                int sampleSize = 1;
+                
+                // Compute the closest power-of-two scale factor 
+                // and pass that to sBitmapOptionsCache.inSampleSize, which will
+                // result in faster decoding and better quality
+                sBitmapOptionsCache.inJustDecodeBounds = true;
+                BitmapFactory.decodeFileDescriptor(
+                        fd.getFileDescriptor(), null, sBitmapOptionsCache);
+                int nextWidth = sBitmapOptionsCache.outWidth >> 1;
+                int nextHeight = sBitmapOptionsCache.outHeight >> 1;
+                while (nextWidth>w && nextHeight>h) {
+                    sampleSize <<= 1;
+                    nextWidth >>= 1;
+                    nextHeight >>= 1;
+                }
+
+                sBitmapOptionsCache.inSampleSize = sampleSize;
+                sBitmapOptionsCache.inJustDecodeBounds = false;
+                Bitmap b = BitmapFactory.decodeFileDescriptor(
+                        fd.getFileDescriptor(), null, sBitmapOptionsCache);
+
+                if (b != null) {
+                    // finally rescale to exactly the size we need
+                    if (sBitmapOptionsCache.outWidth != w || sBitmapOptionsCache.outHeight != h) {
+                        Bitmap tmp = Bitmap.createScaledBitmap(b, w, h, true);
+                        b.recycle();
+                        b = tmp;
+                    }
+                }
+                
+                return b;
+            } catch (FileNotFoundException e) {
+            } finally {
+                try {
+                    if (fd != null)
+                        fd.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+        return null;
+    }
+    
+    // Get album art for specified album. You should not pass in the album id
+    // for the "unknown" album here (use -1 instead)
+    public static Bitmap getArtwork(Context context, int album_id) {
+
+        if (album_id < 0) {
+            // This is something that is not in the database, so get the album art directly
+            // from the file.
+            Bitmap bm = getArtworkFromFile(context, null, -1);
+            if (bm != null) {
+                return bm;
+            }
+            return getDefaultArtwork(context);
+        }
+
+        ContentResolver res = context.getContentResolver();
+        Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
+        if (uri != null) {
+            InputStream in = null;
+            try {
+                in = res.openInputStream(uri);
+                return BitmapFactory.decodeStream(in, null, sBitmapOptions);
+            } catch (FileNotFoundException ex) {
+                // The album art thumbnail does not actually exist. Maybe the user deleted it, or
+                // maybe it never existed to begin with.
+                Bitmap bm = getArtworkFromFile(context, null, album_id);
+                if (bm != null) {
+                    // Put the newly found artwork in the database.
+                    // Note that this shouldn't be done for the "unknown" album,
+                    // but if this method is called correctly, that won't happen.
+                    
+                    // first write it somewhere
+                    String file = Environment.getExternalStorageDirectory()
+                        + "/albumthumbs/" + String.valueOf(System.currentTimeMillis());
+                    if (ensureFileExists(file)) {
+                        try {
+                            OutputStream outstream = new FileOutputStream(file);
+                            if (bm.getConfig() == null) {
+                                bm = bm.copy(Bitmap.Config.RGB_565, false);
+                                if (bm == null) {
+                                    return getDefaultArtwork(context);
+                                }
+                            }
+                            boolean success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream);
+                            outstream.close();
+                            if (success) {
+                                ContentValues values = new ContentValues();
+                                values.put("album_id", album_id);
+                                values.put("_data", file);
+                                Uri newuri = res.insert(sArtworkUri, values);
+                                if (newuri == null) {
+                                    // Failed to insert in to the database. The most likely
+                                    // cause of this is that the item already existed in the
+                                    // database, and the most likely cause of that is that
+                                    // the album was scanned before, but the user deleted the
+                                    // album art from the sd card.
+                                    // We can ignore that case here, since the media provider
+                                    // will regenerate the album art for those entries when
+                                    // it detects this.
+                                    success = false;
+                                }
+                            }
+                            if (!success) {
+                                File f = new File(file);
+                                f.delete();
+                            }
+                        } catch (FileNotFoundException e) {
+                            Log.e(TAG, "error creating file", e);
+                        } catch (IOException e) {
+                            Log.e(TAG, "error creating file", e);
+                        }
+                    }
+                } else {
+                    bm = getDefaultArtwork(context);
+                }
+                return bm;
+            } finally {
+                try {
+                    if (in != null) {
+                        in.close();
+                    }
+                } catch (IOException ex) {
+                }
+            }
+        }
+        
+        return null;
+    }
+
+    // copied from MediaProvider
+    private static boolean ensureFileExists(String path) {
+        File file = new File(path);
+        if (file.exists()) {
+            return true;
+        } else {
+            // we will not attempt to create the first directory in the path
+            // (for example, do not create /sdcard if the SD card is not mounted)
+            int secondSlash = path.indexOf('/', 1);
+            if (secondSlash < 1) return false;
+            String directoryPath = path.substring(0, secondSlash);
+            File directory = new File(directoryPath);
+            if (!directory.exists())
+                return false;
+            file.getParentFile().mkdirs();
+            try {
+                return file.createNewFile();
+            } catch(IOException ioe) {
+                Log.e(TAG, "File creation failed", ioe);
+            }
+            return false;
+        }
+    }
+    
+    // get album art for specified file
+    private static final String sExternalMediaUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString();
+    private static Bitmap getArtworkFromFile(Context context, Uri uri, int albumid) {
+        Bitmap bm = null;
+        byte [] art = null;
+        String path = null;
+
+        if (sArtId == albumid) {
+            //Log.i("@@@@@@ ", "reusing cached data", new Exception());
+            if (mCachedBit != null) {
+                return mCachedBit;
+            }
+            art = mCachedArt;
+        } else {
+            // try reading embedded artwork
+            if (uri == null) {
+                try {
+                    int curalbum = sService.getAlbumId();
+                    if (curalbum == albumid || albumid < 0) {
+                        path = sService.getPath();
+                        if (path != null) {
+                            uri = Uri.parse(path);
+                        }
+                    }
+                } catch (RemoteException ex) {
+                }
+            }
+            if (uri == null) {
+                if (albumid >= 0) {
+                    Cursor c = query(context,MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                            new String[] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.ALBUM },
+                            MediaStore.Audio.Media.ALBUM_ID + "=?", new String [] {String.valueOf(albumid)},
+                            null);
+                    if (c != null) {
+                        c.moveToFirst();
+                        if (!c.isAfterLast()) {
+                            int trackid = c.getInt(0);
+                            uri = ContentUris.withAppendedId(
+                                    MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, trackid);
+                        }
+                        if (c.getString(1).equals(MediaFile.UNKNOWN_STRING)) {
+                            albumid = -1;
+                        }
+                        c.close();
+                    }
+                }
+            }
+            if (uri != null) {
+                MediaScanner scanner = new MediaScanner(context);
+                ParcelFileDescriptor pfd = null;
+                try {
+                    pfd = context.getContentResolver().openFileDescriptor(uri, "r");
+                    if (pfd != null) {
+                        FileDescriptor fd = pfd.getFileDescriptor();
+                        art = scanner.extractAlbumArt(fd);
+                    }
+                } catch (IOException ex) {
+                } catch (SecurityException ex) {
+                } finally {
+                    try {
+                        if (pfd != null) {
+                            pfd.close();
+                        }
+                    } catch (IOException ex) {
+                    }
+                }
+            }
+        }
+        // if no embedded art exists, look for AlbumArt.jpg in same directory as the media file
+        if (art == null && path != null) {
+            if (path.startsWith(sExternalMediaUri)) {
+                // get the real path
+                Cursor c = query(context,Uri.parse(path),
+                        new String[] { MediaStore.Audio.Media.DATA},
+                        null, null, null);
+                if (c != null) {
+                    c.moveToFirst();
+                    if (!c.isAfterLast()) {
+                        path = c.getString(0);
+                    }
+                    c.close();
+                }
+            }
+            int lastSlash = path.lastIndexOf('/');
+            if (lastSlash > 0) {
+                String artPath = path.substring(0, lastSlash + 1) + "AlbumArt.jpg";
+                File file = new File(artPath);
+                if (file.exists()) {
+                    art = new byte[(int)file.length()];
+                    FileInputStream stream = null;
+                    try {
+                        stream = new FileInputStream(file);
+                        stream.read(art);
+                    } catch (IOException ex) {
+                        art = null;
+                    } finally {
+                        try {
+                            if (stream != null) {
+                                stream.close();
+                            }
+                        } catch (IOException ex) {
+                        }
+                    }
+                } else {
+                    // TODO: try getting album art from the web
+                }
+            }
+        }
+        
+        if (art != null) {
+            try {
+                // get the size of the bitmap
+                BitmapFactory.Options opts = new BitmapFactory.Options();
+                opts.inJustDecodeBounds = true;
+                opts.inSampleSize = 1;
+                BitmapFactory.decodeByteArray(art, 0, art.length, opts);
+                
+                // request a reasonably sized output image
+                // TODO: don't hardcode the size
+                while (opts.outHeight > 320 || opts.outWidth > 320) {
+                    opts.outHeight /= 2;
+                    opts.outWidth /= 2;
+                    opts.inSampleSize *= 2;
+                }
+                
+                // get the image for real now
+                opts.inJustDecodeBounds = false;
+                bm = BitmapFactory.decodeByteArray(art, 0, art.length, opts);
+                if (albumid != -1) {
+                    sArtId = albumid;
+                }
+                mCachedArt = art;
+                mCachedBit = bm;
+            } catch (Exception e) {
+            }
+        }
+        return bm;
+    }
+    
+    private static Bitmap getDefaultArtwork(Context context) {
+        BitmapFactory.Options opts = new BitmapFactory.Options();
+        opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
+        return BitmapFactory.decodeStream(
+                context.getResources().openRawResource(R.drawable.albumart_mp_unknown), null, opts);
+    }
+    
+    static int getIntPref(Context context, String name, int def) {
+        SharedPreferences prefs =
+            context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
+        return prefs.getInt(name, def);
+    }
+    
+    static void setIntPref(Context context, String name, int value) {
+        SharedPreferences prefs =
+            context.getSharedPreferences("com.android.music", Context.MODE_PRIVATE);
+        Editor ed = prefs.edit();
+        ed.putInt(name, value);
+        ed.commit();
+    }
+
+    static void setRingtone(Context context, long id) {
+        ContentResolver resolver = context.getContentResolver();
+        // Set the flag in the database to mark this as a ringtone
+        Uri ringUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
+        try {
+            ContentValues values = new ContentValues(2);
+            values.put(MediaStore.Audio.Media.IS_RINGTONE, "1");
+            values.put(MediaStore.Audio.Media.IS_ALARM, "1");
+            resolver.update(ringUri, values, null, null);
+        } catch (UnsupportedOperationException ex) {
+            // most likely the card just got unmounted
+            Log.e(TAG, "couldn't set ringtone flag for id " + id);
+            return;
+        }
+
+        String[] cols = new String[] {
+                MediaStore.Audio.Media._ID,
+                MediaStore.Audio.Media.DATA,
+                MediaStore.Audio.Media.TITLE
+        };
+
+        String where = MediaStore.Audio.Media._ID + "=" + id;
+        Cursor cursor = query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                cols, where , null, null);
+        try {
+            if (cursor != null && cursor.getCount() == 1) {
+                // Set the system setting to make this the current ringtone
+                cursor.moveToFirst();
+                Settings.System.putString(resolver, Settings.System.RINGTONE, ringUri.toString());
+                String message = context.getString(R.string.ringtone_set, cursor.getString(2));
+                Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+}
diff --git a/src/com/android/music/PlaylistBrowserActivity.java b/src/com/android/music/PlaylistBrowserActivity.java
new file mode 100644 (file)
index 0000000..bb5cc21
--- /dev/null
@@ -0,0 +1,439 @@
+/*
+ * 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.android.music;
+
+import java.text.Collator;
+import java.util.ArrayList;
+
+import android.app.ListActivity;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+
+import com.android.internal.database.ArrayListCursor;
+import android.database.Cursor;
+import android.database.MergeCursor;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+
+public class PlaylistBrowserActivity extends ListActivity
+    implements View.OnCreateContextMenuListener, MusicUtils.Defs
+{
+    private static final int DELETE_PLAYLIST = CHILD_MENU_BASE + 1;
+    private static final int EDIT_PLAYLIST = CHILD_MENU_BASE + 2;
+    private static final int RENAME_PLAYLIST = CHILD_MENU_BASE + 3;
+    private static final int CHANGE_WEEKS = CHILD_MENU_BASE + 4;
+    private static final long RECENTLY_ADDED_PLAYLIST = -1;
+    private static final long ALL_SONGS_PLAYLIST = -2;
+
+    private boolean mCreateShortcut;
+
+    public PlaylistBrowserActivity()
+    {
+    }
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle icicle)
+    {
+        super.onCreate(icicle);
+
+        final Intent intent = getIntent();
+        final String action = intent.getAction();
+        if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
+            mCreateShortcut = true;
+        }
+
+        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+        MusicUtils.bindToService(this, new ServiceConnection() {
+            public void onServiceConnected(ComponentName classname, IBinder obj) {
+                if (Intent.ACTION_VIEW.equals(action)) {
+                    long id = Long.parseLong(intent.getExtras().getString("playlist"));
+                    if (id == RECENTLY_ADDED_PLAYLIST) {
+                        playRecentlyAdded();
+                    } else if (id == ALL_SONGS_PLAYLIST) {
+                        int [] list = MusicUtils.getAllSongs(PlaylistBrowserActivity.this);
+                        if (list != null) {
+                            MusicUtils.playAll(PlaylistBrowserActivity.this, list, 0);
+                        }
+                    } else {
+                        MusicUtils.playPlaylist(PlaylistBrowserActivity.this, id);
+                    }
+                    finish();
+                }
+            }
+
+            public void onServiceDisconnected(ComponentName classname) {
+            }
+        
+        });
+        IntentFilter f = new IntentFilter();
+        f.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+        f.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
+        f.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+        f.addDataScheme("file");
+        registerReceiver(mScanListener, f);
+
+        init();
+    }
+    
+    
+    
+    @Override
+    public void onDestroy() {
+        MusicUtils.unbindFromService(this);
+        if (mPlaylistCursor != null) {
+            mPlaylistCursor.close();
+        }        
+        unregisterReceiver(mScanListener);
+        super.onDestroy();
+    }
+    
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        MusicUtils.setSpinnerState(this);
+    }
+    @Override
+    public void onPause() {
+        mReScanHandler.removeCallbacksAndMessages(null);
+        super.onPause();
+    }
+    private BroadcastReceiver mScanListener = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            MusicUtils.setSpinnerState(PlaylistBrowserActivity.this);
+            mReScanHandler.sendEmptyMessage(0);
+        }
+    };
+    
+    private Handler mReScanHandler = new Handler() {
+        public void handleMessage(Message msg) {
+            init();
+            if (mPlaylistCursor == null) {
+                sendEmptyMessageDelayed(0, 1000);
+            }
+        }
+    };
+    public void init() {
+
+        mPlaylistCursor = getPlaylistCursor(null);
+
+        if (mPlaylistCursor == null) {
+            MusicUtils.displayDatabaseError(this);
+            setContentView(R.layout.no_sd_card);
+            return;
+        }
+
+        setContentView(R.layout.media_picker_activity);
+
+        if (mPlaylistCursor.getCount() > 0) {
+            mPlaylistCursor.moveToFirst();
+
+            setTitle(R.string.playlists_title);
+        } else {
+            setTitle(R.string.no_playlists_title);
+        }
+
+        // Map Cursor columns to views defined in media_list_item.xml
+        PlaylistListAdapter adapter = new PlaylistListAdapter(
+                this,
+                R.layout.track_list_item,
+                mPlaylistCursor,
+                new String[] { MediaStore.Audio.Playlists.NAME},
+                new int[] { android.R.id.text1 });
+
+        setListAdapter(adapter);
+        ListView lv = getListView();
+        lv.setOnCreateContextMenuListener(this);
+        lv.setTextFilterEnabled(true);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        if (!mCreateShortcut) {
+            menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(
+                    R.drawable.ic_menu_music_library);
+            menu.add(0, GOTO_PLAYBACK, 0, R.string.goto_playback).setIcon(
+                    R.drawable.ic_menu_playback).setVisible(MusicUtils.isMusicLoaded());
+        }
+        return super.onCreateOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        Intent intent;
+        switch (item.getItemId()) {
+            case GOTO_START:
+                intent = new Intent();
+                intent.setClass(this, MusicBrowserActivity.class);
+                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                startActivity(intent);
+                return true;
+
+            case GOTO_PLAYBACK:
+                intent = new Intent("com.android.music.PLAYBACK_VIEWER");
+                startActivity(intent);
+                return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+    
+    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
+        if (mCreateShortcut) {
+            return;
+        }
+
+        AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn;
+        
+        if (mi.id < 0) {
+            menu.add(0, PLAY_SELECTION, 0, R.string.play_selection);
+            menu.add(0, EDIT_PLAYLIST, 0, R.string.edit_playlist_menu);
+            mPlaylistCursor.moveToPosition(mi.position);
+            menu.setHeaderTitle(mPlaylistCursor.getString(mPlaylistCursor.getColumnIndex(MediaStore.Audio.Playlists.NAME)));
+        } else {
+            menu.add(0, PLAY_SELECTION, 0, R.string.play_selection);
+            menu.add(0, DELETE_PLAYLIST, 0, R.string.delete_playlist_menu);
+            menu.add(0, EDIT_PLAYLIST, 0, R.string.edit_playlist_menu);
+            menu.add(0, RENAME_PLAYLIST, 0, R.string.rename_playlist_menu);
+            mPlaylistCursor.moveToPosition(mi.position);
+            menu.setHeaderTitle(mPlaylistCursor.getString(mPlaylistCursor.getColumnIndex(MediaStore.Audio.Playlists.NAME)));
+        }
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        AdapterContextMenuInfo mi = (AdapterContextMenuInfo) item.getMenuInfo();
+        switch (item.getItemId()) {
+            case PLAY_SELECTION:
+                if (mi.id == RECENTLY_ADDED_PLAYLIST) {
+                    playRecentlyAdded();
+                } else {
+                    MusicUtils.playPlaylist(this, mi.id);
+                }
+                break;
+            case DELETE_PLAYLIST:
+                mPlaylistCursor.moveToPosition(mi.position);
+                mPlaylistCursor.deleteRow();
+                mPlaylistCursor.commitUpdates();
+                Toast.makeText(this, R.string.playlist_deleted_message, Toast.LENGTH_SHORT).show();
+                if (mPlaylistCursor.getCount() == 0) {
+                    setTitle(R.string.no_playlists_title);
+                }
+                break;
+            case EDIT_PLAYLIST:
+                if (mi.id == RECENTLY_ADDED_PLAYLIST) {
+                    Intent intent = new Intent();
+                    intent.setClass(this, WeekSelector.class);
+                    startActivityForResult(intent, CHANGE_WEEKS);
+                    return true;
+                } else {
+                    Intent intent = new Intent(Intent.ACTION_EDIT);
+                    intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
+                    intent.putExtra("playlist", Long.valueOf(mi.id).toString());
+                    startActivity(intent);
+                }
+                break;
+            case RENAME_PLAYLIST:
+                Intent intent = new Intent();
+                intent.setClass(this, RenamePlaylist.class);
+                intent.putExtra("rename", mi.id);
+                startActivityForResult(intent, RENAME_PLAYLIST);
+                break;
+        }
+        return true;
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+        switch (requestCode) {
+            case SCAN_DONE:
+                if (resultCode == RESULT_CANCELED) {
+                    finish();
+                } else {
+                    init();
+                }
+                break;
+        }
+    }
+
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id)
+    {
+        if (mCreateShortcut) {
+            final Intent shortcut = new Intent();
+            shortcut.setAction(Intent.ACTION_VIEW);
+            shortcut.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/playlist");
+            shortcut.putExtra("playlist", String.valueOf(id));
+
+            final Intent intent = new Intent();
+            intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcut);
+            intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, ((TextView) v.findViewById(R.id.line1)).getText());
+            intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(
+                    this, R.drawable.app_music));
+
+            setResult(RESULT_OK, intent);
+            finish();
+            return;
+        }
+        if (id == RECENTLY_ADDED_PLAYLIST) {
+            playRecentlyAdded();
+        } else {
+            MusicUtils.playPlaylist(this, id);
+        }
+    }
+
+    private void playRecentlyAdded() {
+        // do a query for all playlists in the last X weeks
+        int X = MusicUtils.getIntPref(this, "numweeks", 2) * (3600 * 24 * 7);
+        final String[] ccols = new String[] { MediaStore.Audio.Media._ID};
+        String where = MediaStore.MediaColumns.DATE_ADDED + ">" + (System.currentTimeMillis() / 1000 - X);
+        Cursor cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                ccols, where, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+        
+        if (cursor == null) {
+            // Todo: show a message
+            return;
+        }
+        int len = cursor.getCount();
+        int [] list = new int[len];
+        for (int i = 0; i < len; i++) {
+            cursor.moveToNext();
+            list[i] = cursor.getInt(0);
+        }
+        cursor.close();
+        MusicUtils.playAll(this, list, 0);
+    }
+    
+    private Cursor getPlaylistCursor(String filterstring) {
+        String[] cols = new String[] {
+                MediaStore.Audio.Playlists._ID,
+                MediaStore.Audio.Playlists.NAME
+        };
+
+        StringBuilder where = new StringBuilder();
+        where.append(MediaStore.Audio.Playlists.NAME + " != ''");
+        
+        // Add in the filtering constraints
+        String [] keywords = null;
+        if (filterstring != null) {
+            String [] searchWords = filterstring.split(" ");
+            keywords = new String[searchWords.length];
+            Collator col = Collator.getInstance();
+            col.setStrength(Collator.PRIMARY);
+            for (int i = 0; i < searchWords.length; i++) {
+                keywords[i] = '%' + searchWords[i] + '%';
+            }
+            for (int i = 0; i < searchWords.length; i++) {
+                where.append(" AND ");
+                where.append(MediaStore.Audio.Playlists.NAME + " LIKE ?");
+            }
+        }
+        
+        String whereclause = where.toString();
+        
+        Cursor results = MusicUtils.query(this, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+            cols, whereclause, keywords,
+            MediaStore.Audio.Playlists.NAME);
+        
+        if (results == null) {
+            return null;
+        }
+        ArrayList<ArrayList> autoplaylists = new ArrayList<ArrayList>();
+        if (mCreateShortcut) {
+            ArrayList<Object> all = new ArrayList<Object>(2);
+            all.add(ALL_SONGS_PLAYLIST);
+            all.add(getString(R.string.play_all));
+            autoplaylists.add(all);
+        }
+        ArrayList<Object> recent = new ArrayList<Object>(2);
+        recent.add(RECENTLY_ADDED_PLAYLIST);
+        recent.add(getString(R.string.recentlyadded));
+        autoplaylists.add(recent);
+
+        ArrayListCursor autoplaylistscursor = new ArrayListCursor(cols, autoplaylists);
+        
+        Cursor c = new MergeCursor(new Cursor [] {autoplaylistscursor, results});
+        
+        return c;
+    }
+    
+    class PlaylistListAdapter extends SimpleCursorAdapter {
+        int mTitleIdx;
+
+        PlaylistListAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to) {
+            super(context, layout, cursor, from, to);
+            
+            mTitleIdx = cursor.getColumnIndex(MediaStore.Audio.Playlists.NAME);
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            
+            TextView tv = (TextView) view.findViewById(R.id.line1);
+            
+            String name = cursor.getString(mTitleIdx);
+            tv.setText(name);
+            
+            ImageView iv = (ImageView) view.findViewById(R.id.icon);
+            iv.setImageResource(R.drawable.ic_mp_playlist_list);
+            ViewGroup.LayoutParams p = iv.getLayoutParams();
+            p.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+            p.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+
+            iv = (ImageView) view.findViewById(R.id.play_indicator);
+            iv.setVisibility(View.GONE);
+        }
+
+        @Override
+        public void changeCursor(Cursor cursor) {
+            super.changeCursor(cursor);
+            mPlaylistCursor = cursor;
+        }
+        @Override
+        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+            return getPlaylistCursor(constraint.toString());
+        }
+    }
+    
+    private Cursor mPlaylistCursor;
+}
+
diff --git a/src/com/android/music/QueryBrowserActivity.java b/src/com/android/music/QueryBrowserActivity.java
new file mode 100644 (file)
index 0000000..c8860d2
--- /dev/null
@@ -0,0 +1,314 @@
+/*
+ * 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.android.music;
+
+import android.app.ListActivity;
+import android.app.SearchManager;
+import android.content.Context;
+import android.content.Intent;
+import com.android.internal.database.ArrayListCursor;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.media.AudioManager;
+import android.media.MediaFile;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.ViewGroup.OnHierarchyChangeListener;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+public class QueryBrowserActivity extends ListActivity implements MusicUtils.Defs
+{
+    private final static int PLAY_NOW = 0;
+    private final static int ADD_TO_QUEUE = 1;
+    private final static int PLAY_NEXT = 2;
+    private final static int PLAY_ARTIST = 3;
+    private final static int EXPLORE_ARTIST = 4;
+    private final static int PLAY_ALBUM = 5;
+    private final static int EXPLORE_ALBUM = 6;
+    private final static int REQUERY = 3;
+    private String mSearchString = null;
+
+    public QueryBrowserActivity()
+    {
+    }
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle icicle)
+    {
+        super.onCreate(icicle);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+        MusicUtils.bindToService(this);
+        
+        if (icicle == null) {
+            Intent intent = getIntent();
+            mSearchString = intent.getStringExtra(SearchManager.QUERY);
+        }
+        if (mSearchString == null) {
+            mSearchString = "";
+        }
+        init();
+    }
+    
+    @Override
+    public void onDestroy() {
+        MusicUtils.unbindFromService(this);
+        super.onDestroy();
+    }
+    
+    public void init() {
+        // Set the layout for this activity.  You can find it
+        // in assets/res/any/layout/media_picker_activity.xml
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.query_activity);
+        setTitle(R.string.search_title);
+        mTrackList = (ListView) findViewById(android.R.id.list);
+
+        mQueryCursor = getQueryCursor(mSearchString.length() == 0 ? "" : null);
+
+        ListView lv = getListView();
+        lv.setTextFilterEnabled(true);
+        
+        if (mQueryCursor == null) {
+            MusicUtils.displayDatabaseError(this);
+            setListAdapter(null);
+            lv.setFocusable(false);
+            return;
+        }
+
+        // Map Cursor columns to views defined in media_list_item.xml
+        QueryListAdapter adapter = new QueryListAdapter(
+                this,
+                R.layout.track_list_item,
+                mQueryCursor,
+                new String[] {},
+                new int[] {});
+
+        setListAdapter(adapter);
+
+        if (mSearchString.length() != 0) {
+            // Hack. Can be removed once ListView supports starting filtering with a given string
+            lv.setOnHierarchyChangeListener(mHierarchyListener);
+        }
+    }
+    
+    OnHierarchyChangeListener mHierarchyListener = new OnHierarchyChangeListener() {
+        public void onChildViewAdded(View parent, View child) {
+            ((ListView)parent).setOnHierarchyChangeListener(null);
+            // need to do this here to be sure all the views have been initialized
+            startFilteringWithString(mSearchString);
+        }
+
+        public void onChildViewRemoved(View parent, View child) {
+        }
+    };
+
+    private KeyEvent eventForChar(char c) {
+        int code = -1;
+        if (c >= 'a' && c <= 'z') {
+            code = KeyEvent.KEYCODE_A + (c - 'a');
+        } else if (c >= 'A' && c <= 'Z') {
+            code = KeyEvent.KEYCODE_A + (c - 'A');
+        } else if (c >= '0' && c <= '9') {
+            code = KeyEvent.KEYCODE_0 + (c - '0');
+        }
+        if (code != -1) {
+            return new KeyEvent(KeyEvent.ACTION_DOWN, code);
+        }
+        return null;
+    }
+    private void startFilteringWithString(String filterstring) {
+        ListView lv = getListView();
+        for (int i = 0; i < filterstring.length(); i++) {
+            KeyEvent event = eventForChar(filterstring.charAt(i));
+            if (event != null) {
+                lv.onKeyDown(event.getKeyCode(), event);
+            }
+        }
+    }
+    
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id)
+    {
+        // Dialog doesn't allow us to wait for a result, so we need to store
+        // the info we need for when the dialog posts its result
+        mQueryCursor.moveToPosition(position);
+        if (mQueryCursor.isBeforeFirst() || mQueryCursor.isAfterLast()) {
+            return;
+        }
+        String selectedType = mQueryCursor.getString(mQueryCursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE));
+        
+        if ("artist".equals(selectedType)) {
+            Intent intent = new Intent(Intent.ACTION_PICK);
+            intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/album");
+            intent.putExtra("artist", Long.valueOf(id).toString());
+            startActivity(intent);
+        } else if ("album".equals(selectedType)) {
+            Intent intent = new Intent(Intent.ACTION_PICK);
+            intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
+            intent.putExtra("album", Long.valueOf(id).toString());
+            startActivity(intent);
+        } else if (position >= 0 && id >= 0){
+            int [] list = new int[] { (int) id };
+            MusicUtils.playAll(this, list, 0);
+        } else {
+            Log.e("QueryBrowser", "invalid position/id: " + position + "/" + id);
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case USE_AS_RINGTONE: {
+                // Set the system setting to make this the current ringtone
+                MusicUtils.setRingtone(this, mTrackList.getSelectedItemId());
+                return true;
+            }
+
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    private Cursor getQueryCursor(String filterstring) {
+        String[] ccols = new String[] {
+                "_id",   // this will be the artist, album or track ID
+                MediaStore.Audio.Media.MIME_TYPE, // mimetype of audio file, or "artist" or "album"
+                SearchManager.SUGGEST_COLUMN_TEXT_1,
+                "data1",
+                "data2"
+        };
+        if (filterstring == null) {
+            ArrayList<ArrayList> placeholder = new ArrayList<ArrayList>();
+            ArrayList<Object> row = new ArrayList<Object>(5);
+            row.add(-1);
+            row.add("");
+            row.add("");
+            row.add("");
+            row.add("");
+            placeholder.add(row);
+            return new ArrayListCursor(ccols, placeholder);
+        }
+        Uri search = Uri.parse("content://media/external/audio/" + 
+                SearchManager.SUGGEST_URI_PATH_QUERY + "/" + Uri.encode(filterstring));
+        
+        return MusicUtils.query(this, search, ccols, null, null, null);
+        
+    }
+    
+    class QueryListAdapter extends SimpleCursorAdapter {
+        QueryListAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to) {
+            super(context, layout, cursor, from, to);
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            
+            TextView tv1 = (TextView) view.findViewById(R.id.line1);
+            TextView tv2 = (TextView) view.findViewById(R.id.line2);
+            ImageView iv = (ImageView) view.findViewById(R.id.icon);
+            ViewGroup.LayoutParams p = iv.getLayoutParams();
+            if (p == null) {
+                // seen this happen, not sure why
+                DatabaseUtils.dumpCursor(cursor);
+                return;
+            }
+            p.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+            p.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+            
+            String mimetype = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE));
+            
+            if (mimetype == null) {
+                mimetype = "audio/";
+            }
+            if (mimetype.equals("artist")) {
+                iv.setImageResource(R.drawable.ic_search_category_music_artist);
+                String name = cursor.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1));
+                String displayname = name;
+                if (name.equals(MediaFile.UNKNOWN_STRING)) {
+                    displayname = context.getString(R.string.unknown_artist_name);
+                }
+                tv1.setText(displayname);
+
+                int numalbums = cursor.getInt(cursor.getColumnIndex("data1"));
+                int numsongs = cursor.getInt(cursor.getColumnIndex("data2"));
+                
+                String songs_albums = MusicUtils.makeAlbumsSongsLabel(context,
+                        numalbums, numsongs, name.equals(MediaFile.UNKNOWN_STRING));
+                
+                tv2.setText(songs_albums);
+            
+            } else if (mimetype.equals("album")) {
+                iv.setImageResource(R.drawable.ic_search_category_music_album);
+                String name = cursor.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1));
+                String displayname = name;
+                if (name.equals(MediaFile.UNKNOWN_STRING)) {
+                    displayname = context.getString(R.string.unknown_album_name);
+                }
+                tv1.setText(displayname);
+                
+                name = cursor.getString(cursor.getColumnIndex("data1"));
+                displayname = name;
+                if (name.equals(MediaFile.UNKNOWN_STRING)) {
+                    displayname = context.getString(R.string.unknown_artist_name);
+                }
+                tv2.setText(displayname);
+                
+            } else if(mimetype.startsWith("audio/") ||
+                    mimetype.equals("application/ogg") ||
+                    mimetype.equals("application/x-ogg")) {
+                iv.setImageResource(R.drawable.ic_search_category_music_song);
+                String name = cursor.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1));
+                tv1.setText(name);
+
+                String displayname = cursor.getString(cursor.getColumnIndex("data1"));
+                if (name.equals(MediaFile.UNKNOWN_STRING)) {
+                    displayname = context.getString(R.string.unknown_artist_name);
+                }
+                name = cursor.getString(cursor.getColumnIndex("data2"));
+                if (name.equals(MediaFile.UNKNOWN_STRING)) {
+                    name = context.getString(R.string.unknown_artist_name);
+                }
+                tv2.setText(displayname + " - " + name);
+            }
+        }
+        @Override
+        public void changeCursor(Cursor cursor) {
+            super.changeCursor(cursor);
+            mQueryCursor = cursor;
+        }
+        @Override
+        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+            return getQueryCursor(constraint.toString());
+        }
+    }
+
+    private ListView mTrackList;
+    private Cursor mQueryCursor;
+}
+
diff --git a/src/com/android/music/RenamePlaylist.java b/src/com/android/music/RenamePlaylist.java
new file mode 100644 (file)
index 0000000..f81e2af
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+ * 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.android.music;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class RenamePlaylist extends Activity
+{
+    private EditText mPlaylist;
+    private TextView mPrompt;
+    private Button mSaveButton;
+    private long mRenameId;
+    private String mOriginalName;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.create_playlist);
+        getWindow().setLayout(WindowManager.LayoutParams.FILL_PARENT,
+                                    WindowManager.LayoutParams.WRAP_CONTENT);
+
+        mPrompt = (TextView)findViewById(R.id.prompt);
+        mPlaylist = (EditText)findViewById(R.id.playlist);
+        mSaveButton = (Button) findViewById(R.id.create);
+        mSaveButton.setOnClickListener(mOpenClicked);
+
+        ((Button)findViewById(R.id.cancel)).setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                finish();
+            }
+        });
+
+        mRenameId = icicle != null ? icicle.getLong("rename")
+                : getIntent().getLongExtra("rename", -1);
+        mOriginalName = nameForId(mRenameId);
+        String defaultname = icicle != null ? icicle.getString("defaultname") : mOriginalName;
+        
+        if (mRenameId < 0 || mOriginalName == null || defaultname == null) {
+            Log.i("@@@@", "Rename failed: " + mRenameId + "/" + defaultname);
+            finish();
+            return;
+        }
+        
+        String promptformat;
+        if (mOriginalName.equals(defaultname)) {
+            promptformat = getString(R.string.rename_playlist_same_prompt);
+        } else {
+            promptformat = getString(R.string.rename_playlist_diff_prompt);
+        }
+                
+        String prompt = String.format(promptformat, mOriginalName, defaultname);
+        mPrompt.setText(prompt);
+        mPlaylist.setText(defaultname);
+        mPlaylist.setSelection(defaultname.length());
+        mPlaylist.addTextChangedListener(mTextWatcher);
+        setSaveButton();
+    }
+    
+    TextWatcher mTextWatcher = new TextWatcher() {
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+            // don't care about this one
+        }
+        public void onTextChanged(CharSequence s, int start, int before, int count) {
+            // check if playlist with current name exists already, and warn the user if so.
+            setSaveButton();
+        };
+        public void afterTextChanged(Editable s) {
+            // don't care about this one
+        }
+    };
+    
+    private void setSaveButton() {
+        String typedname = mPlaylist.getText().toString(); 
+        if (idForplaylist(typedname) >= 0
+                && ! mOriginalName.equals(typedname)) {
+            mSaveButton.setText(R.string.create_playlist_overwrite_text);
+        } else {
+            mSaveButton.setText(R.string.create_playlist_create_text);
+        }
+    }
+    
+    private int idForplaylist(String name) {
+        Cursor c = MusicUtils.query(this, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+                new String[] { MediaStore.Audio.Playlists._ID },
+                MediaStore.Audio.Playlists.NAME + "=?",
+                new String[] { name },
+                MediaStore.Audio.Playlists.NAME);
+        int id = -1;
+        if (c != null) {
+            c.moveToFirst();
+            if (!c.isAfterLast()) {
+                id = c.getInt(0);
+            }
+        }
+        c.close();
+        return id;
+    }
+    
+    private String nameForId(long id) {
+        Cursor c = MusicUtils.query(this, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+                new String[] { MediaStore.Audio.Playlists.NAME },
+                MediaStore.Audio.Playlists._ID + "=?",
+                new String[] { Long.valueOf(id).toString() },
+                MediaStore.Audio.Playlists.NAME);
+        String name = null;
+        if (c != null) {
+            c.moveToFirst();
+            if (!c.isAfterLast()) {
+                name = c.getString(0);
+            }
+        }
+        c.close();
+        return name;
+    }
+    
+    
+    @Override
+    public void onSaveInstanceState(Bundle outcicle) {
+        outcicle.putString("defaultname", mPlaylist.getText().toString());
+        outcicle.putLong("rename", mRenameId);
+    }
+    
+    @Override
+    public void onResume() {
+        super.onResume();
+    }
+
+    private View.OnClickListener mOpenClicked = new View.OnClickListener() {
+        public void onClick(View v) {
+            String name = mPlaylist.getText().toString();
+            if (name != null && name.length() > 0) {
+                ContentResolver resolver = getContentResolver();
+                ContentValues values = new ContentValues(1);
+                values.put(MediaStore.Audio.Playlists.NAME, name);
+                resolver.update(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+                        values,
+                        MediaStore.Audio.Playlists._ID + "=?",
+                        new String[] { Long.valueOf(mRenameId).toString()});
+                
+                setResult(RESULT_OK);
+                Toast.makeText(RenamePlaylist.this, R.string.playlist_renamed_message, Toast.LENGTH_SHORT).show();
+                finish();
+            }
+        }
+    };
+}
diff --git a/src/com/android/music/RepeatingImageButton.java b/src/com/android/music/RepeatingImageButton.java
new file mode 100644 (file)
index 0000000..08c951c
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * 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.android.music;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageButton;
+
+/**
+ * A button that will repeatedly call a 'listener' method
+ * as long as the button is pressed.
+ */
+public class RepeatingImageButton extends ImageButton {
+
+    private long mStartTime;
+    private int mRepeatCount;
+    private RepeatListener mListener;
+    private long mInterval = 500;
+    
+    public RepeatingImageButton(Context context) {
+        this(context, null);
+    }
+
+    public RepeatingImageButton(Context context, AttributeSet attrs) {
+        this(context, attrs, android.R.attr.imageButtonStyle);
+    }
+
+    public RepeatingImageButton(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        setFocusable(true);
+        setLongClickable(true);
+    }
+    
+    /**
+     * Sets the listener to be called while the button is pressed and
+     * the interval in milliseconds with which it will be called.
+     * @param l The listener that will be called
+     * @param interval The interval in milliseconds for calls 
+     */
+    public void setRepeatListener(RepeatListener l, long interval) {
+        mListener = l;
+        mInterval = interval;
+    }
+    
+    @Override
+    public boolean performLongClick() {
+        mStartTime = SystemClock.elapsedRealtime();
+        mRepeatCount = 0;
+        post(mRepeater);
+        return true;
+    }
+    
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_UP) {
+            // remove the repeater, but call the hook one more time
+            removeCallbacks(mRepeater);
+            if (mStartTime != 0) {
+                doRepeat(true);
+                mStartTime = 0;
+            }
+        }
+        return super.onTouchEvent(event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        switch (keyCode) {
+        case KeyEvent.KEYCODE_DPAD_CENTER:
+        case KeyEvent.KEYCODE_ENTER:
+            // remove the repeater, but call the hook one more time
+            removeCallbacks(mRepeater);
+            if (mStartTime != 0) {
+                doRepeat(true);
+                mStartTime = 0;
+            }
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+    
+    private Runnable mRepeater = new Runnable() {
+        public void run() {
+            doRepeat(false);
+            if (isPressed()) {
+                postDelayed(this, mInterval);
+            }
+        }
+    };
+
+    private  void doRepeat(boolean last) {
+        long now = SystemClock.elapsedRealtime();
+        if (mListener != null) {
+            mListener.onRepeat(this, now - mStartTime, last ? -1 : mRepeatCount++);
+        }
+    }
+    
+    public interface RepeatListener {
+        /**
+         * This method will be called repeatedly at roughly the interval
+         * specified in setRepeatListener(), for as long as the button
+         * is pressed.
+         * @param v The button as a View.
+         * @param duration The number of milliseconds the button has been pressed so far.
+         * @param repeatcount The number of previous calls in this sequence.
+         * If this is going to be the last call in this sequence (i.e. the user
+         * just stopped pressing the button), the value will be -1.  
+         */
+        void onRepeat(View v, long duration, int repeatcount);
+    }
+}
diff --git a/src/com/android/music/ScanningProgress.java b/src/com/android/music/ScanningProgress.java
new file mode 100644 (file)
index 0000000..e5eae94
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * 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.android.music;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.view.Window;
+import android.view.WindowManager;
+
+public class ScanningProgress extends Activity
+{
+    private final static int CHECK = 0;
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg)
+        {
+            if (msg.what == CHECK) {
+                String status = Environment.getExternalStorageState();
+                if (!status.equals(Environment.MEDIA_MOUNTED)) {
+                    // If the card suddenly got unmounted again, there's
+                    // really no need to keep waiting for the media scanner.
+                    finish();
+                    return;
+                }
+                Cursor c = MusicUtils.query(ScanningProgress.this,
+                        MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+                        null, null, null, null);
+                if (c != null) {
+                    // The external media database is now ready for querying
+                    // (though it may still be in the process of being filled).
+                    c.close();
+                    setResult(RESULT_OK);
+                    finish();
+                    return;
+                }
+                Message next = obtainMessage(CHECK);
+                sendMessageDelayed(next, 3000);
+            }
+        }
+    };
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.scanning);
+        getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT,
+                                    WindowManager.LayoutParams.WRAP_CONTENT);
+        setResult(RESULT_CANCELED);
+        
+        Message msg = mHandler.obtainMessage(CHECK);
+        mHandler.sendMessageDelayed(msg, 1000);
+    }
+    
+    @Override
+    public void onDestroy() {
+        mHandler.removeMessages(CHECK);
+        super.onDestroy();
+    }
+}
diff --git a/src/com/android/music/StreamStarter.java b/src/com/android/music/StreamStarter.java
new file mode 100644 (file)
index 0000000..0537bad
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * 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.android.music;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.Window;
+import android.widget.TextView;
+
+public class StreamStarter extends Activity
+{
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.streamstarter);
+        
+        TextView tv = (TextView) findViewById(R.id.streamloading);
+        
+        Uri uri = getIntent().getData();
+        String msg = getString(R.string.streamloadingtext, uri.getHost());
+        tv.setText(msg);
+    }
+    
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        MusicUtils.bindToService(this, new ServiceConnection() {
+            public void onServiceConnected(ComponentName classname, IBinder obj) {
+                try {
+                    IntentFilter f = new IntentFilter();
+                    f.addAction(MediaPlaybackService.ASYNC_OPEN_COMPLETE);
+                    registerReceiver(mStatusListener, new IntentFilter(f));
+                    MusicUtils.sService.openfileAsync(getIntent().getData().toString());
+                } catch (RemoteException ex) {
+                }
+            }
+
+            public void onServiceDisconnected(ComponentName classname) {
+            }
+        });
+    }
+    
+    private BroadcastReceiver mStatusListener = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            try {
+                MusicUtils.sService.play();
+                intent = new Intent("com.android.music.PLAYBACK_VIEWER");
+                intent.putExtra("oneshot", true);
+                startActivity(intent);
+            } catch (RemoteException ex) {
+            }
+            finish();
+        }
+    };
+
+    @Override
+    public void onPause() {
+        if (MusicUtils.sService != null) {
+            try {
+                // This looks a little weird (when it's not playing, stop playing)
+                // but it is correct. When nothing is playing, it means that this
+                // was paused before a connection was established, in which case
+                // we stop trying to connect/play.
+                // Otherwise, this call to onPause() was a result of the call to
+                // finish() above, and we should let playback continue.
+                if (! MusicUtils.sService.isPlaying()) {
+                    MusicUtils.sService.stop();
+                }
+            } catch (RemoteException ex) {
+            }
+        }
+        unregisterReceiver(mStatusListener);
+        MusicUtils.unbindFromService(this);
+        super.onPause();
+    }
+}
diff --git a/src/com/android/music/TouchInterceptor.java b/src/com/android/music/TouchInterceptor.java
new file mode 100644 (file)
index 0000000..ef5eb2e
--- /dev/null
@@ -0,0 +1,375 @@
+/*
+ * 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.android.music;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.widget.AdapterView;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+public class TouchInterceptor extends ListView {
+    
+    private View mDragView;
+    private WindowManager mWindowManager;
+    private WindowManager.LayoutParams mWindowParams;
+    private int mDragPos;      // which item is being dragged
+    private int mFirstDragPos; // where was the dragged item originally
+    private int mDragPoint;    // at what offset inset the item did the user grab it
+    private int mCoordOffset;  // the difference between screen coordinates and coordinates in this view
+    private DragListener mDragListener;
+    private DropListener mDropListener;
+    private RemoveListener mRemoveListener;
+    private int mUpperBound;
+    private int mLowerBound;
+    private int mHeight;
+    private int mTouchSlop;
+    private int mBackGroundColor;
+    private GestureDetector mGestureDetector;
+    private static final int FLING = 0;
+    private static final int SLIDE = 1;
+    private int mRemoveMode = -1;
+    private Rect mTempRect = new Rect();
+
+    public TouchInterceptor(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        SharedPreferences pref = context.getSharedPreferences("Music", 3);
+        mRemoveMode = pref.getInt("deletemode", -1);
+    }
+    
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (mRemoveListener != null && mGestureDetector == null) {
+            if (mRemoveMode == FLING) {
+                mGestureDetector = new GestureDetector(new SimpleOnGestureListener() {
+                    @Override
+                    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+                            float velocityY) {
+                        if (mDragView != null) {
+                            if (velocityX > 1000) {
+                                Rect r = mTempRect;
+                                mDragView.getDrawingRect(r);
+                                if ( e2.getX() > r.right * 2 / 3) {
+                                    // fast fling right with release near the right edge of the screen
+                                    stopDragging();
+                                    mRemoveListener.remove(mFirstDragPos);
+                                    unExpandViews(true);
+                                }
+                            }
+                            // flinging while dragging should have no effect
+                            return true;
+                        }
+                        return false;
+                    }
+                });
+            }
+        }
+        if (mDragListener != null || mDropListener != null) {
+            switch (ev.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                    int x = (int) ev.getX();
+                    int y = (int) ev.getY();
+                    int itemnum = pointToPosition(x, y);
+                    if (itemnum == AdapterView.INVALID_POSITION) {
+                        break;
+                    }
+                    ViewGroup item = (ViewGroup) getChildAt(itemnum - getFirstVisiblePosition());
+                    mDragPoint = y - item.getTop();
+                    mCoordOffset = ((int)ev.getRawY()) - y;
+                    View dragger = item.findViewById(R.id.icon);
+                    Rect r = mTempRect;
+                    dragger.getDrawingRect(r);
+                    if (x < r.right) {
+                        item.setDrawingCacheEnabled(true);
+                        Bitmap bitmap = item.getDrawingCache();
+                        startDragging(bitmap, y);
+                        mDragPos = itemnum;
+                        mFirstDragPos = mDragPos;
+                        mHeight = getHeight();
+                        mTouchSlop = ViewConfiguration.getTouchSlop();
+                        mUpperBound = Math.min(y - mTouchSlop, mHeight / 3);
+                        mLowerBound = Math.max(y + mTouchSlop, mHeight * 2 /3);
+                        return false;
+                    }
+                    mDragView = null;
+                    break;
+            }
+        }
+        return super.onInterceptTouchEvent(ev);
+    }
+    
+    /*
+     * pointToPosition() doesn't consider invisible views, but we
+     * need to, so implement a slightly different version.
+     */
+    private int myPointToPosition(int x, int y) {
+        Rect frame = mTempRect;
+        final int count = getChildCount();
+        for (int i = count - 1; i >= 0; i--) {
+            final View child = getChildAt(i);
+            child.getHitRect(frame);
+            if (frame.contains(x, y)) {
+                return getFirstVisiblePosition() + i;
+            }
+        }
+        return INVALID_POSITION;
+    }
+    
+    private int getItemForPosition(int y) {
+        int pos = myPointToPosition(0, y - mDragPoint - 32);
+        if (pos >= 0) {
+            if (pos <= mFirstDragPos) {
+                pos += 1;
+            }
+        } else if ((y - mDragPoint) < 0) {
+            pos = 0;
+        }
+        return pos;
+    }
+    
+    private void adjustScrollBounds(int y) {
+        if (y >= mHeight / 3) {
+            mUpperBound = mHeight / 3;
+        }
+        if (y <= mHeight * 2 / 3) {
+            mLowerBound = mHeight * 2 / 3;
+        }
+    }
+
+    /*
+     * Restore size and visibility for all listitems
+     */
+    private void unExpandViews(boolean deletion) {
+        for (int i = 0;; i++) {
+            View v = getChildAt(i);
+            if (v == null) {
+                if (deletion) {
+                    // HACK force update of mItemCount
+                    int position = getFirstVisiblePosition();
+                    int y = getChildAt(0).getTop();
+                    setAdapter(getAdapter());
+                    setSelectionFromTop(position, y);
+                    // end hack
+                }
+                layoutChildren(); // force children to be recreated where needed
+                v = getChildAt(i);
+                if (v == null) {
+                    break;
+                }
+            }
+            ViewGroup.LayoutParams params = v.getLayoutParams();
+            params.height = 64;
+            v.setLayoutParams(params);
+            v.setVisibility(View.VISIBLE);
+        }
+    }
+    
+    /* Adjust visibility and size to make it appear as though
+     * an item is being dragged around and other items are making
+     * room for it:
+     * If dropping the item would result in it still being in the
+     * same place, then make the dragged listitem's size normal,
+     * but make the item invisible.
+     * Otherwise, if the dragged listitem is still on screen, make
+     * it as small as possible and expand the item below the insert
+     * point.
+     * If the dragged item is not on screen, only expand the item
+     * below the current insertpoint.
+     */
+    private void doExpansion() {
+        int childnum = mDragPos - getFirstVisiblePosition();
+        if (mDragPos > mFirstDragPos) {
+            childnum++;
+        }
+        View v = getChildAt(childnum);
+        if (v== null) {
+            return;
+        }
+        View first = getChildAt(mFirstDragPos - getFirstVisiblePosition());
+
+        for (int i = 0;; i++) {
+            View vv = getChildAt(i);
+            if (vv == null) {
+                break;
+            }
+            int height = 64;
+            int visibility = View.VISIBLE;
+            if (vv.equals(first)) {
+                // processing the item that is being dragged
+                if (mDragPos == mFirstDragPos) {
+                    // hovering over the original location
+                    visibility = View.INVISIBLE;
+                } else {
+                    // not hovering over it
+                    height = 1;
+                }
+            } else if (i == (mDragPos - getFirstVisiblePosition() + (mDragPos > mFirstDragPos ? 1 : 0))) {
+                height = 128;
+            }
+            ViewGroup.LayoutParams params = vv.getLayoutParams();
+            params.height = height;
+            vv.setLayoutParams(params);
+            vv.setVisibility(visibility);
+        }
+    }
+    
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (mGestureDetector != null) {
+            mGestureDetector.onTouchEvent(ev);
+        }
+        if ((mDragListener != null || mDropListener != null) && mDragView != null) {
+            switch (ev.getAction()) {
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    Rect r = mTempRect;
+                    mDragView.getDrawingRect(r);
+                    stopDragging();
+                    if (mRemoveMode == SLIDE && ev.getX() > r.right * 3 / 4) {
+                        if (mRemoveListener != null) {
+                            mRemoveListener.remove(mFirstDragPos);
+                        }
+                        unExpandViews(true);
+                    } else {
+                        if (mDropListener != null) {
+                            mDropListener.drop(mFirstDragPos, mDragPos);
+                        }
+                        unExpandViews(false);
+                    }
+                    break;
+                    
+                case MotionEvent.ACTION_MOVE:
+                    int x = (int) ev.getX();
+                    int y = (int) ev.getY();
+                    dragView(x, y);
+                    int itemnum = getItemForPosition(y);
+                    if (itemnum >= 0) {
+                        if (true || itemnum != mDragPos) {
+                            if (mDragListener != null) {
+                                mDragListener.drag(mDragPos, itemnum);
+                            }
+                            mDragPos = itemnum;
+                            doExpansion();
+                        }
+                        int speed = 0;
+                        adjustScrollBounds(y);
+                        if (y > mLowerBound) {
+                            // scroll the list up a bit
+                            speed = y > (mHeight + mLowerBound) / 2 ? 16 : 4;
+                        } else if (y < mUpperBound) {
+                            // scroll the list down a bit
+                            speed = y < mUpperBound / 2 ? -16 : -4;
+                        }
+                        if (speed != 0) {
+                            int ref = pointToPosition(0, mHeight / 2);
+                            if (ref == AdapterView.INVALID_POSITION) {
+                                //we hit a divider or an invisible view, check somewhere else
+                                ref = pointToPosition(0, mHeight / 2 + getDividerHeight() + 64);
+                            }
+                            View v = getChildAt(ref - getFirstVisiblePosition());
+                            if (v!= null) {
+                                int pos = v.getTop();
+                                setSelectionFromTop(ref, pos - speed);
+                            }
+                        }
+                    }
+                    break;
+            }
+            return true;
+        }
+        return super.onTouchEvent(ev);
+    }
+    
+    private void startDragging(Bitmap bm, int y) {
+        mWindowParams = new WindowManager.LayoutParams();
+        mWindowParams.gravity = Gravity.TOP;
+        mWindowParams.x = 0;
+        mWindowParams.y = y - mDragPoint + mCoordOffset;
+
+        mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
+        mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
+        mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+        mWindowParams.format = PixelFormat.TRANSLUCENT;
+        mWindowParams.windowAnimations = 0;
+        
+        ImageView v = new ImageView(mContext);
+        mBackGroundColor = mContext.getResources().getColor(R.color.dragndrop_background);
+        v.setBackgroundColor(mBackGroundColor);
+        v.setImageBitmap(bm);
+        mWindowManager = (WindowManager)mContext.getSystemService("window");
+        mWindowManager.addView(v, mWindowParams);
+        mDragView = v;
+    }
+    
+    private void dragView(int x, int y) {
+        if (mRemoveMode == SLIDE) {
+            float alpha = 1.0f;
+            int width = mDragView.getWidth();
+            if (x > width / 2) {
+                alpha = ((float)(width - x)) / (width / 2);
+            }
+            mWindowParams.alpha = alpha;
+        }
+        mWindowParams.y = y - mDragPoint + mCoordOffset;
+        mWindowManager.updateViewLayout(mDragView, mWindowParams);
+    }
+    
+    private void stopDragging() {
+        WindowManager wm = (WindowManager)mContext.getSystemService("window");
+        wm.removeView(mDragView);
+        mDragView = null;
+    }
+    
+    public void setDragListener(DragListener l) {
+        mDragListener = l;
+    }
+    
+    public void setDropListener(DropListener l) {
+        mDropListener = l;
+    }
+    
+    public void setRemoveListener(RemoveListener l) {
+        mRemoveListener = l;
+    }
+
+    public interface DragListener {
+        void drag(int from, int to);
+    }
+    public interface DropListener {
+        void drop(int from, int to);
+    }
+    public interface RemoveListener {
+        void remove(int which);
+    }
+};
diff --git a/src/com/android/music/TrackBrowserActivity.java b/src/com/android/music/TrackBrowserActivity.java
new file mode 100644 (file)
index 0000000..9484856
--- /dev/null
@@ -0,0 +1,1148 @@
+/*
+ * 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.android.music;
+
+import android.app.ListActivity;
+import android.content.*;
+import android.database.AbstractCursor;
+import android.database.CharArrayBuffer;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.media.MediaFile;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.RemoteException;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Audio.Playlists;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.text.Collator;
+import java.util.Arrays;
+import java.util.Map;
+
+public class TrackBrowserActivity extends ListActivity
+        implements View.OnCreateContextMenuListener, MusicUtils.Defs
+{
+    private final int Q_SELECTED = CHILD_MENU_BASE;
+    private final int Q_ALL = CHILD_MENU_BASE + 1;
+    private final int SAVE_AS_PLAYLIST = CHILD_MENU_BASE + 2;
+    private final int PLAY_ALL = CHILD_MENU_BASE + 3;
+    private final int CLEAR_PLAYLIST = CHILD_MENU_BASE + 4;
+    private final int REMOVE = CHILD_MENU_BASE + 5;
+
+    private final int PLAY_NOW = 0;
+    private final int ADD_TO_QUEUE = 1;
+    private final int PLAY_NEXT = 2;
+    private final int JUMP_TO = 3;
+    private final int CLEAR_HISTORY = 4;
+    private final int CLEAR_ALL = 5;
+
+    private static final String LOGTAG = "TrackBrowser";
+
+    private String[] mCursorCols;
+    private String[] mPlaylistMemberCols;
+    private boolean mDeletedOneRow = false;
+    private boolean mEditMode = false;
+    private String mCurrentTrackName;
+
+    public TrackBrowserActivity()
+    {
+    }
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle icicle)
+    {
+        super.onCreate(icicle);
+        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+        if (icicle != null) {
+            mSelectedId = icicle.getLong("selectedtrack");
+            mAlbumId = icicle.getString("album");
+            mArtistId = icicle.getString("artist");
+            mPlaylist = icicle.getString("playlist");
+            mGenre = icicle.getString("genre");
+            mEditMode = icicle.getBoolean("editmode", false);
+        } else {
+            mAlbumId = getIntent().getStringExtra("album");
+            // If we have an album, show everything on the album, not just stuff
+            // by a particular artist.
+            Intent intent = getIntent();
+            mArtistId = intent.getStringExtra("artist");
+            mPlaylist = intent.getStringExtra("playlist");
+            mGenre = intent.getStringExtra("genre");
+            mEditMode = intent.getAction().equals(Intent.ACTION_EDIT);
+        }
+
+        mCursorCols = new String[] {
+                MediaStore.Audio.Media._ID,
+                MediaStore.Audio.Media.TITLE,
+                MediaStore.Audio.Media.TITLE_KEY,
+                MediaStore.Audio.Media.DATA,
+                MediaStore.Audio.Media.ALBUM,
+                MediaStore.Audio.Media.ARTIST,
+                MediaStore.Audio.Media.ARTIST_ID,
+                MediaStore.Audio.Media.DURATION
+        };
+        mPlaylistMemberCols = new String[] {
+                MediaStore.Audio.Playlists.Members._ID,
+                MediaStore.Audio.Media.TITLE,
+                MediaStore.Audio.Media.TITLE_KEY,
+                MediaStore.Audio.Media.DATA,
+                MediaStore.Audio.Media.ALBUM,
+                MediaStore.Audio.Media.ARTIST,
+                MediaStore.Audio.Media.ARTIST_ID,
+                MediaStore.Audio.Media.DURATION,
+                MediaStore.Audio.Playlists.Members.PLAY_ORDER,
+                MediaStore.Audio.Playlists.Members.AUDIO_ID
+        };
+
+        MusicUtils.bindToService(this);
+
+        IntentFilter f = new IntentFilter();
+        f.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+        f.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
+        f.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+        f.addDataScheme("file");
+        registerReceiver(mScanListener, f);
+
+        init();
+        //Debug.startMethodTracing();
+    }
+
+    @Override
+    public void onDestroy() {
+        //Debug.stopMethodTracing();
+        MusicUtils.unbindFromService(this);
+        try {
+            if ("nowplaying".equals(mPlaylist)) {
+                unregisterReceiver(mNowPlayingListener);
+            } else {
+                unregisterReceiver(mTrackListListener);
+            }
+        } catch (IllegalArgumentException ex) {
+            // we end up here in case we never registered the listeners
+        }
+        if (mTrackCursor != null) {
+            mTrackCursor.close();
+        }
+        unregisterReceiver(mScanListener);
+        super.onDestroy();
+   }
+    
+    @Override
+    public void onResume() {
+        super.onResume();
+        if (mTrackCursor != null) {
+            getListView().invalidateViews();
+        }
+        MusicUtils.setSpinnerState(this);
+    }
+    @Override
+    public void onPause() {
+        mReScanHandler.removeCallbacksAndMessages(null);
+        super.onPause();
+    }
+    private BroadcastReceiver mScanListener = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            MusicUtils.setSpinnerState(TrackBrowserActivity.this);
+            mReScanHandler.sendEmptyMessage(0);
+        }
+    };
+    
+    private Handler mReScanHandler = new Handler() {
+        public void handleMessage(Message msg) {
+            init();
+            if (mTrackCursor == null) {
+                sendEmptyMessageDelayed(0, 1000);
+            }
+        }
+    };
+    
+    public void onSaveInstanceState(Bundle outcicle) {
+        // need to store the selected item so we don't lose it in case
+        // of an orientation switch. Otherwise we could lose it while
+        // in the middle of specifying a playlist to add the item to.
+        outcicle.putLong("selectedtrack", mSelectedId);
+        outcicle.putString("artist", mArtistId);
+        outcicle.putString("album", mAlbumId);
+        outcicle.putString("playlist", mPlaylist);
+        outcicle.putString("genre", mGenre);
+        outcicle.putBoolean("editmode", mEditMode);
+        super.onSaveInstanceState(outcicle);
+    }
+    
+    public void init() {
+
+        mTrackCursor = getTrackCursor(null);
+
+        setContentView(R.layout.media_picker_activity);
+        mTrackList = (ListView) findViewById(android.R.id.list);
+        if (mEditMode) {
+            //((TouchInterceptor) mTrackList).setDragListener(mDragListener);
+            ((TouchInterceptor) mTrackList).setDropListener(mDropListener);
+            ((TouchInterceptor) mTrackList).setRemoveListener(mRemoveListener);
+        }
+
+        if (mTrackCursor == null) {
+            MusicUtils.displayDatabaseError(this);
+            return;
+        }
+
+        int numresults = mTrackCursor.getCount();
+        if (numresults > 0) {
+            mTrackCursor.moveToFirst();
+
+            CharSequence fancyName = null;
+            if (mAlbumId != null) {
+                fancyName = mTrackCursor.getString(mTrackCursor.getColumnIndex(MediaStore.Audio.Media.ALBUM));
+                // For compilation albums show only the album title,
+                // but for regular albums show "artist - album".
+                // To determine whether something is a compilation
+                // album, do a query for the artist + album of the
+                // first item, and see if it returns the same number
+                // of results as the album query.
+                String where = MediaStore.Audio.Media.ALBUM_ID + "='" + mAlbumId +
+                        "' AND " + MediaStore.Audio.Media.ARTIST_ID + "='" + 
+                        mTrackCursor.getString(mTrackCursor.getColumnIndex(MediaStore.Audio.Media.ARTIST_ID)) + "'";
+                Cursor cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                    new String[] {MediaStore.Audio.Media.ALBUM}, where, null, null);
+                if (cursor != null) {
+                    if (cursor.getCount() != numresults) {
+                        // compilation album
+                        fancyName = mTrackCursor.getString(mTrackCursor.getColumnIndex(MediaStore.Audio.Media.ALBUM));
+                    }    
+                    cursor.deactivate();
+                }
+            } else if (mPlaylist != null) {
+                if (mPlaylist.equals("nowplaying")) {
+                    if (MusicUtils.getCurrentShuffleMode() == MediaPlaybackService.SHUFFLE_AUTO) {
+                        fancyName = getText(R.string.partyshuffle_title);
+                    } else {
+                        fancyName = getText(R.string.nowplaying_title);
+                    }
+                } else {
+                    String [] cols = new String [] {
+                    MediaStore.Audio.Playlists.NAME
+                    };
+                    Cursor cursor = MusicUtils.query(this,
+                            ContentUris.withAppendedId(Playlists.EXTERNAL_CONTENT_URI, Long.valueOf(mPlaylist)),
+                            cols, null, null, null);
+                    if (cursor != null) {
+                        if (cursor.getCount() != 0) {
+                            cursor.moveToFirst();
+                            fancyName = cursor.getString(0);
+                        }
+                        cursor.deactivate();
+                    }
+                }
+            } else if (mGenre != null) {
+                String [] cols = new String [] {
+                MediaStore.Audio.Genres.NAME
+                };
+                Cursor cursor = MusicUtils.query(this,
+                        ContentUris.withAppendedId(MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, Long.valueOf(mGenre)),
+                        cols, null, null, null);
+                if (cursor != null) {
+                    if (cursor.getCount() != 0) {
+                        cursor.moveToFirst();
+                        fancyName = cursor.getString(0);
+                    }
+                    cursor.deactivate();
+                }
+            }
+
+            if (fancyName != null) {
+                setTitle(fancyName);
+            } else {
+                setTitle(R.string.tracks_title);
+            }
+        } else {
+            setTitle(R.string.no_tracks_title);
+        }
+
+        TrackListAdapter adapter = new TrackListAdapter(
+                this,
+                mEditMode ? R.layout.edit_track_list_item : R.layout.track_list_item,
+                mTrackCursor,
+                new String[] {},
+                new int[] {},
+                "nowplaying".equals(mPlaylist));
+
+        setListAdapter(adapter);
+        ListView lv = getListView();
+        lv.setOnCreateContextMenuListener(this);
+        lv.setCacheColorHint(0);
+        if (!mEditMode) {
+            lv.setTextFilterEnabled(true);
+        }
+
+        // When showing the queue, position the selection on the currently playing track
+        // Otherwise, position the selection on the first matching artist, if any
+        IntentFilter f = new IntentFilter();
+        f.addAction(MediaPlaybackService.META_CHANGED);
+        f.addAction(MediaPlaybackService.QUEUE_CHANGED);
+        if ("nowplaying".equals(mPlaylist)) {
+            try {
+                int cur = MusicUtils.sService.getQueuePosition();
+                setSelection(cur);
+                registerReceiver(mNowPlayingListener, new IntentFilter(f));
+                mNowPlayingListener.onReceive(this, new Intent(MediaPlaybackService.META_CHANGED));
+            } catch (RemoteException ex) {
+            }
+        } else {
+            String key = getIntent().getStringExtra("artist");
+            if (key != null) {
+                int keyidx = mTrackCursor.getColumnIndex(MediaStore.Audio.Media.ARTIST_ID);
+                mTrackCursor.moveToFirst();
+                while (! mTrackCursor.isAfterLast()) {
+                    String artist = mTrackCursor.getString(keyidx);
+                    if (artist.equals(key)) {
+                        setSelection(mTrackCursor.getPosition());
+                        break;
+                    }
+                    mTrackCursor.moveToNext();
+                }
+            }
+            registerReceiver(mTrackListListener, new IntentFilter(f));
+            mTrackListListener.onReceive(this, new Intent(MediaPlaybackService.META_CHANGED));
+        }
+    }
+    
+    private TouchInterceptor.DragListener mDragListener =
+        new TouchInterceptor.DragListener() {
+        public void drag(int from, int to) {
+            if (mTrackCursor instanceof NowPlayingCursor) {
+                NowPlayingCursor c = (NowPlayingCursor) mTrackCursor;
+                c.moveItem(from, to);
+                ((TrackListAdapter)getListAdapter()).notifyDataSetChanged();
+                getListView().invalidateViews();
+                mDeletedOneRow = true;
+            }
+        }
+    };
+    private TouchInterceptor.DropListener mDropListener =
+        new TouchInterceptor.DropListener() {
+        public void drop(int from, int to) {
+            if (mTrackCursor instanceof NowPlayingCursor) {
+                // update the currently playing list
+                NowPlayingCursor c = (NowPlayingCursor) mTrackCursor;
+                c.moveItem(from, to);
+                ((TrackListAdapter)getListAdapter()).notifyDataSetChanged();
+                getListView().invalidateViews();
+                mDeletedOneRow = true;
+            } else {
+                // update a saved playlist
+                int colidx = mTrackCursor.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER);
+                if (from < to) {
+                    // move the item to somewhere later in the list
+                    mTrackCursor.moveToPosition(to);
+                    int toidx = mTrackCursor.getInt(colidx);
+                    mTrackCursor.moveToPosition(from);
+                    mTrackCursor.updateInt(colidx, toidx);
+                    for (int i = from + 1; i <= to; i++) {
+                        mTrackCursor.moveToPosition(i);
+                        mTrackCursor.updateInt(colidx, i - 1);
+                    }
+                    mTrackCursor.commitUpdates();
+                } else if (from > to) {
+                    // move the item to somewhere earlier in the list
+                    mTrackCursor.moveToPosition(to);
+                    int toidx = mTrackCursor.getInt(colidx);
+                    mTrackCursor.moveToPosition(from);
+                    mTrackCursor.updateInt(colidx, toidx);
+                    for (int i = from - 1; i >= to; i--) {
+                        mTrackCursor.moveToPosition(i);
+                        mTrackCursor.updateInt(colidx, i + 1);
+                    }
+                    mTrackCursor.commitUpdates();
+                }
+            }
+        }
+    };
+    
+    private TouchInterceptor.RemoveListener mRemoveListener =
+        new TouchInterceptor.RemoveListener() {
+        public void remove(int which) {
+            removePlaylistItem(which);
+        }
+    };
+
+    private void removePlaylistItem(int which) {
+        View v = mTrackList.getChildAt(which - mTrackList.getFirstVisiblePosition());
+        try {
+            if (MusicUtils.sService != null
+                    && which != MusicUtils.sService.getQueuePosition()) {
+                mDeletedOneRow = true;
+            }
+        } catch (RemoteException e) {
+            // Service died, so nothing playing.
+            mDeletedOneRow = true;
+        }
+        v.setVisibility(View.GONE);
+        mTrackList.invalidateViews();
+        mTrackCursor.moveToPosition(which);
+        mTrackCursor.deleteRow();
+        mTrackCursor.commitUpdates();
+        v.setVisibility(View.VISIBLE);
+        mTrackList.invalidateViews();
+    }
+    
+    private BroadcastReceiver mTrackListListener = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            getListView().invalidateViews();
+        }
+    };
+
+    private BroadcastReceiver mNowPlayingListener = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(MediaPlaybackService.META_CHANGED)) {
+                getListView().invalidateViews();
+            } else if (intent.getAction().equals(MediaPlaybackService.QUEUE_CHANGED)) {
+                if (mDeletedOneRow) {
+                    // This is the notification for a single row that was
+                    // deleted previously, which is already reflected in
+                    // the UI.
+                    mDeletedOneRow = false;
+                    return;
+                }
+                mTrackCursor.close();
+                mTrackCursor = new NowPlayingCursor(MusicUtils.sService, mCursorCols);
+                if (mTrackCursor.getCount() == 0) {
+                    finish();
+                    return;
+                }
+                ((TrackListAdapter)getListAdapter()).changeCursor(mTrackCursor);
+                getListView().invalidateViews();
+            }
+        }
+    };
+    
+    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
+        menu.add(0, PLAY_SELECTION, 0, R.string.play_selection);
+        SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0, R.string.add_to_playlist);
+        MusicUtils.makePlaylistMenu(this, sub);
+        if (mEditMode) {
+            menu.add(0, REMOVE, 0, R.string.remove_from_playlist);
+        }
+        menu.add(0, USE_AS_RINGTONE, 0, R.string.ringtone_menu);
+        menu.add(0, DELETE_ITEM, 0, R.string.delete_item);
+        AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn;
+        mSelectedPosition =  mi.position;
+        mTrackCursor.moveToPosition(mSelectedPosition);
+        int id_idx = mTrackCursor.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID);
+        if (id_idx < 0 ) {
+            mSelectedId = mi.id;
+        } else {
+            mSelectedId = mTrackCursor.getInt(id_idx);
+        }
+        mCurrentTrackName = mTrackCursor.getString(mTrackCursor.getColumnIndex(MediaStore.Audio.Media.TITLE));
+        menu.setHeaderTitle(mCurrentTrackName);
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case PLAY_SELECTION: {
+                // play the track
+                int position = mSelectedPosition;
+                MusicUtils.playAll(this, mTrackCursor, position);
+                return true;
+            }
+
+            case QUEUE: {
+                int [] list = new int[] { (int) mSelectedId };
+                MusicUtils.addToCurrentPlaylist(this, list);
+                return true;
+            }
+
+            case NEW_PLAYLIST: {
+                Intent intent = new Intent();
+                intent.setClass(this, CreatePlaylist.class);
+                startActivityForResult(intent, NEW_PLAYLIST);
+                return true;
+            }
+
+            case PLAYLIST_SELECTED: {
+                int [] list = new int[] { (int) mSelectedId };
+                int playlist = item.getIntent().getIntExtra("playlist", 0);
+                MusicUtils.addToPlaylist(this, list, playlist);
+                return true;
+            }
+
+            case USE_AS_RINGTONE:
+                // Set the system setting to make this the current ringtone
+                MusicUtils.setRingtone(this, mSelectedId);
+                return true;
+
+            case DELETE_ITEM: {
+                int [] list = new int[1];
+                list[0] = (int) mSelectedId;
+                Bundle b = new Bundle();
+                b.putString("description", mCurrentTrackName);
+                b.putIntArray("items", list);
+                Intent intent = new Intent();
+                intent.setClass(this, DeleteItems.class);
+                intent.putExtras(b);
+                startActivityForResult(intent, -1);
+                return true;
+            }
+            
+            case REMOVE:
+                removePlaylistItem(mSelectedPosition);
+                return true;
+        }
+        return super.onContextItemSelected(item);
+    }
+
+    // In order to use alt-up/down as a shortcut for moving the selected item
+    // in the list, we need to override dispatchKeyEvent, not onKeyDown.
+    // (onKeyDown never sees these events, since they are handled by the list)
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        if (mPlaylist != null && event.getMetaState() != 0 && event.isDown()) {
+            switch (event.getKeyCode()) {
+                case KeyEvent.KEYCODE_DPAD_UP:
+                    moveItem(true);
+                    return true;
+                case KeyEvent.KEYCODE_DPAD_DOWN:
+                    moveItem(false);
+                    return true;
+                case KeyEvent.KEYCODE_DEL:
+                    removeItem();
+                    return true;
+            }
+        }
+
+        return super.dispatchKeyEvent(event);
+    }
+
+    private void removeItem() {
+        int curcount = mTrackCursor.getCount();
+        int curpos = mTrackList.getSelectedItemPosition();
+        if (curcount == 0 || curpos < 0) {
+            return;
+        }
+        
+        if ("nowplaying".equals(mPlaylist)) {
+            // remove track from queue
+
+            // Work around bug 902971. To get quick visual feedback
+            // of the deletion of the item, hide the selected view.
+            try {
+                if (curpos != MusicUtils.sService.getQueuePosition()) {
+                    mDeletedOneRow = true;
+                }
+            } catch (RemoteException ex) {
+            }
+            View v = mTrackList.getSelectedView();
+            v.setVisibility(View.GONE);
+            mTrackList.invalidateViews();
+            mTrackCursor.moveToPosition(curpos);
+            mTrackCursor.deleteRow();
+            mTrackCursor.commitUpdates();
+            v.setVisibility(View.VISIBLE);
+            mTrackList.invalidateViews();
+        } else {
+            // remove track from playlist
+            mTrackCursor.moveToPosition(curpos);
+            mTrackCursor.deleteRow();
+            mTrackCursor.commitUpdates();
+            curcount--;
+            if (curcount == 0) {
+                finish();
+            } else {
+                mTrackList.setSelection(curpos < curcount ? curpos : curcount);
+            }
+        }
+    }
+    
+    private void moveItem(boolean up) {
+        int curcount = mTrackCursor.getCount(); 
+        int curpos = mTrackList.getSelectedItemPosition();
+        if ( (up && curpos < 1) || (!up  && curpos >= curcount - 1)) {
+            return;
+        }
+
+        if (mTrackCursor instanceof NowPlayingCursor) {
+            NowPlayingCursor c = (NowPlayingCursor) mTrackCursor;
+            c.moveItem(curpos, up ? curpos - 1 : curpos + 1);
+            ((TrackListAdapter)getListAdapter()).notifyDataSetChanged();
+            getListView().invalidateViews();
+            mDeletedOneRow = true;
+            if (up) {
+                mTrackList.setSelection(curpos - 1);
+            } else {
+                mTrackList.setSelection(curpos + 1);
+            }
+        } else {
+            int colidx = mTrackCursor.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER);
+            mTrackCursor.moveToPosition(curpos);
+            int currentplayidx = mTrackCursor.getInt(colidx);
+            if (up) {
+                    mTrackCursor.updateInt(colidx, currentplayidx - 1);
+                    mTrackCursor.moveToPrevious();
+            } else {
+                    mTrackCursor.updateInt(colidx, currentplayidx + 1);
+                    mTrackCursor.moveToNext();
+            }
+            mTrackCursor.updateInt(colidx, currentplayidx);
+            mTrackCursor.commitUpdates();
+        }
+    }
+    
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id)
+    {
+        if (mTrackCursor.getCount() == 0) {
+            return;
+        }
+        MusicUtils.playAll(this, mTrackCursor, position);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        /* This activity is used for a number of different browsing modes, and the menu can
+         * be different for each of them:
+         * - all tracks, optionally restricted to an album, artist or playlist
+         * - the list of currently playing songs
+         */
+        super.onCreateOptionsMenu(menu);
+        if (mPlaylist == null) {
+            menu.add(0, PLAY_ALL, 0, R.string.play_all).setIcon(R.drawable.ic_menu_play_clip);
+        }
+        menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(R.drawable.ic_menu_music_library);
+        menu.add(0, GOTO_PLAYBACK, 0, R.string.goto_playback).setIcon(R.drawable.ic_menu_playback)
+                .setVisible(MusicUtils.isMusicLoaded());
+        menu.add(0, SHUFFLE_ALL, 0, R.string.shuffle_all).setIcon(R.drawable.ic_menu_shuffle);
+        if (mPlaylist != null) {
+            menu.add(0, SAVE_AS_PLAYLIST, 0, R.string.save_as_playlist).setIcon(R.drawable.ic_menu_save);
+            if (mPlaylist.equals("nowplaying")) {
+                menu.add(0, CLEAR_PLAYLIST, 0, R.string.clear_playlist).setIcon(R.drawable.ic_menu_clear_playlist);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        Intent intent;
+        Cursor cursor;
+        switch (item.getItemId()) {
+            case PLAY_ALL: {
+                MusicUtils.playAll(this, mTrackCursor);
+                return true;
+            }
+
+            case GOTO_START:
+                intent = new Intent();
+                intent.setClass(this, MusicBrowserActivity.class);
+                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                startActivity(intent);
+                return true;
+
+            case GOTO_PLAYBACK:
+                intent = new Intent("com.android.music.PLAYBACK_VIEWER");
+                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                startActivity(intent);
+                return true;
+                
+            case SHUFFLE_ALL:
+                // Should 'shuffle all' shuffle ALL, or only the tracks shown?
+                cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                        new String [] { MediaStore.Audio.Media._ID}, 
+                        MediaStore.Audio.Media.IS_MUSIC + "=1", null,
+                        MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+                if (cursor != null) {
+                    MusicUtils.shuffleAll(this, cursor);
+                    cursor.close();
+                }
+                return true;
+                
+            case SAVE_AS_PLAYLIST:
+                intent = new Intent();
+                intent.setClass(this, CreatePlaylist.class);
+                startActivityForResult(intent, SAVE_AS_PLAYLIST);
+                return true;
+                
+            case CLEAR_PLAYLIST:
+                // We only clear the current playlist
+                MusicUtils.clearQueue();
+                return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+        switch (requestCode) {
+            case SCAN_DONE:
+                if (resultCode == RESULT_CANCELED) {
+                    finish();
+                } else {
+                    init();
+                }
+                break;
+                
+            case NEW_PLAYLIST:
+                if (resultCode == RESULT_OK) {
+                    Uri uri = Uri.parse(intent.getAction());
+                    if (uri != null) {
+                        int [] list = new int[] { (int) mSelectedId };
+                        MusicUtils.addToPlaylist(this, list, Integer.valueOf(uri.getLastPathSegment()));
+                    }
+                }
+                break;
+
+            case SAVE_AS_PLAYLIST:
+                if (resultCode == RESULT_OK) {
+                    Uri uri = Uri.parse(intent.getAction());
+                    if (uri != null) {
+                        int [] list = MusicUtils.getSongListForCursor(mTrackCursor);
+                        int plid = Integer.parseInt(uri.getLastPathSegment());
+                        MusicUtils.addToPlaylist(this, list, plid);
+                    }
+                }
+                break;
+        }
+    }
+
+    private Cursor getTrackCursor(String filterstring) {
+        Cursor ret = null;
+        mSortOrder = MediaStore.Audio.Media.TITLE_KEY;
+        StringBuilder where = new StringBuilder();
+        where.append(MediaStore.Audio.Media.TITLE + " != ''");
+        
+        // Add in the filtering constraints
+        String [] keywords = null;
+        if (filterstring != null) {
+            String [] searchWords = filterstring.split(" ");
+            keywords = new String[searchWords.length];
+            Collator col = Collator.getInstance();
+            col.setStrength(Collator.PRIMARY);
+            for (int i = 0; i < searchWords.length; i++) {
+                keywords[i] = '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%';
+            }
+            for (int i = 0; i < searchWords.length; i++) {
+                where.append(" AND ");
+                where.append(MediaStore.Audio.Media.ARTIST_KEY + "||");
+                where.append(MediaStore.Audio.Media.ALBUM_KEY + "||");
+                where.append(MediaStore.Audio.Media.TITLE_KEY + " LIKE ?");
+            }
+        }
+        
+        if (mGenre != null) {
+            mSortOrder = MediaStore.Audio.Genres.Members.DEFAULT_SORT_ORDER;
+            ret = MusicUtils.query(this,
+                    MediaStore.Audio.Genres.Members.getContentUri("external", Integer.valueOf(mGenre)),
+                    mCursorCols, where.toString(), keywords, mSortOrder);
+        } else if (mPlaylist != null) {
+            if (mPlaylist.equals("nowplaying")) {
+                if (MusicUtils.sService != null) {
+                    ret = new NowPlayingCursor(MusicUtils.sService, mCursorCols);
+                    if (ret.getCount() == 0) {
+                        finish();
+                    }
+                } else {
+                    // Nothing is playing.
+                }
+            } else {
+                mSortOrder = MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER;
+                ret = MusicUtils.query(this,
+                        MediaStore.Audio.Playlists.Members.getContentUri("external", Long.valueOf(mPlaylist)),
+                        mPlaylistMemberCols, where.toString(), keywords, mSortOrder);
+            }
+        } else {
+            if (mAlbumId != null) {
+                where.append(" AND " + MediaStore.Audio.Media.ALBUM_ID + "='" + mAlbumId + "'");
+                mSortOrder = MediaStore.Audio.Media.TRACK + ", " + mSortOrder;
+            }
+            where.append(" AND " + MediaStore.Audio.Media.IS_MUSIC + "=1");
+            ret = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                mCursorCols, where.toString() , keywords, mSortOrder);
+        }
+        return ret;
+    }
+
+    private class NowPlayingCursor extends AbstractCursor
+    {
+        public NowPlayingCursor(IMediaPlaybackService service, String [] cols)
+        {
+            mCols = cols;
+            mService  = service;
+            makeNowPlayingCursor();
+        }
+        private void makeNowPlayingCursor() {
+            mCurrentPlaylistCursor = null;
+            try {
+                mNowPlaying = mService.getQueue();
+            } catch (RemoteException ex) {
+                mNowPlaying = new int[0];
+            }
+            mSize = mNowPlaying.length;
+            if (mSize == 0) {
+                return;
+            }
+
+            StringBuilder where = new StringBuilder();
+            where.append(MediaStore.Audio.Media._ID + " IN (");
+            for (int i = 0; i < mSize; i++) {
+                where.append(mNowPlaying[i]);
+                if (i < mSize - 1) {
+                    where.append(",");
+                }
+            }
+            where.append(")");
+
+            mCurrentPlaylistCursor = MusicUtils.query(TrackBrowserActivity.this,
+                    MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                    mCols, where.toString(), null, MediaStore.Audio.Media._ID);
+
+            if (mCurrentPlaylistCursor == null) {
+                mSize = 0;
+                return;
+            }
+            
+            int size = mCurrentPlaylistCursor.getCount();
+            mCursorIdxs = new int[size];
+            mCurrentPlaylistCursor.moveToFirst();
+            int colidx = mCurrentPlaylistCursor.getColumnIndex(MediaStore.Audio.Media._ID);
+            for (int i = 0; i < size; i++) {
+                mCursorIdxs[i] = mCurrentPlaylistCursor.getInt(colidx);
+                mCurrentPlaylistCursor.moveToNext();
+            }
+            mCurrentPlaylistCursor.moveToFirst();
+            mCurPos = -1;
+        }
+
+        @Override
+        public int getCount()
+        {
+            return mSize;
+        }
+
+        @Override
+        public boolean onMove(int oldPosition, int newPosition)
+        {
+            if (oldPosition == newPosition)
+                return true;
+            
+            if (mNowPlaying == null || mCursorIdxs == null) {
+                return false;
+            }
+
+            // The cursor doesn't have any duplicates in it, and is not ordered
+            // in queue-order, so we need to figure out where in the cursor we
+            // should be.
+           
+            int newid = mNowPlaying[newPosition];
+            int crsridx = Arrays.binarySearch(mCursorIdxs, newid);
+            mCurrentPlaylistCursor.moveToPosition(crsridx);
+            mCurPos = newPosition;
+            
+            return true;
+        }
+
+        @Override
+        public boolean deleteRow()
+        {
+            try {
+                if (mService.removeTracks((int)mCurPos, (int)mCurPos) == 0) {
+                    return false; // delete failed
+                }
+                int i = (int) mCurPos;
+                mSize--;
+                while (i < mSize) {
+                    mNowPlaying[i] = mNowPlaying[i+1];
+                    i++;
+                }
+                onMove(-1, (int) mCurPos);
+            } catch (RemoteException ex) {
+            }
+            return true;
+        }
+        
+        public void moveItem(int from, int to) {
+            try {
+                mService.moveQueueItem(from, to);
+                mNowPlaying = mService.getQueue();
+                onMove(-1, mCurPos); // update the underlying cursor
+            } catch (RemoteException ex) {
+            }
+        }
+
+        private void dump() {
+            String where = "(";
+            for (int i = 0; i < mSize; i++) {
+                where += mNowPlaying[i];
+                if (i < mSize - 1) {
+                    where += ",";
+                }
+            }
+            where += ")";
+            Log.i("NowPlayingCursor: ", where);
+        }
+
+        @Override
+        public String getString(int column)
+        {
+            try {
+                return mCurrentPlaylistCursor.getString(column);
+            } catch (Exception ex) {
+                onChange(true);
+                return "";
+            }
+        }
+
+        @Override
+        public short getShort(int column)
+        {
+            return mCurrentPlaylistCursor.getShort(column);
+        }
+
+        @Override
+        public int getInt(int column)
+        {
+            try {
+                return mCurrentPlaylistCursor.getInt(column);
+            } catch (Exception ex) {
+                onChange(true);
+                return 0;
+            }
+        }
+
+        @Override
+        public long getLong(int column)
+        {
+            return mCurrentPlaylistCursor.getLong(column);
+        }
+
+        @Override
+        public float getFloat(int column)
+        {
+            return mCurrentPlaylistCursor.getFloat(column);
+        }
+
+        @Override
+        public double getDouble(int column)
+        {
+            return mCurrentPlaylistCursor.getDouble(column);
+        }
+
+        @Override
+        public boolean isNull(int column)
+        {
+            return mCurrentPlaylistCursor.isNull(column);
+        }
+
+        @Override
+        public String[] getColumnNames()
+        {
+            return mCols;
+        }
+        
+        @Override
+        public void deactivate()
+        {
+            if (mCurrentPlaylistCursor != null)
+                mCurrentPlaylistCursor.deactivate();
+        }
+
+        @Override
+        public boolean requery()
+        {
+            makeNowPlayingCursor();
+            return true;
+        }
+
+        private String [] mCols;
+        private Cursor mCurrentPlaylistCursor;     // updated in onMove
+        private int mSize;          // size of the queue
+        private int[] mNowPlaying;
+        private int[] mCursorIdxs;
+        private int mCurPos;
+        private IMediaPlaybackService mService;
+    }
+    
+    class TrackListAdapter extends SimpleCursorAdapter {
+        boolean mIsNowPlaying;
+
+        final int mTitleIdx;
+        final int mArtistIdx;
+        final int mAlbumIdx;
+        final int mDurationIdx;
+        int mAudioIdIdx;
+
+        private final StringBuilder mBuilder = new StringBuilder();
+        private final String mUnknownArtist;
+        private final String mUnknownAlbum;
+
+        class ViewHolder {
+            TextView line1;
+            TextView line2;
+            TextView duration;
+            ImageView play_indicator;
+            CharArrayBuffer buffer1;
+            char [] buffer2;
+        }
+        
+        TrackListAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to,
+                boolean isnowplaying) {
+            super(context, layout, cursor, from, to);
+            mIsNowPlaying = isnowplaying;
+            mUnknownArtist = context.getString(R.string.unknown_artist_name);
+            mUnknownAlbum = context.getString(R.string.unknown_album_name);
+            
+            mTitleIdx = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
+            mArtistIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
+            mAlbumIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM);
+            mDurationIdx = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION);
+            mAudioIdIdx = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID);
+            if (mAudioIdIdx < 0) {
+                mAudioIdIdx = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
+            }
+        }
+
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+            View v = super.newView(context, cursor, parent);
+            ImageView iv = (ImageView) v.findViewById(R.id.icon);
+            if (mEditMode) {
+                iv.setVisibility(View.VISIBLE);
+                iv.setImageResource(R.drawable.ic_mp_move);
+                ViewGroup.LayoutParams p = iv.getLayoutParams();
+                p.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+                p.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+            } else {
+                iv.setVisibility(View.GONE);
+            }
+            
+            ViewHolder vh = new ViewHolder();
+            vh.line1 = (TextView) v.findViewById(R.id.line1);
+            vh.line2 = (TextView) v.findViewById(R.id.line2);
+            vh.duration = (TextView) v.findViewById(R.id.duration);
+            vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator);
+            vh.buffer1 = new CharArrayBuffer(100);
+            vh.buffer2 = new char[200];
+            v.setTag(vh);
+            return v;
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            
+            ViewHolder vh = (ViewHolder) view.getTag();
+            
+            cursor.copyStringToBuffer(mTitleIdx, vh.buffer1);
+            vh.line1.setText(vh.buffer1.data, 0, vh.buffer1.sizeCopied);
+            
+            int secs = cursor.getInt(mDurationIdx) / 1000;
+            if (secs == 0) {
+                vh.duration.setText("");
+            } else {
+                vh.duration.setText(MusicUtils.makeTimeString(context, secs));
+            }
+            
+            final StringBuilder builder = mBuilder;
+            builder.delete(0, builder.length());
+
+            String name = cursor.getString(mAlbumIdx);
+            if (name == null || name.equals(MediaFile.UNKNOWN_STRING)) {
+                builder.append(mUnknownAlbum);
+            } else {
+                builder.append(name);
+            }
+            builder.append('\n');
+            name = cursor.getString(mArtistIdx);
+            if (name == null || name.equals(MediaFile.UNKNOWN_STRING)) {
+                builder.append(mUnknownArtist);
+            } else {
+                builder.append(name);
+            }
+            int len = builder.length();
+            if (vh.buffer2.length < len) {
+                vh.buffer2 = new char[len];
+            }
+            builder.getChars(0, len, vh.buffer2, 0);
+            vh.line2.setText(vh.buffer2, 0, len);
+
+            ImageView iv = vh.play_indicator;
+            int id = -1;
+            if (MusicUtils.sService != null) {
+                // TODO: IPC call on each bind??
+                try {
+                    if (mIsNowPlaying) {
+                        id = MusicUtils.sService.getQueuePosition();
+                    } else {
+                        id = MusicUtils.sService.getAudioId();
+                    }
+                } catch (RemoteException ex) {
+                }
+            }
+            if ( (mIsNowPlaying && cursor.getPosition() == id) ||
+                 (!mIsNowPlaying && cursor.getInt(mAudioIdIdx) == id)) {
+                iv.setImageResource(R.drawable.indicator_ic_mp_playing_list);
+                iv.setVisibility(View.VISIBLE);
+            } else {
+                iv.setVisibility(View.GONE);
+            }
+        }
+        
+        @Override
+        public void changeCursor(Cursor cursor) {
+            super.changeCursor(cursor);
+            mTrackCursor = cursor;
+        }
+        @Override
+        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+            return getTrackCursor(constraint.toString());
+        }
+    }
+
+    private ListView mTrackList;
+    private Cursor mTrackCursor;
+    private String mAlbumId;
+    private String mArtistId;
+    private String mPlaylist;
+    private String mGenre;
+    private String mSortOrder;
+    private int mSelectedPosition;
+    private long mSelectedId;
+}
+
diff --git a/src/com/android/music/VideoBrowserActivity.java b/src/com/android/music/VideoBrowserActivity.java
new file mode 100644 (file)
index 0000000..8e6c752
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * 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.android.music;
+
+import android.app.ListActivity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+
+import java.lang.Integer;
+
+public class VideoBrowserActivity extends ListActivity implements MusicUtils.Defs
+{
+    public VideoBrowserActivity()
+    {
+    }
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle icicle)
+    {
+        super.onCreate(icicle);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+        init();
+    }
+
+    public void init() {
+
+        // Set the layout for this activity.  You can find it
+        // in assets/res/any/layout/media_picker_activity.xml
+        setContentView(R.layout.media_picker_activity);
+
+        mTrackList = (ListView) findViewById(android.R.id.list);
+
+        MakeCursor();
+
+        if (mCursor == null) {
+            MusicUtils.displayDatabaseError(this);
+            return;
+        }
+
+        if (mCursor.getCount() > 0) {
+            setTitle(R.string.videos_title);
+        } else {
+            setTitle(R.string.no_videos_title);
+        }
+
+        // Map Cursor columns to views defined in media_list_item.xml
+        SimpleCursorAdapter adapter = new SimpleCursorAdapter(
+                this,
+                android.R.layout.simple_list_item_1,
+                mCursor,
+                new String[] { MediaStore.Video.Media.TITLE},
+                new int[] { android.R.id.text1 });
+
+        setListAdapter(adapter);
+    }
+
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id)
+    {
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        mCursor.moveToPosition(position);
+        String type = mCursor.getString(mCursor.getColumnIndex(MediaStore.Video.Media.MIME_TYPE));
+        intent.setDataAndType(ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id), type);
+        
+        startActivity(intent);
+    }
+
+    private void MakeCursor() {
+        String[] cols = new String[] {
+                MediaStore.Video.Media._ID,
+                MediaStore.Video.Media.TITLE,
+                MediaStore.Video.Media.DATA,
+                MediaStore.Video.Media.MIME_TYPE,
+                MediaStore.Video.Media.ARTIST
+        };
+        ContentResolver resolver = getContentResolver();
+        if (resolver == null) {
+            System.out.println("resolver = null");
+        } else {
+            mSortOrder = MediaStore.Video.Media.TITLE + " COLLATE UNICODE";
+            mWhereClause = MediaStore.Video.Media.TITLE + " != ''";
+            mCursor = resolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+                cols, mWhereClause , null, mSortOrder);
+        }
+    }
+
+    private ListView mTrackList;
+    private Cursor mCursor;
+    private String mWhereClause;
+    private String mSortOrder;
+}
+
diff --git a/src/com/android/music/WeekSelector.java b/src/com/android/music/WeekSelector.java
new file mode 100644 (file)
index 0000000..59a6921
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * 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.android.music;
+
+import com.android.internal.widget.VerticalTextSpinner;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+public class WeekSelector extends Activity
+{
+    VerticalTextSpinner mWeeks;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.weekpicker);
+        getWindow().setLayout(WindowManager.LayoutParams.FILL_PARENT,
+                                    WindowManager.LayoutParams.WRAP_CONTENT);
+
+        mWeeks = (VerticalTextSpinner)findViewById(R.id.weeks);
+        mWeeks.setItems(getResources().getStringArray(R.array.weeklist));
+        mWeeks.setWrapAround(false);
+        mWeeks.setScrollInterval(200);
+        
+        int def = MusicUtils.getIntPref(this, "numweeks", 2); 
+        int pos = icicle != null ? icicle.getInt("numweeks", def - 1) : def - 1;
+        mWeeks.setSelectedPos(pos);
+        
+        ((Button) findViewById(R.id.set)).setOnClickListener(mListener);
+    }
+    
+    @Override
+    public void onSaveInstanceState(Bundle outcicle) {
+        outcicle.putInt("numweeks", mWeeks.getCurrentSelectedPos());
+    }
+    
+    @Override
+    public void onResume() {
+        super.onResume();
+    }
+    
+    private View.OnClickListener mListener = new View.OnClickListener() {
+        public void onClick(View v) {
+            int numweeks = mWeeks.getCurrentSelectedPos() + 1;
+            MusicUtils.setIntPref(WeekSelector.this, "numweeks", numweeks);
+            setResult(RESULT_OK);
+            finish();
+        }
+    };
+}