Merge branch 'master' into feature/static_analysis_fixes

This commit is contained in:
Xiao Bao Clark 2015-08-22 07:55:43 +10:00
commit 45879c571a
40 changed files with 1507 additions and 690 deletions

View File

@ -23,8 +23,8 @@ THE SOFTWARE.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="ch.blinkenlights.android.vanilla"
android:versionName="1.0.20"
android:versionCode="1020"
android:versionName="1.0.31"
android:versionCode="1031"
android:installLocation="auto">
<uses-sdk android:minSdkVersion="15" android:targetSdkVersion="21" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

View File

@ -20,12 +20,17 @@ function show(n) {
</head>
<body>
<h1>Vanilla Music</h1>
<p><b>Version:</b> 1.0.20 released Jun. 10, 2015<br><br>
<p><b>Version:</b> 1.0.31 Released Aug. 20, 2015<br><br>
<b>Website:</b> <a href='https://github.com/vanilla-music/vanilla'>https://github.com/vanilla-music/vanilla</a><br>
<b>Issue tracker:</b> <a href='https://github.com/vanilla-music/vanilla/issues'>https://github.com/vanilla-music/vanilla/issues</a><br>
<br>
<b>Changelog</b> [<a href='javascript:show("changelog")'>show</a>]
<pre id='changelog'>
<b>1.0.30</b>
- <b>NEW</b> Display album artwork in listview
- <b>NEW</b> Fling-Remove of playlist items
- <b>FIX</b> Fixed playlist reordering bug
<b>1.0.20</b>
- <b>NEW</b> Support for heads-up-display notifications
- <b>NEW</b> Added 'skip 10 seconds' longpress action

View File

@ -1,72 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2014 Adrian Ulrich <adrian@blinkenlights.ch>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<ch.blinkenlights.android.vanilla.DraggableRow
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<LinearLayout
android:background="?android:attr/selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<View
android:id="@+id/pmark"
android:layout_width="4dip"
android:layout_height="44dip"
android:visibility="invisible"
android:background="@color/vanillaAccent" />
<TextView
android:id="@+id/text"
android:maxLines="2"
android:textColor="?android:textColorPrimary"
android:gravity="left|center_vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingTop="2dip"
android:paddingBottom="2dip"
android:paddingLeft="4dip" />
<!-- spacer -->
<View
android:layout_height="match_parent"
android:layout_width="0px"
android:layout_weight="1"/>
<CheckedTextView
android:id="@+id/checkbox"
android:visibility="gone"
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center_vertical"
android:layout_height="match_parent"
android:layout_width="44dip"/>
<ImageView
android:id="@+id/dragger"
android:scaleType="center"
android:src="@drawable/grabber"
android:layout_width="44dip"
android:layout_height="44dip" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="?divider_color"/>
</ch.blinkenlights.android.vanilla.DraggableRow>

View File

@ -30,38 +30,47 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<View
android:id="@+id/pmark"
android:visibility="gone"
android:layout_width="4dip"
android:layout_height="44dip"
android:visibility="invisible"
android:background="@android:color/holo_blue_dark" />
android:background="@color/now_playing_marker" />
<ch.blinkenlights.android.vanilla.LazyCoverView
android:id="@+id/cover"
android:visibility="gone"
android:background="?android:attr/selectableItemBackground"
android:longClickable="true"
android:scaleType="fitCenter"
android:layout_width="44dip"
android:layout_height="44dip" />
<TextView
android:id="@+id/text"
android:maxLines="2"
android:textColor="?android:textColorPrimary"
android:gravity="left|center_vertical"
android:layout_width="wrap_content"
android:layout_width="0px"
android:layout_weight="1"
android:layout_height="match_parent"
android:paddingTop="2dip"
android:paddingBottom="2dip"
android:paddingLeft="4dip" />
<!-- spacer -->
<View
android:layout_height="match_parent"
android:layout_width="0px"
android:layout_weight="1"/>
<CheckedTextView
android:id="@+id/checkbox"
android:visibility="gone"
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center_vertical"
android:layout_height="match_parent"
android:layout_width="44dip"/>
<ImageView
android:id="@+id/dragger"
android:scaleType="center"
android:src="@drawable/grabber"
android:layout_width="44dip"
android:layout_height="44dip" />
<CheckedTextView
android:id="@+id/checkbox"
android:visibility="gone"
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center_vertical"
android:layout_height="match_parent"
android:layout_width="44dip"/>
<ImageView
android:id="@+id/dragger"
android:visibility="gone"
android:scaleType="center"
android:src="@drawable/grabber"
android:layout_width="44dip"
android:layout_height="44dip" />
</LinearLayout>
<View

View File

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2012 Christopher Eby <kreed@kreed.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:maxLines="2"
android:textColor="?android:textColorPrimary"
android:gravity="left|center_vertical"
android:padding="3dip"
android:minHeight="44dip" />

View File

@ -25,6 +25,15 @@ THE SOFTWARE.
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ch.blinkenlights.android.vanilla.LazyCoverView
android:id="@+id/cover"
android:background="?android:attr/selectableItemBackground"
android:longClickable="true"
android:scaleType="fitCenter"
android:layout_width="44dip"
android:layout_height="44dip"
android:visibility="gone"
/>
<TextView
android:id="@+id/text"
android:longClickable="true"
@ -40,6 +49,7 @@ THE SOFTWARE.
android:id="@+id/divider"
android:layout_width="1dip"
android:layout_height="fill_parent"
android:visibility="gone"
android:background="?divider_color" />
<ImageView
android:id="@+id/arrow"
@ -49,5 +59,6 @@ THE SOFTWARE.
android:src="@drawable/arrow"
android:layout_width="44dip"
android:layout_height="44dip"
android:visibility="gone"
android:contentDescription="@string/expand" />
</LinearLayout>

View File

@ -26,4 +26,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
dslv:drag_enabled="false"
dslv:drag_start_mode="onMove"
dslv:float_background_color="?float_color"
dslv:remove_enabled="true"
dslv:remove_mode="flingRemove"
dslv:fling_handle_id="@+id/dragger"
dslv:drag_handle_id="@+id/dragger"/>

View File

@ -154,6 +154,8 @@
<string name="notification_action_title">Akce upozornění</string>
<string name="notification_invert_color_title">Převrátit barvu upozornění</string>
<string name="notification_invert_color_summary">Použít bílý text místo černého</string>
<string name="notification_nag">Velmi detailní upozornění</string>
<string name="notification_nag_summary">Oznamovat změny skladeb za použití \'Heads-Up-Notification\'</string>
<string name="playback_screen">Obrazovka přehrávání</string>
<string name="playback_on_startup_title">Otevřít při spuštění</string>
<string name="playback_on_startup_summary">Při spuštění otevřít náhled přehrávání</string>

View File

@ -3,34 +3,34 @@
<string name="app_name">Vanilla Music</string>
<!--Playback Activity (main screen)-->
<string name="no_songs">Keine Titel auf dem Gerät gefunden.</string>
<string name="empty_queue">Keine Titel ausgewählt. Wählen Sie Titel aus der Bibliothek oder starten Sie den Zufallsmodus durch Antippen dieser Nachricht.</string>
<string name="empty_queue">Keine Titel ausgewählt. Wählen Sie Titel aus der Bibliothek oder starten Sie die Zufallswiedergabe durch Antippen dieser Nachricht.</string>
<string name="settings">Einstellungen</string>
<string name="library">Bibliothek</string>
<string name="no_shuffle">Keine zufällige Wiedergabe</string>
<string name="shuffle_songs">Titel zufällig wiedergeben</string>
<string name="shuffle_songs_continuously">Titel kontinuierlich durchwürfeln</string>
<string name="shuffle_albums">Alben zufällig wiedergeben</string>
<string name="no_shuffle">Keine Zufallswiedergabe</string>
<string name="shuffle_songs">Zufallswiedergabe Titel</string>
<string name="shuffle_songs_continuously">Dauerzufallswiedergabe Titel</string>
<string name="shuffle_albums">Zufallswiedergabe Alben</string>
<string name="no_repeat">Nicht wiederholen</string>
<string name="repeat">Wiederholen</string>
<string name="repeat_current_song">Aktuellen Titel wiederholen</string>
<string name="stop_current_song">Nach aktuellem Titel stoppen</string>
<string name="random">Zufällige Wiedergabe</string>
<string name="random_enabling">Zufällige Wiedergabe aktiv</string>
<string name="song_load_failed">Konnte Titel %s nicht laden. Er ist möglicherweise beschädigt oder fehlt.</string>
<string name="queue_cleared">Wiedergabereihe geleert.</string>
<string name="random">Zufallswiedergabe</string>
<string name="random_enabling">Zufallswiedergabe aktiviert</string>
<string name="song_load_failed">Titel %s konnte nicht geladen werden. Er ist möglicherweise beschädigt oder fehlt.</string>
<string name="queue_cleared">Warteschlange geleert.</string>
<string name="cover_art">Cover</string>
<string name="close_notification">Schließen-Benachrichtigung</string>
<string name="close_notification">Benachrichtigung beim Schließen</string>
<string name="_genre">Genre</string>
<string name="_track">Titel</string>
<string name="_composer">Komponist</string>
<string name="_format">Format</string>
<string name="_title">Titel</string>
<string name="_artist">Künstler</string>
<string name="_artist">Interpret</string>
<string name="_album">Album</string>
<string name="_year">Jahr</string>
<string name="_replaygain">rgain</string>
<!--New Playlist Dialog-->
<string name="choose_playlist_name">Namen der Wiedergabeliste auswählen</string>
<string name="choose_playlist_name">Name der Wiedergabeliste auswählen</string>
<string name="create">Erstellen</string>
<string name="overwrite">Überschreiben</string>
<string name="cancel">Abbrechen</string>
@ -41,19 +41,19 @@
<string name="play">Abspielen</string>
<string name="edit">Bearbeiten</string>
<string name="rename">Umbenennen</string>
<string name="save_as_playlist">Als Wiedergabeliste speichern…</string>
<string name="add_to_playlist">Zur Wiedergabeliste hinzufügen…</string>
<string name="add_to_favorites">Zu Favoriten hinzufügen…</string>
<string name="save_as_playlist">Als Wiedergabeliste speichern </string>
<string name="add_to_playlist">Zur Wiedergabeliste hinzufügen </string>
<string name="add_to_favorites">Zu Favoriten hinzufügen </string>
<string name="playlist_favorites">Favoriten</string>
<string name="new_playlist">Neue Wiedergabeliste…</string>
<string name="new_playlist">Neue Wiedergabeliste </string>
<string name="expand">Aufklappen</string>
<string name="delete">Löschen</string>
<string name="playback_view">Aktueller Titel</string>
<string name="sort_by">Sortiere nach</string>
<string name="sort_by">Sortieren nach</string>
<string name="search">Suche</string>
<string name="done">Fertig</string>
<string name="remove">Entfernen</string>
<string name="clear_search">Suche entfernen</string>
<string name="clear_search">Suche leeren</string>
<string name="play_or_enqueue">Einreihen - oder abspielen falls pausiert</string>
<plurals name="playing">
<item quantity="one">1 Titel wird abgespielt.</item>
@ -72,9 +72,9 @@
<item quantity="other">%d Titel gelöscht.</item>
</plurals>
<string name="delete_file_failed">Löschen von %s fehlgeschlagen.</string>
<string name="deleted_item">\'%s\' wurde gelöscht.</string>
<string name="delete_item">\'%s\' löschen?</string>
<string name="delete_file">Datei \'%s\' löschen?</string>
<string name="deleted_item">\"%s\" wurde gelöscht.</string>
<string name="delete_item">\"%s\" löschen?</string>
<string name="delete_file">Datei \"%s\" löschen?</string>
<string name="artists">Interpreten</string>
<string name="albums">Alben</string>
<string name="songs">Titel</string>
@ -86,7 +86,7 @@
<string name="play_all">Alle abspielen</string>
<string name="enqueue_all">Alle einreihen</string>
<string name="all_songs">Alle Titel</string>
<string name="more_from_artist">Mehr vom Künstler</string>
<string name="more_from_artist">Mehr vom Interpreten</string>
<string name="more_from_album">Mehr aus dem Album</string>
<string name="name">Name</string>
<string name="number_of_tracks">Anzahl der Titel</string>
@ -101,7 +101,7 @@
<string name="ascending">Aufsteigend</string>
<string name="descending">Absteigend</string>
<!--Notification-->
<string name="notification_title_paused">%s (Angehalten)</string>
<string name="notification_title_paused">%s (angehalten)</string>
<!--Preferences-->
<plurals name="seconds">
<item quantity="one">1 Sekunde</item>
@ -117,82 +117,82 @@
</plurals>
<string name="audio">Audio</string>
<string name="volume_title">Lautstärke</string>
<string name="media_button_title">Kopfhörer/Bluetooth-Steuerung</string>
<string name="media_button_title">Headset-/Bluetooth-Steuerung</string>
<string name="media_button_summary">Dies wird auch für die Steuerelemente der ICS-Bildschirmsperre benötigt.</string>
<string name="media_button_beep_title">Headset Steuerung</string>
<string name="media_button_beep_summary">Piepen nachdem ein Titel via Headset übersprungen wurde</string>
<string name="media_button_beep_title">Headset-Steuerung signalisieren</string>
<string name="media_button_beep_summary">Akustisches Signal nach dem Überspringen eines Titels via Headset-Steuerung.</string>
<string name="headset_only_title">Nur externer Ausgang</string>
<string name="headset_only_summary">Verhindert, dass Musik über die internen Lautsprecher wiedergegeben wird.</string>
<string name="headset_pause_title">Anhalten wenn nicht angeschlossen</string>
<string name="headset_pause_summary">Anhalten, wenn die Kopfhörer entfernt werden.</string>
<string name="headset_play_title">Abspielen beim anschließen</string>
<string name="headset_play_summary">Abspielen wenn der Kopfhörer angeschlossen wurde. (Nur wenn der Dienst aktiv ist)</string>
<string name="headset_pause_title">Pausieren, wenn nicht angeschlossen</string>
<string name="headset_pause_summary">Pausieren, wenn der Kopfhörer entfernt wird.</string>
<string name="headset_play_title">Abspielen beim Anschließen</string>
<string name="headset_play_summary">Abspielen, wenn der Kopfhörer angeschlossen wird (funkioniert nur bei aktivem Dienst).</string>
<string name="replaygain">Replay Gain</string>
<string name="replaygain_title">Replay Gain aktivieren</string>
<string name="replaygain_summary">Replay Gain Einstellungen</string>
<string name="replaygain_track_title">Aktiviere per-track Replay Gain</string>
<string name="replaygain_track_summary">Titel in einheitlicher Lautstärke abspielen</string>
<string name="replaygain_album_title">Aktiviere Album Replay Gain</string>
<string name="replaygain_summary">Replay Gain-Einstellungen</string>
<string name="replaygain_track_title">Per-Titel-Replay Gain aktivieren</string>
<string name="replaygain_track_summary">Alle Titel in einheitlicher Lautstärke abspielen</string>
<string name="replaygain_album_title">Album-Replay Gain aktivieren</string>
<string name="replaygain_album_summary">Lautstärke von Alben beibehalten</string>
<string name="replaygain_bump_title">Replay Gain Vorverstärker</string>
<string name="replaygain_untagged_debump_title">Titel ohne Replay Gain Information</string>
<string name="replaygain_bump_title">Replay Gain-Vorverstärker</string>
<string name="replaygain_untagged_debump_title">Titel ohne Replay Gain-Information</string>
<string name="replaygain_untagged_debump_summary">Lautstärke senken um: </string>
<string name="readahead">Datei vorauslesen</string>
<string name="readahead_summary">Liest den momentan abgespielten Titel im Voraus ein (löst Probleme mit langsamen SD-Karten).</string>
<string name="equalizer">Equalizer</string>
<string name="cycle_continuous_shuffling">Fortlaufend durchwürfeln</string>
<string name="cycle_continuous_shuffling_summary">Im \'Zufällige Wiedergabe\' Modus werden die Titel fortlaufend neu angeordnet</string>
<string name="cycle_continuous_shuffling">Dauerzufallswiedergabe</string>
<string name="cycle_continuous_shuffling_summary">Titel werden fortlaufend neu durchgemischt</string>
<string name="notifications">Benachrichtigungen</string>
<string name="notification_mode_title">Benachrichtigungsmodus</string>
<string name="notification_action_title">Benachrichtigungsaktion</string>
<string name="notification_invert_color_title">Farbe der Benachrichtigung umkehren</string>
<string name="notification_invert_color_summary">Text weiß statt schwarz anzeigen</string>
<string name="notification_nag">Auffällige Notifikation</string>
<string name="notification_nag_summary">Aktuellen Titel als \'Heads-Up-Notifikation\' anzeigen</string>
<string name="playback_screen">Wiedergabebildschirm</string>
<string name="notification_nag">Auffällige Benachrichtigung</string>
<string name="notification_nag_summary">Titelwechsel als \"Heads-Up\"-Benachrichtigung anzeigen</string>
<string name="playback_screen">Wiedergabeansicht</string>
<string name="playback_on_startup_title">Beim Start öffnen</string>
<string name="playback_on_startup_summary">Wiedergabeansicht beim Start anzeigen</string>
<string name="playback_on_startup_summary">Wiedergabeansicht beim Start öffnen</string>
<string name="display_mode_title">Anzeigemodus</string>
<string name="swipe_up_action_title">Hoch-Wisch Aktion</string>
<string name="swipe_down_action_title">Runter-Wisch Aktion</string>
<string name="cover_press_action_title">Cover-Antippen Aktion</string>
<string name="cover_longpress_action_title">Aktion bei langem Cover-Drücken</string>
<string name="library_screen">Bibliotheksbildschirm</string>
<string name="swipe_up_action_title">Aktion beim Hochwischen</string>
<string name="swipe_down_action_title">Aktion beim Runterwischen</string>
<string name="cover_press_action_title">Aktion beim Coverantippen</string>
<string name="cover_longpress_action_title">Aktion bei langem Coverdrücken</string>
<string name="library_screen">Bibliotheksansicht</string>
<string name="controls_in_selector_title">Steuerelemente in Bibliotheksansicht</string>
<string name="controls_in_selector_summary">Zeige den aktuell gespielten Titel und die Steuerelemente in der Bibliotheksansicht</string>
<string name="controls_in_selector_summary">Aktuell abgespielten Titel und Steuerelemente in Bibliotheksansicht anzeigen</string>
<string name="default_action_title">Standardaktion</string>
<string name="default_playlist_action_title">Standard Wiedergabelisten-Aktion</string>
<string name="accelerometer_shake">Beschleunigungssensor-Schütteln</string>
<string name="enable_shake_title">Beschleunigungssensor-Schütteln aktivieren</string>
<string name="enable_shake_summary">Ist nur aktiv wenn Musik abgespielt wird. Funktioniert auf einigen Geräten nicht bei ausgeschaltetem Bildschirm.</string>
<string name="default_playlist_action_title">Wiedergabelisten-Standardaktion</string>
<string name="accelerometer_shake">Schütteln</string>
<string name="enable_shake_title">Schütteln aktivieren</string>
<string name="enable_shake_summary">Ist nur während abgespielter Musik aktiv. Funktioniert auf einigen Geräten nicht bei ausgeschaltetem Bildschirm.</string>
<string name="shake_action_title">Aktion beim Schütteln</string>
<string name="shake_threshold_title">Schwellwert beim Schütteln</string>
<string name="misc_features">Sonstige Funktionen</string>
<string name="use_dark_theme_title">Dunkles Theme verwenden</string>
<string name="use_dark_theme_summary">Aktiviert das \'Dark Material Design Theme\'</string>
<string name="use_dark_theme_title">Dunkles Design verwenden</string>
<string name="use_dark_theme_summary">Aktiviert das dunkle \"Material\"-Design</string>
<string name="disable_lockscreen_title">Bildschirmsperre deaktivieren</string>
<string name="disable_lockscreen_summary">Bildschirmsperre verhindern wenn Bibliothek- oder Wiedergabeansicht aktiv</string>
<string name="use_idle_timeout_title">Inaktivitäts-Zeitabschaltung aktivieren</string>
<string name="use_idle_timeout_summary">Wenn aktiviert, wird die Wiedergabe nach der angegebenen Inaktivität beendet</string>
<string name="idle_timeout_title">Inaktive Zeit</string>
<string name="disable_lockscreen_summary">Bildschirmsperre verhindern, wenn Bibliothek- oder Wiedergabeansicht aktiv</string>
<string name="use_idle_timeout_title">Inaktivitätszeitabschaltung aktivieren</string>
<string name="use_idle_timeout_summary">Falls aktiviert, wird die Wiedergabe nach der angegebenen Inaktivität beendet</string>
<string name="idle_timeout_title">Inaktivitätszeitabschaltung</string>
<string name="disable_cover_art_title">Cover-Darstellung deaktivieren</string>
<string name="disable_cover_art_summary">Jegliches Laden von Cover-Art vermeiden</string>
<string name="disable_cover_art_summary">Jegliches Laden von Albumcovern vermeiden</string>
<string name="coverloader_android_title">Albumcover von Android verwenden</string>
<string name="coverloader_android_summary">Verwendet Androids interne Mediendatenbank um Albumcover zu laden</string>
<string name="coverloader_android_summary">Androids interne Mediendatenbank verwenden, um Albumcover zu laden</string>
<string name="coverloader_vanilla_title">Cover aus Ordner laden</string>
<string name="coverloader_vanilla_summary">Sucht nach Dateien mit den Namen cover.jpg, album.jpg oder artwork.jpg und verwendet jene als Albumcover</string>
<string name="coverloader_shadow_title">Cover aus \'verstecktem\' Ordner laden</string>
<string name="coverloader_shadow_summary">Verwendet die Datei unter \'/sdcard/Music/.vanilla/ARTIST/ALBUM.jpg\' als Albumcover. (z.B: /sdcard/Music/.vanilla/Jack White/Blunderbuss.jpg)</string>
<string name="double_tap_title">Widget-Doppelklick</string>
<string name="double_tap_summary">Doppeltes Antippen des 1x1 Widget öffnet die Wiedergabeansicht. Verursacht eine Verzögerung von 400 ms bevor das Widget auf Aktionen reagiert.</string>
<string name="scrobble_title">ScrobbleDroid API verwenden</string>
<string name="coverloader_vanilla_summary">Nach Dateien mit den Namen cover.jpg, album.jpg oder artwork.jpg suchen und diese als Albumcover anzeigen</string>
<string name="coverloader_shadow_title">Cover aus verstecktem Ordner laden</string>
<string name="coverloader_shadow_summary">Albumcover von \"/sdcard/Music/.vanilla/ARTIST/ALBUM.jpg\" laden, falls möglich.</string>
<string name="double_tap_title">Widget-Doppelantippen</string>
<string name="double_tap_summary">Doppeltes Antippen des 1x1-Widgets öffnet die Wiedergabeansicht. Verursacht eine Verzögerung von 400 ms bevor das Widget auf Aktionen reagiert.</string>
<string name="scrobble_title">ScrobbleDroid-API verwenden</string>
<string name="scrobble_summary">Zu Last.FM scrobbeln mithilfe von ScrobbleDroid oder Simple Last.FM Scrobbler</string>
<string name="stock_broadcast_title">Emuliere Standardplayer-Benachrichtigungen</string>
<string name="stock_broadcast_summary">Sende Benachrichtigungen, welche den normalen Standardplayer emulieren, um mit externen Bildschirmsperre-Steuerelementen, Widgets etc. kompatibel zu sein.</string>
<string name="stock_broadcast_title">Standardplayer-Benachrichtigungen emulieren</string>
<string name="stock_broadcast_summary">Benachrichtigungen senden, welche den normalen Standardplayer emulieren, um mit externen Bildschirmsperre-Steuerelementen, Widgets etc. kompatibel zu sein.</string>
<string name="media_scan">Bibliothek aktualisieren</string>
<string name="tap_to_scan">Antippen, um externem Speicher nach Musik zu durchsuchen</string>
<string name="scan_in_progress">Aktualisierung wird durchgeführt</string>
<string name="finished_scanning">Durchsuchen abgeschlossen. Antippen für erneute Suche.</string>
<string name="scan_in_progress">Suche wird durchgeführt </string>
<string name="finished_scanning">Durchsuchen abgeschlossen. Für erneute Suche antippen.</string>
<string name="about">Über</string>
<string name="tabs">Tab-Reihenfolge</string>
<string name="customize_tab_order">Anpassen der Reihenfolge und Sichtbarkeit der Bibliothek-Tabs</string>
@ -203,21 +203,21 @@
<string name="show_when_playing">Bei Wiedergabe anzeigen (Standard)</string>
<string name="always_show">Immer anzeigen</string>
<string name="open_main_activity">Bibliothek öffnen (Standard)</string>
<string name="open_mini_popup">Mini-popup öffnen</string>
<string name="open_mini_popup">Mini-Popup öffnen</string>
<string name="skip_to_next_song">Zum nächsten Titel springen</string>
<string name="open_full_player">Vanilla Music öffnen</string>
<string name="info_on_cover">Info auf dem Cover</string>
<string name="info_below_cover">Info unter dem Cover</string>
<string name="fixed_info">Info fest oben</string>
<string name="fixed_info">Info oben fixiert</string>
<string name="do_nothing">Keine</string>
<string name="open_library">Bibliothek öffnen</string>
<string name="play_pause">Wiedergabe/Pause</string>
<string name="next_song">Nächster Titel</string>
<string name="previous_song">Voriger Titel</string>
<string name="previous_song">Vorheriger Titel</string>
<string name="next_album">Nächstes Album</string>
<string name="previous_album">Voriges Album</string>
<string name="cycle_repeat_mode">Wiederholungsart durchwählen</string>
<string name="cycle_shuffle_mode">Zufällige Wiedergabearten durchwählen</string>
<string name="previous_album">Vorheriges Album</string>
<string name="cycle_repeat_mode">Wiederholungsmodi wechseln</string>
<string name="cycle_shuffle_mode">Zufallswiedergabemodi wechseln</string>
<string name="enqueue_current_album">Album einreihen</string>
<string name="enqueue_current_artist">Interpret einreihen</string>
<string name="enqueue_current_genre">Genre einreihen</string>
@ -226,10 +226,10 @@
<string name="show_queue">Warteschlange anzeigen</string>
<string name="dequeue_rest">Warteschlange leeren</string>
<string name="queue">Warteschlange</string>
<string name="toggle_controls">Steuerelemente ein/-ausblenden</string>
<string name="toggle_controls">Steuerelemente ein-/ausblenden</string>
<string name="seek_10s_backward">10 Sekunden zurück springen</string>
<string name="seek_10s_forward">10 Sekunden vorwärts springen</string>
<string name="filebrowser_start">Startverzeichnis</string>
<string name="customize_filebrowser_start">Dateibrowser startet in diesem Verzeichnis</string>
<string name="filebrowser_start">Startordner</string>
<string name="customize_filebrowser_start">Dateimanager startet in diesem Ordner</string>
<string name="select">Auswählen</string>
</resources>

View File

@ -0,0 +1,197 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="app_name">Ванила музика</string>
<!--Playback Activity (main screen)-->
<string name="no_songs">Нема песама на вашем уређају.</string>
<string name="empty_queue">Ништа није изабрано. Изаберите песме из библиотеке или уђите у насумични режим тапом на ову поруку.</string>
<string name="settings">Поставке</string>
<string name="library">Библиотека</string>
<string name="no_shuffle">Без тумбања</string>
<string name="shuffle_songs">Претумбај песме</string>
<string name="shuffle_songs_continuously">Претумбавај песме непрестано</string>
<string name="shuffle_albums">Претумбај албуме</string>
<string name="no_repeat">Не понављај</string>
<string name="repeat">Понављај</string>
<string name="repeat_current_song">Понови текућу песму</string>
<string name="stop_current_song">Заустави након текуће песме</string>
<string name="random">Насумично</string>
<string name="song_load_failed">Неуспех учитавања песме %s. Можда је фајл оштећен или га нема.</string>
<string name="queue_cleared">Ред пуштања очишћен.</string>
<string name="cover_art">Омот</string>
<string name="close_notification">Затвори обавештење</string>
<string name="_genre">жанр</string>
<string name="_track">нумера</string>
<string name="_composer">композитор</string>
<string name="_format">формат</string>
<string name="_title">наслов</string>
<string name="_artist">извођач</string>
<string name="_album">албум</string>
<string name="_year">година</string>
<!--New Playlist Dialog-->
<string name="choose_playlist_name">Наслов листе нумера</string>
<string name="create">Направи</string>
<string name="overwrite">Пребриши</string>
<string name="cancel">Одустани</string>
<string name="delete_playlist">Да обришем листу нумера %s?</string>
<!--Library-->
<string name="enqueue">Стави у ред</string>
<string name="enqueue_as_next">Пусти следећу</string>
<string name="play">Пусти</string>
<string name="edit">Уреди</string>
<string name="rename">Преименуј</string>
<string name="save_as_playlist">Сачувај као листу нумера…</string>
<string name="add_to_playlist">Сачувај у листу нумера…</string>
<string name="add_to_favorites">Додај у омиљене</string>
<string name="playlist_favorites">Омиљене</string>
<string name="new_playlist">Нова листа нумера…</string>
<string name="expand">Рашири</string>
<string name="delete">Обриши</string>
<string name="playback_view">Текућа песма</string>
<string name="sort_by">Поређај по</string>
<string name="search">Тражи</string>
<string name="done">Готово</string>
<string name="remove">Уклони</string>
<string name="clear_search">Очисти претрагу</string>
<string name="play_or_enqueue">У ред ако је пуштено; пусти ако је паузирано</string>
<plurals name="playing">
<item quantity="one">1 песма пуштена.</item>
<item quantity="few">%d песме пуштене.</item>
<item quantity="other">%d песама пуштено.</item>
</plurals>
<plurals name="enqueued">
<item quantity="one">1 песма стављена у ред.</item>
<item quantity="few">%d песме стављене у ред.</item>
<item quantity="other">%d песама стављено у ред.</item>
</plurals>
<plurals name="added_to_playlist">
<item quantity="one">1 песма додата у листу нумера %2$s.</item>
<item quantity="few">%1$d песме додате у листу нумера %2$s.</item>
<item quantity="other">%1$d песама додато у листу нумера %2$s.</item>
</plurals>
<plurals name="deleted">
<item quantity="one">1 песма обрисана.</item>
<item quantity="few">%d песме обрисане.</item>
<item quantity="other">%d песама обрисано.</item>
</plurals>
<string name="delete_file_failed">Брисање %s није успело.</string>
<string name="deleted_item">„%s“ обрисана.</string>
<string name="delete_item">Да обришем „%s“?</string>
<string name="delete_file">Да обришем фајл „%s“?</string>
<string name="artists">Извођачи</string>
<string name="albums">Албуми</string>
<string name="songs">Песме</string>
<string name="playlists">Листе нумера</string>
<string name="genres">Жанрови</string>
<string name="files">Фајлови</string>
<string name="none">Ништа</string>
<string name="unknown">Непознато</string>
<string name="play_all">Пусти све</string>
<string name="enqueue_all">Стави све у ред</string>
<string name="all_songs">Све песме</string>
<string name="more_from_artist">Још од извођача</string>
<string name="more_from_album">Још са албума</string>
<string name="name">називу</string>
<string name="number_of_tracks">броју нумера</string>
<string name="year">години</string>
<string name="date_added">датуму додавања</string>
<string name="artist_album">извођачу, албуму</string>
<string name="artist_album_track">извођачу, албуму, нумери</string>
<string name="artist_album_title">извођачу, албуму, наслову</string>
<string name="artist_year">извођачу, години</string>
<string name="album_track">албуму, нумери</string>
<string name="song_playcount">броју пуштања</string>
<string name="ascending">Растуће</string>
<string name="descending">Опадајуће</string>
<!--Notification-->
<string name="notification_title_paused">%s (паузирана)</string>
<!--Preferences-->
<plurals name="seconds">
<item quantity="one">1 секунда</item>
<item quantity="few">%d секунде</item>
<item quantity="other">%d секунди</item>
</plurals>
<plurals name="minutes">
<item quantity="one">1 минута</item>
<item quantity="few">%d минуте</item>
<item quantity="other">%d минута</item>
</plurals>
<plurals name="hours">
<item quantity="one">1 сат</item>
<item quantity="few">%d сата</item>
<item quantity="other">%d сати</item>
</plurals>
<string name="audio">Звук</string>
<string name="headset_pause_title">Паузирај по искључењу</string>
<string name="headset_pause_summary">Пауза по искључењу слушалица.</string>
<string name="headset_play_title">Пусти по укључењу </string>
<string name="headset_play_summary">Пуштање музике по укључењу слушалица. (Ради само ако је сервис покренут.)</string>
<string name="readahead">Укључи предучитавање</string>
<string name="equalizer">Еквилајзер</string>
<string name="notifications">Обавештења</string>
<string name="notification_mode_title">Приказ обавештења</string>
<string name="notification_action_title">Радња обавештења</string>
<string name="notification_nag">Веома исцрпно обавештење</string>
<string name="playback_screen">Екран пуштања</string>
<string name="playback_on_startup_title">Отвори по покретању</string>
<string name="playback_on_startup_summary">Отвара екран пуштања по покретању</string>
<string name="display_mode_title">Приказ информација</string>
<string name="swipe_up_action_title">Превлачење на горе</string>
<string name="swipe_down_action_title">Превлачење на доле</string>
<string name="cover_press_action_title">Тап на омот</string>
<string name="cover_longpress_action_title">Дужи притисак на омот</string>
<string name="library_screen">Екран библиотеке</string>
<string name="default_action_title">Подразумевана радња</string>
<string name="default_playlist_action_title">Подразумевана радња листе нумера</string>
<string name="accelerometer_shake">Протресање телефона</string>
<string name="enable_shake_title">Омогући протресање телефона</string>
<string name="enable_shake_summary">Активно само ако је музика пуштена. На неким уређајима не ради када је екран угашен.</string>
<string name="shake_action_title">Протресање</string>
<string name="shake_threshold_title">Праг силе протресања</string>
<string name="misc_features">Разне функције</string>
<string name="use_dark_theme_title">Користи тамну тему</string>
<string name="use_dark_theme_summary">Тамна материјал тема</string>
<string name="double_tap_title">Двотап на виџет</string>
<string name="double_tap_summary">Двотап на 1x1 виџет отвара плејер. Опција поставља застој од 400ms пре одзива виџета на радње.</string>
<string name="media_scan">Освежи библиотеку</string>
<string name="scan_in_progress">Скенирање у току…</string>
<string name="finished_scanning">Скенирање завршено. Тапните за поновно скенирање.</string>
<string name="about">О програму</string>
<string name="tabs">Редослед језичака</string>
<string name="customize_tab_order">Прилагођавање редоследа и видљивости језичака библиотеке</string>
<string name="restore_default">Врати подразумевано</string>
<!--The following are for the list preferences-->
<string name="last_used_action">претходно коришћена радња</string>
<string name="never_show">никад</string>
<string name="show_when_playing">у току пуштања (подраз.)</string>
<string name="always_show">увек</string>
<string name="open_main_activity">отвара библиотеку (подраз.)</string>
<string name="open_mini_popup">отвара искачуће прозорче</string>
<string name="skip_to_next_song">пушта следећу песму</string>
<string name="open_full_player">отвара цео плејер</string>
<string name="info_on_cover">на омоту</string>
<string name="info_below_cover">испод омота</string>
<string name="fixed_info">фиксиран на врху</string>
<string name="do_nothing">не ради ништа</string>
<string name="open_library">отвара библиотеку</string>
<string name="play_pause">пушта/паузира</string>
<string name="next_song">пушта следећу песму</string>
<string name="previous_song">пушта претходну песму</string>
<string name="next_album">пушта следећи албум</string>
<string name="previous_album">пушта претходни албум</string>
<string name="cycle_repeat_mode">мења режим понављања</string>
<string name="cycle_shuffle_mode">мења режим тумбања</string>
<string name="enqueue_current_album">Стави албум у ред</string>
<string name="enqueue_current_artist">Стави извођача у ред</string>
<string name="enqueue_current_genre">Стави жанр у ред</string>
<string name="clear_queue">Очисти ред</string>
<string name="empty_the_queue">Испразни ред</string>
<string name="show_queue">Прикажи ред</string>
<string name="dequeue_rest">Уклони остале из реда</string>
<string name="queue">Ред пуштања</string>
<string name="toggle_controls">мења приказ контрола</string>
<string name="seek_10s_backward">прескаче 10 секунди уназад</string>
<string name="seek_10s_forward">прескаче 10 секунди унапред</string>
<string name="filebrowser_start">Почетна страница</string>
<string name="customize_filebrowser_start">Прегледач фајлова почиње од ове фасцикле</string>
<string name="select">Изабери</string>
</resources>

View File

@ -147,6 +147,8 @@
<string name="notification_action_title">Bildirim Eylemi</string>
<string name="notification_invert_color_title">Bildirim rengini tersine döndür</string>
<string name="notification_invert_color_summary">Siyah metin yerine beyaz metin kullan</string>
<string name="notification_nag">Tam sözlü bildirim</string>
<string name="notification_nag_summary">Şarkı değişikliklerini \'Ekran Üstü Bildirimi\' ile göster</string>
<string name="playback_screen">Oynatma Ekranı</string>
<string name="playback_on_startup_title">Başlangıçta Aç</string>
<string name="playback_on_startup_summary">Başlangıçta oynatım görünümünü açar</string>

View File

@ -35,6 +35,9 @@ Copyright (C) 2015 Adrian Ulrich <adrian@blinkenlights.ch>
<color name="controls_active">@color/vanillaAccent</color>
<color name="controls_normal">@color/material_grey_600</color>
<!-- showqueue info -->
<color name="now_playing_marker">@color/vanillaAccent</color>
<!-- styles -->
<style name="Dialog" parent="android:Theme.Material.Light.Dialog" />
<style name="DialogMinWidth" parent="android:Theme.Material.Light.Dialog.MinWidth" />

View File

@ -0,0 +1,228 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="app_name">Vanilla Music</string>
<!--Playback Activity (main screen)-->
<string name="no_songs">在你的设备上未找到音乐。</string>
<string name="empty_queue">没有选定的音乐。由媒体库或是轻点此信息进入随机模式挑选。</string>
<string name="settings">设置</string>
<string name="library">媒体库</string>
<string name="no_shuffle">不随机播放</string>
<string name="shuffle_songs">随机播放音乐</string>
<string name="shuffle_songs_continuously">连续随机播放音乐</string>
<string name="shuffle_albums">随机播放专辑</string>
<string name="no_repeat">不重复播放</string>
<string name="repeat">重复播放</string>
<string name="repeat_current_song">重播当前曲目</string>
<string name="stop_current_song">当前曲目之后停止</string>
<string name="random">随机</string>
<string name="random_enabling">启用随机</string>
<string name="song_load_failed">载入音乐 %s 失败。它可能已经损坏或遗失。</string>
<string name="queue_cleared">序列已清除</string>
<string name="cover_art">专辑封面</string>
<string name="close_notification">关闭通知</string>
<string name="_genre">类型</string>
<string name="_track">曲目</string>
<string name="_composer">作曲</string>
<string name="_format">格式</string>
<string name="_title">标题</string>
<string name="_artist">演出者</string>
<string name="_album">专辑</string>
<string name="_year">年份</string>
<string name="_replaygain">重播增益</string>
<!--New Playlist Dialog-->
<string name="choose_playlist_name">选择播放列表名字</string>
<string name="create">创建</string>
<string name="overwrite">覆盖</string>
<string name="cancel">取消</string>
<string name="delete_playlist">删除播放列表 %s </string>
<!--Library-->
<string name="enqueue">序列</string>
<string name="enqueue_as_next">播放下一首</string>
<string name="play">播放</string>
<string name="edit">编辑</string>
<string name="rename">重命名</string>
<string name="save_as_playlist">保存播放列表为…</string>
<string name="add_to_playlist">添加到播放列表…</string>
<string name="add_to_favorites">添加到最爱</string>
<string name="playlist_favorites">最爱</string>
<string name="new_playlist">新建播放列表…</string>
<string name="expand">展开</string>
<string name="delete">删除</string>
<string name="playback_view">当前播放</string>
<string name="sort_by">排序依据</string>
<string name="search">搜索</string>
<string name="done">完成</string>
<string name="remove">移除</string>
<string name="clear_search">清空搜索栏</string>
<string name="play_or_enqueue">如果播放序列;如果暫停播放</string>
<plurals name="playing">
<item quantity="other">正在播放 %d 首音乐。</item>
</plurals>
<plurals name="enqueued">
<item quantity="other">%d 首音乐存入序列。</item>
</plurals>
<plurals name="added_to_playlist">
<item quantity="other">%1$d 首音乐已经加入播放列表 %2$s。</item>
</plurals>
<plurals name="deleted">
<item quantity="other">%d 首音乐已删除。</item>
</plurals>
<string name="delete_file_failed">删除 %s 失败。</string>
<string name="deleted_item">“%s”已删除。</string>
<string name="delete_item">删除“%s”</string>
<string name="delete_file">删除文件“%s”</string>
<string name="artists">演出者</string>
<string name="albums">专辑</string>
<string name="songs">音乐</string>
<string name="playlists">播放清单</string>
<string name="genres">风格</string>
<string name="files">文件</string>
<string name="none"></string>
<string name="unknown">未知</string>
<string name="play_all">全部播放</string>
<string name="enqueue_all">全部加入序列</string>
<string name="all_songs">全部歌曲</string>
<string name="more_from_artist">更多来自演出者</string>
<string name="more_from_album">更多来自专辑</string>
<string name="name">名称</string>
<string name="number_of_tracks">曲目号</string>
<string name="year">年份</string>
<string name="date_added">添加日期</string>
<string name="artist_album">演出者,专辑</string>
<string name="artist_album_track">演出者,专辑,曲目号</string>
<string name="artist_album_title">演出者,专辑,标题</string>
<string name="artist_year">演出者,年份</string>
<string name="album_track">演出者,曲目号</string>
<string name="song_playcount">播放次数</string>
<string name="ascending">升序</string>
<string name="descending">绛序</string>
<!--Notification-->
<string name="notification_title_paused">%s (已暂停)</string>
<!--Preferences-->
<plurals name="seconds">
<item quantity="other">%d 秒</item>
</plurals>
<plurals name="minutes">
<item quantity="other">%d 分钟</item>
</plurals>
<plurals name="hours">
<item quantity="other">%d 小时</item>
</plurals>
<string name="audio">音频</string>
<string name="volume_title">音量</string>
<string name="media_button_title">耳机 / 蓝牙控制器</string>
<string name="media_button_summary">此特性也需要 ICS 锁屏控制器。</string>
<string name="media_button_beep_title">耳机操作时发出哔的一声</string>
<string name="media_button_beep_summary">使用耳机控制器连按两次以后切换到下一曲目,并发出哔的一声。</string>
<string name="headset_only_title">只有外部输出</string>
<string name="headset_only_summary">防止经由内部扬声器播放音乐</string>
<string name="headset_pause_title">拔出时暂停播放</string>
<string name="headset_pause_summary">当耳机拔出时暂停播放</string>
<string name="headset_play_title">插入时播放</string>
<string name="headset_play_summary">当耳机插入时播放。(仅当系统运行时)</string>
<string name="replaygain">重播增益</string>
<string name="replaygain_title">启用重播增益</string>
<string name="replaygain_summary">配置重播增益信息</string>
<string name="replaygain_track_title">启用每个曲目的播放增益</string>
<string name="replaygain_track_summary">以同等响度播放所有音乐</string>
<string name="replaygain_album_title">启用专辑重播增益</string>
<string name="replaygain_album_summary">维持专辑力度变化</string>
<string name="replaygain_bump_title">重播增益的前置放大器</string>
<string name="replaygain_untagged_debump_title">禁用重播增益的歌曲标记</string>
<string name="replaygain_untagged_debump_summary">降低音量依据</string>
<string name="readahead">启用预先读取</string>
<string name="readahead_summary">预先读取当前播放的音乐。此功能可以解决“音频滞后”的问题。(由慢速 SD 卡导致的)</string>
<string name="equalizer">均衡器</string>
<string name="cycle_continuous_shuffling">连续随机播放</string>
<string name="cycle_continuous_shuffling_summary">“随机模式”将会以完全随机顺序播放音乐</string>
<string name="notifications">通知</string>
<string name="notification_mode_title">通知模式</string>
<string name="notification_action_title">通知操作</string>
<string name="notification_invert_color_title">反转通知颜色</string>
<string name="notification_invert_color_summary">用白色文字替代黑色文字</string>
<string name="notification_nag">非常详尽的通知</string>
<string name="notification_nag_summary">使用“抬头通知”来告知曲面变换</string>
<string name="playback_screen">播放界面</string>
<string name="playback_on_startup_title">启动时开启</string>
<string name="playback_on_startup_summary">启动时打开播放界面</string>
<string name="display_mode_title">显示模式</string>
<string name="swipe_up_action_title">向上滑动</string>
<string name="swipe_down_action_title">向下滑动</string>
<string name="cover_press_action_title">轻触专辑封面的操作</string>
<string name="cover_longpress_action_title">长按专辑封面的操作</string>
<string name="library_screen">媒体库界面</string>
<string name="controls_in_selector_title">媒体库中的控制按钮</string>
<string name="controls_in_selector_summary">显示当前播放的曲目并在媒体库视图中显示媒体控制按钮</string>
<string name="default_action_title">默认动作</string>
<string name="default_playlist_action_title">默认播放列表动作</string>
<string name="accelerometer_shake">摇一摇</string>
<string name="enable_shake_title">启用摇一摇</string>
<string name="enable_shake_summary">只在音乐播放时可用。有些设备在屏幕关闭时不可用。</string>
<string name="shake_action_title">摇动操作</string>
<string name="shake_threshold_title">摇动力道阈值</string>
<string name="misc_features">杂项特性</string>
<string name="use_dark_theme_title">使用暗色界面</string>
<string name="use_dark_theme_summary">使用暗色材质主题</string>
<string name="disable_lockscreen_title">禁用锁屏</string>
<string name="disable_lockscreen_summary">在媒体库界面或回放屏幕时禁用锁屏</string>
<string name="use_idle_timeout_title">启用闲置超时</string>
<string name="use_idle_timeout_summary">启用时,闲置一段时间后会停止播放</string>
<string name="idle_timeout_title">闲置超时</string>
<string name="disable_cover_art_title">禁用专辑封面</string>
<string name="disable_cover_art_summary">不要在此应用的任何情况下载入专辑封面</string>
<string name="coverloader_android_title">从 Android 设备载入</string>
<string name="coverloader_android_summary">查询 Android 内部的媒体数据库来做专辑封面</string>
<string name="coverloader_vanilla_title">从文件夹中载入封面</string>
<string name="coverloader_vanilla_summary">搜索文件名是 cover.jpg、album.jpg 或 artwork.jpg 的图片文件,以其作为专辑封面</string>
<string name="coverloader_shadow_title">从隐藏文件载入封面</string>
<string name="coverloader_shadow_summary">尝试从 \'/sdcard/Music/.vanilla/ARTIST/ALBUM.jpg\' 中载入封面图</string>
<string name="double_tap_title">双击小工具</string>
<string name="double_tap_summary">双击 1 x 1 的小工具将会打开播放器。小工具做出响应之前会有 400 毫秒延迟。</string>
<string name="scrobble_title">使用 ScrobbleDroid API</string>
<string name="scrobble_summary">Scrobble 透过 ScrobbleDroid 到 Last.FM 或只是 Last.FM Scrobbler</string>
<string name="stock_broadcast_title">模拟原生应用广播</string>
<string name="stock_broadcast_summary">发送广播以模拟那些由本地原生播放器而产生的效果,例如第三方锁屏控制,小工具等等。</string>
<string name="media_scan">刷新媒体库</string>
<string name="tap_to_scan">轻触以扫描外部存储的音乐</string>
<string name="scan_in_progress">扫描进行中…</string>
<string name="finished_scanning">完成扫描。轻触以再次扫描。</string>
<string name="about">关于</string>
<string name="tabs">标签页排序</string>
<string name="customize_tab_order">调整媒体库标签页的顺序及可见性</string>
<string name="restore_default">返回默认</string>
<!--The following are for the list preferences-->
<string name="last_used_action">上次使用的操作</string>
<string name="never_show">永不显示</string>
<string name="show_when_playing">播放时显示 (默认)</string>
<string name="always_show">永远显示</string>
<string name="open_main_activity">打开媒体库 (默认)</string>
<string name="open_mini_popup">打开迷你弹出框</string>
<string name="skip_to_next_song">跳到下一首</string>
<string name="open_full_player">打开完整播放器</string>
<string name="info_on_cover">信息在封面之上</string>
<string name="info_below_cover">信息在封面之下</string>
<string name="fixed_info">信息固定在顶部</string>
<string name="do_nothing">什么也不做</string>
<string name="open_library">打开媒体库</string>
<string name="play_pause">播放 / 暂停</string>
<string name="next_song">下一曲</string>
<string name="previous_song">上一首</string>
<string name="next_album">下一专辑</string>
<string name="previous_album">上一专辑</string>
<string name="cycle_repeat_mode">循环重复模式</string>
<string name="cycle_shuffle_mode">循环随机模式</string>
<string name="enqueue_current_album">以专辑加入序列</string>
<string name="enqueue_current_artist">以演出者加入序列</string>
<string name="enqueue_current_genre">以风格加入序列</string>
<string name="clear_queue">清空序列</string>
<string name="empty_the_queue">空序列</string>
<string name="show_queue">显示序列</string>
<string name="dequeue_rest">剩余的移出序列</string>
<string name="queue">序列</string>
<string name="toggle_controls">切换控制器</string>
<string name="seek_10s_backward">向后倒退 10 秒</string>
<string name="seek_10s_forward">向前快进 10 秒</string>
<string name="filebrowser_start">起始目录</string>
<string name="customize_filebrowser_start">在此目录开始浏览文件</string>
<string name="select">选择</string>
</resources>

View File

@ -25,6 +25,9 @@ THE SOFTWARE.
<color name="controls_active">@android:color/holo_blue_dark</color>
<color name="controls_normal">#fff5f5f5</color>
<!-- showqueue info -->
<color name="now_playing_marker">@android:color/holo_blue_dark</color>
<!-- styles -->
<style name="Dialog" parent="android:Theme.Holo.Dialog" />
<style name="DialogMinWidth" parent="android:Theme.Holo.Dialog.MinWidth" />

View File

@ -233,7 +233,7 @@ THE SOFTWARE.
<string name="double_tap_title">Double Tap Widget</string>
<string name="double_tap_summary">Double-tapping the 1x1 widget will open the player. Incurs a 400ms delay before the widget responds to actions.</string>
<string name="scrobble_title">Use ScrobbleDroid API</string>
<string name="scrobble_summary">Scrobble to Last.FM through ScrobbleDroid or Simple Last.FM Scrobbler</string>
<string name="scrobble_summary">Scrobble to Last.fm through ScrobbleDroid or Simple Last.fm Scrobbler</string>
<string name="stock_broadcast_title">Emulate Stock Broadcasts</string>
<string name="stock_broadcast_summary">Send broadcasts emulating those sent by the stock music player to work with 3rd party lockscreen controls, widgets, etc.</string>

View File

@ -181,7 +181,7 @@ public final class CoverBitmap {
int bitmapWidth = Math.max(coverWidth, boxWidth);
int bitmapHeight = Math.max(coverHeight, boxHeight);
Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.RGB_565);
Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
if (cover != null) {
@ -267,7 +267,7 @@ public final class CoverBitmap {
int bitmapWidth = horizontal ? coverWidth + boxWidth : Math.max(coverWidth, boxWidth);
int bitmapHeight = horizontal ? Math.max(coverHeight, boxHeight) : coverHeight + boxHeight;
Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.RGB_565);
Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
if (cover != null) {
@ -322,7 +322,7 @@ public final class CoverBitmap {
float scale = Math.min((float)width / sourceWidth, (float)height / sourceHeight);
sourceWidth *= scale;
sourceHeight *= scale;
return Bitmap.createScaledBitmap(source, sourceWidth, sourceHeight, false);
return Bitmap.createScaledBitmap(source, sourceWidth, sourceHeight, true);
}
/**

View File

@ -0,0 +1,279 @@
/*
* Copyright (C) 2015 Adrian Ulrich <adrian@blinkenlights.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ch.blinkenlights.android.vanilla;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.LruCache;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class CoverCache {
/**
* Returned size of small album covers
*/
public final static int SIZE_SMALL = 96;
/**
* Returned size of large (cover view) album covers
*/
public final static int SIZE_LARGE = 600;
/**
* Use all cover providers to load cover art
*/
public static final int COVER_MODE_ALL = 0xF;
/**
* Use androids builtin cover mechanism to load covers
*/
public static final int COVER_MODE_ANDROID = 0x1;
/**
* Use vanilla musics cover load mechanism
*/
public static final int COVER_MODE_VANILLA = 0x2;
/**
* Use vanilla musics SHADOW cover load mechanism
*/
public static final int COVER_MODE_SHADOW = 0x4;
/**
* Shared LRU cache class
*/
private static BitmapLruCache sBitmapLruCache;
/**
* Bitmask on how we are going to load coverart
*/
public static int mCoverLoadMode = 0;
/**
* Constructs a new BitmapCache object
* Will initialize the internal LRU cache on first call
*
* @param context A context to use
*/
public CoverCache(Context context) {
if (sBitmapLruCache == null) {
sBitmapLruCache = new BitmapLruCache(context, 6*1024*1024);
}
}
/**
* Returns a (possibly uncached) cover for the song - may return null if the song has no cover
*
* @param key The cache key to use for storing a generated cover
* @param song The song used to identify the artwork to load
*/
public Bitmap getCoverFromSong(CoverKey key, Song song) {
Bitmap cover = getCachedCover(key);
if (cover == null) {
cover = sBitmapLruCache.createBitmap(song, key.coverSize*key.coverSize);
if (cover != null) {
putCover(key, cover);
}
}
return cover;
}
/**
* Returns a cached version of the cover. Will return null if nothing was cached
*
* @param key The cache key to use
*/
public Bitmap getCachedCover(CoverKey key) {
return sBitmapLruCache.get(key);
}
/**
* Stores a new entry in the cache
*
* @param key The cache key to use
* @param cover The bitmap to store
*/
public void putCover(CoverKey key, Bitmap cover) {
sBitmapLruCache.put(key, cover);
}
/**
* Deletes all items hold in the LRU cache
*/
public static void evictAll() {
if (sBitmapLruCache != null) {
sBitmapLruCache.evictAll();
}
}
/**
* Object used as cache key. Objects with the same
* media type, id and size are considered to be equal
*/
public static class CoverKey {
public final int coverSize;
public final int mediaType;
public final long mediaId;
CoverKey(int mediaType, long mediaId, int coverSize) {
this.mediaType = mediaType;
this.mediaId = mediaId;
this.coverSize = coverSize;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CoverKey
&& this.mediaId == ((CoverKey)obj).mediaId
&& this.mediaType == ((CoverKey)obj).mediaType
&& this.coverSize == ((CoverKey)obj).coverSize) {
return true;
}
return false;
}
@Override
public int hashCode() {
return this.mediaType*10 + (int)this.mediaId + this.coverSize * (int)1e5;
}
}
/**
* The LRU cache implementation, using the CoverKey as key to store Bitmap objects
*
* Note that the implementation does not override create() in order to enable
* the use of fetch-if-cached functions: createBitmap() is therefore called
* by CoverCache itself.
*/
private static class BitmapLruCache extends LruCache<CoverKey, Bitmap> {
/**
* Context required for content resolver
*/
private final Context mContext;
/**
* Filenames we consider to be cover art
*/
private final static String[] coverNames = { "cover.jpg", "cover.png", "album.jpg", "album.png", "artwork.jpg", "artwork.png", "art.jpg", "art.png" };
/**
* Creates a new in-memory LRU cache
*
* @param context the application context
* @param int the lru cache size in bytes
*/
public BitmapLruCache(Context context, int size) {
super(size);
mContext = context;
}
/**
* Returns the cache size in bytes, not objects
*/
@Override
protected int sizeOf(CoverKey key, Bitmap value) {
return value.getByteCount();
}
/**
* Attempts to create a new bitmap object for given song.
* Returns null if no cover art was found
*
* @param song the function will search for artwork of this object
* @param maxPxCount the maximum amount of pixels to return (30*30 = 900)
*/
public Bitmap createBitmap(Song song, long maxPxCount) {
if (song.id < 0) {
// Unindexed song: return early
return null;
}
try {
InputStream inputStream = null;
InputStream sampleInputStream = null; // same as inputStream but used for getSampleSize
if ((CoverCache.mCoverLoadMode & CoverCache.COVER_MODE_VANILLA) != 0) {
String basePath = (new File(song.path)).getParentFile().getAbsolutePath(); // parent dir of the currently playing file
for (String coverFile: coverNames) {
File guessedFile = new File( basePath + "/" + coverFile);
if (guessedFile.exists() && !guessedFile.isDirectory()) {
inputStream = new FileInputStream(guessedFile);
sampleInputStream = new FileInputStream(guessedFile);
break;
}
}
}
if (inputStream == null && (CoverCache.mCoverLoadMode & CoverCache.COVER_MODE_SHADOW) != 0) {
String shadowPath = "/sdcard/Music/.vanilla/"+(song.artist.replaceAll("/", "_"))+"/"+(song.album.replaceAll("/", "_"))+".jpg";
File guessedFile = new File(shadowPath);
if (guessedFile.exists() && !guessedFile.isDirectory()) {
inputStream = new FileInputStream(guessedFile);
sampleInputStream = new FileInputStream(guessedFile);
}
}
if (inputStream == null && (CoverCache.mCoverLoadMode & CoverCache.COVER_MODE_ANDROID) != 0) {
Uri uri = Uri.parse("content://media/external/audio/media/" + song.id + "/albumart");
ContentResolver res = mContext.getContentResolver();
inputStream = res.openInputStream(uri);
sampleInputStream = res.openInputStream(uri);
}
if (inputStream != null) {
BitmapFactory.Options bopts = new BitmapFactory.Options();
bopts.inPreferredConfig = Bitmap.Config.RGB_565;
bopts.inJustDecodeBounds = true;
final int inSampleSize = getSampleSize(sampleInputStream, bopts, maxPxCount);
/* reuse bopts: we are now REALLY going to decode the image */
bopts.inJustDecodeBounds = false;
bopts.inSampleSize = inSampleSize;
return BitmapFactory.decodeStream(inputStream, null, bopts);
}
} catch (Exception e) {
// no cover art found
Log.v("VanillaMusic", "Loading coverart for "+song+" failed with exception "+e);
}
// failed!
return null;
}
/**
* Guess a good sampleSize value for given inputStream
*
* @param inputStream the input stream to read from
* @param bopts the bitmap options to use
* @param maxPxCount how many pixels we are returning at most
*/
private static int getSampleSize(InputStream inputStream, BitmapFactory.Options bopts, long maxPxCount) {
int sampleSize = 1; /* default sample size */
BitmapFactory.decodeStream(inputStream, null, bopts);
long hasPixels = bopts.outHeight * bopts.outWidth;
if(hasPixels > maxPxCount) {
sampleSize = Math.round((int)Math.sqrt((float) hasPixels / (float) maxPxCount));
}
return sampleSize;
}
}
}

View File

@ -365,16 +365,14 @@ public final class CoverView extends View implements Handler.Callback {
Context context = getContext();
Bitmap cover = song == null ? null : song.getCover(context);
if (cover == null && style == CoverBitmap.STYLE_NO_INFO) {
Bitmap def = mDefaultCover;
if (def == null) {
mDefaultCover = def = CoverBitmap.generateDefaultCover(context, getWidth(), getHeight());
if (cover == null && style != CoverBitmap.STYLE_OVERLAPPING_BOX) {
if (mDefaultCover == null) {
mDefaultCover = CoverBitmap.generateDefaultCover(context, getWidth(), getHeight());
}
mBitmaps[i] = def;
} else {
mBitmaps[i] = CoverBitmap.createBitmap(context, style, cover, song, getWidth(), getHeight());
cover = mDefaultCover;
}
mBitmaps[i] = CoverBitmap.createBitmap(context, style, cover, song, getWidth(), getHeight());
postInvalidate();
}

View File

@ -27,64 +27,64 @@ import android.widget.CheckedTextView;
import android.widget.Checkable;
public class DraggableRow extends LinearLayout implements Checkable {
private boolean mShowCheckBox;
private boolean mHighlighted;
/**
* True if the checkbox is checked
*/
private boolean mChecked;
private float mDensity;
private static int sWidth = -1;
/**
* True if setupLayout has been called
*/
private boolean mLayoutSet;
private TextView mTextView;
private CheckedTextView mCheckBox;
private View mPmark;
private ImageView mDragger;
private LazyCoverView mCoverView;
// Hardcoded sizes of elements in DPI
// MUST match definition in draggable_row
private final int DPI_PMARK = 4;
private final int DPI_CHECKBOX = 44;
private final int DPI_DRAGGER = 44;
private final int DPI_SLACK = 1; // safety margin
/**
* Layout types for use with setupLayout
*/
public static final int LAYOUT_CHECKBOXES = 1;
public static final int LAYOUT_COVERVIEW = 2;
public DraggableRow(Context context, AttributeSet attrs) {
super(context, attrs);
mDensity = context.getResources().getDisplayMetrics().density;
}
@Override
public void onFinishInflate() {
mCheckBox = (CheckedTextView)this.findViewById(R.id.checkbox);
mTextView = (TextView)this.findViewById(R.id.text);
mPmark = (View)this.findViewById(R.id.pmark);
mDragger = (ImageView)this.findViewById(R.id.dragger);
setupTextView(false);
}
@Override
protected void onSizeChanged (int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (sWidth != w) {
sWidth = w;
// This view was drawn with an incorrect with
// fix it and force a redraw
setupTextView(true);
}
mCheckBox = (CheckedTextView)this.findViewById(R.id.checkbox);
mTextView = (TextView)this.findViewById(R.id.text);
mPmark = (View)this.findViewById(R.id.pmark);
mDragger = (ImageView)this.findViewById(R.id.dragger);
mCoverView = (LazyCoverView)this.findViewById(R.id.cover);
super.onFinishInflate();
}
/**
* Resizes our textview to the correct size
* @param redraw invalidates the current view if TRUE
* Sets up commonly used layouts - can only be called once per view
*
* @param type the layout type to use
*/
private void setupTextView(boolean redraw) {
int pixelUsed = (int)((DPI_SLACK + DPI_PMARK + DPI_DRAGGER + (mShowCheckBox ? DPI_CHECKBOX : 0)) * mDensity);
int pixelFree = sWidth - pixelUsed;
if (pixelFree > 0)
mTextView.setWidth(pixelFree);
if (redraw == true)
this.invalidate();
public void setupLayout(int type) {
if (!mLayoutSet) {
switch (type) {
case LAYOUT_CHECKBOXES:
mCheckBox.setVisibility(View.VISIBLE);
showDragger(true);
break;
case LAYOUT_COVERVIEW:
mCoverView.setVisibility(View.VISIBLE);
showDragger(true);
break;
default:
break; // do not care
}
mLayoutSet = true;
}
}
@Override
@ -108,26 +108,12 @@ public class DraggableRow extends LinearLayout implements Checkable {
* @param state Enable or disable highlighting
*/
public void highlightRow(boolean state) {
if (mHighlighted != state) {
mPmark.setVisibility( state ? View.VISIBLE : View.INVISIBLE );
mHighlighted = state;
}
}
/**
* Changes the visibility of the checkbox
* @param state show or destroy the checkbox
*/
public void showCheckBox(boolean state) {
if (mShowCheckBox != state) {
mCheckBox.setVisibility( state ? View.VISIBLE : View.GONE);
mShowCheckBox = state;
setupTextView(true);
}
mPmark.setVisibility( state ? View.VISIBLE : View.INVISIBLE );
}
/**
* Change visibility of dragger element
*
* @param state shows or hides the dragger
*/
public void showDragger(boolean state) {
@ -141,4 +127,11 @@ public class DraggableRow extends LinearLayout implements Checkable {
return mTextView;
}
/**
* Returns an instance of our coverview
*/
public LazyCoverView getCoverView() {
return mCoverView;
}
}

View File

@ -188,13 +188,6 @@ public class FileSystemAdapter
return pos;
}
private static class ViewHolder {
public int id;
public TextView text;
public View divider;
public ImageView arrow;
}
@Override
public View getView(int pos, View convertView, ViewGroup parent)
{
@ -205,7 +198,7 @@ public class FileSystemAdapter
view = mInflater.inflate(R.layout.library_row_expandable, null);
holder = new ViewHolder();
holder.text = (TextView)view.findViewById(R.id.text);
holder.divider = view.findViewById(R.id.divider);
holder.divider = (View)view.findViewById(R.id.divider);
holder.arrow = (ImageView)view.findViewById(R.id.arrow);
holder.text.setOnClickListener(this);
holder.arrow.setOnClickListener(this);
@ -219,8 +212,8 @@ public class FileSystemAdapter
boolean isDirectory = file.isDirectory();
holder.id = pos;
holder.text.setText(file.getName());
holder.divider.setVisibility(isDirectory ? View.VISIBLE : View.GONE);
holder.arrow.setVisibility(isDirectory ? View.VISIBLE : View.GONE);
holder.divider.setVisibility(isDirectory ? View.VISIBLE : View.GONE);
holder.text.setCompoundDrawablesWithIntrinsicBounds(isDirectory ? mFolderIcon : null, null, null, null);
return view;
}
@ -295,11 +288,11 @@ public class FileSystemAdapter
public Intent createData(View view)
{
ViewHolder holder = (ViewHolder)view.getTag();
File file = mFiles[holder.id];
File file = mFiles[(int)holder.id];
Intent intent = new Intent();
intent.putExtra(LibraryAdapter.DATA_TYPE, MediaUtils.TYPE_FILE);
intent.putExtra(LibraryAdapter.DATA_ID, (long)holder.id);
intent.putExtra(LibraryAdapter.DATA_ID, holder.id);
intent.putExtra(LibraryAdapter.DATA_TITLE, holder.text.getText().toString());
intent.putExtra(LibraryAdapter.DATA_EXPANDABLE, file.isDirectory());

View File

@ -45,13 +45,6 @@ public class FilebrowserStartAdapter
mInflater = (LayoutInflater)activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
private static class ViewHolder {
public int id;
public TextView text;
public View divider;
public ImageView arrow;
}
@Override
public View getView(int pos, View convertView, ViewGroup parent) {
View view;
@ -82,7 +75,7 @@ public class FilebrowserStartAdapter
@Override
public void onClick(View view) {
ViewHolder holder = (ViewHolder)((View)view.getParent()).getTag();
mActivity.onDirectoryClicked(holder.id);
mActivity.onDirectoryClicked((int)holder.id);
}
}

View File

@ -428,9 +428,6 @@ public class FullPlaybackActivity extends PlaybackActivity
dialog.create().show();
}
break;
case MENU_SHOW_QUEUE:
startActivity(new Intent(this, ShowQueueActivity.class));
break;
default:
return super.onOptionsItemSelected(item);
}

View File

@ -0,0 +1,217 @@
/*
* Copyright (C) 2015 Adrian Ulrich <adrian@blinkenlights.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ch.blinkenlights.android.vanilla;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.AttributeSet;
import android.widget.ImageView;
/**
* LazyCoverView implements a 'song-aware' ImageView
*
* View updates should be triggered via setCover(type, id) to
* instruct the view to load the cover from its own LRU cache.
*
* The cover will automatically be fetched & scaled in a background
* thread on cache miss
*/
public class LazyCoverView extends ImageView
implements Handler.Callback
{
/**
* Context of constructor
*/
private Context mContext;
/**
* UI Thread handler
*/
private static Handler sUiHandler;
/**
* Worker thread handler
*/
private static Handler sHandler;
/**
* The fallback cover image resource encoded as bitmap
*/
private static Bitmap sFallbackBitmap;
/**
* Cover LRU cache LRU cache
*/
private static CoverCache sCoverCache;
/**
* The cover key we are expected to draw
*/
private CoverCache.CoverKey mExpectedKey;
/**
* Cover message we are passing around using mHandler
*/
private static class CoverMsg {
public CoverCache.CoverKey key;
public LazyCoverView view; // The view we are updating
CoverMsg(CoverCache.CoverKey key, LazyCoverView view) {
this.key = key;
this.view = view;
}
/**
* Returns true if the view still requires updating
*/
public boolean isRecent() {
return this.key.equals(this.view.mExpectedKey);
}
}
/**
* Constructor of class inflated from XML
*
* @param context The context of the calling activity
* @param attributes attributes passed by the xml inflater
*/
public LazyCoverView(Context context, AttributeSet attributes) {
super(context, attributes);
mContext = context;
}
/**
* Setup the handler of this view instance. This function
* must be called before calling setCover().
*
* @param looper The worker thread to use for image scaling
*/
public void setup(Looper looper) {
if (sCoverCache == null) {
sCoverCache = new CoverCache(mContext);
}
if (sFallbackBitmap == null) {
sFallbackBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.fallback_cover);
}
if (sUiHandler == null) {
sUiHandler = new Handler(this);
}
if (sHandler == null || sHandler.getLooper().equals(looper) == false) {
sHandler = new Handler(looper, this);
}
}
/**
* mHandler and mUiHandler callbacks
*/
private static final int MSG_CACHE_COVER = 60;
private static final int MSG_DRAW_COVER = 61;
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_CACHE_COVER: {
CoverMsg payload = (CoverMsg)message.obj;
if (payload.isRecent() == false) {
// This RPC is already obsoleted: drop it
break;
}
Bitmap bitmap = sCoverCache.getCachedCover(payload.key);
if (bitmap == null) {
Song song = MediaUtils.getSongByTypeId(mContext.getContentResolver(), payload.key.mediaType, payload.key.mediaId);
if (song != null) {
bitmap = sCoverCache.getCoverFromSong(payload.key, song);
}
if (bitmap == null) {
bitmap = sFallbackBitmap;
}
sCoverCache.putCover(payload.key, bitmap);
}
sUiHandler.sendMessage(sUiHandler.obtainMessage(MSG_DRAW_COVER, payload));
break;
}
case MSG_DRAW_COVER: {
CoverMsg payload = (CoverMsg)message.obj;
// We run in the UI-Thread like setCover()
// and do not need locking: checking if the payload
// is still recent is sufficient.
if (payload.isRecent()) {
payload.view.drawFromCache(payload.key, true);
}
}
default:
return false;
}
return true;
}
/**
* Attempts to set the image of this cover
* Must be called from an UI thread
*
* @param type The Media type
* @param id The id of this media type to query
*/
public void setCover(int type, long id) {
mExpectedKey = new CoverCache.CoverKey(type, id, CoverCache.SIZE_SMALL);
if (drawFromCache(mExpectedKey, false) == false) {
int delay = 1;
if (sHandler.hasMessages(MSG_CACHE_COVER)) {
// User is probably scrolling fast as there is already a queued resize job
// wait 200ms as this view will most likely be obsolete soon anyway.
// This frees us from scaling bitmaps we are never going to show
delay = 200;
}
CoverMsg payload = new CoverMsg(mExpectedKey, this);
sHandler.sendMessageDelayed(sHandler.obtainMessage(MSG_CACHE_COVER, payload), delay);
}
}
/**
* Updates the view with a cached bitmap
* A fallback image will be used on cache miss
*
* @param payload The cover message containing the cache key and view to use
*/
public boolean drawFromCache(CoverCache.CoverKey key, boolean fadeIn) {
boolean cacheHit = true;
Bitmap bitmap = sCoverCache.getCachedCover(key);
if (bitmap == null) {
cacheHit = false;
}
if (fadeIn) {
TransitionDrawable td = new TransitionDrawable(new Drawable[] {
getDrawable(),
(new BitmapDrawable(getResources(), bitmap))
});
setImageDrawable(td);
td.startTransition(120);
} else {
setImageBitmap(bitmap);
}
return cacheHit;
}
}

View File

@ -501,6 +501,23 @@ public class LibraryActivity
}
}
/**
* Updates mCover with the new bitmap, running in the UI thread
*
* @param cover the cover to set, will use a fallback drawable if null
*/
private void updateCover(final Bitmap cover) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (cover == null)
mCover.setImageResource(R.drawable.fallback_cover);
else
mCover.setImageBitmap(cover);
}
});
}
@Override
public void onClick(View view)
{
@ -778,6 +795,9 @@ public class LibraryActivity
controls.setActionView(mActionControls);
controls.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
// Call super after adding the now-playing view as this should be the first item
super.onCreateOptionsMenu(menu);
mSearchMenuItem = menu.add(0, MENU_SEARCH, 0, R.string.search).setIcon(R.drawable.ic_menu_search);
mSearchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW | MenuItem.SHOW_AS_ACTION_ALWAYS);
SearchView mSearchView = new SearchView(getActionBar().getThemedContext());
@ -785,7 +805,9 @@ public class LibraryActivity
mSearchMenuItem.setActionView(mSearchView);
menu.add(0, MENU_SORT, 0, R.string.sort_by).setIcon(R.drawable.ic_menu_sort_alphabetically);
return super.onCreateOptionsMenu(menu);
menu.add(0, MENU_SHOW_QUEUE, 0, R.string.show_queue);
return true;
}
@Override
@ -864,7 +886,11 @@ public class LibraryActivity
/**
* Save the current page, passed in arg1, to SharedPreferences.
*/
private static final int MSG_SAVE_PAGE = 12;
private static final int MSG_SAVE_PAGE = 40;
/**
* Updates mCover using a background thread
*/
private static final int MSG_UPDATE_COVER = 41;
@Override
public boolean handleMessage(Message message)
@ -876,6 +902,16 @@ public class LibraryActivity
editor.commit();
break;
}
case MSG_UPDATE_COVER: {
Bitmap cover = null;
Song song = (Song)message.obj;
if (song != null) {
cover = song.getCover(this);
}
// Dispatch view update to UI thread
updateCover(cover);
break;
}
default:
return super.handleMessage(message);
}
@ -905,8 +941,6 @@ public class LibraryActivity
super.onSongChange(song);
if (mTitle != null) {
Bitmap cover = null;
if (song == null) {
if (mActionControls == null) {
mTitle.setText(R.string.none);
@ -914,7 +948,6 @@ public class LibraryActivity
} else {
mTitle.setText(null);
mArtist.setText(null);
mCover.setImageDrawable(null);
return;
}
} else {
@ -923,16 +956,12 @@ public class LibraryActivity
String artist = song.artist == null ? res.getString(R.string.unknown) : song.artist;
mTitle.setText(title);
mArtist.setText(artist);
cover = song.getCover(this);
}
if (Song.mCoverLoadMode == 0)
mCover.setVisibility(View.GONE);
else if (cover == null)
mCover.setImageResource(R.drawable.fallback_cover);
else
mCover.setImageBitmap(cover);
mCover.setVisibility(CoverCache.mCoverLoadMode == 0 ? View.GONE : View.VISIBLE);
mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_COVER, song));
}
}
@Override

