auto import from //depot/cupcake/@135843
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..93feb9d
--- /dev/null
+++ b/Android.mk
@@ -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
index 0000000..a445f16
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,227 @@
+<?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">
+ <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>
+ <meta-data
+ android:name="android.app.default_searchable"
+ android:value=".QueryBrowserActivity"
+ />
+ </activity>
+ <receiver android:name="MediaButtonIntentReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.MEDIA_BUTTON" />
+ <action android:name="android.media.AUDIO_BECOMING_NOISY" />
+ </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"/>
+ <data android:mimeType="audio/mp4a-latm"/>
+ </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>
+ <meta-data
+ android:name="android.app.searchable"
+ android:resource="@xml/searchable"
+ />
+ </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="MusicPicker" android:label="@string/music_picker_title">
+ <!-- First way to invoke us: someone asks to get content of
+ any of the audio types we support. -->
+ <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="audio/*"/>
+ <data android:mimeType="application/ogg"/>
+ <data android:mimeType="application/x-ogg"/>
+ </intent-filter>
+ <!-- Second way to invoke us: someone asks to pick an item from
+ some media Uri. -->
+ <intent-filter>
+ <action android:name="android.intent.action.PICK" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.OPENABLE" />
+ <data android:mimeType="vnd.android.cursor.dir/audio"/>
+ </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" />
+ <service android:name="MediaPlaybackService" android:exported="true" />
+
+ <receiver android:name="MediaGadgetProvider">
+ <intent-filter>
+ <action android:name="android.gadget.action.GADGET_UPDATE" />
+ </intent-filter>
+ <meta-data android:name="android.gadget.provider" android:resource="@xml/gadget_info" />
+ </receiver>
+ </application>
+</manifest>
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/NOTICE b/NOTICE
new file mode 100644
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/album_border_large.9.png b/res/drawable-finger/album_border_large.9.png
new file mode 100644
index 0000000..e5ffbc7
--- /dev/null
+++ b/res/drawable-finger/album_border_large.9.png
Binary files differ
diff --git a/res/drawable-finger/albumart_mp_unknown.png b/res/drawable-finger/albumart_mp_unknown.png
new file mode 100755
index 0000000..f014a45
--- /dev/null
+++ b/res/drawable-finger/albumart_mp_unknown.png
Binary files 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
index 0000000..ac11762
--- /dev/null
+++ b/res/drawable-finger/albumart_mp_unknown_list.png
Binary files differ
diff --git a/res/drawable-finger/btn_music_highlight.9.png b/res/drawable-finger/btn_music_highlight.9.png
new file mode 100644
index 0000000..0e18a9e
--- /dev/null
+++ b/res/drawable-finger/btn_music_highlight.9.png
Binary files differ
diff --git a/res/drawable-finger/btn_music_normal.9.png b/res/drawable-finger/btn_music_normal.9.png
new file mode 100644
index 0000000..10d2a96
--- /dev/null
+++ b/res/drawable-finger/btn_music_normal.9.png
Binary files differ
diff --git a/res/drawable-finger/btn_music_pressed.9.png b/res/drawable-finger/btn_music_pressed.9.png
new file mode 100644
index 0000000..ef2caa6
--- /dev/null
+++ b/res/drawable-finger/btn_music_pressed.9.png
Binary files differ
diff --git a/res/drawable-finger/gadget_bg.9.png b/res/drawable-finger/gadget_bg.9.png
new file mode 100644
index 0000000..8930c08
--- /dev/null
+++ b/res/drawable-finger/gadget_bg.9.png
Binary files differ
diff --git a/res/drawable-finger/gadget_inner.9.png b/res/drawable-finger/gadget_inner.9.png
new file mode 100644
index 0000000..157afcf
--- /dev/null
+++ b/res/drawable-finger/gadget_inner.9.png
Binary files differ
diff --git a/res/drawable-finger/gadget_next.xml b/res/drawable-finger/gadget_next.xml
new file mode 100644
index 0000000..7066e1d
--- /dev/null
+++ b/res/drawable-finger/gadget_next.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_window_focused="false"
+ android:drawable="@drawable/gadget_next_normal" />
+ <item android:state_pressed="true"
+ android:drawable="@drawable/gadget_next_pressed" />
+ <item android:state_focused="true"
+ android:drawable="@drawable/gadget_next_pressed" />
+ <item
+ android:drawable="@drawable/gadget_next_normal" />
+</selector>
diff --git a/res/drawable-finger/gadget_next_normal.png b/res/drawable-finger/gadget_next_normal.png
new file mode 100644
index 0000000..072499e
--- /dev/null
+++ b/res/drawable-finger/gadget_next_normal.png
Binary files differ
diff --git a/res/drawable-finger/gadget_next_pressed.png b/res/drawable-finger/gadget_next_pressed.png
new file mode 100644
index 0000000..e8ae5d9
--- /dev/null
+++ b/res/drawable-finger/gadget_next_pressed.png
Binary files differ
diff --git a/res/drawable-finger/gadget_pause.xml b/res/drawable-finger/gadget_pause.xml
new file mode 100644
index 0000000..42d47cb
--- /dev/null
+++ b/res/drawable-finger/gadget_pause.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_window_focused="false"
+ android:drawable="@drawable/gadget_pause_normal" />
+ <item android:state_pressed="true"
+ android:drawable="@drawable/gadget_pause_pressed" />
+ <item android:state_focused="true"
+ android:drawable="@drawable/gadget_pause_pressed" />
+ <item
+ android:drawable="@drawable/gadget_pause_normal" />
+</selector>
diff --git a/res/drawable-finger/gadget_pause_normal.png b/res/drawable-finger/gadget_pause_normal.png
new file mode 100644
index 0000000..a40e247
--- /dev/null
+++ b/res/drawable-finger/gadget_pause_normal.png
Binary files differ
diff --git a/res/drawable-finger/gadget_pause_pressed.png b/res/drawable-finger/gadget_pause_pressed.png
new file mode 100644
index 0000000..2a5116c
--- /dev/null
+++ b/res/drawable-finger/gadget_pause_pressed.png
Binary files differ
diff --git a/res/drawable-finger/gadget_play.xml b/res/drawable-finger/gadget_play.xml
new file mode 100644
index 0000000..4b330e9
--- /dev/null
+++ b/res/drawable-finger/gadget_play.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_window_focused="false"
+ android:drawable="@drawable/gadget_play_normal" />
+ <item android:state_pressed="true"
+ android:drawable="@drawable/gadget_play_pressed" />
+ <item android:state_focused="true"
+ android:drawable="@drawable/gadget_play_pressed" />
+ <item
+ android:drawable="@drawable/gadget_play_normal" />
+</selector>
diff --git a/res/drawable-finger/gadget_play_normal.png b/res/drawable-finger/gadget_play_normal.png
new file mode 100644
index 0000000..f812874
--- /dev/null
+++ b/res/drawable-finger/gadget_play_normal.png
Binary files differ
diff --git a/res/drawable-finger/gadget_play_pressed.png b/res/drawable-finger/gadget_play_pressed.png
new file mode 100644
index 0000000..747b5ae
--- /dev/null
+++ b/res/drawable-finger/gadget_play_pressed.png
Binary files differ
diff --git a/res/drawable-finger/ic_menu_delete.png b/res/drawable-finger/ic_menu_delete.png
new file mode 100755
index 0000000..82f9d56
--- /dev/null
+++ b/res/drawable-finger/ic_menu_delete.png
Binary files 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
index 0000000..3f1de60
--- /dev/null
+++ b/res/drawable-finger/ic_menu_music_library.png
Binary files 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
index 0000000..51c3d02
--- /dev/null
+++ b/res/drawable-finger/ic_menu_party_shuffle.png
Binary files differ
diff --git a/res/drawable-finger/ic_menu_playback.png b/res/drawable-finger/ic_menu_playback.png
new file mode 100755
index 0000000..22f203a
--- /dev/null
+++ b/res/drawable-finger/ic_menu_playback.png
Binary files 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
index 0000000..d44d7bd
--- /dev/null
+++ b/res/drawable-finger/ic_menu_set_as_ringtone.png
Binary files differ
diff --git a/res/drawable-finger/ic_menu_shuffle.png b/res/drawable-finger/ic_menu_shuffle.png
new file mode 100755
index 0000000..cb7009d
--- /dev/null
+++ b/res/drawable-finger/ic_menu_shuffle.png
Binary files 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
index 0000000..5084260
--- /dev/null
+++ b/res/drawable-finger/ic_mp_album_playback.png
Binary files differ
diff --git a/res/drawable-finger/ic_mp_artist_list.png b/res/drawable-finger/ic_mp_artist_list.png
new file mode 100644
index 0000000..08cd205
--- /dev/null
+++ b/res/drawable-finger/ic_mp_artist_list.png
Binary files 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
index 0000000..401d7ce
--- /dev/null
+++ b/res/drawable-finger/ic_mp_artist_playback.png
Binary files 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
index 0000000..35449b3
--- /dev/null
+++ b/res/drawable-finger/ic_mp_current_playlist_btn.png
Binary files differ
diff --git a/res/drawable-finger/ic_mp_move.png b/res/drawable-finger/ic_mp_move.png
new file mode 100755
index 0000000..9169ae5
--- /dev/null
+++ b/res/drawable-finger/ic_mp_move.png
Binary files 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
index 0000000..18f09b1
--- /dev/null
+++ b/res/drawable-finger/ic_mp_partyshuffle_on_btn.png
Binary files 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
index 0000000..1fba256
--- /dev/null
+++ b/res/drawable-finger/ic_mp_playlist_list.png
Binary files differ
diff --git a/res/drawable-finger/ic_mp_playlist_recently_added_list.png b/res/drawable-finger/ic_mp_playlist_recently_added_list.png
new file mode 100644
index 0000000..bc2cb79
--- /dev/null
+++ b/res/drawable-finger/ic_mp_playlist_recently_added_list.png
Binary files 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
index 0000000..d1f1e60
--- /dev/null
+++ b/res/drawable-finger/ic_mp_repeat_all_btn.png
Binary files 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
index 0000000..02fbd81
--- /dev/null
+++ b/res/drawable-finger/ic_mp_repeat_off_btn.png
Binary files 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
index 0000000..549da31
--- /dev/null
+++ b/res/drawable-finger/ic_mp_repeat_once_btn.png
Binary files 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
index 0000000..4804ac0
--- /dev/null
+++ b/res/drawable-finger/ic_mp_screen_albums.png
Binary files 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
index 0000000..1803880
--- /dev/null
+++ b/res/drawable-finger/ic_mp_screen_artists.png
Binary files 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
index 0000000..068c5d1
--- /dev/null
+++ b/res/drawable-finger/ic_mp_screen_playlists.png
Binary files 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
index 0000000..f9c3836
--- /dev/null
+++ b/res/drawable-finger/ic_mp_screen_tracks.png
Binary files 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
index 0000000..90e5081
--- /dev/null
+++ b/res/drawable-finger/ic_mp_sd_card.png
Binary files 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
index 0000000..635fac0
--- /dev/null
+++ b/res/drawable-finger/ic_mp_shuffle_off_btn.png
Binary files 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
index 0000000..92bd59d
--- /dev/null
+++ b/res/drawable-finger/ic_mp_shuffle_on_btn.png
Binary files differ
diff --git a/res/drawable-finger/ic_mp_song_list.png b/res/drawable-finger/ic_mp_song_list.png
new file mode 100644
index 0000000..804a2ee
--- /dev/null
+++ b/res/drawable-finger/ic_mp_song_list.png
Binary files 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
index 0000000..4fe03a5
--- /dev/null
+++ b/res/drawable-finger/ic_mp_song_playback.png
Binary files 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
index 0000000..069cb15
--- /dev/null
+++ b/res/drawable-finger/ic_search_category_music_album.png
Binary files 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
index 0000000..78053d3
--- /dev/null
+++ b/res/drawable-finger/ic_search_category_music_artist.png
Binary files 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
index 0000000..b3988c0
--- /dev/null
+++ b/res/drawable-finger/ic_search_category_music_song.png
Binary files differ
diff --git a/res/drawable-finger/ic_slide_keyboard.png b/res/drawable-finger/ic_slide_keyboard.png
new file mode 100755
index 0000000..38a7dbf
--- /dev/null
+++ b/res/drawable-finger/ic_slide_keyboard.png
Binary files differ
diff --git a/res/drawable-finger/indicator_ic_mp_playing_large.png b/res/drawable-finger/indicator_ic_mp_playing_large.png
new file mode 100644
index 0000000..c95888d
--- /dev/null
+++ b/res/drawable-finger/indicator_ic_mp_playing_large.png
Binary files 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
index 0000000..f32e42c
--- /dev/null
+++ b/res/drawable-finger/indicator_ic_mp_playing_list.png
Binary files differ
diff --git a/res/drawable-finger/list_selector.xml b/res/drawable-finger/list_selector.xml
new file mode 100644
index 0000000..d8afcb6
--- /dev/null
+++ b/res/drawable-finger/list_selector.xml
@@ -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
index 0000000..370bc59
--- /dev/null
+++ b/res/drawable-finger/main_menu_button.xml
@@ -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_music_pressed" />
+ <item android:state_focused="true" android:state_pressed="false" android:drawable="@drawable/btn_music_highlight" />
+ <item android:drawable="@drawable/btn_music_normal" />
+</selector>
+
diff --git a/res/drawable-finger/stat_notify_musicplayer.png b/res/drawable-finger/stat_notify_musicplayer.png
new file mode 100755
index 0000000..fd92c18
--- /dev/null
+++ b/res/drawable-finger/stat_notify_musicplayer.png
Binary files 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
index 0000000..ed6c969
--- /dev/null
+++ b/res/drawable-land-finger/albumart_mp_unknown.png
Binary files differ
diff --git a/res/drawable-land-finger/btn_music_highlight.9.png b/res/drawable-land-finger/btn_music_highlight.9.png
new file mode 100644
index 0000000..0e5a270
--- /dev/null
+++ b/res/drawable-land-finger/btn_music_highlight.9.png
Binary files differ
diff --git a/res/drawable-land-finger/btn_music_normal.9.png b/res/drawable-land-finger/btn_music_normal.9.png
new file mode 100644
index 0000000..80e2263
--- /dev/null
+++ b/res/drawable-land-finger/btn_music_normal.9.png
Binary files differ
diff --git a/res/drawable-land-finger/btn_music_pressed.9.png b/res/drawable-land-finger/btn_music_pressed.9.png
new file mode 100644
index 0000000..3592781
--- /dev/null
+++ b/res/drawable-land-finger/btn_music_pressed.9.png
Binary files differ
diff --git a/res/drawable/app_music.png b/res/drawable/app_music.png
new file mode 100644
index 0000000..0353b91
--- /dev/null
+++ b/res/drawable/app_music.png
Binary files differ
diff --git a/res/drawable/app_video.png b/res/drawable/app_video.png
new file mode 100644
index 0000000..0c10731
--- /dev/null
+++ b/res/drawable/app_video.png
Binary files differ
diff --git a/res/drawable/midi.png b/res/drawable/midi.png
new file mode 100644
index 0000000..87f32ac
--- /dev/null
+++ b/res/drawable/midi.png
Binary files differ
diff --git a/res/drawable/movie.png b/res/drawable/movie.png
new file mode 100644
index 0000000..bed7c78
--- /dev/null
+++ b/res/drawable/movie.png
Binary files differ
diff --git a/res/layout-finger/audio_player.xml b/res/layout-finger/audio_player.xml
new file mode 100644
index 0000000..da11163
--- /dev/null
+++ b/res/layout-finger/audio_player.xml
@@ -0,0 +1,143 @@
+<?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">
+
+ <ImageView
+ android:id="@+id/album"
+ android:background="@drawable/album_border_large"
+ android:layout_width="220dip"
+ android:layout_height="220dip"
+ android:layout_marginLeft="4dip"
+ android:layout_marginRight="2dip"
+ android:layout_marginTop="8dip" />
+
+ <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="85dip"
+ android:layout_height="54dip"
+ android:layout_marginTop="14dip" />
+
+ <ImageButton android:id="@+id/shuffle"
+ android:layout_width="85dip"
+ android:layout_height="54dip"
+ android:layout_marginTop="20dip" />
+
+ <ImageButton android:id="@+id/repeat"
+ android:layout_width="85dip"
+ android:layout_height="54dip"
+ android:layout_marginTop="20dip" />
+
+ </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="fill_parent"
+ 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="fill_parent"
+ 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="fill_parent"
+ 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
index 0000000..95320d9
--- /dev/null
+++ b/res/layout-finger/audio_player_common.xml
@@ -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
index 0000000..be8d7ec
--- /dev/null
+++ b/res/layout-finger/confirm_delete.xml
@@ -0,0 +1,51 @@
+<?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:layout_marginLeft="8dip"
+ android:layout_marginTop="8dip"
+ android:layout_marginBottom="8dip"
+ android:drawableLeft="@android: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
index 0000000..4b5c02f
--- /dev/null
+++ b/res/layout-finger/edit_track_list_item.xml
@@ -0,0 +1,41 @@
+<?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="center_vertical"
+ android:ignoreGravity="@+id/icon">
+
+ <include layout="@layout/track_list_item_common" />
+
+ </RelativeLayout>
+
+</LinearLayout>
diff --git a/res/layout-finger/gadget.xml b/res/layout-finger/gadget.xml
new file mode 100644
index 0000000..0786d06
--- /dev/null
+++ b/res/layout-finger/gadget.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/album_gadget"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ >
+
+ <FrameLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="9dip"
+ android:layout_marginRight="10dip"
+ android:background="@drawable/gadget_bg"
+ >
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:background="@drawable/gadget_inner"
+ >
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:gravity="bottom|left"
+ android:textColor="@color/gadget_text"
+ android:textStyle="bold"
+ android:textSize="11sp"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ />
+
+ <TextView
+ android:id="@+id/artist"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:gravity="top|left"
+ android:textColor="@color/gadget_text"
+ android:textSize="11dip"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ />
+
+ </LinearLayout>
+
+ </FrameLayout>
+
+ <ImageButton
+ android:id="@+id/control_next"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:paddingRight="25dip"
+ android:paddingLeft="10dip"
+ android:src="@drawable/gadget_next"
+ android:background="@android:color/transparent"
+ />
+
+ <ImageButton
+ android:id="@+id/control_play"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentBottom="true"
+ android:paddingLeft="16dip"
+ android:paddingRight="10dip"
+ android:paddingBottom="3dip"
+ android:paddingTop="10dip"
+ android:src="@drawable/gadget_play"
+ android:background="@android:color/transparent"
+ />
+
+</RelativeLayout>
diff --git a/res/layout-finger/music_library.xml b/res/layout-finger/music_library.xml
new file mode 100644
index 0000000..25737aa
--- /dev/null
+++ b/res/layout-finger/music_library.xml
@@ -0,0 +1,165 @@
+<?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="35dip"
+ android:paddingLeft="18dip"
+ android:paddingRight="18dip"
+ android:orientation="horizontal"
+ >
+
+ <LinearLayout
+ android:layout_weight="1"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ android:layout_marginRight="18dip"
+ >
+
+ <Button
+ android:id="@+id/browse_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/main_menu_button"
+ android:drawableTop="@drawable/ic_mp_screen_artists"
+ android:text="@string/browse_menu"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorPrimaryDisableOnly"
+ android:singleLine="true"
+ android:layout_marginBottom="35dip"
+ />
+
+ <Button
+ android:id="@+id/tracks_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/main_menu_button"
+ android:drawableTop="@drawable/ic_mp_screen_tracks"
+ android:text="@string/tracks_menu"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorPrimaryDisableOnly"
+ android:singleLine="true"
+ android:layout_marginBottom="35dip"
+ />
+
+ </LinearLayout>
+
+
+ <LinearLayout
+ android:layout_weight="1"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ >
+
+ <Button
+ android:id="@+id/albums_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/main_menu_button"
+ android:drawableTop="@drawable/ic_mp_screen_albums"
+ android:text="@string/albums_menu"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorPrimaryDisableOnly"
+ android:singleLine="true"
+ android:layout_marginBottom="35dip"
+ />
+
+ <Button
+ android:id="@+id/playlists_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/main_menu_button"
+ android:drawableTop="@drawable/ic_mp_screen_playlists"
+ android:text="@string/playlists_menu"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorPrimaryDisableOnly"
+ android:singleLine="true"
+ android:layout_marginBottom="35dip"
+ />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/nowplaying"
+ android:layout_width="fill_parent"
+ android:layout_height="64dip"
+ android:focusable="true"
+ android:visibility="invisible"
+ android:orientation="vertical">
+
+ <ImageView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:drawable/divider_horizontal_dark" />
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal"
+ android:layout_marginLeft="18dip"
+ android:layout_marginRight="18dip">
+
+ <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/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ />
+ <TextView
+ android:id="@+id/artist"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ />
+ </LinearLayout>
+
+ <ImageView android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:background="@drawable/indicator_ic_mp_playing_large"
+ />
+
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/res/layout-finger/streamstarter.xml b/res/layout-finger/streamstarter.xml
new file mode 100644
index 0000000..025d889
--- /dev/null
+++ b/res/layout-finger/streamstarter.xml
@@ -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
index 0000000..aa2cd5d
--- /dev/null
+++ b/res/layout-finger/track_list_item.xml
@@ -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
index 0000000..5c1a07b
--- /dev/null
+++ b/res/layout-finger/track_list_item_child.xml
@@ -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.
+*/
+-->
+<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:gravity="center_vertical"
+ android:ignoreGravity="@+id/icon">
+
+ <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
index 0000000..4ba4a9a
--- /dev/null
+++ b/res/layout-finger/track_list_item_common.xml
@@ -0,0 +1,75 @@
+<?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">
+
+ <!-- icon is used for albumart, the grabber in edit playlist mode, and the playlist icon in the list of playlists -->
+ <ImageView android:id="@+id/icon"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentBottom="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <TextView android:id="@+id/duration"
+ android:textSize="12sp"
+ android:textColor="?android:attr/textColorTertiary"
+ android:textStyle="bold"
+ android:paddingLeft="4dip"
+ android:paddingRight="11dip"
+ android:layout_alignParentRight="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@+id/line1"
+ android:singleLine="true" />
+
+ <!-- The height is set to half the height of the parent, which is 64 dip -->
+ <TextView android:id="@+id/line1"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="wrap_content"
+ android:paddingLeft="9dip"
+ android:layout_height="wrap_content"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_toRightOf="@id/icon"
+ android:layout_toLeftOf="@id/duration"
+ android:ellipsize="marquee"
+ android:singleLine="true" />
+
+ <!-- The height is set to half the height of the parent, which is 64 dip -->
+ <TextView android:id="@+id/line2" android:visibility="visible"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:paddingLeft="9dip"
+ android:scrollHorizontally="true"
+ 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_below="@id/duration"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="12dip" />
+
+</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
index 0000000..8c6f500
--- /dev/null
+++ b/res/layout-finger/track_list_item_group.xml
@@ -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="47dip">
+
+ <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
index 0000000..00ced6f
--- /dev/null
+++ b/res/layout-finger/weekpicker.xml
@@ -0,0 +1,68 @@
+<?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_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-keysexposed/create_playlist.xml b/res/layout-keysexposed/create_playlist.xml
new file mode 100644
index 0000000..1bf2252
--- /dev/null
+++ b/res/layout-keysexposed/create_playlist.xml
@@ -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
index 0000000..bb5c6ac
--- /dev/null
+++ b/res/layout-keyshidden/create_playlist.xml
@@ -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
index 0000000..6d68f4d
--- /dev/null
+++ b/res/layout-land-finger/audio_player.xml
@@ -0,0 +1,150 @@
+<?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">
+
+ <ImageView
+ android:id="@+id/album"
+ android:background="@drawable/album_border_large"
+ android:layout_width="183dip"
+ android:layout_height="183dip"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="15dip"
+ android:layout_marginTop="12dip" />
+
+ <!-- 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="20dip" >
+
+ <ImageButton android:id="@+id/curplaylist"
+ android:src="@drawable/ic_mp_current_playlist_btn"
+ android:layout_width="82dip"
+ android:layout_height="45dip"
+ android:layout_marginRight="8dip" />
+
+ <ImageButton android:id="@+id/shuffle"
+ android:layout_width="82dip"
+ android:layout_height="45dip"
+ android:layout_marginRight="8dip" />
+
+ <ImageButton android:id="@+id/repeat"
+ android:layout_width="82dip"
+ android:layout_height="45dip" />
+
+ </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:singleLine="true"
+ android:ellipsize="end"
+ android:textStyle="bold"
+ android:layout_gravity="center_vertical"
+ android:layout_width="fill_parent"
+ 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:singleLine="true"
+ android:ellipsize="end"
+ android:textStyle="bold"
+ android:layout_gravity="center_vertical"
+ android:layout_width="fill_parent"
+ 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:singleLine="true"
+ android:ellipsize="end"
+ android:textStyle="bold"
+ android:layout_gravity="center_vertical"
+ android:layout_width="fill_parent"
+ 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
index 0000000..2f3019f
--- /dev/null
+++ b/res/layout-land-finger/music_library.xml
@@ -0,0 +1,148 @@
+<?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="37dip"
+ android:paddingLeft="12dip"
+ android:paddingRight="12dip"
+ android:orientation="horizontal"
+ >
+
+ <Button
+ android:id="@+id/browse_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/main_menu_button"
+ android:drawableTop="@drawable/ic_mp_screen_artists"
+ android:text="@string/browse_menu"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorPrimaryDisableOnly"
+ android:singleLine="true"
+ android:layout_marginBottom="35dip"
+ android:layout_marginRight="10dip"
+ />
+
+ <Button
+ android:id="@+id/albums_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/main_menu_button"
+ android:drawableTop="@drawable/ic_mp_screen_albums"
+ android:text="@string/albums_menu"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorPrimaryDisableOnly"
+ android:singleLine="true"
+ android:layout_marginBottom="35dip"
+ android:layout_marginRight="10dip"
+ />
+
+ <Button
+ android:id="@+id/tracks_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/main_menu_button"
+ android:drawableTop="@drawable/ic_mp_screen_tracks"
+ android:text="@string/tracks_menu"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorPrimaryDisableOnly"
+ android:singleLine="true"
+ android:layout_marginBottom="35dip"
+ android:layout_marginRight="10dip"
+ />
+
+ <Button
+ android:id="@+id/playlists_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/main_menu_button"
+ android:drawableTop="@drawable/ic_mp_screen_playlists"
+ android:text="@string/playlists_menu"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?android:attr/textColorPrimaryDisableOnly"
+ android:singleLine="true"
+ android:layout_marginBottom="35dip"
+ />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/nowplaying"
+ android:layout_width="fill_parent"
+ android:layout_height="64dip"
+ android:focusable="true"
+ android:visibility="invisible"
+ android:orientation="vertical">
+
+ <ImageView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:drawable/divider_horizontal_dark" />
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal"
+ android:layout_marginLeft="12dip"
+ android:layout_marginRight="12dip">
+
+ <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/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ />
+ <TextView
+ android:id="@+id/artist"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ />
+ </LinearLayout>
+
+ <ImageView android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:background="@drawable/indicator_ic_mp_playing_large"
+ />
+
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/res/layout/media_picker_activity.xml b/res/layout/media_picker_activity.xml
new file mode 100644
index 0000000..79f080e
--- /dev/null
+++ b/res/layout/media_picker_activity.xml
@@ -0,0 +1,33 @@
+<?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"
+ android:gravity="center_vertical" >
+
+ <include layout="@layout/sd_error" />
+
+ <com.android.music.TouchInterceptor
+ android:id="@android:id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:textSize="18sp"
+ android:drawSelectorOnTop="false"
+ android:fastScrollEnabled="true" />
+
+</LinearLayout>
diff --git a/res/layout/media_picker_activity_expanding.xml b/res/layout/media_picker_activity_expanding.xml
new file mode 100644
index 0000000..3361431
--- /dev/null
+++ b/res/layout/media_picker_activity_expanding.xml
@@ -0,0 +1,35 @@
+<?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:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ android:gravity="center_vertical" >
+
+ <include layout="@layout/sd_error" />
+
+ <ExpandableListView
+ android:id="@android:id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:textSize="18sp"
+ android:drawSelectorOnTop="false"
+ android:fastScrollEnabled="true"
+ android:indicatorLeft="30dip" />
+ android:indicatorRight="60dip" />
+
+</LinearLayout>
diff --git a/res/layout/music_picker.xml b/res/layout/music_picker.xml
new file mode 100644
index 0000000..f9df172
--- /dev/null
+++ b/res/layout/music_picker.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2008 Google Inc.
+ *
+ * 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="fill_parent">
+
+ <FrameLayout
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+
+ <LinearLayout android:id="@+id/progressContainer"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:gravity="center">
+
+ <ProgressBar android:id="@+android:id/progress"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:text="@string/loading"
+ android:paddingTop="4dip"
+ android:singleLine="true" />
+
+ </LinearLayout>
+
+ <FrameLayout android:id="@+id/listContainer"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <ListView android:id="@android:id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:drawSelectorOnTop="false"
+ android:fastScrollEnabled="true" />
+ <TextView android:id="@android:id/empty"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:gravity="center"
+ android:text="@string/no_tracks_title"
+ android:textAppearance="?android:attr/textAppearanceLarge" />
+ </FrameLayout>
+
+ </FrameLayout>
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:layout_marginTop="1dip"
+ android:background="@android:drawable/bottom_bar">
+ <Button android:id="@+id/okayButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_alignParentLeft="true"
+ android:text="@android:string/ok"
+ android:minWidth="120dip"
+ android:minHeight="48dip" />
+ <Button android:id="@+id/cancelButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_alignParentRight="true"
+ android:text="@android:string/cancel"
+ android:minWidth="120dip"
+ android:minHeight="48dip" />
+ </RelativeLayout>
+
+</LinearLayout>
diff --git a/res/layout/music_picker_item.xml b/res/layout/music_picker_item.xml
new file mode 100644
index 0000000..b6e40fe
--- /dev/null
+++ b/res/layout/music_picker_item.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2007, Google Inc.
+**
+** 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.CheckableRelativeLayout
+ 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/radio">
+
+ <RadioButton
+ android:id="@+id/radio"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="4dip"
+ android:focusable="false"
+ android:clickable="false" />
+
+ <TextView android:id="@+id/duration"
+ android:layout_alignParentRight="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@+id/line1"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="#ffe0d090"
+ android:paddingLeft="4dip"
+ android:paddingRight="5dip"
+ 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/radio"
+ 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:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_below="@id/line1"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_toRightOf="@id/radio"
+ 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" />
+
+</com.android.music.CheckableRelativeLayout>
diff --git a/res/layout/query_activity.xml b/res/layout/query_activity.xml
new file mode 100644
index 0000000..72c3513
--- /dev/null
+++ b/res/layout/query_activity.xml
@@ -0,0 +1,31 @@
+<?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"
+ android:gravity="center_vertical" >
+
+ <include layout="@layout/sd_error" />
+
+ <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
index 0000000..ff3caa6
--- /dev/null
+++ b/res/layout/scanning.xml
@@ -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/sd_error.xml b/res/layout/sd_error.xml
new file mode 100644
index 0000000..8a40305
--- /dev/null
+++ b/res/layout/sd_error.xml
@@ -0,0 +1,36 @@
+<?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.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <ImageView
+ android:id="@+id/sd_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:background="@drawable/ic_mp_sd_card"
+ android:visibility="gone" />
+
+ <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"
+ android:visibility="gone" />
+
+</merge>
diff --git a/res/layout/statusbar.xml b/res/layout/statusbar.xml
new file mode 100644
index 0000000..32fdc0b
--- /dev/null
+++ b/res/layout/statusbar.xml
@@ -0,0 +1,54 @@
+<?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:focusable="true"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ 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:maxLines="2"
+ android:scrollHorizontally="true"
+ android:ellipsize="end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/values-cs-keysexposed/strings.xml b/res/values-cs-keysexposed/strings.xml
new file mode 100644
index 0000000..ffba993
--- /dev/null
+++ b/res/values-cs-keysexposed/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Název seznamu stop"</string>
+ <string name="rename_playlist_same_prompt">"Přejmenovat seznam stop <xliff:g id="PLAYLIST">%s</xliff:g> na"</string>
+ <string name="rename_playlist_diff_prompt">"Přejmenovat seznam stop <xliff:g id="PLAYLIST">%s</xliff:g> na"</string>
+</resources>
diff --git a/res/values-cs-keyshidden/strings.xml b/res/values-cs-keyshidden/strings.xml
new file mode 100644
index 0000000..96fd750
--- /dev/null
+++ b/res/values-cs-keyshidden/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Otevřete klávesnici a zadejte název nového seznamu stop. Pokud zvolíte tlačítko Uložit, seznamu stop bude přidělen název %s."</string>
+ <string name="rename_playlist_same_prompt">"Otevřete klávesnici a zadejte nový název seznamu stop <xliff:g id="PLAYLIST">%s</xliff:g>."</string>
+ <string name="rename_playlist_diff_prompt">"Otevřete klávesnici a zadejte nový název seznamu stop <xliff:g id="PLAYLIST">%s</xliff:g>, nebo zvolte tlačítko Uložit a seznamu stop bude přidělen název %s."</string>
+</resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
new file mode 100644
index 0000000..f6c5f9f
--- /dev/null
+++ b/res/values-cs/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="onesong">"1 skladba"</string>
+ <plurals name="Nsongs">
+ <item quantity="other">"Počet skladeb: <xliff:g id="COUNT">%d</xliff:g>"</item>
+ </plurals>
+ <plurals name="Nsongscomp">
+ <item quantity="other">"<xliff:g id="COUNT_FOR_ARTIST">%2$d</xliff:g> z celkem <xliff:g id="TOTAL_COUNT">%1$d</xliff:g> skladeb"</item>
+ </plurals>
+ <plurals name="Nalbums">
+ <item quantity="one">"1 album"</item>
+ <item quantity="other">"Počet alb: <xliff:g id="COUNT">%d</xliff:g>"</item>
+ </plurals>
+ <string name="goto_start">"Knihovna"</string>
+ <string name="goto_playback">"Přehrávání"</string>
+ <string name="party_shuffle">"Náhodně – všechny"</string>
+ <string name="party_shuffle_off">"Vypnout náhodné přehrávání všeho"</string>
+ <string name="delete_item">"Smazat"</string>
+ <string name="shuffle_all">"Náhodně – aktuální seznam"</string>
+ <string name="play_all">"Přehrát vše"</string>
+ <string name="delete_artist_desc">"Všechny skladby interpreta <xliff:g id="ARTIST">%s</xliff:g> budou trvale smazány z karty SD."</string>
+ <string name="delete_album_desc">"Celé album <xliff:g id="ALBUM">%s</xliff:g> bude trvale smazáno z karty SD."</string>
+ <string name="delete_song_desc">"Skladba <xliff:g id="SONG">%s</xliff:g> bude trvale smazána z karty SD."</string>
+ <string name="delete_confirm_button_text">"OK"</string>
+ <plurals name="NNNtracksdeleted">
+ <item quantity="one">"Byla smazána 1 skladba."</item>
+ <item quantity="other">"Smazané skladby: <xliff:g id="SONGS_TO_DELETE">%d</xliff:g>."</item>
+ </plurals>
+ <string name="scanning">"Vyhledávání na kartě SD..."</string>
+ <string name="nowplaying_title">"Nyní se přehrává"</string>
+ <string name="partyshuffle_title">"Náhodně – všechny"</string>
+ <string name="artists_title">"Interpreti"</string>
+ <string name="albums_menu">"Alba"</string>
+ <string name="albums_title">"Alba"</string>
+ <string name="tracks_menu">"Skladby"</string>
+ <string name="tracks_title">"Skladby"</string>
+ <string name="playlists_menu">"Seznamy stop"</string>
+ <string name="playlists_title">"Seznamy stop"</string>
+ <string name="videos_title">"Videa"</string>
+ <string name="all_title">"Všechna média"</string>
+ <string name="browse_menu">"Interpreti"</string>
+ <string name="search_title">"Hledat"</string>
+ <string name="no_tracks_title">"Žádné skladby"</string>
+ <string name="no_videos_title">"Žádná videa"</string>
+ <string name="no_playlists_title">"Žádné seznamy stop"</string>
+ <string name="delete_playlist_menu">"Smazat"</string>
+ <string name="edit_playlist_menu">"Upravit"</string>
+ <string name="rename_playlist_menu">"Přejmenovat"</string>
+ <string name="playlist_deleted_message">"Seznam stop byl smazán."</string>
+ <string name="playlist_renamed_message">"Seznam stop byl přejmenován."</string>
+ <string name="recentlyadded">"Nedávno přidané"</string>
+ <string name="recentlyadded_title">"Nedávno přidané"</string>
+ <string name="podcasts_listitem">"Podcasty"</string>
+ <string name="podcasts_title">"Podcasty"</string>
+ <string name="sdcard_missing_title">"Žádná karta SD není dostupná"</string>
+ <string name="sdcard_missing_message">"V telefonu není vložena karta SD."</string>
+ <string name="sdcard_busy_title">"Karta SD není dostupná"</string>
+ <string name="sdcard_busy_message">"Karta SD je zaneprázdněna."</string>
+ <string name="sdcard_error_title">"Chyba karty SD"</string>
+ <string name="sdcard_error_message">"Na kartě SD došlo k chybě."</string>
+ <string name="unknown_artist_name">"Neznámý interpret"</string>
+ <string name="unknown_album_name">"Neznámé album"</string>
+ <string name="shuffle_on_notif">"Náhodné přehrávání je zapnuto."</string>
+ <string name="shuffle_off_notif">"Náhodné přehrávání je vypnuto."</string>
+ <string name="repeat_off_notif">"Opakování je vypnuto."</string>
+ <string name="repeat_current_notif">"Opakování aktuální skladby."</string>
+ <string name="repeat_all_notif">"Opakování všech skladeb."</string>
+ <string name="ringtone_menu">"Použít jako vyzváněcí tón telefonu"</string>
+ <string name="ringtone_menu_short">"Použít jako vyzváněcí tón"</string>
+ <string name="ringtone_set">"Skladba %s byla nastavena jako vyzváněcí tón."</string>
+ <string name="play_selection">"Přehrát"</string>
+ <string name="add_to_playlist">"Přidat do seznamu stop"</string>
+ <string name="queue">"Aktuální seznam stop"</string>
+ <string name="new_playlist">"Nové"</string>
+ <string name="new_playlist_name_template">"Nový seznam stop <xliff:g id="NUMBER">%d</xliff:g>"</string>
+ <plurals name="NNNtrackstoplaylist">
+ <item quantity="one">"1 skladba byla přidána do seznamu stop."</item>
+ <item quantity="other">"Skladby přidány do seznamu stop: %d."</item>
+ </plurals>
+ <string name="emptyplaylist">"Vybraný seznam stop je prázdný."</string>
+ <string name="create_playlist_create_text">"Uložit"</string>
+ <string name="create_playlist_overwrite_text">"Přepsat"</string>
+ <string name="service_start_error_title">"Problém s přehráváním"</string>
+ <string name="service_start_error_msg">"Skladbu nelze přehrát."</string>
+ <string name="service_start_error_button">"OK"</string>
+ <string-array name="weeklist">
+ <item>"1 týden"</item>
+ <item>"2 týdny"</item>
+ <item>"3 týdny"</item>
+ <item>"4 týdny"</item>
+ <item>"5 týdnů"</item>
+ <item>"6 týdnů"</item>
+ <item>"7 týdnů"</item>
+ <item>"8 týdnů"</item>
+ <item>"9 týdnů"</item>
+ <item>"10 týdnů"</item>
+ <item>"11 týdnů"</item>
+ <item>"12 týdnů"</item>
+ </string-array>
+ <string name="weekpicker_set">"Hotovo"</string>
+ <string name="weekpicker_title">"Nastavit čas"</string>
+ <string name="save_as_playlist">"Uložit jako seznam stop"</string>
+ <string name="clear_playlist">"Vymazat seznam stop"</string>
+ <string name="musicbrowserlabel">"Hudba"</string>
+ <string name="musicshortcutlabel">"Seznam skladeb"</string>
+ <string name="mediaplaybacklabel">"Hudba"</string>
+ <string name="videobrowserlabel">"Videa"</string>
+ <string name="mediapickerlabel">"Hudba"</string>
+ <string name="playback_failed">"Přehrávač nepodporuje tento typ zvukových souborů."</string>
+ <string name="cancel">"Zrušit"</string>
+ <string name="remove_from_playlist">"Odstranit ze seznamu stop"</string>
+ <string name="streamloadingtext">"Připojování k proudu <xliff:g id="HOST">%s</xliff:g>"</string>
+ <string name="mediasearch">"Vyhledat skladbu %s pomocí:"</string>
+ <string name="working_artists">"Interpreti…"</string>
+ <string name="working_albums">"Alba…"</string>
+ <string name="working_songs">"Skladby…"</string>
+ <string name="working_playlists">"Seznamy stop…"</string>
+ <string name="loading">"Načítání"</string>
+ <string name="sort_by_track">"Stopy"</string>
+ <string name="sort_by_album">"Alba"</string>
+ <string name="sort_by_artist">"Interpreti"</string>
+ <string name="music_picker_title">"Vyberte hudební stopu"</string>
+ <string name="gadget_track">"Stopa <xliff:g id="TRACK_NUMBER">%d</xliff:g>"</string>
+</resources>
diff --git a/res/values-de-keysexposed/strings.xml b/res/values-de-keysexposed/strings.xml
new file mode 100644
index 0000000..6ec704b
--- /dev/null
+++ b/res/values-de-keysexposed/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Name der Playlist"</string>
+ <string name="rename_playlist_same_prompt">"\"<xliff:g id="PLAYLIST">%s</xliff:g>\" umbenennen in"</string>
+ <string name="rename_playlist_diff_prompt">"\"<xliff:g id="PLAYLIST">%s</xliff:g>\" umbenennen in"</string>
+</resources>
diff --git a/res/values-de-keyshidden/strings.xml b/res/values-de-keyshidden/strings.xml
new file mode 100644
index 0000000..fbc866f
--- /dev/null
+++ b/res/values-de-keyshidden/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Ziehen Sie die Tastatur heraus, um für die neue Playlist einen Namen festzulegen, oder wählen Sie \"Speichern\", um sie mit dem Namen \"%s\" zu speichern."</string>
+ <string name="rename_playlist_same_prompt">"Ziehen Sie die Tastatur heraus, um \"<xliff:g id="PLAYLIST">%s</xliff:g>\" umzubenennen."</string>
+ <string name="rename_playlist_diff_prompt">"Ziehen Sie die Tastatur heraus, um die Playlist \"<xliff:g id="PLAYLIST">%s</xliff:g>\" umzubenennen, oder wählen Sie \"Speichern\", um sie mit dem Namen \"%s\" zu speichern."</string>
+</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
new file mode 100644
index 0000000..8ca4408
--- /dev/null
+++ b/res/values-de/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="onesong">"1 Titel"</string>
+ <plurals name="Nsongs">
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> Titel"</item>
+ </plurals>
+ <plurals name="Nsongscomp">
+ <item quantity="other">"<xliff:g id="COUNT_FOR_ARTIST">%2$d</xliff:g> von \n<xliff:g id="TOTAL_COUNT">%1$d</xliff:g> Liedern"</item>
+ </plurals>
+ <plurals name="Nalbums">
+ <item quantity="one">"1 Album"</item>
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> Alben"</item>
+ </plurals>
+ <string name="goto_start">"Startübersicht"</string>
+ <string name="goto_playback">"Wiedergeben"</string>
+ <string name="party_shuffle">"Zufallswiedergabe (Party)"</string>
+ <string name="party_shuffle_off">"Zufallswiedergabe (Party) deaktivieren"</string>
+ <string name="delete_item">"Löschen"</string>
+ <string name="shuffle_all">"Alle zufällig wiedergeben"</string>
+ <string name="play_all">"Alle wiedergeben"</string>
+ <string name="delete_artist_desc">"Alle Titel von <xliff:g id="ARTIST">%s</xliff:g> werden endgültig von der SD-Karte gelöscht."</string>
+ <string name="delete_album_desc">"Das gesamte Album \"<xliff:g id="ALBUM">%s</xliff:g>\" wird endgültig von der SD-Karte gelöscht."</string>
+ <string name="delete_song_desc">"\"<xliff:g id="SONG">%s</xliff:g>\" wird endgültig von der SD-Karte gelöscht."</string>
+ <string name="delete_confirm_button_text">"OK"</string>
+ <plurals name="NNNtracksdeleted">
+ <item quantity="one">"1 Titel wurde gelöscht."</item>
+ <item quantity="other">"<xliff:g id="SONGS_TO_DELETE">%d</xliff:g> Titel wurden gelöscht."</item>
+ </plurals>
+ <string name="scanning">"SD-Karte wird gelesenen..."</string>
+ <string name="nowplaying_title">"Aktuelle Wiedergabe"</string>
+ <string name="partyshuffle_title">"Zufallswiedergabe (Party)"</string>
+ <string name="artists_title">"Interpreten"</string>
+ <string name="albums_menu">"Alben"</string>
+ <string name="albums_title">"Alben"</string>
+ <string name="tracks_menu">"Titel"</string>
+ <string name="tracks_title">"Titel"</string>
+ <string name="playlists_menu">"Playlists"</string>
+ <string name="playlists_title">"Playlists"</string>
+ <string name="videos_title">"Videos"</string>
+ <string name="all_title">"Alle Medien"</string>
+ <string name="browse_menu">"Interpreten"</string>
+ <string name="search_title">"Suchen"</string>
+ <string name="no_tracks_title">"Keine Titel"</string>
+ <string name="no_videos_title">"Keine Videos"</string>
+ <string name="no_playlists_title">"Keine Playlists"</string>
+ <string name="delete_playlist_menu">"Löschen"</string>
+ <string name="edit_playlist_menu">"Bearbeiten"</string>
+ <string name="rename_playlist_menu">"Umbenennen"</string>
+ <string name="playlist_deleted_message">"Playlist gelöscht"</string>
+ <string name="playlist_renamed_message">"Playlist umbenannt"</string>
+ <string name="recentlyadded">"Kürzlich hinzugefügt"</string>
+ <string name="recentlyadded_title">"Kürzlich hinzugefügt"</string>
+ <string name="podcasts_listitem">"Podcasts"</string>
+ <string name="podcasts_title">"Podcasts"</string>
+ <string name="sdcard_missing_title">"Keine SD-Karte"</string>
+ <string name="sdcard_missing_message">"Es befindet sich keine SD-Karte im Telefon."</string>
+ <string name="sdcard_busy_title">"SD-Karte nicht verfügbar"</string>
+ <string name="sdcard_busy_message">"Die SD-Karte ist ausgelastet."</string>
+ <string name="sdcard_error_title">"SD-Kartenfehler"</string>
+ <string name="sdcard_error_message">"Bei der SD-Karte ist ein Fehler aufgetreten."</string>
+ <string name="unknown_artist_name">"Unbekannter Interpret"</string>
+ <string name="unknown_album_name">"Unbekanntes Album"</string>
+ <string name="shuffle_on_notif">"Zufallswiedergabe ist aktiviert."</string>
+ <string name="shuffle_off_notif">"Zufallswiedergabe ist deaktiviert."</string>
+ <string name="repeat_off_notif">"Wiederholung ist deaktiviert."</string>
+ <string name="repeat_current_notif">"Aktueller Titel wird wiederholt."</string>
+ <string name="repeat_all_notif">"Alle Titel werden wiederholt."</string>
+ <string name="ringtone_menu">"Als Telefonklingelton verwenden"</string>
+ <string name="ringtone_menu_short">"Als Klingelton verwenden"</string>
+ <string name="ringtone_set">"\"%s\" als Telefonklingelton eingestellt"</string>
+ <string name="play_selection">"Wiedergeben"</string>
+ <string name="add_to_playlist">"Zur Playlist hinzufügen"</string>
+ <string name="queue">"Aktuelle Playlist"</string>
+ <string name="new_playlist">"Neu"</string>
+ <string name="new_playlist_name_template">"Neue Playlist <xliff:g id="NUMBER">%d</xliff:g>"</string>
+ <plurals name="NNNtrackstoplaylist">
+ <item quantity="one">"1 Titel zur Playlist hinzugefügt"</item>
+ <item quantity="other">"%d Titel zur Playlist hinzugefügt."</item>
+ </plurals>
+ <string name="emptyplaylist">"Ausgewählte Playlist ist leer"</string>
+ <string name="create_playlist_create_text">"Speichern"</string>
+ <string name="create_playlist_overwrite_text">"Überschreiben"</string>
+ <string name="service_start_error_title">"Problem bei der Wiedergabe"</string>
+ <string name="service_start_error_msg">"Der Titel kann nicht wiedergegeben werden."</string>
+ <string name="service_start_error_button">"OK"</string>
+ <string-array name="weeklist">
+ <item>"1 Woche"</item>
+ <item>"2 Wochen"</item>
+ <item>"3 Wochen"</item>
+ <item>"4 Wochen"</item>
+ <item>"5 Wochen"</item>
+ <item>"6 Wochen"</item>
+ <item>"7 Wochen"</item>
+ <item>"8 Wochen"</item>
+ <item>"9 Wochen"</item>
+ <item>"10 Wochen"</item>
+ <item>"11 Wochen"</item>
+ <item>"12 Wochen"</item>
+ </string-array>
+ <string name="weekpicker_set">"Fertig"</string>
+ <string name="weekpicker_title">"Uhrzeit einstellen"</string>
+ <string name="save_as_playlist">"Als Playlist speichern"</string>
+ <string name="clear_playlist">"Playlist löschen"</string>
+ <string name="musicbrowserlabel">"Musik"</string>
+ <string name="musicshortcutlabel">"Musikplaylist"</string>
+ <string name="mediaplaybacklabel">"Musik"</string>
+ <string name="videobrowserlabel">"Videos"</string>
+ <string name="mediapickerlabel">"Musik"</string>
+ <string name="playback_failed">"Die Wiedergabe dieses Audiodateityps wird nicht unterstützt."</string>
+ <string name="cancel">"Abbrechen"</string>
+ <string name="remove_from_playlist">"Von Playlist entfernen"</string>
+ <string name="streamloadingtext">"Verbindung mit <xliff:g id="HOST">%s</xliff:g> wird hergestellt"</string>
+ <string name="mediasearch">"Nach %s suchen mithilfe von:"</string>
+ <string name="working_artists">"Künstler…"</string>
+ <string name="working_albums">"Alben…"</string>
+ <string name="working_songs">"Lieder…"</string>
+ <string name="working_playlists">"Playlists…"</string>
+ <string name="loading">"Wird geladen"</string>
+ <string name="sort_by_track">"Titel"</string>
+ <string name="sort_by_album">"Alben"</string>
+ <string name="sort_by_artist">"Künstler"</string>
+ <string name="music_picker_title">"Musiktitel auswählen"</string>
+ <string name="gadget_track">"Track <xliff:g id="TRACK_NUMBER">%d</xliff:g>"</string>
+</resources>
diff --git a/res/values-es-keysexposed/strings.xml b/res/values-es-keysexposed/strings.xml
new file mode 100644
index 0000000..f41a5c0
--- /dev/null
+++ b/res/values-es-keysexposed/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Nombre de lista de reproducción"</string>
+ <string name="rename_playlist_same_prompt">"Cambiar \"<xliff:g id="PLAYLIST">%s</xliff:g>\" por"</string>
+ <string name="rename_playlist_diff_prompt">"Cambiar \"<xliff:g id="PLAYLIST">%s</xliff:g>\" por"</string>
+</resources>
diff --git a/res/values-es-keyshidden/strings.xml b/res/values-es-keyshidden/strings.xml
new file mode 100644
index 0000000..fd8243d
--- /dev/null
+++ b/res/values-es-keyshidden/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Abre el teclado para asignar un nombre a la nueva lista de reproducción o selecciona \"Guardar\" para llamarla \"%s\"."</string>
+ <string name="rename_playlist_same_prompt">"Abre el teclado para asignar un nombre nuevo a la lista de reproducción \"<xliff:g id="PLAYLIST">%s</xliff:g>\"."</string>
+ <string name="rename_playlist_diff_prompt">"Abre el teclado para asignar un nombre nuevo a la lista de reproducción <xliff:g id="PLAYLIST">%s</xliff:g> o selecciona \"Guardar\" para llamarla \"%s\"."</string>
+</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
new file mode 100644
index 0000000..00c743f
--- /dev/null
+++ b/res/values-es/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="onesong">"1 canción"</string>
+ <plurals name="Nsongs">
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> canciones"</item>
+ </plurals>
+ <plurals name="Nsongscomp">
+ <item quantity="other">"<xliff:g id="COUNT_FOR_ARTIST">%2$d</xliff:g> de <xliff:g id="TOTAL_COUNT">%1$d</xliff:g> canciones"</item>
+ </plurals>
+ <plurals name="Nalbums">
+ <item quantity="one">"1 álbum"</item>
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> álbumes"</item>
+ </plurals>
+ <string name="goto_start">"Biblioteca"</string>
+ <string name="goto_playback">"Reproducción"</string>
+ <string name="party_shuffle">"Sesión aleatoria"</string>
+ <string name="party_shuffle_off">"Desactivar sesión aleatoria"</string>
+ <string name="delete_item">"Suprimir"</string>
+ <string name="shuffle_all">"Reproducción aleatoria"</string>
+ <string name="play_all">"Reproducir todo"</string>
+ <string name="delete_artist_desc">"Todas las canciones de <xliff:g id="ARTIST">%s</xliff:g> se eliminarán de forma permanente de la tarjeta SD."</string>
+ <string name="delete_album_desc">"El álbum \"<xliff:g id="ALBUM">%s</xliff:g>\" completo se eliminará de forma permanente de la tarjeta SD."</string>
+ <string name="delete_song_desc">"\"<xliff:g id="SONG">%s</xliff:g>\" se eliminará de forma permanente de la tarjeta SD."</string>
+ <string name="delete_confirm_button_text">"Aceptar"</string>
+ <plurals name="NNNtracksdeleted">
+ <item quantity="one">"Se ha eliminado una canción."</item>
+ <item quantity="other">"Se han eliminado <xliff:g id="SONGS_TO_DELETE">%d</xliff:g> canciones."</item>
+ </plurals>
+ <string name="scanning">"Examinando tarjeta SD…"</string>
+ <string name="nowplaying_title">"Reproduciendo"</string>
+ <string name="partyshuffle_title">"Sesión aleatoria"</string>
+ <string name="artists_title">"Artistas"</string>
+ <string name="albums_menu">"Álbumes"</string>
+ <string name="albums_title">"Álbumes"</string>
+ <string name="tracks_menu">"Canciones"</string>
+ <string name="tracks_title">"Canciones"</string>
+ <string name="playlists_menu">"Listas de reproducción"</string>
+ <string name="playlists_title">"Listas de reproducción"</string>
+ <string name="videos_title">"Vídeos"</string>
+ <string name="all_title">"Todos los medios"</string>
+ <string name="browse_menu">"Artistas"</string>
+ <string name="search_title">"Buscar"</string>
+ <string name="no_tracks_title">"Ninguna canción"</string>
+ <string name="no_videos_title">"Ningún vídeo"</string>
+ <string name="no_playlists_title">"Ninguna lista de reproducción"</string>
+ <string name="delete_playlist_menu">"Suprimir"</string>
+ <string name="edit_playlist_menu">"Editar"</string>
+ <string name="rename_playlist_menu">"Cambiar nombre"</string>
+ <string name="playlist_deleted_message">"Se ha eliminado la lista de reproducción."</string>
+ <string name="playlist_renamed_message">"Se ha cambiado el nombre de la lista de reproducción."</string>
+ <string name="recentlyadded">"Añadidas recientemente"</string>
+ <string name="recentlyadded_title">"Añadidos recientemente"</string>
+ <string name="podcasts_listitem">"Podcasts"</string>
+ <string name="podcasts_title">"Podcasts"</string>
+ <string name="sdcard_missing_title">"Falta la tarjeta SD"</string>
+ <string name="sdcard_missing_message">"Falta la tarjeta SD en el teléfono"</string>
+ <string name="sdcard_busy_title">"Tarjeta SD no disponible"</string>
+ <string name="sdcard_busy_message">"La tarjeta SD está ocupada."</string>
+ <string name="sdcard_error_title">"Error de tarjeta SD"</string>
+ <string name="sdcard_error_message">"Se ha producido un error al acceder a la tarjeta SD."</string>
+ <string name="unknown_artist_name">"Artista desconocido"</string>
+ <string name="unknown_album_name">"Álbum desconocido"</string>
+ <string name="shuffle_on_notif">"La reproducción aleatoria está activada."</string>
+ <string name="shuffle_off_notif">"La reproducción aleatoria está desactivada."</string>
+ <string name="repeat_off_notif">"La repetición está desactivada."</string>
+ <string name="repeat_current_notif">"Repitiendo canción actual..."</string>
+ <string name="repeat_all_notif">"Repitiendo todas las canciones..."</string>
+ <string name="ringtone_menu">"Utilizar como tono del teléfono"</string>
+ <string name="ringtone_menu_short">"Utilizar como tono"</string>
+ <string name="ringtone_set">"Se ha establecido \"%s\" como tono del teléfono."</string>
+ <string name="play_selection">"Reproducir"</string>
+ <string name="add_to_playlist">"Añadir a lista de reproducción"</string>
+ <string name="queue">"Lista de reproducción actual"</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>
+ <plurals name="NNNtrackstoplaylist">
+ <item quantity="one">"Se ha añadido una canción a la lista de reproducción."</item>
+ <item quantity="other">"Se han añadido %d canciones a la lista de reproducción."</item>
+ </plurals>
+ <string name="emptyplaylist">"La lista de reproducción seleccionada está vacía."</string>
+ <string name="create_playlist_create_text">"Guardar"</string>
+ <string name="create_playlist_overwrite_text">"Sobrescribir"</string>
+ <string name="service_start_error_title">"Problema de reproducción"</string>
+ <string name="service_start_error_msg">"No se ha podido reproducir la canción."</string>
+ <string name="service_start_error_button">"Aceptar"</string>
+ <string-array name="weeklist">
+ <item>"1 semana"</item>
+ <item>"2 semanas"</item>
+ <item>"3 semanas"</item>
+ <item>"4 semanas"</item>
+ <item>"5 semanas"</item>
+ <item>"6 semanas"</item>
+ <item>"7 semanas"</item>
+ <item>"8 semanas"</item>
+ <item>"9 semanas"</item>
+ <item>"10 semanas"</item>
+ <item>"11 semanas"</item>
+ <item>"12 semanas"</item>
+ </string-array>
+ <string name="weekpicker_set">"Hecho"</string>
+ <string name="weekpicker_title">"Establecer hora"</string>
+ <string name="save_as_playlist">"Guardar como lista de reproducción"</string>
+ <string name="clear_playlist">"Borrar lista de reproducción"</string>
+ <string name="musicbrowserlabel">"Música"</string>
+ <string name="musicshortcutlabel">"Lista de reproducción de música"</string>
+ <string name="mediaplaybacklabel">"Música"</string>
+ <string name="videobrowserlabel">"Vídeos"</string>
+ <string name="mediapickerlabel">"Música"</string>
+ <string name="playback_failed">"El reproductor no admite este tipo de archivo de audio."</string>
+ <string name="cancel">"Cancelar"</string>
+ <string name="remove_from_playlist">"Eliminar de lista de reproducción"</string>
+ <string name="streamloadingtext">"Conectando con <xliff:g id="HOST">%s</xliff:g>..."</string>
+ <string name="mediasearch">"Buscar %s con:"</string>
+ <string name="working_artists">"Artistas…"</string>
+ <string name="working_albums">"Álbumes…"</string>
+ <string name="working_songs">"Canciones…"</string>
+ <string name="working_playlists">"Listas de reproducción…"</string>
+ <string name="loading">"Cargando"</string>
+ <string name="sort_by_track">"Pistas"</string>
+ <string name="sort_by_album">"Álbumes"</string>
+ <string name="sort_by_artist">"Artistas"</string>
+ <string name="music_picker_title">"Seleccionar pista musical"</string>
+ <string name="gadget_track">"Pista <xliff:g id="TRACK_NUMBER">%d</xliff:g>"</string>
+</resources>
diff --git a/res/values-finger/strings2.xml b/res/values-finger/strings2.xml
new file mode 100644
index 0000000..2381e8d
--- /dev/null
+++ b/res/values-finger/strings2.xml
@@ -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.
+*/
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Do not translate. This is the separator character used when building the string that shows number of albums and songs. -->
+ <string name="albumsongseparator">\n</string>
+
+ <!-- Used for the 2nd and 3rd line (artist and album) in the notification area item for the music app -->
+ <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-keysexposed/strings.xml b/res/values-fr-keysexposed/strings.xml
new file mode 100644
index 0000000..9fa5d9d
--- /dev/null
+++ b/res/values-fr-keysexposed/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Nom de la playlist"</string>
+ <string name="rename_playlist_same_prompt">"Renommer \"<xliff:g id="PLAYLIST">%s</xliff:g>\" en"</string>
+ <string name="rename_playlist_diff_prompt">"Renommer \"<xliff:g id="PLAYLIST">%s</xliff:g>\" en"</string>
+</resources>
diff --git a/res/values-fr-keyshidden/strings.xml b/res/values-fr-keyshidden/strings.xml
new file mode 100644
index 0000000..2ee347c
--- /dev/null
+++ b/res/values-fr-keyshidden/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Ouvrez le clavier afin de saisir un nom pour votre nouvelle playlist ou sélectionnez Enregistrer pour la nommer \"%s\"."</string>
+ <string name="rename_playlist_same_prompt">"Ouvrez le clavier pour renommer la playlist \"<xliff:g id="PLAYLIST">%s</xliff:g>\"."</string>
+ <string name="rename_playlist_diff_prompt">"Ouvrez le clavier pour saisir un nouveau nom pour la playlist \"<xliff:g id="PLAYLIST">%s</xliff:g>\" ou sélectionnez Enregistrer pour la nommer \"%s\"."</string>
+</resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
new file mode 100644
index 0000000..b434be0
--- /dev/null
+++ b/res/values-fr/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="onesong">"1 chanson"</string>
+ <plurals name="Nsongs">
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> chansons"</item>
+ </plurals>
+ <plurals name="Nsongscomp">
+ <item quantity="other">"<xliff:g id="COUNT_FOR_ARTIST">%2$d</xliff:g> chansons sur <xliff:g id="TOTAL_COUNT">%1$d</xliff:g>"</item>
+ </plurals>
+ <plurals name="Nalbums">
+ <item quantity="one">"1 album"</item>
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> albums"</item>
+ </plurals>
+ <string name="goto_start">"Médiathèque"</string>
+ <string name="goto_playback">"Lecture"</string>
+ <string name="party_shuffle">"Lecture aléatoire"</string>
+ <string name="party_shuffle_off">"La lecture aléatoire est désactivée."</string>
+ <string name="delete_item">"Supprimer"</string>
+ <string name="shuffle_all">"Lecture aléatoire de toutes les chansons"</string>
+ <string name="play_all">"Tout lire"</string>
+ <string name="delete_artist_desc">"Toutes les chansons de <xliff:g id="ARTIST">%s</xliff:g> seront définitivement supprimées de la carte SD."</string>
+ <string name="delete_album_desc">"L\'intégralité de l\'album \"<xliff:g id="ALBUM">%s</xliff:g>\" sera définitivement supprimée de la carte SD."</string>
+ <string name="delete_song_desc">"La chanson \"<xliff:g id="SONG">%s</xliff:g>\" sera définitivement supprimée de la carte SD."</string>
+ <string name="delete_confirm_button_text">"OK"</string>
+ <plurals name="NNNtracksdeleted">
+ <item quantity="one">"1 chanson a été supprimée."</item>
+ <item quantity="other">"<xliff:g id="SONGS_TO_DELETE">%d</xliff:g> chansons ont été supprimées."</item>
+ </plurals>
+ <string name="scanning">"Lecture de la carte SD..."</string>
+ <string name="nowplaying_title">"En écoute"</string>
+ <string name="partyshuffle_title">"Lecture aléatoire"</string>
+ <string name="artists_title">"Artistes"</string>
+ <string name="albums_menu">"Albums"</string>
+ <string name="albums_title">"Albums"</string>
+ <string name="tracks_menu">"Chansons"</string>
+ <string name="tracks_title">"Chansons"</string>
+ <string name="playlists_menu">"Playlists"</string>
+ <string name="playlists_title">"Playlists"</string>
+ <string name="videos_title">"Vidéos"</string>
+ <string name="all_title">"Tous le multimédia"</string>
+ <string name="browse_menu">"Artistes"</string>
+ <string name="search_title">"Rechercher"</string>
+ <string name="no_tracks_title">"Aucune chanson"</string>
+ <string name="no_videos_title">"Aucune vidéo trouvée"</string>
+ <string name="no_playlists_title">"Aucune playlist trouvée"</string>
+ <string name="delete_playlist_menu">"Supprimer"</string>
+ <string name="edit_playlist_menu">"Modifier"</string>
+ <string name="rename_playlist_menu">"Renommer"</string>
+ <string name="playlist_deleted_message">"La playlist a été supprimée."</string>
+ <string name="playlist_renamed_message">"La playlist a été renommée."</string>
+ <string name="recentlyadded">"Ajoutés récemment"</string>
+ <string name="recentlyadded_title">"Playlist récente"</string>
+ <string name="podcasts_listitem">"Podcasts"</string>
+ <string name="podcasts_title">"Podcasts"</string>
+ <string name="sdcard_missing_title">"Aucune carte SD trouvée"</string>
+ <string name="sdcard_missing_message">"Aucune carte SD n\'est insérée dans votre téléphone."</string>
+ <string name="sdcard_busy_title">"Carte SD non disponible"</string>
+ <string name="sdcard_busy_message">"Désolé, votre carte SD n\'est pas disponible."</string>
+ <string name="sdcard_error_title">"Erreur de carte SD"</string>
+ <string name="sdcard_error_message">"Une erreur s\'est produite sur votre carte SD."</string>
+ <string name="unknown_artist_name">"Artiste inconnu"</string>
+ <string name="unknown_album_name">"Album inconnu"</string>
+ <string name="shuffle_on_notif">"La lecture aléatoire est activée."</string>
+ <string name="shuffle_off_notif">"Le mode aléatoire est désactivé."</string>
+ <string name="repeat_off_notif">"La lecture en boucle est désactivée."</string>
+ <string name="repeat_current_notif">"Lecture en boucle de la chanson en écoute."</string>
+ <string name="repeat_all_notif">"Lecture en boucle de toutes les chansons"</string>
+ <string name="ringtone_menu">"Utiliser comme sonnerie"</string>
+ <string name="ringtone_menu_short">"Définir comme sonnerie"</string>
+ <string name="ringtone_set">"\"%s\" a été défini comme sonnerie."</string>
+ <string name="play_selection">"Lire"</string>
+ <string name="add_to_playlist">"Ajouter à la playlist"</string>
+ <string name="queue">"Playlist actuelle"</string>
+ <string name="new_playlist">"Nouveau"</string>
+ <string name="new_playlist_name_template">"Nouvelle playlist <xliff:g id="NUMBER">%d</xliff:g>"</string>
+ <plurals name="NNNtrackstoplaylist">
+ <item quantity="one">"1 chanson a été ajoutée à la playlist."</item>
+ <item quantity="other">"%d chansons ont été ajoutées à la playlist."</item>
+ </plurals>
+ <string name="emptyplaylist">"La playlist sélectionnée est vide."</string>
+ <string name="create_playlist_create_text">"Enregistrer"</string>
+ <string name="create_playlist_overwrite_text">"Remplacer"</string>
+ <string name="service_start_error_title">"Erreur de lecture"</string>
+ <string name="service_start_error_msg">"Désolé, la chanson n\'a pas pu être lue."</string>
+ <string name="service_start_error_button">"OK"</string>
+ <string-array name="weeklist">
+ <item>"1 sem."</item>
+ <item>"2 sem."</item>
+ <item>"3 sem."</item>
+ <item>"4 sem."</item>
+ <item>"5 sem."</item>
+ <item>"6 sem."</item>
+ <item>"7 sem."</item>
+ <item>"8 sem."</item>
+ <item>"9 sem."</item>
+ <item>"10 sem."</item>
+ <item>"11 sem."</item>
+ <item>"12 sem."</item>
+ </string-array>
+ <string name="weekpicker_set">"OK"</string>
+ <string name="weekpicker_title">"Définir la durée"</string>
+ <string name="save_as_playlist">"Enregistrer comme playlist"</string>
+ <string name="clear_playlist">"Effacer la playlist"</string>
+ <string name="musicbrowserlabel">"Musique"</string>
+ <string name="musicshortcutlabel">"Playlist musicale"</string>
+ <string name="mediaplaybacklabel">"Musique"</string>
+ <string name="videobrowserlabel">"Vidéos"</string>
+ <string name="mediapickerlabel">"Musique"</string>
+ <string name="playback_failed">"Désolé, le lecteur ne prend pas en charge ce type de fichier audio."</string>
+ <string name="cancel">"Annuler"</string>
+ <string name="remove_from_playlist">"Supprimer de la playlist"</string>
+ <string name="streamloadingtext">"Connexion à <xliff:g id="HOST">%s</xliff:g>"</string>
+ <string name="mediasearch">"Rechercher %s à l\'aide de :"</string>
+ <string name="working_artists">"Artistes…"</string>
+ <string name="working_albums">"Albums…"</string>
+ <string name="working_songs">"Chansons…"</string>
+ <string name="working_playlists">"Playlists…"</string>
+ <string name="loading">"Chargement"</string>
+ <string name="sort_by_track">"Pistes"</string>
+ <string name="sort_by_album">"Albums"</string>
+ <string name="sort_by_artist">"Artistes"</string>
+ <string name="music_picker_title">"Sélectionner une piste"</string>
+ <string name="gadget_track">"Piste <xliff:g id="TRACK_NUMBER">%d</xliff:g>"</string>
+</resources>
diff --git a/res/values-it-keysexposed/strings.xml b/res/values-it-keysexposed/strings.xml
new file mode 100644
index 0000000..ba7ae09
--- /dev/null
+++ b/res/values-it-keysexposed/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Nome playlist"</string>
+ <string name="rename_playlist_same_prompt">"Cambia \"<xliff:g id="PLAYLIST">%s</xliff:g>\" con"</string>
+ <string name="rename_playlist_diff_prompt">"Cambia \"<xliff:g id="PLAYLIST">%s</xliff:g>\" con"</string>
+</resources>
diff --git a/res/values-it-keyshidden/strings.xml b/res/values-it-keyshidden/strings.xml
new file mode 100644
index 0000000..637d1ad
--- /dev/null
+++ b/res/values-it-keyshidden/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Apri la tastiera per assegnare un nome alla nuova playlist o seleziona Salva per denominarla \"%s\"."</string>
+ <string name="rename_playlist_same_prompt">"Apri la tastiera per assegnare un nuovo nome alla playlist \"<xliff:g id="PLAYLIST">%s</xliff:g>\"."</string>
+ <string name="rename_playlist_diff_prompt">"Apri la tastiera per assegnare un nuovo nome alla playlist \"<xliff:g id="PLAYLIST">%s</xliff:g>\" o seleziona Salva per denominarla \"%s\"."</string>
+</resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
new file mode 100644
index 0000000..2a730e9
--- /dev/null
+++ b/res/values-it/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="onesong">"1 brano"</string>
+ <plurals name="Nsongs">
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> brani"</item>
+ </plurals>
+ <plurals name="Nsongscomp">
+ <item quantity="other">"<xliff:g id="COUNT_FOR_ARTIST">%2$d</xliff:g> brani su <xliff:g id="TOTAL_COUNT">%1$d</xliff:g>"</item>
+ </plurals>
+ <plurals name="Nalbums">
+ <item quantity="one">"1 album"</item>
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> album"</item>
+ </plurals>
+ <string name="goto_start">"Raccolta"</string>
+ <string name="goto_playback">"Riproduci"</string>
+ <string name="party_shuffle">"Party shuffle"</string>
+ <string name="party_shuffle_off">"Party shuffle non attiva"</string>
+ <string name="delete_item">"Elimina"</string>
+ <string name="shuffle_all">"Ripr. casuale"</string>
+ <string name="play_all">"Riprod. tutti"</string>
+ <string name="delete_artist_desc">"Tutti i brani di <xliff:g id="ARTIST">%s</xliff:g> verranno eliminati definitivamente dalla scheda SD."</string>
+ <string name="delete_album_desc">"L\'intero album \"<xliff:g id="ALBUM">%s</xliff:g>\" verrà eliminato definitivamente dalla scheda SD."</string>
+ <string name="delete_song_desc">"Il brano \"<xliff:g id="SONG">%s</xliff:g>\" verrà eliminato definitivamente dalla scheda SD."</string>
+ <string name="delete_confirm_button_text">"OK"</string>
+ <plurals name="NNNtracksdeleted">
+ <item quantity="one">"1 brano eliminato."</item>
+ <item quantity="other">"<xliff:g id="SONGS_TO_DELETE">%d</xliff:g> brani eliminati."</item>
+ </plurals>
+ <string name="scanning">"Analisi scheda SD..."</string>
+ <string name="nowplaying_title">"In esecuzione"</string>
+ <string name="partyshuffle_title">"Party shuffle"</string>
+ <string name="artists_title">"Artisti"</string>
+ <string name="albums_menu">"Album"</string>
+ <string name="albums_title">"Album"</string>
+ <string name="tracks_menu">"Brani"</string>
+ <string name="tracks_title">"Brani"</string>
+ <string name="playlists_menu">"Playlist"</string>
+ <string name="playlists_title">"Playlist"</string>
+ <string name="videos_title">"Video"</string>
+ <string name="all_title">"Tutti i media"</string>
+ <string name="browse_menu">"Artisti"</string>
+ <string name="search_title">"Cerca"</string>
+ <string name="no_tracks_title">"Nessun brano"</string>
+ <string name="no_videos_title">"Nessun video"</string>
+ <string name="no_playlists_title">"Nessuna playlist"</string>
+ <string name="delete_playlist_menu">"Elimina"</string>
+ <string name="edit_playlist_menu">"Modifica"</string>
+ <string name="rename_playlist_menu">"Rinomina"</string>
+ <string name="playlist_deleted_message">"Playlist eliminata."</string>
+ <string name="playlist_renamed_message">"Playlist rinominata."</string>
+ <string name="recentlyadded">"Aggiunta di recente"</string>
+ <string name="recentlyadded_title">"Aggiunta di recente"</string>
+ <string name="podcasts_listitem">"Podcast"</string>
+ <string name="podcasts_title">"Podcast"</string>
+ <string name="sdcard_missing_title">"Nessuna scheda SD"</string>
+ <string name="sdcard_missing_message">"Il telefono non contiene una scheda SD."</string>
+ <string name="sdcard_busy_title">"Scheda SD non disponibile"</string>
+ <string name="sdcard_busy_message">"Spiacenti. La scheda SD è già in uso."</string>
+ <string name="sdcard_error_title">"Errore della scheda SD"</string>
+ <string name="sdcard_error_message">"Si è verificato un errore nella scheda SD."</string>
+ <string name="unknown_artist_name">"Artista sconosciuto"</string>
+ <string name="unknown_album_name">"Album sconosciuto"</string>
+ <string name="shuffle_on_notif">"Riproduzione casuale attiva."</string>
+ <string name="shuffle_off_notif">"Riproduzione casuale non attiva."</string>
+ <string name="repeat_off_notif">"Ripetizione non attiva."</string>
+ <string name="repeat_current_notif">"Ripetizione brano corrente."</string>
+ <string name="repeat_all_notif">"Ripetizione di tutti i brani."</string>
+ <string name="ringtone_menu">"Usa come suoneria"</string>
+ <string name="ringtone_menu_short">"Usa come suoneria"</string>
+ <string name="ringtone_set">"\"%s\" impostato come suoneria."</string>
+ <string name="play_selection">"Riprod."</string>
+ <string name="add_to_playlist">"Aggiungi a playlist"</string>
+ <string name="queue">"Playlist corrente"</string>
+ <string name="new_playlist">"Nuova"</string>
+ <string name="new_playlist_name_template">"Nuova playlist <xliff:g id="NUMBER">%d</xliff:g>"</string>
+ <plurals name="NNNtrackstoplaylist">
+ <item quantity="one">"1 brano aggiunto alla playlist."</item>
+ <item quantity="other">"%d brani aggiunti alla playlist."</item>
+ </plurals>
+ <string name="emptyplaylist">"La playlist selezionata è vuota."</string>
+ <string name="create_playlist_create_text">"Salva"</string>
+ <string name="create_playlist_overwrite_text">"Sostituisci"</string>
+ <string name="service_start_error_title">"Problema di riproduzione"</string>
+ <string name="service_start_error_msg">"Spiacenti. Impossibile riprodurre il brano."</string>
+ <string name="service_start_error_button">"OK"</string>
+ <string-array name="weeklist">
+ <item>"1 settimana"</item>
+ <item>"2 settimane"</item>
+ <item>"3 settimane"</item>
+ <item>"4 settimane"</item>
+ <item>"5 settimane"</item>
+ <item>"6 settimane"</item>
+ <item>"7 settimane"</item>
+ <item>"8 settimane"</item>
+ <item>"9 settimane"</item>
+ <item>"10 settimane"</item>
+ <item>"11 settimane"</item>
+ <item>"12 settimane"</item>
+ </string-array>
+ <string name="weekpicker_set">"Fine"</string>
+ <string name="weekpicker_title">"Imposta periodo"</string>
+ <string name="save_as_playlist">"Salva come playlist"</string>
+ <string name="clear_playlist">"Cancella playlist"</string>
+ <string name="musicbrowserlabel">"Musica"</string>
+ <string name="musicshortcutlabel">"Playlist musicale"</string>
+ <string name="mediaplaybacklabel">"Musica"</string>
+ <string name="videobrowserlabel">"Video"</string>
+ <string name="mediapickerlabel">"Musica"</string>
+ <string name="playback_failed">"Spiacenti. Il player non supporta questo tipo di file audio."</string>
+ <string name="cancel">"Annulla"</string>
+ <string name="remove_from_playlist">"Rimuovi da playlist"</string>
+ <string name="streamloadingtext">"Connessione a <xliff:g id="HOST">%s</xliff:g>"</string>
+ <string name="mediasearch">"Cerca %s con:"</string>
+ <string name="working_artists">"Artisti…"</string>
+ <string name="working_albums">"Album…"</string>
+ <string name="working_songs">"Brani…"</string>
+ <string name="working_playlists">"Playlist…"</string>
+ <string name="loading">"Caricamento"</string>
+ <string name="sort_by_track">"Tracce"</string>
+ <string name="sort_by_album">"Album"</string>
+ <string name="sort_by_artist">"Artisti"</string>
+ <string name="music_picker_title">"Seleziona traccia musicale"</string>
+ <string name="gadget_track">"Traccia <xliff:g id="TRACK_NUMBER">%d</xliff:g>"</string>
+</resources>
diff --git a/res/values-ja-keysexposed/strings.xml b/res/values-ja-keysexposed/strings.xml
new file mode 100644
index 0000000..46d1dfd
--- /dev/null
+++ b/res/values-ja-keysexposed/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"プレイリスト名"</string>
+ <string name="rename_playlist_same_prompt">"「<xliff:g id="PLAYLIST">%s</xliff:g>」の新しい名前"</string>
+ <string name="rename_playlist_diff_prompt">"「<xliff:g id="PLAYLIST">%s</xliff:g>」の新しい名前"</string>
+</resources>
diff --git a/res/values-ja-keyshidden/strings.xml b/res/values-ja-keyshidden/strings.xml
new file mode 100644
index 0000000..849e675
--- /dev/null
+++ b/res/values-ja-keyshidden/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"キーボードを表示して新しいプレイリストに名前を付けるか、[保存]を選択して「%s」という名前で保存します。"</string>
+ <string name="rename_playlist_same_prompt">"キーボードを表示して「<xliff:g id="PLAYLIST">%s</xliff:g>」に新しい名前を付けます。"</string>
+ <string name="rename_playlist_diff_prompt">"キーボードを表示して「<xliff:g id="PLAYLIST">%s</xliff:g>」に新しい名前を付けるか、[保存]を選択して「%s」という名前で保存します。"</string>
+</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
new file mode 100644
index 0000000..4c6eecf
--- /dev/null
+++ b/res/values-ja/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="onesong">"曲(1)"</string>
+ <plurals name="Nsongs">
+ <item quantity="other">"曲(<xliff:g id="COUNT">%d</xliff:g>)"</item>
+ </plurals>
+ <plurals name="Nsongscomp">
+ <item quantity="other">"<xliff:g id="TOTAL_COUNT">%1$d</xliff:g>曲中<xliff:g id="COUNT_FOR_ARTIST">%2$d</xliff:g>曲"</item>
+ </plurals>
+ <plurals name="Nalbums">
+ <item quantity="one">"アルバム(1)"</item>
+ <item quantity="other">"アルバム(<xliff:g id="COUNT">%d</xliff:g>)"</item>
+ </plurals>
+ <string name="goto_start">"ライブラリ"</string>
+ <string name="goto_playback">"再生"</string>
+ <string name="party_shuffle">"パーティシャッフル"</string>
+ <string name="party_shuffle_off">"パーティシャッフルOFF"</string>
+ <string name="delete_item">"削除"</string>
+ <string name="shuffle_all">"すべてシャッフル"</string>
+ <string name="play_all">"すべて再生"</string>
+ <string name="delete_artist_desc">"<xliff:g id="ARTIST">%s</xliff:g>の全曲をSDカードから完全に削除します。"</string>
+ <string name="delete_album_desc">"アルバム「<xliff:g id="ALBUM">%s</xliff:g>」全体をSDカードから完全に削除します。"</string>
+ <string name="delete_song_desc">"「<xliff:g id="SONG">%s</xliff:g>」をSDカードから完全に削除します。"</string>
+ <string name="delete_confirm_button_text">"OK"</string>
+ <plurals name="NNNtracksdeleted">
+ <item quantity="one">"1曲削除しました。"</item>
+ <item quantity="other">"<xliff:g id="SONGS_TO_DELETE">%d</xliff:g>曲削除しました。"</item>
+ </plurals>
+ <string name="scanning">"SDカードをスキャン中..."</string>
+ <string name="nowplaying_title">"再生中"</string>
+ <string name="partyshuffle_title">"パーティシャッフル"</string>
+ <string name="artists_title">"アーティスト"</string>
+ <string name="albums_menu">"アルバム"</string>
+ <string name="albums_title">"アルバム"</string>
+ <string name="tracks_menu">"曲"</string>
+ <string name="tracks_title">"曲"</string>
+ <string name="playlists_menu">"プレイリスト"</string>
+ <string name="playlists_title">"プレイリスト"</string>
+ <string name="videos_title">"動画"</string>
+ <string name="all_title">"すべてのメディア"</string>
+ <string name="browse_menu">"アーティスト"</string>
+ <string name="search_title">"検索"</string>
+ <string name="no_tracks_title">"曲がありません"</string>
+ <string name="no_videos_title">"動画がありません"</string>
+ <string name="no_playlists_title">"プレイリストがありません"</string>
+ <string name="delete_playlist_menu">"削除"</string>
+ <string name="edit_playlist_menu">"編集"</string>
+ <string name="rename_playlist_menu">"名前を変更"</string>
+ <string name="playlist_deleted_message">"プレイリストを削除しました。"</string>
+ <string name="playlist_renamed_message">"プレイリストの名前を変更しました。"</string>
+ <string name="recentlyadded">"最近追加したアイテム"</string>
+ <string name="recentlyadded_title">"最近追加した項目"</string>
+ <string name="podcasts_listitem">"ポッドキャスト"</string>
+ <string name="podcasts_title">"ポッドキャスト"</string>
+ <string name="sdcard_missing_title">"SDカードがありません"</string>
+ <string name="sdcard_missing_message">"SDカードが携帯電話に挿入されていません。"</string>
+ <string name="sdcard_busy_title">"SDカードを使用できません"</string>
+ <string name="sdcard_busy_message">"このSDカードは別の端末で使用中です。"</string>
+ <string name="sdcard_error_title">"SDカードエラー"</string>
+ <string name="sdcard_error_message">"SDカードでエラーが発生しました。"</string>
+ <string name="unknown_artist_name">"不明なアーティスト"</string>
+ <string name="unknown_album_name">"不明なアルバム"</string>
+ <string name="shuffle_on_notif">"シャッフルをONにしました。"</string>
+ <string name="shuffle_off_notif">"シャッフルをOFFにしました。"</string>
+ <string name="repeat_off_notif">"繰り返しをOFFにしました。"</string>
+ <string name="repeat_current_notif">"現在の曲を繰り返し再生します。"</string>
+ <string name="repeat_all_notif">"全曲繰り返しで再生します。"</string>
+ <string name="ringtone_menu">"着信音に設定"</string>
+ <string name="ringtone_menu_short">"着信音に設定"</string>
+ <string name="ringtone_set">"「%s」を着信音として設定しました。"</string>
+ <string name="play_selection">"再生"</string>
+ <string name="add_to_playlist">"プレイリストに追加"</string>
+ <string name="queue">"現在のプレイリスト"</string>
+ <string name="new_playlist">"新規"</string>
+ <string name="new_playlist_name_template">"新規プレイリスト<xliff:g id="NUMBER">%d</xliff:g>"</string>
+ <plurals name="NNNtrackstoplaylist">
+ <item quantity="one">"プレイリストに1曲追加しました。"</item>
+ <item quantity="other">"プレイリストに%d曲追加しました。"</item>
+ </plurals>
+ <string name="emptyplaylist">"選択したプレイリストは空です。"</string>
+ <string name="create_playlist_create_text">"保存"</string>
+ <string name="create_playlist_overwrite_text">"上書き保存"</string>
+ <string name="service_start_error_title">"再生エラー"</string>
+ <string name="service_start_error_msg">"再生できません。"</string>
+ <string name="service_start_error_button">"OK"</string>
+ <string-array name="weeklist">
+ <item>"1週間"</item>
+ <item>"2週間"</item>
+ <item>"3週間"</item>
+ <item>"4週間"</item>
+ <item>"5週間"</item>
+ <item>"6週間"</item>
+ <item>"7週間"</item>
+ <item>"8週間"</item>
+ <item>"9週間"</item>
+ <item>"10週間"</item>
+ <item>"11週間"</item>
+ <item>"12週間"</item>
+ </string-array>
+ <string name="weekpicker_set">"完了"</string>
+ <string name="weekpicker_title">"期間の設定"</string>
+ <string name="save_as_playlist">"プレイリストとして保存"</string>
+ <string name="clear_playlist">"プレイリストをクリア"</string>
+ <string name="musicbrowserlabel">"ミュージック"</string>
+ <string name="musicshortcutlabel">"ミュージックプレイリスト"</string>
+ <string name="mediaplaybacklabel">"ミュージック"</string>
+ <string name="videobrowserlabel">"動画"</string>
+ <string name="mediapickerlabel">"ミュージック"</string>
+ <string name="playback_failed">"プレーヤーが対応していない音声ファイル形式です。"</string>
+ <string name="cancel">"キャンセル"</string>
+ <string name="remove_from_playlist">"プレイリストから削除"</string>
+ <string name="streamloadingtext">"<xliff:g id="HOST">%s</xliff:g>に接続中"</string>
+ <string name="mediasearch">"%sの検索:"</string>
+ <string name="working_artists">"アーティスト..."</string>
+ <string name="working_albums">"アルバム..."</string>
+ <string name="working_songs">"曲..."</string>
+ <string name="working_playlists">"再生リスト..."</string>
+ <string name="loading">"読み込み中"</string>
+ <string name="sort_by_track">"トラック"</string>
+ <string name="sort_by_album">"アルバム"</string>
+ <string name="sort_by_artist">"アーティスト"</string>
+ <string name="music_picker_title">"音楽トラックを選択"</string>
+ <string name="gadget_track">"トラック<xliff:g id="TRACK_NUMBER">%d</xliff:g>"</string>
+</resources>
diff --git a/res/values-keysexposed/strings.xml b/res/values-keysexposed/strings.xml
new file mode 100644
index 0000000..0c520aa
--- /dev/null
+++ b/res/values-keysexposed/strings.xml
@@ -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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Prompt in dialog when creating a playlist. The user will enter the name of the playlist in a textfield underneath this prompt. -->
+ <string name="create_playlist_create_text_prompt">Playlist name</string>
+ <!-- Prompt in dialog when renaming a playlist, used when the current name and the new name are the same. The user will enter the new name of the playlist in a textfield underneath this prompt. -->
+ <string name="rename_playlist_same_prompt">Rename \"<xliff:g id="playlist">%s</xliff:g>\" to</string>
+ <!-- Prompt in dialog when renaming a playlist, used when the current name the new name are different. The user will enter the new name of the playlist in a textfield underneath this prompt. -->
+ <string name="rename_playlist_diff_prompt">Rename \"<xliff:g id="playlist">%s</xliff:g>\" to</string>
+</resources>
+
diff --git a/res/values-keyshidden/strings.xml b/res/values-keyshidden/strings.xml
new file mode 100644
index 0000000..b086fd0
--- /dev/null
+++ b/res/values-keyshidden/strings.xml
@@ -0,0 +1,29 @@
+<?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">
+ <!-- Prompt in dialog when creating a playlist and the keyboard is closed. -->
+ <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>
+
+ <!-- Prompt in dialog when renaming a playlist, and the entered name is the same as the old one. -->
+ <string name="rename_playlist_same_prompt">Open the keyboard to give playlist \"<xliff:g id="playlist">%s</xliff:g>\" a new name.</string>
+ <!-- Prompt in dialog when renaming a playlist, and the entered name is different from the old one. -->
+ <string name="rename_playlist_diff_prompt">Open the keyboard to give playlist \"<xliff:g id="playlist">%s</xliff:g>\" a new name, or select Save to name it \"%s\".</string>
+</resources>
+
diff --git a/res/values-ko-keysexposed/strings.xml b/res/values-ko-keysexposed/strings.xml
new file mode 100644
index 0000000..60dc323
--- /dev/null
+++ b/res/values-ko-keysexposed/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"재생 목록 이름"</string>
+ <string name="rename_playlist_same_prompt">"\'<xliff:g id="PLAYLIST">%s</xliff:g>\'을(를) 다음으로 변경"</string>
+ <string name="rename_playlist_diff_prompt">"\'<xliff:g id="PLAYLIST">%s</xliff:g>\'을(를) 다음으로 변경"</string>
+</resources>
diff --git a/res/values-ko-keyshidden/strings.xml b/res/values-ko-keyshidden/strings.xml
new file mode 100644
index 0000000..4a452df
--- /dev/null
+++ b/res/values-ko-keyshidden/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"키보드를 사용하여 새 재생 목록에 이름을 지정하거나 \'저장\'을 선택하여 \'%s\'(이)라고 지정합니다."</string>
+ <string name="rename_playlist_same_prompt">"키보드를 사용하여 \'<xliff:g id="PLAYLIST">%s</xliff:g>\' 재생 목록에 새 이름을 지정합니다."</string>
+ <string name="rename_playlist_diff_prompt">"키보드를 사용하여 \'<xliff:g id="PLAYLIST">%s</xliff:g>\' 재생 목록에 새 이름을 지정하거나 \'저장\'을 선택하여 \'%s\'(이)라고 지정합니다."</string>
+</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
new file mode 100644
index 0000000..2d96a00
--- /dev/null
+++ b/res/values-ko/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="onesong">"1개의 노래"</string>
+ <plurals name="Nsongs">
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g>개의 노래"</item>
+ </plurals>
+ <plurals name="Nsongscomp">
+ <item quantity="other">"노래 <xliff:g id="TOTAL_COUNT">%1$d</xliff:g>곡 중 <xliff:g id="COUNT_FOR_ARTIST">%2$d</xliff:g>곡"</item>
+ </plurals>
+ <plurals name="Nalbums">
+ <item quantity="one">"1개의 앨범"</item>
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g>개의 앨범"</item>
+ </plurals>
+ <string name="goto_start">"라이브러리"</string>
+ <string name="goto_playback">"재생"</string>
+ <string name="party_shuffle">"파티 셔플"</string>
+ <string name="party_shuffle_off">"파티 셔플 해제"</string>
+ <string name="delete_item">"삭제"</string>
+ <string name="shuffle_all">"모두 셔플"</string>
+ <string name="play_all">"모두 재생"</string>
+ <string name="delete_artist_desc">"<xliff:g id="ARTIST">%s</xliff:g>의 모든 노래가 SD 카드에서 완전히 삭제됩니다."</string>
+ <string name="delete_album_desc">"전체 앨범 \'<xliff:g id="ALBUM">%s</xliff:g>\'이(가) SD 카드에서 완전히 삭제됩니다."</string>
+ <string name="delete_song_desc">"\'<xliff:g id="SONG">%s</xliff:g>\'이(가) SD 카드에서 완전히 삭제됩니다."</string>
+ <string name="delete_confirm_button_text">"확인"</string>
+ <plurals name="NNNtracksdeleted">
+ <item quantity="one">"노래 1개가 삭제되었습니다."</item>
+ <item quantity="other">"<xliff:g id="SONGS_TO_DELETE">%d</xliff:g>개의 노래가 삭제되었습니다."</item>
+ </plurals>
+ <string name="scanning">"SD 카드 스캔 중..."</string>
+ <string name="nowplaying_title">"지금 재생 중"</string>
+ <string name="partyshuffle_title">"파티 셔플"</string>
+ <string name="artists_title">"아티스트"</string>
+ <string name="albums_menu">"앨범"</string>
+ <string name="albums_title">"앨범"</string>
+ <string name="tracks_menu">"노래"</string>
+ <string name="tracks_title">"노래"</string>
+ <string name="playlists_menu">"재생 목록"</string>
+ <string name="playlists_title">"재생 목록"</string>
+ <string name="videos_title">"동영상"</string>
+ <string name="all_title">"전체 미디어"</string>
+ <string name="browse_menu">"아티스트"</string>
+ <string name="search_title">"검색"</string>
+ <string name="no_tracks_title">"노래 없음"</string>
+ <string name="no_videos_title">"동영상 없음"</string>
+ <string name="no_playlists_title">"재생 목록 없음"</string>
+ <string name="delete_playlist_menu">"삭제"</string>
+ <string name="edit_playlist_menu">"편집"</string>
+ <string name="rename_playlist_menu">"이름 바꾸기"</string>
+ <string name="playlist_deleted_message">"재생 목록이 삭제되었습니다."</string>
+ <string name="playlist_renamed_message">"재생 목록 이름이 변경되었습니다."</string>
+ <string name="recentlyadded">"최근 추가 목록"</string>
+ <string name="recentlyadded_title">"최근 추가 목록"</string>
+ <string name="podcasts_listitem">"Podcast"</string>
+ <string name="podcasts_title">"Podcast"</string>
+ <string name="sdcard_missing_title">"SD 카드 없음"</string>
+ <string name="sdcard_missing_message">"전화에 SD 카드가 삽입되어 있지 않습니다."</string>
+ <string name="sdcard_busy_title">"SD 카드를 사용할 수 없음"</string>
+ <string name="sdcard_busy_message">"SD 카드가 사용 중입니다."</string>
+ <string name="sdcard_error_title">"SD 카드 오류"</string>
+ <string name="sdcard_error_message">"SD 카드에 오류가 발생했습니다."</string>
+ <string name="unknown_artist_name">"알 수 없는 아티스트"</string>
+ <string name="unknown_album_name">"알 수 없는 앨범"</string>
+ <string name="shuffle_on_notif">"셔플이 설정되었습니다."</string>
+ <string name="shuffle_off_notif">"셔플이 해제되었습니다."</string>
+ <string name="repeat_off_notif">"반복이 해제되었습니다."</string>
+ <string name="repeat_current_notif">"현재 노래를 반복 중입니다."</string>
+ <string name="repeat_all_notif">"모든 노래를 반복 중입니다."</string>
+ <string name="ringtone_menu">"전화 벨소리로 사용"</string>
+ <string name="ringtone_menu_short">"벨소리로 사용"</string>
+ <string name="ringtone_set">"\'%s\'이(가) 전화 벨소리로 설정되었습니다."</string>
+ <string name="play_selection">"재생"</string>
+ <string name="add_to_playlist">"재생 목록에 추가"</string>
+ <string name="queue">"현재 재생 목록"</string>
+ <string name="new_playlist">"새로 만들기"</string>
+ <string name="new_playlist_name_template">"새 재생 목록 <xliff:g id="NUMBER">%d</xliff:g>"</string>
+ <plurals name="NNNtrackstoplaylist">
+ <item quantity="one">"노래 1개가 재생 목록에 추가되었습니다."</item>
+ <item quantity="other">"%d개의 노래가 재생 목록에 추가되었습니다."</item>
+ </plurals>
+ <string name="emptyplaylist">"선택한 재생 목록이 비어 있습니다."</string>
+ <string name="create_playlist_create_text">"저장"</string>
+ <string name="create_playlist_overwrite_text">"덮어쓰기"</string>
+ <string name="service_start_error_title">"재생 문제"</string>
+ <string name="service_start_error_msg">"노래를 재생할 수 없습니다."</string>
+ <string name="service_start_error_button">"확인"</string>
+ <string-array name="weeklist">
+ <item>"1주"</item>
+ <item>"2주"</item>
+ <item>"3주"</item>
+ <item>"4주"</item>
+ <item>"5주"</item>
+ <item>"6주"</item>
+ <item>"7주"</item>
+ <item>"8주"</item>
+ <item>"9주"</item>
+ <item>"10주"</item>
+ <item>"11주"</item>
+ <item>"12주"</item>
+ </string-array>
+ <string name="weekpicker_set">"완료"</string>
+ <string name="weekpicker_title">"시간 설정"</string>
+ <string name="save_as_playlist">"재생 목록으로 저장"</string>
+ <string name="clear_playlist">"재생 목록 지우기"</string>
+ <string name="musicbrowserlabel">"음악"</string>
+ <string name="musicshortcutlabel">"음악 재생 목록"</string>
+ <string name="mediaplaybacklabel">"음악"</string>
+ <string name="videobrowserlabel">"동영상"</string>
+ <string name="mediapickerlabel">"음악"</string>
+ <string name="playback_failed">"플레이어에서 지원하지 않는 오디오 파일 형식입니다."</string>
+ <string name="cancel">"취소"</string>
+ <string name="remove_from_playlist">"재생 목록에서 삭제"</string>
+ <string name="streamloadingtext">"<xliff:g id="HOST">%s</xliff:g>에 연결 중"</string>
+ <string name="mediasearch">"다음을 사용하여 %s 검색:"</string>
+ <string name="working_artists">"아티스트…"</string>
+ <string name="working_albums">"앨범…"</string>
+ <string name="working_songs">"노래…"</string>
+ <string name="working_playlists">"재생 목록..."</string>
+ <string name="loading">"로드 중"</string>
+ <string name="sort_by_track">"트랙"</string>
+ <string name="sort_by_album">"앨범"</string>
+ <string name="sort_by_artist">"아티스트"</string>
+ <string name="music_picker_title">"음악 트랙 선택"</string>
+ <string name="gadget_track">"트랙 <xliff:g id="TRACK_NUMBER">%d</xliff:g>"</string>
+</resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
new file mode 100644
index 0000000..4adf891
--- /dev/null
+++ b/res/values-nb/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="onesong">"1 sang"</string>
+ <plurals name="Nsongs">
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> sanger"</item>
+ </plurals>
+ <plurals name="Nsongscomp">
+ <item quantity="other">"<xliff:g id="COUNT_FOR_ARTIST">%2$d</xliff:g> av <xliff:g id="TOTAL_COUNT">%1$d</xliff:g> sanger"</item>
+ </plurals>
+ <plurals name="Nalbums">
+ <item quantity="one">"1 album"</item>
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> album"</item>
+ </plurals>
+ <string name="goto_start">"Bibliotek"</string>
+ <string name="goto_playback">"Avspilling"</string>
+ <string name="party_shuffle">"Tilfeldig"</string>
+ <string name="party_shuffle_off">"Tilfeldig av"</string>
+ <string name="delete_item">"Slett"</string>
+ <string name="shuffle_all">"Stokk alle"</string>
+ <string name="play_all">"Spill alle"</string>
+ <string name="delete_artist_desc">"Alle sanger av <xliff:g id="ARTIST">%s</xliff:g> vil vil bli slettet permanent fra minnekortet."</string>
+ <string name="delete_album_desc">"Hele albumet \"<xliff:g id="ALBUM">%s</xliff:g>\" vil bli slettet permanent fra minnekortet."</string>
+ <string name="delete_song_desc">"\"<xliff:g id="SONG">%s</xliff:g>\" vil bli slettet permanent fra minnekortet."</string>
+ <string name="delete_confirm_button_text">"OK"</string>
+ <plurals name="NNNtracksdeleted">
+ <item quantity="one">"1 sang ble slettet."</item>
+ <item quantity="other">"<xliff:g id="SONGS_TO_DELETE">%d</xliff:g> sang ble slettet."</item>
+ </plurals>
+ <string name="scanning">"Søker i minnekort…"</string>
+ <string name="nowplaying_title">"Spilles nå"</string>
+ <string name="partyshuffle_title">"Tilfeldig"</string>
+ <string name="artists_title">"Artister"</string>
+ <string name="albums_menu">"Album"</string>
+ <string name="albums_title">"Album"</string>
+ <string name="tracks_menu">"Sanger"</string>
+ <string name="tracks_title">"Sanger"</string>
+ <string name="playlists_menu">"Spillelister"</string>
+ <string name="playlists_title">"Spillelister"</string>
+ <string name="videos_title">"Videoer"</string>
+ <string name="all_title">"Alle medier"</string>
+ <string name="browse_menu">"Artister"</string>
+ <string name="search_title">"Søk"</string>
+ <string name="no_tracks_title">"Ingen sanger"</string>
+ <string name="no_videos_title">"Ingen videoer"</string>
+ <string name="no_playlists_title">"Ingen spillelister"</string>
+ <string name="delete_playlist_menu">"Slett"</string>
+ <string name="edit_playlist_menu">"Rediger"</string>
+ <string name="rename_playlist_menu">"Gi nytt navn"</string>
+ <string name="playlist_deleted_message">"Spillelisten ble slettet."</string>
+ <string name="playlist_renamed_message">"Spillelisten fikk nytt navn."</string>
+ <string name="recentlyadded">"Nylig lagt til"</string>
+ <string name="recentlyadded_title">"Nylig lagt til"</string>
+ <string name="podcasts_listitem">"Podcasts"</string>
+ <string name="podcasts_title">"Podcasts"</string>
+ <string name="sdcard_missing_title">"Mangler minnekort"</string>
+ <string name="sdcard_missing_message">"Telefonen har ikke noe minnekort i seg."</string>
+ <string name="sdcard_busy_title">"Minnekort utilgjengelig"</string>
+ <string name="sdcard_busy_message">"Minnekortet er opptatt."</string>
+ <string name="sdcard_error_title">"Feil i minnekort"</string>
+ <string name="sdcard_error_message">"En feil oppsto i minnekortet."</string>
+ <string name="unknown_artist_name">"Ukjent artist"</string>
+ <string name="unknown_album_name">"Ukjent album"</string>
+ <string name="shuffle_on_notif">"Tilfeldig avspilling er på."</string>
+ <string name="shuffle_off_notif">"Tilfeldig avspilling er av."</string>
+ <string name="repeat_off_notif">"Ingen gjentagelse."</string>
+ <string name="repeat_current_notif">"Gjentar denne sangen."</string>
+ <string name="repeat_all_notif">"Gjentar alle sanger."</string>
+ <string name="ringtone_menu">"Bruk som telefonringetone"</string>
+ <string name="ringtone_menu_short">"Bruk som ringetone"</string>
+ <string name="ringtone_set">"\"%s\" valgt som telefonringetone."</string>
+ <string name="play_selection">"Spill"</string>
+ <string name="add_to_playlist">"Legg til spillelisten"</string>
+ <string name="queue">"Gjeldende spilleliste"</string>
+ <string name="new_playlist">"Ny"</string>
+ <string name="new_playlist_name_template">"Ny spilleliste <xliff:g id="NUMBER">%d</xliff:g>"</string>
+ <plurals name="NNNtrackstoplaylist">
+ <item quantity="one">"1 sang ble lagt til spillelisten."</item>
+ <item quantity="other">"%d sanger ble lagt til spillelisten."</item>
+ </plurals>
+ <string name="emptyplaylist">"Den valgte spillelisten er tom."</string>
+ <string name="create_playlist_create_text">"Lagre"</string>
+ <string name="create_playlist_overwrite_text">"Erstatt"</string>
+ <string name="service_start_error_title">"Avspillingsproblem"</string>
+ <string name="service_start_error_msg">"Beklager, kunne ikke spille av sangen."</string>
+ <string name="service_start_error_button">"OK"</string>
+ <string-array name="weeklist">
+ <item>"1 uke"</item>
+ <item>"2 uker"</item>
+ <item>"3 uker"</item>
+ <item>"4 uker"</item>
+ <item>"5 uker"</item>
+ <item>"6 uker"</item>
+ <item>"7 uker"</item>
+ <item>"8 uker"</item>
+ <item>"9 uker"</item>
+ <item>"10 uker"</item>
+ <item>"11 uker"</item>
+ <item>"12 uker"</item>
+ </string-array>
+ <string name="weekpicker_set">"Ferdig"</string>
+ <string name="weekpicker_title">"Velg tid"</string>
+ <string name="save_as_playlist">"Lagre som spilleliste"</string>
+ <string name="clear_playlist">"Tøm spilleliste"</string>
+ <string name="musicbrowserlabel">"Musikk"</string>
+ <string name="musicshortcutlabel">"Musikk-spilleliste"</string>
+ <string name="mediaplaybacklabel">"Musikk"</string>
+ <string name="videobrowserlabel">"Videoer"</string>
+ <string name="mediapickerlabel">"Musikk"</string>
+ <string name="playback_failed">"Beklager, spilleren støtter ikke denne typen lydfiler."</string>
+ <string name="cancel">"Avbryt"</string>
+ <string name="remove_from_playlist">"Fjern fra spilleliste"</string>
+ <string name="streamloadingtext">"Kobler til <xliff:g id="HOST">%s</xliff:g>"</string>
+ <string name="mediasearch">"Søk etter %s ved hjelp av:"</string>
+ <string name="working_artists">"Artister…"</string>
+ <string name="working_albums">"Album…"</string>
+ <string name="working_songs">"Sanger…"</string>
+ <string name="working_playlists">"Spillelister…"</string>
+ <string name="loading">"Laster"</string>
+ <string name="sort_by_track">"Spor"</string>
+ <string name="sort_by_album">"Album"</string>
+ <string name="sort_by_artist">"Artister"</string>
+ <string name="music_picker_title">"Velg musikkspor"</string>
+ <string name="gadget_track">"Spor <xliff:g id="TRACK_NUMBER">%d</xliff:g>"</string>
+</resources>
diff --git a/res/values-nl-keysexposed/strings.xml b/res/values-nl-keysexposed/strings.xml
new file mode 100644
index 0000000..a06ee49
--- /dev/null
+++ b/res/values-nl-keysexposed/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Naam afspeellijst"</string>
+ <string name="rename_playlist_same_prompt">"Naam van \'<xliff:g id="PLAYLIST">%s</xliff:g>\' wijzigen in"</string>
+ <string name="rename_playlist_diff_prompt">"Naam van \'<xliff:g id="PLAYLIST">%s</xliff:g>\' wijzigen in"</string>
+</resources>
diff --git a/res/values-nl-keyshidden/strings.xml b/res/values-nl-keyshidden/strings.xml
new file mode 100644
index 0000000..26c6e7b
--- /dev/null
+++ b/res/values-nl-keyshidden/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Open het toetsenbord om uw nieuwe afspeellijst een naam te geven of selecteer \'Opslaan\' om deze de naam \'%s\' te geven."</string>
+ <string name="rename_playlist_same_prompt">"Open het toetsenbord om afspeellijst \'<xliff:g id="PLAYLIST">%s</xliff:g>\' een nieuwe naam te geven."</string>
+ <string name="rename_playlist_diff_prompt">"Open het toetsenbord om afspeellijst \'<xliff:g id="PLAYLIST">%s</xliff:g>\' een nieuwe naam te geven of selecteer \'Opslaan\' om deze de naam \'%s\' te geven."</string>
+</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
new file mode 100644
index 0000000..830b1f7
--- /dev/null
+++ b/res/values-nl/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="onesong">"1 nummer"</string>
+ <plurals name="Nsongs">
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> nummers"</item>
+ </plurals>
+ <plurals name="Nsongscomp">
+ <item quantity="other">"<xliff:g id="COUNT_FOR_ARTIST">%2$d</xliff:g> van <xliff:g id="TOTAL_COUNT">%1$d</xliff:g> nummers"</item>
+ </plurals>
+ <plurals name="Nalbums">
+ <item quantity="one">"1 album"</item>
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> albums"</item>
+ </plurals>
+ <string name="goto_start">"Bibliotheek"</string>
+ <string name="goto_playback">"Afspelen"</string>
+ <string name="party_shuffle">"Party shuffle"</string>
+ <string name="party_shuffle_off">"Party shuffle uit"</string>
+ <string name="delete_item">"Verwijderen"</string>
+ <string name="shuffle_all">"Alles in willekeurige volgorde afspelen"</string>
+ <string name="play_all">"Alles afspelen"</string>
+ <string name="delete_artist_desc">"Alle nummers van <xliff:g id="ARTIST">%s</xliff:g> worden definitief van de SD-kaart verwijderd."</string>
+ <string name="delete_album_desc">"Het hele album \'<xliff:g id="ALBUM">%s</xliff:g>\' wordt definitief van de SD-kaart verwijderd."</string>
+ <string name="delete_song_desc">"<xliff:g id="SONG">%s</xliff:g>\' wordt definitief van de SD-kaart verwijderd."</string>
+ <string name="delete_confirm_button_text">"OK"</string>
+ <plurals name="NNNtracksdeleted">
+ <item quantity="one">"1 nummer is verwijderd."</item>
+ <item quantity="other">"<xliff:g id="SONGS_TO_DELETE">%d</xliff:g> nummers zijn verwijderd."</item>
+ </plurals>
+ <string name="scanning">"SD-kaart scannen..."</string>
+ <string name="nowplaying_title">"Wordt nu afgespeeld"</string>
+ <string name="partyshuffle_title">"Party shuffle"</string>
+ <string name="artists_title">"Artiesten"</string>
+ <string name="albums_menu">"Albums"</string>
+ <string name="albums_title">"Albums"</string>
+ <string name="tracks_menu">"Nummers"</string>
+ <string name="tracks_title">"Nummers"</string>
+ <string name="playlists_menu">"Afspeellijsten"</string>
+ <string name="playlists_title">"Afspeellijsten"</string>
+ <string name="videos_title">"Video\'s"</string>
+ <string name="all_title">"Alle media"</string>
+ <string name="browse_menu">"Artiesten"</string>
+ <string name="search_title">"Zoeken"</string>
+ <string name="no_tracks_title">"Geen nummers"</string>
+ <string name="no_videos_title">"Geen video\'s"</string>
+ <string name="no_playlists_title">"Geen afspeellijsten"</string>
+ <string name="delete_playlist_menu">"Verwijderen"</string>
+ <string name="edit_playlist_menu">"Bewerken"</string>
+ <string name="rename_playlist_menu">"Naam wijzigen"</string>
+ <string name="playlist_deleted_message">"Afspeellijst verwijderd."</string>
+ <string name="playlist_renamed_message">"Naam van afspeellijst gewijzigd."</string>
+ <string name="recentlyadded">"Onlangs toegevoegd"</string>
+ <string name="recentlyadded_title">"Onlangs toegevoegd"</string>
+ <string name="podcasts_listitem">"Podcasts"</string>
+ <string name="podcasts_title">"Podcasts"</string>
+ <string name="sdcard_missing_title">"Geen SD-kaart"</string>
+ <string name="sdcard_missing_message">"Er is geen SD-kaart in de telefoon geplaatst."</string>
+ <string name="sdcard_busy_title">"SD-kaart niet beschikbaar"</string>
+ <string name="sdcard_busy_message">"De SD-kaart is in gebruik."</string>
+ <string name="sdcard_error_title">"Fout met SD-kaart"</string>
+ <string name="sdcard_error_message">"Er is een fout opgetreden met de SD-kaart."</string>
+ <string name="unknown_artist_name">"Onbekende artiest"</string>
+ <string name="unknown_album_name">"Onbekend album"</string>
+ <string name="shuffle_on_notif">"Shuffle is ingeschakeld."</string>
+ <string name="shuffle_off_notif">"Shuffle is uitgeschakeld."</string>
+ <string name="repeat_off_notif">"Herhalen is uitgeschakeld."</string>
+ <string name="repeat_current_notif">"Huidig nummer wordt herhaald."</string>
+ <string name="repeat_all_notif">"Alle nummers worden herhaald."</string>
+ <string name="ringtone_menu">"Gebruiken als beltoon van telefoon"</string>
+ <string name="ringtone_menu_short">"Gebruiken als beltoon"</string>
+ <string name="ringtone_set">"%s\' ingesteld als beltoon van telefoon."</string>
+ <string name="play_selection">"Afspelen"</string>
+ <string name="add_to_playlist">"Toevoegen aan afspeellijst"</string>
+ <string name="queue">"Huidige afspeellijst"</string>
+ <string name="new_playlist">"Nieuw"</string>
+ <string name="new_playlist_name_template">"Nieuwe afspeellijst <xliff:g id="NUMBER">%d</xliff:g>"</string>
+ <plurals name="NNNtrackstoplaylist">
+ <item quantity="one">"1 nummer toegevoegd aan afspeellijst."</item>
+ <item quantity="other">"%d nummers toegevoegd aan afspeellijst."</item>
+ </plurals>
+ <string name="emptyplaylist">"Geselecteerde afspeellijst is leeg."</string>
+ <string name="create_playlist_create_text">"Opslaan"</string>
+ <string name="create_playlist_overwrite_text">"Overschrijven"</string>
+ <string name="service_start_error_title">"Probleem met afspelen"</string>
+ <string name="service_start_error_msg">"Het nummer kan niet worden afgespeeld."</string>
+ <string name="service_start_error_button">"OK"</string>
+ <string-array name="weeklist">
+ <item>"1 week"</item>
+ <item>"2 weken"</item>
+ <item>"3 weken"</item>
+ <item>"4 weken"</item>
+ <item>"5 weken"</item>
+ <item>"6 weken"</item>
+ <item>"7 weken"</item>
+ <item>"8 weken"</item>
+ <item>"9 weken"</item>
+ <item>"10 weken"</item>
+ <item>"11 weken"</item>
+ <item>"12 weken"</item>
+ </string-array>
+ <string name="weekpicker_set">"Gereed"</string>
+ <string name="weekpicker_title">"Tijd instellen"</string>
+ <string name="save_as_playlist">"Opslaan als afspeellijst"</string>
+ <string name="clear_playlist">"Afspeellijst wissen"</string>
+ <string name="musicbrowserlabel">"Muziek"</string>
+ <string name="musicshortcutlabel">"Afspeellijst voor muziek"</string>
+ <string name="mediaplaybacklabel">"Muziek"</string>
+ <string name="videobrowserlabel">"Video\'s"</string>
+ <string name="mediapickerlabel">"Muziek"</string>
+ <string name="playback_failed">"De speler ondersteunt dit type audiobestand niet."</string>
+ <string name="cancel">"Annuleren"</string>
+ <string name="remove_from_playlist">"Verwijderen uit afspeellijst"</string>
+ <string name="streamloadingtext">"Verbinding maken met <xliff:g id="HOST">%s</xliff:g>"</string>
+ <string name="mediasearch">"Zoeken naar %s via:"</string>
+ <string name="working_artists">"Artiesten…"</string>
+ <string name="working_albums">"Albums…"</string>
+ <string name="working_songs">"Nummers…"</string>
+ <string name="working_playlists">"Afspeellijsten…"</string>
+ <string name="loading">"Laden"</string>
+ <string name="sort_by_track">"Tracks"</string>
+ <string name="sort_by_album">"Albums"</string>
+ <string name="sort_by_artist">"Artiesten"</string>
+ <string name="music_picker_title">"Muziektrack selecteren"</string>
+ <string name="gadget_track">"Track <xliff:g id="TRACK_NUMBER">%d</xliff:g>"</string>
+</resources>
diff --git a/res/values-pl-keysexposed/strings.xml b/res/values-pl-keysexposed/strings.xml
new file mode 100644
index 0000000..d4294bb
--- /dev/null
+++ b/res/values-pl-keysexposed/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Nazwa playlisty"</string>
+ <string name="rename_playlist_same_prompt">"Zmień nazwę „<xliff:g id="PLAYLIST">%s</xliff:g>” na"</string>
+ <string name="rename_playlist_diff_prompt">"Zmień nazwę „<xliff:g id="PLAYLIST">%s</xliff:g>” na"</string>
+</resources>
diff --git a/res/values-pl-keyshidden/strings.xml b/res/values-pl-keyshidden/strings.xml
new file mode 100644
index 0000000..8dcbae0
--- /dev/null
+++ b/res/values-pl-keyshidden/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Otwórz klawiaturę, aby nazwać nową playlistę lub wybierz polecenie Zapisz, aby nazwać ją „%s”."</string>
+ <string name="rename_playlist_same_prompt">"Otwórz klawiaturę, aby nadać nową nazwę playliście „<xliff:g id="PLAYLIST">%s</xliff:g>”."</string>
+ <string name="rename_playlist_diff_prompt">"Otwórz klawiaturę, aby nadać playliście „<xliff:g id="PLAYLIST">%s</xliff:g>” nową nazwę lub wybierz polecenie Zapisz, aby nazwać ją „%s”."</string>
+</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
new file mode 100644
index 0000000..1b5a390
--- /dev/null
+++ b/res/values-pl/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="onesong">"1 utwór"</string>
+ <plurals name="Nsongs">
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> utworów"</item>
+ </plurals>
+ <plurals name="Nsongscomp">
+ <item quantity="other">"<xliff:g id="COUNT_FOR_ARTIST">%2$d</xliff:g> z <xliff:g id="TOTAL_COUNT">%1$d</xliff:g> utworów"</item>
+ </plurals>
+ <plurals name="Nalbums">
+ <item quantity="one">"1 album"</item>
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> albumów"</item>
+ </plurals>
+ <string name="goto_start">"Biblioteka"</string>
+ <string name="goto_playback">"Odtwórz"</string>
+ <string name="party_shuffle">"Losowo w trybie imprezy"</string>
+ <string name="party_shuffle_off">"Wyłączono losowe odtwarzanie w trybie imprezy"</string>
+ <string name="delete_item">"Usuń"</string>
+ <string name="shuffle_all">"Wszystkie losowo"</string>
+ <string name="play_all">"Odtwórz wszystko"</string>
+ <string name="delete_artist_desc">"Wszystkie utwory wykonawcy <xliff:g id="ARTIST">%s</xliff:g> zostaną na stałe usunięte z karty SD."</string>
+ <string name="delete_album_desc">"Cały album „<xliff:g id="ALBUM">%s</xliff:g>” zostanie na stałe usunięty z karty SD."</string>
+ <string name="delete_song_desc">"Utwór „<xliff:g id="SONG">%s</xliff:g>” zostanie na stałe usunięty z karty SD."</string>
+ <string name="delete_confirm_button_text">"OK"</string>
+ <plurals name="NNNtracksdeleted">
+ <item quantity="one">"1 utwór został usunięty."</item>
+ <item quantity="other">"Liczba usuniętych utworów: <xliff:g id="SONGS_TO_DELETE">%d</xliff:g>."</item>
+ </plurals>
+ <string name="scanning">"Skanowanie karty SD..."</string>
+ <string name="nowplaying_title">"Teraz odtwarzane"</string>
+ <string name="partyshuffle_title">"Losowo w trybie imprezy"</string>
+ <string name="artists_title">"Wykonawcy"</string>
+ <string name="albums_menu">"Albumy"</string>
+ <string name="albums_title">"Albumy"</string>
+ <string name="tracks_menu">"Utwory"</string>
+ <string name="tracks_title">"Utwory"</string>
+ <string name="playlists_menu">"Playlisty"</string>
+ <string name="playlists_title">"Playlisty"</string>
+ <string name="videos_title">"Filmy wideo"</string>
+ <string name="all_title">"Wszystkie multimedia"</string>
+ <string name="browse_menu">"Wykonawcy"</string>
+ <string name="search_title">"Szukaj"</string>
+ <string name="no_tracks_title">"Brak utworów"</string>
+ <string name="no_videos_title">"Brak filmów wideo"</string>
+ <string name="no_playlists_title">"Brak playlist"</string>
+ <string name="delete_playlist_menu">"Usuń"</string>
+ <string name="edit_playlist_menu">"Edytuj"</string>
+ <string name="rename_playlist_menu">"Zmień nazwę"</string>
+ <string name="playlist_deleted_message">"Playlista została usunięta."</string>
+ <string name="playlist_renamed_message">"Zmieniono nazwę playlisty."</string>
+ <string name="recentlyadded">"Ostatnio dodane"</string>
+ <string name="recentlyadded_title">"Ostatnio dodane"</string>
+ <string name="podcasts_listitem">"Podcasty"</string>
+ <string name="podcasts_title">"Podcasty"</string>
+ <string name="sdcard_missing_title">"Brak karty SD"</string>
+ <string name="sdcard_missing_message">"Brak karty SD w telefonie."</string>
+ <string name="sdcard_busy_title">"Karta SD jest niedostępna"</string>
+ <string name="sdcard_busy_message">"Niestety, karta SD jest zajęta."</string>
+ <string name="sdcard_error_title">"Błąd karty SD"</string>
+ <string name="sdcard_error_message">"Napotkano błąd na karcie SD."</string>
+ <string name="unknown_artist_name">"Nieznany wykonawca"</string>
+ <string name="unknown_album_name">"Nieznany album"</string>
+ <string name="shuffle_on_notif">"Odtwarzanie losowe włączone"</string>
+ <string name="shuffle_off_notif">"Odtwarzanie losowe wyłączone"</string>
+ <string name="repeat_off_notif">"Powtarzanie jest wyłączone."</string>
+ <string name="repeat_current_notif">"Powtarzanie obecnego utworu."</string>
+ <string name="repeat_all_notif">"Powtarzanie wszystkich utworów."</string>
+ <string name="ringtone_menu">"Ustaw jako dzwonek telefonu"</string>
+ <string name="ringtone_menu_short">"Ustaw jako dzwonek"</string>
+ <string name="ringtone_set">"„%s” ustawiono jako dzwonek telefonu."</string>
+ <string name="play_selection">"Odtwórz"</string>
+ <string name="add_to_playlist">"Dodaj do playlisty"</string>
+ <string name="queue">"Bieżąca playlista"</string>
+ <string name="new_playlist">"Nowy"</string>
+ <string name="new_playlist_name_template">"Nowa playlista <xliff:g id="NUMBER">%d</xliff:g>"</string>
+ <plurals name="NNNtrackstoplaylist">
+ <item quantity="one">"Dodano 1 utwór do playlisty."</item>
+ <item quantity="other">"%d utworów dodano do listy odtwarzania."</item>
+ </plurals>
+ <string name="emptyplaylist">"Wybrana playlista jest pusta."</string>
+ <string name="create_playlist_create_text">"Zapisz"</string>
+ <string name="create_playlist_overwrite_text">"Zastąp"</string>
+ <string name="service_start_error_title">"Problem z odtwarzaniem"</string>
+ <string name="service_start_error_msg">"Niestety, nie można odtworzyć utworu."</string>
+ <string name="service_start_error_button">"OK"</string>
+ <string-array name="weeklist">
+ <item>"tydzień"</item>
+ <item>"2 tygodnie"</item>
+ <item>"3 tygodnie"</item>
+ <item>"4 tygodnie"</item>
+ <item>"5 tygodni"</item>
+ <item>"6 tygodni"</item>
+ <item>"7 tygodni"</item>
+ <item>"8 tygodni"</item>
+ <item>"9 tygodni"</item>
+ <item>"10 tygodni"</item>
+ <item>"11 tygodni"</item>
+ <item>"12 tygodni"</item>
+ </string-array>
+ <string name="weekpicker_set">"Gotowe"</string>
+ <string name="weekpicker_title">"Ustaw czas"</string>
+ <string name="save_as_playlist">"Zapisz jako playlistę"</string>
+ <string name="clear_playlist">"Wyczyść playlistę"</string>
+ <string name="musicbrowserlabel">"Muzyka"</string>
+ <string name="musicshortcutlabel">"Playlista muzyczna"</string>
+ <string name="mediaplaybacklabel">"Muzyka"</string>
+ <string name="videobrowserlabel">"Filmy wideo"</string>
+ <string name="mediapickerlabel">"Muzyka"</string>
+ <string name="playback_failed">"Niestety, odtwarzacz nie obsługuje plików audio tego typu."</string>
+ <string name="cancel">"Anuluj"</string>
+ <string name="remove_from_playlist">"Usuń z playlisty"</string>
+ <string name="streamloadingtext">"Łączenie z: <xliff:g id="HOST">%s</xliff:g>"</string>
+ <string name="mediasearch">"Wyszukaj %s z wykorzystaniem:"</string>
+ <string name="working_artists">"Wykonawcy…"</string>
+ <string name="working_albums">"Albumy…"</string>
+ <string name="working_songs">"Utwory…"</string>
+ <string name="working_playlists">"Listy odtwarzania..."</string>
+ <string name="loading">"Ładowanie"</string>
+ <string name="sort_by_track">"Utwory"</string>
+ <string name="sort_by_album">"Albumy"</string>
+ <string name="sort_by_artist">"Wykonawcy"</string>
+ <string name="music_picker_title">"Wybierz utwór muzyczny"</string>
+ <string name="gadget_track">"Ścieżka <xliff:g id="TRACK_NUMBER">%d</xliff:g>"</string>
+</resources>
diff --git a/res/values-ru-keysexposed/strings.xml b/res/values-ru-keysexposed/strings.xml
new file mode 100644
index 0000000..f87495e
--- /dev/null
+++ b/res/values-ru-keysexposed/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Название плейлиста"</string>
+ <string name="rename_playlist_same_prompt">"Переименовать \"<xliff:g id="PLAYLIST">%s</xliff:g>\" в"</string>
+ <string name="rename_playlist_diff_prompt">"Переименовать \"<xliff:g id="PLAYLIST">%s</xliff:g>\" в"</string>
+</resources>
diff --git a/res/values-ru-keyshidden/strings.xml b/res/values-ru-keyshidden/strings.xml
new file mode 100644
index 0000000..8be9846
--- /dev/null
+++ b/res/values-ru-keyshidden/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"Введите название нового плейлиста с клавиатуры или нажмите Сохранить, чтобы назвать его \"%s\"."</string>
+ <string name="rename_playlist_same_prompt">"Введите новое название плейлиста \"<xliff:g id="PLAYLIST">%s</xliff:g>\" с клавиатуры."</string>
+ <string name="rename_playlist_diff_prompt">"Введите новое название плейлиста \"<xliff:g id="PLAYLIST">%s</xliff:g>\" с клавиатуры или нажмите Сохранить, чтобы назвать его \"%s\"."</string>
+</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
new file mode 100644
index 0000000..cbdac78
--- /dev/null
+++ b/res/values-ru/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="onesong">"1 композиция"</string>
+ <plurals name="Nsongs">
+ <item quantity="other">"Композиций: <xliff:g id="COUNT">%d</xliff:g>"</item>
+ </plurals>
+ <plurals name="Nsongscomp">
+ <item quantity="other">"<xliff:g id="COUNT_FOR_ARTIST">%2$d</xliff:g> из <xliff:g id="TOTAL_COUNT">%1$d</xliff:g> песен"</item>
+ </plurals>
+ <plurals name="Nalbums">
+ <item quantity="one">"1 альбом"</item>
+ <item quantity="other">"Альбомов: <xliff:g id="COUNT">%d</xliff:g>"</item>
+ </plurals>
+ <string name="goto_start">"Библиотека"</string>
+ <string name="goto_playback">"Воспроизведение"</string>
+ <string name="party_shuffle">"Вечеринка"</string>
+ <string name="party_shuffle_off">"Режим вечеринки отключен"</string>
+ <string name="delete_item">"Удалить"</string>
+ <string name="shuffle_all">"Перемешать все"</string>
+ <string name="play_all">"Воспроизвести все"</string>
+ <string name="delete_artist_desc">"Все композиции артиста <xliff:g id="ARTIST">%s</xliff:g> будут удалены с карты SD."</string>
+ <string name="delete_album_desc">"Весь альбом \"<xliff:g id="ALBUM">%s</xliff:g>\" будет удален с карты SD."</string>
+ <string name="delete_song_desc">"Композиция \"<xliff:g id="SONG">%s</xliff:g>\" будет удалена с карты SD."</string>
+ <string name="delete_confirm_button_text">"ОК"</string>
+ <plurals name="NNNtracksdeleted">
+ <item quantity="one">"1 композиция была удалена."</item>
+ <item quantity="other">"Композиции (<xliff:g id="SONGS_TO_DELETE">%d</xliff:g>) были удалены."</item>
+ </plurals>
+ <string name="scanning">"Сканирование карты SD…"</string>
+ <string name="nowplaying_title">"Воспроизводится"</string>
+ <string name="partyshuffle_title">"Вечеринка"</string>
+ <string name="artists_title">"Артисты"</string>
+ <string name="albums_menu">"Альбомы"</string>
+ <string name="albums_title">"Альбомы"</string>
+ <string name="tracks_menu">"Композиции"</string>
+ <string name="tracks_title">"Композиции"</string>
+ <string name="playlists_menu">"Плейлисты"</string>
+ <string name="playlists_title">"Плейлисты"</string>
+ <string name="videos_title">"Видео"</string>
+ <string name="all_title">"Все мультимедийное содержание"</string>
+ <string name="browse_menu">"Артисты"</string>
+ <string name="search_title">"Поиск"</string>
+ <string name="no_tracks_title">"Нет композиций"</string>
+ <string name="no_videos_title">"Нет видео"</string>
+ <string name="no_playlists_title">"Нет плейлистов"</string>
+ <string name="delete_playlist_menu">"Удалить"</string>
+ <string name="edit_playlist_menu">"Изменить"</string>
+ <string name="rename_playlist_menu">"Переименовать"</string>
+ <string name="playlist_deleted_message">"Плейлист удален."</string>
+ <string name="playlist_renamed_message">"Плейлист переименован."</string>
+ <string name="recentlyadded">"Недавно добавленные"</string>
+ <string name="recentlyadded_title">"Недавно добавленные"</string>
+ <string name="podcasts_listitem">"Подкасты"</string>
+ <string name="podcasts_title">"Подкасты"</string>
+ <string name="sdcard_missing_title">"Нет карты SD"</string>
+ <string name="sdcard_missing_message">"В телефоне не установлена карта SD."</string>
+ <string name="sdcard_busy_title">"Карта SD недоступна"</string>
+ <string name="sdcard_busy_message">"К сожалению, карта SD занята."</string>
+ <string name="sdcard_error_title">"Ошибка карты SD"</string>
+ <string name="sdcard_error_message">"Ошибка при доступе к карте SD."</string>
+ <string name="unknown_artist_name">"Неизвестный артист"</string>
+ <string name="unknown_album_name">"Неизвестный альбом"</string>
+ <string name="shuffle_on_notif">"Перемешивание включено."</string>
+ <string name="shuffle_off_notif">"Перемешивание отключено."</string>
+ <string name="repeat_off_notif">"Повтор отключен."</string>
+ <string name="repeat_current_notif">"Повтор текущей композиции."</string>
+ <string name="repeat_all_notif">"Повтор всех композиций."</string>
+ <string name="ringtone_menu">"Использовать как мелодию звонка телефона"</string>
+ <string name="ringtone_menu_short">"Использовать как мелодию звонка"</string>
+ <string name="ringtone_set">"Установлена мелодия звонка телефона: \"%s\"."</string>
+ <string name="play_selection">"Воспроизвести"</string>
+ <string name="add_to_playlist">"Добавить в плейлист"</string>
+ <string name="queue">"Текущий плейлист"</string>
+ <string name="new_playlist">"Создать"</string>
+ <string name="new_playlist_name_template">"Новый плейлист <xliff:g id="NUMBER">%d</xliff:g>"</string>
+ <plurals name="NNNtrackstoplaylist">
+ <item quantity="one">"1 композиция добавлена в плейлист."</item>
+ <item quantity="other">"Композиции (%d) добавлены в плейлист."</item>
+ </plurals>
+ <string name="emptyplaylist">"Выбранный плейлист пуст."</string>
+ <string name="create_playlist_create_text">"Сохранить"</string>
+ <string name="create_playlist_overwrite_text">"Перезаписать"</string>
+ <string name="service_start_error_title">"Проблема с воспроизведением"</string>
+ <string name="service_start_error_msg">"К сожалению, воспроизвести композицию не удалось."</string>
+ <string name="service_start_error_button">"ОК"</string>
+ <string-array name="weeklist">
+ <item>"1 неделя"</item>
+ <item>"2 недели"</item>
+ <item>"3 недели"</item>
+ <item>"4 недели"</item>
+ <item>"5 недель"</item>
+ <item>"6 недель"</item>
+ <item>"7 недель"</item>
+ <item>"8 недель"</item>
+ <item>"9 недель"</item>
+ <item>"10 недель"</item>
+ <item>"11 недель"</item>
+ <item>"12 недель"</item>
+ </string-array>
+ <string name="weekpicker_set">"Готово"</string>
+ <string name="weekpicker_title">"Выбор времени"</string>
+ <string name="save_as_playlist">"Сохранить как плейлист"</string>
+ <string name="clear_playlist">"Очистить плейлист"</string>
+ <string name="musicbrowserlabel">"Музыка"</string>
+ <string name="musicshortcutlabel">"Музыкальный плейлист"</string>
+ <string name="mediaplaybacklabel">"Музыка"</string>
+ <string name="videobrowserlabel">"Видео"</string>
+ <string name="mediapickerlabel">"Музыка"</string>
+ <string name="playback_failed">"Извините, проигрыватель не поддерживает этот тип аудиофайлов."</string>
+ <string name="cancel">"Отмена"</string>
+ <string name="remove_from_playlist">"Удалить из плейлиста"</string>
+ <string name="streamloadingtext">"Идет подключение к хосту <xliff:g id="HOST">%s</xliff:g>"</string>
+ <string name="mediasearch">"Поиск %s с помощью:"</string>
+ <string name="working_artists">"Артисты…"</string>
+ <string name="working_albums">"Альбомы…"</string>
+ <string name="working_songs">"Композиции…"</string>
+ <string name="working_playlists">"Плейлисты..."</string>
+ <string name="loading">"Идет загрузка"</string>
+ <string name="sort_by_track">"Дорожки"</string>
+ <string name="sort_by_album">"Альбомы"</string>
+ <string name="sort_by_artist">"Артисты"</string>
+ <string name="music_picker_title">"Выбрать музыкальную дорожку"</string>
+ <string name="gadget_track">"Трек <xliff:g id="TRACK_NUMBER">%d</xliff:g>"</string>
+</resources>
diff --git a/res/values-zh-rCN-keysexposed/strings.xml b/res/values-zh-rCN-keysexposed/strings.xml
new file mode 100644
index 0000000..048c393
--- /dev/null
+++ b/res/values-zh-rCN-keysexposed/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"播放列表名称"</string>
+ <string name="rename_playlist_same_prompt">"将“<xliff:g id="PLAYLIST">%s</xliff:g>”重命名为"</string>
+ <string name="rename_playlist_diff_prompt">"将“<xliff:g id="PLAYLIST">%s</xliff:g>”重命名为"</string>
+</resources>
diff --git a/res/values-zh-rCN-keyshidden/strings.xml b/res/values-zh-rCN-keyshidden/strings.xml
new file mode 100644
index 0000000..787bade
--- /dev/null
+++ b/res/values-zh-rCN-keyshidden/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"打开键盘以为新播放列表命名,或选择“保存”将其命名为“%s”。"</string>
+ <string name="rename_playlist_same_prompt">"打开键盘以赋予播放列表“<xliff:g id="PLAYLIST">%s</xliff:g>”新名称。"</string>
+ <string name="rename_playlist_diff_prompt">"打开键盘以赋予播放列表“<xliff:g id="PLAYLIST">%s</xliff:g>”新名称,或选择“保存”将其命名为“%s”。"</string>
+</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..fb7ca95
--- /dev/null
+++ b/res/values-zh-rCN/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="onesong">"1 首歌曲"</string>
+ <plurals name="Nsongs">
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> 首歌曲"</item>
+ </plurals>
+ <plurals name="Nsongscomp">
+ <item quantity="other">"共 <xliff:g id="TOTAL_COUNT">%1$d</xliff:g> 首歌曲,其中 <xliff:g id="COUNT_FOR_ARTIST">%2$d</xliff:g> 首"</item>
+ </plurals>
+ <plurals name="Nalbums">
+ <item quantity="one">"1 个专辑"</item>
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> 个专辑"</item>
+ </plurals>
+ <string name="goto_start">"音乐库"</string>
+ <string name="goto_playback">"播放"</string>
+ <string name="party_shuffle">"派对随机播放"</string>
+ <string name="party_shuffle_off">"派对随机播放已关闭"</string>
+ <string name="delete_item">"删除"</string>
+ <string name="shuffle_all">"全部随机播放"</string>
+ <string name="play_all">"全部播放"</string>
+ <string name="delete_artist_desc">"<xliff:g id="ARTIST">%s</xliff:g>的所有歌曲都会从 SD 卡中永久删除。"</string>
+ <string name="delete_album_desc">"整个专辑“<xliff:g id="ALBUM">%s</xliff:g>”会从 SD 卡中永久删除。"</string>
+ <string name="delete_song_desc">"“<xliff:g id="SONG">%s</xliff:g>”会从 SD 卡中永久删除。"</string>
+ <string name="delete_confirm_button_text">"确定"</string>
+ <plurals name="NNNtracksdeleted">
+ <item quantity="one">"已删除 1 首歌曲。"</item>
+ <item quantity="other">"已删除 <xliff:g id="SONGS_TO_DELETE">%d</xliff:g> 首歌曲。"</item>
+ </plurals>
+ <string name="scanning">"正在扫描 SD 卡..."</string>
+ <string name="nowplaying_title">"正在播放"</string>
+ <string name="partyshuffle_title">"派对随机播放"</string>
+ <string name="artists_title">"艺术家"</string>
+ <string name="albums_menu">"专辑"</string>
+ <string name="albums_title">"专辑"</string>
+ <string name="tracks_menu">"歌曲"</string>
+ <string name="tracks_title">"歌曲"</string>
+ <string name="playlists_menu">"播放列表"</string>
+ <string name="playlists_title">"播放列表"</string>
+ <string name="videos_title">"视频"</string>
+ <string name="all_title">"所有媒体"</string>
+ <string name="browse_menu">"艺术家"</string>
+ <string name="search_title">"搜索"</string>
+ <string name="no_tracks_title">"无歌曲"</string>
+ <string name="no_videos_title">"无视频"</string>
+ <string name="no_playlists_title">"无播放列表"</string>
+ <string name="delete_playlist_menu">"删除"</string>
+ <string name="edit_playlist_menu">"编辑"</string>
+ <string name="rename_playlist_menu">"重命名"</string>
+ <string name="playlist_deleted_message">"已删除播放列表。"</string>
+ <string name="playlist_renamed_message">"已重命名播放列表。"</string>
+ <string name="recentlyadded">"最近添加的歌曲"</string>
+ <string name="recentlyadded_title">"最近添加的"</string>
+ <string name="podcasts_listitem">"播客"</string>
+ <string name="podcasts_title">"播客"</string>
+ <string name="sdcard_missing_title">"无 SD 卡"</string>
+ <string name="sdcard_missing_message">"您的手机未插入 SD 卡。"</string>
+ <string name="sdcard_busy_title">"SD 卡不可用"</string>
+ <string name="sdcard_busy_message">"很抱歉,SD 卡正忙。"</string>
+ <string name="sdcard_error_title">"SD 卡错误"</string>
+ <string name="sdcard_error_message">"SD 卡出现错误。"</string>
+ <string name="unknown_artist_name">"未知艺术家"</string>
+ <string name="unknown_album_name">"未知专辑"</string>
+ <string name="shuffle_on_notif">"随机播放已打开。"</string>
+ <string name="shuffle_off_notif">"随机播放已关闭。"</string>
+ <string name="repeat_off_notif">"重复播放已关闭。"</string>
+ <string name="repeat_current_notif">"正在重复播放当前的歌曲。"</string>
+ <string name="repeat_all_notif">"重复播放所有歌曲。"</string>
+ <string name="ringtone_menu">"用作手机铃声"</string>
+ <string name="ringtone_menu_short">"用作铃声"</string>
+ <string name="ringtone_set">"“%s”已设为手机铃声。"</string>
+ <string name="play_selection">"播放"</string>
+ <string name="add_to_playlist">"添加至播放列表"</string>
+ <string name="queue">"当前的播放列表"</string>
+ <string name="new_playlist">"新建"</string>
+ <string name="new_playlist_name_template">"新播放列表 <xliff:g id="NUMBER">%d</xliff:g>"</string>
+ <plurals name="NNNtrackstoplaylist">
+ <item quantity="one">"已向播放列表添加了 1 首歌曲。"</item>
+ <item quantity="other">"%d 首歌曲已添加至播放列表。"</item>
+ </plurals>
+ <string name="emptyplaylist">"选择的播放列表为空。"</string>
+ <string name="create_playlist_create_text">"保存"</string>
+ <string name="create_playlist_overwrite_text">"覆盖"</string>
+ <string name="service_start_error_title">"播放问题"</string>
+ <string name="service_start_error_msg">"很抱歉,无法播放此歌曲。"</string>
+ <string name="service_start_error_button">"确定"</string>
+ <string-array name="weeklist">
+ <item>"1 周"</item>
+ <item>"2 周"</item>
+ <item>"3 周"</item>
+ <item>"4 周"</item>
+ <item>"5 周"</item>
+ <item>"6 周"</item>
+ <item>"7 周"</item>
+ <item>"8 周"</item>
+ <item>"9 周"</item>
+ <item>"10 周"</item>
+ <item>"11 周"</item>
+ <item>"12 周"</item>
+ </string-array>
+ <string name="weekpicker_set">"完成"</string>
+ <string name="weekpicker_title">"设置时间"</string>
+ <string name="save_as_playlist">"另存为播放列表"</string>
+ <string name="clear_playlist">"清除播放列表"</string>
+ <string name="musicbrowserlabel">"音乐"</string>
+ <string name="musicshortcutlabel">"音乐播放列表"</string>
+ <string name="mediaplaybacklabel">"音乐"</string>
+ <string name="videobrowserlabel">"视频"</string>
+ <string name="mediapickerlabel">"音乐"</string>
+ <string name="playback_failed">"很抱歉,此播放器不支持这种类型的音频文件。"</string>
+ <string name="cancel">"取消"</string>
+ <string name="remove_from_playlist">"从播放列表中删除"</string>
+ <string name="streamloadingtext">"正连接至 <xliff:g id="HOST">%s</xliff:g>"</string>
+ <string name="mediasearch">"使用以下内容搜索 %s:"</string>
+ <string name="working_artists">"艺术家..."</string>
+ <string name="working_albums">"专辑..."</string>
+ <string name="working_songs">"歌曲..."</string>
+ <string name="working_playlists">"播放列表..."</string>
+ <string name="loading">"正在载入"</string>
+ <string name="sort_by_track">"曲目"</string>
+ <string name="sort_by_album">"专辑"</string>
+ <string name="sort_by_artist">"艺术家"</string>
+ <string name="music_picker_title">"选择曲目"</string>
+ <string name="gadget_track">"曲目 <xliff:g id="TRACK_NUMBER">%d</xliff:g>"</string>
+</resources>
diff --git a/res/values-zh-rTW-keysexposed/strings.xml b/res/values-zh-rTW-keysexposed/strings.xml
new file mode 100644
index 0000000..2034d1a
--- /dev/null
+++ b/res/values-zh-rTW-keysexposed/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"播放清單名稱"</string>
+ <string name="rename_playlist_same_prompt">"將「<xliff:g id="PLAYLIST">%s</xliff:g>」重新命名為"</string>
+ <string name="rename_playlist_diff_prompt">"將「<xliff:g id="PLAYLIST">%s</xliff:g>」重新命名為"</string>
+</resources>
diff --git a/res/values-zh-rTW-keyshidden/strings.xml b/res/values-zh-rTW-keyshidden/strings.xml
new file mode 100644
index 0000000..09f596e
--- /dev/null
+++ b/res/values-zh-rTW-keyshidden/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="create_playlist_create_text_prompt">"開啟鍵盤為新 [播放清單] 命名,或選取 [儲存] 將之命名為「%s」。"</string>
+ <string name="rename_playlist_same_prompt">"開啟鍵盤以重新命名「<xliff:g id="PLAYLIST">%s</xliff:g>」播放清單。"</string>
+ <string name="rename_playlist_diff_prompt">"開啟鍵盤以重新命名「<xliff:g id="PLAYLIST">%s</xliff:g>」播放清單,或選取 [儲存] 將之命名為「%s」。"</string>
+</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..35ab3b5
--- /dev/null
+++ b/res/values-zh-rTW/strings.xml
@@ -0,0 +1,139 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="onesong">"1 首歌曲"</string>
+ <plurals name="Nsongs">
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> 首歌曲"</item>
+ </plurals>
+ <plurals name="Nsongscomp">
+ <item quantity="other">"專輯的 <xliff:g id="TOTAL_COUNT">%1$d</xliff:g> 首歌曲中,有 <xliff:g id="COUNT_FOR_ARTIST">%2$d</xliff:g> 首該演出者的歌曲"</item>
+ </plurals>
+ <plurals name="Nalbums">
+ <item quantity="one">"1 張專輯"</item>
+ <item quantity="other">"<xliff:g id="COUNT">%d</xliff:g> 專輯"</item>
+ </plurals>
+ <string name="goto_start">"媒體庫"</string>
+ <string name="goto_playback">"播放內容"</string>
+ <string name="party_shuffle">"派對隨機播放"</string>
+ <string name="party_shuffle_off">"關閉派對隨機播放"</string>
+ <string name="delete_item">"刪除"</string>
+ <string name="shuffle_all">"全部隨機播放"</string>
+ <string name="play_all">"全部播放"</string>
+ <string name="delete_artist_desc">"<xliff:g id="ARTIST">%s</xliff:g> 演唱的所有歌曲會從 SD 卡永久刪除。"</string>
+ <string name="delete_album_desc">"「<xliff:g id="ALBUM">%s</xliff:g>」的所有內容會從 SD 卡永久刪除。"</string>
+ <string name="delete_song_desc">"「<xliff:g id="SONG">%s</xliff:g>」會從 SD 卡永久刪除。"</string>
+ <string name="delete_confirm_button_text">"確定"</string>
+ <plurals name="NNNtracksdeleted">
+ <item quantity="one">"已刪除 1 首歌曲。"</item>
+ <item quantity="other">"已刪除 <xliff:g id="SONGS_TO_DELETE">%d</xliff:g> 首歌曲。"</item>
+ </plurals>
+ <string name="scanning">"掃描 SD 卡中..."</string>
+ <string name="nowplaying_title">"目前播放"</string>
+ <string name="partyshuffle_title">"派對隨機播放"</string>
+ <string name="artists_title">"演唱者"</string>
+ <string name="albums_menu">"專輯"</string>
+ <string name="albums_title">"專輯"</string>
+ <string name="tracks_menu">"歌曲"</string>
+ <string name="tracks_title">"歌曲"</string>
+ <string name="playlists_menu">"播放清單"</string>
+ <string name="playlists_title">"播放清單"</string>
+ <string name="videos_title">"影片"</string>
+ <string name="all_title">"所有媒體"</string>
+ <string name="browse_menu">"演唱者"</string>
+ <string name="search_title">"搜尋"</string>
+ <string name="no_tracks_title">"沒有歌曲"</string>
+ <string name="no_videos_title">"沒有影片"</string>
+ <string name="no_playlists_title">"沒有播放清單"</string>
+ <string name="delete_playlist_menu">"刪除"</string>
+ <string name="edit_playlist_menu">"編輯"</string>
+ <string name="rename_playlist_menu">"重新命名"</string>
+ <string name="playlist_deleted_message">"播放清單已刪除。"</string>
+ <string name="playlist_renamed_message">"已重新命名播放清單。"</string>
+ <string name="recentlyadded">"最近新增的項目"</string>
+ <string name="recentlyadded_title">"最近新增的歌曲"</string>
+ <string name="podcasts_listitem">"Podcast"</string>
+ <string name="podcasts_title">"Podcast"</string>
+ <string name="sdcard_missing_title">"沒有 SD 卡"</string>
+ <string name="sdcard_missing_message">"您的手機未插入 SD 卡。"</string>
+ <string name="sdcard_busy_title">"無法使用 SD 卡"</string>
+ <string name="sdcard_busy_message">"抱歉,SD 卡忙碌中。"</string>
+ <string name="sdcard_error_title">"SD 卡錯誤"</string>
+ <string name="sdcard_error_message">"SD 卡發生錯誤。"</string>
+ <string name="unknown_artist_name">"未知的演唱者"</string>
+ <string name="unknown_album_name">"未知的專輯"</string>
+ <string name="shuffle_on_notif">"已開啟隨機播放。"</string>
+ <string name="shuffle_off_notif">"已關閉隨機播放。"</string>
+ <string name="repeat_off_notif">"已關閉重複播放。"</string>
+ <string name="repeat_current_notif">"重複播放目前歌曲。"</string>
+ <string name="repeat_all_notif">"重複播放所有歌曲。"</string>
+ <string name="ringtone_menu">"設成來電鈴聲"</string>
+ <string name="ringtone_menu_short">"設成鈴聲"</string>
+ <string name="ringtone_set">"已將「%s」設為來電鈴聲。"</string>
+ <string name="play_selection">"播放"</string>
+ <string name="add_to_playlist">"新增至播放清單"</string>
+ <string name="queue">"目前播放清單"</string>
+ <string name="new_playlist">"新增"</string>
+ <string name="new_playlist_name_template">"新播放清單 <xliff:g id="NUMBER">%d</xliff:g>"</string>
+ <plurals name="NNNtrackstoplaylist">
+ <item quantity="one">"已將 1 首歌曲加入播放清單。"</item>
+ <item quantity="other">"%d 歌曲已新增至播放清單。"</item>
+ </plurals>
+ <string name="emptyplaylist">"您選取的播放清單是空的。"</string>
+ <string name="create_playlist_create_text">"儲存"</string>
+ <string name="create_playlist_overwrite_text">"覆寫"</string>
+ <string name="service_start_error_title">"播放內容問題"</string>
+ <string name="service_start_error_msg">"抱歉,此歌曲無法播放。"</string>
+ <string name="service_start_error_button">"確定"</string>
+ <string-array name="weeklist">
+ <item>"1 週"</item>
+ <item>"2 週"</item>
+ <item>"3 週"</item>
+ <item>"4 週"</item>
+ <item>"5 週"</item>
+ <item>"6 週"</item>
+ <item>"7 週"</item>
+ <item>"8 週"</item>
+ <item>"9 週"</item>
+ <item>"10 週"</item>
+ <item>"11 週"</item>
+ <item>"12 週"</item>
+ </string-array>
+ <string name="weekpicker_set">"完成"</string>
+ <string name="weekpicker_title">"設定時間"</string>
+ <string name="save_as_playlist">"另存為播放清單"</string>
+ <string name="clear_playlist">"清除播放清單"</string>
+ <string name="musicbrowserlabel">"音樂"</string>
+ <string name="musicshortcutlabel">"音樂播放清單"</string>
+ <string name="mediaplaybacklabel">"音樂"</string>
+ <string name="videobrowserlabel">"影片"</string>
+ <string name="mediapickerlabel">"音樂"</string>
+ <string name="playback_failed">"抱歉,撥放器不支援此音訊檔格式。"</string>
+ <string name="cancel">"取消"</string>
+ <string name="remove_from_playlist">"從播放清單移除"</string>
+ <string name="streamloadingtext">"連線到 <xliff:g id="HOST">%s</xliff:g>"</string>
+ <string name="mediasearch">"搜尋 %s,使用:"</string>
+ <string name="working_artists">"演唱者..."</string>
+ <string name="working_albums">"專輯..."</string>
+ <string name="working_songs">"歌曲..."</string>
+ <string name="working_playlists">"播放清單..."</string>
+ <string name="loading">"載入中"</string>
+ <string name="sort_by_track">"曲目"</string>
+ <string name="sort_by_album">"專輯"</string>
+ <string name="sort_by_artist">"演唱者"</string>
+ <string name="music_picker_title">"選取音樂曲目"</string>
+ <string name="gadget_track">"音軌 <xliff:g id="TRACK_NUMBER">%d</xliff:g>"</string>
+</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
new file mode 100644
index 0000000..5ebc209
--- /dev/null
+++ b/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <color name="gadget_text">#ffffffff</color>
+</resources>
+
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
new file mode 100644
index 0000000..d7d705d
--- /dev/null
+++ b/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!-- Size of gadget album art cutout -->
+ <dimen name="gadget_cutout">198dip</dimen>
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..855b443
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,286 @@
+<?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">
+
+ <!-- used in various places to indicate there is one song for a given artist or album -->
+ <string name="onesong">1 song</string>
+ <!-- used in various places to indicate there is some number other than one songs for a given artist or album. -->
+ <plurals name="Nsongs">
+ <item quantity="other"><xliff:g id="count">%d</xliff:g> songs</item>
+ </plurals>
+
+ <!-- shows how many songs on the album are by the selected artist, out of how many total, if those two numbers are different -->
+ <plurals name="Nsongscomp">
+ <item quantity="other"><xliff:g id="count_for_artist">%2$d</xliff:g> of <xliff:g id="total_count">%1$d</xliff:g> songs</item>
+ </plurals>
+
+ <!-- Used in artists list view, indicates how many albums exist for a given artist. -->
+ <plurals name="Nalbums">
+ <!-- number of albums is one -->
+ <item quantity="one">1 album</item>
+ <!-- number of albums is not equal to one -->
+ <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
+ -->
+ <skip/>
+ <!-- Do not translate. Duration format. -->
+ <string name="durationformat"><xliff:g id="format">%2$d:%5$02d</xliff:g></string>
+
+ <!-- Menu item that takes the user back to the top level screen of the music player -->
+ <string name="goto_start">Library</string>
+ <!-- Menu item that takes the user to the "now playing" screen of the music player -->
+ <string name="goto_playback">Playback</string>
+ <!-- Menu item that switches the music player in to party shuffle mode -->
+ <string name="party_shuffle">Party shuffle</string>
+ <!-- Menu item that switches the music player out of party shuffle mode -->
+ <string name="party_shuffle_off">Party shuffle off</string>
+ <!-- Menu item that deletes the currently selected item, which might be a single song, or a collection of songs.
+ The user will be prompted to confirm before deletion actually takes place -->
+ <string name="delete_item">Delete</string>
+ <!-- Menu item to play back all the songs in the currently showing list in shuffle mode -->
+ <string name="shuffle_all">Shuffle all</string>
+ <!-- Menu item to play back all the songs in the currently showing list in list order -->
+ <string name="play_all">Play all</string>
+
+ <!-- Delete confirmation dialog when deleting an entire artist -->
+ <string name="delete_artist_desc">All songs by <xliff:g id="artist">%s</xliff:g> will be permanently deleted from the SD card.</string>
+
+ <!-- Delete confirmation dialog when deleting an entire album -->
+ <string name="delete_album_desc">The entire album \"<xliff:g id="album">%s</xliff:g>\" will be permanently deleted from the SD card.</string>
+
+ <!-- Delete confirmation dialog when deleting a single song -->
+ <string name="delete_song_desc">\"<xliff:g id="song">%s</xliff:g>\" will be permanently deleted from the SD card.</string>
+
+ <!-- Delete confirmation dialog, confirmation button text -->
+ <string name="delete_confirm_button_text">OK</string>
+ <!-- Toast confirming that song(s) was/were deleted. -->
+ <plurals name="NNNtracksdeleted">
+ <!-- delete confirmation message for 1 song -->
+ <item quantity="one">1 song was deleted.</item>
+ <!-- delete confirmation message for 0 or more than 1 songs -->
+ <item quantity="other"><xliff:g id="songs_to_delete">%d</xliff:g> songs were deleted.</item>
+ </plurals>
+
+ <!-- shown in dialog while the media scanner is starting up -->
+ <string name="scanning">Scanning SD card\u2026</string>
+
+ <!-- title of the "current playlist" screen when not in party shuffle mode -->
+ <string name="nowplaying_title">Now playing</string>
+ <!-- title of the "current playlist" screen when in party shuffle mode-->
+ <string name="partyshuffle_title">Party shuffle</string>
+ <!-- Artist screen title -->
+ <string name="artists_title">Artists</string>
+ <!-- Category label on Library screen -->
+ <string name="albums_menu">Albums</string>
+ <!-- Albums screen title -->
+ <string name="albums_title">Albums</string>
+ <!-- Category label on Library screen -->
+ <string name="tracks_menu">Songs</string>
+ <!-- Songs screen title -->
+ <string name="tracks_title">Songs</string>
+ <!-- Category label on Library screen -->
+ <string name="playlists_menu">Playlists</string>
+ <!-- Playlists screen title -->
+ <string name="playlists_title">Playlists</string>
+ <!-- Videos screen title -->
+ <string name="videos_title">Videos</string>
+ <!-- All media screen title -->
+ <string name="all_title">All media</string>
+ <!-- Category label on Library screen -->
+ <string name="browse_menu">Artists</string>
+ <!-- Library screen, menu item -->
+ <string name="search_title">Search</string>
+ <!-- Title of screen when there are no songs, or if the SD card is busy -->
+ <string name="no_tracks_title">No songs</string>
+ <!-- Title of screen when there are no videos, or if the SD card is busy -->
+ <string name="no_videos_title">No videos</string>
+ <!-- Title of screen when there are no playlists, or if the SD card is busy -->
+ <string name="no_playlists_title">No playlists</string>
+ <!-- Playlist context menu item to delete the selected playlist. -->
+ <string name="delete_playlist_menu">Delete</string>
+ <!-- Playlist context menu item to edit the selected playlist -->
+ <string name="edit_playlist_menu">Edit</string>
+ <!-- Playlist context menu item to rename the selected playlist-->
+ <string name="rename_playlist_menu">Rename</string>
+ <!-- Transient popup message shown after deleting a playlist -->
+ <string name="playlist_deleted_message">Playlist deleted.</string>
+ <!-- Transient popup message shown after renaming a playlist -->
+ <string name="playlist_renamed_message">Playlist renamed.</string>
+ <!-- The name of the pseudo-playlist that holds all the recently added files, shown in list view -->
+ <string name="recentlyadded">Recently added</string>
+ <!-- The name of the pseudo-playlist that holds all the recently added files, shown in title bar of songs list -->
+ <string name="recentlyadded_title">Recently added</string>
+ <!-- The name of the pseudo-playlist that holds all the podcasts, shown in list view -->
+ <string name="podcasts_listitem">Podcasts</string>
+ <!-- The name of the pseudo-playlist that holds all the podcasts, shown in title bar of songs list -->
+ <string name="podcasts_title">Podcasts</string>
+ <!-- Title of screen when no sd card is present -->
+ <string name="sdcard_missing_title">No SD card</string>
+ <!-- label underneath icon used to indicate that no sd card is present -->
+ <string name="sdcard_missing_message">Your phone does not have an SD card inserted.</string>
+ <!-- label underneath icon used to indicate that the sd card is present, but currently unavailable -->
+ <string name="sdcard_busy_title">SD card unavailable</string>
+ <!-- label underneath icon used to indicate sd card is mounted to your computer via USB -->
+ <string name="sdcard_busy_message">Sorry, your SD card is busy.</string>
+ <!-- Title of screen when there was an error accessing the sd card -->
+ <string name="sdcard_error_title">SD card error</string>
+ <!-- label underneath icon used to indicate there was an error accessing the sd card -->
+ <string name="sdcard_error_message">An error was encountered on your SD card.</string>
+ <!-- Default name of artist that doesn't have a name in the metadata -->
+ <string name="unknown_artist_name">Unknown artist</string>
+ <!-- Default name of album that doesn't have a name in the metadata -->
+ <string name="unknown_album_name">Unknown album</string>
+ <!-- Toast after turning shuffle on -->
+ <string name="shuffle_on_notif">Shuffle is on.</string>
+ <!-- Toast after turning shuffle off -->
+ <string name="shuffle_off_notif">Shuffle is off.</string>
+ <!-- Toast after turning repeat off -->
+ <string name="repeat_off_notif">Repeat is off.</string>
+ <!-- Toast after turning single repeat on -->
+ <string name="repeat_current_notif">Repeating current song.</string>
+ <!-- Toast after turning repeat all on -->
+ <string name="repeat_all_notif">Repeating all songs.</string>
+ <!-- Individual song context menu item -->
+ <string name="ringtone_menu">Use as phone ringtone</string>
+ <!-- Menu item -->
+ <string name="ringtone_menu_short">Use as ringtone</string>
+ <!-- Toast after setting a song as phone ringtone -->
+ <string name="ringtone_set">\"%s\" set as phone ringtone.</string>
+ <!-- Context menu item -->
+ <string name="play_selection">Play</string>
+ <!-- Context menu item -->
+ <string name="add_to_playlist">Add to playlist</string>
+ <!-- Context menu item -->
+ <string name="queue">Current playlist</string>
+ <!-- Context menu item -->
+ <string name="new_playlist">New</string>
+ <!-- Template for newly created playlist name -->
+ <string name="new_playlist_name_template">New playlist <xliff:g id="number">%d</xliff:g></string>
+ <!-- Toasts after adding song(s) to playlists -->
+ <plurals name="NNNtrackstoplaylist">
+ <!-- message shown when one song was added -->
+ <item quantity="one">1 song added to playlist.</item>
+ <!-- message shown when zero or more than one song was added -->
+ <item quantity="other">%d songs added to playlist.</item>
+ </plurals>
+ <!-- Toast after selecting an empty playlist -->
+ <string name="emptyplaylist">Selected playlist is empty.</string>
+ <!-- Button name when saving a playlist -->
+ <string name="create_playlist_create_text">Save</string>
+ <!-- Button name when saving a playlist and the new playlist will overwrite an existing one -->
+ <string name="create_playlist_overwrite_text">Overwrite</string>
+ <!-- Dialog box title -->
+ <string name="service_start_error_title">Playback problem</string>
+ <!-- Dialog box message -->
+ <string name="service_start_error_msg">Sorry, the song could not be played.</string>
+ <!-- Dialog box button -->
+ <string name="service_start_error_button">OK</string>
+ <!-- Time span edit options that appear when editing system playlist "Recently added" -->
+ <!-- Used to indicate the number of weeks the "recently added" playlist covers in a selector widget -->
+ <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>
+ <!-- Button name in time span picker -->
+ <string name="weekpicker_set">Done</string>
+ <!-- Title of time span picker -->
+ <string name="weekpicker_title">Set time</string>
+ <!-- Do not translate. Background color for currently dragged item in playlist edit mode. -->
+ <color name="dragndrop_background">#e0103010</color>
+ <!-- Do not translate. Background color for albums in the artists list view. -->
+ <color name="expanding_child_background">#ff404040</color>
+
+ <!-- menu item to save the current list as a new playlist -->
+ <string name="save_as_playlist">Save as playlist</string>
+ <!-- menu item to clear the current playlist -->
+ <string name="clear_playlist">Clear playlist</string>
+
+ <!-- Activity label. This might show up in the activity-picker -->
+ <string name="musicbrowserlabel">Music</string>
+ <!-- Activity label. This might show up in the activity-picker -->
+ <string name="musicshortcutlabel">Music playlist</string>
+ <!-- Activity label. This might show up in the activity-picker -->
+ <string name="mediaplaybacklabel">Music</string>
+ <!-- Activity label. This might show up in the activity-picker -->
+ <string name="videobrowserlabel">Videos</string>
+ <!-- Activity label. This might show up in the activity-picker -->
+ <string name="mediapickerlabel">Music</string>
+
+ <!-- Shown as a transient message whenever a file fails to play -->
+ <string name="playback_failed">Sorry, the player does not support this type of audio file.</string>
+
+ <!-- Text for the "cancel" button in the "delete" and "create playlist" confirmation dialogs -->
+ <string name="cancel">Cancel</string>
+
+ <!-- context menu item to remove the selected item from the playlist -->
+ <string name="remove_from_playlist">Remove from playlist</string>
+
+ <!-- shown when connecting to a music stream, before it starts playing -->
+ <string name="streamloadingtext">Connecting to <xliff:g id="host">%s</xliff:g></string>
+
+ <!-- title of contextual music search menu -->
+ <string name="mediasearch">Search for %s using:</string>
+
+ <!-- Shown in the title bar while the list of artists is being retrieved in the background -->
+ <string name="working_artists">Artists\u2026</string>
+ <!-- Shown in the title bar while the list of albums is being retrieved in the background -->
+ <string name="working_albums">Albums\u2026</string>
+ <!-- Shown in the title bar while the list of songs is being retrieved in the background -->
+ <string name="working_songs">Songs\u2026</string>
+ <!-- Shown in the title bar while the list of playlists is being retrieved in the background -->
+ <string name="working_playlists">Playlists\u2026</string>
+
+ <!-- Shown in the music picker while loading the music database. -->
+ <string name="loading">Loading</string>
+ <!-- Menu in music picker to sort the list by track/song name. -->
+ <string name="sort_by_track">Tracks</string>
+ <!-- Menu in music picker to sort the list by album name. -->
+ <string name="sort_by_album">Albums</string>
+ <!-- Menu in music picker to sort the list by artist name. -->
+ <string name="sort_by_artist">Artists</string>
+ <!-- Title of the music picker activity. -->
+ <string name="music_picker_title">Select music track</string>
+
+ <!-- Title for track number in music gadget -->
+ <string name="gadget_track">Track <xliff:g id="track_number">%d</xliff:g></string>
+
+ <!-- Mixes together track number and track title in music gadget -->
+ <string name="gadget_track_num_title" translatable="false"><xliff:g id="track_number">%d</xliff:g>. <xliff:g id="track_title">%s</xliff:g></string>
+</resources>
+
diff --git a/res/values/strings2.xml b/res/values/strings2.xml
new file mode 100644
index 0000000..4cbf119
--- /dev/null
+++ b/res/values/strings2.xml
@@ -0,0 +1,24 @@
+<?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>
+ <!-- Do not translate. This is the separator character used when building the string that shows number of albums and songs. -->
+ <string name="albumsongseparator">, </string>
+</resources>
+
diff --git a/res/xml/gadget_info.xml b/res/xml/gadget_info.xml
new file mode 100644
index 0000000..bbaeaa3
--- /dev/null
+++ b/res/xml/gadget_info.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<gadget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="294dip"
+ android:minHeight="72dip"
+ android:updatePeriodMillis="0"
+ android:initialLayout="@layout/gadget"
+ >
+</gadget-provider>
diff --git a/res/xml/searchable.xml b/res/xml/searchable.xml
new file mode 100644
index 0000000..1bff884
--- /dev/null
+++ b/res/xml/searchable.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<searchable xmlns:android="http://schemas.android.com/apk/res/android"
+ android:label="@string/search_title"
+
+ android:searchSuggestAuthority="media"
+ android:searchSuggestPath="external/audio"
+ android:searchSuggestIntentAction="android.intent.action.VIEW"
+/>
diff --git a/src/com/android/music/AlbumBrowserActivity.java b/src/com/android/music/AlbumBrowserActivity.java
new file mode 100644
index 0000000..c28b7da
--- /dev/null
+++ b/src/com/android/music/AlbumBrowserActivity.java
@@ -0,0 +1,638 @@
+/*
+ * 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.AsyncQueryHandler;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+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.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.Adapter;
+import android.widget.AlphabetIndexer;
+import android.widget.CursorAdapter;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.SectionIndexer;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+
+import java.text.Collator;
+
+public class AlbumBrowserActivity extends ListActivity
+ implements View.OnCreateContextMenuListener, MusicUtils.Defs
+{
+ private String mCurrentAlbumId;
+ private String mCurrentAlbumName;
+ private String mCurrentArtistNameForAlbum;
+ private AlbumListAdapter mAdapter;
+ private boolean mAdapterSent;
+ private final static int SEARCH = CHILD_MENU_BASE;
+
+ 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);
+
+ setContentView(R.layout.media_picker_activity);
+ ListView lv = getListView();
+ lv.setFastScrollEnabled(true);
+ lv.setOnCreateContextMenuListener(this);
+ lv.setTextFilterEnabled(true);
+
+ mAdapter = (AlbumListAdapter) getLastNonConfigurationInstance();
+ if (mAdapter == null) {
+ //Log.i("@@@", "starting query");
+ mAdapter = new AlbumListAdapter(
+ getApplication(),
+ this,
+ R.layout.track_list_item,
+ mAlbumCursor,
+ new String[] {},
+ new int[] {});
+ setListAdapter(mAdapter);
+ setTitle(R.string.working_albums);
+ getAlbumCursor(mAdapter.getQueryHandler(), null);
+ } else {
+ mAdapter.setActivity(this);
+ setListAdapter(mAdapter);
+ mAlbumCursor = mAdapter.getCursor();
+ if (mAlbumCursor != null) {
+ init(mAlbumCursor);
+ } else {
+ getAlbumCursor(mAdapter.getQueryHandler(), null);
+ }
+ }
+ }
+
+ @Override
+ public Object onRetainNonConfigurationInstance() {
+ mAdapterSent = true;
+ return mAdapter;
+ }
+
+ @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 (!mAdapterSent) {
+ Cursor c = mAdapter.getCursor();
+ if (c != null) {
+ c.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() {
+ @Override
+ public void handleMessage(Message msg) {
+ getAlbumCursor(mAdapter.getQueryHandler(), null);
+ }
+ };
+
+ @Override
+ public void onPause() {
+ unregisterReceiver(mTrackListListener);
+ mReScanHandler.removeCallbacksAndMessages(null);
+ super.onPause();
+ }
+
+ public void init(Cursor c) {
+
+ mAdapter.changeCursor(c); // also sets mAlbumCursor
+
+ if (mAlbumCursor == null) {
+ MusicUtils.displayDatabaseError(this);
+ closeContextMenu();
+ mReScanHandler.sendEmptyMessageDelayed(0, 1000);
+ return;
+ }
+
+ MusicUtils.hideDatabaseError(this);
+ setTitle();
+ }
+
+ private void setTitle() {
+ CharSequence fancyName = "";
+ if (mAlbumCursor != null && 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);
+ }
+
+ @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);
+ menu.add(0, SEARCH, 0, R.string.search_title);
+
+ 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));
+ mCurrentArtistNameForAlbum = mAlbumCursor.getString(
+ mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST));
+ 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;
+ }
+ case SEARCH:
+ doSearch();
+ return true;
+
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ void doSearch() {
+ CharSequence title = null;
+ String query = null;
+
+ Intent i = new Intent();
+ i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
+
+ title = mCurrentAlbumName;
+ query = mCurrentArtistNameForAlbum + " " + mCurrentAlbumName;
+ i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, mCurrentArtistNameForAlbum);
+ i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, mCurrentAlbumName);
+ i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE);
+ title = getString(R.string.mediasearch, title);
+ i.putExtra(SearchManager.QUERY, query);
+
+ startActivity(Intent.createChooser(i, title));
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ switch (requestCode) {
+ case SCAN_DONE:
+ if (resultCode == RESULT_CANCELED) {
+ finish();
+ } else {
+ getAlbumCursor(mAdapter.getQueryHandler(), null);
+ }
+ break;
+
+ case NEW_PLAYLIST:
+ if (resultCode == RESULT_OK) {
+ Uri uri = intent.getData();
+ 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(AsyncQueryHandler async, String filter) {
+ StringBuilder where = new StringBuilder();
+ where.append(MediaStore.Audio.Albums.ALBUM + " != ''");
+
+ // Add in the filtering constraints
+ String [] keywords = null;
+ if (filter != null) {
+ String [] searchWords = filter.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 = null;
+ if (mArtistId != null) {
+ if (async != null) {
+ async.startQuery(0, null,
+ 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.Artists.Albums.getContentUri("external",
+ Long.valueOf(mArtistId)),
+ cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
+ }
+ } else {
+ if (async != null) {
+ async.startQuery(0, null,
+ MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
+ 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;
+ }
+
+ static class AlbumListAdapter extends SimpleCursorAdapter implements SectionIndexer {
+
+ private final Drawable mNowPlayingOverlay;
+ private final BitmapDrawable mDefaultAlbumIcon;
+ private int mAlbumIdx;
+ private int mArtistIdx;
+ private int mNumSongsIdx;
+ private 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];
+ private AlphabetIndexer mIndexer;
+ private AlbumBrowserActivity mActivity;
+ private AsyncQueryHandler mQueryHandler;
+ private String mConstraint = null;
+ private boolean mConstraintIsValid = false;
+
+ class ViewHolder {
+ TextView line1;
+ TextView line2;
+ TextView duration;
+ ImageView play_indicator;
+ ImageView icon;
+ }
+
+ class QueryHandler extends AsyncQueryHandler {
+ QueryHandler(ContentResolver res) {
+ super(res);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ //Log.i("@@@", "query complete");
+ mActivity.init(cursor);
+ }
+ }
+
+ AlbumListAdapter(Context context, AlbumBrowserActivity currentactivity,
+ int layout, Cursor cursor, String[] from, int[] to) {
+ super(context, layout, cursor, from, to);
+
+ mActivity = currentactivity;
+ mQueryHandler = new QueryHandler(context.getContentResolver());
+
+ mUnknownAlbum = context.getString(R.string.unknown_album_name);
+ mUnknownArtist = context.getString(R.string.unknown_artist_name);
+ mAlbumSongSeparator = context.getString(R.string.albumsongseparator);
+
+ Resources r = context.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);
+ getColumnIndices(cursor);
+ mResources = context.getResources();
+ }
+
+ private void getColumnIndices(Cursor cursor) {
+ if (cursor != null) {
+ 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);
+
+ if (mIndexer != null) {
+ mIndexer.setCursor(cursor);
+ } else {
+ mIndexer = new MusicAlphabetIndexer(cursor, mAlbumIdx, mResources.getString(
+ com.android.internal.R.string.fast_scroll_alphabet));
+ }
+ }
+ }
+
+ public void setActivity(AlbumBrowserActivity newactivity) {
+ mActivity = newactivity;
+ }
+
+ public AsyncQueryHandler getQueryHandler() {
+ return mQueryHandler;
+ }
+
+ @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(0, 0, 1, 0);
+ 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;
+ boolean unknown = name.equals(MediaFile.UNKNOWN_STRING);
+ if (unknown) {
+ displayname = mUnknownAlbum;
+ }
+ vh.line1.setText(displayname);
+
+ name = cursor.getString(mArtistIdx);
+ displayname = name;
+ if (MediaFile.UNKNOWN_STRING.equals(name)) {
+ displayname = mUnknownArtist;
+ }
+ vh.line2.setText(displayname);
+
+ 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 (unknown || 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) {
+ if (cursor != mActivity.mAlbumCursor) {
+ mActivity.mAlbumCursor = cursor;
+ getColumnIndices(cursor);
+ super.changeCursor(cursor);
+ }
+ }
+
+ @Override
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ String s = constraint.toString();
+ if (mConstraintIsValid && (
+ (s == null && mConstraint == null) ||
+ (s != null && s.equals(mConstraint)))) {
+ return getCursor();
+ }
+ Cursor c = mActivity.getAlbumCursor(null, s);
+ mConstraint = s;
+ mConstraintIsValid = true;
+ return c;
+ }
+
+ public Object[] getSections() {
+ return mIndexer.getSections();
+ }
+
+ public int getPositionForSection(int section) {
+ return mIndexer.getPositionForSection(section);
+ }
+
+ public int getSectionForPosition(int position) {
+ return 0;
+ }
+ }
+
+ private Cursor mAlbumCursor;
+ private String mArtistId;
+ private boolean mHasHeader = false;
+}
+
diff --git a/src/com/android/music/ArtistAlbumBrowserActivity.java b/src/com/android/music/ArtistAlbumBrowserActivity.java
new file mode 100644
index 0000000..2ed367b
--- /dev/null
+++ b/src/com/android/music/ArtistAlbumBrowserActivity.java
@@ -0,0 +1,817 @@
+/*
+ * 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.music.QueryBrowserActivity.QueryListAdapter.QueryHandler;
+
+import android.app.ExpandableListActivity;
+import android.app.SearchManager;
+import android.content.AsyncQueryHandler;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+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.CursorAdapter;
+import android.widget.CursorTreeAdapter;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.ImageView;
+import android.widget.SectionIndexer;
+import android.widget.SimpleCursorTreeAdapter;
+import android.widget.TextView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+
+import java.text.Collator;
+
+
+public class ArtistAlbumBrowserActivity extends ExpandableListActivity
+ implements View.OnCreateContextMenuListener, MusicUtils.Defs
+{
+ private String mCurrentArtistId;
+ private String mCurrentArtistName;
+ private String mCurrentAlbumId;
+ private String mCurrentAlbumName;
+ private String mCurrentArtistNameForAlbum;
+ private ArtistAlbumListAdapter mAdapter;
+ private boolean mAdapterSent;
+ private final static int SEARCH = CHILD_MENU_BASE;
+
+ 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);
+
+ setContentView(R.layout.media_picker_activity_expanding);
+ ExpandableListView lv = getExpandableListView();
+ lv.setFastScrollEnabled(true);
+ lv.setOnCreateContextMenuListener(this);
+ lv.setTextFilterEnabled(true);
+
+ mAdapter = (ArtistAlbumListAdapter) getLastNonConfigurationInstance();
+ if (mAdapter == null) {
+ //Log.i("@@@", "starting query");
+ mAdapter = new ArtistAlbumListAdapter(
+ getApplication(),
+ this,
+ null, // cursor
+ R.layout.track_list_item_group,
+ new String[] {},
+ new int[] {},
+ R.layout.track_list_item_child,
+ new String[] {},
+ new int[] {});
+ setListAdapter(mAdapter);
+ setTitle(R.string.working_artists);
+ getArtistCursor(mAdapter.getQueryHandler(), null);
+ } else {
+ mAdapter.setActivity(this);
+ setListAdapter(mAdapter);
+ mArtistCursor = mAdapter.getCursor();
+ if (mArtistCursor != null) {
+ init(mArtistCursor);
+ } else {
+ getArtistCursor(mAdapter.getQueryHandler(), null);
+ }
+ }
+ }
+
+ @Override
+ public Object onRetainNonConfigurationInstance() {
+ mAdapterSent = true;
+ return mAdapter;
+ }
+
+ @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 (!mAdapterSent) {
+ Cursor c = mAdapter.getCursor();
+ if (c != null) {
+ c.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() {
+ @Override
+ public void handleMessage(Message msg) {
+ getArtistCursor(mAdapter.getQueryHandler(), null);
+ }
+ };
+
+ @Override
+ public void onPause() {
+ unregisterReceiver(mTrackListListener);
+ mReScanHandler.removeCallbacksAndMessages(null);
+ super.onPause();
+ }
+
+ public void init(Cursor c) {
+
+ mAdapter.changeCursor(c); // also sets mArtistCursor
+
+ if (mArtistCursor == null) {
+ MusicUtils.displayDatabaseError(this);
+ closeContextMenu();
+ mReScanHandler.sendEmptyMessageDelayed(0, 1000);
+ return;
+ }
+
+ MusicUtils.hideDatabaseError(this);
+ setTitle();
+ }
+
+ private void setTitle() {
+ setTitle(R.string.artists_title);
+ }
+
+ @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);
+ Cursor c = (Cursor) getExpandableListAdapter().getChild(groupPosition, childPosition);
+ String album = c.getString(c.getColumnIndex(MediaStore.Audio.Albums.ALBUM));
+ if (album.equals(MediaFile.UNKNOWN_STRING)) {
+ // unknown album, so we should include the artist ID to limit the songs to songs only by that artist
+ mArtistCursor.moveToPosition(groupPosition);
+ mCurrentArtistId = mArtistCursor.getString(mArtistCursor.getColumnIndex(MediaStore.Audio.Artists._ID));
+ 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);
+ menu.add(0, SEARCH, 0, R.string.search_title);
+
+ 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));
+ gpos = gpos - getExpandableListView().getHeaderViewsCount();
+ mArtistCursor.moveToPosition(gpos);
+ mCurrentArtistNameForAlbum = mArtistCursor.getString(
+ mArtistCursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST));
+ 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;
+ }
+
+ case SEARCH:
+ doSearch();
+ return true;
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ void doSearch() {
+ CharSequence title = null;
+ String query = null;
+
+ Intent i = new Intent();
+ i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
+
+ if (mCurrentArtistId != null) {
+ title = mCurrentArtistName;
+ query = mCurrentArtistName;
+ i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, mCurrentArtistName);
+ i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE);
+ } else {
+ title = mCurrentAlbumName;
+ query = mCurrentArtistNameForAlbum + " " + mCurrentAlbumName;
+ i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, mCurrentArtistNameForAlbum);
+ i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, mCurrentAlbumName);
+ i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE);
+ }
+ title = getString(R.string.mediasearch, title);
+ i.putExtra(SearchManager.QUERY, query);
+
+ startActivity(Intent.createChooser(i, title));
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ switch (requestCode) {
+ case SCAN_DONE:
+ if (resultCode == RESULT_CANCELED) {
+ finish();
+ } else {
+ getArtistCursor(mAdapter.getQueryHandler(), null);
+ }
+ break;
+
+ case NEW_PLAYLIST:
+ if (resultCode == RESULT_OK) {
+ Uri uri = intent.getData();
+ 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(AsyncQueryHandler async, String filter) {
+
+ StringBuilder where = new StringBuilder();
+ where.append(MediaStore.Audio.Artists.ARTIST + " != ''");
+
+ // Add in the filtering constraints
+ String [] keywords = null;
+ if (filter != null) {
+ String [] searchWords = filter.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 = null;
+ if (async != null) {
+ async.startQuery(0, null, MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
+ cols, whereclause , keywords, MediaStore.Audio.Artists.ARTIST_KEY);
+ } else {
+ ret = MusicUtils.query(this, MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI,
+ cols, whereclause , keywords, MediaStore.Audio.Artists.ARTIST_KEY);
+ }
+ return ret;
+ }
+
+ static class ArtistAlbumListAdapter extends SimpleCursorTreeAdapter implements SectionIndexer {
+
+ private final Drawable mNowPlayingOverlay;
+ private final BitmapDrawable mDefaultAlbumIcon;
+ private int mGroupArtistIdIdx;
+ private int mGroupArtistIdx;
+ private int mGroupAlbumIdx;
+ private int mGroupSongIdx;
+ private final Context mContext;
+ 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];
+ private final Object[] mFormatArgs3 = new Object[3];
+ private MusicAlphabetIndexer mIndexer;
+ private ArtistAlbumBrowserActivity mActivity;
+ private AsyncQueryHandler mQueryHandler;
+ private String mConstraint = null;
+ private boolean mConstraintIsValid = false;
+
+ class ViewHolder {
+ TextView line1;
+ TextView line2;
+ ImageView play_indicator;
+ ImageView icon;
+ }
+
+ class QueryHandler extends AsyncQueryHandler {
+ QueryHandler(ContentResolver res) {
+ super(res);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ //Log.i("@@@", "query complete");
+ mActivity.init(cursor);
+ }
+ }
+
+ ArtistAlbumListAdapter(Context context, ArtistAlbumBrowserActivity currentactivity,
+ Cursor cursor, int glayout, String[] gfrom, int[] gto,
+ int clayout, String[] cfrom, int[] cto) {
+ super(context, cursor, glayout, gfrom, gto, clayout, cfrom, cto);
+ mActivity = currentactivity;
+ mQueryHandler = new QueryHandler(context.getContentResolver());
+
+ Resources r = context.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);
+
+ mContext = context;
+ getColumnIndices(cursor);
+ 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);
+ }
+
+ private void getColumnIndices(Cursor cursor) {
+ if (cursor != null) {
+ 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);
+ if (mIndexer != null) {
+ mIndexer.setCursor(cursor);
+ } else {
+ mIndexer = new MusicAlphabetIndexer(cursor, mGroupArtistIdx,
+ mResources.getString(com.android.internal.R.string.fast_scroll_alphabet));
+ }
+ }
+ }
+
+ public void setActivity(ArtistAlbumBrowserActivity newactivity) {
+ mActivity = newactivity;
+ }
+
+ public AsyncQueryHandler getQueryHandler() {
+ return mQueryHandler;
+ }
+
+ @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(0, 0, 1, 0);
+ 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(0, 0, 1, 0);
+ 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.makeAlbumsLabel(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;
+ boolean unknown = name.equals(MediaFile.UNKNOWN_STRING);
+ if (unknown) {
+ displayname = mUnknownAlbum;
+ }
+ vh.line1.setText(displayname);
+
+ int numsongs = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.NUMBER_OF_SONGS));
+ int numartistsongs = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST));
+ 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 (unknown) {
+ numsongs = numartistsongs;
+ }
+
+ if (numsongs == 1) {
+ builder.append(context.getString(R.string.onesong));
+ } else {
+ if (numsongs == numartistsongs) {
+ final Object[] args = mFormatArgs;
+ args[0] = numsongs;
+ builder.append(mResources.getQuantityString(R.plurals.Nsongs, numsongs, args));
+ } else {
+ final Object[] args = mFormatArgs3;
+ args[0] = numsongs;
+ args[1] = numartistsongs;
+ args[2] = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST));
+ builder.append(mResources.getQuantityString(R.plurals.Nsongscomp, 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(cursor.getColumnIndexOrThrow(
+ MediaStore.Audio.Albums.ALBUM_ART));
+ if (unknown || 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.NUMBER_OF_SONGS,
+ MediaStore.Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST,
+ MediaStore.Audio.Albums.FIRST_YEAR,
+ MediaStore.Audio.Albums.LAST_YEAR,
+ MediaStore.Audio.Albums.ALBUM_ART
+ };
+ Cursor c = MusicUtils.query(mActivity,
+ MediaStore.Audio.Artists.Albums.getContentUri("external", id),
+ cols, null, null, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
+
+ class MyCursorWrapper extends CursorWrapper {
+ String mArtistName;
+ int mMagicColumnIdx;
+ MyCursorWrapper(Cursor c, String artist) {
+ super(c);
+ mArtistName = artist;
+ if (MediaFile.UNKNOWN_STRING.equals(mArtistName)) {
+ mArtistName = mUnknownArtist;
+ }
+ mMagicColumnIdx = c.getColumnCount();
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ if (columnIndex != mMagicColumnIdx) {
+ return super.getString(columnIndex);
+ }
+ return mArtistName;
+ }
+
+ @Override
+ public int getColumnIndexOrThrow(String name) {
+ if (name.equals(MediaStore.Audio.Albums.ARTIST)) {
+ return mMagicColumnIdx;
+ }
+ return super.getColumnIndexOrThrow(name);
+ }
+
+ @Override
+ public String getColumnName(int idx) {
+ if (idx != mMagicColumnIdx) {
+ return super.getColumnName(idx);
+ }
+ return MediaStore.Audio.Albums.ARTIST;
+ }
+
+ @Override
+ public int getColumnCount() {
+ return super.getColumnCount() + 1;
+ }
+ }
+ return new MyCursorWrapper(c, groupCursor.getString(mGroupArtistIdx));
+ }
+
+ @Override
+ public void changeCursor(Cursor cursor) {
+ if (cursor != mActivity.mArtistCursor) {
+ mActivity.mArtistCursor = cursor;
+ getColumnIndices(cursor);
+ super.changeCursor(cursor);
+ }
+ }
+
+ @Override
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ String s = constraint.toString();
+ if (mConstraintIsValid && (
+ (s == null && mConstraint == null) ||
+ (s != null && s.equals(mConstraint)))) {
+ return getCursor();
+ }
+ Cursor c = mActivity.getArtistCursor(null, s);
+ mConstraint = s;
+ mConstraintIsValid = true;
+ return c;
+ }
+
+ public Object[] getSections() {
+ return mIndexer.getSections();
+ }
+
+ public int getPositionForSection(int sectionIndex) {
+ return mIndexer.getPositionForSection(sectionIndex);
+ }
+
+ public int getSectionForPosition(int position) {
+ return 0;
+ }
+ }
+
+ private Cursor mArtistCursor;
+}
+
diff --git a/src/com/android/music/CheckableRelativeLayout.java b/src/com/android/music/CheckableRelativeLayout.java
new file mode 100644
index 0000000..25c837b
--- /dev/null
+++ b/src/com/android/music/CheckableRelativeLayout.java
@@ -0,0 +1,63 @@
+/*
+ * 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.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.RelativeLayout;
+
+/**
+ * A special variation of RelativeLayout that can be used as a checkable object.
+ * This allows it to be used as the top-level view of a list view item, which
+ * also supports checking. Otherwise, it works identically to a RelativeLayout.
+ */
+public class CheckableRelativeLayout extends RelativeLayout implements Checkable {
+ private boolean mChecked;
+
+ private static final int[] CHECKED_STATE_SET = {
+ android.R.attr.state_checked
+ };
+
+ public CheckableRelativeLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (isChecked()) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+
+ public void toggle() {
+ setChecked(!mChecked);
+ }
+
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ public void setChecked(boolean checked) {
+ if (mChecked != checked) {
+ mChecked = checked;
+ refreshDrawableState();
+ }
+ }
+}
diff --git a/src/com/android/music/CreatePlaylist.java b/src/com/android/music/CreatePlaylist.java
new file mode 100644
index 0000000..9005023
--- /dev/null
+++ b/src/com/android/music/CreatePlaylist.java
@@ -0,0 +1,187 @@
+/*
+ * 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();
+ if (defaultname == null) {
+ finish();
+ return;
+ }
+ 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);
+
+ if (c == null) {
+ return null;
+ }
+
+ 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()).setData(uri));
+ finish();
+ }
+ }
+ };
+}
diff --git a/src/com/android/music/DeleteItems.java b/src/com/android/music/DeleteItems.java
new file mode 100644
index 0000000..15e681f
--- /dev/null
+++ b/src/com/android/music/DeleteItems.java
@@ -0,0 +1,79 @@
+/*
+ * 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");
+
+ mPrompt.setText(desc);
+ }
+
+ 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
index 0000000..5698cc5
--- /dev/null
+++ b/src/com/android/music/IMediaPlaybackService.aidl
@@ -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
index 0000000..76812df
--- /dev/null
+++ b/src/com/android/music/MediaButtonIntentReceiver.java
@@ -0,0 +1,148 @@
+/*
+ * 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.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothError;
+import android.content.Context;
+import android.content.Intent;
+import android.content.BroadcastReceiver;
+import android.content.SharedPreferences;
+import android.media.AudioManager;
+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 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ context.startActivity(i);
+ mLaunched = true;
+ }
+ break;
+ }
+ }
+ };
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String intentAction = intent.getAction();
+ if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intentAction)) {
+ Intent i = new Intent(context, MediaPlaybackService.class);
+ i.setAction(MediaPlaybackService.SERVICECMD);
+ i.putExtra(MediaPlaybackService.CMDNAME, MediaPlaybackService.CMDPAUSE);
+ context.startService(i);
+ } else if (Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) {
+ 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.
+
+ String command = null;
+ switch (keycode) {
+ case KeyEvent.KEYCODE_STOP:
+ command = MediaPlaybackService.CMDSTOP;
+ break;
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_PLAYPAUSE:
+ command = MediaPlaybackService.CMDTOGGLEPAUSE;
+ break;
+ case KeyEvent.KEYCODE_NEXTSONG:
+ command = MediaPlaybackService.CMDNEXT;
+ break;
+ case KeyEvent.KEYCODE_PREVIOUSSONG:
+ command = MediaPlaybackService.CMDPREVIOUS;
+ break;
+ }
+
+ if (command != null) {
+ if (action == KeyEvent.ACTION_DOWN) {
+ if (!mDown) {
+ // only if this isn't a repeat event
+
+ if (MediaPlaybackService.CMDTOGGLEPAUSE.equals(command)) {
+ // 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 (keycode == KeyEvent.KEYCODE_HEADSETHOOK &&
+ eventtime - mLastClickTime < 300) {
+ i.putExtra(MediaPlaybackService.CMDNAME, MediaPlaybackService.CMDNEXT);
+ context.startService(i);
+ mLastClickTime = 0;
+ } else {
+ i.putExtra(MediaPlaybackService.CMDNAME, command);
+ 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/MediaGadgetProvider.java b/src/com/android/music/MediaGadgetProvider.java
new file mode 100644
index 0000000..46ba391
--- /dev/null
+++ b/src/com/android/music/MediaGadgetProvider.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.music;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.gadget.GadgetManager;
+import android.gadget.GadgetProvider;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.media.MediaFile;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.util.Config;
+import android.util.Log;
+import android.view.View;
+import android.widget.RemoteViews;
+
+/**
+ * Simple gadget to show currently playing album art along
+ * with play/pause and next track buttons.
+ */
+public class MediaGadgetProvider extends GadgetProvider {
+ static final String TAG = "MusicGadgetProvider";
+
+ public static final String CMDGADGETUPDATE = "gadgetupdate";
+
+ static final ComponentName THIS_GADGET =
+ new ComponentName("com.android.music",
+ "com.android.music.MediaGadgetProvider");
+
+ private static MediaGadgetProvider sInstance;
+
+ static synchronized MediaGadgetProvider getInstance() {
+ if (sInstance == null) {
+ sInstance = new MediaGadgetProvider();
+ }
+ return sInstance;
+ }
+
+ @Override
+ public void onUpdate(Context context, GadgetManager gadgetManager, int[] gadgetIds) {
+ defaultGadget(context, gadgetIds);
+
+ // Send broadcast intent to any running MediaPlaybackService so it can
+ // wrap around with an immediate update.
+ Intent updateIntent = new Intent(MediaPlaybackService.SERVICECMD);
+ updateIntent.putExtra(MediaPlaybackService.CMDNAME,
+ MediaGadgetProvider.CMDGADGETUPDATE);
+ updateIntent.putExtra(GadgetManager.EXTRA_GADGET_IDS, gadgetIds);
+ updateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ context.sendBroadcast(updateIntent);
+ }
+
+ /**
+ * Initialize given gadgets to default state, where we launch Music on default click
+ * and hide actions if service not running.
+ */
+ private void defaultGadget(Context context, int[] gadgetIds) {
+ final Resources res = context.getResources();
+ final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.gadget);
+
+ views.setTextViewText(R.id.title, res.getText(R.string.emptyplaylist));
+
+ linkButtons(context, views, false /* not playing */);
+ pushUpdate(context, gadgetIds, views);
+ }
+
+ private void pushUpdate(Context context, int[] gadgetIds, RemoteViews views) {
+ // Update specific list of gadgetIds if given, otherwise default to all
+ final GadgetManager gm = GadgetManager.getInstance(context);
+ if (gadgetIds != null) {
+ gm.updateGadget(gadgetIds, views);
+ } else {
+ final ComponentName thisGadget = new ComponentName(context,
+ MediaGadgetProvider.class);
+ gm.updateGadget(thisGadget, views);
+ }
+ }
+
+ /**
+ * Check against {@link GadgetManager} if there are any instances of this gadget.
+ */
+ private boolean hasInstances(Context context) {
+ GadgetManager gadgetManager = GadgetManager.getInstance(context);
+ int[] gadgetIds = gadgetManager.getGadgetIds(THIS_GADGET);
+ return (gadgetIds.length > 0);
+ }
+
+ /**
+ * Handle a change notification coming over from {@link MediaPlaybackService}
+ */
+ void notifyChange(MediaPlaybackService service, String what) {
+ if (hasInstances(service)) {
+ if (MediaPlaybackService.PLAYBACK_COMPLETE.equals(what) ||
+ MediaPlaybackService.META_CHANGED.equals(what) ||
+ MediaPlaybackService.PLAYSTATE_CHANGED.equals(what)) {
+ performUpdate(service, null);
+ }
+ }
+ }
+
+ /**
+ * Update all active gadget instances by pushing changes
+ */
+ void performUpdate(MediaPlaybackService service, int[] gadgetIds) {
+ final Resources res = service.getResources();
+ final RemoteViews views = new RemoteViews(service.getPackageName(), R.layout.gadget);
+
+ final int track = service.getQueuePosition() + 1;
+ final String titleName = service.getTrackName();
+ final String artistName = service.getArtistName();
+
+ // Format title string with track number, or show SD card message
+ CharSequence titleString = "";
+ String status = Environment.getExternalStorageState();
+ if (titleName != null) {
+ titleString = res.getString(R.string.gadget_track_num_title, track, titleName);
+ } else if (status.equals(Environment.MEDIA_SHARED) ||
+ status.equals(Environment.MEDIA_UNMOUNTED)) {
+ titleString = res.getText(R.string.sdcard_busy_title);
+ } else if (status.equals(Environment.MEDIA_REMOVED)) {
+ titleString = res.getText(R.string.sdcard_missing_title);
+ } else {
+ titleString = res.getText(R.string.emptyplaylist);
+ }
+
+ views.setTextViewText(R.id.title, titleString);
+ views.setTextViewText(R.id.artist, artistName);
+
+ // Set correct drawable for pause state
+ final boolean playing = service.isPlaying();
+ views.setImageViewResource(R.id.control_play, playing ?
+ R.drawable.gadget_pause : R.drawable.gadget_play);
+
+ // Link actions buttons to intents
+ linkButtons(service, views, playing);
+
+ pushUpdate(service, gadgetIds, views);
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntents}.
+ *
+ * @param playerActive True if player is active in background, which means
+ * gadget click will launch {@link MediaPlaybackActivity},
+ * otherwise we launch {@link MusicBrowserActivity}.
+ */
+ private void linkButtons(Context context, RemoteViews views, boolean playerActive) {
+ // Connect up various buttons and touch events
+ Intent intent;
+ PendingIntent pendingIntent;
+
+ final ComponentName serviceName = new ComponentName(context, MediaPlaybackService.class);
+
+ if (playerActive) {
+ intent = new Intent(context, MediaPlaybackActivity.class);
+ pendingIntent = PendingIntent.getActivity(context,
+ 0 /* no requestCode */, intent, 0 /* no flags */);
+ views.setOnClickPendingIntent(R.id.album_gadget, pendingIntent);
+ } else {
+ intent = new Intent(context, MusicBrowserActivity.class);
+ pendingIntent = PendingIntent.getActivity(context,
+ 0 /* no requestCode */, intent, 0 /* no flags */);
+ views.setOnClickPendingIntent(R.id.album_gadget, pendingIntent);
+ }
+
+ intent = new Intent(MediaPlaybackService.TOGGLEPAUSE_ACTION);
+ intent.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context,
+ 0 /* no requestCode */, intent, 0 /* no flags */);
+ views.setOnClickPendingIntent(R.id.control_play, pendingIntent);
+
+ intent = new Intent(MediaPlaybackService.NEXT_ACTION);
+ intent.setComponent(serviceName);
+ pendingIntent = PendingIntent.getService(context,
+ 0 /* no requestCode */, intent, 0 /* no flags */);
+ views.setOnClickPendingIntent(R.id.control_next, pendingIntent);
+ }
+}
diff --git a/src/com/android/music/MediaPickerActivity.java b/src/com/android/music/MediaPickerActivity.java
new file mode 100644
index 0000000..9ef6375
--- /dev/null
+++ b/src/com/android/music/MediaPickerActivity.java
@@ -0,0 +1,297 @@
+/*
+ * 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();
+ if (mCursor != null) {
+ mCursor.close();
+ }
+ }
+
+ 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.getColumnIndexOrThrow(
+ 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.getColumnIndexOrThrow(
+ MediaStore.Video.Media._ID));
+ } else {
+ uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ mediaId = mCursor.getLong(mCursor.getColumnIndexOrThrow(
+ 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.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE);
+ mArtistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST);
+ mAlbumIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM);
+ mMimeIdx = cursor.getColumnIndexOrThrow(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
index 0000000..2b9125d
--- /dev/null
+++ b/src/com/android/music/MediaPlaybackActivity.java
@@ -0,0 +1,1310 @@
+/*
+ * 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.app.SearchManager;
+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.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.provider.MediaStore;
+import android.text.Layout;
+import android.text.TextUtils.TruncateAt;
+import android.util.Log;
+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.ViewConfiguration;
+import android.view.Window;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+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, View.OnLongClickListener
+{
+ 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;
+ private int mTouchSlop;
+
+ 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 = (ImageView) 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);
+ v.setOnLongClickListener(this);
+
+ v = (View)mAlbumName.getParent();
+ v.setOnTouchListener(this);
+ v.setOnLongClickListener(this);
+
+ v = (View)mTrackName.getParent();
+ v.setOnTouchListener(this);
+ v.setOnLongClickListener(this);
+
+ 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);
+ }
+
+ mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
+ }
+
+ int mInitialX = -1;
+ int mLastX = -1;
+ int mTextWidth = 0;
+ int mViewWidth = 0;
+ boolean mDraggingLabel = false;
+
+ TextView textViewForContainer(View v) {
+ View vv = v.findViewById(R.id.artistname);
+ if (vv != null) return (TextView) vv;
+ vv = v.findViewById(R.id.albumname);
+ if (vv != null) return (TextView) vv;
+ vv = v.findViewById(R.id.trackname);
+ if (vv != null) return (TextView) vv;
+ return null;
+ }
+
+ public boolean onTouch(View v, MotionEvent event) {
+ int action = event.getAction();
+ TextView tv = textViewForContainer(v);
+ if (tv == null) {
+ return false;
+ }
+ if (action == MotionEvent.ACTION_DOWN) {
+ v.setBackgroundColor(0xff606060);
+ mInitialX = mLastX = (int) event.getX();
+ mDraggingLabel = false;
+ } else if (action == MotionEvent.ACTION_UP ||
+ action == MotionEvent.ACTION_CANCEL) {
+ v.setBackgroundColor(0);
+ if (mDraggingLabel) {
+ Message msg = mLabelScroller.obtainMessage(0, tv);
+ mLabelScroller.sendMessageDelayed(msg, 1000);
+ }
+ } else if (action == MotionEvent.ACTION_MOVE) {
+ if (mDraggingLabel) {
+ int scrollx = tv.getScrollX();
+ int x = (int) event.getX();
+ int delta = mLastX - x;
+ if (delta != 0) {
+ mLastX = x;
+ scrollx += delta;
+ if (scrollx > mTextWidth) {
+ // scrolled the text completely off the view to the left
+ scrollx -= mTextWidth;
+ scrollx -= mViewWidth;
+ }
+ if (scrollx < -mViewWidth) {
+ // scrolled the text completely off the view to the right
+ scrollx += mViewWidth;
+ scrollx += mTextWidth;
+ }
+ tv.scrollTo(scrollx, 0);
+ }
+ return true;
+ }
+ int delta = mInitialX - (int) event.getX();
+ if (Math.abs(delta) > mTouchSlop) {
+ // start moving
+ mLabelScroller.removeMessages(0, tv);
+
+ // Only turn ellipsizing off when it's not already off, because it
+ // causes the scroll position to be reset to 0.
+ if (tv.getEllipsize() != null) {
+ tv.setEllipsize(null);
+ }
+ Layout ll = tv.getLayout();
+ // layout might be null if the text just changed, or ellipsizing
+ // was just turned off
+ if (ll == null) {
+ return false;
+ }
+ // get the non-ellipsized line width, to determine whether scrolling
+ // should even be allowed
+ mTextWidth = (int) tv.getLayout().getLineWidth(0);
+ mViewWidth = tv.getWidth();
+ if (mViewWidth > mTextWidth) {
+ tv.setEllipsize(TruncateAt.END);
+ v.cancelLongPress();
+ return false;
+ }
+ mDraggingLabel = true;
+ tv.setHorizontalFadingEdgeEnabled(true);
+ v.cancelLongPress();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ Handler mLabelScroller = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ TextView tv = (TextView) msg.obj;
+ int x = tv.getScrollX();
+ x = x * 3 / 4;
+ tv.scrollTo(x, 0);
+ if (x == 0) {
+ tv.setEllipsize(TruncateAt.END);
+ } else {
+ Message newmsg = obtainMessage(0, tv);
+ mLabelScroller.sendMessageDelayed(newmsg, 15);
+ }
+ }
+ };
+
+ public boolean onLongClick(View view) {
+
+ CharSequence title = null;
+ String mime = null;
+ String query = null;
+ String artist;
+ String album;
+ String song;
+
+ try {
+ artist = mService.getArtistName();
+ album = mService.getAlbumName();
+ song = mService.getTrackName();
+ } catch (RemoteException ex) {
+ return true;
+ }
+
+ boolean knownartist = !MediaFile.UNKNOWN_STRING.equals(artist);
+ boolean knownalbum = !MediaFile.UNKNOWN_STRING.equals(album);
+
+ if (knownartist && view.equals(mArtistName.getParent())) {
+ title = artist;
+ query = artist.toString();
+ mime = MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE;
+ } else if (knownalbum && view.equals(mAlbumName.getParent())) {
+ title = album;
+ if (knownartist) {
+ query = artist + " " + album;
+ } else {
+ query = album;
+ }
+ mime = MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE;
+ } else if (view.equals(mTrackName.getParent()) || !knownartist || !knownalbum) {
+ title = song;
+ if (knownartist) {
+ query = artist + " " + song;
+ } else {
+ query = song;
+ }
+ mime = "audio/*"; // the specific type doesn't matter, so don't bother retrieving it
+ } else {
+ throw new RuntimeException("shouldn't be here");
+ }
+ title = getString(R.string.mediasearch, title);
+
+ Intent i = new Intent();
+ i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
+ i.putExtra(SearchManager.QUERY, query);
+ if(knownartist) {
+ i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, artist);
+ }
+ if(knownalbum) {
+ i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, album);
+ }
+ i.putExtra(MediaStore.EXTRA_MEDIA_TITLE, song);
+ i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, mime);
+
+ startActivity(Intent.createChooser(i, title));
+ return true;
+ }
+
+ 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(android.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", getString(R.string.delete_song_desc,
+ 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 = intent.getData();
+ 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 || mService.getAudioId() < 0) {
+ 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 ImageView 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.setImageBitmap((Bitmap)msg.obj);
+ mAlbum.getDrawable().setDither(true);
+ 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 {
+ String path = mService.getPath();
+ if (path == null) {
+ finish();
+ return;
+ }
+
+ if (mService.getAudioId() < 0 && path.toLowerCase().startsWith("http://")) {
+ ((View) mArtistName.getParent()).setVisibility(View.INVISIBLE);
+ ((View) mAlbumName.getParent()).setVisibility(View.INVISIBLE);
+ mAlbum.setVisibility(View.GONE);
+ mTrackName.setText(path);
+ mAlbumArtHandler.removeMessages(GET_ALBUM_ART);
+ mAlbumArtHandler.obtainMessage(GET_ALBUM_ART, -1, 0).sendToTarget();
+ } else {
+ ((View) mArtistName.getParent()).setVisibility(View.VISIBLE);
+ ((View) mAlbumName.getParent()).setVisibility(View.VISIBLE);
+ 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();
+ mAlbum.setVisibility(View.VISIBLE);
+ }
+ 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
index 0000000..ac90156
--- /dev/null
+++ b/src/com/android/music/MediaPlaybackService.java
@@ -0,0 +1,1833 @@
+/*
+ * 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.ContentUris;
+import android.content.ContentValues;
+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.database.sqlite.SQLiteException;
+import android.gadget.GadgetManager;
+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.util.Log;
+import android.widget.RemoteViews;
+import android.widget.Toast;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneStateIntentReceiver;
+
+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 CMDSTOP = "stop";
+ public static final String CMDPAUSE = "pause";
+ public static final String CMDPREVIOUS = "previous";
+ public static final String CMDNEXT = "next";
+
+ public static final String TOGGLEPAUSE_ACTION = "com.android.music.musicservicecommand.togglepause";
+ public static final String PAUSE_ACTION = "com.android.music.musicservicecommand.pause";
+ public static final String PREVIOUS_ACTION = "com.android.music.musicservicecommand.previous";
+ public static final String NEXT_ACTION = "com.android.music.musicservicecommand.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 FADEIN = 4;
+ 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", // index must match IDCOLIDX below
+ 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,
+ MediaStore.Audio.Media.IS_PODCAST, // index must match PODCASTCOLIDX below
+ MediaStore.Audio.Media.BOOKMARK // index must match BOOKMARKCOLIDX below
+ };
+ private final static int IDCOLIDX = 0;
+ private final static int PODCASTCOLIDX = 8;
+ private final static int BOOKMARKCOLIDX = 9;
+ private BroadcastReceiver mUnmountReceiver = null;
+ private WakeLock mWakeLock;
+ private int mServiceStartId = -1;
+ private boolean mServiceInUse = false;
+ private boolean mResumeAfterCall = false;
+ private boolean mWasPlaying = false;
+ private boolean mQuietMode = 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;
+
+ private MediaGadgetProvider mGadgetProvider = MediaGadgetProvider.getInstance();
+
+ // 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
+ startAndFadeIn();
+ mResumeAfterCall = false;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ };
+
+ private void startAndFadeIn() {
+ mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10);
+ }
+
+ private Handler mMediaplayerHandler = new Handler() {
+ float mCurrentVolume = 1.0f;
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case FADEIN:
+ if (!isPlaying()) {
+ mCurrentVolume = 0f;
+ mPlayer.setVolume(mCurrentVolume);
+ play();
+ mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10);
+ } else {
+ mCurrentVolume += 0.01f;
+ if (mCurrentVolume < 1.0f) {
+ mMediaplayerHandler.sendEmptyMessageDelayed(FADEIN, 10);
+ } else {
+ mCurrentVolume = 1.0f;
+ }
+ mPlayer.setVolume(mCurrentVolume);
+ }
+ break;
+ 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 action = intent.getAction();
+ String cmd = intent.getStringExtra("command");
+ if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) {
+ next(true);
+ } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) {
+ prev();
+ } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) {
+ if (isPlaying()) {
+ pause();
+ } else {
+ play();
+ }
+ } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) {
+ pause();
+ } else if (CMDSTOP.equals(cmd)) {
+ pause();
+ seek(0);
+ } else if (MediaGadgetProvider.CMDGADGETUPDATE.equals(cmd)) {
+ // Someone asked us to refresh a set of specific gadgets, probably
+ // because they were just added.
+ int[] gadgetIds = intent.getIntArrayExtra(GadgetManager.EXTRA_GADGET_IDS);
+ mGadgetProvider.performUpdate(MediaPlaybackService.this, gadgetIds);
+ }
+ }
+ };
+
+ 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();
+
+ IntentFilter commandFilter = new IntentFilter();
+ commandFilter.addAction(SERVICECMD);
+ commandFilter.addAction(TOGGLEPAUSE_ACTION);
+ commandFilter.addAction(PAUSE_ACTION);
+ commandFilter.addAction(NEXT_ACTION);
+ commandFilter.addAction(PREVIOUS_ACTION);
+ registerReceiver(mIntentReceiver, commandFilter);
+
+ mPsir.registerIntent();
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getName());
+ mWakeLock.setReferenceCounted(false);
+
+ // If the service was idle, but got killed before it stopped itself, the
+ // system will relaunch it. Make sure it gets stopped again in that case.
+ Message msg = mDelayedStopHandler.obtainMessage();
+ mDelayedStopHandler.sendMessageDelayed(msg, IDLE_DELAY);
+ }
+
+ @Override
+ public void onDestroy() {
+ // Check that we're not being destroyed while something is still playing.
+ if (isPlaying()) {
+ Log.e("MediaPlaybackService", "Service being destroyed while still playing.");
+ }
+ // and for good measure, call mPlayer.stop(), which calls MediaPlayer.reset(), which
+ // releases the MediaPlayer's wake lock, if any.
+ mPlayer.stop();
+
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+
+ 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;
+ mQuietMode = true;
+ openCurrent();
+ mQuietMode = false;
+ 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 action = intent.getAction();
+ String cmd = intent.getStringExtra("command");
+
+ if (CMDNEXT.equals(cmd) || NEXT_ACTION.equals(action)) {
+ next(true);
+ } else if (CMDPREVIOUS.equals(cmd) || PREVIOUS_ACTION.equals(action)) {
+ prev();
+ } else if (CMDTOGGLEPAUSE.equals(cmd) || TOGGLEPAUSE_ACTION.equals(action)) {
+ if (isPlaying()) {
+ pause();
+ } else {
+ play();
+ }
+ } else if (CMDPAUSE.equals(cmd) || PAUSE_ACTION.equals(action)) {
+ pause();
+ } else if (CMDSTOP.equals(cmd)) {
+ pause();
+ seek(0);
+ }
+
+ // 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 changed
+ // 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);
+ }
+
+ // Share this notification directly with our gadgets
+ mGadgetProvider.notifyChange(this, what);
+ }
+
+ 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
+ }
+
+ // insert the list of songs at the specified position in the playlist
+ 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, or a random position if the
+ * specified position is 0.
+ * @param list The new list of tracks.
+ */
+ public void open(int [] list, int position) {
+ synchronized (this) {
+ if (mShuffleMode == SHUFFLE_AUTO) {
+ mShuffleMode = SHUFFLE_NORMAL;
+ }
+ int listlength = list.length;
+ boolean newlist = true;
+ if (mPlayListLen == listlength) {
+ // possible fast path: list might be the same
+ newlist = false;
+ for (int i = 0; i < listlength; i++) {
+ if (list[i] != mPlayList[i]) {
+ newlist = true;
+ break;
+ }
+ }
+ }
+ if (newlist) {
+ addToPlayList(list, -1);
+ notifyChange(QUEUE_CHANGED);
+ }
+ int oldpos = mPlayPos;
+ if (position >= 0) {
+ mPlayPos = position;
+ } else {
+ mPlayPos = mRand.nextInt(mPlayListLen);
+ }
+ mHistory.clear();
+
+ saveBookmarkIfNeeded();
+ openCurrent();
+ if (!newlist && mPlayPos != oldpos) {
+ // the queue didn't change, but the position did
+ notifyChange(META_CHANGED);
+ }
+ }
+ }
+
+ /**
+ * 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);
+ // go to bookmark if needed
+ if (isPodcast()) {
+ long bookmark = getBookmark();
+ // Start playing a little bit before the bookmark,
+ // so it's easier to get back in to the narrative.
+ seek(bookmark - 5000);
+ }
+ }
+ }
+ }
+
+ 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(IDCOLIDX);
+ 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;
+ if (!mQuietMode) {
+ 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);
+
+ 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);
+ if (getAudioId() < 0) {
+ // streaming
+ views.setTextViewText(R.id.trackname, getPath());
+ views.setTextViewText(R.id.artistalbum, null);
+ } else {
+ String artist = getArtistName();
+ views.setTextViewText(R.id.trackname, getTrackName());
+ 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);
+ if (!mWasPlaying) {
+ notifyChange(PLAYSTATE_CHANGED);
+ }
+ mWasPlaying = true;
+ } else if (mPlayListLen <= 0) {
+ // This is mostly so that if you press 'play' on a bluetooth headset
+ // without every having played anything before, it will still play
+ // something.
+ setShuffleMode(SHUFFLE_AUTO);
+ }
+ }
+
+ 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);
+ if (remove_status_icon) {
+ 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);
+ saveBookmarkIfNeeded();
+ }
+ }
+
+ /** 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;
+ }
+ }
+ saveBookmarkIfNeeded();
+ 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
+ if (mPlayPos >= 0) {
+ 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++;
+ }
+ }
+ saveBookmarkIfNeeded();
+ 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);
+ }
+
+ private void saveBookmarkIfNeeded() {
+ try {
+ if (isPodcast()) {
+ long pos = position();
+ long bookmark = getBookmark();
+ long duration = duration();
+ if ((pos < bookmark && (pos + 10000) > bookmark) ||
+ (pos > bookmark && (pos - 10000) < bookmark)) {
+ // The existing bookmark is close to the current
+ // position, so don't update it.
+ return;
+ }
+ if (pos < 15000 || (pos + 10000) > duration) {
+ // if we're near the start or end, clear the bookmark
+ pos = 0;
+ }
+
+ // write 'pos' to the bookmark field
+ ContentValues values = new ContentValues();
+ values.put(MediaStore.Audio.Media.BOOKMARK, pos);
+ Uri uri = ContentUris.withAppendedId(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursor.getLong(IDCOLIDX));
+ getContentResolver().update(uri, values, null, null);
+ }
+ } catch (SQLiteException ex) {
+ }
+ }
+
+ // Make sure there are at least 5 items after the currently playing item
+ // and no more than 10 items before.
+ private void doAutoShuffleUpdate() {
+ boolean notify = false;
+ // remove old entries
+ if (mPlayPos > 10) {
+ removeTracks(0, mPlayPos - 9);
+ notify = true;
+ }
+ // add new entries if needed
+ int to_add = 7 - (mPlayListLen - (mPlayPos < 0 ? -1 : mPlayPos));
+ 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];
+ ensurePlayListCapacity(mPlayListLen + 1);
+ mPlayList[mPlayListLen++] = which;
+ notify = true;
+ }
+ if (notify) {
+ 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) {
+ int numremoved = removeTracksInternal(first, last);
+ if (numremoved > 0) {
+ notifyChange(QUEUE_CHANGED);
+ }
+ return numremoved;
+ }
+
+ private int removeTracksInternal(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;
+ }
+ boolean wasPlaying = isPlaying();
+ stop(false);
+ openCurrent();
+ if (wasPlaying) {
+ play();
+ }
+ }
+ }
+ 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 += removeTracksInternal(i, i);
+ i--;
+ }
+ }
+ }
+ if (numremoved > 0) {
+ notifyChange(QUEUE_CHANGED);
+ }
+ return numremoved;
+ }
+
+ public void setShuffleMode(int shufflemode) {
+ synchronized(this) {
+ if (mShuffleMode == shufflemode && mPlayListLen > 0) {
+ return;
+ }
+ mShuffleMode = shufflemode;
+ if (mShuffleMode == SHUFFLE_AUTO) {
+ if (makeAutoShuffleList()) {
+ mPlayListLen = 0;
+ doAutoShuffleUpdate();
+ mPlayPos = 0;
+ openCurrent();
+ play();
+ notifyChange(META_CHANGED);
+ return;
+ } else {
+ // failed to build a list of files to shuffle
+ mShuffleMode = SHUFFLE_NONE;
+ }
+ }
+ saveQueue(false);
+ }
+ }
+ public int getShuffleMode() {
+ return mShuffleMode;
+ }
+
+ public void setRepeatMode(int repeatmode) {
+ synchronized(this) {
+ mRepeatMode = repeatmode;
+ saveQueue(false);
+ }
+ }
+ 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() {
+ synchronized(this) {
+ if (mCursor == null) {
+ return null;
+ }
+ return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
+ }
+ }
+
+ public int getArtistId() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return -1;
+ }
+ return mCursor.getInt(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID));
+ }
+ }
+
+ public String getAlbumName() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return null;
+ }
+ return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM));
+ }
+ }
+
+ public int getAlbumId() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return -1;
+ }
+ return mCursor.getInt(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID));
+ }
+ }
+
+ public String getTrackName() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return null;
+ }
+ return mCursor.getString(mCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
+ }
+ }
+
+ private boolean isPodcast() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return false;
+ }
+ return (mCursor.getInt(PODCASTCOLIDX) > 0);
+ }
+ }
+
+ private long getBookmark() {
+ synchronized (this) {
+ if (mCursor == null) {
+ return 0;
+ }
+ return mCursor.getLong(BOOKMARKCOLIDX);
+ }
+ }
+
+ /**
+ * 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();
+ }
+ 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;
+ }
+
+ public void setVolume(float vol) {
+ mMediaPlayer.setVolume(vol, vol);
+ }
+ }
+
+ 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/MusicAlphabetIndexer.java b/src/com/android/music/MusicAlphabetIndexer.java
new file mode 100644
index 0000000..c05f3c1
--- /dev/null
+++ b/src/com/android/music/MusicAlphabetIndexer.java
@@ -0,0 +1,44 @@
+/*
+ * 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.database.Cursor;
+import android.provider.MediaStore;
+import android.widget.AlphabetIndexer;
+
+/**
+ * Handles comparisons in a different way because the Album, Song and Artist name
+ * are stripped of some prefixes such as "a", "an", "the" and some symbols.
+ *
+ */
+class MusicAlphabetIndexer extends AlphabetIndexer {
+
+ public MusicAlphabetIndexer(Cursor cursor, int sortedColumnIndex, CharSequence alphabet) {
+ super(cursor, sortedColumnIndex, alphabet);
+ }
+
+ @Override
+ protected int compare(String word, String letter) {
+ String wordKey = MediaStore.Audio.keyFor(word);
+ String letterKey = MediaStore.Audio.keyFor(letter);
+ if (wordKey.startsWith(letter)) {
+ return 0;
+ } else {
+ return wordKey.compareTo(letterKey);
+ }
+ }
+}
diff --git a/src/com/android/music/MusicBrowserActivity.java b/src/com/android/music/MusicBrowserActivity.java
new file mode 100644
index 0000000..632db1e
--- /dev/null
+++ b/src/com/android/music/MusicBrowserActivity.java
@@ -0,0 +1,278 @@
+/*
+ * 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.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.widget.ImageButton;
+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() {
+ setContentView(R.layout.music_library);
+ mNowPlayingView = findViewById(R.id.nowplaying);
+ mTitle = (TextView) mNowPlayingView.findViewById(R.id.title);
+ mArtist = (TextView) mNowPlayingView.findViewById(R.id.artist);
+
+ View b = (View) findViewById(R.id.browse_button);
+ b.setOnClickListener(this);
+
+ b = (View) findViewById(R.id.albums_button);
+ b.setOnClickListener(this);
+
+ b = (View) findViewById(R.id.tracks_button);
+ b.setOnClickListener(this);
+
+ b = (View) findViewById(R.id.playlists_button);
+ b.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(android.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: {
+ startSearch("", false, null, false);
+ return true;
+ }
+ }
+ } catch (RemoteException ex) {
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ 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);
+ mNowPlayingView.setSelected(true);
+ } else {
+ mNowPlayingView.setBackgroundDrawable(mBack);
+ mNowPlayingView.setSelected(false);
+ }
+ }
+ };
+
+ 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/MusicPicker.java b/src/com/android/music/MusicPicker.java
new file mode 100644
index 0000000..c5be26d
--- /dev/null
+++ b/src/com/android/music/MusicPicker.java
@@ -0,0 +1,735 @@
+/*
+ * 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.ListActivity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.CharArrayBuffer;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.animation.AnimationUtils;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.RadioButton;
+import android.widget.SectionIndexer;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+import java.io.IOException;
+import java.text.Collator;
+import java.util.Formatter;
+import java.util.Locale;
+
+/**
+ * Activity allowing the user to select a music track on the device, and
+ * return it to its caller. The music picker user interface is fairly
+ * extensive, providing information about each track like the music
+ * application (title, author, album, duration), as well as the ability to
+ * previous tracks and sort them in different orders.
+ *
+ * <p>This class also illustrates how you can load data from a content
+ * provider asynchronously, providing a good UI while doing so, perform
+ * indexing of the content for use inside of a {@link FastScrollView}, and
+ * perform filtering of the data as the user presses keys.
+ */
+public class MusicPicker extends ListActivity
+ implements View.OnClickListener, MediaPlayer.OnCompletionListener,
+ MusicUtils.Defs {
+ static final boolean DBG = false;
+ static final String TAG = "MusicPicker";
+
+ /** Holds the previous state of the list, to restore after the async
+ * query has completed. */
+ static final String LIST_STATE_KEY = "liststate";
+ /** Remember whether the list last had focus for restoring its state. */
+ static final String FOCUS_KEY = "focused";
+ /** Remember the last ordering mode for restoring state. */
+ static final String SORT_MODE_KEY = "sortMode";
+
+ /** Arbitrary number, doesn't matter since we only do one query type. */
+ final int MY_QUERY_TOKEN = 42;
+
+ /** Menu item to sort the music list by track title. */
+ static final int TRACK_MENU = Menu.FIRST;
+ /** Menu item to sort the music list by album title. */
+ static final int ALBUM_MENU = Menu.FIRST+1;
+ /** Menu item to sort the music list by artist name. */
+ static final int ARTIST_MENU = Menu.FIRST+2;
+
+ /** These are the columns in the music cursor that we are interested in. */
+ static final String[] CURSOR_COLS = 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,
+ MediaStore.Audio.Media.TRACK
+ };
+
+ /** Formatting optimization to avoid creating many temporary objects. */
+ static StringBuilder sFormatBuilder = new StringBuilder();
+ /** Formatting optimization to avoid creating many temporary objects. */
+ static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
+ /** Formatting optimization to avoid creating many temporary objects. */
+ static final Object[] sTimeArgs = new Object[5];
+
+ /** Uri to the directory of all music being displayed. */
+ Uri mBaseUri;
+
+ /** This is the adapter used to display all of the tracks. */
+ TrackListAdapter mAdapter;
+ /** Our instance of QueryHandler used to perform async background queries. */
+ QueryHandler mQueryHandler;
+
+ /** Used to keep track of the last scroll state of the list. */
+ Parcelable mListState = null;
+ /** Used to keep track of whether the list last had focus. */
+ boolean mListHasFocus;
+
+ /** The current cursor on the music that is being displayed. */
+ Cursor mCursor;
+ /** The actual sort order the user has selected. */
+ int mSortMode = -1;
+ /** SQL order by string describing the currently selected sort order. */
+ String mSortOrder;
+
+ /** Container of the in-screen progress indicator, to be able to hide it
+ * when done loading the initial cursor. */
+ View mProgressContainer;
+ /** Container of the list view hierarchy, to be able to show it when done
+ * loading the initial cursor. */
+ View mListContainer;
+ /** Set to true when the list view has been shown for the first time. */
+ boolean mListShown;
+
+ /** View holding the okay button. */
+ View mOkayButton;
+ /** View holding the cancel button. */
+ View mCancelButton;
+
+ /** Which track row ID the user has last selected. */
+ long mSelectedId = -1;
+ /** Completel Uri that the user has last selected. */
+ Uri mSelectedUri;
+
+ /** If >= 0, we are currently playing a track for preview, and this is its
+ * row ID. */
+ long mPlayingId = -1;
+
+ /** This is used for playing previews of the music files. */
+ MediaPlayer mMediaPlayer;
+
+ /**
+ * A special implementation of SimpleCursorAdapter that knows how to bind
+ * our cursor data to our list item structure, and takes care of other
+ * advanced features such as indexing and filtering.
+ */
+ class TrackListAdapter extends SimpleCursorAdapter
+ implements SectionIndexer {
+ final ListView mListView;
+
+ private final StringBuilder mBuilder = new StringBuilder();
+ private final String mUnknownArtist;
+ private final String mUnknownAlbum;
+
+ private int mIdIdx;
+ private int mTitleIdx;
+ private int mArtistIdx;
+ private int mAlbumIdx;
+ private int mDurationIdx;
+ private int mAudioIdIdx;
+ private int mTrackIdx;
+
+ private boolean mLoading = true;
+ private int mIndexerSortMode;
+ private boolean mIndexerOutOfDate;
+ private MusicAlphabetIndexer mIndexer;
+
+ class ViewHolder {
+ TextView line1;
+ TextView line2;
+ TextView duration;
+ RadioButton radio;
+ ImageView play_indicator;
+ CharArrayBuffer buffer1;
+ char [] buffer2;
+ }
+
+ TrackListAdapter(Context context, ListView listView, int layout,
+ String[] from, int[] to) {
+ super(context, layout, null, from, to);
+ mListView = listView;
+ mUnknownArtist = context.getString(R.string.unknown_artist_name);
+ mUnknownAlbum = context.getString(R.string.unknown_album_name);
+ }
+
+ /**
+ * The mLoading flag is set while we are performing a background
+ * query, to avoid displaying the "No music" empty view during
+ * this time.
+ */
+ public void setLoading(boolean loading) {
+ mLoading = loading;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ if (mLoading) {
+ // We don't want the empty state to show when loading.
+ return false;
+ } else {
+ return super.isEmpty();
+ }
+ }
+
+ @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.radio = (RadioButton) v.findViewById(R.id.radio);
+ 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(makeTimeString(context, secs));
+ }
+
+ final StringBuilder builder = mBuilder;
+ builder.delete(0, builder.length());
+
+ String name = cursor.getString(mAlbumIdx);
+ if (name == null || name.equals("<unknown>")) {
+ builder.append(mUnknownAlbum);
+ } else {
+ builder.append(name);
+ }
+ builder.append('\n');
+ name = cursor.getString(mArtistIdx);
+ if (name == null || name.equals("<unknown>")) {
+ 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);
+
+ // Update the checkbox of the item, based on which the user last
+ // selected. Note that doing it this way means we must have the
+ // list view update all of its items when the selected item
+ // changes.
+ final long id = cursor.getLong(mIdIdx);
+ vh.radio.setChecked(id == mSelectedId);
+ if (DBG) Log.v(TAG, "Binding id=" + id + " sel=" + mSelectedId
+ + " playing=" + mPlayingId + " cursor=" + cursor);
+
+ // Likewise, display the "now playing" icon if this item is
+ // currently being previewed for the user.
+ ImageView iv = vh.play_indicator;
+ if (id == mPlayingId) {
+ iv.setImageResource(R.drawable.indicator_ic_mp_playing_list);
+ iv.setVisibility(View.VISIBLE);
+ } else {
+ iv.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * This method is called whenever we receive a new cursor due to
+ * an async query, and must take care of plugging the new one in
+ * to the adapter.
+ */
+ @Override
+ public void changeCursor(Cursor cursor) {
+ super.changeCursor(cursor);
+ if (DBG) Log.v(TAG, "Setting cursor to: " + cursor
+ + " from: " + MusicPicker.this.mCursor);
+
+ MusicPicker.this.mCursor = cursor;
+
+ if (cursor != null) {
+ // Retrieve indices of the various columns we are interested in.
+ mIdIdx = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
+ 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);
+ int audioIdIdx = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID);
+ if (audioIdIdx < 0) {
+ audioIdIdx = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
+ }
+ mAudioIdIdx = audioIdIdx;
+ mTrackIdx = cursor.getColumnIndex(MediaStore.Audio.Media.TRACK);
+ }
+
+ // The next time the indexer is needed, we will need to rebind it
+ // to this cursor.
+ mIndexerOutOfDate = true;
+
+ // Ensure that the list is shown (and initial progress indicator
+ // hidden) in case this is the first cursor we have gotten.
+ makeListShown();
+ }
+
+ /**
+ * This method is called from a background thread by the list view
+ * when the user has typed a letter that should result in a filtering
+ * of the displayed items. It returns a Cursor, when will then be
+ * handed to changeCursor.
+ */
+ @Override
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ if (DBG) Log.v(TAG, "Getting new cursor...");
+ return doQuery(true, constraint.toString());
+ }
+
+ public int getPositionForSection(int section) {
+ Cursor cursor = getCursor();
+ if (cursor == null) {
+ // No cursor, the section doesn't exist so just return 0
+ return 0;
+ }
+
+ // If the sort mode has changed, or we haven't yet created an
+ // indexer one, then create a new one that is indexing the
+ // appropriate column based on the sort mode.
+ if (mIndexerSortMode != mSortMode || mIndexer == null) {
+ mIndexerSortMode = mSortMode;
+ int idx = mTitleIdx;
+ switch (mIndexerSortMode) {
+ case ARTIST_MENU:
+ idx = mArtistIdx;
+ break;
+ case ALBUM_MENU:
+ idx = mAlbumIdx;
+ break;
+ }
+ mIndexer = new MusicAlphabetIndexer(cursor, idx,
+ getResources().getString(
+ com.android.internal.R.string.fast_scroll_alphabet));
+
+ // If we have a valid indexer, but the cursor has changed since
+ // its last use, then point it to the current cursor.
+ } else if (mIndexerOutOfDate) {
+ mIndexer.setCursor(cursor);
+ }
+
+ mIndexerOutOfDate = false;
+
+ return mIndexer.getPositionForSection(section);
+ }
+
+ public int getSectionForPosition(int position) {
+ return 0;
+ }
+
+ public Object[] getSections() {
+ return mIndexer.getSections();
+ }
+ }
+
+ /**
+ * This is our specialization of AsyncQueryHandler applies new cursors
+ * to our state as they become available.
+ */
+ private final class QueryHandler extends AsyncQueryHandler {
+ public QueryHandler(Context context) {
+ super(context.getContentResolver());
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ if (!isFinishing()) {
+ // Update the adapter: we are no longer loading, and have
+ // a new cursor for it.
+ mAdapter.setLoading(false);
+ mAdapter.changeCursor(cursor);
+ setProgressBarIndeterminateVisibility(false);
+
+ // Now that the cursor is populated again, it's possible to restore the list state
+ if (mListState != null) {
+ getListView().onRestoreInstanceState(mListState);
+ if (mListHasFocus) {
+ getListView().requestFocus();
+ }
+ mListHasFocus = false;
+ mListState = null;
+ }
+ } else {
+ cursor.close();
+ }
+ }
+ }
+
+ 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();
+ }
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+
+ int sortMode = TRACK_MENU;
+ if (icicle == null) {
+ mSelectedUri = getIntent().getParcelableExtra(
+ RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
+ } else {
+ mSelectedUri = (Uri)icicle.getParcelable(
+ RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
+ // Retrieve list state. This will be applied after the
+ // QueryHandler has run
+ mListState = icicle.getParcelable(LIST_STATE_KEY);
+ mListHasFocus = icicle.getBoolean(FOCUS_KEY);
+ sortMode = icicle.getInt(SORT_MODE_KEY, sortMode);
+ }
+ if (Intent.ACTION_GET_CONTENT.equals(getIntent().getAction())) {
+ mBaseUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ } else {
+ mBaseUri = getIntent().getData();
+ if (mBaseUri == null) {
+ Log.w("MusicPicker", "No data URI given to PICK action");
+ finish();
+ return;
+ }
+ }
+
+ setContentView(R.layout.music_picker);
+
+ mSortOrder = MediaStore.Audio.Media.TITLE_KEY;
+
+ final ListView listView = getListView();
+
+ listView.setItemsCanFocus(false);
+
+ mAdapter = new TrackListAdapter(this, listView,
+ R.layout.music_picker_item, new String[] {},
+ new int[] {});
+
+ setListAdapter(mAdapter);
+
+ listView.setTextFilterEnabled(true);
+
+ // We manually save/restore the listview state
+ listView.setSaveEnabled(false);
+
+ mQueryHandler = new QueryHandler(this);
+
+ mProgressContainer = findViewById(R.id.progressContainer);
+ mListContainer = findViewById(R.id.listContainer);
+
+ mOkayButton = findViewById(R.id.okayButton);
+ mOkayButton.setOnClickListener(this);
+ mCancelButton = findViewById(R.id.cancelButton);
+ mCancelButton.setOnClickListener(this);
+
+ // If there is a currently selected Uri, then try to determine who
+ // it is.
+ if (mSelectedUri != null) {
+ Uri.Builder builder = mSelectedUri.buildUpon();
+ String path = mSelectedUri.getEncodedPath();
+ int idx = path.lastIndexOf('/');
+ if (idx >= 0) {
+ path = path.substring(0, idx);
+ }
+ builder.encodedPath(path);
+ Uri baseSelectedUri = builder.build();
+ if (DBG) Log.v(TAG, "Selected Uri: " + mSelectedUri);
+ if (DBG) Log.v(TAG, "Selected base Uri: " + baseSelectedUri);
+ if (DBG) Log.v(TAG, "Base Uri: " + mBaseUri);
+ if (baseSelectedUri.equals(mBaseUri)) {
+ // If the base Uri of the selected Uri is the same as our
+ // content's base Uri, then use the selection!
+ mSelectedId = ContentUris.parseId(mSelectedUri);
+ }
+ }
+
+ setSortMode(sortMode);
+ }
+
+ @Override public void onRestart() {
+ super.onRestart();
+ doQuery(false, null);
+ }
+
+ @Override public boolean onOptionsItemSelected(MenuItem item) {
+ if (setSortMode(item.getItemId())) {
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ menu.add(Menu.NONE, TRACK_MENU, Menu.NONE, R.string.sort_by_track);
+ menu.add(Menu.NONE, ALBUM_MENU, Menu.NONE, R.string.sort_by_album);
+ menu.add(Menu.NONE, ARTIST_MENU, Menu.NONE, R.string.sort_by_artist);
+ return true;
+ }
+
+ @Override protected void onSaveInstanceState(Bundle icicle) {
+ super.onSaveInstanceState(icicle);
+ // Save list state in the bundle so we can restore it after the
+ // QueryHandler has run
+ icicle.putParcelable(LIST_STATE_KEY, getListView().onSaveInstanceState());
+ icicle.putBoolean(FOCUS_KEY, getListView().hasFocus());
+ icicle.putInt(SORT_MODE_KEY, mSortMode);
+ }
+
+ @Override public void onPause() {
+ super.onPause();
+ stopMediaPlayer();
+ }
+
+ @Override public void onStop() {
+ super.onStop();
+
+ // We don't want the list to display the empty state, since when we
+ // resume it will still be there and show up while the new query is
+ // happening. After the async query finishes in response to onResume()
+ // setLoading(false) will be called.
+ mAdapter.setLoading(true);
+ mAdapter.changeCursor(null);
+ }
+
+ /**
+ * Changes the current sort order, building the appropriate query string
+ * for the selected order.
+ */
+ boolean setSortMode(int sortMode) {
+ if (sortMode != mSortMode) {
+ switch (sortMode) {
+ case TRACK_MENU:
+ mSortMode = sortMode;
+ mSortOrder = MediaStore.Audio.Media.TITLE_KEY;
+ doQuery(false, null);
+ return true;
+ case ALBUM_MENU:
+ mSortMode = sortMode;
+ mSortOrder = MediaStore.Audio.Media.ALBUM_KEY + " ASC, "
+ + MediaStore.Audio.Media.TRACK + " ASC, "
+ + MediaStore.Audio.Media.TITLE_KEY + " ASC";
+ doQuery(false, null);
+ return true;
+ case ARTIST_MENU:
+ mSortMode = sortMode;
+ mSortOrder = MediaStore.Audio.Media.ARTIST_KEY + " ASC, "
+ + MediaStore.Audio.Media.ALBUM_KEY + " ASC, "
+ + MediaStore.Audio.Media.TRACK + " ASC, "
+ + MediaStore.Audio.Media.TITLE_KEY + " ASC";
+ doQuery(false, null);
+ return true;
+ }
+
+ }
+ return false;
+ }
+
+ /**
+ * The first time this is called, we hide the large progress indicator
+ * and show the list view, doing fade animations between them.
+ */
+ void makeListShown() {
+ if (!mListShown) {
+ mListShown = true;
+ mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
+ this, android.R.anim.fade_out));
+ mProgressContainer.setVisibility(View.GONE);
+ mListContainer.startAnimation(AnimationUtils.loadAnimation(
+ this, android.R.anim.fade_in));
+ mListContainer.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Common method for performing a query of the music database, called for
+ * both top-level queries and filtering.
+ *
+ * @param sync If true, this query should be done synchronously and the
+ * resulting cursor returned. If false, it will be done asynchronously and
+ * null returned.
+ * @param filterstring If non-null, this is a filter to apply to the query.
+ */
+ Cursor doQuery(boolean sync, String filterstring) {
+ // Cancel any pending queries
+ mQueryHandler.cancelOperation(MY_QUERY_TOKEN);
+
+ 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 ?");
+ }
+ }
+
+ // We want to show all audio files, even recordings. Enforcing the
+ // following condition would hide recordings.
+ //where.append(" AND " + MediaStore.Audio.Media.IS_MUSIC + "=1");
+
+ if (sync) {
+ try {
+ return getContentResolver().query(mBaseUri, CURSOR_COLS,
+ where.toString(), keywords, mSortOrder);
+ } catch (UnsupportedOperationException ex) {
+ }
+ } else {
+ mAdapter.setLoading(true);
+ setProgressBarIndeterminateVisibility(true);
+ mQueryHandler.startQuery(MY_QUERY_TOKEN, null, mBaseUri, CURSOR_COLS,
+ where.toString(), keywords, mSortOrder);
+ }
+ return null;
+ }
+
+ @Override protected void onListItemClick(ListView l, View v, int position,
+ long id) {
+ mCursor.moveToPosition(position);
+ if (DBG) Log.v(TAG, "Click on " + position + " (id=" + id
+ + ", cursid="
+ + mCursor.getLong(mCursor.getColumnIndex(MediaStore.Audio.Media._ID))
+ + ") in cursor " + mCursor
+ + " adapter=" + l.getAdapter());
+ setSelected(mCursor);
+ }
+
+ void setSelected(Cursor c) {
+ Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ long newId = mCursor.getLong(mCursor.getColumnIndex(MediaStore.Audio.Media._ID));
+ mSelectedUri = ContentUris.withAppendedId(uri, newId);
+
+ mSelectedId = newId;
+ if (newId != mPlayingId || mMediaPlayer == null) {
+ stopMediaPlayer();
+ mMediaPlayer = new MediaPlayer();
+ try {
+ mMediaPlayer.setDataSource(this, mSelectedUri);
+ mMediaPlayer.setOnCompletionListener(this);
+ mMediaPlayer.setAudioStreamType(AudioManager.STREAM_RING);
+ mMediaPlayer.prepare();
+ mMediaPlayer.start();
+ mPlayingId = newId;
+ getListView().invalidateViews();
+ } catch (IOException e) {
+ Log.w("MusicPicker", "Unable to play track", e);
+ }
+ } else if (mMediaPlayer != null) {
+ stopMediaPlayer();
+ getListView().invalidateViews();
+ }
+ }
+
+ public void onCompletion(MediaPlayer mp) {
+ if (mMediaPlayer == mp) {
+ mp.stop();
+ mp.release();
+ mMediaPlayer = null;
+ mPlayingId = -1;
+ getListView().invalidateViews();
+ }
+ }
+
+ void stopMediaPlayer() {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.stop();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ mPlayingId = -1;
+ }
+ }
+
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.okayButton:
+ if (mSelectedId >= 0) {
+ setResult(RESULT_OK, new Intent().setData(mSelectedUri));
+ finish();
+ }
+ break;
+
+ case R.id.cancelButton:
+ finish();
+ break;
+ }
+ }
+}
diff --git a/src/com/android/music/MusicUtils.java b/src/com/android/music/MusicUtils.java
new file mode 100644
index 0000000..2f5aeca
--- /dev/null
+++ b/src/com/android/music/MusicUtils.java
@@ -0,0 +1,1196 @@
+/*
+ * 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.View;
+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 QUEUE = 12;
+ public final static int CHILD_MENU_BASE = 13; // this should be the last item
+ }
+
+ public static String makeAlbumsLabel(Context context, int numalbums, int numsongs, boolean isUnknown) {
+ // There are two formats for the albums/songs information:
+ // "N Song(s)" - used for unknown artist/album
+ // "N Album(s)" - used for known albums
+
+ StringBuilder songs_albums = new StringBuilder();
+
+ Resources r = context.getResources();
+ if (isUnknown) {
+ if (numsongs == 1) {
+ songs_albums.append(context.getString(R.string.onesong));
+ } else {
+ String f = r.getQuantityText(R.plurals.Nsongs, numsongs).toString();
+ sFormatBuilder.setLength(0);
+ sFormatter.format(f, Integer.valueOf(numsongs));
+ songs_albums.append(sFormatBuilder);
+ }
+ } else {
+ 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));
+ }
+ return songs_albums.toString();
+ }
+
+ /**
+ * This is now only used for the query screen
+ */
+ 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);
+ if (sConnectionMap.isEmpty()) {
+ // presumably there is nobody interested in the service at this point,
+ // so don't hang on to the ServiceConnection
+ sService = null;
+ }
+ }
+
+ 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 = -1;
+ try {
+ colidx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.Members.AUDIO_ID);
+ } catch (IllegalArgumentException ex) {
+ 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) {
+
+ Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", plid);
+ context.getContentResolver().delete(uri, null, null);
+ 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.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;
+ 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) ||
+ status.equals(Environment.MEDIA_UNMOUNTED)) {
+ 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);
+ View v = a.findViewById(R.id.sd_message);
+ if (v != null) {
+ v.setVisibility(View.VISIBLE);
+ }
+ v = a.findViewById(R.id.sd_icon);
+ if (v != null) {
+ v.setVisibility(View.VISIBLE);
+ }
+ v = a.findViewById(android.R.id.list);
+ if (v != null) {
+ v.setVisibility(View.GONE);
+ }
+ TextView tv = (TextView) a.findViewById(R.id.sd_message);
+ tv.setText(message);
+ }
+
+ public static void hideDatabaseError(Activity a) {
+ View v = a.findViewById(R.id.sd_message);
+ if (v != null) {
+ v.setVisibility(View.GONE);
+ }
+ v = a.findViewById(R.id.sd_icon);
+ if (v != null) {
+ v.setVisibility(View.GONE);
+ }
+ v = a.findViewById(android.R.id.list);
+ if (v != null) {
+ v.setVisibility(View.VISIBLE);
+ }
+ }
+
+ 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, force_shuffle ? -1 : 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 border on the right side in the ImageView
+ // used to display this drawable. Take it into account now, so we don't have to
+ // scale later.
+ w -= 1;
+ 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);
+ // Bitmap.createScaledBitmap() can return the same bitmap
+ if (tmp != b) 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) {
+ return getArtwork(context, album_id, true);
+ }
+
+ /** 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, boolean allowDefault) {
+
+ 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;
+ }
+ if (allowDefault) {
+ return getDefaultArtwork(context);
+ } else {
+ return null;
+ }
+ }
+
+ 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) {
+ if (allowDefault) {
+ return getDefaultArtwork(context);
+ } else {
+ return null;
+ }
+ }
+ }
+ 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 if (allowDefault) {
+ bm = getDefaultArtwork(context);
+ } else {
+ bm = null;
+ }
+ 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.d(TAG, "File creation failed for " + path);
+ }
+ 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) {
+ return null;
+ } catch (NullPointerException ex) {
+ return null;
+ }
+ }
+ 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
index 0000000..aa0525e
--- /dev/null
+++ b/src/com/android/music/PlaylistBrowserActivity.java
@@ -0,0 +1,605 @@
+/*
+ * 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.AsyncQueryHandler;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+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.database.sqlite.SQLiteException;
+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 String TAG = "PlaylistBrowserActivity";
+ 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 static final long PODCASTS_PLAYLIST = -3;
+ private PlaylistListAdapter mAdapter;
+ boolean mAdapterSent;
+
+ 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 == PODCASTS_PLAYLIST) {
+ playPodcasts();
+ } 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);
+
+ setContentView(R.layout.media_picker_activity);
+ ListView lv = getListView();
+ lv.setOnCreateContextMenuListener(this);
+ lv.setTextFilterEnabled(true);
+
+ mAdapter = (PlaylistListAdapter) getLastNonConfigurationInstance();
+ if (mAdapter == null) {
+ //Log.i("@@@", "starting query");
+ mAdapter = new PlaylistListAdapter(
+ getApplication(),
+ this,
+ R.layout.track_list_item,
+ mPlaylistCursor,
+ new String[] { MediaStore.Audio.Playlists.NAME},
+ new int[] { android.R.id.text1 });
+ setListAdapter(mAdapter);
+ setTitle(R.string.working_playlists);
+ getPlaylistCursor(mAdapter.getQueryHandler(), null);
+ } else {
+ mAdapter.setActivity(this);
+ setListAdapter(mAdapter);
+ mPlaylistCursor = mAdapter.getCursor();
+ // If mPlaylistCursor is null, this can be because it doesn't have
+ // a cursor yet (because the initial query that sets its cursor
+ // is still in progress), or because the query failed.
+ // In order to not flash the error dialog at the user for the
+ // first case, simply retry the query when the cursor is null.
+ // Worst case, we end up doing the same query twice.
+ if (mPlaylistCursor != null) {
+ init(mPlaylistCursor);
+ } else {
+ setTitle(R.string.working_playlists);
+ getPlaylistCursor(mAdapter.getQueryHandler(), null);
+ }
+ }
+ }
+
+ @Override
+ public Object onRetainNonConfigurationInstance() {
+ PlaylistListAdapter a = mAdapter;
+ mAdapterSent = true;
+ return a;
+ }
+
+ @Override
+ public void onDestroy() {
+ MusicUtils.unbindFromService(this);
+ if (!mAdapterSent) {
+ Cursor c = mAdapter.getCursor();
+ if (c != null) {
+ c.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) {
+ getPlaylistCursor(mAdapter.getQueryHandler(), null);
+ }
+ };
+ public void init(Cursor cursor) {
+
+ mAdapter.changeCursor(cursor);
+
+ if (mPlaylistCursor == null) {
+ MusicUtils.displayDatabaseError(this);
+ closeContextMenu();
+ mReScanHandler.sendEmptyMessageDelayed(0, 1000);
+ return;
+ }
+
+ MusicUtils.hideDatabaseError(this);
+ setTitle();
+ }
+
+ private void setTitle() {
+ setTitle(R.string.playlists_title);
+ }
+
+ @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;
+
+ menu.add(0, PLAY_SELECTION, 0, R.string.play_selection);
+
+ if (mi.id >= 0 /*|| mi.id == PODCASTS_PLAYLIST*/) {
+ menu.add(0, DELETE_PLAYLIST, 0, R.string.delete_playlist_menu);
+ }
+
+ if (mi.id == RECENTLY_ADDED_PLAYLIST) {
+ menu.add(0, EDIT_PLAYLIST, 0, R.string.edit_playlist_menu);
+ }
+
+ if (mi.id >= 0) {
+ menu.add(0, RENAME_PLAYLIST, 0, R.string.rename_playlist_menu);
+ }
+
+ mPlaylistCursor.moveToPosition(mi.position);
+ menu.setHeaderTitle(mPlaylistCursor.getString(mPlaylistCursor.getColumnIndexOrThrow(
+ 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 if (mi.id == PODCASTS_PLAYLIST) {
+ playPodcasts();
+ } else {
+ MusicUtils.playPlaylist(this, mi.id);
+ }
+ break;
+ case DELETE_PLAYLIST:
+ Uri uri = ContentUris.withAppendedId(
+ MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, mi.id);
+ getContentResolver().delete(uri, null, null);
+ 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 {
+ Log.e(TAG, "should not be here");
+ }
+ 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 {
+ getPlaylistCursor(mAdapter.getQueryHandler(), null);
+ }
+ 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) {
+ Intent intent = new Intent(Intent.ACTION_PICK);
+ intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
+ intent.putExtra("playlist", "recentlyadded");
+ startActivity(intent);
+ } else if (id == PODCASTS_PLAYLIST) {
+ Intent intent = new Intent(Intent.ACTION_PICK);
+ intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
+ intent.putExtra("playlist", "podcasts");
+ startActivity(intent);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_EDIT);
+ intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
+ intent.putExtra("playlist", Long.valueOf(id).toString());
+ startActivity(intent);
+ }
+ }
+
+ private void playRecentlyAdded() {
+ // do a query for all songs added 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;
+ }
+ try {
+ int len = cursor.getCount();
+ int [] list = new int[len];
+ for (int i = 0; i < len; i++) {
+ cursor.moveToNext();
+ list[i] = cursor.getInt(0);
+ }
+ MusicUtils.playAll(this, list, 0);
+ } catch (SQLiteException ex) {
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void playPodcasts() {
+ // do a query for all files that are podcasts
+ final String[] ccols = new String[] { MediaStore.Audio.Media._ID};
+ Cursor cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ ccols, MediaStore.Audio.Media.IS_PODCAST + "=1",
+ null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+
+ if (cursor == null) {
+ // Todo: show a message
+ return;
+ }
+ try {
+ int len = cursor.getCount();
+ int [] list = new int[len];
+ for (int i = 0; i < len; i++) {
+ cursor.moveToNext();
+ list[i] = cursor.getInt(0);
+ }
+ MusicUtils.playAll(this, list, 0);
+ } catch (SQLiteException ex) {
+ } finally {
+ cursor.close();
+ }
+ }
+
+
+ String[] mCols = new String[] {
+ MediaStore.Audio.Playlists._ID,
+ MediaStore.Audio.Playlists.NAME
+ };
+
+ private Cursor getPlaylistCursor(AsyncQueryHandler async, String filterstring) {
+
+ 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();
+
+
+ if (async != null) {
+ async.startQuery(0, null, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+ mCols, whereclause, keywords, MediaStore.Audio.Playlists.NAME);
+ return null;
+ }
+ Cursor c = null;
+ c = MusicUtils.query(this, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+ mCols, whereclause, keywords, MediaStore.Audio.Playlists.NAME);
+
+ return mergedCursor(c);
+ }
+
+ private Cursor mergedCursor(Cursor c) {
+ if (c == null) {
+ return null;
+ }
+ if (c instanceof MergeCursor) {
+ // this shouldn't happen, but fail gracefully
+ Log.d("PlaylistBrowserActivity", "Already wrapped");
+ return c;
+ }
+ 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);
+
+ // check if there are any podcasts
+ Cursor counter = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ new String[] {"count(*)"}, "is_podcast=1", null, null);
+ if (counter != null) {
+ counter.moveToFirst();
+ int numpodcasts = counter.getInt(0);
+ counter.close();
+ if (numpodcasts > 0) {
+ ArrayList<Object> podcasts = new ArrayList<Object>(2);
+ podcasts.add(PODCASTS_PLAYLIST);
+ podcasts.add(getString(R.string.podcasts_listitem));
+ autoplaylists.add(podcasts);
+ }
+ }
+
+ ArrayListCursor autoplaylistscursor = new ArrayListCursor(mCols, autoplaylists);
+
+ Cursor cc = new MergeCursor(new Cursor [] {autoplaylistscursor, c});
+ return cc;
+ }
+
+ static class PlaylistListAdapter extends SimpleCursorAdapter {
+ int mTitleIdx;
+ int mIdIdx;
+ private PlaylistBrowserActivity mActivity = null;
+ private AsyncQueryHandler mQueryHandler;
+ private String mConstraint = null;
+ private boolean mConstraintIsValid = false;
+
+ class QueryHandler extends AsyncQueryHandler {
+ QueryHandler(ContentResolver res) {
+ super(res);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ //Log.i("@@@", "query complete: " + cursor.getCount() + " " + mActivity);
+ if (cursor != null) {
+ cursor = mActivity.mergedCursor(cursor);
+ }
+ mActivity.init(cursor);
+ }
+ }
+
+ PlaylistListAdapter(Context context, PlaylistBrowserActivity currentactivity,
+ int layout, Cursor cursor, String[] from, int[] to) {
+ super(context, layout, cursor, from, to);
+ mActivity = currentactivity;
+ getColumnIndices(cursor);
+ mQueryHandler = new QueryHandler(context.getContentResolver());
+ }
+ private void getColumnIndices(Cursor cursor) {
+ if (cursor != null) {
+ mTitleIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.NAME);
+ mIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists._ID);
+ }
+ }
+
+ public void setActivity(PlaylistBrowserActivity newactivity) {
+ mActivity = newactivity;
+ }
+
+ public AsyncQueryHandler getQueryHandler() {
+ return mQueryHandler;
+ }
+
+ @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);
+
+ long id = cursor.getLong(mIdIdx);
+
+ ImageView iv = (ImageView) view.findViewById(R.id.icon);
+ if (id == RECENTLY_ADDED_PLAYLIST) {
+ iv.setImageResource(R.drawable.ic_mp_playlist_recently_added_list);
+ } else {
+ 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);
+
+ view.findViewById(R.id.line2).setVisibility(View.GONE);
+ }
+
+ @Override
+ public void changeCursor(Cursor cursor) {
+ if (cursor != mActivity.mPlaylistCursor) {
+ mActivity.mPlaylistCursor = cursor;
+ super.changeCursor(cursor);
+ getColumnIndices(cursor);
+ }
+ }
+
+ @Override
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ String s = constraint.toString();
+ if (mConstraintIsValid && (
+ (s == null && mConstraint == null) ||
+ (s != null && s.equals(mConstraint)))) {
+ return getCursor();
+ }
+ Cursor c = mActivity.getPlaylistCursor(null, s);
+ mConstraint = s;
+ mConstraintIsValid = true;
+ return c;
+ }
+ }
+
+ private Cursor mPlaylistCursor;
+}
+
diff --git a/src/com/android/music/QueryBrowserActivity.java b/src/com/android/music/QueryBrowserActivity.java
new file mode 100644
index 0000000..2a482ae
--- /dev/null
+++ b/src/com/android/music/QueryBrowserActivity.java
@@ -0,0 +1,421 @@
+/*
+ * 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.AsyncQueryHandler;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+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.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+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 QueryListAdapter mAdapter;
+ private boolean mAdapterSent;
+ private String mFilterString = "";
+
+ 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);
+ IntentFilter f = new IntentFilter();
+ f.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+ f.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+ f.addDataScheme("file");
+ registerReceiver(mScanListener, f);
+
+ if (icicle == null) {
+ Intent intent = getIntent();
+
+ if (intent.getAction().equals(Intent.ACTION_VIEW)) {
+ // this is something we got from the search bar
+ Uri uri = intent.getData();
+ String path = uri.toString();
+ if (path.startsWith("content://media/external/audio/media/")) {
+ // This is a specific file
+ String id = uri.getLastPathSegment();
+ int [] list = new int[] { Integer.valueOf(id) };
+ MusicUtils.playAll(this, list, 0);
+ finish();
+ return;
+ } else if (path.startsWith("content://media/external/audio/albums/")) {
+ // This is an album, show the songs on it
+ Intent i = new Intent(Intent.ACTION_PICK);
+ i.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
+ i.putExtra("album", uri.getLastPathSegment());
+ startActivity(i);
+ finish();
+ return;
+ } else if (path.startsWith("content://media/external/audio/artists/")) {
+ // This is an artist, show the albums for that artist
+ Intent i = new Intent(Intent.ACTION_PICK);
+ i.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/album");
+ i.putExtra("artist", uri.getLastPathSegment());
+ startActivity(i);
+ finish();
+ return;
+ }
+ }
+ mFilterString = intent.getStringExtra(SearchManager.QUERY);
+ }
+
+ setContentView(R.layout.query_activity);
+ mTrackList = getListView();
+ mTrackList.setTextFilterEnabled(true);
+ mAdapter = (QueryListAdapter) getLastNonConfigurationInstance();
+ if (mAdapter == null) {
+ mAdapter = new QueryListAdapter(
+ getApplication(),
+ this,
+ R.layout.track_list_item,
+ null, // cursor
+ new String[] {},
+ new int[] {});
+ setListAdapter(mAdapter);
+ if (TextUtils.isEmpty(mFilterString)) {
+ getQueryCursor(mAdapter.getQueryHandler(), null);
+ } else {
+ mTrackList.setFilterText(mFilterString);
+ mFilterString = null;
+ }
+ } else {
+ mAdapter.setActivity(this);
+ setListAdapter(mAdapter);
+ mQueryCursor = mAdapter.getCursor();
+ if (mQueryCursor != null) {
+ init(mQueryCursor);
+ } else {
+ getQueryCursor(mAdapter.getQueryHandler(), mFilterString);
+ }
+ }
+ }
+
+ @Override
+ public Object onRetainNonConfigurationInstance() {
+ mAdapterSent = true;
+ return mAdapter;
+ }
+
+ @Override
+ public void onPause() {
+ mReScanHandler.removeCallbacksAndMessages(null);
+ super.onPause();
+ }
+
+ @Override
+ public void onDestroy() {
+ MusicUtils.unbindFromService(this);
+ unregisterReceiver(mScanListener);
+ super.onDestroy();
+ if (!mAdapterSent && mAdapter != null) {
+ Cursor c = mAdapter.getCursor();
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ /*
+ * This listener gets called when the media scanner starts up, and when the
+ * sd card is unmounted.
+ */
+ private BroadcastReceiver mScanListener = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ MusicUtils.setSpinnerState(QueryBrowserActivity.this);
+ mReScanHandler.sendEmptyMessage(0);
+ }
+ };
+
+ private Handler mReScanHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ getQueryCursor(mAdapter.getQueryHandler(), null);
+ // if the query results in a null cursor, onQueryComplete() will
+ // call init(), which will post a delayed message to this handler
+ // in order to try again.
+ }
+ };
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ switch (requestCode) {
+ case SCAN_DONE:
+ if (resultCode == RESULT_CANCELED) {
+ finish();
+ } else {
+ getQueryCursor(mAdapter.getQueryHandler(), null);
+ }
+ break;
+ }
+ }
+
+ public void init(Cursor c) {
+
+ mAdapter.changeCursor(c);
+
+ if (mQueryCursor == null) {
+ MusicUtils.displayDatabaseError(this);
+ setListAdapter(null);
+ mReScanHandler.sendEmptyMessageDelayed(0, 1000);
+ return;
+ }
+ MusicUtils.hideDatabaseError(this);
+ }
+
+ @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.getColumnIndexOrThrow(
+ 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(AsyncQueryHandler async, String filter) {
+ if (filter == null) {
+ filter = "";
+ }
+ 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"
+ };
+
+ Uri search = Uri.parse("content://media/external/audio/" +
+ SearchManager.SUGGEST_URI_PATH_QUERY + "/" + Uri.encode(filter));
+
+ Cursor ret = null;
+ if (async != null) {
+ async.startQuery(0, null, search, ccols, null, null, null);
+ } else {
+ ret = MusicUtils.query(this, search, ccols, null, null, null);
+ }
+ return ret;
+ }
+
+ static class QueryListAdapter extends SimpleCursorAdapter {
+ private QueryBrowserActivity mActivity = null;
+ private AsyncQueryHandler mQueryHandler;
+ private String mConstraint = null;
+ private boolean mConstraintIsValid = false;
+
+ class QueryHandler extends AsyncQueryHandler {
+ QueryHandler(ContentResolver res) {
+ super(res);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ mActivity.init(cursor);
+ }
+ }
+
+ QueryListAdapter(Context context, QueryBrowserActivity currentactivity,
+ int layout, Cursor cursor, String[] from, int[] to) {
+ super(context, layout, cursor, from, to);
+ mActivity = currentactivity;
+ mQueryHandler = new QueryHandler(context.getContentResolver());
+ }
+
+ public void setActivity(QueryBrowserActivity newactivity) {
+ mActivity = newactivity;
+ }
+
+ public AsyncQueryHandler getQueryHandler() {
+ return mQueryHandler;
+ }
+
+ @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.getColumnIndexOrThrow(
+ MediaStore.Audio.Media.MIME_TYPE));
+
+ if (mimetype == null) {
+ mimetype = "audio/";
+ }
+ if (mimetype.equals("artist")) {
+ iv.setImageResource(R.drawable.ic_mp_artist_list);
+ String name = cursor.getString(cursor.getColumnIndexOrThrow(
+ 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.getColumnIndexOrThrow("data1"));
+ int numsongs = cursor.getInt(cursor.getColumnIndexOrThrow("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.albumart_mp_unknown_list);
+ String name = cursor.getString(cursor.getColumnIndexOrThrow(
+ 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.getColumnIndexOrThrow("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_mp_song_list);
+ String name = cursor.getString(cursor.getColumnIndexOrThrow(
+ SearchManager.SUGGEST_COLUMN_TEXT_1));
+ tv1.setText(name);
+
+ String displayname = cursor.getString(cursor.getColumnIndexOrThrow("data1"));
+ if (name.equals(MediaFile.UNKNOWN_STRING)) {
+ displayname = context.getString(R.string.unknown_artist_name);
+ }
+ name = cursor.getString(cursor.getColumnIndexOrThrow("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) {
+ if (cursor != mActivity.mQueryCursor) {
+ mActivity.mQueryCursor = cursor;
+ super.changeCursor(cursor);
+ }
+ }
+ @Override
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ String s = constraint.toString();
+ if (mConstraintIsValid && (
+ (s == null && mConstraint == null) ||
+ (s != null && s.equals(mConstraint)))) {
+ return getCursor();
+ }
+ Cursor c = mActivity.getQueryCursor(null, s);
+ mConstraint = s;
+ mConstraintIsValid = true;
+ return c;
+ }
+ }
+
+ 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
index 0000000..f81e2af
--- /dev/null
+++ b/src/com/android/music/RenamePlaylist.java
@@ -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
index 0000000..08c951c
--- /dev/null
+++ b/src/com/android/music/RepeatingImageButton.java
@@ -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
index 0000000..e5eae94
--- /dev/null
+++ b/src/com/android/music/ScanningProgress.java
@@ -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
index 0000000..0537bad
--- /dev/null
+++ b/src/com/android/music/StreamStarter.java
@@ -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
index 0000000..4276b7b
--- /dev/null
+++ b/src/com/android/music/TouchInterceptor.java
@@ -0,0 +1,389 @@
+/*
+ * 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.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 inside 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 GestureDetector mGestureDetector;
+ private static final int FLING = 0;
+ private static final int SLIDE = 1;
+ private int mRemoveMode = -1;
+ private Rect mTempRect = new Rect();
+ private Bitmap mDragBitmap;
+ private final int mTouchSlop;
+
+ public TouchInterceptor(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ SharedPreferences pref = context.getSharedPreferences("Music", 3);
+ mRemoveMode = pref.getInt("deletemode", -1);
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (mRemoveListener != null && mGestureDetector == null) {
+ if (mRemoveMode == FLING) {
+ mGestureDetector = new GestureDetector(getContext(), 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);
+ // Create a copy of the drawing cache so that it does not get recycled
+ // by the framework when the list tries to clean up memory
+ Bitmap bitmap = Bitmap.createBitmap(item.getDrawingCache());
+ startDragging(bitmap, y);
+ mDragPos = itemnum;
+ mFirstDragPos = mDragPos;
+ mHeight = getHeight();
+ int touchSlop = mTouchSlop;
+ mUpperBound = Math.min(y - touchSlop, mHeight / 3);
+ mLowerBound = Math.max(y + touchSlop, 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 adjustedy = y - mDragPoint - 32;
+ int pos = myPointToPosition(0, adjustedy);
+ if (pos >= 0) {
+ if (pos <= mFirstDragPos) {
+ pos += 1;
+ }
+ } else if (adjustedy < 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 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 == childnum) {
+ if (mDragPos < getCount() - 1) {
+ 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) {
+ int action = ev.getAction();
+ switch (action) {
+ 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 && mDragPos >= 0 && mDragPos < getCount()) {
+ mDropListener.drop(mFirstDragPos, mDragPos);
+ }
+ unExpandViews(false);
+ }
+ break;
+
+ case MotionEvent.ACTION_DOWN:
+ 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 (action == MotionEvent.ACTION_DOWN || 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);
+ int backGroundColor = mContext.getResources().getColor(R.color.dragndrop_background);
+ v.setBackgroundColor(backGroundColor);
+ v.setImageBitmap(bm);
+
+ if (mDragBitmap != null) {
+ mDragBitmap.recycle();
+ }
+ mDragBitmap = 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;
+ if (mDragBitmap != null) {
+ mDragBitmap.recycle();
+ mDragBitmap = 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
index 0000000..58f556c
--- /dev/null
+++ b/src/com/android/music/TrackBrowserActivity.java
@@ -0,0 +1,1461 @@
+/*
+ * 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.AsyncQueryHandler;
+import android.content.BroadcastReceiver;
+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.IntentFilter;
+import android.content.ServiceConnection;
+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.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+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.AlphabetIndexer;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.SectionIndexer;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+
+import java.text.Collator;
+import java.util.Arrays;
+
+public class TrackBrowserActivity extends ListActivity
+ implements View.OnCreateContextMenuListener, MusicUtils.Defs, ServiceConnection
+{
+ 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 SEARCH = CHILD_MENU_BASE + 6;
+
+
+ private static final String LOGTAG = "TrackBrowser";
+
+ private String[] mCursorCols;
+ private String[] mPlaylistMemberCols;
+ private boolean mDeletedOneRow = false;
+ private boolean mEditMode = false;
+ private String mCurrentTrackName;
+ private String mCurrentAlbumName;
+ private String mCurrentArtistNameForAlbum;
+ private ListView mTrackList;
+ private Cursor mTrackCursor;
+ private TrackListAdapter mAdapter;
+ private boolean mAdapterSent = false;
+ private String mAlbumId;
+ private String mArtistId;
+ private String mPlaylist;
+ private String mGenre;
+ private String mSortOrder;
+ private int mSelectedPosition;
+ private long mSelectedId;
+
+ 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
+ };
+
+ setContentView(R.layout.media_picker_activity);
+ mTrackList = getListView();
+ mTrackList.setOnCreateContextMenuListener(this);
+ if (mEditMode) {
+ //((TouchInterceptor) mTrackList).setDragListener(mDragListener);
+ ((TouchInterceptor) mTrackList).setDropListener(mDropListener);
+ ((TouchInterceptor) mTrackList).setRemoveListener(mRemoveListener);
+ mTrackList.setCacheColorHint(0);
+ } else {
+ mTrackList.setTextFilterEnabled(true);
+ }
+ mAdapter = (TrackListAdapter) getLastNonConfigurationInstance();
+
+ if (mAdapter != null) {
+ mAdapter.setActivity(this);
+ setListAdapter(mAdapter);
+ }
+ MusicUtils.bindToService(this, this);
+ }
+
+ public void onServiceConnected(ComponentName name, IBinder service)
+ {
+ 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);
+
+ if (mAdapter == null) {
+ //Log.i("@@@", "starting query");
+ mAdapter = new TrackListAdapter(
+ getApplication(), // need to use application context to avoid leaks
+ this,
+ mEditMode ? R.layout.edit_track_list_item : R.layout.track_list_item,
+ null, // cursor
+ new String[] {},
+ new int[] {},
+ "nowplaying".equals(mPlaylist),
+ mPlaylist != null &&
+ !(mPlaylist.equals("podcasts") || mPlaylist.equals("recentlyadded")));
+ setListAdapter(mAdapter);
+ setTitle(R.string.working_songs);
+ getTrackCursor(mAdapter.getQueryHandler(), null);
+ } else {
+ mTrackCursor = mAdapter.getCursor();
+ // If mTrackCursor is null, this can be because it doesn't have
+ // a cursor yet (because the initial query that sets its cursor
+ // is still in progress), or because the query failed.
+ // In order to not flash the error dialog at the user for the
+ // first case, simply retry the query when the cursor is null.
+ // Worst case, we end up doing the same query twice.
+ if (mTrackCursor != null) {
+ init(mTrackCursor);
+ } else {
+ setTitle(R.string.working_songs);
+ getTrackCursor(mAdapter.getQueryHandler(), null);
+ }
+ }
+ }
+
+ public void onServiceDisconnected(ComponentName name) {
+ // we can't really function without the service, so don't
+ finish();
+ }
+
+ @Override
+ public Object onRetainNonConfigurationInstance() {
+ TrackListAdapter a = mAdapter;
+ mAdapterSent = true;
+ return a;
+ }
+
+ @Override
+ public void onDestroy() {
+ 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 we didn't send the adapter off to another activity, we should
+ // close the cursor
+ if (!mAdapterSent) {
+ Cursor c = mAdapter.getCursor();
+ if (c != null) {
+ c.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();
+ }
+
+ /*
+ * This listener gets called when the media scanner starts up or finishes, and
+ * when the sd card is unmounted.
+ */
+ private BroadcastReceiver mScanListener = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_MEDIA_SCANNER_STARTED.equals(action) ||
+ Intent.ACTION_MEDIA_SCANNER_FINISHED.equals(action)) {
+ MusicUtils.setSpinnerState(TrackBrowserActivity.this);
+ }
+ mReScanHandler.sendEmptyMessage(0);
+ }
+ };
+
+ private Handler mReScanHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ getTrackCursor(mAdapter.getQueryHandler(), null);
+ // if the query results in a null cursor, onQueryComplete() will
+ // call init(), which will post a delayed message to this handler
+ // in order to try again.
+ }
+ };
+
+ 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(Cursor newCursor) {
+
+ mAdapter.changeCursor(newCursor); // also sets mTrackCursor
+
+ if (mTrackCursor == null) {
+ MusicUtils.displayDatabaseError(this);
+ closeContextMenu();
+ mReScanHandler.sendEmptyMessageDelayed(0, 1000);
+ return;
+ }
+
+ MusicUtils.hideDatabaseError(this);
+ setTitle();
+
+ // 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.getColumnIndexOrThrow(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 void setTitle() {
+
+ CharSequence fancyName = null;
+ if (mAlbumId != null) {
+ int numresults = mTrackCursor != null ? mTrackCursor.getCount() : 0;
+ if (numresults > 0) {
+ mTrackCursor.moveToFirst();
+ int idx = mTrackCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM);
+ fancyName = mTrackCursor.getString(idx);
+ // 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.getLong(mTrackCursor.getColumnIndexOrThrow(
+ 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(idx);
+ }
+ cursor.deactivate();
+ }
+ if (fancyName.equals(MediaFile.UNKNOWN_STRING)) {
+ fancyName = getString(R.string.unknown_album_name);
+ }
+ }
+ } 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 if (mPlaylist.equals("podcasts")){
+ fancyName = getText(R.string.podcasts_title);
+ } else if (mPlaylist.equals("recentlyadded")){
+ fancyName = getText(R.string.recentlyadded_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);
+ }
+ }
+
+ 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
+ Uri baseUri = MediaStore.Audio.Playlists.Members.getContentUri("external",
+ Long.valueOf(mPlaylist));
+ ContentValues values = new ContentValues();
+ String where = MediaStore.Audio.Playlists.Members._ID + "=?";
+ String [] wherearg = new String[1];
+ ContentResolver res = getContentResolver();
+
+ int colidx = mTrackCursor.getColumnIndexOrThrow(
+ 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);
+ values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, toidx);
+ wherearg[0] = mTrackCursor.getString(0);
+ res.update(baseUri, values, where, wherearg);
+ for (int i = from + 1; i <= to; i++) {
+ mTrackCursor.moveToPosition(i);
+ values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, i - 1);
+ wherearg[0] = mTrackCursor.getString(0);
+ res.update(baseUri, values, where, wherearg);
+ }
+ } else if (from > to) {
+ // move the item to somewhere earlier in the list
+ mTrackCursor.moveToPosition(to);
+ int toidx = mTrackCursor.getInt(colidx);
+ mTrackCursor.moveToPosition(from);
+ values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, toidx);
+ wherearg[0] = mTrackCursor.getString(0);
+ res.update(baseUri, values, where, wherearg);
+ for (int i = from - 1; i >= to; i--) {
+ mTrackCursor.moveToPosition(i);
+ values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, i + 1);
+ wherearg[0] = mTrackCursor.getString(0);
+ res.update(baseUri, values, where, wherearg);
+ }
+ }
+ }
+ }
+ };
+
+ 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();
+ if (mTrackCursor instanceof NowPlayingCursor) {
+ ((NowPlayingCursor)mTrackCursor).removeItem(which);
+ } else {
+ int colidx = mTrackCursor.getColumnIndexOrThrow(
+ MediaStore.Audio.Playlists.Members._ID);
+ mTrackCursor.moveToPosition(which);
+ long id = mTrackCursor.getLong(colidx);
+ Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external",
+ Long.valueOf(mPlaylist));
+ getContentResolver().delete(
+ ContentUris.withAppendedId(uri, id), null, null);
+ }
+ 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;
+ }
+ Cursor c = new NowPlayingCursor(MusicUtils.sService, mCursorCols);
+ if (c.getCount() == 0) {
+ finish();
+ return;
+ }
+ mAdapter.changeCursor(c);
+ }
+ }
+ };
+
+ @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);
+ 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);
+ menu.add(0, SEARCH, 0, R.string.search_title);
+ AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn;
+ mSelectedPosition = mi.position;
+ mTrackCursor.moveToPosition(mSelectedPosition);
+ try {
+ int id_idx = mTrackCursor.getColumnIndexOrThrow(
+ MediaStore.Audio.Playlists.Members.AUDIO_ID);
+ mSelectedId = mTrackCursor.getInt(id_idx);
+ } catch (IllegalArgumentException ex) {
+ mSelectedId = mi.id;
+ }
+ mCurrentAlbumName = mTrackCursor.getString(mTrackCursor.getColumnIndexOrThrow(
+ MediaStore.Audio.Media.ALBUM));
+ mCurrentArtistNameForAlbum = mTrackCursor.getString(mTrackCursor.getColumnIndexOrThrow(
+ MediaStore.Audio.Media.ARTIST));
+ mCurrentTrackName = mTrackCursor.getString(mTrackCursor.getColumnIndexOrThrow(
+ 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();
+ String f = getString(R.string.delete_song_desc);
+ String desc = String.format(f, mCurrentTrackName);
+ 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;
+ }
+
+ case REMOVE:
+ removePlaylistItem(mSelectedPosition);
+ return true;
+
+ case SEARCH:
+ doSearch();
+ return true;
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ void doSearch() {
+ CharSequence title = null;
+ String query = null;
+
+ Intent i = new Intent();
+ i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
+
+ title = mCurrentAlbumName;
+ query = mCurrentArtistNameForAlbum + " " + mCurrentAlbumName;
+ i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, mCurrentArtistNameForAlbum);
+ i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, mCurrentAlbumName);
+ i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, "audio/*");
+ title = getString(R.string.mediasearch, title);
+ i.putExtra(SearchManager.QUERY, query);
+
+ startActivity(Intent.createChooser(i, title));
+ }
+
+ // 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.getAction() == KeyEvent.ACTION_DOWN) {
+ 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();
+ ((NowPlayingCursor)mTrackCursor).removeItem(curpos);
+ v.setVisibility(View.VISIBLE);
+ mTrackList.invalidateViews();
+ } else {
+ // remove track from playlist
+ int colidx = mTrackCursor.getColumnIndexOrThrow(
+ MediaStore.Audio.Playlists.Members._ID);
+ mTrackCursor.moveToPosition(curpos);
+ long id = mTrackCursor.getLong(colidx);
+ Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external",
+ Long.valueOf(mPlaylist));
+ getContentResolver().delete(
+ ContentUris.withAppendedId(uri, id), null, null);
+ 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.getColumnIndexOrThrow(
+ MediaStore.Audio.Playlists.Members.PLAY_ORDER);
+ mTrackCursor.moveToPosition(curpos);
+ int currentplayidx = mTrackCursor.getInt(colidx);
+ Uri baseUri = MediaStore.Audio.Playlists.Members.getContentUri("external",
+ Long.valueOf(mPlaylist));
+ ContentValues values = new ContentValues();
+ String where = MediaStore.Audio.Playlists.Members._ID + "=?";
+ String [] wherearg = new String[1];
+ ContentResolver res = getContentResolver();
+ if (up) {
+ values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx - 1);
+ wherearg[0] = mTrackCursor.getString(0);
+ res.update(baseUri, values, where, wherearg);
+ mTrackCursor.moveToPrevious();
+ } else {
+ values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx + 1);
+ wherearg[0] = mTrackCursor.getString(0);
+ res.update(baseUri, values, where, wherearg);
+ mTrackCursor.moveToNext();
+ }
+ values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx);
+ wherearg[0] = mTrackCursor.getString(0);
+ res.update(baseUri, values, where, wherearg);
+ }
+ }
+
+ @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(com.android.internal.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(android.R.drawable.ic_menu_save);
+ if (mPlaylist.equals("nowplaying")) {
+ menu.add(0, CLEAR_PLAYLIST, 0, R.string.clear_playlist).setIcon(com.android.internal.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 {
+ getTrackCursor(mAdapter.getQueryHandler(), null);
+ }
+ break;
+
+ case NEW_PLAYLIST:
+ if (resultCode == RESULT_OK) {
+ Uri uri = intent.getData();
+ 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 = intent.getData();
+ if (uri != null) {
+ int [] list = MusicUtils.getSongListForCursor(mTrackCursor);
+ int plid = Integer.parseInt(uri.getLastPathSegment());
+ MusicUtils.addToPlaylist(this, list, plid);
+ }
+ }
+ break;
+ }
+ }
+
+ private Cursor getTrackCursor(AsyncQueryHandler async, String filter) {
+ 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 (filter != null) {
+ String [] searchWords = filter.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;
+ if (async != null) {
+ async.startQuery(0, null,
+ MediaStore.Audio.Genres.Members.getContentUri("external",
+ Integer.valueOf(mGenre)),
+ mCursorCols, where.toString(), keywords, mSortOrder);
+ ret = null;
+ } else {
+ 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 if (mPlaylist.equals("podcasts")) {
+ where.append(" AND " + MediaStore.Audio.Media.IS_PODCAST + "=1");
+ if (async != null) {
+ async.startQuery(0, null,
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursorCols,
+ where.toString(), keywords, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+ ret = null;
+ } else {
+ ret = MusicUtils.query(this,
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursorCols,
+ where.toString(), keywords, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+ }
+ } else if (mPlaylist.equals("recentlyadded")) {
+ // do a query for all songs added in the last X weeks
+ int X = MusicUtils.getIntPref(this, "numweeks", 2) * (3600 * 24 * 7);
+ where.append(" AND " + MediaStore.MediaColumns.DATE_ADDED + ">");
+ where.append(System.currentTimeMillis() / 1000 - X);
+ if (async != null) {
+ async.startQuery(0, null,
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursorCols,
+ where.toString(), keywords, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+ ret = null;
+ } else {
+ ret = MusicUtils.query(this,
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCursorCols,
+ where.toString(), keywords, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+ }
+ } else {
+ mSortOrder = MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER;
+ if (async != null) {
+ async.startQuery(0, null,
+ MediaStore.Audio.Playlists.Members.getContentUri("external", Long.valueOf(mPlaylist)),
+ mPlaylistMemberCols, where.toString(), keywords, mSortOrder);
+ ret = null;
+ } else {
+ 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;
+ }
+ if (mArtistId != null) {
+ where.append(" AND " + MediaStore.Audio.Media.ARTIST_ID + "=" + mArtistId);
+ }
+ where.append(" AND " + MediaStore.Audio.Media.IS_MUSIC + "=1");
+ if (async != null) {
+ async.startQuery(0, null,
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ mCursorCols, where.toString() , keywords, mSortOrder);
+ ret = null;
+ } else {
+ ret = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ mCursorCols, where.toString() , keywords, mSortOrder);
+ }
+ }
+
+ // This special case is for the "nowplaying" cursor, which cannot be handled
+ // asynchronously using AsyncQueryHandler, so we do some extra initialization here.
+ if (ret != null && async != null) {
+ init(ret);
+ setTitle();
+ }
+ 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.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
+ for (int i = 0; i < size; i++) {
+ mCursorIdxs[i] = mCurrentPlaylistCursor.getInt(colidx);
+ mCurrentPlaylistCursor.moveToNext();
+ }
+ mCurrentPlaylistCursor.moveToFirst();
+ mCurPos = -1;
+
+ // At this point we can verify the 'now playing' list we got
+ // earlier to make sure that all the items in there still exist
+ // in the database, and remove those that aren't. This way we
+ // don't get any blank items in the list.
+ try {
+ int removed = 0;
+ for (int i = mNowPlaying.length - 1; i >= 0; i--) {
+ int trackid = mNowPlaying[i];
+ int crsridx = Arrays.binarySearch(mCursorIdxs, trackid);
+ if (crsridx < 0) {
+ //Log.i("@@@@@", "item no longer exists in db: " + trackid);
+ removed += mService.removeTrack(trackid);
+ }
+ }
+ if (removed > 0) {
+ mNowPlaying = mService.getQueue();
+ mSize = mNowPlaying.length;
+ if (mSize == 0) {
+ mCursorIdxs = null;
+ return;
+ }
+ }
+ } catch (RemoteException ex) {
+ mNowPlaying = new int[0];
+ }
+ }
+
+ @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;
+ }
+
+ public boolean removeItem(int which)
+ {
+ try {
+ if (mService.removeTracks(which, which) == 0) {
+ return false; // delete failed
+ }
+ int i = (int) which;
+ 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)
+ {
+ try {
+ return mCurrentPlaylistCursor.getLong(column);
+ } catch (Exception ex) {
+ onChange(true);
+ return 0;
+ }
+ }
+
+ @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;
+ }
+
+ static class TrackListAdapter extends SimpleCursorAdapter implements SectionIndexer {
+ boolean mIsNowPlaying;
+ boolean mDisableNowPlayingIndicator;
+
+ int mTitleIdx;
+ int mArtistIdx;
+ int mAlbumIdx;
+ int mDurationIdx;
+ int mAudioIdIdx;
+
+ private final StringBuilder mBuilder = new StringBuilder();
+ private final String mUnknownArtist;
+ private final String mUnknownAlbum;
+
+ private AlphabetIndexer mIndexer;
+
+ private TrackBrowserActivity mActivity = null;
+ private AsyncQueryHandler mQueryHandler;
+ private String mConstraint = null;
+ private boolean mConstraintIsValid = false;
+
+ class ViewHolder {
+ TextView line1;
+ TextView line2;
+ TextView duration;
+ ImageView play_indicator;
+ CharArrayBuffer buffer1;
+ char [] buffer2;
+ }
+
+ class QueryHandler extends AsyncQueryHandler {
+ QueryHandler(ContentResolver res) {
+ super(res);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ //Log.i("@@@", "query complete: " + cursor.getCount() + " " + mActivity);
+ mActivity.init(cursor);
+ }
+ }
+
+ TrackListAdapter(Context context, TrackBrowserActivity currentactivity,
+ int layout, Cursor cursor, String[] from, int[] to,
+ boolean isnowplaying, boolean disablenowplayingindicator) {
+ super(context, layout, cursor, from, to);
+ mActivity = currentactivity;
+ getColumnIndices(cursor);
+ mIsNowPlaying = isnowplaying;
+ mDisableNowPlayingIndicator = disablenowplayingindicator;
+ mUnknownArtist = context.getString(R.string.unknown_artist_name);
+ mUnknownAlbum = context.getString(R.string.unknown_album_name);
+
+ mQueryHandler = new QueryHandler(context.getContentResolver());
+ }
+
+ public void setActivity(TrackBrowserActivity newactivity) {
+ mActivity = newactivity;
+ }
+
+ public AsyncQueryHandler getQueryHandler() {
+ return mQueryHandler;
+ }
+
+ private void getColumnIndices(Cursor cursor) {
+ if (cursor != null) {
+ mTitleIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE);
+ mArtistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST);
+ mAlbumIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM);
+ mDurationIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION);
+ try {
+ mAudioIdIdx = cursor.getColumnIndexOrThrow(
+ MediaStore.Audio.Playlists.Members.AUDIO_ID);
+ } catch (IllegalArgumentException ex) {
+ mAudioIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
+ }
+
+ if (mIndexer != null) {
+ mIndexer.setCursor(cursor);
+ } else if (!mActivity.mEditMode) {
+ String alpha = mActivity.getString(
+ com.android.internal.R.string.fast_scroll_alphabet);
+
+ mIndexer = new MusicAlphabetIndexer(cursor, mTitleIdx, alpha);
+ }
+ }
+ }
+
+ @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 (mActivity.mEditMode) {
+ iv.setVisibility(View.VISIBLE);
+ iv.setImageResource(R.drawable.ic_mp_move);
+ } 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(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) {
+ }
+ }
+
+ // Determining whether and where to show the "now playing indicator
+ // is tricky, because we don't actually keep track of where the songs
+ // in the current playlist came from after they've started playing.
+ //
+ // If the "current playlists" is shown, then we can simply match by position,
+ // otherwise, we need to match by id. Match-by-id gets a little weird if
+ // a song appears in a playlist more than once, and you're in edit-playlist
+ // mode. In that case, both items will have the "now playing" indicator.
+ // For this reason, we don't show the play indicator at all when in edit
+ // playlist mode (except when you're viewing the "current playlist",
+ // which is not really a playlist)
+ if ( (mIsNowPlaying && cursor.getPosition() == id) ||
+ (!mIsNowPlaying && !mDisableNowPlayingIndicator && 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) {
+ if (cursor != mActivity.mTrackCursor) {
+ mActivity.mTrackCursor = cursor;
+ super.changeCursor(cursor);
+ getColumnIndices(cursor);
+ }
+ }
+
+ @Override
+ public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+ String s = constraint.toString();
+ if (mConstraintIsValid && (
+ (s == null && mConstraint == null) ||
+ (s != null && s.equals(mConstraint)))) {
+ return getCursor();
+ }
+ Cursor c = mActivity.getTrackCursor(null, s);
+ mConstraint = s;
+ mConstraintIsValid = true;
+ return c;
+ }
+
+ // SectionIndexer methods
+
+ public Object[] getSections() {
+ if (mIndexer != null) {
+ return mIndexer.getSections();
+ } else {
+ return null;
+ }
+ }
+
+ public int getPositionForSection(int section) {
+ int pos = mIndexer.getPositionForSection(section);
+ return pos;
+ }
+
+ public int getSectionForPosition(int position) {
+ return 0;
+ }
+ }
+}
+
diff --git a/src/com/android/music/VideoBrowserActivity.java b/src/com/android/music/VideoBrowserActivity.java
new file mode 100644
index 0000000..e8aaf74
--- /dev/null
+++ b/src/com/android/music/VideoBrowserActivity.java
@@ -0,0 +1,112 @@
+/*
+ * 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);
+
+ 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.getColumnIndexOrThrow(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 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
index 0000000..9fe5bdc
--- /dev/null
+++ b/src/com/android/music/WeekSelector.java
@@ -0,0 +1,90 @@
+/*
+ * 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);
+
+ ((Button) findViewById(R.id.cancel)).setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ });
+ }
+
+ @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();
+ }
+ };
+}
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..589842e
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,16 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := MusicTests
+
+LOCAL_INSTRUMENTATION_FOR := Music
+
+include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..b8a6fe7
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.music.tests">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name=".MusicPlayerLaunchPerformance"
+ android:targetPackage="com.android.music"
+ android:label="Music Launch Performance">
+ </instrumentation>
+
+ <instrumentation android:name=".MusicPlayerFunctionalTestRunner"
+ android:targetPackage="com.android.music"
+ android:label="Music Player Functional Test">
+ </instrumentation>
+
+ <instrumentation android:name=".MusicPlayerStressTestRunner"
+ android:targetPackage="com.android.music"
+ android:label="Music Player Stress Test">
+ </instrumentation>
+
+</manifest>
diff --git a/tests/src/com/android/music/MusicPlayerFunctionalTestRunner.java b/tests/src/com/android/music/MusicPlayerFunctionalTestRunner.java
new file mode 100644
index 0000000..7801bf1
--- /dev/null
+++ b/tests/src/com/android/music/MusicPlayerFunctionalTestRunner.java
@@ -0,0 +1,56 @@
+/*
+ * 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.tests;
+
+import android.test.InstrumentationTestRunner;
+import android.test.InstrumentationTestSuite;
+import com.android.music.tests.functional.TestSongs;
+import com.android.music.tests.functional.TestPlaylist;
+
+import junit.framework.TestSuite;
+
+
+/**
+ * Instrumentation Test Runner for all Music Player tests.
+ *
+ * Precondition: Opened keyboard and wipe the userdata
+ *
+ * Running all tests:
+ *
+ * adb shell am instrument \
+ * -w com.android.music.tests/.MusicPlayerFunctionalTestRunner
+ */
+
+public class MusicPlayerFunctionalTestRunner extends InstrumentationTestRunner {
+
+
+ @Override
+ public TestSuite getAllTests() {
+ TestSuite suite = new InstrumentationTestSuite(this);
+ suite.addTestSuite(TestSongs.class);
+ suite.addTestSuite(TestPlaylist.class);
+ return suite;
+ }
+
+ @Override
+ public ClassLoader getLoader() {
+ return MusicPlayerFunctionalTestRunner.class.getClassLoader();
+ }
+}
+
+
+
diff --git a/tests/src/com/android/music/MusicPlayerLaunchPerformance.java b/tests/src/com/android/music/MusicPlayerLaunchPerformance.java
new file mode 100644
index 0000000..80342ba
--- /dev/null
+++ b/tests/src/com/android/music/MusicPlayerLaunchPerformance.java
@@ -0,0 +1,53 @@
+/*
+ * 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.tests;
+
+import android.app.Activity;
+import android.test.LaunchPerformanceBase;
+import android.os.Bundle;
+
+import java.util.Map;
+
+/**
+ * Instrumentation class for Media Player launch performance testing.
+ */
+public class MusicPlayerLaunchPerformance extends LaunchPerformanceBase {
+
+ public static final String LOG_TAG = "MusicPlayerLaunchPerformance";
+
+ public MusicPlayerLaunchPerformance() {
+ super();
+ }
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+
+ mIntent.setClassName(getTargetContext(), "com.android.music.MusicBrowserActivity");
+ start();
+ }
+
+ /**
+ * Calls LaunchApp and finish.
+ */
+ @Override
+ public void onStart() {
+ super.onStart();
+ LaunchApp();
+ finish(Activity.RESULT_OK, mResults);
+ }
+}
diff --git a/tests/src/com/android/music/MusicPlayerNames.java b/tests/src/com/android/music/MusicPlayerNames.java
new file mode 100644
index 0000000..f5f5d48
--- /dev/null
+++ b/tests/src/com/android/music/MusicPlayerNames.java
@@ -0,0 +1,61 @@
+/*
+ * 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.tests;
+
+/**
+ *
+ * This class has the names of the all the activity name and variables
+ * in the instrumentation test.
+ *
+ */
+public class MusicPlayerNames {
+
+ //Expected result of the sorted playlistname
+ public static final String expectedPlaylistTitle[] = { "**1E?:|}{[]~~.,;'",
+ "//><..", "0123456789",
+ "0random@112", "MyPlaylist", "UPPERLETTER",
+ "combination011", "loooooooog",
+ "normal", "~!@#$%^&*()_+"
+ };
+
+ //Unsorted input playlist name
+ public static final String unsortedPlaylistTitle[] = { "//><..","MyPlaylist",
+ "0random@112", "UPPERLETTER","normal",
+ "combination011", "0123456789",
+ "~!@#$%^&*()_+","**1E?:|}{[]~~.,;'",
+ "loooooooog"
+ };
+
+ public static final String DELETE_PLAYLIST_NAME = "testDeletPlaylist";
+ public static final String ORIGINAL_PLAYLIST_NAME = "original_playlist_name";
+ public static final String RENAMED_PLAYLIST_NAME = "rename_playlist_name";
+
+ public static int NO_OF_PLAYLIST = 10;
+ public static int WAIT_SHORT_TIME = 1000;
+ public static int WAIT_LONG_TIME = 2000;
+ public static int WAIT_VERY_LONG_TIME = 6000;
+ public static int SKIP_WAIT_TIME = 500;
+ public static int DEFAULT_PLAYLIST_LENGTH = 15;
+ public static int NO_ALBUMS_TOBE_PLAYED = 50;
+ public static int NO_SKIPPING_SONGS = 500;
+
+ public static final String DELETESONG = "/sdcard/toBeDeleted.amr";
+ public static final String GOLDENSONG = "/sdcard/media_api/music/AMRNB.amr";
+ public static final String TOBEDELETESONGNAME = "toBeDeleted";
+
+ public static int EXPECTED_NO_RINGTONE = 1;
+}
diff --git a/tests/src/com/android/music/MusicPlayerStressTestRunner.java b/tests/src/com/android/music/MusicPlayerStressTestRunner.java
new file mode 100644
index 0000000..269627d
--- /dev/null
+++ b/tests/src/com/android/music/MusicPlayerStressTestRunner.java
@@ -0,0 +1,51 @@
+/*
+ * 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.tests;
+
+import com.android.music.tests.stress.AlbumsPlaybackStress;
+import com.android.music.tests.stress.MusicPlaybackStress;
+
+import android.test.InstrumentationTestRunner;
+import android.test.InstrumentationTestSuite;
+
+import junit.framework.TestSuite;
+
+/**
+ * Instrumentation Test Runner for all music player stress tests.
+ *
+ * Running all tests:
+ *
+ * adb shell am instrument \
+ * -w com.android.music.tests/.MusicPlayerStressTestRunner
+ */
+
+public class MusicPlayerStressTestRunner extends InstrumentationTestRunner {
+
+ @Override
+ public TestSuite getAllTests() {
+ TestSuite suite = new InstrumentationTestSuite(this);
+ //suite.addTestSuite(MusicPlaybackStress.class);
+ suite.addTestSuite(AlbumsPlaybackStress.class);
+ return suite;
+ }
+
+ @Override
+ public ClassLoader getLoader() {
+ return MusicPlayerStressTestRunner.class.getClassLoader();
+ }
+}
+
diff --git a/tests/src/com/android/music/functional/TestPlaylist.java b/tests/src/com/android/music/functional/TestPlaylist.java
new file mode 100644
index 0000000..64432d7
--- /dev/null
+++ b/tests/src/com/android/music/functional/TestPlaylist.java
@@ -0,0 +1,174 @@
+/*
+ * 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.tests.functional;
+
+import android.app.Activity;
+import android.content.*;
+import android.app.Instrumentation;
+import android.app.Instrumentation.ActivityMonitor;
+import android.content.Intent;
+import android.test.ActivityInstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.KeyEvent;
+import android.provider.MediaStore;
+import android.content.ContentResolver;
+import android.database.Cursor;
+
+import com.android.music.CreatePlaylist;
+import com.android.music.MusicUtils;
+import com.android.music.PlaylistBrowserActivity;
+import com.android.music.TrackBrowserActivity;
+
+import com.android.music.tests.MusicPlayerNames;
+import com.android.music.tests.functional.TestSongs;
+
+/**
+ * Junit / Instrumentation test case for the PlaylistBrowserActivity
+ * This test case need to run in the landscape mode and opened keyboard
+
+ */
+public class TestPlaylist extends ActivityInstrumentationTestCase <PlaylistBrowserActivity>{
+ private static String TAG = "musicplayertests";
+
+ public TestPlaylist() {
+ super("com.android.music",PlaylistBrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+
+ private void clearSearchString(int length){
+ Instrumentation inst = getInstrumentation();
+ for (int j=0; j< length; j++)
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DEL);
+ }
+ /**
+ * Remove playlist
+ */
+ public void deletePlaylist(String playlistname) throws Exception{
+ Instrumentation inst = getInstrumentation();
+ inst.sendStringSync(playlistname);
+ Thread.sleep(MusicPlayerNames.WAIT_SHORT_TIME);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN);
+ inst.invokeContextMenuAction(getActivity(), MusicUtils.Defs.CHILD_MENU_BASE + 1, 0);
+ Thread.sleep(MusicPlayerNames.WAIT_SHORT_TIME);
+ clearSearchString(playlistname.length());
+
+ }
+
+ /**
+ * Start the trackBrowserActivity and add the new playlist
+ */
+ public void addNewPlaylist(String playListName) throws Exception{
+ Instrumentation inst = getInstrumentation();
+ Activity trackBrowserActivity;
+ ActivityMonitor trackBrowserMon = inst.addMonitor("com.android.music.TrackBrowserActivity",
+ null, false);
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_PICK);
+ intent.setClassName("com.android.music", "com.android.music.TrackBrowserActivity");
+ getActivity().startActivity(intent);
+ Thread.sleep(MusicPlayerNames.WAIT_LONG_TIME);
+ trackBrowserActivity = trackBrowserMon.waitForActivityWithTimeout(2000);
+ inst.invokeContextMenuAction(trackBrowserActivity, MusicUtils.Defs.NEW_PLAYLIST, 0);
+ Thread.sleep(MusicPlayerNames.WAIT_SHORT_TIME);
+ //Remove the default playlist name
+ clearSearchString(MusicPlayerNames.DEFAULT_PLAYLIST_LENGTH);
+ inst.sendStringSync(playListName);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(MusicPlayerNames.WAIT_LONG_TIME);
+ trackBrowserActivity.finish();
+ clearSearchString(playListName.length());
+
+ }
+
+ /**
+ * Rename playlist
+ */
+ public void renamePlaylist(String oldPlaylistName, String newPlaylistName) throws Exception{
+ Instrumentation inst = getInstrumentation();
+ inst.sendStringSync(oldPlaylistName);
+ Thread.sleep(MusicPlayerNames.WAIT_SHORT_TIME);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN);
+ inst.invokeContextMenuAction(getActivity(), MusicUtils.Defs.CHILD_MENU_BASE + 3, 0);
+ Thread.sleep(MusicPlayerNames.WAIT_SHORT_TIME);
+ //Remove the old playlist name
+ clearSearchString(oldPlaylistName.length());
+ inst.sendStringSync(newPlaylistName);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(MusicPlayerNames.WAIT_LONG_TIME);
+ clearSearchString(oldPlaylistName.length());
+ }
+
+ public boolean verifyPlaylist(String playlistname) throws Exception{
+ Cursor mCursor;
+ boolean isEmptyPlaylist = true;
+ String[] cols = new String[] {
+ MediaStore.Audio.Playlists.NAME
+ };
+ ContentResolver resolver = getActivity().getContentResolver();
+ if (resolver == null) {
+ System.out.println("resolver = null");
+ assertNull(TAG, resolver);
+ } else {
+ String whereclause = MediaStore.Audio.Playlists.NAME + " = '" + playlistname +"'";
+ mCursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+ cols, whereclause, null,
+ MediaStore.Audio.Playlists.NAME);
+ isEmptyPlaylist = mCursor.moveToFirst();
+ }
+ return isEmptyPlaylist;
+ }
+
+ /**
+ * Test case 1: Add a playlist and delet the playlist just added.
+ * Verification: The mediastore playlist should be empty
+ */
+ @LargeTest
+ public void testDeletePlaylist() throws Exception{
+ boolean isEmptyPlaylist = true;
+ addNewPlaylist(MusicPlayerNames.DELETE_PLAYLIST_NAME);
+ deletePlaylist(MusicPlayerNames.DELETE_PLAYLIST_NAME);
+ isEmptyPlaylist = verifyPlaylist(MusicPlayerNames.DELETE_PLAYLIST_NAME);
+ assertFalse("testDeletePlaylist", isEmptyPlaylist);
+ }
+
+ /**
+ * Test case 2: Add playlist and rename the playlist just added.
+ * Verification: The mediastore playlist should contain the updated name.
+ */
+ @LargeTest
+ public void testRenamePlaylist() throws Exception{
+ boolean isEmptyPlaylist = true;
+ addNewPlaylist(MusicPlayerNames.ORIGINAL_PLAYLIST_NAME);
+ renamePlaylist(MusicPlayerNames.ORIGINAL_PLAYLIST_NAME, MusicPlayerNames.RENAMED_PLAYLIST_NAME);
+ isEmptyPlaylist = verifyPlaylist(MusicPlayerNames.RENAMED_PLAYLIST_NAME);
+ deletePlaylist(MusicPlayerNames.RENAMED_PLAYLIST_NAME);
+ assertTrue("testDeletePlaylist", isEmptyPlaylist);
+ }
+
+}
diff --git a/tests/src/com/android/music/functional/TestSongs.java b/tests/src/com/android/music/functional/TestSongs.java
new file mode 100644
index 0000000..488d691
--- /dev/null
+++ b/tests/src/com/android/music/functional/TestSongs.java
@@ -0,0 +1,218 @@
+/*
+ * 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.tests.functional;
+
+import android.app.Activity;
+import android.content.*;
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.test.ActivityInstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.content.ContentResolver;
+import android.content.pm.ActivityInfo;
+import android.database.Cursor;
+import android.content.Intent;
+import android.content.BroadcastReceiver;
+import android.content.IntentFilter;
+
+import com.android.music.CreatePlaylist;
+import com.android.music.TrackBrowserActivity;
+import com.android.music.MusicUtils;
+
+import com.android.music.tests.MusicPlayerNames;
+
+import java.io.*;
+
+/**
+ * Junit / Instrumentation test case for the TrackBrowserActivity
+
+ */
+public class TestSongs extends ActivityInstrumentationTestCase <TrackBrowserActivity>{
+ private static String TAG = "musicplayertests";
+
+ public TestSongs() {
+ super("com.android.music",TrackBrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * Add 10 new playlists with unsorted title order
+ */
+ public void addNewPlaylist() throws Exception{
+ Instrumentation inst = getInstrumentation();
+ for (int i=0; i< MusicPlayerNames.NO_OF_PLAYLIST; i++){
+ inst.invokeContextMenuAction(getActivity(), MusicUtils.Defs.NEW_PLAYLIST, 0);
+ Thread.sleep(MusicPlayerNames.WAIT_SHORT_TIME);
+ //Remove the default playlist name
+ for (int j=0; j< MusicPlayerNames.DEFAULT_PLAYLIST_LENGTH; j++)
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DEL);
+ inst.sendStringSync(MusicPlayerNames.unsortedPlaylistTitle[i]);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(MusicPlayerNames.WAIT_LONG_TIME);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN);
+ Thread.sleep(MusicPlayerNames.WAIT_LONG_TIME);
+ }
+ }
+
+ private void copy(File src, File dst) throws IOException {
+ InputStream in = new FileInputStream(src);
+ OutputStream out = new FileOutputStream(dst);
+
+ // Transfer bytes from in to out
+ byte[] buf = new byte[1024];
+ int len;
+ while ((len = in.read(buf)) > 0) {
+ out.write(buf, 0, len);
+ }
+ in.close();
+ out.close();
+ Log.v(TAG, "Copy file");
+ }
+
+ //Rescan the sdcard after copy the file
+ private void rescanSdcard() throws Exception{
+ Intent scanIntent = new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"
+ + Environment.getExternalStorageDirectory()));
+ Log.v(TAG,"start the intent");
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_SCANNER_STARTED);
+ intentFilter.addDataScheme("file");
+ getActivity().sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"
+ + Environment.getExternalStorageDirectory())));
+ Thread.sleep(MusicPlayerNames.WAIT_VERY_LONG_TIME);
+ }
+
+
+ /**
+ * Test case 1: tests the new playlist added with sorted order.
+ * Verification: The new playlist title should be sorted in alphabetical order
+ */
+ @LargeTest
+ public void testAddPlaylist() throws Exception{
+ Cursor mCursor;
+ addNewPlaylist();
+
+ //Verify the new playlist is created, check the playlist table
+ String[] cols = new String[] {
+ MediaStore.Audio.Playlists.NAME
+ };
+ ContentResolver resolver = getActivity().getContentResolver();
+ if (resolver == null) {
+ System.out.println("resolver = null");
+ } else {
+ String whereclause = MediaStore.Audio.Playlists.NAME + " != ''";
+ mCursor = resolver.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
+ cols, whereclause, null,
+ MediaStore.Audio.Playlists.NAME);
+ //Check the new playlist
+ mCursor.moveToFirst();
+
+ for (int j=0;j<10;j++){
+ assertEquals("New sorted Playlist title:", MusicPlayerNames.expectedPlaylistTitle[j], mCursor.getString(0));
+ mCursor.moveToNext();
+ }
+ }
+ }
+
+ /**
+ * Test case 2: Set a song as ringtone
+ * Test case precondition: The testing device should wipe data before
+ * run the test case.
+ * Verification: The count of audio.media.is_ringtone equal to 1.
+ */
+ @LargeTest
+ public void testSetRingtone() throws Exception{
+ Cursor mCursor;
+ Instrumentation inst = getInstrumentation();
+ inst.invokeContextMenuAction(getActivity(), MusicUtils.Defs.USE_AS_RINGTONE, 0);
+ //This only check if there only 1 ringtone set in music player
+ ContentResolver resolver = getActivity().getContentResolver();
+ if (resolver == null) {
+ System.out.println("resolver = null");
+ } else {
+ String whereclause = MediaStore.Audio.Media.IS_RINGTONE + " = 1";
+ mCursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ null, whereclause, null, null);
+ //Check the new playlist
+ mCursor.moveToFirst();
+ int isRingtoneSet = mCursor.getCount();
+ assertEquals(TAG, MusicPlayerNames.EXPECTED_NO_RINGTONE, isRingtoneSet);
+ }
+ }
+
+ /**
+ * Test case 3: Delete a song
+ * Test case precondition: Copy a song and rescan the sdcard
+ * Verification: The song is deleted from the sdcard and mediastore
+ */
+ @LargeTest
+ public void testDeleteSong() throws Exception{
+ Instrumentation inst = getInstrumentation();
+ Cursor mCursor;
+
+ //Copy a song from the golden directory
+ Log.v(TAG, "Copy a temp file to the sdcard");
+ File goldenfile = new File(MusicPlayerNames.GOLDENSONG);
+ File toBeDeleteSong = new File(MusicPlayerNames.DELETESONG);
+ copy(goldenfile, toBeDeleteSong);
+ rescanSdcard();
+
+ //Delete the file from music player
+ Thread.sleep(MusicPlayerNames.WAIT_LONG_TIME);
+ inst.sendStringSync(MusicPlayerNames.TOBEDELETESONGNAME);
+ Thread.sleep(MusicPlayerNames.WAIT_LONG_TIME);
+ inst.invokeContextMenuAction(getActivity(), MusicUtils.Defs.DELETE_ITEM, 0);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(MusicPlayerNames.WAIT_LONG_TIME);
+
+ //Clear the search string
+ for (int j=0; j< MusicPlayerNames.TOBEDELETESONGNAME.length(); j++)
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DEL);
+
+ //Verfiy the item is removed from sdcard
+ File checkDeletedFile = new File(MusicPlayerNames.DELETESONG);
+ assertFalse(TAG, checkDeletedFile.exists());
+
+ ContentResolver resolver = getActivity().getContentResolver();
+ if (resolver == null) {
+ System.out.println("resolver = null");
+ } else {
+ String whereclause = MediaStore.Audio.Media.DISPLAY_NAME + " = '" +
+ MusicPlayerNames.TOBEDELETESONGNAME + "'";
+ mCursor = resolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ null, whereclause, null, null);
+ boolean isEmptyCursor = mCursor.moveToFirst();
+ assertFalse(TAG,isEmptyCursor);
+ }
+ }
+}
+
diff --git a/tests/src/com/android/music/stress/AlbumsPlaybackStress.java b/tests/src/com/android/music/stress/AlbumsPlaybackStress.java
new file mode 100644
index 0000000..f396317
--- /dev/null
+++ b/tests/src/com/android/music/stress/AlbumsPlaybackStress.java
@@ -0,0 +1,91 @@
+/*
+ * 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.tests.stress;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.Instrumentation;
+import android.app.Instrumentation.ActivityMonitor;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.test.ActivityInstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.KeyEvent;
+import android.util.Log;
+
+import com.android.music.AlbumBrowserActivity;
+import com.android.music.tests.MusicPlayerNames;
+
+public class AlbumsPlaybackStress extends ActivityInstrumentationTestCase <AlbumBrowserActivity>{
+
+ private Activity browseActivity;
+ private String[] testing;
+ private String TAG = "AlbumsPlaybackStress";
+
+ public AlbumsPlaybackStress() {
+ super("com.android.music",AlbumBrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /*
+ * Test case: Keeps launching music playback from Albums and then go
+ * back to the album screen
+ * Verification: Check if it is in low memory
+ * The test depends on the test media in the sdcard
+ */
+ @LargeTest
+ public void testAlbumPlay() {
+ Instrumentation inst = getInstrumentation();
+ try{
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_RIGHT);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_RIGHT);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(MusicPlayerNames.WAIT_LONG_TIME);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
+ for(int i=0; i< MusicPlayerNames.NO_ALBUMS_TOBE_PLAYED; i++){
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(MusicPlayerNames.WAIT_LONG_TIME);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
+ }
+ }catch (Exception e){
+ Log.v(TAG, e.toString());
+ }
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
+
+ //Verification: check if it is in low memory
+ ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
+ ((ActivityManager)getActivity().getSystemService("activity")).getMemoryInfo(mi);
+ assertFalse(TAG, mi.lowMemory);
+
+
+ }
+}
diff --git a/tests/src/com/android/music/stress/MusicPlaybackStress.java b/tests/src/com/android/music/stress/MusicPlaybackStress.java
new file mode 100644
index 0000000..80661f2
--- /dev/null
+++ b/tests/src/com/android/music/stress/MusicPlaybackStress.java
@@ -0,0 +1,87 @@
+/*
+ * 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.tests.stress;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.Instrumentation;
+import android.app.Instrumentation.ActivityMonitor;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.test.ActivityInstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.KeyEvent;
+import android.util.Log;
+import android.content.Context;
+import android.os.ServiceManager;
+
+
+import com.android.music.MusicBrowserActivity;
+import com.android.music.MusicUtils;
+import com.android.music.TrackBrowserActivity;
+import com.android.music.tests.MusicPlayerNames;
+
+public class MusicPlaybackStress extends ActivityInstrumentationTestCase <TrackBrowserActivity>{
+ private static String TAG = "mediaplayertests";
+
+ public MusicPlaybackStress() {
+ super("com.android.music",TrackBrowserActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @LargeTest
+ public void testPlayAllSongs() {
+ Activity mediaPlaybackActivity;
+ try{
+ Instrumentation inst = getInstrumentation();
+ ActivityMonitor mediaPlaybackMon = inst.addMonitor("com.android.music.MediaPlaybackActivity",
+ null, false);
+ inst.invokeMenuActionSync(getActivity(), MusicUtils.Defs.CHILD_MENU_BASE + 3, 0);
+ Thread.sleep(MusicPlayerNames.WAIT_LONG_TIME);
+ mediaPlaybackActivity = mediaPlaybackMon.waitForActivityWithTimeout(2000);
+ for (int i=0;i< MusicPlayerNames.NO_SKIPPING_SONGS;i++){
+ Thread.sleep(MusicPlayerNames.SKIP_WAIT_TIME);
+ if (i==0){
+ //Set the repeat all
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_RIGHT);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_UP);
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+
+ //Set focus on the next button
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN);
+ }
+ inst.sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_CENTER);
+ }
+ mediaPlaybackActivity.finish();
+ }catch (Exception e){
+ Log.e(TAG, e.toString());
+ }
+ //Verification: check if it is in low memory
+ ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
+ ((ActivityManager)getActivity().getSystemService("activity")).getMemoryInfo(mi);
+ assertFalse(TAG, mi.lowMemory);
+ }
+}