├── .gitignore ├── app ├── libs │ ├── gson-2.1.jar │ ├── http-request-2.0.jar │ ├── android-pusher-0.6.jar │ └── android-support-v4.jar ├── res │ ├── drawable-hdpi │ │ ├── launcher.png │ │ ├── action_add.png │ │ ├── menu_clear.png │ │ ├── menu_stars.png │ │ ├── action_pause.png │ │ ├── action_play.png │ │ ├── action_save.png │ │ ├── action_search.png │ │ ├── menu_refresh.png │ │ ├── menu_settings.png │ │ └── notification.png │ ├── drawable-ldpi │ │ ├── launcher.png │ │ ├── action_add.png │ │ ├── menu_clear.png │ │ ├── menu_stars.png │ │ ├── action_pause.png │ │ ├── action_play.png │ │ ├── action_save.png │ │ ├── action_search.png │ │ ├── menu_refresh.png │ │ ├── menu_settings.png │ │ └── notification.png │ ├── drawable-mdpi │ │ ├── launcher.png │ │ ├── action_add.png │ │ ├── menu_clear.png │ │ ├── menu_stars.png │ │ ├── action_pause.png │ │ ├── action_play.png │ │ ├── action_save.png │ │ ├── action_search.png │ │ ├── menu_refresh.png │ │ ├── menu_settings.png │ │ └── notification.png │ ├── drawable-nodpi │ │ ├── cd_case.png │ │ ├── spinner_1.png │ │ ├── spinner_2.png │ │ ├── spinner_3.png │ │ ├── spinner_4.png │ │ ├── action_star.png │ │ ├── cd_playing.png │ │ ├── action_remove.png │ │ ├── action_unstar.png │ │ ├── actionbar_pattern.png │ │ ├── selection_checked.png │ │ ├── dialog_search_album.png │ │ ├── selection_unchecked.png │ │ └── dialog_search_artist.png │ ├── drawable-xhdpi │ │ ├── launcher.png │ │ ├── action_add.png │ │ ├── action_pause.png │ │ ├── action_play.png │ │ ├── action_save.png │ │ ├── menu_clear.png │ │ ├── menu_refresh.png │ │ ├── menu_stars.png │ │ ├── notification.png │ │ ├── action_search.png │ │ └── menu_settings.png │ ├── drawable-hdpi-v9 │ │ └── notification.png │ ├── drawable-ldpi-v9 │ │ └── notification.png │ ├── drawable-mdpi-v9 │ │ └── notification.png │ ├── drawable-hdpi-v11 │ │ └── notification.png │ ├── drawable-ldpi-v11 │ │ └── notification.png │ ├── drawable-mdpi-v11 │ │ └── notification.png │ ├── drawable-xhdpi-v11 │ │ └── notification.png │ ├── drawable-xhdpi-v9 │ │ └── notification.png │ ├── layout │ │ ├── divider.xml │ │ ├── loading.xml │ │ ├── main.xml │ │ ├── search_separator.xml │ │ ├── search.xml │ │ ├── queued.xml │ │ ├── search_album.xml │ │ ├── search_song.xml │ │ ├── now_playing.xml │ │ ├── settings.xml │ │ └── song_dialog.xml │ ├── drawable │ │ ├── edit_text_cursor.xml │ │ ├── now_playing.xml │ │ ├── now_playing_pressed.xml │ │ ├── list_header.xml │ │ ├── now_playing_divider.xml │ │ ├── list_divider.xml │ │ ├── queued_cd.xml │ │ ├── playing_cd.xml │ │ ├── actionbar.xml │ │ ├── queued_background.xml │ │ ├── now_playing_background.xml │ │ ├── edit_text_background.xml │ │ └── spinner_animation.xml │ ├── menu │ │ ├── add.xml │ │ ├── settings.xml │ │ ├── songs.xml │ │ ├── search.xml │ │ └── main.xml │ ├── xml │ │ └── searchable.xml │ └── values │ │ ├── theme.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml ├── .classpath ├── project.properties ├── .project ├── src │ └── main │ │ └── java │ │ └── com │ │ └── github │ │ └── play │ │ ├── core │ │ ├── SongPusher.java │ │ ├── SongCallback.java │ │ ├── StatusUpdate.java │ │ ├── StreamingInfo.java │ │ ├── StarSongTask.java │ │ ├── UnstarSongTask.java │ │ ├── SearchTask.java │ │ ├── DequeueSongTask.java │ │ ├── QueueSongsTask.java │ │ ├── QueueStarsTask.java │ │ ├── SongResult.java │ │ ├── FetchStatusTask.java │ │ ├── FetchSettingsTask.java │ │ ├── Song.java │ │ ├── PlayPreferences.java │ │ └── PlayService.java │ │ ├── app │ │ ├── SearchSuggestionsProvider.java │ │ ├── ViewArtistActivity.java │ │ ├── ViewAlbumActivity.java │ │ ├── SearchActivity.java │ │ ├── SettingsActivity.java │ │ ├── MusicStreamService.java │ │ ├── SongViewActivity.java │ │ ├── StatusService.java │ │ └── PlayActivity.java │ │ └── widget │ │ ├── PlayListAdapter.java │ │ ├── SearchListAdapter.java │ │ └── SongArtWrapper.java ├── proguard.cfg ├── .settings │ └── org.eclipse.jdt.ui.prefs ├── AndroidManifest.xml └── pom.xml ├── pom.xml ├── .travis.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | gen/ 3 | app/proguard 4 | target/ 5 | -------------------------------------------------------------------------------- /app/libs/gson-2.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/libs/gson-2.1.jar -------------------------------------------------------------------------------- /app/libs/http-request-2.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/libs/http-request-2.0.jar -------------------------------------------------------------------------------- /app/libs/android-pusher-0.6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/libs/android-pusher-0.6.jar -------------------------------------------------------------------------------- /app/libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/libs/android-support-v4.jar -------------------------------------------------------------------------------- /app/res/drawable-hdpi/launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-hdpi/launcher.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi/launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-ldpi/launcher.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi/launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-mdpi/launcher.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/cd_case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/cd_case.png -------------------------------------------------------------------------------- /app/res/drawable-hdpi/action_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-hdpi/action_add.png -------------------------------------------------------------------------------- /app/res/drawable-hdpi/menu_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-hdpi/menu_clear.png -------------------------------------------------------------------------------- /app/res/drawable-hdpi/menu_stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-hdpi/menu_stars.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi/action_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-ldpi/action_add.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi/menu_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-ldpi/menu_clear.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi/menu_stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-ldpi/menu_stars.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi/action_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-mdpi/action_add.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi/menu_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-mdpi/menu_clear.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi/menu_stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-mdpi/menu_stars.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/spinner_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/spinner_1.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/spinner_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/spinner_2.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/spinner_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/spinner_3.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/spinner_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/spinner_4.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi/launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-xhdpi/launcher.png -------------------------------------------------------------------------------- /app/res/drawable-hdpi/action_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-hdpi/action_pause.png -------------------------------------------------------------------------------- /app/res/drawable-hdpi/action_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-hdpi/action_play.png -------------------------------------------------------------------------------- /app/res/drawable-hdpi/action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-hdpi/action_save.png -------------------------------------------------------------------------------- /app/res/drawable-hdpi/action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-hdpi/action_search.png -------------------------------------------------------------------------------- /app/res/drawable-hdpi/menu_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-hdpi/menu_refresh.png -------------------------------------------------------------------------------- /app/res/drawable-hdpi/menu_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-hdpi/menu_settings.png -------------------------------------------------------------------------------- /app/res/drawable-hdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-hdpi/notification.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi/action_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-ldpi/action_pause.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi/action_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-ldpi/action_play.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi/action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-ldpi/action_save.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi/action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-ldpi/action_search.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi/menu_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-ldpi/menu_refresh.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi/menu_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-ldpi/menu_settings.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-ldpi/notification.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi/action_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-mdpi/action_pause.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi/action_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-mdpi/action_play.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi/action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-mdpi/action_save.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi/action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-mdpi/action_search.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi/menu_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-mdpi/menu_refresh.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi/menu_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-mdpi/menu_settings.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-mdpi/notification.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/action_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/action_star.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/cd_playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/cd_playing.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi/action_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-xhdpi/action_add.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi/action_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-xhdpi/action_pause.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi/action_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-xhdpi/action_play.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi/action_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-xhdpi/action_save.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi/menu_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-xhdpi/menu_clear.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi/menu_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-xhdpi/menu_refresh.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi/menu_stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-xhdpi/menu_stars.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-xhdpi/notification.png -------------------------------------------------------------------------------- /app/res/drawable-hdpi-v9/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-hdpi-v9/notification.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi-v9/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-ldpi-v9/notification.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi-v9/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-mdpi-v9/notification.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/action_remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/action_remove.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/action_unstar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/action_unstar.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi/action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-xhdpi/action_search.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi/menu_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-xhdpi/menu_settings.png -------------------------------------------------------------------------------- /app/res/drawable-hdpi-v11/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-hdpi-v11/notification.png -------------------------------------------------------------------------------- /app/res/drawable-ldpi-v11/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-ldpi-v11/notification.png -------------------------------------------------------------------------------- /app/res/drawable-mdpi-v11/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-mdpi-v11/notification.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/actionbar_pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/actionbar_pattern.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/selection_checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/selection_checked.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi-v11/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-xhdpi-v11/notification.png -------------------------------------------------------------------------------- /app/res/drawable-xhdpi-v9/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-xhdpi-v9/notification.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/dialog_search_album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/dialog_search_album.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/selection_unchecked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/selection_unchecked.png -------------------------------------------------------------------------------- /app/res/drawable-nodpi/dialog_search_artist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/play/play-android/master/app/res/drawable-nodpi/dialog_search_artist.png -------------------------------------------------------------------------------- /app/res/layout/divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /app/res/drawable/edit_text_cursor.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/res/drawable/now_playing.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/res/drawable/now_playing_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/res/menu/add.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/res/menu/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/res/drawable/list_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/res/drawable/now_playing_divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/res/xml/searchable.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /app/res/drawable/list_divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/res/drawable/queued_cd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/res/drawable/playing_cd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/res/menu/songs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /app/res/layout/loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/res/drawable/actionbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 4.0.0 8 | 9 | 2.0-SNAPSHOT 10 | com.github.play 11 | play-android-parent 12 | Play Android App parent 13 | pom 14 | 15 | 16 | app 17 | 18 | 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: oraclejdk7 3 | 4 | notifications: 5 | email: false 6 | 7 | before_install: 8 | - sudo apt-get update -qq 9 | - if [ `uname -m` = x86_64 ]; then sudo apt-get install -qq --force-yes libgd2-xpm ia32-libs ia32-libs-multiarch > /dev/null; fi 10 | - wget http://dl.google.com/android/android-sdk_r21.1-linux.tgz 11 | - tar -zxf android-sdk_r21.1-linux.tgz 12 | - export ANDROID_HOME=$PWD/android-sdk-linux 13 | - export PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools 14 | - android update sdk --filter platform-tools,android-16 --no-ui --force > /dev/null 15 | -------------------------------------------------------------------------------- /app/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=proguard.cfg 12 | 13 | # Project target. 14 | target=android-16 15 | android.library.reference.1=../../ActionBarSherlock/library 16 | android.library.reference.2=../../wishlist/lib 17 | -------------------------------------------------------------------------------- /app/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 23 | 24 | -------------------------------------------------------------------------------- /app/res/values/theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /app/res/menu/search.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 14 | 18 | 23 | 24 | -------------------------------------------------------------------------------- /app/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #282828 5 | #515252 6 | #EDEDED 7 | #D2D2D2 8 | #CCCCCC 9 | #000000 10 | #A6A6A6 11 | #FFFFFF 12 | #EA36F5 13 | #FFFFFF 14 | #D4D4D4 15 | #F9F9F9 16 | #F8F8F8 17 | #EDEDED 18 | #959595 19 | 20 | -------------------------------------------------------------------------------- /app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | play 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/res/drawable/queued_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/res/layout/search_separator.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | 18 | 25 | 26 | -------------------------------------------------------------------------------- /app/res/drawable/now_playing_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/res/drawable/edit_text_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/res/drawable/spinner_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 11 | 12 | 13 | 17 | 18 | 19 | 23 | 24 | 25 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/res/layout/search.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 15 | 16 | 22 | 23 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/SongPusher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import static android.os.Build.VERSION.SDK_INT; 19 | import static android.os.Build.VERSION_CODES.FROYO; 20 | 21 | import com.emorym.android_pusher.Pusher; 22 | 23 | /** 24 | * Pusher to receive song data 25 | */ 26 | public class SongPusher extends Pusher { 27 | 28 | /** 29 | * Create standard pusher 30 | * 31 | * @param applicationKey 32 | */ 33 | public SongPusher(final String applicationKey) { 34 | super(applicationKey, true, SDK_INT <= FROYO); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 16 | 22 | 28 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/SongCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import java.io.IOException; 19 | 20 | /** 21 | * Callback for when the current song changes or an error occurs 22 | */ 23 | public interface SongCallback { 24 | 25 | /** 26 | * Currently playing or queued songs have changed 27 | * 28 | * @param playing 29 | * @param queued 30 | */ 31 | void onUpdate(Song playing, Song[] queued); 32 | 33 | /** 34 | * Exception occurred contacting Play server 35 | * 36 | * @param e 37 | */ 38 | void onError(IOException e); 39 | } 40 | -------------------------------------------------------------------------------- /app/res/layout/queued.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 23 | 24 | 27 | 28 | 31 | 32 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/StatusUpdate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import java.io.Serializable; 19 | 20 | /** 21 | * Wrapper class to encapsulate all data send via a Play push update 22 | */ 23 | public class StatusUpdate implements Serializable { 24 | 25 | private static final long serialVersionUID = -877849357884315386L; 26 | 27 | /** 28 | * Currently playing song 29 | */ 30 | public final Song playing; 31 | 32 | /** 33 | * Songs in the queue 34 | */ 35 | public final Song[] queued; 36 | 37 | /** 38 | * Create status update 39 | * 40 | * @param playing 41 | * @param queued 42 | */ 43 | public StatusUpdate(final Song playing, final Song[] queued) { 44 | this.playing = playing; 45 | this.queued = queued; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/proguard.cfg: -------------------------------------------------------------------------------- 1 | -ignorewarnings 2 | -dontoptimize 3 | -dontobfuscate 4 | -optimizationpasses 5 5 | -dontusemixedcaseclassnames 6 | -dontskipnonpubliclibraryclasses 7 | -dontpreverify 8 | -verbose 9 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* 10 | 11 | -keep public class * extends android.app.Activity 12 | -keep public class * extends android.app.Application 13 | -keep public class * extends android.app.Service 14 | -keep public class * extends android.content.BroadcastReceiver 15 | -keep public class * extends android.content.ContentProvider 16 | -keep public class * extends android.app.backup.BackupAgentHelper 17 | -keep public class * extends android.preference.Preference 18 | -keep public class com.android.vending.licensing.ILicensingService 19 | 20 | -keepclasseswithmembernames class * { 21 | native ; 22 | } 23 | 24 | -keepclasseswithmembers class * { 25 | public (android.content.Context, android.util.AttributeSet); 26 | } 27 | 28 | -keepclasseswithmembers class * { 29 | public (android.content.Context, android.util.AttributeSet, int); 30 | } 31 | 32 | -keepclassmembers class * extends android.app.Activity { 33 | public void *(android.view.View); 34 | } 35 | 36 | -keepclassmembers enum * { 37 | public static **[] values(); 38 | public static ** valueOf(java.lang.String); 39 | } 40 | 41 | -keep class * implements android.os.Parcelable { 42 | public static final android.os.Parcelable$Creator *; 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Play Android App [![Google Play](http://developer.android.com/images/brand/en_generic_rgb_wo_45.png)](https://play.google.com/store/apps/details?id=com.github.play) [![Build Status](https://travis-ci.org/play/play-android.png)](https://travis-ci.org/play/play-android) 2 | 3 | Stream music and control the queue of a [play](https://github.com/play/play) 4 | server from your Android device. 5 | 6 | ## Dependencies 7 | 8 | * [ActionBarSherlock](https://github.com/JakeWharton/ActionBarSherlock) 9 | * [http-request](https://github.com/kevinsawicki/http-request) 10 | * [android-pusher](https://github.com/kevinsawicki/android_pusher) 11 | * [wishlist](https://github.com/kevinsawicki/wishlist) 12 | * [gson](http://code.google.com/p/google-gson/) 13 | 14 | ## Building 15 | 16 | From the root of the repository: 17 | 18 | ``` 19 | $ mvn clean package 20 | ``` 21 | 22 | ## License 23 | 24 | * [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) 25 | 26 | ## Screenshots 27 | 28 | ### View what's playing and what's up next 29 | 30 | ![](http://f.cl.ly/items/110W2r1B0k2a040w2e1j/Dashboard.png) 31 | 32 | ### Search for songs and add them to the queue 33 | 34 | ![](http://f.cl.ly/items/122q0O162Y0a3Y3m1I3h/song_search.png) 35 | 36 | ### Song Actions. Star, Remove, View Album, View Artist 37 | 38 | ![](http://f.cl.ly/items/0w2W2n3K3I2P2F1q3e1m/Song_actions.png) 39 | 40 | ### Notification bar showing what's currently playing 41 | 42 | ![](http://f.cl.ly/items/1s0E0s1p2R1K1I0t2I1u/unnamed.jpeg) 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/StreamingInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import java.io.Serializable; 19 | 20 | /** 21 | * Information about the streaming location of the Play server and also the 22 | * pusher application key 23 | */ 24 | public class StreamingInfo implements Serializable { 25 | 26 | private static final long serialVersionUID = -6632557432169389967L; 27 | 28 | /** 29 | * URL to stream music from 30 | */ 31 | public final String streamUrl; 32 | 33 | /** 34 | * Pusher application key 35 | */ 36 | public final String pusherKey; 37 | 38 | /** 39 | * Create streaming info with given values 40 | * 41 | * @param streamUrl 42 | * @param pusherKey 43 | */ 44 | public StreamingInfo(final String streamUrl, final String pusherKey) { 45 | this.streamUrl = streamUrl; 46 | this.pusherKey = pusherKey; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/res/layout/search_album.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 27 | 28 | 36 | 37 | 40 | 41 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/StarSongTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import android.os.AsyncTask; 19 | import android.util.Log; 20 | 21 | import java.io.IOException; 22 | import java.util.concurrent.atomic.AtomicReference; 23 | 24 | /** 25 | * Task to star a song 26 | */ 27 | public class StarSongTask extends AsyncTask { 28 | 29 | private static final String TAG = "StarSongTask"; 30 | 31 | private final AtomicReference service; 32 | 33 | /** 34 | * Create task to star a song 35 | * 36 | * @param service 37 | */ 38 | public StarSongTask(final AtomicReference service) { 39 | this.service = service; 40 | } 41 | 42 | @Override 43 | protected IOException doInBackground(Song... params) { 44 | try { 45 | service.get().star(params[0]); 46 | return null; 47 | } catch (IOException e) { 48 | return e; 49 | } 50 | } 51 | 52 | @Override 53 | protected void onPostExecute(IOException result) { 54 | super.onPostExecute(result); 55 | 56 | if (result != null) 57 | Log.d(TAG, "Unstarring failed", result); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/UnstarSongTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import android.os.AsyncTask; 19 | import android.util.Log; 20 | 21 | import java.io.IOException; 22 | import java.util.concurrent.atomic.AtomicReference; 23 | 24 | /** 25 | * Task to unstar a song 26 | */ 27 | public class UnstarSongTask extends AsyncTask { 28 | 29 | private static final String TAG = "UnstarSongTask"; 30 | 31 | private final AtomicReference service; 32 | 33 | /** 34 | * Create task to unstar a song 35 | * 36 | * @param service 37 | */ 38 | public UnstarSongTask(final AtomicReference service) { 39 | this.service = service; 40 | } 41 | 42 | @Override 43 | protected IOException doInBackground(Song... params) { 44 | try { 45 | service.get().unstar(params[0]); 46 | return null; 47 | } catch (IOException e) { 48 | return e; 49 | } 50 | } 51 | 52 | @Override 53 | protected void onPostExecute(IOException result) { 54 | super.onPostExecute(result); 55 | 56 | if (result != null) 57 | Log.d(TAG, "Unstarring failed", result); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/SearchTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import android.os.AsyncTask; 19 | import android.util.Log; 20 | 21 | import java.io.IOException; 22 | import java.util.concurrent.atomic.AtomicReference; 23 | 24 | /** 25 | * Task for search for songs 26 | */ 27 | public class SearchTask extends AsyncTask { 28 | 29 | private static final String TAG = "SearchTask"; 30 | 31 | private final AtomicReference service; 32 | 33 | /** 34 | * Create task to search for songs 35 | * 36 | * @param service 37 | */ 38 | public SearchTask(final AtomicReference service) { 39 | this.service = service; 40 | } 41 | 42 | @Override 43 | protected SongResult doInBackground(String... params) { 44 | try { 45 | return new SongResult(service.get().search(params[0])); 46 | } catch (IOException e) { 47 | return new SongResult(e); 48 | } 49 | } 50 | 51 | @Override 52 | protected void onPostExecute(SongResult result) { 53 | super.onPostExecute(result); 54 | 55 | if (result.exception != null) 56 | Log.d(TAG, "Searching failed", result.exception); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/DequeueSongTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import android.os.AsyncTask; 19 | import android.util.Log; 20 | 21 | import java.io.IOException; 22 | import java.util.concurrent.atomic.AtomicReference; 23 | 24 | /** 25 | * Task to remove a song from the queue 26 | */ 27 | public class DequeueSongTask extends AsyncTask { 28 | 29 | private static final String TAG = "DequeueSongTask"; 30 | 31 | private final AtomicReference service; 32 | 33 | /** 34 | * Create task to dequeue a song 35 | * 36 | * @param service 37 | */ 38 | public DequeueSongTask(final AtomicReference service) { 39 | this.service = service; 40 | } 41 | 42 | @Override 43 | protected IOException doInBackground(Song... songs) { 44 | for (Song song : songs) 45 | try { 46 | service.get().dequeue(song); 47 | } catch (IOException e) { 48 | return e; 49 | } 50 | return null; 51 | } 52 | 53 | @Override 54 | protected void onPostExecute(IOException result) { 55 | super.onPostExecute(result); 56 | 57 | if (result != null) 58 | Log.d(TAG, "Dequeueing failed", result); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/res/layout/search_song.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 27 | 28 | 36 | 37 | 40 | 41 | 44 | 45 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/QueueSongsTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import android.os.AsyncTask; 19 | import android.util.Log; 20 | 21 | import java.io.IOException; 22 | import java.util.concurrent.atomic.AtomicReference; 23 | 24 | /** 25 | * Task to add one or more songs to the queue 26 | */ 27 | public class QueueSongsTask extends AsyncTask { 28 | 29 | private static final String TAG = "QueueSongsTask"; 30 | 31 | private final AtomicReference service; 32 | 33 | /** 34 | * Create task to add one or more songs to the queue 35 | * 36 | * @param service 37 | */ 38 | public QueueSongsTask(final AtomicReference service) { 39 | this.service = service; 40 | } 41 | 42 | @Override 43 | protected IOException doInBackground(Song... params) { 44 | for (Song song : params) 45 | try { 46 | service.get().queue(song); 47 | } catch (IOException e) { 48 | return e; 49 | } 50 | return null; 51 | } 52 | 53 | @Override 54 | protected void onPostExecute(IOException result) { 55 | super.onPostExecute(result); 56 | 57 | if (result != null) 58 | Log.d(TAG, "Queueing songs failed", result); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/QueueStarsTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import android.os.AsyncTask; 19 | import android.util.Log; 20 | 21 | import java.io.IOException; 22 | import java.util.concurrent.atomic.AtomicReference; 23 | 24 | /** 25 | * Task to add starred songs to the queue 26 | */ 27 | public class QueueStarsTask extends AsyncTask { 28 | 29 | private static final String TAG = "QueueStarsTask"; 30 | 31 | private final AtomicReference service; 32 | 33 | /** 34 | * Create task to add starred songs to the queue 35 | * 36 | * @param service 37 | */ 38 | public QueueStarsTask(final AtomicReference service) { 39 | this.service = service; 40 | } 41 | 42 | @Override 43 | protected SongResult doInBackground(Void... params) { 44 | try { 45 | return new SongResult(service.get().queueStars()); 46 | } catch (IOException e) { 47 | return new SongResult(e); 48 | } 49 | } 50 | 51 | @Override 52 | protected void onPostExecute(final SongResult result) { 53 | super.onPostExecute(result); 54 | 55 | if (result.exception != null) 56 | Log.d(TAG, "Queueing starred songs failed", result.exception); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/res/layout/now_playing.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 23 | 24 | 31 | 32 | 35 | 36 | 39 | 40 | 43 | 44 | 45 | 46 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/SongResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import static java.lang.String.CASE_INSENSITIVE_ORDER; 19 | 20 | import java.io.IOException; 21 | import java.util.Map; 22 | import java.util.TreeMap; 23 | 24 | /** 25 | * Result class for a task that results in an {@link IOException} or an array of 26 | * {@link Song} objects 27 | */ 28 | public class SongResult { 29 | 30 | /** 31 | * Songs 32 | */ 33 | public final Song[] songs; 34 | 35 | /** 36 | * Failure exception 37 | */ 38 | public final IOException exception; 39 | 40 | /** 41 | * Albums 42 | */ 43 | public final Song[] albums; 44 | 45 | /** 46 | * Create result with songs 47 | * 48 | * @param songs 49 | */ 50 | public SongResult(final Song[] songs) { 51 | this.songs = songs; 52 | exception = null; 53 | 54 | final Map albums = new TreeMap( 55 | CASE_INSENSITIVE_ORDER); 56 | for (Song song : songs) 57 | albums.put(song.getAlbumId(), song); 58 | this.albums = albums.values().toArray(new Song[albums.size()]); 59 | } 60 | 61 | /** 62 | * Create result with exception 63 | * 64 | * @param error 65 | */ 66 | public SongResult(final IOException error) { 67 | songs = null; 68 | albums = null; 69 | exception = error; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/app/SearchSuggestionsProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.app; 17 | 18 | import android.content.Context; 19 | import android.content.SearchRecentSuggestionsProvider; 20 | import android.provider.SearchRecentSuggestions; 21 | 22 | /** 23 | * Suggestion provider for previous Play searches 24 | */ 25 | public class SearchSuggestionsProvider extends SearchRecentSuggestionsProvider { 26 | 27 | /** 28 | * Authority of search suggestion provider 29 | */ 30 | public static final String AUTHORITY = "com.github.play.search.suggest"; 31 | 32 | /** 33 | * Clear all recent suggestions in history 34 | * 35 | * @param context 36 | */ 37 | public static final void clear(final Context context) { 38 | new SearchRecentSuggestions(context, AUTHORITY, DATABASE_MODE_QUERIES) 39 | .clearHistory(); 40 | } 41 | 42 | /** 43 | * Add recent suggestion query to history 44 | * 45 | * @param context 46 | * @param query 47 | */ 48 | public static final void add(final Context context, final String query) { 49 | new SearchRecentSuggestions(context, AUTHORITY, DATABASE_MODE_QUERIES) 50 | .saveRecentQuery(query, null); 51 | } 52 | 53 | /** 54 | * Search suggestion provider for Play 55 | */ 56 | public SearchSuggestionsProvider() { 57 | setupSuggestions(AUTHORITY, DATABASE_MODE_QUERIES); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/FetchStatusTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import android.os.AsyncTask; 19 | 20 | import java.io.IOException; 21 | import java.util.concurrent.atomic.AtomicReference; 22 | 23 | /** 24 | * Obtain the currently playing and queued songs 25 | */ 26 | public class FetchStatusTask extends AsyncTask { 27 | 28 | private final AtomicReference service; 29 | 30 | private final SongCallback callback; 31 | 32 | /** 33 | * Create task with callback to call from {@link #onPostExecute(Object[])} 34 | * 35 | * @param service 36 | * @param callback 37 | */ 38 | public FetchStatusTask(final AtomicReference service, 39 | final SongCallback callback) { 40 | this.service = service; 41 | this.callback = callback; 42 | } 43 | 44 | @Override 45 | protected Object[] doInBackground(Void... params) { 46 | PlayService service = this.service.get(); 47 | try { 48 | Song playing = service.getNowPlaying(); 49 | Song[] queue = service.getQueue(); 50 | return new Object[] { playing, queue }; 51 | } catch (IOException e) { 52 | return new Object[] { e }; 53 | } 54 | } 55 | 56 | @Override 57 | protected void onPostExecute(final Object[] result) { 58 | if (result.length == 2) 59 | callback.onUpdate((Song) result[0], (Song[]) result[1]); 60 | else 61 | callback.onError((IOException) result[0]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/FetchSettingsTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import android.os.AsyncTask; 19 | 20 | import com.github.play.core.FetchSettingsTask.PlaySettings; 21 | 22 | import java.io.IOException; 23 | import java.util.concurrent.atomic.AtomicReference; 24 | 25 | /** 26 | * Task to fetch the streaming URL and pusher application key of a configured 27 | * Play server 28 | */ 29 | public class FetchSettingsTask extends AsyncTask { 30 | 31 | /** 32 | * Retrieved Play server settings 33 | */ 34 | public static class PlaySettings { 35 | 36 | /** 37 | * Streaming info 38 | */ 39 | public final StreamingInfo streamingInfo; 40 | 41 | /** 42 | * Exception that occurred retrieving streaming URL 43 | */ 44 | public final IOException exception; 45 | 46 | private PlaySettings(final StreamingInfo streamingInfo, 47 | final IOException exception) { 48 | this.streamingInfo = streamingInfo; 49 | this.exception = exception; 50 | } 51 | 52 | private PlaySettings(final StreamingInfo streamingInfo) { 53 | this(streamingInfo, null); 54 | } 55 | 56 | private PlaySettings(final IOException exception) { 57 | this(null, exception); 58 | } 59 | } 60 | 61 | private final AtomicReference service; 62 | 63 | /** 64 | * Create task to fetch streaming URL 65 | * 66 | * @param service 67 | */ 68 | public FetchSettingsTask(final AtomicReference service) { 69 | this.service = service; 70 | } 71 | 72 | @Override 73 | protected PlaySettings doInBackground(Void... params) { 74 | try { 75 | StreamingInfo streamingInfo = service.get().getStreamingInfo(); 76 | return new PlaySettings(streamingInfo); 77 | } catch (IOException e) { 78 | return new PlaySettings(e); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/widget/PlayListAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.widget; 17 | 18 | import android.app.Activity; 19 | import android.view.View; 20 | 21 | import com.github.kevinsawicki.wishlist.SingleTypeAdapter; 22 | import com.github.play.R.drawable; 23 | import com.github.play.R.id; 24 | import com.github.play.core.PlayService; 25 | import com.github.play.core.Song; 26 | 27 | import java.util.concurrent.atomic.AtomicReference; 28 | 29 | /** 30 | * List adapter for songs 31 | */ 32 | public class PlayListAdapter extends SingleTypeAdapter { 33 | 34 | private final SongArtWrapper albumArt; 35 | 36 | /** 37 | * @param activity 38 | * @param viewId 39 | * @param service 40 | */ 41 | public PlayListAdapter(Activity activity, int viewId, 42 | AtomicReference service) { 43 | super(activity, viewId); 44 | 45 | albumArt = new SongArtWrapper(activity, service); 46 | } 47 | 48 | @Override 49 | protected int[] getChildViewIds() { 50 | return new int[] { id.tv_artist, id.tv_song, id.tv_album, id.iv_art }; 51 | } 52 | 53 | @Override 54 | public View initialize(View view) { 55 | return super.initialize(view); 56 | } 57 | 58 | /** 59 | * Update view for as currently playing song 60 | * 61 | * @param view 62 | * @param song 63 | */ 64 | public void updatePlaying(final View view, final Song song) { 65 | setCurrentView(view); 66 | if (song != null) { 67 | setText(0, song.artist); 68 | setText(1, song.name); 69 | setText(2, song.album); 70 | } else { 71 | setText(0, null); 72 | setText(1, null); 73 | setText(2, null); 74 | } 75 | albumArt.update(imageView(3), drawable.playing_cd, song); 76 | } 77 | 78 | @Override 79 | protected void update(int position, Song song) { 80 | setText(0, song.artist); 81 | setText(1, song.name); 82 | setText(2, song.album); 83 | 84 | albumArt.update(imageView(3), drawable.queued_cd, song); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/Song.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import java.io.Serializable; 19 | 20 | /** 21 | * Song model 22 | */ 23 | public class Song implements Serializable { 24 | 25 | private static final long serialVersionUID = -4757872544894909909L; 26 | 27 | /** 28 | * Id 29 | */ 30 | public final String id; 31 | 32 | /** 33 | * Name 34 | */ 35 | public final String name; 36 | 37 | /** 38 | * Artist 39 | */ 40 | public final String artist; 41 | 42 | /** 43 | * Album 44 | */ 45 | public final String album; 46 | 47 | /** 48 | * Starred status 49 | */ 50 | public final boolean starred; 51 | 52 | /** 53 | * Duration 54 | */ 55 | public final double duration; 56 | 57 | /** 58 | * Create song 59 | * 60 | * @param id 61 | * @param name 62 | * @param artist 63 | * @param album 64 | * @param starred 65 | * @param duration 66 | */ 67 | public Song(final String id, final String name, final String artist, 68 | final String album, final boolean starred, final double duration) { 69 | this.id = id; 70 | this.name = name; 71 | this.artist = artist; 72 | this.album = album; 73 | this.starred = starred; 74 | this.duration = duration; 75 | } 76 | 77 | @Override 78 | public int hashCode() { 79 | return id != null ? id.hashCode() : null; 80 | } 81 | 82 | /** 83 | * Get unique album id 84 | * 85 | * @return id 86 | */ 87 | public String getAlbumId() { 88 | return artist + '#' + album; 89 | } 90 | 91 | @Override 92 | public boolean equals(Object o) { 93 | if (id == null) 94 | return false; 95 | if (this == o) 96 | return true; 97 | if (!(o instanceof Song)) 98 | return false; 99 | 100 | return id.equals(((Song) o).id); 101 | } 102 | 103 | @Override 104 | public String toString() { 105 | return name + " by " + artist + " from " + album; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/app/ViewArtistActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.app; 17 | 18 | import android.content.Context; 19 | import android.content.Intent; 20 | import android.os.AsyncTask; 21 | import android.os.Bundle; 22 | 23 | import com.actionbarsherlock.view.Menu; 24 | import com.github.play.R.menu; 25 | import com.github.play.core.Song; 26 | import com.github.play.core.SongResult; 27 | 28 | import java.io.IOException; 29 | 30 | /** 31 | * Activity to view all the songs by an artist and be able to add them to the 32 | * queue 33 | */ 34 | public class ViewArtistActivity extends SongViewActivity { 35 | 36 | private static final String EXTRA_SONG = "song"; 37 | 38 | /** 39 | * Create intent for song 40 | * 41 | * @param context 42 | * @param song 43 | * @return intent 44 | */ 45 | public static Intent createIntent(final Context context, final Song song) { 46 | final Intent intent = new Intent(context, ViewArtistActivity.class); 47 | intent.putExtra(EXTRA_SONG, song); 48 | return intent; 49 | } 50 | 51 | private Song song; 52 | 53 | @Override 54 | protected void onCreate(Bundle savedInstanceState) { 55 | song = (Song) getIntent().getSerializableExtra(EXTRA_SONG); 56 | getSupportActionBar().setSubtitle(song.artist); 57 | 58 | super.onCreate(savedInstanceState); 59 | } 60 | 61 | @Override 62 | public boolean onCreateOptionsMenu(Menu optionsMenu) { 63 | getSupportMenuInflater().inflate(menu.songs, optionsMenu); 64 | 65 | return super.onCreateOptionsMenu(optionsMenu); 66 | } 67 | 68 | @Override 69 | protected void refreshSongs() { 70 | showLoading(true); 71 | 72 | new AsyncTask() { 73 | 74 | @Override 75 | protected SongResult doInBackground(Song... params) { 76 | try { 77 | return new SongResult(service.get().getSongs(song.artist)); 78 | } catch (IOException e) { 79 | return new SongResult(e); 80 | } 81 | } 82 | 83 | @Override 84 | protected void onPostExecute(SongResult result) { 85 | displaySongs(result); 86 | } 87 | }.execute(song); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/app/ViewAlbumActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.app; 17 | 18 | import android.content.Context; 19 | import android.content.Intent; 20 | import android.os.AsyncTask; 21 | import android.os.Bundle; 22 | 23 | import com.actionbarsherlock.view.Menu; 24 | import com.github.play.R.menu; 25 | import com.github.play.core.Song; 26 | import com.github.play.core.SongResult; 27 | 28 | import java.io.IOException; 29 | 30 | /** 31 | * Activity to view all the songs on an album and be able to add them to the 32 | * queue 33 | */ 34 | public class ViewAlbumActivity extends SongViewActivity { 35 | 36 | private static final String EXTRA_SONG = "song"; 37 | 38 | /** 39 | * Create intent for song 40 | * 41 | * @param context 42 | * @param song 43 | * @return intent 44 | */ 45 | public static Intent createIntent(final Context context, final Song song) { 46 | final Intent intent = new Intent(context, ViewAlbumActivity.class); 47 | intent.putExtra(EXTRA_SONG, song); 48 | return intent; 49 | } 50 | 51 | private Song song; 52 | 53 | @Override 54 | protected void onCreate(Bundle savedInstanceState) { 55 | song = (Song) getIntent().getSerializableExtra(EXTRA_SONG); 56 | getSupportActionBar().setSubtitle(song.album); 57 | 58 | super.onCreate(savedInstanceState); 59 | } 60 | 61 | @Override 62 | public boolean onCreateOptionsMenu(Menu optionsMenu) { 63 | getSupportMenuInflater().inflate(menu.songs, optionsMenu); 64 | 65 | return super.onCreateOptionsMenu(optionsMenu); 66 | } 67 | 68 | @Override 69 | protected void refreshSongs() { 70 | showLoading(true); 71 | 72 | new AsyncTask() { 73 | 74 | @Override 75 | protected SongResult doInBackground(Song... params) { 76 | try { 77 | return new SongResult(service.get().getSongs(song.artist, 78 | song.album)); 79 | } catch (IOException e) { 80 | return new SongResult(e); 81 | } 82 | } 83 | 84 | @Override 85 | protected void onPostExecute(SongResult result) { 86 | displaySongs(result); 87 | } 88 | }.execute(song); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/app/SearchActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.app; 17 | 18 | import static android.app.SearchManager.QUERY; 19 | import static android.content.Intent.ACTION_SEARCH; 20 | import android.content.Intent; 21 | import android.text.TextUtils; 22 | 23 | import com.actionbarsherlock.view.Menu; 24 | import com.actionbarsherlock.view.MenuItem; 25 | import com.github.play.R.id; 26 | import com.github.play.R.menu; 27 | import com.github.play.core.SearchTask; 28 | import com.github.play.core.SongResult; 29 | 30 | /** 31 | * Activity to search for songs and add them to the queue 32 | */ 33 | public class SearchActivity extends SongViewActivity { 34 | 35 | @Override 36 | protected void refreshSongs() { 37 | search(getIntent()); 38 | } 39 | 40 | @Override 41 | public boolean onCreateOptionsMenu(Menu optionsMenu) { 42 | getSupportMenuInflater().inflate(menu.search, optionsMenu); 43 | 44 | return super.onCreateOptionsMenu(optionsMenu); 45 | } 46 | 47 | @Override 48 | public boolean onOptionsItemSelected(MenuItem item) { 49 | switch (item.getItemId()) { 50 | case id.m_search: 51 | onSearchRequested(); 52 | return true; 53 | case id.m_clear: 54 | SearchSuggestionsProvider.clear(this); 55 | return true; 56 | default: 57 | return super.onOptionsItemSelected(item); 58 | } 59 | } 60 | 61 | @Override 62 | protected void onNewIntent(Intent intent) { 63 | setIntent(intent); 64 | search(intent); 65 | } 66 | 67 | private void search(final Intent intent) { 68 | if (intent == null) 69 | return; 70 | if (!ACTION_SEARCH.equals(intent.getAction())) 71 | return; 72 | 73 | String query = intent.getStringExtra(QUERY); 74 | if (TextUtils.isEmpty(query)) 75 | return; 76 | 77 | getSupportActionBar().setSubtitle(query); 78 | 79 | SearchSuggestionsProvider.add(this, query); 80 | 81 | showLoading(true); 82 | 83 | new SearchTask(service) { 84 | 85 | @Override 86 | protected void onPostExecute(SongResult result) { 87 | super.onPostExecute(result); 88 | 89 | displaySongs(result); 90 | showLoading(false); 91 | } 92 | }.execute(query); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/res/layout/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 21 | 22 | 31 | 32 | 39 | 40 | 48 | 49 | 57 | 58 | 59 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Play 5 | Album Art 6 | Play Server URL 7 | Play Token 8 | Pause 9 | Play 10 | Error contacting play server: {0} 11 | Unstarring \'\'{0}\'\' 12 | Starring \'\'{0}\'\' 13 | Starring \'\'{0}\'\' failed 14 | Unstarring \'\'{0}\'\' failed 15 | Refresh 16 | Save 17 | Removing \'\'{0}\'\' 18 | Removing \'\'{0}\'\' failed 19 | {0} songs added to the queue 20 | 1 song added to the queue 21 | No songs found 22 | Search 23 | Search music 24 | Clear history 25 | Search again 26 | Add to queue 27 | Adding to queue failed 28 | Adding to the queue 29 | Searching songs failed 30 | {0} selected 31 | 1 selected 32 | abc123 33 | play.example.com 34 | Failed to add starred songs to the queue 35 | Play favorites 36 | Enter a valid Play Server URL 37 | Get Token 38 | Settings 39 | Remove from queue 40 | Star this song 41 | Unstar this song 42 | View songs on this album 43 | View songs by this artist 44 | Select all 45 | Selected 46 | Starred status 47 | 48 | -------------------------------------------------------------------------------- /app/.settings/org.eclipse.jdt.ui.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true 3 | sp_cleanup.add_default_serial_version_id=true 4 | sp_cleanup.add_generated_serial_version_id=false 5 | sp_cleanup.add_missing_annotations=false 6 | sp_cleanup.add_missing_deprecated_annotations=true 7 | sp_cleanup.add_missing_methods=false 8 | sp_cleanup.add_missing_nls_tags=false 9 | sp_cleanup.add_missing_override_annotations=true 10 | sp_cleanup.add_missing_override_annotations_interface_methods=false 11 | sp_cleanup.add_serial_version_id=false 12 | sp_cleanup.always_use_blocks=true 13 | sp_cleanup.always_use_parentheses_in_expressions=false 14 | sp_cleanup.always_use_this_for_non_static_field_access=false 15 | sp_cleanup.always_use_this_for_non_static_method_access=false 16 | sp_cleanup.convert_to_enhanced_for_loop=false 17 | sp_cleanup.correct_indentation=false 18 | sp_cleanup.format_source_code=false 19 | sp_cleanup.format_source_code_changes_only=false 20 | sp_cleanup.make_local_variable_final=false 21 | sp_cleanup.make_parameters_final=false 22 | sp_cleanup.make_private_fields_final=true 23 | sp_cleanup.make_type_abstract_if_missing_method=false 24 | sp_cleanup.make_variable_declarations_final=false 25 | sp_cleanup.never_use_blocks=false 26 | sp_cleanup.never_use_parentheses_in_expressions=true 27 | sp_cleanup.on_save_use_additional_actions=true 28 | sp_cleanup.organize_imports=false 29 | sp_cleanup.qualify_static_field_accesses_with_declaring_class=false 30 | sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true 31 | sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true 32 | sp_cleanup.qualify_static_member_accesses_with_declaring_class=false 33 | sp_cleanup.qualify_static_method_accesses_with_declaring_class=false 34 | sp_cleanup.remove_private_constructors=true 35 | sp_cleanup.remove_trailing_whitespaces=true 36 | sp_cleanup.remove_trailing_whitespaces_all=true 37 | sp_cleanup.remove_trailing_whitespaces_ignore_empty=false 38 | sp_cleanup.remove_unnecessary_casts=false 39 | sp_cleanup.remove_unnecessary_nls_tags=false 40 | sp_cleanup.remove_unused_imports=false 41 | sp_cleanup.remove_unused_local_variables=false 42 | sp_cleanup.remove_unused_private_fields=true 43 | sp_cleanup.remove_unused_private_members=false 44 | sp_cleanup.remove_unused_private_methods=true 45 | sp_cleanup.remove_unused_private_types=true 46 | sp_cleanup.sort_members=false 47 | sp_cleanup.sort_members_all=false 48 | sp_cleanup.use_blocks=false 49 | sp_cleanup.use_blocks_only_for_return_and_throw=false 50 | sp_cleanup.use_parentheses_in_expressions=false 51 | sp_cleanup.use_this_for_non_static_field_access=false 52 | sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true 53 | sp_cleanup.use_this_for_non_static_method_access=false 54 | sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true 55 | -------------------------------------------------------------------------------- /app/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 12 | 13 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 37 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 54 | 58 | 59 | 63 | 64 | 67 | 68 | 69 | 70 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/PlayPreferences.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import static android.content.Context.MODE_PRIVATE; 19 | import android.content.Context; 20 | import android.content.SharedPreferences; 21 | import android.text.TextUtils; 22 | 23 | import java.net.URI; 24 | import java.net.URISyntaxException; 25 | 26 | /** 27 | * Stored settings for the configured Play server 28 | */ 29 | public class PlayPreferences { 30 | 31 | private static final String URL = "url"; 32 | 33 | private static final String TOKEN = "token"; 34 | 35 | private SharedPreferences preferences; 36 | 37 | /** 38 | * Create settings for context 39 | * 40 | * @param context 41 | */ 42 | public PlayPreferences(final Context context) { 43 | preferences = context.getSharedPreferences("play-settings", 44 | MODE_PRIVATE); 45 | } 46 | 47 | /** 48 | * Set preference key to given value 49 | * 50 | * @param key 51 | * @param value 52 | * @return this settings instance 53 | */ 54 | protected PlayPreferences set(final String key, String value) { 55 | if (value.length() == 0) 56 | value = null; 57 | preferences.edit().putString(key, value).commit(); 58 | return this; 59 | } 60 | 61 | /** 62 | * Does the given URL contain syntax errors? 63 | * 64 | * @param url 65 | * @return true if no syntax errors, false otherwise 66 | */ 67 | protected boolean isValidUrl(final String url) { 68 | if (TextUtils.isEmpty(url)) 69 | return false; 70 | try { 71 | new URI(url); 72 | return true; 73 | } catch (URISyntaxException e) { 74 | return false; 75 | } 76 | } 77 | 78 | /** 79 | * Get Play server URL 80 | * 81 | * @return URL or null if not configured 82 | */ 83 | public String getUrl() { 84 | String url = preferences.getString(URL, null); 85 | return isValidUrl(url) ? url : null; 86 | } 87 | 88 | /** 89 | * Set Play server URL 90 | * 91 | * @param url 92 | * @return this settings instance 93 | */ 94 | public PlayPreferences setUrl(final String url) { 95 | return isValidUrl(url) ? set(URL, url) : this; 96 | } 97 | 98 | /** 99 | * Get configured token for API calls 100 | * 101 | * @return login or null if not configured 102 | */ 103 | public String getToken() { 104 | return preferences.getString(TOKEN, null); 105 | } 106 | 107 | /** 108 | * Set token to be used for API calls 109 | * 110 | * @param token 111 | * @return this settings instance 112 | */ 113 | public PlayPreferences setToken(final String token) { 114 | return set(TOKEN, token); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 14 | 15 | 19 | 20 | 27 | 28 | 35 | 36 | 43 | 44 | 51 | 52 | 59 | 60 | 68 | 69 | 76 | 77 | 85 | 86 | 94 | 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/app/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.app; 17 | 18 | import static android.content.Intent.ACTION_VIEW; 19 | import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; 20 | import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; 21 | import static android.graphics.Paint.UNDERLINE_TEXT_FLAG; 22 | import android.content.Intent; 23 | import android.net.Uri; 24 | import android.os.Bundle; 25 | import android.text.TextUtils; 26 | import android.view.View; 27 | import android.view.View.OnClickListener; 28 | import android.widget.EditText; 29 | import android.widget.TextView; 30 | 31 | import com.actionbarsherlock.app.SherlockActivity; 32 | import com.actionbarsherlock.view.Menu; 33 | import com.actionbarsherlock.view.MenuItem; 34 | import com.github.kevinsawicki.wishlist.EditTextUtils; 35 | import com.github.kevinsawicki.wishlist.EditTextUtils.BooleanRunnable; 36 | import com.github.kevinsawicki.wishlist.Toaster; 37 | import com.github.kevinsawicki.wishlist.ViewFinder; 38 | import com.github.play.R.id; 39 | import com.github.play.R.layout; 40 | import com.github.play.R.menu; 41 | import com.github.play.R.string; 42 | import com.github.play.core.PlayPreferences; 43 | 44 | import java.net.URI; 45 | import java.net.URISyntaxException; 46 | 47 | /** 48 | * Activity to configure the settings for a Play server 49 | */ 50 | public class SettingsActivity extends SherlockActivity { 51 | 52 | private static final String PREFIX_HTTP = "http://"; 53 | 54 | private static final String PREFIX_HTTPS = "https://"; 55 | 56 | private PlayPreferences settings; 57 | 58 | private EditText tokenText; 59 | 60 | private EditText urlText; 61 | 62 | @Override 63 | protected void onCreate(Bundle savedInstanceState) { 64 | super.onCreate(savedInstanceState); 65 | 66 | setContentView(layout.settings); 67 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 68 | 69 | settings = new PlayPreferences(this); 70 | 71 | ViewFinder finder = new ViewFinder(this); 72 | tokenText = finder.find(id.et_token); 73 | String token = settings.getToken(); 74 | if (token != null) 75 | tokenText.setText(token); 76 | 77 | urlText = finder.find(id.et_url); 78 | String url = settings.getUrl(); 79 | if (url != null) 80 | urlText.setText(url); 81 | 82 | TextView tokenLink = finder.find(id.tv_token_link); 83 | tokenLink 84 | .setPaintFlags(UNDERLINE_TEXT_FLAG | tokenLink.getPaintFlags()); 85 | tokenLink.setOnClickListener(new OnClickListener() { 86 | 87 | public void onClick(View v) { 88 | String url = urlText.getText().toString(); 89 | boolean valid = !TextUtils.isEmpty(url); 90 | if (valid) { 91 | if (!url.startsWith(PREFIX_HTTP) 92 | & !url.startsWith(PREFIX_HTTPS)) 93 | url = PREFIX_HTTPS + url; 94 | if (!url.endsWith("/")) 95 | url += "/token"; 96 | else 97 | url += "token"; 98 | try { 99 | new URI(url); 100 | } catch (URISyntaxException e) { 101 | valid = false; 102 | } 103 | } 104 | 105 | if (valid) 106 | startActivity(new Intent(ACTION_VIEW, Uri.parse(url))); 107 | else 108 | Toaster.showLong(SettingsActivity.this, 109 | string.enter_play_server_url); 110 | } 111 | }); 112 | 113 | EditTextUtils.onDone(tokenText, new BooleanRunnable() { 114 | 115 | public boolean run() { 116 | saveSettings(); 117 | return true; 118 | } 119 | }); 120 | 121 | } 122 | 123 | @Override 124 | public boolean onCreateOptionsMenu(Menu optionsMenu) { 125 | getSupportMenuInflater().inflate(menu.settings, optionsMenu); 126 | return true; 127 | } 128 | 129 | private void saveSettings() { 130 | String token = tokenText.getText().toString().trim(); 131 | if (TextUtils.isEmpty(token)) 132 | return; 133 | String url = urlText.getText().toString().trim(); 134 | if (TextUtils.isEmpty(url)) 135 | return; 136 | 137 | if (!url.startsWith(PREFIX_HTTP) & !url.startsWith(PREFIX_HTTPS)) 138 | url = PREFIX_HTTPS + url; 139 | 140 | try { 141 | new URI(url); 142 | } catch (URISyntaxException e) { 143 | Toaster.showLong(this, string.enter_play_server_url); 144 | return; 145 | } 146 | 147 | boolean changed = !token.equals(settings.getToken()) 148 | || !url.equals(settings.getUrl()); 149 | if (changed) { 150 | settings.setToken(token).setUrl(url); 151 | setResult(RESULT_OK); 152 | } else 153 | setResult(RESULT_CANCELED); 154 | finish(); 155 | } 156 | 157 | @Override 158 | public boolean onOptionsItemSelected(MenuItem item) { 159 | switch (item.getItemId()) { 160 | case android.R.id.home: 161 | Intent intent = new Intent(this, PlayActivity.class); 162 | intent.addFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP); 163 | startActivity(intent); 164 | return true; 165 | case id.m_save: 166 | saveSettings(); 167 | return true; 168 | default: 169 | return super.onOptionsItemSelected(item); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /app/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 4.0.0 8 | 9 | com.github.play 10 | play-android 11 | 2.0-SNAPSHOT 12 | apk 13 | Play Android App 14 | Android client for a Play server 15 | 2012 16 | http://github.com/play/play-android 17 | 18 | 19 | https://github.com/play/play-android 20 | scm:git:git://github.com/play/play-android.git 21 | scm:git:git@github.com:play/play-android.git 22 | 23 | 24 | 25 | 26 | kevinsawicki@gmail.com 27 | Kevin Sawicki 28 | https://github.com/kevinsawicki 29 | kevinsawicki 30 | 31 | 32 | 33 | 34 | 35 | Apache License, Version 2.0 36 | http://www.apache.org/licenses/LICENSE-2.0.html 37 | repo 38 | 39 | 40 | 41 | 42 | https://github.com/play/play-android/issues 43 | GitHub Issues 44 | 45 | 46 | 47 | UTF-8 48 | 49 | 50 | 51 | 52 | com.google.android 53 | android 54 | 4.1.1.4 55 | provided 56 | 57 | 58 | 59 | com.github.kevinsawicki 60 | http-request 61 | 4.2 62 | 63 | 64 | 65 | com.github.kevinsawicki 66 | wishlist 67 | 0.9 68 | apklib 69 | 70 | 71 | 72 | com.github.kevinsawicki 73 | android-pusher 74 | 0.6 75 | 76 | 77 | 78 | com.google.code.gson 79 | gson 80 | 2.2.3 81 | 82 | 83 | 84 | com.actionbarsherlock 85 | library 86 | 4.1.0 87 | apklib 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | com.jayway.maven.plugins.android.generation2 96 | android-maven-plugin 97 | true 98 | 3.3.0 99 | 100 | 101 | 16 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | release 111 | 112 | 113 | 114 | org.apache.maven.plugins 115 | maven-jarsigner-plugin 116 | 117 | 118 | 119 | sign 120 | verify 121 | 122 | package 123 | true 124 | 125 | true 126 | 127 | ${sign.keystore} 128 | ${sign.alias} 129 | ${sign.storepass} 130 | ${sign.keypass} 131 | true 132 | 133 | 134 | 135 | 136 | 137 | com.jayway.maven.plugins.android.generation2 138 | android-maven-plugin 139 | true 140 | 141 | 142 | false 143 | 144 | 145 | true 146 | ${project.build.directory}/${project.artifactId}-release.apk 147 | 148 | 149 | 150 | false 151 | 152 | 153 | 154 | 155 | package 156 | 157 | zipalign 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/app/MusicStreamService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.app; 17 | 18 | import static android.media.AudioManager.STREAM_MUSIC; 19 | import static android.media.MediaPlayer.MEDIA_ERROR_SERVER_DIED; 20 | import static android.media.MediaPlayer.MEDIA_ERROR_UNKNOWN; 21 | import android.app.Service; 22 | import android.content.Context; 23 | import android.content.Intent; 24 | import android.media.MediaPlayer; 25 | import android.media.MediaPlayer.OnErrorListener; 26 | import android.media.MediaPlayer.OnPreparedListener; 27 | import android.os.IBinder; 28 | import android.text.TextUtils; 29 | import android.util.Log; 30 | 31 | import java.io.IOException; 32 | 33 | /** 34 | * Service that plays music 35 | */ 36 | public class MusicStreamService extends Service implements OnPreparedListener, 37 | OnErrorListener { 38 | 39 | /** 40 | * Action to use for broadcasting updates 41 | */ 42 | public static final String UPDATE = "com.github.play.action.STREAMING_UPDATE"; 43 | 44 | /** 45 | * Intent extra denoting whether music is currently streaming 46 | */ 47 | public static final String EXTRA_STREAMING = "streaming"; 48 | 49 | /** 50 | * Start streaming service to given URL 51 | * 52 | * @param context 53 | * @param url 54 | */ 55 | public static void start(final Context context, final String url) { 56 | Intent intent = new Intent(ACTION); 57 | if (!TextUtils.isEmpty(url)) 58 | intent.putExtra(EXTRA_URL, url); 59 | context.startService(intent); 60 | } 61 | 62 | /** 63 | * Start streaming service 64 | * 65 | * @param context 66 | */ 67 | public static void start(final Context context) { 68 | start(context, null); 69 | } 70 | 71 | /** 72 | * Stop service 73 | * 74 | * @param context 75 | */ 76 | public static void stop(final Context context) { 77 | context.stopService(new Intent(ACTION)); 78 | } 79 | 80 | /** 81 | * Action name for this service 82 | */ 83 | private static final String ACTION = "com.github.play.action.STREAM"; 84 | 85 | private static final String EXTRA_URL = "url"; 86 | 87 | private static final String TAG = "MusicStreamService"; 88 | 89 | private MediaPlayer player; 90 | 91 | private boolean prepared; 92 | 93 | private String url; 94 | 95 | @Override 96 | public void onDestroy() { 97 | super.onDestroy(); 98 | 99 | Log.d(TAG, "Destroying music stream service"); 100 | 101 | if (player != null) { 102 | player.release(); 103 | player = null; 104 | } 105 | } 106 | 107 | @Override 108 | public void onCreate() { 109 | super.onCreate(); 110 | 111 | Log.d(TAG, "Creating music stream service"); 112 | 113 | player = new MediaPlayer(); 114 | player.setOnPreparedListener(this); 115 | player.setOnErrorListener(this); 116 | player.setAudioStreamType(STREAM_MUSIC); 117 | } 118 | 119 | @Override 120 | public int onStartCommand(final Intent intent, final int flags, 121 | final int startId) { 122 | int result = super.onStartCommand(intent, flags, startId); 123 | 124 | if (intent == null) 125 | return result; 126 | 127 | String intentUrl = intent.getStringExtra(EXTRA_URL); 128 | if (!TextUtils.isEmpty(intentUrl) && !intentUrl.equals(url)) { 129 | prepareAsync(intentUrl); 130 | return result; 131 | } 132 | 133 | broadcastStatus(prepared); 134 | 135 | return result; 136 | } 137 | 138 | private void broadcastStatus(final boolean streaming) { 139 | Intent intent = new Intent(UPDATE); 140 | intent.putExtra(EXTRA_STREAMING, streaming); 141 | sendBroadcast(intent); 142 | } 143 | 144 | /** 145 | * Prepare a connection to the given URL 146 | * 147 | * @param url 148 | */ 149 | private void prepareAsync(final String url) { 150 | Log.d(TAG, "Preparing streaming connection to: " + url); 151 | 152 | try { 153 | player.setDataSource(url); 154 | this.url = url; 155 | player.prepareAsync(); 156 | } catch (IOException e) { 157 | Log.d(TAG, "Exception configuring streaming", e); 158 | } catch (IllegalArgumentException e) { 159 | Log.d(TAG, "Exception configuring streaming", e); 160 | } catch (IllegalStateException e) { 161 | Log.d(TAG, "Exception configuring streaming", e); 162 | } catch (SecurityException e) { 163 | Log.d(TAG, "Exception configuring streaming", e); 164 | } 165 | } 166 | 167 | @Override 168 | public IBinder onBind(final Intent intent) { 169 | return null; 170 | } 171 | 172 | public void onPrepared(final MediaPlayer mp) { 173 | Log.d(TAG, "Media player stream prepared"); 174 | try { 175 | mp.start(); 176 | prepared = true; 177 | broadcastStatus(true); 178 | } catch (IllegalStateException e) { 179 | Log.d(TAG, "Starting media player failed", e); 180 | } 181 | } 182 | 183 | public boolean onError(final MediaPlayer mp, final int what, final int extra) { 184 | switch (what) { 185 | case MEDIA_ERROR_SERVER_DIED: 186 | Log.d(TAG, "Media server died"); 187 | break; 188 | case MEDIA_ERROR_UNKNOWN: 189 | Log.d(TAG, "Unknown media player error"); 190 | break; 191 | default: 192 | Log.d(TAG, "Media player error: " + what + " Extra: " + extra); 193 | break; 194 | } 195 | 196 | prepared = false; 197 | broadcastStatus(false); 198 | return false; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /app/res/layout/song_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 16 | 17 | 24 | 25 | 32 | 33 | 36 | 37 | 40 | 41 | 44 | 45 | 48 | 49 | 50 | 51 | 52 | 58 | 59 | 60 | 61 | 67 | 68 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 92 | 93 | 101 | 102 | 108 | 109 | 110 | 111 | 112 | 118 | 119 | 127 | 128 | 133 | 134 | 135 | 136 | 137 | 143 | 144 | 152 | 153 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/widget/SearchListAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.widget; 17 | 18 | import static com.github.kevinsawicki.wishlist.ViewUpdater.FORMAT_INT; 19 | import android.app.Activity; 20 | import android.util.SparseBooleanArray; 21 | import android.view.View; 22 | 23 | import com.github.kevinsawicki.wishlist.MultiTypeAdapter; 24 | import com.github.play.R.drawable; 25 | import com.github.play.R.id; 26 | import com.github.play.R.layout; 27 | import com.github.play.core.PlayService; 28 | import com.github.play.core.Song; 29 | import com.github.play.core.SongResult; 30 | 31 | import java.util.ArrayList; 32 | import java.util.List; 33 | import java.util.concurrent.atomic.AtomicReference; 34 | 35 | /** 36 | * List adapter for searched songs 37 | */ 38 | public class SearchListAdapter extends MultiTypeAdapter { 39 | 40 | private static final int TYPE_ALBUM_HEADER = 0; 41 | 42 | private static final int TYPE_ALBUM = 1; 43 | 44 | private static final int TYPE_SONG_HEADER = 2; 45 | 46 | private static final int TYPE_SONG = 3; 47 | 48 | private final SparseBooleanArray selected; 49 | 50 | private final SongArtWrapper albumArt; 51 | 52 | private SongResult result; 53 | 54 | /** 55 | * Create search list adapter 56 | * 57 | * @param activity 58 | * @param service 59 | */ 60 | public SearchListAdapter(final Activity activity, 61 | final AtomicReference service) { 62 | super(activity); 63 | 64 | albumArt = new SongArtWrapper(activity, service); 65 | selected = new SparseBooleanArray(); 66 | } 67 | 68 | /** 69 | * Set songs to display 70 | * 71 | * @param result 72 | * @return this adapter 73 | */ 74 | public SearchListAdapter setSongs(final SongResult result) { 75 | this.result = result; 76 | clear(); 77 | addItem(TYPE_ALBUM_HEADER, TYPE_ALBUM_HEADER); 78 | addItems(TYPE_ALBUM, result.albums); 79 | addItem(TYPE_SONG_HEADER, TYPE_SONG_HEADER); 80 | addItems(TYPE_SONG, result.songs); 81 | return this; 82 | } 83 | 84 | /** 85 | * Toggle selected of item at given position 86 | * 87 | * @param position 88 | * @return true if song or album was selected, false otherwise 89 | */ 90 | public boolean toggleSelection(final int position) { 91 | return setSelected(position, !selected.get(position, false)); 92 | } 93 | 94 | /** 95 | * Set item as selected or deselected 96 | * 97 | * @param position 98 | * @param selected 99 | * @return true if song or album was selected, false otherwise 100 | */ 101 | public boolean setSelected(final int position, final boolean selected) { 102 | int type = getItemViewType(position); 103 | boolean validType = type == TYPE_SONG || type == TYPE_ALBUM; 104 | if (validType) 105 | if (selected) 106 | this.selected.put(position, true); 107 | else 108 | this.selected.delete(position); 109 | return validType; 110 | } 111 | 112 | @Override 113 | public int getViewTypeCount() { 114 | return 4; 115 | } 116 | 117 | @Override 118 | protected int getChildLayoutId(final int type) { 119 | switch (type) { 120 | case TYPE_ALBUM_HEADER: 121 | case TYPE_SONG_HEADER: 122 | return layout.search_separator; 123 | case TYPE_ALBUM: 124 | return layout.search_album; 125 | case TYPE_SONG: 126 | return layout.search_song; 127 | default: 128 | return -1; 129 | } 130 | } 131 | 132 | /** 133 | * Get number of items selected 134 | * 135 | * @return selected count 136 | */ 137 | public int getSelectedCount() { 138 | return selected.size(); 139 | } 140 | 141 | /** 142 | * Get selected songs 143 | * 144 | * @return non-null but possibly empty array of songs 145 | */ 146 | public Song[] getSelectedSongs() { 147 | List songs = new ArrayList(); 148 | for (int i = 0; i < selected.size(); i++) { 149 | int position = selected.keyAt(i); 150 | if (TYPE_SONG == getItemViewType(position)) 151 | songs.add((Song) getItem(position)); 152 | } 153 | return songs.toArray(new Song[songs.size()]); 154 | } 155 | 156 | /** 157 | * Get selected songs 158 | * 159 | * @return non-null but possibly empty array of songs 160 | */ 161 | public Song[] getSelectedAlbums() { 162 | List songs = new ArrayList(); 163 | for (int i = 0; i < selected.size(); i++) { 164 | int position = selected.keyAt(i); 165 | if (TYPE_ALBUM == getItemViewType(position)) 166 | songs.add((Song) getItem(position)); 167 | } 168 | return songs.toArray(new Song[songs.size()]); 169 | } 170 | 171 | @Override 172 | protected int[] getChildViewIds(final int type) { 173 | switch (type) { 174 | case TYPE_ALBUM_HEADER: 175 | case TYPE_SONG_HEADER: 176 | return new int[] { id.tv_label, id.tv_count }; 177 | case TYPE_ALBUM: 178 | return new int[] { id.tv_artist, id.tv_album, id.iv_art, 179 | id.iv_check }; 180 | case TYPE_SONG: 181 | return new int[] { id.tv_artist, id.tv_song, id.tv_album, 182 | id.iv_art, id.iv_check }; 183 | default: 184 | return null; 185 | } 186 | } 187 | 188 | /** 189 | * Update item at given position 190 | * 191 | * @param position 192 | * @param view 193 | * @param item 194 | */ 195 | public void update(final int position, final View view, final Object item) { 196 | setCurrentView(view); 197 | update(position, item, getItemViewType(position)); 198 | } 199 | 200 | @Override 201 | protected void update(final int position, final Object item, final int type) { 202 | switch (type) { 203 | case TYPE_ALBUM_HEADER: 204 | setText(0, "Albums"); 205 | setText(1, '(' + FORMAT_INT.format(result.albums.length) + ')'); 206 | return; 207 | case TYPE_ALBUM: 208 | Song album = (Song) item; 209 | if (selected.get(position)) 210 | imageView(3).setImageResource(drawable.selection_checked); 211 | else 212 | imageView(3).setImageResource(drawable.selection_unchecked); 213 | setText(0, album.artist); 214 | setText(1, album.album); 215 | 216 | albumArt.update(imageView(2), drawable.queued_cd, album, 217 | album.getAlbumId()); 218 | return; 219 | case TYPE_SONG_HEADER: 220 | setText(0, "Songs"); 221 | setText(1, '(' + FORMAT_INT.format(result.songs.length) + ')'); 222 | return; 223 | case TYPE_SONG: 224 | Song song = (Song) item; 225 | if (selected.get(position)) 226 | imageView(4).setImageResource(drawable.selection_checked); 227 | else 228 | imageView(4).setImageResource(drawable.selection_unchecked); 229 | setText(0, song.artist); 230 | setText(1, song.name); 231 | setText(2, song.album); 232 | 233 | albumArt.update(imageView(3), drawable.queued_cd, song); 234 | return; 235 | } 236 | } 237 | 238 | @Override 239 | public boolean isEnabled(final int position) { 240 | final int type = getItemViewType(position); 241 | return TYPE_ALBUM_HEADER != type && TYPE_SONG_HEADER != type; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/app/SongViewActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.app; 17 | 18 | import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; 19 | import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; 20 | import static com.github.play.app.PlayActivity.ACTION_QUEUE; 21 | import android.content.Intent; 22 | import android.graphics.drawable.AnimationDrawable; 23 | import android.os.Bundle; 24 | import android.util.Log; 25 | import android.view.View; 26 | import android.widget.AdapterView; 27 | import android.widget.AdapterView.OnItemClickListener; 28 | import android.widget.ListView; 29 | 30 | import com.actionbarsherlock.app.SherlockActivity; 31 | import com.actionbarsherlock.view.ActionMode; 32 | import com.actionbarsherlock.view.ActionMode.Callback; 33 | import com.actionbarsherlock.view.Menu; 34 | import com.actionbarsherlock.view.MenuItem; 35 | import com.github.kevinsawicki.wishlist.Toaster; 36 | import com.github.kevinsawicki.wishlist.ViewFinder; 37 | import com.github.kevinsawicki.wishlist.ViewUtils; 38 | import com.github.play.R.id; 39 | import com.github.play.R.layout; 40 | import com.github.play.R.menu; 41 | import com.github.play.R.string; 42 | import com.github.play.core.PlayPreferences; 43 | import com.github.play.core.PlayService; 44 | import com.github.play.core.QueueSongsTask; 45 | import com.github.play.core.Song; 46 | import com.github.play.core.SongResult; 47 | import com.github.play.widget.SearchListAdapter; 48 | 49 | import java.io.IOException; 50 | import java.text.MessageFormat; 51 | import java.util.LinkedHashSet; 52 | import java.util.Set; 53 | import java.util.concurrent.atomic.AtomicReference; 54 | 55 | /** 56 | * Base activity to display a list of songs and optionally queue them 57 | */ 58 | public abstract class SongViewActivity extends SherlockActivity implements 59 | OnItemClickListener { 60 | 61 | private static final String TAG = "SongViewActivity"; 62 | 63 | /** 64 | * Play service reference 65 | */ 66 | protected final AtomicReference service = new AtomicReference(); 67 | 68 | /** 69 | * List view 70 | */ 71 | protected ListView listView; 72 | 73 | private View loadingView; 74 | 75 | private SearchListAdapter adapter; 76 | 77 | private ActionMode actionMode; 78 | 79 | private Callback selectionModeCallback = new Callback() { 80 | 81 | public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 82 | int count = adapter.getSelectedCount(); 83 | if (count > 0) { 84 | mode.setTitle(MessageFormat.format( 85 | getString(string.multiple_selected), count)); 86 | return false; 87 | } else if (count == 1) { 88 | mode.setTitle(string.single_selected); 89 | return false; 90 | } else { 91 | mode.finish(); 92 | return true; 93 | } 94 | } 95 | 96 | public void onDestroyActionMode(ActionMode mode) { 97 | actionMode = null; 98 | unselectAllSongs(); 99 | } 100 | 101 | public boolean onCreateActionMode(ActionMode mode, Menu actionMenu) { 102 | mode.getMenuInflater().inflate(menu.add, actionMenu); 103 | return true; 104 | } 105 | 106 | public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 107 | switch (item.getItemId()) { 108 | case id.m_add: 109 | queueSelectedSongs(); 110 | mode.finish(); 111 | return true; 112 | default: 113 | return false; 114 | } 115 | } 116 | }; 117 | 118 | @Override 119 | protected void onCreate(Bundle savedInstanceState) { 120 | super.onCreate(savedInstanceState); 121 | 122 | setContentView(layout.search); 123 | 124 | final ViewFinder finder = new ViewFinder(this); 125 | loadingView = finder.find(id.ll_loading); 126 | loadingView.post(new Runnable() { 127 | 128 | public void run() { 129 | ((AnimationDrawable) finder.find(id.v_loading).getBackground()) 130 | .start(); 131 | } 132 | }); 133 | 134 | listView = finder.find(android.R.id.list); 135 | listView.setOnItemClickListener(this); 136 | adapter = new SearchListAdapter(this, service); 137 | listView.setAdapter(adapter); 138 | 139 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 140 | 141 | PlayPreferences settings = new PlayPreferences(this); 142 | service.set(new PlayService(settings.getUrl(), settings.getToken())); 143 | 144 | refreshSongs(); 145 | } 146 | 147 | /** 148 | * Show/hide loading view 149 | * 150 | * @param loading 151 | */ 152 | protected void showLoading(final boolean loading) { 153 | ViewUtils.setGone(loadingView, !loading); 154 | ViewUtils.setGone(listView, loading); 155 | } 156 | 157 | /** 158 | * Refresh songs being displayed 159 | */ 160 | protected abstract void refreshSongs(); 161 | 162 | /** 163 | * Display loaded songs 164 | * 165 | * @param result 166 | */ 167 | protected void displaySongs(final SongResult result) { 168 | if (result.exception == null) 169 | adapter.setSongs(result); 170 | else { 171 | Log.d(TAG, "Searching songs failed", result.exception); 172 | Toaster.showLong(SongViewActivity.this, string.search_failed); 173 | } 174 | 175 | showLoading(false); 176 | } 177 | 178 | @Override 179 | public boolean onOptionsItemSelected(MenuItem item) { 180 | switch (item.getItemId()) { 181 | case android.R.id.home: 182 | Intent intent = new Intent(this, PlayActivity.class); 183 | intent.addFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP); 184 | startActivity(intent); 185 | return true; 186 | case id.m_refresh: 187 | refreshSongs(); 188 | return true; 189 | case id.m_select_all: 190 | selectAllSongs(); 191 | return true; 192 | default: 193 | return super.onOptionsItemSelected(item); 194 | } 195 | } 196 | 197 | /** 198 | * Select all songs 199 | */ 200 | protected void selectAllSongs() { 201 | for (int i = 0; i < adapter.getCount(); i++) 202 | adapter.setSelected(i, true); 203 | 204 | adapter.notifyDataSetChanged(); 205 | startSelectionMode(); 206 | } 207 | 208 | /** 209 | * Select all songs 210 | */ 211 | protected void unselectAllSongs() { 212 | for (int i = 0; i < adapter.getCount(); i++) 213 | adapter.setSelected(i, false); 214 | 215 | adapter.notifyDataSetChanged(); 216 | } 217 | 218 | /** 219 | * Add selected songs to the queue and finish this activity when complete 220 | */ 221 | protected void queueSelectedSongs() { 222 | if (adapter.getSelectedCount() < 1) 223 | return; 224 | 225 | final Song[] albums = adapter.getSelectedAlbums(); 226 | final Song[] songs = adapter.getSelectedSongs(); 227 | 228 | Toaster.showShort(SongViewActivity.this, string.adding_to_queue); 229 | 230 | new QueueSongsTask(service) { 231 | 232 | @Override 233 | protected IOException doInBackground(Song... params) { 234 | if (albums.length > 0) { 235 | Set albumSongs = new LinkedHashSet(); 236 | for (Song album : albums) 237 | try { 238 | for (Song song : service.get().getSongs( 239 | album.artist, album.album)) 240 | albumSongs.add(song); 241 | } catch (IOException e) { 242 | return e; 243 | } 244 | if (!albumSongs.isEmpty()) { 245 | for (Song song : params) 246 | albumSongs.add(song); 247 | params = albumSongs 248 | .toArray(new Song[albumSongs.size()]); 249 | } 250 | } 251 | 252 | return super.doInBackground(params); 253 | } 254 | 255 | @Override 256 | protected void onPostExecute(IOException result) { 257 | super.onPostExecute(result); 258 | 259 | if (result != null) 260 | Toaster.showLong(SongViewActivity.this, 261 | string.queueing_failed); 262 | else { 263 | sendBroadcast(new Intent(ACTION_QUEUE)); 264 | setResult(RESULT_OK); 265 | finish(); 266 | } 267 | } 268 | }.execute(songs); 269 | } 270 | 271 | private void startSelectionMode() { 272 | if (actionMode != null) 273 | actionMode.invalidate(); 274 | else 275 | actionMode = startActionMode(selectionModeCallback); 276 | } 277 | 278 | public void onItemClick(AdapterView parent, View view, int position, 279 | long itemId) { 280 | if (adapter.toggleSelection(position)) { 281 | adapter.update(position, view, parent.getItemAtPosition(position)); 282 | startSelectionMode(); 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/core/PlayService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.core; 17 | 18 | import static android.os.Build.VERSION.SDK_INT; 19 | import static android.os.Build.VERSION_CODES.FROYO; 20 | import static com.github.kevinsawicki.http.HttpRequest.CHARSET_UTF8; 21 | import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES; 22 | 23 | import com.github.kevinsawicki.http.HttpRequest; 24 | import com.github.kevinsawicki.http.HttpRequest.HttpRequestException; 25 | import com.google.gson.Gson; 26 | import com.google.gson.GsonBuilder; 27 | import com.google.gson.JsonParseException; 28 | 29 | import java.io.File; 30 | import java.io.IOException; 31 | import java.io.Reader; 32 | import java.io.UnsupportedEncodingException; 33 | import java.lang.reflect.Type; 34 | import java.net.URLEncoder; 35 | 36 | /** 37 | * Service class to make requests to the Play API 38 | */ 39 | public class PlayService { 40 | 41 | private static final String USER_AGENT = "PlayAndroid/2.0"; 42 | 43 | private static final Song[] EMPTY_SONGS = new Song[0]; 44 | 45 | private static class SongWrapper { 46 | 47 | private Song[] songs; 48 | } 49 | 50 | static { 51 | // Disable http.keepAlive on Froyo and below 52 | if (SDK_INT <= FROYO) 53 | HttpRequest.keepAlive(false); 54 | } 55 | 56 | private static String encode(final String raw) { 57 | try { 58 | String encoded = URLEncoder.encode(raw, CHARSET_UTF8); 59 | return encoded.replace("+", "%20"); 60 | } catch (UnsupportedEncodingException e) { 61 | return raw; 62 | } 63 | } 64 | 65 | private final Gson gson = new GsonBuilder().setFieldNamingPolicy( 66 | LOWER_CASE_WITH_UNDERSCORES).create(); 67 | 68 | private final String baseUrl; 69 | 70 | private final String token; 71 | 72 | /** 73 | * Create play service using base URL 74 | * 75 | * @param baseUrl 76 | * @param token 77 | */ 78 | public PlayService(final String baseUrl, final String token) { 79 | if (baseUrl.endsWith("/")) 80 | this.baseUrl = baseUrl; 81 | else 82 | this.baseUrl = baseUrl + '/'; 83 | this.token = token; 84 | } 85 | 86 | /** 87 | * Create object of class type from content of request 88 | * 89 | * @param request 90 | * @param target 91 | * @return object of target class type 92 | * @throws IOException 93 | */ 94 | protected V fromJson(final HttpRequest request, final Type target) 95 | throws IOException { 96 | final Reader reader = request.bufferedReader(); 97 | try { 98 | return gson.fromJson(reader, target); 99 | } catch (JsonParseException e) { 100 | IOException ioException = new IOException("Parsing JSON failed"); 101 | ioException.initCause(e); 102 | throw ioException; 103 | } finally { 104 | try { 105 | reader.close(); 106 | } catch (IOException ignored) { 107 | // Ignored 108 | } 109 | } 110 | } 111 | 112 | /** 113 | * Get songs from request response 114 | * 115 | * @param request 116 | * @return non-null but possibly empty array of songs 117 | * @throws IOException 118 | */ 119 | protected Song[] getSongs(final HttpRequest request) throws IOException { 120 | final SongWrapper wrapper = fromJson(request, SongWrapper.class); 121 | if (wrapper != null && wrapper.songs != null) 122 | return wrapper.songs; 123 | else 124 | return EMPTY_SONGS; 125 | } 126 | 127 | /** 128 | * Verify request response code is a 200 OK and throw an exception when it 129 | * is not 130 | * 131 | * @param request 132 | * @return request 133 | * @throws IOException 134 | */ 135 | protected HttpRequest ok(HttpRequest request) throws IOException { 136 | if (!request.ok()) 137 | throw new IOException("Unexpected response code of " 138 | + request.code()); 139 | return request; 140 | } 141 | 142 | /** 143 | * Create a GET request for the given URL 144 | * 145 | * @param url 146 | * @return request 147 | */ 148 | protected HttpRequest get(final String url) { 149 | return HttpRequest.get(baseUrl + url).authorization(token) 150 | .userAgent(USER_AGENT); 151 | } 152 | 153 | /** 154 | * Create a POST request for the given URL 155 | * 156 | * @param url 157 | * @return request 158 | */ 159 | protected HttpRequest post(final String url) { 160 | String encoded = HttpRequest.encode(baseUrl + url); 161 | return HttpRequest.post(encoded).authorization(token) 162 | .userAgent(USER_AGENT); 163 | } 164 | 165 | /** 166 | * Create a DELETE request for the given URL 167 | * 168 | * @param url 169 | * @return request 170 | */ 171 | protected HttpRequest delete(final String url) { 172 | String encoded = HttpRequest.encode(baseUrl + url); 173 | return HttpRequest.delete(encoded).authorization(token) 174 | .userAgent(USER_AGENT); 175 | } 176 | 177 | /** 178 | * Get currently playing song 179 | * 180 | * @return song 181 | * @throws IOException 182 | */ 183 | public Song getNowPlaying() throws IOException { 184 | try { 185 | return fromJson(ok(get("now_playing")), Song.class); 186 | } catch (HttpRequestException e) { 187 | throw e.getCause(); 188 | } 189 | } 190 | 191 | /** 192 | * Get songs in the queue 193 | * 194 | * @return non-null but possibly empty array of songs 195 | * @throws IOException 196 | */ 197 | public Song[] getQueue() throws IOException { 198 | try { 199 | return getSongs(ok(get("queue"))); 200 | } catch (HttpRequestException e) { 201 | throw e.getCause(); 202 | } 203 | } 204 | 205 | /** 206 | * Get {@link StreamingInfo} for Play service 207 | * 208 | * @return URL to music stream 209 | * @throws IOException 210 | */ 211 | public StreamingInfo getStreamingInfo() throws IOException { 212 | try { 213 | return fromJson(ok(get("streaming_info")), StreamingInfo.class); 214 | } catch (HttpRequestException e) { 215 | throw e.getCause(); 216 | } 217 | } 218 | 219 | /** 220 | * Download art image for {@link Song} to given file 221 | * 222 | * @param song 223 | * @param file 224 | * @return true if succeeded, false if failed 225 | */ 226 | public boolean getArt(Song song, File file) { 227 | try { 228 | HttpRequest request = get("images/art/" + song.id + ".png"); 229 | if (request.ok() && request.contentLength() > 0) { 230 | request.receive(file); 231 | return true; 232 | } else 233 | return false; 234 | } catch (HttpRequestException e) { 235 | return false; 236 | } 237 | } 238 | 239 | /** 240 | * Star song 241 | * 242 | * @param song 243 | * @throws IOException 244 | */ 245 | public void star(Song song) throws IOException { 246 | try { 247 | ok(post("star?id=" + song.id)); 248 | } catch (HttpRequestException e) { 249 | throw e.getCause(); 250 | } 251 | } 252 | 253 | /** 254 | * Unstar song 255 | * 256 | * @param song 257 | * @throws IOException 258 | */ 259 | public void unstar(Song song) throws IOException { 260 | try { 261 | ok(delete("star?id=" + song.id)); 262 | } catch (HttpRequestException e) { 263 | throw e.getCause(); 264 | } 265 | } 266 | 267 | /** 268 | * Remove the given song from the queue 269 | * 270 | * @param song 271 | * @throws IOException 272 | */ 273 | public void dequeue(Song song) throws IOException { 274 | try { 275 | ok(delete("queue?id=" + song.id)); 276 | } catch (HttpRequestException e) { 277 | throw e.getCause(); 278 | } 279 | } 280 | 281 | /** 282 | * Add the given song to the queue 283 | * 284 | * @param song 285 | * @throws IOException 286 | */ 287 | public void queue(Song song) throws IOException { 288 | try { 289 | ok(post("queue?id=" + song.id)); 290 | } catch (HttpRequestException e) { 291 | throw e.getCause(); 292 | } 293 | } 294 | 295 | /** 296 | * Add starred songs to the queue 297 | * 298 | * @return non-null but possibly empty array of queued songs 299 | * @throws IOException 300 | */ 301 | public Song[] queueStars() throws IOException { 302 | try { 303 | return getSongs(ok(post("queue/stars"))); 304 | } catch (HttpRequestException e) { 305 | throw e.getCause(); 306 | } 307 | } 308 | 309 | /** 310 | * Requests some songs that match the freeform subject to be played 311 | * 312 | * @param subject 313 | * @return non-null but possibly empty array of queued songs 314 | * @throws IOException 315 | */ 316 | public Song[] queueSubject(String subject) throws IOException { 317 | try { 318 | return getSongs(ok(post("freeform?subject=" + subject))); 319 | } catch (HttpRequestException e) { 320 | throw e.getCause(); 321 | } 322 | } 323 | 324 | /** 325 | * Search for songs matching query 326 | * 327 | * @param query 328 | * @return non-null but possibly empty array of queued songs 329 | * @throws IOException 330 | */ 331 | public Song[] search(final String query) throws IOException { 332 | try { 333 | return getSongs(ok(get("search?q=" + encode(query)))); 334 | } catch (HttpRequestException e) { 335 | throw e.getCause(); 336 | } 337 | } 338 | 339 | /** 340 | * Get all songs on album by artist 341 | * 342 | * @param artist 343 | * @param album 344 | * @return non-null but possibly empty array of songs 345 | * @throws IOException 346 | */ 347 | public Song[] getSongs(final String artist, final String album) 348 | throws IOException { 349 | try { 350 | return getSongs(ok(get("artist/" + encode(artist) + "/album/" 351 | + encode(album)))); 352 | } catch (HttpRequestException e) { 353 | throw e.getCause(); 354 | } 355 | } 356 | 357 | /** 358 | * Get all songs by artist 359 | * 360 | * @param artist 361 | * @return non-null but possibly empty array of songs 362 | * @throws IOException 363 | */ 364 | public Song[] getSongs(final String artist) throws IOException { 365 | try { 366 | return getSongs(ok(get("artist/" + encode(artist)))); 367 | } catch (HttpRequestException e) { 368 | throw e.getCause(); 369 | } 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/app/StatusService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.app; 17 | 18 | import static android.app.Notification.FLAG_ONGOING_EVENT; 19 | import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; 20 | import static android.os.Build.VERSION.SDK_INT; 21 | import static android.os.Build.VERSION_CODES.HONEYCOMB; 22 | import static android.os.Build.VERSION_CODES.JELLY_BEAN; 23 | import android.app.Notification; 24 | import android.app.Notification.BigTextStyle; 25 | import android.app.Notification.Builder; 26 | import android.app.NotificationManager; 27 | import android.app.PendingIntent; 28 | import android.app.Service; 29 | import android.content.Context; 30 | import android.content.Intent; 31 | import android.os.IBinder; 32 | import android.text.TextUtils; 33 | import android.util.Log; 34 | 35 | import com.emorym.android_pusher.Pusher; 36 | import com.emorym.android_pusher.PusherCallback; 37 | import com.github.play.R.drawable; 38 | import com.github.play.core.Song; 39 | import com.github.play.core.SongPusher; 40 | import com.github.play.core.StatusUpdate; 41 | import com.github.play.widget.SongArtWrapper; 42 | 43 | import java.util.ArrayList; 44 | import java.util.List; 45 | import java.util.concurrent.Executor; 46 | import java.util.concurrent.Executors; 47 | 48 | import org.json.JSONArray; 49 | import org.json.JSONObject; 50 | 51 | /** 52 | * Service to receive push notifications about the currently playing song and 53 | * queued songs 54 | */ 55 | public class StatusService extends Service { 56 | 57 | /** 58 | * Action to use for broadcasting updates 59 | */ 60 | public static final String UPDATE = "com.github.play.action.STATUS_UPDATE"; 61 | 62 | /** 63 | * Intent extra key to a {@link StatusUpdate} handle 64 | */ 65 | public static final String EXTRA_UPDATE = "update"; 66 | 67 | /** 68 | * Start service with application key 69 | * 70 | * @param context 71 | * @param applicationKey 72 | * @param nowPlaying 73 | */ 74 | public static void start(final Context context, 75 | final String applicationKey, final Song nowPlaying) { 76 | Intent intent = new Intent(ACTION); 77 | intent.putExtra(EXTRA_KEY, applicationKey); 78 | intent.putExtra(EXTRA_SONG, nowPlaying); 79 | context.startService(intent); 80 | } 81 | 82 | /** 83 | * Start service with application key 84 | * 85 | * @param context 86 | * @param applicationKey 87 | * @param sendNotification 88 | * @param nowPlaying 89 | */ 90 | public static void start(final Context context, 91 | final String applicationKey, final boolean sendNotification, 92 | final Song nowPlaying) { 93 | Intent intent = new Intent(ACTION); 94 | intent.putExtra(EXTRA_KEY, applicationKey); 95 | intent.putExtra(EXTRA_NOTIFY, sendNotification); 96 | intent.putExtra(EXTRA_SONG, nowPlaying); 97 | context.startService(intent); 98 | } 99 | 100 | /** 101 | * Start service with application key 102 | * 103 | * @param context 104 | */ 105 | public static void stop(final Context context) { 106 | context.stopService(new Intent(ACTION)); 107 | } 108 | 109 | /** 110 | * Action to use for intents 111 | */ 112 | private static final String ACTION = "com.github.play.action.STATUS"; 113 | 114 | private static final String EXTRA_KEY = "applicationKey"; 115 | 116 | private static final String EXTRA_NOTIFY = "notify"; 117 | 118 | private static final String EXTRA_SONG = "song"; 119 | 120 | private static final String TAG = "StatusService"; 121 | 122 | private static Song parseSong(final JSONObject object) { 123 | String id = object.optString("id"); 124 | if (id == null) 125 | id = ""; 126 | 127 | String artist = object.optString("artist"); 128 | if (artist == null) 129 | artist = ""; 130 | 131 | String album = object.optString("album"); 132 | if (album == null) 133 | album = ""; 134 | 135 | String name = object.optString("name"); 136 | if (name == null) 137 | name = ""; 138 | 139 | return new Song(id, name, artist, album, object.optBoolean("starred"), 140 | object.optDouble("duration")); 141 | } 142 | 143 | private final Executor backgroundThread = Executors.newFixedThreadPool(1); 144 | 145 | private final PusherCallback callback = new PusherCallback() { 146 | 147 | public void onEvent(JSONObject eventData) { 148 | JSONObject nowPlaying = eventData.optJSONObject("now_playing"); 149 | if (nowPlaying == null) 150 | return; 151 | 152 | JSONArray upcomingSongs = eventData.optJSONArray("songs"); 153 | if (upcomingSongs == null) 154 | return; 155 | 156 | Song playing = parseSong(nowPlaying); 157 | 158 | List parsedSongs = new ArrayList(upcomingSongs.length()); 159 | for (int i = 0; i < upcomingSongs.length(); i++) { 160 | JSONObject song = upcomingSongs.optJSONObject(i); 161 | if (song == null) 162 | continue; 163 | parsedSongs.add(parseSong(song)); 164 | } 165 | Song[] queued = parsedSongs.toArray(new Song[parsedSongs.size()]); 166 | 167 | Intent intent = new Intent(UPDATE); 168 | intent.putExtra(EXTRA_UPDATE, new StatusUpdate(playing, queued)); 169 | sendBroadcast(intent); 170 | 171 | updateNotification(playing); 172 | } 173 | }; 174 | 175 | private Pusher pusher; 176 | 177 | private String applicationKey; 178 | 179 | private boolean sendNotification; 180 | 181 | private boolean notificationSent; 182 | 183 | @Override 184 | public IBinder onBind(final Intent intent) { 185 | return null; 186 | } 187 | 188 | @Override 189 | public void onDestroy() { 190 | super.onDestroy(); 191 | 192 | Log.d(TAG, "Destroying status service"); 193 | 194 | destroyPusher(pusher); 195 | stopForeground(true); 196 | } 197 | 198 | @Override 199 | public void onCreate() { 200 | super.onCreate(); 201 | 202 | Log.d(TAG, "Creating status service"); 203 | } 204 | 205 | @Override 206 | public int onStartCommand(final Intent intent, final int flags, 207 | final int startId) { 208 | if (intent != null) { 209 | String intentKey = intent.getStringExtra(EXTRA_KEY); 210 | if (!TextUtils.isEmpty(intentKey) 211 | && !intentKey.equals(applicationKey)) { 212 | destroyPusher(pusher); 213 | createPusher(intentKey); 214 | } 215 | 216 | boolean updateNotification = intent.hasExtra(EXTRA_NOTIFY); 217 | if (updateNotification) 218 | sendNotification = intent.getBooleanExtra(EXTRA_NOTIFY, false); 219 | Song song = (Song) intent.getSerializableExtra(EXTRA_SONG); 220 | if (updateNotification && !sendNotification) { 221 | clearNotification(); 222 | } else if (song != null) 223 | updateNotification(song); 224 | } 225 | 226 | return super.onStartCommand(intent, flags, startId); 227 | } 228 | 229 | private void destroyPusher(final Pusher pusher) { 230 | if (pusher != null) 231 | backgroundThread.execute(new Runnable() { 232 | 233 | public void run() { 234 | pusher.disconnect(); 235 | } 236 | }); 237 | } 238 | 239 | private void createPusher(String applicationKey) { 240 | this.applicationKey = applicationKey; 241 | 242 | final Pusher pusher = new SongPusher(applicationKey); 243 | backgroundThread.execute(new Runnable() { 244 | 245 | public void run() { 246 | pusher.subscribe("now_playing_updates").bind( 247 | "update_now_playing", callback); 248 | } 249 | }); 250 | this.pusher = pusher; 251 | } 252 | 253 | private CharSequence getTickerText(final Song song) { 254 | StringBuilder text = new StringBuilder(); 255 | text.append(song.name); 256 | if (!TextUtils.isEmpty(song.artist)) 257 | text.append(" by ").append(song.artist); 258 | return text; 259 | } 260 | 261 | private CharSequence getContentText(final Song song) { 262 | StringBuilder text = new StringBuilder(); 263 | text.append(song.name); 264 | if (!TextUtils.isEmpty(song.album)) 265 | text.append(" from ").append(song.album); 266 | return text; 267 | } 268 | 269 | private Notification createBigNotification(final Context context, 270 | final Song song, final PendingIntent intent) { 271 | Builder builder = new Builder(context); 272 | builder.setOngoing(true); 273 | builder.setSmallIcon(drawable.notification); 274 | builder.setTicker(getTickerText(song)); 275 | builder.setContentTitle(song.artist); 276 | builder.setLargeIcon(SongArtWrapper.getCachedArt(context, song)); 277 | CharSequence contextText = getContentText(song); 278 | builder.setContentText(contextText); 279 | builder.setContentIntent(intent); 280 | return new BigTextStyle(builder).bigText(contextText).build(); 281 | } 282 | 283 | @SuppressWarnings("deprecation") 284 | private Notification createNotification(final Context context, 285 | final Song song, final PendingIntent intent) { 286 | Notification notification = new Notification(); 287 | notification.icon = drawable.notification; 288 | notification.flags |= FLAG_ONGOING_EVENT; 289 | notification.tickerText = getTickerText(song); 290 | if (SDK_INT >= HONEYCOMB) 291 | notification.largeIcon = SongArtWrapper.getCachedArt(context, song); 292 | notification.setLatestEventInfo(context, song.artist, 293 | getContentText(song), intent); 294 | return notification; 295 | } 296 | 297 | private NotificationManager getNotificationManager() { 298 | return (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 299 | } 300 | 301 | private void clearNotification() { 302 | if (notificationSent) { 303 | stopForeground(true); 304 | notificationSent = false; 305 | } 306 | } 307 | 308 | private void updateNotification(Song song) { 309 | if (!sendNotification) 310 | return; 311 | 312 | Context context = getApplicationContext(); 313 | PendingIntent intent = PendingIntent.getActivity(context, 0, 314 | new Intent(context, PlayActivity.class), FLAG_UPDATE_CURRENT); 315 | 316 | Notification notification; 317 | if (SDK_INT >= JELLY_BEAN) 318 | notification = createBigNotification(context, song, intent); 319 | else 320 | notification = createNotification(context, song, intent); 321 | 322 | if (notificationSent) 323 | getNotificationManager().notify(1, notification); 324 | else { 325 | notificationSent = true; 326 | startForeground(1, notification); 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/widget/SongArtWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.widget; 17 | 18 | import static android.graphics.Bitmap.CompressFormat.PNG; 19 | import static android.graphics.Bitmap.Config.ARGB_8888; 20 | import static com.github.kevinsawicki.http.HttpRequest.CHARSET_UTF8; 21 | import android.app.Activity; 22 | import android.content.Context; 23 | import android.content.res.Resources; 24 | import android.graphics.Bitmap; 25 | import android.graphics.BitmapFactory; 26 | import android.graphics.BitmapFactory.Options; 27 | import android.graphics.Point; 28 | import android.graphics.drawable.BitmapDrawable; 29 | import android.graphics.drawable.Drawable; 30 | import android.graphics.drawable.LayerDrawable; 31 | import android.util.Log; 32 | import android.util.SparseArray; 33 | import android.widget.ImageView; 34 | 35 | import com.github.play.R.id; 36 | import com.github.play.core.PlayService; 37 | import com.github.play.core.Song; 38 | 39 | import java.io.File; 40 | import java.io.FileNotFoundException; 41 | import java.io.FileOutputStream; 42 | import java.io.IOException; 43 | import java.io.UnsupportedEncodingException; 44 | import java.math.BigInteger; 45 | import java.security.MessageDigest; 46 | import java.security.NoSuchAlgorithmException; 47 | import java.util.Arrays; 48 | import java.util.LinkedHashMap; 49 | import java.util.Map; 50 | import java.util.concurrent.Executor; 51 | import java.util.concurrent.Executors; 52 | import java.util.concurrent.atomic.AtomicReference; 53 | 54 | /** 55 | * View wrapper to load and display art for a {@link Song} 56 | */ 57 | public class SongArtWrapper { 58 | 59 | /** 60 | * Get cached art for song 61 | * 62 | * @param context 63 | * @param song 64 | * @return art or null if not available locally 65 | */ 66 | public static Bitmap getCachedArt(final Context context, final Song song) { 67 | File file = getArtFile(getArtDirectory(context), song); 68 | if (!isValid(file)) 69 | return null; 70 | 71 | Options options = new Options(); 72 | options.inDither = false; 73 | options.inPreferredConfig = ARGB_8888; 74 | Bitmap decoded = BitmapFactory.decodeFile(file.getAbsolutePath(), 75 | options); 76 | if (decoded == null) 77 | Log.d(TAG, "Decoding " + file.getName() + " failed"); 78 | return decoded; 79 | } 80 | 81 | private static final String TAG = "SongArtWrapper"; 82 | 83 | private static final int DIGEST_LENGTH = 40; 84 | 85 | private static final Executor EXECUTORS = Executors.newFixedThreadPool(1); 86 | 87 | private static final int MAX_RECENT = 50; 88 | 89 | private static final int MAX_SIZE_DP = 80; 90 | 91 | private static final MessageDigest SHA1_DIGEST; 92 | 93 | private static final String ART_FOLDER = "art"; 94 | 95 | /** 96 | * Version of art to display 97 | *