View File

@ -43,6 +43,7 @@ import android.util.LruCache;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.LinearLayout;
import java.util.Arrays;
/**
@ -53,7 +54,7 @@ public class LibraryPagerAdapter
implements Handler.Callback
, ViewPager.OnPageChangeListener
, View.OnCreateContextMenuListener
, AdapterView.OnItemClickListener
, View.OnClickListener
{
/**
* The number of unique list types. The number of visible lists may be
@ -162,9 +163,9 @@ public class LibraryPagerAdapter
* song limiters.
*/
private String mHeaderText;
private TextView mArtistHeader;
private TextView mAlbumHeader;
private TextView mSongHeader;
private LinearLayout mArtistHeader;
private LinearLayout mAlbumHeader;
private LinearLayout mSongHeader;
/**
* The current filter text, or null if none.
*/
@ -307,30 +308,31 @@ public class LibraryPagerAdapter
LibraryActivity activity = mActivity;
LayoutInflater inflater = activity.getLayoutInflater();
LibraryAdapter adapter;
TextView header = null;
LinearLayout header = null;
Looper looper = mWorkerHandler.getLooper();
switch (type) {
case MediaUtils.TYPE_ARTIST:
adapter = mArtistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ARTIST, null);
adapter = mArtistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ARTIST, null, looper);
mArtistAdapter.setExpandable(mSongsPosition != -1 || mAlbumsPosition != -1);
mArtistHeader = header = (TextView)inflater.inflate(R.layout.library_row, null);
mArtistHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row_expandable, null);
break;
case MediaUtils.TYPE_ALBUM:
adapter = mAlbumAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ALBUM, mPendingAlbumLimiter);
adapter = mAlbumAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ALBUM, mPendingAlbumLimiter, looper);
mAlbumAdapter.setExpandable(mSongsPosition != -1);
mPendingAlbumLimiter = null;
mAlbumHeader = header = (TextView)inflater.inflate(R.layout.library_row, null);
mAlbumHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row_expandable, null);
break;
case MediaUtils.TYPE_SONG:
adapter = mSongAdapter = new MediaAdapter(activity, MediaUtils.TYPE_SONG, mPendingSongLimiter);
adapter = mSongAdapter = new MediaAdapter(activity, MediaUtils.TYPE_SONG, mPendingSongLimiter, looper);
mPendingSongLimiter = null;
mSongHeader = header = (TextView)inflater.inflate(R.layout.library_row, null);
mSongHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row_expandable, null);
break;
case MediaUtils.TYPE_PLAYLIST:
adapter = mPlaylistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_PLAYLIST, null);
adapter = mPlaylistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_PLAYLIST, null, looper);
break;
case MediaUtils.TYPE_GENRE:
adapter = mGenreAdapter = new MediaAdapter(activity, MediaUtils.TYPE_GENRE, null);
adapter = mGenreAdapter = new MediaAdapter(activity, MediaUtils.TYPE_GENRE, null, looper);
mGenreAdapter.setExpandable(mSongsPosition != -1);
break;
case MediaUtils.TYPE_FILE:
@ -343,11 +345,13 @@ public class LibraryPagerAdapter
view = (ListView)inflater.inflate(R.layout.listview, null);
view.setOnCreateContextMenuListener(this);
view.setOnItemClickListener(this);
view.setTag(type);
if (header != null) {
header.setText(mHeaderText);
header.setTag(type);
TextView headerText = (TextView)header.findViewById(R.id.text);
headerText.setText(mHeaderText);
headerText.setOnClickListener(this);
header.setTag(new ViewHolder()); // behave like a normal library row
view.addHeaderView(header);
}
view.setAdapter(adapter);
@ -466,11 +470,11 @@ public class LibraryPagerAdapter
public void setHeaderText(String text)
{
if (mArtistHeader != null)
mArtistHeader.setText(text);
((TextView)mArtistHeader.findViewById(R.id.text)).setText(text);
if (mAlbumHeader != null)
mAlbumHeader.setText(text);
((TextView)mAlbumHeader.findViewById(R.id.text)).setText(text);
if (mSongHeader != null)
mSongHeader.setText(text);
((TextView)mSongHeader.findViewById(R.id.text)).setText(text);
mHeaderText = text;
}
@ -819,6 +823,7 @@ public class LibraryPagerAdapter
*/
private static Intent createHeaderIntent(View header)
{
header = (View)header.getParent(); // tag is set on parent view of header
int type = (Integer)header.getTag();
Intent intent = new Intent();
intent.putExtra(LibraryAdapter.DATA_ID, LibraryAdapter.HEADER_ID);
@ -836,13 +841,12 @@ public class LibraryPagerAdapter
}
@Override
public void onItemClick(AdapterView<?> list, View view, int position, long id)
{
Intent intent = id == -1 ? createHeaderIntent(view) : mCurrentAdapter.createData(view);
public void onClick(View view) {
view = (View)view.getParent(); // get view of linear layout, not the click consumer
Intent intent = createHeaderIntent(view);
mActivity.onItemClicked(intent);
}
/**
* LRU implementation for filebrowser position cache
*/

View File

@ -28,6 +28,7 @@ import android.database.Cursor;
import android.database.DatabaseUtils;
import android.graphics.Color;
import android.net.Uri;
import android.os.Looper;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.text.Spannable;
@ -76,6 +77,7 @@ public class MediaAdapter
* The current data.
*/
private Cursor mCursor;
private Looper mLooper;
/**
* The type of media represented by this adapter. Must be one of the
* MediaUtils.FIELD_* constants. Determines which content provider to query for
@ -134,6 +136,11 @@ public class MediaAdapter
* If true, show the expander button on each row.
*/
private boolean mExpandable;
/**
* Defines the media type to use for this entry
* Setting this to MediaUtils.TYPE_INVALID disables cover artwork
*/
private int mCoverCacheType;
/**
* Construct a MediaAdapter representing the given <code>type</code> of
@ -145,12 +152,16 @@ public class MediaAdapter
* and what fields to display in the views.
* @param limiter An initial limiter to use
*/
public MediaAdapter(LibraryActivity activity, int type, Limiter limiter)
public MediaAdapter(LibraryActivity activity, int type, Limiter limiter, Looper looper)
{
mActivity = activity;
mType = type;
mLimiter = limiter;
mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mLooper = looper;
mCoverCacheType = MediaUtils.TYPE_INVALID;
String coverCacheKey = "0"; // SQL dummy entry
switch (type) {
case MediaUtils.TYPE_ARTIST:
@ -169,6 +180,8 @@ public class MediaAdapter
mSongSort = MediaUtils.ALBUM_SORT;
mSortEntries = new int[] { R.string.name, R.string.artist_album, R.string.year, R.string.number_of_tracks, R.string.date_added };
mSortValues = new String[] { "album_key %1$s", "artist_key %1$s,album_key %1$s", "minyear %1$s,album_key %1$s", "numsongs %1$s,album_key %1$s", "_id %1$s" };
mCoverCacheType = MediaUtils.TYPE_ALBUM;
coverCacheKey = BaseColumns._ID;
break;
case MediaUtils.TYPE_SONG:
mStore = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
@ -178,6 +191,8 @@ public class MediaAdapter
R.string.artist_year, R.string.album_track, R.string.year, R.string.date_added, R.string.song_playcount };
mSortValues = new String[] { "title_key %1$s", "artist_key %1$s,album_key %1$s,track %1$s", "artist_key %1$s,album_key %1$s,title_key %1$s",
"artist_key %1$s,year %1$s,track %1$s", "album_key %1$s,track %1s", "year %1$s,title_key %1$s", "_id %1$s", SORT_MAGIC_PLAYCOUNT };
mCoverCacheType = MediaUtils.TYPE_ALBUM;
coverCacheKey = MediaStore.Audio.Albums.ALBUM_ID;
break;
case MediaUtils.TYPE_PLAYLIST:
mStore = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI;
@ -199,9 +214,9 @@ public class MediaAdapter
}
if (mFields.length == 1)
mProjection = new String[] { BaseColumns._ID, mFields[0] };
mProjection = new String[] { BaseColumns._ID, coverCacheKey, mFields[0] };
else
mProjection = new String[] { BaseColumns._ID, mFields[mFields.length - 1], mFields[0] };
mProjection = new String[] { BaseColumns._ID, coverCacheKey, mFields[mFields.length - 1], mFields[0] };
}
/**
@ -252,7 +267,7 @@ public class MediaAdapter
// Magic sort mode: sort by playcount
if (sortStringRaw == SORT_MAGIC_PLAYCOUNT) {
ArrayList<Long> topSongs = (new PlayCountsHelper(mActivity)).getTopSongs();
ArrayList<Long> topSongs = (new PlayCountsHelper(mActivity)).getTopSongs(4096);
int sortWeight = -1 * topSongs.size(); // Sort mode is actually reversed (default: mostplayed -> leastplayed)
StringBuilder sb = new StringBuilder("CASE WHEN _id=0 THEN 0"); // include dummy statement in initial string -> topSongs may be empty
@ -396,15 +411,15 @@ public class MediaAdapter
switch (mType) {
case MediaUtils.TYPE_ARTIST:
fields = new String[] { cursor.getString(1) };
fields = new String[] { cursor.getString(2) };
data = String.format("%s=%d", MediaStore.Audio.Media.ARTIST_ID, id);
break;
case MediaUtils.TYPE_ALBUM:
fields = new String[] { cursor.getString(2), cursor.getString(1) };
fields = new String[] { cursor.getString(3), cursor.getString(2) };
data = String.format("%s=%d", MediaStore.Audio.Media.ALBUM_ID, id);
break;
case MediaUtils.TYPE_GENRE:
fields = new String[] { cursor.getString(1) };
fields = new String[] { cursor.getString(2) };
data = id;
break;
default:
@ -433,37 +448,31 @@ public class MediaAdapter
}
}
private static class ViewHolder {
public long id;
public String title;
public TextView text;
public ImageView arrow;
}
@Override
public View getView(int position, View view, ViewGroup parent)
{
ViewHolder holder;
if (view == null || mExpandable != view instanceof LinearLayout) {
if (view == null) {
// We must create a new view if we're not given a recycle view or
// if the recycle view has the wrong layout.
int layout = mExpandable ? R.layout.library_row_expandable : R.layout.library_row;
view = mInflater.inflate(layout, null);
view = mInflater.inflate(R.layout.library_row_expandable, null);
holder = new ViewHolder();
view.setTag(holder);
if (mExpandable) {
holder.text = (TextView)view.findViewById(R.id.text);
holder.arrow = (ImageView)view.findViewById(R.id.arrow);
holder.arrow.setOnClickListener(this);
} else {
holder.text = (TextView)view;
view.setLongClickable(true);
}
holder.text = (TextView)view.findViewById(R.id.text);
holder.divider = (View)view.findViewById(R.id.divider);
holder.arrow = (ImageView)view.findViewById(R.id.arrow);
holder.cover = (LazyCoverView)view.findViewById(R.id.cover);
holder.arrow.setOnClickListener(this);
holder.text.setOnClickListener(this);
holder.cover.setOnClickListener(this);
holder.divider.setVisibility(mExpandable ? View.VISIBLE : View.GONE);
holder.arrow.setVisibility(mExpandable ? View.VISIBLE : View.GONE);
holder.cover.setVisibility(mCoverCacheType != MediaUtils.TYPE_INVALID ? View.VISIBLE : View.GONE);
holder.cover.setup(mLooper);
} else {
holder = (ViewHolder)view.getTag();
}
@ -471,9 +480,10 @@ public class MediaAdapter
Cursor cursor = mCursor;
cursor.moveToPosition(position);
holder.id = cursor.getLong(0);
if (mFields.length > 1) {
String line1 = cursor.getString(1);
String line2 = cursor.getString(2);
long cacheId = cursor.getLong(1);
if (mFields.length > 2) {
String line1 = cursor.getString(2);
String line2 = cursor.getString(3);
if(line1 == null) { line1 = "???"; }
if(line2 == null) { line2 = "???"; }
SpannableStringBuilder sb = new SpannableStringBuilder(line1);
@ -483,12 +493,16 @@ public class MediaAdapter
holder.text.setText(sb);
holder.title = line1;
} else {
String title = cursor.getString(1);
String title = cursor.getString(2);
if(title == null) { title = "???"; }
holder.text.setText(title);
holder.title = title;
}
if (mCoverCacheType != MediaUtils.TYPE_INVALID) {
holder.cover.setCover(mCoverCacheType, cacheId);
}
return view;
}
@ -565,8 +579,7 @@ public class MediaAdapter
public void onClick(View view)
{
int id = view.getId();
if (mExpandable)
view = (View)view.getParent();
view = (View)view.getParent(); // get view of linear layout, not the click consumer
Intent intent = createData(view);
if (id == R.id.arrow) {
mActivity.onItemExpanded(intent);

View File

@ -405,6 +405,27 @@ public class MediaUtils {
sAllSongs = null;
}
/**
* Returns the first matching song (or NULL) of given type + id combination
*
* @param resolver A ContentResolver to use.
* @param type The MediaTye to query
* @param id The id of given type to query
*/
public static Song getSongByTypeId(ContentResolver resolver, int type, long id) {
Song song = new Song(-1);
QueryTask query = buildQuery(type, id, Song.FILLED_PROJECTION, null);
Cursor cursor = query.runQuery(resolver);
if (cursor != null) {
if (cursor.getCount() > 0) {
cursor.moveToPosition(0);
song.populate(cursor);
}
cursor.close();
}
return song.id == -1 ? null : song;
}
/**
* Returns a song randomly selected from all the songs in the Android
* MediaStore.

View File

@ -68,26 +68,31 @@ public class PlayCountsHelper extends SQLiteOpenHelper {
public void countSong(Song song) {
long id = Song.getId(song);
SQLiteDatabase dbh = this.getWritableDatabase();
SQLiteDatabase dbh = getWritableDatabase();
dbh.execSQL("INSERT OR IGNORE INTO "+TABLE_PLAYCOUNTS+" (type, type_id, playcount) VALUES ("+MediaUtils.TYPE_SONG+", "+id+", 0);"); // Creates row if not exists
dbh.execSQL("UPDATE "+TABLE_PLAYCOUNTS+" SET playcount=playcount+1 WHERE type="+MediaUtils.TYPE_SONG+" AND type_id="+id+";");
performGC(dbh, MediaUtils.TYPE_SONG);
dbh.close();
performGC(MediaUtils.TYPE_SONG);
}
/**
* Returns a sorted array list of most often listen song ids
*/
public ArrayList<Long> getTopSongs() {
public ArrayList<Long> getTopSongs(int limit) {
ArrayList<Long> payload = new ArrayList<Long>();
SQLiteDatabase dbh = this.getReadableDatabase();
Cursor cursor = dbh.rawQuery("SELECT type_id FROM "+TABLE_PLAYCOUNTS+" WHERE type="+MediaUtils.TYPE_SONG+" ORDER BY playcount DESC limit 4096", null);
SQLiteDatabase dbh = getReadableDatabase();
Cursor cursor = dbh.rawQuery("SELECT type_id FROM "+TABLE_PLAYCOUNTS+" WHERE type="+MediaUtils.TYPE_SONG+" AND playcount != 0 ORDER BY playcount DESC limit "+limit, null);
while (cursor.moveToNext()) {
payload.add(cursor.getLong(0));
}
cursor.close();
dbh.close();
return payload;
}
@ -96,7 +101,8 @@ public class PlayCountsHelper extends SQLiteOpenHelper {
* and checks them against Androids media database.
* Items not found in the media library are removed from the DBH's database
*/
private int performGC(SQLiteDatabase dbh, int type) {
private int performGC(int type) {
SQLiteDatabase dbh = getWritableDatabase();
ArrayList<Long> toCheck = new ArrayList<Long>(); // List of songs we are going to check
QueryTask query; // Reused query object
Cursor cursor; // recycled cursor
@ -119,6 +125,7 @@ public class PlayCountsHelper extends SQLiteOpenHelper {
cursor.close();
}
Log.v("VanillaMusic", "performGC: items removed="+removed);
dbh.close();
return removed;
}

View File

@ -183,7 +183,7 @@ public abstract class PlaybackActivity extends Activity
PlaybackService service = PlaybackService.get(this);
int state = service.playPause();
if ((state & PlaybackService.FLAG_ERROR) != 0)
Toast.makeText(this, service.getErrorMessage(), Toast.LENGTH_LONG).show();
showToast(service.getErrorMessage(), Toast.LENGTH_LONG);
setState(state);
}
@ -371,6 +371,9 @@ public abstract class PlaybackActivity extends Activity
case MENU_CLEAR_QUEUE:
PlaybackService.get(this).clearQueue();
break;
case MENU_SHOW_QUEUE:
startActivity(new Intent(this, ShowQueueActivity.class));
break;
default:
return false;
}
@ -451,7 +454,7 @@ public abstract class PlaybackActivity extends Activity
}
String message = getResources().getQuantityString(R.plurals.added_to_playlist, count, count, playlistTask.name);
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
showToast(message, Toast.LENGTH_SHORT);
}
/**
@ -485,9 +488,20 @@ public abstract class PlaybackActivity extends Activity
message = res.getString(R.string.deleted_item, intent.getStringExtra("title"));
}
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
showToast(message, Toast.LENGTH_SHORT);
}
/**
* Creates and displays a new toast message
*/
private void showToast(final String message, final int duration) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), message, duration).show();
}
});
}
/**

View File

@ -432,9 +432,9 @@ public final class PlaybackService extends Service
mScrobble = settings.getBoolean(PrefKeys.SCROBBLE, false);
mIdleTimeout = settings.getBoolean(PrefKeys.USE_IDLE_TIMEOUT, false) ? settings.getInt(PrefKeys.IDLE_TIMEOUT, 3600) : 0;
Song.mCoverLoadMode = settings.getBoolean(PrefKeys.COVERLOADER_ANDROID, true) ? Song.mCoverLoadMode | Song.COVER_MODE_ANDROID : Song.mCoverLoadMode & ~(Song.COVER_MODE_ANDROID);
Song.mCoverLoadMode = settings.getBoolean(PrefKeys.COVERLOADER_VANILLA, true) ? Song.mCoverLoadMode | Song.COVER_MODE_VANILLA : Song.mCoverLoadMode & ~(Song.COVER_MODE_VANILLA);
Song.mCoverLoadMode = settings.getBoolean(PrefKeys.COVERLOADER_SHADOW , true) ? Song.mCoverLoadMode | Song.COVER_MODE_SHADOW : Song.mCoverLoadMode & ~(Song.COVER_MODE_SHADOW);
CoverCache.mCoverLoadMode = settings.getBoolean(PrefKeys.COVERLOADER_ANDROID, true) ? CoverCache.mCoverLoadMode | CoverCache.COVER_MODE_ANDROID : CoverCache.mCoverLoadMode & ~(CoverCache.COVER_MODE_ANDROID);
CoverCache.mCoverLoadMode = settings.getBoolean(PrefKeys.COVERLOADER_VANILLA, true) ? CoverCache.mCoverLoadMode | CoverCache.COVER_MODE_VANILLA : CoverCache.mCoverLoadMode & ~(CoverCache.COVER_MODE_VANILLA);
CoverCache.mCoverLoadMode = settings.getBoolean(PrefKeys.COVERLOADER_SHADOW , true) ? CoverCache.mCoverLoadMode | CoverCache.COVER_MODE_SHADOW : CoverCache.mCoverLoadMode & ~(CoverCache.COVER_MODE_SHADOW);
mHeadsetOnly = settings.getBoolean(PrefKeys.HEADSET_ONLY, false);
mCycleContinuousShuffling = settings.getBoolean(PrefKeys.CYCLE_CONTINUOUS_SHUFFLING, false);
@ -499,14 +499,14 @@ public final class PlaybackService extends Service
play();
}
} else if (ACTION_TOGGLE_PLAYBACK_DELAYED.equals(action)) {
if (mHandler.hasMessages(CALL_GO, Integer.valueOf(0))) {
mHandler.removeMessages(CALL_GO, Integer.valueOf(0));
if (mHandler.hasMessages(MSG_CALL_GO, Integer.valueOf(0))) {
mHandler.removeMessages(MSG_CALL_GO, Integer.valueOf(0));
Intent launch = new Intent(this, LibraryActivity.class);
launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
launch.setAction(Intent.ACTION_MAIN);
startActivity(launch);
} else {
mHandler.sendMessageDelayed(mHandler.obtainMessage(CALL_GO, 0, 0, Integer.valueOf(0)), 400);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CALL_GO, 0, 0, Integer.valueOf(0)), 400);
}
} else if (ACTION_NEXT_SONG.equals(action)) {
setCurrentSong(1);
@ -515,14 +515,14 @@ public final class PlaybackService extends Service
setCurrentSong(1);
play();
} else if (ACTION_NEXT_SONG_DELAYED.equals(action)) {
if (mHandler.hasMessages(CALL_GO, Integer.valueOf(1))) {
mHandler.removeMessages(CALL_GO, Integer.valueOf(1));
if (mHandler.hasMessages(MSG_CALL_GO, Integer.valueOf(1))) {
mHandler.removeMessages(MSG_CALL_GO, Integer.valueOf(1));
Intent launch = new Intent(this, LibraryActivity.class);
launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
launch.setAction(Intent.ACTION_MAIN);
startActivity(launch);
} else {
mHandler.sendMessageDelayed(mHandler.obtainMessage(CALL_GO, 1, 0, Integer.valueOf(1)), 400);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CALL_GO, 1, 0, Integer.valueOf(1)), 400);
}
} else if (ACTION_PREVIOUS_SONG.equals(action)) {
setCurrentSong(-1);
@ -808,14 +808,14 @@ public final class PlaybackService extends Service
mIdleTimeout = settings.getBoolean(PrefKeys.USE_IDLE_TIMEOUT, false) ? settings.getInt(PrefKeys.IDLE_TIMEOUT, 3600) : 0;
userActionTriggered();
} else if (PrefKeys.COVERLOADER_ANDROID.equals(key)) {
Song.mCoverLoadMode = settings.getBoolean(PrefKeys.COVERLOADER_ANDROID, true) ? Song.mCoverLoadMode | Song.COVER_MODE_ANDROID : Song.mCoverLoadMode & ~(Song.COVER_MODE_ANDROID);
Song.mFlushCoverCache = true;
CoverCache.mCoverLoadMode = settings.getBoolean(PrefKeys.COVERLOADER_ANDROID, true) ? CoverCache.mCoverLoadMode | CoverCache.COVER_MODE_ANDROID : CoverCache.mCoverLoadMode & ~(CoverCache.COVER_MODE_ANDROID);
CoverCache.evictAll();
} else if (PrefKeys.COVERLOADER_VANILLA.equals(key)) {
Song.mCoverLoadMode = settings.getBoolean(PrefKeys.COVERLOADER_VANILLA, true) ? Song.mCoverLoadMode | Song.COVER_MODE_VANILLA : Song.mCoverLoadMode & ~(Song.COVER_MODE_VANILLA);
Song.mFlushCoverCache = true;
CoverCache.mCoverLoadMode = settings.getBoolean(PrefKeys.COVERLOADER_VANILLA, true) ? CoverCache.mCoverLoadMode | CoverCache.COVER_MODE_VANILLA : CoverCache.mCoverLoadMode & ~(CoverCache.COVER_MODE_VANILLA);
CoverCache.evictAll();
} else if (PrefKeys.COVERLOADER_SHADOW.equals(key)) {
Song.mCoverLoadMode = settings.getBoolean(PrefKeys.COVERLOADER_SHADOW, true) ? Song.mCoverLoadMode | Song.COVER_MODE_SHADOW : Song.mCoverLoadMode & ~(Song.COVER_MODE_SHADOW);
Song.mFlushCoverCache = true;
CoverCache.mCoverLoadMode = settings.getBoolean(PrefKeys.COVERLOADER_SHADOW, true) ? CoverCache.mCoverLoadMode | CoverCache.COVER_MODE_SHADOW : CoverCache.mCoverLoadMode & ~(CoverCache.COVER_MODE_SHADOW);
CoverCache.evictAll();
} else if (PrefKeys.HEADSET_ONLY.equals(key)) {
mHeadsetOnly = settings.getBoolean(key, false);
if (mHeadsetOnly && isSpeakerOn())
@ -912,8 +912,8 @@ public final class PlaybackService extends Service
mState = state;
if (state != oldState) {
mHandler.sendMessage(mHandler.obtainMessage(PROCESS_STATE, oldState, state));
mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_CHANGE, state, 0));
mHandler.sendMessage(mHandler.obtainMessage(MSG_PROCESS_STATE, oldState, state));
mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_CHANGE, state, 0));
}
return state;
@ -940,7 +940,7 @@ public final class PlaybackService extends Service
mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
mHandler.removeMessages(ENTER_SLEEP_STATE);
mHandler.removeMessages(MSG_ENTER_SLEEP_STATE);
try {
if (mWakeLock != null && mWakeLock.isHeld() == false)
mWakeLock.acquire();
@ -961,7 +961,7 @@ public final class PlaybackService extends Service
// Delay entering deep sleep. This allows the headset
// button to continue to function for a short period after
// pausing and keeps the AudioFX session open
mHandler.sendEmptyMessageDelayed(ENTER_SLEEP_STATE, SLEEP_STATE_DELAY);
mHandler.sendEmptyMessageDelayed(MSG_ENTER_SLEEP_STATE, SLEEP_STATE_DELAY);
}
setupSensor();
@ -1228,11 +1228,11 @@ public final class PlaybackService extends Service
}
}
mHandler.removeMessages(PROCESS_SONG);
mHandler.removeMessages(MSG_PROCESS_SONG);
mMediaPlayerInitialized = false;
mHandler.sendMessage(mHandler.obtainMessage(PROCESS_SONG, song));
mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_CHANGE, -1, 0, song));
mHandler.sendMessage(mHandler.obtainMessage(MSG_PROCESS_SONG, song));
mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_CHANGE, -1, 0, song));
return song;
}
@ -1261,8 +1261,8 @@ public final class PlaybackService extends Service
mMediaPlayerInitialized = true;
// Cancel any pending gapless updates and re-send them
mHandler.removeMessages(GAPLESS_UPDATE);
mHandler.sendEmptyMessage(GAPLESS_UPDATE);
mHandler.removeMessages(MSG_GAPLESS_UPDATE);
mHandler.sendEmptyMessage(MSG_GAPLESS_UPDATE);
if (mPendingSeek != 0 && mPendingSeekSong == song.id) {
mMediaPlayer.seekTo(mPendingSeek);
@ -1287,7 +1287,7 @@ public final class PlaybackService extends Service
* This will stop after skipping 10 songs to avoid endless loops (queue full of broken stuff */
if(mTimeline.isEndOfQueue() == false && getSong(1) != null && (playing || (mSkipBroken > 0 && mSkipBroken < 10))) {
mSkipBroken++;
mHandler.sendMessageDelayed(mHandler.obtainMessage(SKIP_BROKEN_SONG, getTimelinePosition(), 0), 1000);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SKIP_BROKEN_SONG, getTimelinePosition(), 0), 1000);
}
}
@ -1303,7 +1303,8 @@ public final class PlaybackService extends Service
// Count this song as played
Song song = mTimeline.getSong(0);
mPlayCounts.countSong(song);
mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_PLAYCOUNTS, song));
if (finishAction(mState) == SongTimeline.FINISH_REPEAT_CURRENT) {
setCurrentSong(0);
@ -1378,84 +1379,85 @@ public final class PlaybackService extends Service
/**
* Releases mWakeLock and closes any open AudioFx sessions
*/
private static final int ENTER_SLEEP_STATE = 1;
private static final int MSG_ENTER_SLEEP_STATE = 1;
/**
* Run the given query and add the results to the timeline.
*
* obj is the QueryTask. arg1 is the add mode (one of SongTimeline.MODE_*)
*/
private static final int QUERY = 2;
private static final int MSG_QUERY = 2;
/**
* This message is sent with a delay specified by a user preference. After
* this delay, assuming no new IDLE_TIMEOUT messages cancel it, playback
* will be stopped.
*/
private static final int IDLE_TIMEOUT = 4;
private static final int MSG_IDLE_TIMEOUT = 4;
/**
* Decrease the volume gradually over five seconds, pausing when 0 is
* reached.
*
* arg1 should be the progress in the fade as a percentage, 1-100.
*/
private static final int FADE_OUT = 7;
private static final int MSG_FADE_OUT = 7;
/**
* If arg1 is 0, calls {@link PlaybackService#playPause()}.
* Otherwise, calls {@link PlaybackService#setCurrentSong(int)} with arg1.
*/
private static final int CALL_GO = 8;
private static final int BROADCAST_CHANGE = 10;
private static final int SAVE_STATE = 12;
private static final int PROCESS_SONG = 13;
private static final int PROCESS_STATE = 14;
private static final int SKIP_BROKEN_SONG = 15;
private static final int GAPLESS_UPDATE = 16;
private static final int MSG_CALL_GO = 8;
private static final int MSG_BROADCAST_CHANGE = 10;
private static final int MSG_SAVE_STATE = 12;
private static final int MSG_PROCESS_SONG = 13;
private static final int MSG_PROCESS_STATE = 14;
private static final int MSG_SKIP_BROKEN_SONG = 15;
private static final int MSG_GAPLESS_UPDATE = 16;
private static final int MSG_UPDATE_PLAYCOUNTS = 17;
@Override
public boolean handleMessage(Message message)
{
switch (message.what) {
case CALL_GO:
case MSG_CALL_GO:
if (message.arg1 == 0)
playPause();
else
setCurrentSong(message.arg1);
break;
case SAVE_STATE:
case MSG_SAVE_STATE:
// For unexpected terminations: crashes, task killers, etc.
// In most cases onDestroy will handle this
saveState(0);
break;
case PROCESS_SONG:
case MSG_PROCESS_SONG:
processSong((Song)message.obj);
break;
case QUERY:
case MSG_QUERY:
runQuery((QueryTask)message.obj);
break;
case IDLE_TIMEOUT:
case MSG_IDLE_TIMEOUT:
if ((mState & FLAG_PLAYING) != 0) {
mHandler.sendMessage(mHandler.obtainMessage(FADE_OUT, 0));
mHandler.sendMessage(mHandler.obtainMessage(MSG_FADE_OUT, 0));
}
break;
case FADE_OUT:
case MSG_FADE_OUT:
if (mFadeOut <= 0.0f) {
mIdleStart = SystemClock.elapsedRealtime();
unsetFlag(FLAG_PLAYING);
} else {
mFadeOut -= 0.01f;
mHandler.sendMessageDelayed(mHandler.obtainMessage(FADE_OUT, 0), 50);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_FADE_OUT, 0), 50);
}
refreshReplayGainValues(); /* Updates the volume using the new mFadeOut value */
break;
case PROCESS_STATE:
case MSG_PROCESS_STATE:
processNewState(message.arg1, message.arg2);
break;
case BROADCAST_CHANGE:
case MSG_BROADCAST_CHANGE:
broadcastChange(message.arg1, (Song)message.obj, message.getWhen());
break;
case ENTER_SLEEP_STATE:
case MSG_ENTER_SLEEP_STATE:
enterSleepState();
break;
case SKIP_BROKEN_SONG:
case MSG_SKIP_BROKEN_SONG:
/* Advance to next song if the user didn't already change.
* But we are restoring the Playing state in ANY case as we are most
* likely still stopped due to the error
@ -1467,11 +1469,15 @@ public final class PlaybackService extends Service
// Optimistically claim to have recovered from this error
mErrorMessage = null;
unsetFlag(FLAG_ERROR);
mHandler.sendMessage(mHandler.obtainMessage(CALL_GO, 0, 0));
mHandler.sendMessage(mHandler.obtainMessage(MSG_CALL_GO, 0, 0));
break;
case GAPLESS_UPDATE:
case MSG_GAPLESS_UPDATE:
triggerGaplessUpdate();
break;
case MSG_UPDATE_PLAYCOUNTS:
Song song = (Song)message.obj;
mPlayCounts.countSong(song);
break;
default:
return false;
}
@ -1604,10 +1610,10 @@ public final class PlaybackService extends Service
*/
public void userActionTriggered()
{
mHandler.removeMessages(FADE_OUT);
mHandler.removeMessages(IDLE_TIMEOUT);
mHandler.removeMessages(MSG_FADE_OUT);
mHandler.removeMessages(MSG_IDLE_TIMEOUT);
if (mIdleTimeout != 0)
mHandler.sendEmptyMessageDelayed(IDLE_TIMEOUT, mIdleTimeout * 1000);
mHandler.sendEmptyMessageDelayed(MSG_IDLE_TIMEOUT, mIdleTimeout * 1000);
if (mFadeOut != 1.0f) {
mFadeOut = 1.0f;
@ -1662,7 +1668,7 @@ public final class PlaybackService extends Service
*/
public void addSongs(QueryTask query)
{
mHandler.sendMessage(mHandler.obtainMessage(QUERY, query));
mHandler.sendMessage(mHandler.obtainMessage(MSG_QUERY, query));
}
/**
@ -1731,14 +1737,14 @@ public final class PlaybackService extends Service
@Override
public void timelineChanged()
{
mHandler.removeMessages(SAVE_STATE);
mHandler.sendEmptyMessageDelayed(SAVE_STATE, SAVE_STATE_DELAY);
mHandler.removeMessages(MSG_SAVE_STATE);
mHandler.sendEmptyMessageDelayed(MSG_SAVE_STATE, SAVE_STATE_DELAY);
// Trigger a gappless update for the new timeline
// This might get canceled if setCurrentSong() also fired a call
// to processSong();
mHandler.removeMessages(GAPLESS_UPDATE);
mHandler.sendEmptyMessageDelayed(GAPLESS_UPDATE, 100);
mHandler.removeMessages(MSG_GAPLESS_UPDATE);
mHandler.sendEmptyMessageDelayed(MSG_GAPLESS_UPDATE, 100);
ArrayList<PlaybackActivity> list = sActivities;
for (int i = list.size(); --i != -1; )

View File

@ -46,6 +46,8 @@ public class PlaylistActivity extends Activity
implements View.OnClickListener
, AbsListView.OnItemClickListener
, DialogInterface.OnClickListener
, DragSortListView.DropListener
, DragSortListView.RemoveListener
{
/**
* The SongTimeline play mode corresponding to each
@ -102,7 +104,8 @@ public class PlaylistActivity extends Activity
DragSortListView view = (DragSortListView)findViewById(R.id.list);
view.setOnItemClickListener(this);
view.setOnCreateContextMenuListener(this);
view.setDropListener(onDrop);
view.setDropListener(this);
view.setRemoveListener(this);
mListView = view;
View header = LayoutInflater.from(this).inflate(R.layout.playlist_buttons, null);
@ -287,12 +290,18 @@ public class PlaylistActivity extends Activity
* @param from the item index that was dragged
* @param to the index where the item was dropped
*/
private DragSortListView.DropListener onDrop =
new DragSortListView.DropListener() {
@Override
public void drop(int from, int to) {
mAdapter.moveItem(from, to);
}
};
@Override
public void drop(int from, int to) {
mAdapter.moveItem(from, to);
}
/**
* Fired from adapter listview if user fling-removed an item
* @param position The position of the removed item
*/
@Override
public void remove(int position) {
mAdapter.removeItem(position);
}
}

View File

@ -1,5 +1,6 @@
/*
* Copyright (C) 2011 Christopher Eby <kreed@kreed.org>
* Copyright (C) 2015 Adrian Ulrich <adrian@blinkenlights.ch>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@ -24,6 +25,7 @@ package ch.blinkenlights.android.vanilla;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
@ -38,7 +40,6 @@ import android.widget.CursorAdapter;
import android.widget.TextView;
import android.provider.MediaStore.Audio.Playlists.Members;
/**
* CursorAdapter backed by MediaStore playlists.
*/
@ -48,6 +49,7 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
MediaStore.Audio.Playlists.Members.TITLE,
MediaStore.Audio.Playlists.Members.ARTIST,
MediaStore.Audio.Playlists.Members.AUDIO_ID,
MediaStore.Audio.Playlists.Members.ALBUM_ID,
MediaStore.Audio.Playlists.Members.PLAY_ORDER,
};
@ -106,10 +108,16 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
public void bindView(View view, Context context, Cursor cursor)
{
DraggableRow dview = (DraggableRow)view;
dview.setupLayout(DraggableRow.LAYOUT_COVERVIEW);
dview.showDragger(mEditable);
TextView textView = dview.getTextView();
textView.setText(cursor.getString(1));
textView.setTag(cursor.getLong(3));
LazyCoverView cover = dview.getCoverView();
cover.setup(mWorkerHandler.getLooper());
cover.setCover(MediaUtils.TYPE_ALBUM, cursor.getLong(4));
}
/**
@ -166,15 +174,6 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
* @param from original position of item
* @param to destination of item
**/
public void moveItem(int from, int to) {
if (from == to)
return;
android.provider.MediaStore.Audio.Playlists.Members.moveItem(mContext.getContentResolver(), mPlaylistId , from, to);
mUiHandler.sendEmptyMessage(MSG_RUN_QUERY);
}
/* fixme: does the move-after-delete bug still exist in 4.x?
public void moveItem(int from, int to)
{
if (from == to)
@ -202,11 +201,11 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
order = 0;
} else {
cursor.moveToPosition(start - 1);
order = cursor.getLong(4) + 1;
order = cursor.getLong(5) + 1;
}
cursor.moveToPosition(end);
long endOrder = cursor.getLong(4);
long endOrder = cursor.getLong(5);
// clear the rows we are replacing
String[] args = new String[] { Long.toString(order), Long.toString(endOrder) };
@ -227,7 +226,7 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
changeCursor(runQuery(resolver));
}
*/
public void removeItem(int position)
{
ContentResolver resolver = mContext.getContentResolver();

View File

@ -96,6 +96,11 @@ public class RemoteControl {
// Create a copy of the cover art, since RemoteControlClient likes
// to recycle what we give it.
bitmap = bitmap.copy(Bitmap.Config.RGB_565, false);
} else {
// Some lockscreen implementations fail to clear the cover artwork
// if we send a null bitmap. We are creating a 16x16 transparent
// bitmap to work around this limitation.
bitmap = Bitmap.createBitmap(16, 16, Bitmap.Config.ARGB_8888);
}
editor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, bitmap);
}

View File

@ -81,11 +81,11 @@ public class SeekBarPreference extends DialogPreference implements SeekBar.OnSee
*/
private String getSummary(int value)
{
if ("shake_threshold".equals(getKey())) {
if (PrefKeys.SHAKE_THRESHOLD.equals(getKey())) {
return String.valueOf(value / 10.0f);
} else if("replaygain_bump".equals(getKey())) {
} else if(PrefKeys.REPLAYGAIN_BUMP.equals(getKey())) {
return String.format("%+.1fdB", 2*(value-75)/10f);
} else if("replaygain_untagged_debump".equals(getKey())) {
} else if(PrefKeys.REPLAYGAIN_UNTAGGED_DEBUMP.equals(getKey())) {
String summary = (String)mContext.getResources().getText(R.string.replaygain_untagged_debump_summary);
return String.format("%s %.1fdB", summary, (value-150)/10f);
} else {
@ -102,7 +102,10 @@ public class SeekBarPreference extends DialogPreference implements SeekBar.OnSee
mValueText.setText(getSummary(mValue));
SeekBar seekBar = (SeekBar)view.findViewById(R.id.seek_bar);
seekBar.setMax(150);
int maxValue = (PrefKeys.SHAKE_THRESHOLD.equals(getKey()) ? 300 : 150);
seekBar.setMax(maxValue);
seekBar.setProgress(mValue);
seekBar.setOnSeekBarChangeListener(this);

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2013-2014 Adrian Ulrich <adrian@blinkenlights.ch>
* Copyright (C) 2013-2015 Adrian Ulrich <adrian@blinkenlights.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -22,6 +22,7 @@ import java.util.ArrayList;
import android.app.Activity;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Message;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
@ -32,8 +33,10 @@ import android.widget.ListView;
import com.mobeta.android.dslv.DragSortListView;
public class ShowQueueActivity extends PlaybackActivity
implements DialogInterface.OnDismissListener
{
implements DialogInterface.OnDismissListener,
DragSortListView.DropListener,
DragSortListView.RemoveListener
{
private DragSortListView mListView;
private ShowQueueAdapter listAdapter;
private PlaybackService mService;
@ -48,10 +51,10 @@ public class ShowQueueActivity extends PlaybackActivity
mService = PlaybackService.get(this);
mListView = (DragSortListView) findViewById(R.id.list);
listAdapter = new ShowQueueAdapter(this, R.layout.draggable_row);
listAdapter = new ShowQueueAdapter(this, R.layout.draggable_row, mHandler.getLooper());
mListView.setAdapter(listAdapter);
mListView.setDropListener(onDrop);
mListView.setRemoveListener(onRemove);
mListView.setDropListener(this);
mListView.setRemoveListener(this);
mListView.setOnItemClickListener(new OnItemClickListener() {
@Override
@ -120,38 +123,32 @@ public class ShowQueueActivity extends PlaybackActivity
* @param from the item index that was dragged
* @param to the index where the item was dropped
*/
private DragSortListView.DropListener onDrop =
new DragSortListView.DropListener() {
@Override
public void drop(int from, int to) {
if (from != to) {
mService.moveSongPosition(from, to);
}
}
};
@Override
public void drop(int from, int to) {
if (from != to) {
mService.moveSongPosition(from, to);
}
}
/**
* Fired from adapter listview after user removed a song
* @param which index to remove from queue
*/
private DragSortListView.RemoveListener onRemove =
new DragSortListView.RemoveListener() {
@Override
public void remove(int which) {
mService.removeSongPosition(which);
}
};
@Override
public void remove(int which) {
mService.removeSongPosition(which);
}
/**
* Fired if user dismisses the create-playlist dialog
*
* @param dialogInterface the dismissed interface dialog
* Saves the current queue as a playlist
*/
protected static final int MSG_SAVE_QUEUE_AS_PLAYLIST = 30;
@Override
public void onDismiss(DialogInterface dialogInterface) {
NewPlaylistDialog dialog = (NewPlaylistDialog)dialogInterface;
if (dialog.isAccepted()) {
String playlistName = dialog.getText();
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_SAVE_QUEUE_AS_PLAYLIST:
String playlistName = (String)message.obj;
long playlistId = Playlist.createPlaylist(getContentResolver(), playlistName);
PlaylistTask playlistTask = new PlaylistTask(playlistId, playlistName);
playlistTask.audioIds = new ArrayList<Long>();
@ -163,7 +160,26 @@ public class ShowQueueActivity extends PlaybackActivity
break;
playlistTask.audioIds.add(song.id);
}
mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_TO_PLAYLIST, playlistTask));
addToPlaylist(playlistTask);
break;
default:
return super.handleMessage(message);
}
return true;
}
/**
* Fired if user dismisses the create-playlist dialog
*
* @param dialogInterface the dismissed interface dialog
*/
@Override
public void onDismiss(DialogInterface dialogInterface) {
NewPlaylistDialog dialog = (NewPlaylistDialog)dialogInterface;
if (dialog.isAccepted()) {
String playlistName = dialog.getText();
mHandler.sendMessage(mHandler.obtainMessage(MSG_SAVE_QUEUE_AS_PLAYLIST, playlistName));
}
}

View File

@ -19,6 +19,7 @@ package ch.blinkenlights.android.vanilla;
import android.content.Context;
import android.app.Activity;
import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
@ -35,14 +36,16 @@ public class ShowQueueAdapter
extends ArrayAdapter<Song>
{
int mResource;
int mHighlightRow;
Context mContext;
private int mResource;
private int mHighlightRow;
private Context mContext;
private Looper mLooper;
public ShowQueueAdapter(Context context, int resource) {
public ShowQueueAdapter(Context context, int resource, Looper looper) {
super(context, resource);
mResource = resource;
mContext = context;
mLooper = looper;
mHighlightRow = -1;
}
@ -64,6 +67,8 @@ public class ShowQueueAdapter
} else {
LayoutInflater inflater = ((Activity)mContext).getLayoutInflater();
row = (DraggableRow)inflater.inflate(mResource, parent, false);
row.setupLayout(DraggableRow.LAYOUT_COVERVIEW);
row.getCoverView().setup(mLooper);
}
Song song = getItem(position);
@ -74,6 +79,7 @@ public class ShowQueueAdapter
sb.append(song.album+", "+song.artist);
sb.setSpan(new ForegroundColorSpan(Color.GRAY), song.title.length() + 1, sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
row.getTextView().setText(sb);
row.getCoverView().setCover(MediaUtils.TYPE_ALBUM, song.albumId);
}
row.highlightRow(position == mHighlightRow);

View File

@ -22,20 +22,10 @@
package ch.blinkenlights.android.vanilla;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.LruCache;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import android.util.Log;
/**
* Represents a Song backed by the MediaStore. Includes basic metadata and
* utilities to retrieve songs from the MediaStore.
@ -54,22 +44,6 @@ public class Song implements Comparable<Song> {
* The number of flags.
*/
public static final int FLAG_COUNT = 2;
/**
* Use all cover providers to load cover art
*/
public static final int COVER_MODE_ALL = 0xF;
/**
* Use androids builtin cover mechanism to load covers
*/
public static final int COVER_MODE_ANDROID = 0x1;
/**
* Use vanilla musics cover load mechanism
*/
public static final int COVER_MODE_VANILLA = 0x2;
/**
* Use vanilla musics SHADOW cover load mechanism
*/
public static final int COVER_MODE_SHADOW = 0x4;
public static final String[] EMPTY_PROJECTION = {
@ -104,158 +78,12 @@ public class Song implements Comparable<Song> {
MediaStore.Audio.Playlists.Members.TRACK,
};
private class LruCacheKey {
long id;
long artistId;
long albumId;
String path;
public LruCacheKey(long id, long artistId, long albumId, String path) {
this.id = id;
this.artistId = artistId;
this.albumId = albumId;
this.path = path;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof LruCacheKey && this.albumId == ((LruCacheKey)obj).albumId && this.artistId == ((LruCacheKey)obj).artistId) {
return true;
}
return false;
}
@Override
public int hashCode() {
return (int)( 0xFFFFFF & (this.artistId + this.albumId) );
}
@Override
public String toString() {
return "LruCacheKey<"+this.id+"> = "+this.path;
}
}
/**
* A cache of 6 MiB of covers.
*/
private static class CoverCache extends LruCache<LruCacheKey, Bitmap> {
private final Context mContext;
// Possible coverart names if we are going to load the cover on our own
private static String[] coverNames = { "cover.jpg", "cover.png", "album.jpg", "album.png", "artwork.jpg", "artwork.png", "art.jpg", "art.png" };
public CoverCache(Context context)
{
super(6 * 1024 * 1024);
mContext = context;
}
@Override
public Bitmap create(LruCacheKey key)
{
try {
InputStream inputStream = null;
InputStream sampleInputStream = null; // same as inputStream but used for getSampleSize
if ((mCoverLoadMode & COVER_MODE_VANILLA) != 0) {
String basePath = (new File(key.path)).getParentFile().getAbsolutePath(); // ../ of the currently playing file
for (String coverFile: coverNames) {
File guessedFile = new File( basePath + "/" + coverFile);
if (guessedFile.exists() && !guessedFile.isDirectory()) {
inputStream = new FileInputStream(guessedFile);
sampleInputStream = new FileInputStream(guessedFile);
break;
}
}
}
if (inputStream == null && (mCoverLoadMode & COVER_MODE_SHADOW) != 0) {
String[] projection = new String [] { MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM };
QueryTask query = MediaUtils.buildQuery(MediaUtils.TYPE_SONG, key.id, projection, null);
Cursor cursor = query.runQuery(mContext.getContentResolver());
if (cursor != null) {
if (cursor.getCount() > 0) {
cursor.moveToNext();
String thisArtist = cursor.getString(0);
String thisAlbum = cursor.getString(1);
String shadowPath = "/sdcard/Music/.vanilla/"+(thisArtist.replaceAll("/", "_"))+"/"+(thisAlbum.replaceAll("/", "_"))+".jpg";
File guessedFile = new File(shadowPath);
if (guessedFile.exists() && !guessedFile.isDirectory()) {
inputStream = new FileInputStream(guessedFile);
sampleInputStream = new FileInputStream(guessedFile);
}
}
cursor.close();
}
}
if (inputStream == null && (mCoverLoadMode & COVER_MODE_ANDROID) != 0 && key.id >= 0) {
Uri uri = Uri.parse("content://media/external/audio/media/" + key.id + "/albumart");
ContentResolver res = mContext.getContentResolver();
inputStream = res.openInputStream(uri);
sampleInputStream = res.openInputStream(uri);
}
if (inputStream != null) {
BitmapFactory.Options bopts = new BitmapFactory.Options();
bopts.inPreferredConfig = Bitmap.Config.RGB_565;
bopts.inJustDecodeBounds = true;
final int inSampleSize = getSampleSize(sampleInputStream, bopts);
/* reuse bopts: we are now REALLY going to decode the image */
bopts.inJustDecodeBounds = false;
bopts.inSampleSize = inSampleSize;
return BitmapFactory.decodeStream(inputStream, null, bopts);
}
} catch (Exception e) {
// no cover art found
Log.v("VanillaMusic", "Loading coverart for "+key+" failed with exception "+e);
}
return null;
}
/**
* Guess a good sampleSize value for given inputStream
*/
private static int getSampleSize(InputStream inputStream, BitmapFactory.Options bopts) {
int sampleSize = 1; /* default sample size */
long maxVal = 600*600; /* max number of pixels we are accepting */
BitmapFactory.decodeStream(inputStream, null, bopts);
long hasPixels = bopts.outHeight * bopts.outWidth;
if(hasPixels > maxVal) {
sampleSize = Math.round((int)Math.sqrt((float) hasPixels / (float) maxVal));
}
return sampleSize;
}
@Override
protected int sizeOf(LruCacheKey key, Bitmap value)
{
return value.getRowBytes() * value.getHeight();
}
}
/**
* The cache instance.
*/
private static CoverCache sCoverCache = null;
/**
* Bitmask on how we are going to load coverart
*/
public static int mCoverLoadMode = 0;
/**
* We will evict our own cache if set to true
*/
public static boolean mFlushCoverCache = false;
/**
* Id of this song in the MediaStore
*/
@ -367,19 +195,14 @@ public class Song implements Comparable<Song> {
*/
public Bitmap getCover(Context context)
{
if (mCoverLoadMode == 0 || id == -1 || (flags & FLAG_NO_COVER) != 0)
if (CoverCache.mCoverLoadMode == 0 || id == -1 || (flags & FLAG_NO_COVER) != 0)
return null;
if (sCoverCache == null)
sCoverCache = new CoverCache(context.getApplicationContext());
if (mFlushCoverCache) {
mFlushCoverCache = false;
sCoverCache.evictAll();
}
LruCacheKey key = new LruCacheKey(id, artistId, albumId, path);
Bitmap cover = sCoverCache.get(key);
CoverCache.CoverKey key = new CoverCache.CoverKey(MediaUtils.TYPE_ALBUM, this.albumId, CoverCache.SIZE_LARGE);
Bitmap cover = sCoverCache.getCoverFromSong(key, this);
if (cover == null)
flags |= FLAG_NO_COVER;

View File

@ -34,7 +34,11 @@ import com.mobeta.android.dslv.DragSortListView;
/**
* The preferences activity in which one can change application preferences.
*/
public class TabOrderActivity extends Activity implements View.OnClickListener, OnItemClickListener {
public class TabOrderActivity extends Activity
implements View.OnClickListener,
OnItemClickListener,
DragSortListView.DropListener
{
private TabOrderAdapter mAdapter;
private DragSortListView mList;
@ -54,7 +58,7 @@ public class TabOrderActivity extends Activity implements View.OnClickListener,
DragSortListView list = (DragSortListView)findViewById(R.id.list);
list.setAdapter(mAdapter);
list.setOnItemClickListener(this);
list.setDropListener(onDrop);
list.setDropListener(this);
mList = list;
load();
@ -161,28 +165,25 @@ public class TabOrderActivity extends Activity implements View.OnClickListener,
* @param from the item index that was dragged
* @param to the index where the item was dropped
*/
private DragSortListView.DropListener onDrop =
new DragSortListView.DropListener() {
@Override
public void drop(int from, int to) {
if (from == to)
return;
@Override
public void drop(int from, int to) {
if (from == to)
return;
int[] ids = mAdapter.getTabIds();
int tempId = ids[from];
int[] ids = mAdapter.getTabIds();
int tempId = ids[from];
if (from > to) {
System.arraycopy(ids, to, ids, to + 1, from - to);
} else {
System.arraycopy(ids, from + 1, ids, from, to - from);
}
if (from > to) {
System.arraycopy(ids, to, ids, to + 1, from - to);
} else {
System.arraycopy(ids, from + 1, ids, from, to - from);
}
ids[to] = tempId;
save();
mAdapter.notifyDataSetChanged();
// no need to update the copy in mAdapter: We worked on a reference
}
};
ids[to] = tempId;
save();
mAdapter.notifyDataSetChanged();
// no need to update the copy in mAdapter: We worked on a reference
}
}

View File

@ -90,11 +90,11 @@ public class TabOrderAdapter extends BaseAdapter {
DraggableRow view;
if (convert == null) {
view = (DraggableRow)mInflater.inflate(R.layout.draggable_row, null);
view.setupLayout(DraggableRow.LAYOUT_CHECKBOXES);
} else {
view = (DraggableRow)convert;
}
view.getTextView().setText(LibraryPagerAdapter.TITLES[mTabIds[position]]);
view.showCheckBox(true);
return view;
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2015 Adrian Ulrich <adrian@blinkenlights.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ch.blinkenlights.android.vanilla;
import android.view.View;
import android.widget.TextView;
import android.widget.ImageView;
public class ViewHolder {
public long id;
public String title;
public TextView text;
public View divider;
public ImageView arrow;
public LazyCoverView cover;
}