98 | * This counter should be incremented when {@link #MAX_SIZE_DP} changes or 99 | * if old art should be cleared and re-downloaded 100 | */ 101 | private static final int ART_VERSION = 1; 102 | 103 | static { 104 | MessageDigest digest; 105 | try { 106 | digest = MessageDigest.getInstance("SHA-1"); 107 | } catch (NoSuchAlgorithmException e) { 108 | digest = null; 109 | } 110 | SHA1_DIGEST = digest; 111 | } 112 | 113 | private static final SparseArray> RECENT_ART = new SparseArray>( 114 | 2); 115 | 116 | private static final SparseArray EMPTY_ART = new SparseArray( 117 | 2); 118 | 119 | private static String digest(Song song) { 120 | if (SHA1_DIGEST == null) 121 | return null; 122 | 123 | byte[] value; 124 | try { 125 | value = song.getAlbumId().getBytes(CHARSET_UTF8); 126 | } catch (UnsupportedEncodingException e) { 127 | return null; 128 | } 129 | 130 | byte[] digested; 131 | synchronized (SHA1_DIGEST) { 132 | SHA1_DIGEST.reset(); 133 | digested = SHA1_DIGEST.digest(value); 134 | } 135 | String hashed = new BigInteger(1, digested).toString(16); 136 | int padding = DIGEST_LENGTH - hashed.length(); 137 | if (padding > 0) { 138 | char[] zeros = new char[padding]; 139 | Arrays.fill(zeros, '0'); 140 | hashed = new String(zeros) + hashed; 141 | } 142 | return hashed; 143 | } 144 | 145 | private static Map createCacheMap() { 146 | return new LinkedHashMap(MAX_RECENT, 1.0F) { 147 | 148 | private static final long serialVersionUID = -3434208982358063608L; 149 | 150 | @Override 151 | protected boolean removeEldestEntry( 152 | Map.Entry eldest) { 153 | return size() >= MAX_RECENT; 154 | } 155 | }; 156 | } 157 | 158 | private static Drawable getCachedArt(final int drawable, final Song song) { 159 | final String digest = digest(song); 160 | if (digest != null) 161 | synchronized (RECENT_ART) { 162 | Map cache = RECENT_ART.get(drawable); 163 | return cache != null ? cache.get(digest) : null; 164 | } 165 | else 166 | return null; 167 | } 168 | 169 | private static Drawable getEmptyArt(final int drawable, 170 | final Context context) { 171 | Drawable cached = EMPTY_ART.get(drawable); 172 | if (cached == null) { 173 | cached = context.getResources().getDrawable(drawable); 174 | EMPTY_ART.put(drawable, cached); 175 | } 176 | return cached; 177 | } 178 | 179 | private static void putCachedArt(final int drawable, final Song song, 180 | final Drawable bitmap) { 181 | if (bitmap == null) 182 | return; 183 | 184 | final String digest = digest(song); 185 | if (digest != null) 186 | synchronized (RECENT_ART) { 187 | Map cache = RECENT_ART.get(drawable); 188 | if (cache == null) { 189 | cache = createCacheMap(); 190 | RECENT_ART.put(drawable, cache); 191 | } 192 | cache.put(digest, bitmap); 193 | } 194 | } 195 | 196 | private static Point getSize(final File file) { 197 | final Options options = new Options(); 198 | options.inJustDecodeBounds = true; 199 | 200 | BitmapFactory.decodeFile(file.getAbsolutePath(), options); 201 | if (options.outWidth <= 0 || options.outHeight <= 0) 202 | Log.d(TAG, "Decoding bounds of " + file.getName() + " failed"); 203 | return new Point(options.outWidth, options.outHeight); 204 | } 205 | 206 | /** 207 | * Get art file for song 208 | * 209 | * @param parent 210 | * @param song 211 | * @return file 212 | */ 213 | protected static File getArtFile(final File parent, final Song song) { 214 | return new File(parent, digest(song) + ".png"); 215 | } 216 | 217 | /** 218 | * Get art directory 219 | * 220 | * @param context 221 | * @return directory for storing song art 222 | */ 223 | protected static File getArtDirectory(Context context) { 224 | File artFolder = new File(context.getCacheDir(), ART_FOLDER 225 | + ART_VERSION); 226 | if (!artFolder.exists()) 227 | artFolder.mkdirs(); 228 | return artFolder; 229 | } 230 | 231 | /** 232 | * Is file non-null, existent and non-empty? 233 | * 234 | * @param file 235 | * @return true if valid, false otherwise 236 | */ 237 | protected static boolean isValid(final File file) { 238 | return file != null && file.exists() && file.length() > 0; 239 | } 240 | 241 | private final File artFolder; 242 | 243 | private final AtomicReference service; 244 | 245 | private final int maxSize; 246 | 247 | private final Activity activity; 248 | 249 | private boolean oldArtDeleted; 250 | 251 | /** 252 | * Create view wrapper to display art for a {@link Song} 253 | * 254 | * @param activity 255 | * @param service 256 | */ 257 | public SongArtWrapper(Activity activity, 258 | final AtomicReference service) { 259 | this.activity = activity; 260 | artFolder = getArtDirectory(activity); 261 | this.service = service; 262 | Resources resources = activity.getResources(); 263 | maxSize = Math.round(resources.getDisplayMetrics().density 264 | * MAX_SIZE_DP + 0.5F); 265 | } 266 | 267 | /** 268 | * Decode {@link Bitmap} from given {@link File} 269 | * 270 | * @param file 271 | * @return bitmap 272 | */ 273 | protected Bitmap decode(final File file) { 274 | Point size = getSize(file); 275 | int currWidth = size.x; 276 | int currHeight = size.y; 277 | 278 | int scale = 1; 279 | while (currWidth >= maxSize || currHeight >= maxSize) { 280 | currWidth /= 2; 281 | currHeight /= 2; 282 | scale *= 2; 283 | } 284 | 285 | Options options = new Options(); 286 | options.inDither = false; 287 | options.inSampleSize = scale; 288 | options.inPreferredConfig = ARGB_8888; 289 | Bitmap decoded = BitmapFactory.decodeFile(file.getAbsolutePath(), 290 | options); 291 | if (decoded == null) 292 | Log.d(TAG, "Decoding " + file.getName() + " failed"); 293 | return decoded; 294 | } 295 | 296 | /** 297 | * Write {@link Bitmap} to given {@link File} 298 | * 299 | * @param bitmap 300 | * @param file 301 | * @return bitmap 302 | */ 303 | protected Bitmap write(final Bitmap bitmap, final File file) { 304 | FileOutputStream stream = null; 305 | try { 306 | stream = new FileOutputStream(file); 307 | if (!bitmap.compress(PNG, 100, stream)) 308 | Log.d(TAG, "Compressing " + file.getName() + " failed"); 309 | return bitmap; 310 | } catch (FileNotFoundException e) { 311 | return bitmap; 312 | } finally { 313 | if (stream != null) 314 | try { 315 | stream.close(); 316 | } catch (IOException ignored) { 317 | // Ignored 318 | } 319 | } 320 | } 321 | 322 | private void updateDrawable(final ImageView view, final Drawable art) { 323 | view.setTag(null); 324 | view.setImageDrawable(art); 325 | } 326 | 327 | /** 328 | * Delete file or directory include child files 329 | * 330 | * @param file 331 | */ 332 | private void delete(final File file) { 333 | if (!file.exists()) 334 | return; 335 | 336 | if (file.isDirectory()) { 337 | Log.d(TAG, "Deleting art directory: " + file.getName()); 338 | File[] children = file.listFiles(); 339 | if (children != null) 340 | for (File child : children) 341 | delete(child); 342 | } 343 | file.delete(); 344 | } 345 | 346 | /** 347 | * Delete art in old folders 348 | */ 349 | private void deleteOldArt() { 350 | if (oldArtDeleted) 351 | return; 352 | 353 | File root = artFolder.getParentFile(); 354 | delete(new File(root, ART_FOLDER)); 355 | for (int i = 0; i < ART_VERSION; i++) 356 | delete(new File(root, ART_FOLDER + i)); 357 | oldArtDeleted = true; 358 | } 359 | 360 | /** 361 | * Update view with art for song album 362 | * 363 | * @param artView 364 | * @param drawable 365 | * a layer drawable with an album art layer 366 | * @param song 367 | */ 368 | public void update(final ImageView artView, final int drawable, 369 | final Song song) { 370 | update(artView, drawable, song, song); 371 | } 372 | 373 | /** 374 | * Update view with art for song album 375 | * 376 | * @param artView 377 | * @param drawable 378 | * a layer drawable with an album art layer 379 | * @param song 380 | * @param tag 381 | */ 382 | public void update(final ImageView artView, final int drawable, 383 | final Song song, final Object tag) { 384 | if (song == null) { 385 | updateDrawable(artView, getEmptyArt(drawable, activity)); 386 | return; 387 | } 388 | 389 | Drawable cachedBitmap = getCachedArt(drawable, song); 390 | if (cachedBitmap != null) { 391 | updateDrawable(artView, cachedBitmap); 392 | return; 393 | } 394 | 395 | updateDrawable(artView, getEmptyArt(drawable, activity)); 396 | artView.setTag(tag); 397 | 398 | EXECUTORS.execute(new Runnable() { 399 | 400 | public void run() { 401 | deleteOldArt(); 402 | 403 | Drawable image = getCachedArt(drawable, song); 404 | 405 | if (image == null) { 406 | File artFile = getArtFile(artFolder, song); 407 | Bitmap bitmap = null; 408 | if (isValid(artFile)) 409 | bitmap = decode(artFile); 410 | else if (service.get().getArt(song, artFile)) { 411 | bitmap = decode(artFile); 412 | if (bitmap != null) 413 | write(bitmap, artFile); 414 | } 415 | 416 | if (bitmap != null) { 417 | image = new BitmapDrawable(artView.getResources(), 418 | bitmap); 419 | LayerDrawable layers = (LayerDrawable) activity 420 | .getResources().getDrawable(drawable); 421 | layers.setDrawableByLayerId(id.i_album_art, image); 422 | putCachedArt(drawable, song, layers); 423 | image = layers; 424 | } 425 | } 426 | 427 | final Drawable imageDrawable = image; 428 | activity.runOnUiThread(new Runnable() { 429 | 430 | public void run() { 431 | if (tag.equals(artView.getTag())) 432 | if (imageDrawable != null) 433 | updateDrawable(artView, imageDrawable); 434 | else 435 | updateDrawable(artView, 436 | getEmptyArt(drawable, activity)); 437 | } 438 | }); 439 | } 440 | }); 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/play/app/PlayActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Kevin Sawicki 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.play.app; 17 | 18 | import static com.github.play.app.MusicStreamService.EXTRA_STREAMING; 19 | import static com.github.play.app.StatusService.EXTRA_UPDATE; 20 | import android.content.BroadcastReceiver; 21 | import android.content.Context; 22 | import android.content.Intent; 23 | import android.content.IntentFilter; 24 | import android.graphics.drawable.AnimationDrawable; 25 | import android.os.Bundle; 26 | import android.text.TextUtils; 27 | import android.util.Log; 28 | import android.view.View; 29 | import android.view.View.OnClickListener; 30 | import android.widget.AdapterView; 31 | import android.widget.AdapterView.OnItemClickListener; 32 | import android.widget.ListView; 33 | 34 | import com.actionbarsherlock.app.SherlockActivity; 35 | import com.actionbarsherlock.view.Menu; 36 | import com.actionbarsherlock.view.MenuItem; 37 | import com.github.kevinsawicki.wishlist.LightDialog; 38 | import com.github.kevinsawicki.wishlist.Toaster; 39 | import com.github.kevinsawicki.wishlist.ViewFinder; 40 | import com.github.kevinsawicki.wishlist.ViewUtils; 41 | import com.github.play.R.drawable; 42 | import com.github.play.R.id; 43 | import com.github.play.R.layout; 44 | import com.github.play.R.menu; 45 | import com.github.play.R.string; 46 | import com.github.play.core.DequeueSongTask; 47 | import com.github.play.core.FetchSettingsTask; 48 | import com.github.play.core.FetchStatusTask; 49 | import com.github.play.core.PlayPreferences; 50 | import com.github.play.core.PlayService; 51 | import com.github.play.core.QueueStarsTask; 52 | import com.github.play.core.Song; 53 | import com.github.play.core.SongCallback; 54 | import com.github.play.core.SongResult; 55 | import com.github.play.core.StarSongTask; 56 | import com.github.play.core.StatusUpdate; 57 | import com.github.play.core.StreamingInfo; 58 | import com.github.play.core.UnstarSongTask; 59 | import com.github.play.widget.PlayListAdapter; 60 | import com.github.play.widget.SongArtWrapper; 61 | 62 | import java.io.IOException; 63 | import java.text.MessageFormat; 64 | import java.util.concurrent.atomic.AtomicReference; 65 | 66 | /** 67 | * Activity to view what is playing and listen to music 68 | */ 69 | public class PlayActivity extends SherlockActivity implements SongCallback, 70 | OnItemClickListener { 71 | 72 | /** 73 | * Action for broadcasting that the queue has been updated 74 | */ 75 | public static final String ACTION_QUEUE = "com.github.play.action.QUEUE_UPDATE"; 76 | 77 | private static final String TAG = "PlayActivity"; 78 | 79 | private static final String STREAMING_INFO = "streamingInfo"; 80 | 81 | private static final int REQUEST_SETTINGS = 1; 82 | 83 | private ListView listView; 84 | 85 | private View loadingView; 86 | 87 | private final AtomicReference playService = new AtomicReference(); 88 | 89 | private View nowPlayingView; 90 | 91 | private PlayListAdapter playListAdapter; 92 | 93 | private boolean streaming; 94 | 95 | private boolean queueEmpty = true; 96 | 97 | private MenuItem playItem; 98 | 99 | private MenuItem refreshItem; 100 | 101 | private MenuItem searchItem; 102 | 103 | private MenuItem playStarsItem; 104 | 105 | private PlayPreferences settings; 106 | 107 | private StreamingInfo streamingInfo; 108 | 109 | private Song nowPlaying; 110 | 111 | private final BroadcastReceiver updateReceiver = new BroadcastReceiver() { 112 | 113 | public void onReceive(Context context, Intent intent) { 114 | StatusUpdate update = (StatusUpdate) intent 115 | .getSerializableExtra(EXTRA_UPDATE); 116 | onUpdate(update.playing, update.queued); 117 | } 118 | }; 119 | 120 | private final BroadcastReceiver queueReceiver = new BroadcastReceiver() { 121 | 122 | public void onReceive(Context context, Intent intent) { 123 | runOnUiThread(new Runnable() { 124 | 125 | public void run() { 126 | refreshSongs(); 127 | } 128 | }); 129 | } 130 | }; 131 | 132 | private final BroadcastReceiver streamReceiver = new BroadcastReceiver() { 133 | 134 | public void onReceive(Context context, Intent intent) { 135 | final boolean streaming = intent.getBooleanExtra(EXTRA_STREAMING, 136 | false); 137 | runOnUiThread(new Runnable() { 138 | 139 | public void run() { 140 | setStreaming(streaming); 141 | } 142 | }); 143 | } 144 | }; 145 | 146 | @Override 147 | protected void onDestroy() { 148 | super.onDestroy(); 149 | 150 | if (!streaming) 151 | StatusService.stop(getApplicationContext()); 152 | 153 | unregisterReceiver(updateReceiver); 154 | unregisterReceiver(streamReceiver); 155 | unregisterReceiver(queueReceiver); 156 | } 157 | 158 | @Override 159 | protected void onSaveInstanceState(Bundle outState) { 160 | super.onSaveInstanceState(outState); 161 | 162 | if (hasStreamingInfo()) 163 | outState.putSerializable(STREAMING_INFO, streamingInfo); 164 | } 165 | 166 | @Override 167 | public void onCreate(Bundle savedInstanceState) { 168 | super.onCreate(savedInstanceState); 169 | 170 | setContentView(layout.main); 171 | 172 | final ViewFinder finder = new ViewFinder(this); 173 | 174 | loadingView = finder.find(id.ll_loading); 175 | loadingView.post(new Runnable() { 176 | 177 | public void run() { 178 | ((AnimationDrawable) finder.find(id.v_loading).getBackground()) 179 | .start(); 180 | } 181 | }); 182 | 183 | playListAdapter = new PlayListAdapter(this, layout.queued, playService); 184 | 185 | listView = finder.find(android.R.id.list); 186 | listView.setOnItemClickListener(this); 187 | 188 | nowPlayingView = finder.find(id.ll_now_playing); 189 | ViewUtils.setGone(nowPlayingView, true); 190 | nowPlayingView.setOnClickListener(new OnClickListener() { 191 | 192 | public void onClick(View v) { 193 | showSongDialog(nowPlaying, 0); 194 | } 195 | }); 196 | playListAdapter.initialize(nowPlayingView); 197 | listView.setAdapter(playListAdapter); 198 | 199 | if (savedInstanceState != null) 200 | streamingInfo = (StreamingInfo) savedInstanceState 201 | .getSerializable(STREAMING_INFO); 202 | 203 | settings = new PlayPreferences(this); 204 | 205 | if (hasSettings()) { 206 | playService.set(new PlayService(settings.getUrl(), settings 207 | .getToken())); 208 | load(); 209 | } else 210 | startActivityForResult(new Intent(this, SettingsActivity.class), 211 | REQUEST_SETTINGS); 212 | 213 | registerReceiver(updateReceiver, new IntentFilter(StatusService.UPDATE)); 214 | registerReceiver(streamReceiver, new IntentFilter( 215 | MusicStreamService.UPDATE)); 216 | registerReceiver(queueReceiver, new IntentFilter(ACTION_QUEUE)); 217 | } 218 | 219 | private void setStreaming(final boolean streaming) { 220 | this.streaming = streaming; 221 | updatePlayMenuItem(); 222 | } 223 | 224 | private void updatePlayMenuItem() { 225 | if (playItem == null) 226 | return; 227 | 228 | if (streaming) 229 | playItem.setIcon(drawable.action_pause).setTitle(string.pause); 230 | else 231 | playItem.setIcon(drawable.action_play).setTitle(string.play); 232 | } 233 | 234 | private void startStream() { 235 | if (!hasSettings() || !hasStreamingInfo()) 236 | return; 237 | 238 | Log.d(TAG, "Starting stream"); 239 | 240 | setStreaming(true); 241 | 242 | Context context = getApplicationContext(); 243 | MusicStreamService.start(context, streamingInfo.streamUrl); 244 | StatusService.start(context, streamingInfo.pusherKey, true, nowPlaying); 245 | 246 | refreshSongs(); 247 | } 248 | 249 | private void load() { 250 | if (!hasSettings()) 251 | return; 252 | 253 | if (!hasStreamingInfo()) 254 | new FetchSettingsTask(playService) { 255 | 256 | protected void onPostExecute(PlaySettings result) { 257 | if (result.streamingInfo != null) { 258 | streamingInfo = result.streamingInfo; 259 | load(); 260 | } else 261 | onError(result.exception); 262 | } 263 | 264 | }.execute(); 265 | else if (isReady()) { 266 | setMenuItemsEnabled(true); 267 | Context context = getApplicationContext(); 268 | MusicStreamService.start(context); 269 | StatusService.start(context, streamingInfo.pusherKey, nowPlaying); 270 | refreshSongs(); 271 | } 272 | } 273 | 274 | private void setMenuItemsEnabled(final boolean enabled) { 275 | if (playItem != null) 276 | playItem.setEnabled(enabled && hasStreamingInfo()); 277 | if (refreshItem != null) 278 | refreshItem.setEnabled(enabled); 279 | if (searchItem != null) 280 | searchItem.setEnabled(enabled); 281 | if (playStarsItem != null) 282 | playStarsItem.setEnabled(enabled); 283 | } 284 | 285 | private void stopStream() { 286 | if (!hasSettings()) 287 | return; 288 | 289 | Log.d(TAG, "Stopping stream"); 290 | 291 | setStreaming(false); 292 | 293 | Context context = getApplicationContext(); 294 | MusicStreamService.stop(context); 295 | if (hasStreamingInfo()) 296 | StatusService.start(context, streamingInfo.pusherKey, false, 297 | nowPlaying); 298 | } 299 | 300 | public void onUpdate(final Song playing, final Song[] queued) { 301 | runOnUiThread(new Runnable() { 302 | 303 | public void run() { 304 | nowPlaying = playing; 305 | updateSongs(playing, queued); 306 | } 307 | }); 308 | } 309 | 310 | private void showLoading(final boolean loading) { 311 | ViewUtils.setGone(loadingView, !loading); 312 | ViewUtils.setGone(nowPlayingView, loading); 313 | ViewUtils.setGone(listView, loading); 314 | } 315 | 316 | private void updateSongs(final Song playing, final Song[] queued) { 317 | queueEmpty = playing == null && (queued == null || queued.length == 0); 318 | 319 | playListAdapter.updatePlaying(nowPlayingView, playing); 320 | playListAdapter.setItems(queued); 321 | 322 | showLoading(false); 323 | } 324 | 325 | private void refreshSongs() { 326 | if (!isReady()) 327 | return; 328 | 329 | if (queueEmpty) 330 | showLoading(true); 331 | 332 | new FetchStatusTask(playService, this).execute(); 333 | } 334 | 335 | private boolean hasSettings() { 336 | final PlayPreferences settings = this.settings; 337 | return settings != null && settings.getUrl() != null 338 | && settings.getToken() != null; 339 | } 340 | 341 | private boolean hasStreamingInfo() { 342 | final StreamingInfo info = streamingInfo; 343 | return info != null && !TextUtils.isEmpty(info.pusherKey) 344 | && !TextUtils.isEmpty(info.streamUrl); 345 | } 346 | 347 | private boolean isReady() { 348 | return hasSettings() && playService.get() != null; 349 | } 350 | 351 | @Override 352 | public boolean onOptionsItemSelected(MenuItem item) { 353 | switch (item.getItemId()) { 354 | case id.m_pause: 355 | if (streaming) 356 | stopStream(); 357 | else 358 | startStream(); 359 | return true; 360 | case id.m_refresh: 361 | refreshSongs(); 362 | return true; 363 | case id.m_settings: 364 | startActivityForResult(new Intent(this, SettingsActivity.class), 365 | REQUEST_SETTINGS); 366 | return true; 367 | case id.m_search: 368 | onSearchRequested(); 369 | return true; 370 | case id.m_play_stars: 371 | playStars(); 372 | return true; 373 | default: 374 | return super.onOptionsItemSelected(item); 375 | } 376 | 377 | } 378 | 379 | @Override 380 | public boolean onCreateOptionsMenu(Menu optionsMenu) { 381 | getSupportMenuInflater().inflate(menu.main, optionsMenu); 382 | 383 | playItem = optionsMenu.findItem(id.m_pause); 384 | updatePlayMenuItem(); 385 | 386 | refreshItem = optionsMenu.findItem(id.m_refresh); 387 | searchItem = optionsMenu.findItem(id.m_search); 388 | playStarsItem = optionsMenu.findItem(id.m_play_stars); 389 | 390 | if (isReady()) 391 | setMenuItemsEnabled(true); 392 | 393 | return true; 394 | } 395 | 396 | public void onError(IOException e) { 397 | Log.d(TAG, "Play server exception", e); 398 | 399 | ViewUtils.setGone(loadingView, true); 400 | 401 | Toaster.showLong(this, string.error_contacting_play_server, 402 | e.getMessage()); 403 | } 404 | 405 | @Override 406 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 407 | if (requestCode == REQUEST_SETTINGS && resultCode == RESULT_OK) { 408 | stopStream(); 409 | if (hasSettings()) { 410 | playService.set(new PlayService(settings.getUrl(), settings 411 | .getToken())); 412 | streamingInfo = null; 413 | load(); 414 | } 415 | return; 416 | } 417 | 418 | super.onActivityResult(requestCode, resultCode, data); 419 | } 420 | 421 | private void starSong(final Song song) { 422 | if (!isReady()) 423 | return; 424 | 425 | Toaster.showShort(this, string.starring_song, song.name); 426 | 427 | new StarSongTask(playService) { 428 | 429 | @Override 430 | protected void onPostExecute(IOException result) { 431 | super.onPostExecute(result); 432 | 433 | if (result != null) 434 | Toaster.showLong(PlayActivity.this, string.starring_failed, 435 | song.name); 436 | else 437 | refreshSongs(); 438 | } 439 | }.execute(song); 440 | } 441 | 442 | private void unstarSong(final Song song) { 443 | if (!isReady()) 444 | return; 445 | 446 | Toaster.showShort(this, string.unstarring_song, song.name); 447 | 448 | new UnstarSongTask(playService) { 449 | 450 | @Override 451 | protected void onPostExecute(IOException result) { 452 | super.onPostExecute(result); 453 | 454 | if (result != null) 455 | Toaster.showShort(PlayActivity.this, 456 | string.unstarring_failed, song.name); 457 | else 458 | refreshSongs(); 459 | } 460 | }.execute(song); 461 | } 462 | 463 | private void dequeueSong(final Song song) { 464 | if (!isReady()) 465 | return; 466 | 467 | Toaster.showShort(PlayActivity.this, string.removing_song, song.name); 468 | new DequeueSongTask(playService) { 469 | 470 | @Override 471 | protected void onPostExecute(IOException result) { 472 | super.onPostExecute(result); 473 | 474 | if (result != null) 475 | Toaster.showShort(PlayActivity.this, 476 | string.removing_song_failed, song.name); 477 | else 478 | refreshSongs(); 479 | } 480 | }.execute(song); 481 | } 482 | 483 | private void playStars() { 484 | if (!isReady()) 485 | return; 486 | 487 | new QueueStarsTask(playService) { 488 | 489 | @Override 490 | protected void onPostExecute(SongResult result) { 491 | super.onPostExecute(result); 492 | 493 | String message; 494 | if (result.exception != null) 495 | message = getString(string.queueing_stars_failed); 496 | else if (result.songs.length > 1) 497 | message = MessageFormat.format( 498 | getString(string.multiple_songs_queued), 499 | result.songs.length); 500 | else if (result.songs.length == 1) 501 | message = getString(string.single_song_queued); 502 | else 503 | message = getString(string.no_songs_found); 504 | 505 | Toaster.showShort(PlayActivity.this, message); 506 | 507 | refreshSongs(); 508 | } 509 | }.execute(); 510 | } 511 | 512 | private void showSongDialog(final Song song, final double previousTime) { 513 | if (song == null) 514 | return; 515 | 516 | final LightDialog dialog = LightDialog.create(this); 517 | dialog.setCancelable(true); 518 | dialog.setCanceledOnTouchOutside(true); 519 | View dialogView = getLayoutInflater().inflate(layout.song_dialog, null); 520 | ViewFinder finder = new ViewFinder(dialogView); 521 | finder.setText(id.tv_album, song.album); 522 | finder.setText(id.tv_artist, song.artist); 523 | finder.setText(id.tv_song, song.name); 524 | 525 | long minutes = Math.round(song.duration / 60); 526 | long seconds = Math.round(song.duration / 60); 527 | if (seconds > 10) 528 | finder.setText(id.tv_duration, "Time: " + minutes + ':' + seconds); 529 | else if (seconds > 1) 530 | finder.setText(id.tv_duration, "Time: " + minutes + ":0" + seconds); 531 | else 532 | finder.setText(id.tv_duration, "Time: " + minutes + ":00"); 533 | 534 | long backlog = Math.round(previousTime / 60); 535 | if (backlog > 60) { 536 | backlog = Math.round((float) backlog / 60); 537 | if (backlog > 1) 538 | finder.setText(id.tv_soon, new StringBuilder( 539 | "This song will be playing in about ").append(backlog) 540 | .append(" hours")); 541 | else 542 | finder.setText(id.tv_soon, new StringBuilder( 543 | "This song will be playing in about 1 hour")); 544 | } else if (backlog > 1) 545 | finder.setText(id.tv_soon, new StringBuilder( 546 | "This song will be playing in about ").append(backlog) 547 | .append(" minutes")); 548 | else if (backlog == 1) 549 | finder.setText(id.tv_soon, "This song will be playing in about "); 550 | else 551 | ViewUtils.setGone(finder.find(id.tv_soon), true); 552 | 553 | if (song.starred) { 554 | finder.setText(id.tv_star, string.unstar_this_song); 555 | finder.setDrawable(id.iv_star_icon, drawable.action_unstar); 556 | } else { 557 | finder.setText(id.tv_star, string.star_this_song); 558 | finder.setDrawable(id.iv_star_icon, drawable.action_star); 559 | } 560 | finder.onClick(id.rl_star_area, new Runnable() { 561 | 562 | public void run() { 563 | dialog.dismiss(); 564 | if (song.starred) 565 | unstarSong(song); 566 | else 567 | starSong(song); 568 | } 569 | }); 570 | finder.onClick(id.rl_remove_area, new Runnable() { 571 | 572 | public void run() { 573 | dialog.dismiss(); 574 | dequeueSong(song); 575 | } 576 | }); 577 | finder.onClick(id.rl_search_album_area, new Runnable() { 578 | 579 | public void run() { 580 | dialog.dismiss(); 581 | startActivity(ViewAlbumActivity.createIntent( 582 | getApplicationContext(), song)); 583 | } 584 | }); 585 | finder.onClick(id.rl_search_artist_area, new Runnable() { 586 | 587 | public void run() { 588 | dialog.dismiss(); 589 | startActivity(ViewArtistActivity.createIntent( 590 | getApplicationContext(), song)); 591 | } 592 | }); 593 | new SongArtWrapper(this, playService).update( 594 | finder.imageView(id.iv_art), drawable.queued_cd, song); 595 | dialog.setView(dialogView, 0, 0, 0, 0); 596 | dialog.show(); 597 | } 598 | 599 | public void onItemClick(AdapterView listView, View view, int position, 600 | long itemId) { 601 | double previousTime = 0; 602 | for (int i = 0; i < position; i++) 603 | previousTime += ((Song) listView.getItemAtPosition(i)).duration; 604 | showSongDialog((Song) listView.getItemAtPosition(position), 605 | previousTime); 606 | } 607 | } --------------------------------------------------------------------------------