├── src └── main │ ├── assets │ └── .gitkeep │ ├── res │ ├── drawable-hdpi │ │ ├── zzz_ph_bt_stop.png │ │ ├── zzz_ph_bt_back_enabled.png │ │ ├── zzz_ph_bt_play_enabled.png │ │ ├── zzz_ph_bt_skip_enabled.png │ │ ├── zzz_ph_ic_notification.png │ │ ├── zzz_ph_bt_back_disabled.png │ │ ├── zzz_ph_bt_pause_disabled.png │ │ ├── zzz_ph_bt_pause_enabled.png │ │ ├── zzz_ph_bt_play_disabled.png │ │ └── zzz_ph_bt_skip_disabled.png │ ├── drawable-mdpi │ │ ├── zzz_ph_bt_back_enabled.png │ │ ├── zzz_ph_bt_play_enabled.png │ │ ├── zzz_ph_bt_skip_enabled.png │ │ ├── zzz_ph_ic_notification.png │ │ ├── zzz_ph_bt_back_disabled.png │ │ ├── zzz_ph_bt_pause_disabled.png │ │ ├── zzz_ph_bt_pause_enabled.png │ │ ├── zzz_ph_bt_play_disabled.png │ │ └── zzz_ph_bt_skip_disabled.png │ ├── drawable-hdpi-v9 │ │ └── zzz_ph_ic_notification.png │ ├── drawable-ldpi-v9 │ │ └── zzz_ph_ic_notification.png │ ├── drawable-mdpi-v9 │ │ └── zzz_ph_ic_notification.png │ ├── drawable-xhdpi │ │ ├── zzz_ph_bt_back_disabled.png │ │ ├── zzz_ph_bt_back_enabled.png │ │ ├── zzz_ph_bt_pause_disabled.png │ │ ├── zzz_ph_bt_pause_enabled.png │ │ ├── zzz_ph_bt_play_disabled.png │ │ ├── zzz_ph_bt_play_enabled.png │ │ ├── zzz_ph_bt_skip_disabled.png │ │ ├── zzz_ph_bt_skip_enabled.png │ │ └── zzz_ph_ic_notification.png │ ├── drawable-hdpi-v11 │ │ └── zzz_ph_ic_notification.png │ ├── drawable-ldpi-v11 │ │ └── zzz_ph_ic_notification.png │ ├── drawable-mdpi-v11 │ │ └── zzz_ph_ic_notification.png │ ├── drawable-xhdpi-v11 │ │ └── zzz_ph_ic_notification.png │ ├── drawable-xhdpi-v9 │ │ └── zzz_ph_ic_notification.png │ ├── values-v8 │ │ └── player_hater_flags.xml │ ├── values-v14 │ │ └── player_hater_flags.xml │ ├── values-v18 │ │ └── player_hater_flags.xml │ ├── values-v11 │ │ └── player_hater_flags.xml │ ├── values-v16 │ │ └── player_hater_flags.xml │ ├── drawable │ │ ├── zzz_ph_bt_back.xml │ │ ├── zzz_ph_bt_play.xml │ │ ├── zzz_ph_bt_skip.xml │ │ └── zzz_ph_bt_pause.xml │ ├── values │ │ └── player_hater_flags.xml │ ├── xml │ │ └── zzz_ph_config_defaults.xml │ ├── layout-v11 │ │ └── zzz_ph_hc_notification.xml │ └── layout-v16 │ │ └── zzz_ph_jbb_notification.xml │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── prx │ │ └── playerhater │ │ ├── broadcast │ │ ├── RemoteControlButtonReceiver.java │ │ ├── OnAudioFocusChangedListener.java │ │ ├── HeadphoneButtonGestureHelper.java │ │ └── Receiver.java │ │ ├── PlayerHaterListener.java │ │ ├── util │ │ ├── Log.java │ │ ├── PlaylistParser.java │ │ └── Config.java │ │ ├── wrappers │ │ ├── ThreadsafeServicePlayerHater.java │ │ └── ServicePlayerHater.java │ │ ├── plugins │ │ ├── PebblePlugin.java │ │ ├── ScrubbableLockScreenControlsPlugin.java │ │ ├── AudioFocusPlugin.java │ │ ├── ExpandableNotificationPlugin.java │ │ ├── AbstractPlugin.java │ │ ├── NotificationPlugin.java │ │ ├── PlayerHaterListenerPlugin.java │ │ └── LockScreenControlsPlugin.java │ │ ├── BroadcastReceiver.java │ │ ├── songs │ │ ├── Songs.java │ │ ├── RemoteSong.java │ │ ├── SongHost.java │ │ └── SongQueue.java │ │ ├── Song.java │ │ ├── PlayerHater.java │ │ ├── mediaplayer │ │ ├── Player.java │ │ ├── MediaPlayerPool.java │ │ └── SynchronousPlayer.java │ │ ├── service │ │ └── PlayerStateWatcher.java │ │ ├── ipc │ │ ├── PlayerHaterClient.java │ │ ├── ClientPlugin.java │ │ └── PlayerHaterServer.java │ │ └── PlaybackService.java │ └── aidl │ └── org │ └── prx │ └── playerhater │ └── ipc │ ├── IPlayerHaterClient.aidl │ └── IPlayerHaterServer.aidl ├── legacyLibraryProject └── PlayerHater │ ├── res │ ├── assets │ ├── AndroidManifest.xml │ ├── src │ └── org │ │ └── prx │ │ └── playerhater │ │ ├── Song.java │ │ ├── util │ │ ├── Log.java │ │ ├── Config.java │ │ ├── IPlayerHater.java │ │ └── PlaylistParser.java │ │ ├── PlayerHater.java │ │ ├── songs │ │ ├── Songs.java │ │ ├── SongHost.java │ │ ├── SongQueue.java │ │ └── RemoteSong.java │ │ ├── BroadcastReceiver.java │ │ ├── PlaybackService.java │ │ ├── PlayerHaterPlugin.java │ │ ├── PlayerHaterListener.java │ │ ├── broadcast │ │ ├── Receiver.java │ │ ├── HeadphoneButtonGestureHelper.java │ │ ├── OnAudioFocusChangedListener.java │ │ └── RemoteControlButtonReceiver.java │ │ ├── ipc │ │ ├── ClientPlugin.java │ │ ├── IPlayerHaterClient.aidl │ │ ├── IPlayerHaterServer.aidl │ │ ├── PlayerHaterClient.java │ │ ├── PlayerHaterServer.java │ │ └── ServerPlayerHater.java │ │ ├── mediaplayer │ │ ├── Player.java │ │ ├── StatelyPlayer.java │ │ ├── MediaPlayerPool.java │ │ ├── SynchronousPlayer.java │ │ └── PlaylistSupportingPlayer.java │ │ ├── plugins │ │ ├── PebblePlugin.java │ │ ├── AbstractPlugin.java │ │ ├── AudioFocusPlugin.java │ │ ├── PluginCollection.java │ │ ├── BackgroundedPlugin.java │ │ ├── NotificationPlugin.java │ │ ├── LockScreenControlsPlugin.java │ │ ├── PlayerHaterListenerPlugin.java │ │ ├── TouchableNotificationPlugin.java │ │ └── ExpandableNotificationPlugin.java │ │ ├── wrappers │ │ ├── BoundPlayerHater.java │ │ ├── ServicePlayerHater.java │ │ ├── ThreadsafePlayerHater.java │ │ └── ThreadsafeServicePlayerHater.java │ │ └── service │ │ ├── PlayerHaterService.java │ │ └── PlayerStateWatcher.java │ ├── proguard-project.txt │ ├── project.properties │ └── build.xml ├── lint.xml ├── .gitignore ├── proguard-player-hater.txt ├── gradle.properties ├── gradlew.bat └── README.md /src/main/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/res: -------------------------------------------------------------------------------- 1 | ../../src/main/res -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/assets: -------------------------------------------------------------------------------- 1 | ../../src/main/assets -------------------------------------------------------------------------------- /lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | ../../src/main/AndroidManifest.xml -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/Song.java: -------------------------------------------------------------------------------- 1 | ../../../../../../src/main/java/org/prx/playerhater/Song.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/util/Log.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/util/Log.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/PlayerHater.java: -------------------------------------------------------------------------------- 1 | ../../../../../../src/main/java/org/prx/playerhater/PlayerHater.java -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/zzz_ph_bt_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-hdpi/zzz_ph_bt_stop.png -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/songs/Songs.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/songs/Songs.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/util/Config.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/util/Config.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/BroadcastReceiver.java: -------------------------------------------------------------------------------- 1 | ../../../../../../src/main/java/org/prx/playerhater/BroadcastReceiver.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/PlaybackService.java: -------------------------------------------------------------------------------- 1 | ../../../../../../src/main/java/org/prx/playerhater/PlaybackService.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/PlayerHaterPlugin.java: -------------------------------------------------------------------------------- 1 | ../../../../../../src/main/java/org/prx/playerhater/PlayerHaterPlugin.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/songs/SongHost.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/songs/SongHost.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/songs/SongQueue.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/songs/SongQueue.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/PlayerHaterListener.java: -------------------------------------------------------------------------------- 1 | ../../../../../../src/main/java/org/prx/playerhater/PlayerHaterListener.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/broadcast/Receiver.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/broadcast/Receiver.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/ipc/ClientPlugin.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/ipc/ClientPlugin.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/mediaplayer/Player.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/mediaplayer/Player.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/songs/RemoteSong.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/songs/RemoteSong.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/util/IPlayerHater.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/util/IPlayerHater.java -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/zzz_ph_bt_back_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-hdpi/zzz_ph_bt_back_enabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/zzz_ph_bt_play_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-hdpi/zzz_ph_bt_play_enabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/zzz_ph_bt_skip_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-hdpi/zzz_ph_bt_skip_enabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/zzz_ph_ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-hdpi/zzz_ph_ic_notification.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/zzz_ph_bt_back_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-mdpi/zzz_ph_bt_back_enabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/zzz_ph_bt_play_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-mdpi/zzz_ph_bt_play_enabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/zzz_ph_bt_skip_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-mdpi/zzz_ph_bt_skip_enabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/zzz_ph_ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-mdpi/zzz_ph_ic_notification.png -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/plugins/PebblePlugin.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/plugins/PebblePlugin.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/util/PlaylistParser.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/util/PlaylistParser.java -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi-v9/zzz_ph_ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-hdpi-v9/zzz_ph_ic_notification.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/zzz_ph_bt_back_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-hdpi/zzz_ph_bt_back_disabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/zzz_ph_bt_pause_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-hdpi/zzz_ph_bt_pause_disabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/zzz_ph_bt_pause_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-hdpi/zzz_ph_bt_pause_enabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/zzz_ph_bt_play_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-hdpi/zzz_ph_bt_play_disabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/zzz_ph_bt_skip_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-hdpi/zzz_ph_bt_skip_disabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-ldpi-v9/zzz_ph_ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-ldpi-v9/zzz_ph_ic_notification.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi-v9/zzz_ph_ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-mdpi-v9/zzz_ph_ic_notification.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/zzz_ph_bt_back_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-mdpi/zzz_ph_bt_back_disabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/zzz_ph_bt_pause_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-mdpi/zzz_ph_bt_pause_disabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/zzz_ph_bt_pause_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-mdpi/zzz_ph_bt_pause_enabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/zzz_ph_bt_play_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-mdpi/zzz_ph_bt_play_disabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/zzz_ph_bt_skip_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-mdpi/zzz_ph_bt_skip_disabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/zzz_ph_bt_back_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-xhdpi/zzz_ph_bt_back_disabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/zzz_ph_bt_back_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-xhdpi/zzz_ph_bt_back_enabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/zzz_ph_bt_pause_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-xhdpi/zzz_ph_bt_pause_disabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/zzz_ph_bt_pause_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-xhdpi/zzz_ph_bt_pause_enabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/zzz_ph_bt_play_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-xhdpi/zzz_ph_bt_play_disabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/zzz_ph_bt_play_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-xhdpi/zzz_ph_bt_play_enabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/zzz_ph_bt_skip_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-xhdpi/zzz_ph_bt_skip_disabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/zzz_ph_bt_skip_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-xhdpi/zzz_ph_bt_skip_enabled.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/zzz_ph_ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-xhdpi/zzz_ph_ic_notification.png -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/ipc/IPlayerHaterClient.aidl: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/aidl/org/prx/playerhater/ipc/IPlayerHaterClient.aidl -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/ipc/IPlayerHaterServer.aidl: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/aidl/org/prx/playerhater/ipc/IPlayerHaterServer.aidl -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/ipc/PlayerHaterClient.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/ipc/PlayerHaterClient.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/ipc/PlayerHaterServer.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/ipc/PlayerHaterServer.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/ipc/ServerPlayerHater.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/ipc/ServerPlayerHater.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/plugins/AbstractPlugin.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/plugins/AbstractPlugin.java -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi-v11/zzz_ph_ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-hdpi-v11/zzz_ph_ic_notification.png -------------------------------------------------------------------------------- /src/main/res/drawable-ldpi-v11/zzz_ph_ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-ldpi-v11/zzz_ph_ic_notification.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi-v11/zzz_ph_ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-mdpi-v11/zzz_ph_ic_notification.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi-v11/zzz_ph_ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-xhdpi-v11/zzz_ph_ic_notification.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi-v9/zzz_ph_ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cqr/PlayerHater/HEAD/src/main/res/drawable-xhdpi-v9/zzz_ph_ic_notification.png -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/mediaplayer/StatelyPlayer.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/mediaplayer/StatelyPlayer.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/plugins/AudioFocusPlugin.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/plugins/AudioFocusPlugin.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/plugins/PluginCollection.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/plugins/PluginCollection.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/wrappers/BoundPlayerHater.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/wrappers/BoundPlayerHater.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/mediaplayer/MediaPlayerPool.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/mediaplayer/MediaPlayerPool.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/plugins/BackgroundedPlugin.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/plugins/BackgroundedPlugin.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/plugins/NotificationPlugin.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/plugins/NotificationPlugin.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/service/PlayerHaterService.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/service/PlayerHaterService.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/service/PlayerStateWatcher.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/service/PlayerStateWatcher.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/wrappers/ServicePlayerHater.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/wrappers/ServicePlayerHater.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/mediaplayer/SynchronousPlayer.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/mediaplayer/SynchronousPlayer.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/wrappers/ThreadsafePlayerHater.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/wrappers/ThreadsafePlayerHater.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/plugins/LockScreenControlsPlugin.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/plugins/LockScreenControlsPlugin.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/plugins/PlayerHaterListenerPlugin.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/plugins/PlayerHaterListenerPlugin.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/plugins/TouchableNotificationPlugin.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/plugins/TouchableNotificationPlugin.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/broadcast/HeadphoneButtonGestureHelper.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/broadcast/HeadphoneButtonGestureHelper.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/broadcast/OnAudioFocusChangedListener.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/broadcast/OnAudioFocusChangedListener.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/broadcast/RemoteControlButtonReceiver.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/broadcast/RemoteControlButtonReceiver.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/mediaplayer/PlaylistSupportingPlayer.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/mediaplayer/PlaylistSupportingPlayer.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/plugins/ExpandableNotificationPlugin.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/plugins/ExpandableNotificationPlugin.java -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/src/org/prx/playerhater/wrappers/ThreadsafeServicePlayerHater.java: -------------------------------------------------------------------------------- 1 | ../../../../../../../src/main/java/org/prx/playerhater/wrappers/ThreadsafeServicePlayerHater.java -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | gen 3 | local.properties 4 | .settings/* 5 | .project 6 | .classpath 7 | .DS_Store 8 | .gradle 9 | build 10 | .idea 11 | gradle 12 | gradlew 13 | PlayerHater.iml 14 | out 15 | traces.txt 16 | -------------------------------------------------------------------------------- /proguard-player-hater.txt: -------------------------------------------------------------------------------- 1 | # Player Hater Plugins are instantiated and used utilizing reflection, 2 | # so they need to be protected from proguard. 3 | -keep public class * implements org.prx.playerhater.PlayerHaterPlugin -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/res/values-v8/player_hater_flags.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | true 19 | 20 | -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /src/main/res/values-v14/player_hater_flags.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | false 19 | true 20 | 21 | -------------------------------------------------------------------------------- /src/main/res/values-v18/player_hater_flags.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | false 19 | true 20 | 21 | -------------------------------------------------------------------------------- /src/main/res/values-v11/player_hater_flags.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | false 19 | true 20 | 21 | -------------------------------------------------------------------------------- /src/main/res/values-v16/player_hater_flags.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | false 19 | true 20 | 21 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Settings specified in this file will override any Gradle settings 5 | # configured through the IDE. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /src/main/res/drawable/zzz_ph_bt_back.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/res/drawable/zzz_ph_bt_play.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/res/drawable/zzz_ph_bt_skip.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/res/drawable/zzz_ph_bt_pause.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/broadcast/RemoteControlButtonReceiver.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.broadcast; 17 | 18 | import android.content.Context; 19 | 20 | interface RemoteControlButtonReceiver { 21 | public void onRemoteControlButtonPressed(int keyCode, Context context); 22 | } 23 | -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/project.properties: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 | # This file is automatically generated by Android Tools. 17 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 18 | # 19 | # This file must be checked in Version Control Systems. 20 | # 21 | # To customize properties used by the Ant build system use, 22 | # "ant.properties", and override values to adapt the script to your 23 | # project structure. 24 | 25 | # Project target. 26 | target=android-18 27 | android.library=true 28 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/PlayerHaterListener.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater; 17 | 18 | /** 19 | * A simple interface for receiving events from the {@linkplain PlayerHater} 20 | * service. 21 | * 22 | * @since 1.0.0 23 | * @version 2.0.0 24 | * 25 | * @author Chris Rhoden 26 | */ 27 | public interface PlayerHaterListener { 28 | void onStopped(); 29 | 30 | void onPaused(Song song); 31 | 32 | void onLoading(Song song); 33 | 34 | void onPlaying(Song song, int progress); 35 | 36 | void onStreaming(Song song); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/res/values/player_hater_flags.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | true 20 | false 21 | false 22 | false 23 | false 24 | 25 | Stop 26 | Previous 27 | Album Art 28 | Skip 29 | Toggle Playback 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/util/Log.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.util; 17 | 18 | import org.prx.playerhater.BuildConfig; 19 | 20 | public class Log { 21 | 22 | public static String TAG = "PlayerHater"; 23 | 24 | public static void v(String msg) { 25 | if (BuildConfig.DEBUG) { 26 | android.util.Log.v(TAG, msg); 27 | } 28 | } 29 | 30 | public static void d(String msg) { 31 | if (BuildConfig.DEBUG) { 32 | android.util.Log.d(TAG, msg); 33 | } 34 | } 35 | 36 | public static void e(String string, Exception e) { 37 | android.util.Log.e(TAG, string, e); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/wrappers/ThreadsafeServicePlayerHater.java: -------------------------------------------------------------------------------- 1 | package org.prx.playerhater.wrappers; 2 | 3 | import org.prx.playerhater.ipc.IPlayerHaterClient; 4 | import org.prx.playerhater.service.PlayerHaterService; 5 | 6 | import android.app.Notification; 7 | 8 | public class ThreadsafeServicePlayerHater extends ThreadsafePlayerHater { 9 | private final PlayerHaterService mService; 10 | 11 | public ThreadsafeServicePlayerHater(PlayerHaterService service) { 12 | super(new ServicePlayerHater(service)); 13 | mService = service; 14 | } 15 | 16 | /* 17 | * Service Specific stuff. 18 | */ 19 | 20 | public void setClient(IPlayerHaterClient client) { 21 | mService.setClient(client); 22 | } 23 | 24 | public void onRemoteControlButtonPressed(int keyCode) { 25 | mService.onRemoteControlButtonPressed(keyCode); 26 | } 27 | 28 | public void startForeground(int notificationNu, Notification notification) { 29 | mService.startForeground(notificationNu, notification); 30 | } 31 | 32 | public void stopForeground(boolean removeNotification) { 33 | mService.stopForeground(removeNotification); 34 | mService.quit(); 35 | } 36 | 37 | public boolean pause(final boolean fromApp) { 38 | return new PlayerHaterTask(mHandler) { 39 | @Override 40 | protected Boolean run() { 41 | return mService.pause(fromApp); 42 | } 43 | }.get(); 44 | } 45 | 46 | public void duck() { 47 | mService.duck(); 48 | } 49 | 50 | public void unduck() { 51 | mService.unduck(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/res/xml/zzz_ph_config_defaults.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 22 | 25 | 28 | 31 | 34 | 37 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/plugins/PebblePlugin.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.plugins; 17 | 18 | import android.content.Intent; 19 | 20 | import org.prx.playerhater.Song; 21 | 22 | public class PebblePlugin extends AbstractPlugin { 23 | 24 | private Song mSong; 25 | 26 | @Override 27 | public void onSongChanged(Song song) { 28 | mSong = song; 29 | if (getPlayerHater().isPlaying()) { 30 | onAudioStarted(); 31 | } 32 | } 33 | 34 | @Override 35 | public void onAudioStarted() { 36 | if (mSong != null) { 37 | Intent intent = new Intent("com.getpebble.action.NOW_PLAYING"); 38 | intent.putExtra("artist", mSong.getArtist()); 39 | intent.putExtra("track", mSong.getTitle()); 40 | intent.putExtra("album", mSong.getAlbumTitle()); 41 | 42 | getContext().sendBroadcast(intent); 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/BroadcastReceiver.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater; 17 | 18 | import org.prx.playerhater.broadcast.Receiver; 19 | 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.content.IntentFilter; 23 | import android.media.AudioManager; 24 | 25 | public class BroadcastReceiver extends Receiver { 26 | private static Receiver sInstance; 27 | 28 | public static void register(Context context) { 29 | BroadcastReceiver receiver = new BroadcastReceiver(); 30 | IntentFilter filter = new IntentFilter(); 31 | filter.addAction(Intent.ACTION_HEADSET_PLUG); 32 | filter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); 33 | filter.addAction(Intent.ACTION_MEDIA_BUTTON); 34 | filter.setPriority(10000); 35 | context.registerReceiver(receiver, filter); 36 | sInstance = receiver; 37 | } 38 | 39 | public static void release(Context context) { 40 | if (sInstance != null) { 41 | context.unregisterReceiver(sInstance); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/aidl/org/prx/playerhater/ipc/IPlayerHaterClient.aidl: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.ipc; 17 | 18 | import android.net.Uri; 19 | import android.app.PendingIntent; 20 | 21 | interface IPlayerHaterClient { 22 | 23 | /** 24 | * Plugin Methods 25 | */ 26 | void onSongChanged(int songTag); 27 | void onSongFinished(int songTag, int reason); 28 | void onDurationChanged(int duration); 29 | void onAudioLoading(); 30 | void onAudioPaused(); 31 | void onAudioResumed(); 32 | void onAudioStarted(); 33 | void onAudioStopped(); 34 | void onTitleChanged(String title); 35 | void onArtistChanged(String artist); 36 | void onAlbumTitleChanged(String albumTitle); 37 | void onAlbumArtChanged(in Uri uri); 38 | void onTransportControlFlagsChanged(int transportControlFlags); 39 | void onNextSongAvailable(int songTag); 40 | void onNextSongUnavailable(); 41 | void onPlayerHaterShutdown(); 42 | void onChangesComplete(); 43 | void onIntentActivityChanged(in PendingIntent intent); 44 | 45 | /** 46 | * SongHost Methods 47 | */ 48 | String getSongTitle(int songTag); 49 | String getSongArtist(int songTag); 50 | String getSongAlbumTitle(int songTag); 51 | Uri getSongAlbumArt(int songTag); 52 | Uri getSongUri(int songTag); 53 | Bundle getSongExtra(int songTag); 54 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/aidl/org/prx/playerhater/ipc/IPlayerHaterServer.aidl: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.ipc; 17 | 18 | import android.net.Uri; 19 | import org.prx.playerhater.ipc.IPlayerHaterClient; 20 | import android.app.Notification; 21 | import android.app.PendingIntent; 22 | 23 | interface IPlayerHaterServer { 24 | 25 | /** 26 | * Server-specific methods 27 | */ 28 | void setClient(IPlayerHaterClient client); 29 | void onRemoteControlButtonPressed(int keyCode); 30 | void startForeground(int notificationNu, in Notification notification); 31 | void stopForeground(boolean fact); 32 | void duck(); 33 | void unduck(); 34 | 35 | /** 36 | * PlayerHater Methods 37 | */ 38 | boolean pause(); 39 | boolean stop(); 40 | boolean resume(); 41 | boolean playAtTime(int startTime); 42 | boolean play(int songTag, in Bundle songData, int startTime); 43 | boolean seekTo(int startTime); 44 | int enqueue(int songTag, in Bundle songData); 45 | void enqueueAtPosition(int position, int songTag, in Bundle songData); 46 | boolean skipTo(int position); 47 | void skip(); 48 | void skipBack(); 49 | void emptyQueue(); 50 | int getCurrentPosition(); 51 | int getDuration(); 52 | int nowPlaying(); 53 | boolean isPlaying(); 54 | boolean isLoading(); 55 | int getState(); 56 | void setTransportControlFlags(int transportControlFlags); 57 | int getTransportControlFlags(); 58 | void setPendingIntent(in PendingIntent intent); 59 | int getQueueLength(); 60 | int getQueuePosition(); 61 | boolean removeFromQueue(int position); 62 | 63 | /** 64 | * SongHost Methods 65 | */ 66 | String getSongTitle(int songTag); 67 | String getSongArtist(int songTag); 68 | String getSongAlbumTitle(int songTag); 69 | Uri getSongAlbumArt(int songTag); 70 | Uri getSongUri(int songTag); 71 | Bundle getSongExtra(int songTag); 72 | void slurp(int songTag, in Bundle songData); 73 | } -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/songs/Songs.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.songs; 17 | 18 | import org.prx.playerhater.Song; 19 | 20 | import android.net.Uri; 21 | import android.os.Bundle; 22 | 23 | public class Songs { 24 | 25 | private static final String TITLE = "title"; 26 | private static final String ARTIST = "artist"; 27 | private static final String ALBUM = "album"; 28 | private static final String ALBUMART = "album_art"; 29 | private static final String URI = "uri"; 30 | private static final String EXTRA = "extra"; 31 | 32 | public static Bundle toBundle(Song song) { 33 | Bundle bundle = new Bundle(); 34 | bundle.putString(TITLE, song.getTitle()); 35 | bundle.putString(ARTIST, song.getArtist()); 36 | bundle.putString(ALBUM, song.getAlbumTitle()); 37 | bundle.putParcelable(ALBUMART, song.getAlbumArt()); 38 | bundle.putParcelable(URI, song.getUri()); 39 | bundle.putBundle(EXTRA, song.getExtra()); 40 | return bundle; 41 | } 42 | 43 | public static Song fromBundle(Bundle bundle) { 44 | return new UnbundledSong(bundle); 45 | } 46 | 47 | private static class UnbundledSong implements Song { 48 | 49 | private final String mTitle, mArtist, mAlbum; 50 | private final Uri mUri, mAlbumArt; 51 | private final Bundle mExtra; 52 | 53 | public UnbundledSong(Bundle bundle) { 54 | mTitle = bundle.getString(TITLE); 55 | mArtist = bundle.getString(ARTIST); 56 | mAlbum = bundle.getString(ALBUM); 57 | 58 | mUri = bundle.getParcelable(URI); 59 | mAlbumArt = bundle.getParcelable(ALBUMART); 60 | 61 | mExtra = bundle.getBundle(EXTRA); 62 | } 63 | 64 | @Override 65 | public String getTitle() { 66 | return mTitle; 67 | } 68 | 69 | @Override 70 | public String getArtist() { 71 | return mArtist; 72 | } 73 | 74 | @Override 75 | public String getAlbumTitle() { 76 | return mAlbum; 77 | } 78 | 79 | @Override 80 | public Uri getAlbumArt() { 81 | return mAlbumArt; 82 | } 83 | 84 | @Override 85 | public Uri getUri() { 86 | return mUri; 87 | } 88 | 89 | @Override 90 | public Bundle getExtra() { 91 | return mExtra; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/plugins/ScrubbableLockScreenControlsPlugin.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2014 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.plugins; 17 | 18 | import android.annotation.TargetApi; 19 | import android.media.RemoteControlClient; 20 | import android.os.Build; 21 | 22 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) 23 | public class ScrubbableLockScreenControlsPlugin extends LockScreenControlsPlugin implements RemoteControlClient.OnPlaybackPositionUpdateListener, RemoteControlClient.OnGetPlaybackPositionListener { 24 | private RemoteControlClient mRemoteControlClient; 25 | 26 | @Override 27 | public void onPlaybackPositionUpdate(long l) { 28 | if (getPlayerHater().seekTo((int) l)) { 29 | getRemoteControlClient().setPlaybackState(getPlaybackState(), l, 1f); 30 | } else { 31 | onGetPlaybackPosition(); 32 | } 33 | } 34 | 35 | @Override 36 | public long onGetPlaybackPosition() { 37 | if (getIsLoading()) { 38 | return -1; 39 | } 40 | setPlaybackState(getPlaybackState()); 41 | return getCurrentPosition(); 42 | } 43 | 44 | @Override 45 | protected RemoteControlClient getRemoteControlClient() { 46 | if (mRemoteControlClient == null) { 47 | mRemoteControlClient = super.getRemoteControlClient(); 48 | mRemoteControlClient.setOnGetPlaybackPositionListener(this); 49 | mRemoteControlClient.setPlaybackPositionUpdateListener(this); 50 | } 51 | return mRemoteControlClient; 52 | } 53 | 54 | @Override 55 | protected void setPlaybackState(int state) { 56 | getRemoteControlClient().setPlaybackState(state, getCurrentPosition(), 1f); 57 | getAudioManager().registerRemoteControlClient(getRemoteControlClient()); 58 | } 59 | 60 | private int getCurrentPosition() { 61 | try { 62 | return getPlayerHater().getCurrentPosition(); 63 | } catch (IllegalStateException exception) { 64 | return -1; 65 | } 66 | } 67 | 68 | private boolean getIsLoading() { 69 | try { 70 | return getPlayerHater().isLoading(); 71 | } catch (IllegalStateException exception) { 72 | return false; 73 | } 74 | } 75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/broadcast/OnAudioFocusChangedListener.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.broadcast; 17 | 18 | import org.prx.playerhater.wrappers.ServicePlayerHater; 19 | 20 | import android.annotation.TargetApi; 21 | import android.media.AudioManager; 22 | import android.os.Build; 23 | 24 | @TargetApi(Build.VERSION_CODES.FROYO) 25 | public class OnAudioFocusChangedListener implements 26 | AudioManager.OnAudioFocusChangeListener { 27 | 28 | // 5 seconds 29 | private static final int REWIND_ON_RESUME_DURATION = 5000; 30 | 31 | // 5 minutes 32 | private static final int SKIP_RESUME_AFTER_DURATION = 300000; 33 | 34 | private final ServicePlayerHater mPlayerHater; 35 | 36 | private long pausedAt; 37 | private boolean isBeingDucked; 38 | private boolean isBeingPaused; 39 | 40 | public OnAudioFocusChangedListener(ServicePlayerHater playerHater) { 41 | isBeingDucked = false; 42 | mPlayerHater = playerHater; 43 | } 44 | 45 | @Override 46 | public void onAudioFocusChange(int focusChange) { 47 | switch (focusChange) { 48 | case AudioManager.AUDIOFOCUS_GAIN: 49 | // Good, glad to hear it. 50 | if (isBeingPaused && !mPlayerHater.isPlaying()) { 51 | isBeingPaused = false; 52 | if (pausedAt + (SKIP_RESUME_AFTER_DURATION) > System 53 | .currentTimeMillis()) { 54 | mPlayerHater.play(); 55 | } 56 | } 57 | 58 | if (isBeingDucked) { 59 | isBeingDucked = false; 60 | mPlayerHater.unduck(); 61 | } 62 | break; 63 | case AudioManager.AUDIOFOCUS_LOSS: 64 | // Oh, no! Ok, let's handle that. 65 | if (mPlayerHater.isPlaying()) { 66 | mPlayerHater.pause(); 67 | } 68 | break; 69 | case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 70 | // Let's pause, expecting it to come back. 71 | if (mPlayerHater.isPlaying()) { 72 | pausedAt = System.currentTimeMillis(); 73 | isBeingPaused = true; 74 | mPlayerHater.pause(); 75 | mPlayerHater.seekTo(Math.max(0, 76 | mPlayerHater.getCurrentPosition() 77 | - REWIND_ON_RESUME_DURATION)); 78 | } 79 | break; 80 | case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 81 | if (mPlayerHater.isPlaying() && !isBeingDucked) { 82 | isBeingDucked = true; 83 | mPlayerHater.duck(); 84 | } 85 | break; 86 | default: 87 | // Dunno. 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/Song.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater; 17 | 18 | import android.net.Uri; 19 | import android.os.Bundle; 20 | 21 | /** 22 | * An interface which can be used for playback in {@linkplain PlayerHater}. This 23 | * interface is primarily for use by {@linkplain PlayerHaterPlugin}s, which use 24 | * the metadata provided to display notifications, widgets, and lock screen 25 | * controls. 26 | * 27 | * @see PlayerHater#play(Song) 28 | * @version 3.0.0 29 | * @author Chris Rhoden 30 | * @since 2.0.0 31 | */ 32 | public interface Song { 33 | 34 | /** 35 | * An accessor for the title attribute of a {@linkplain Song} playable 36 | * entity in PlayerHater 37 | * 38 | * @return The title of the {@linkplain Song} 39 | */ 40 | String getTitle(); 41 | 42 | /** 43 | * An accessor for the artist attribute of a {@linkplain Song} playable 44 | * entity in PlayerHater 45 | * 46 | * @return The name of the artist performing the {@linkplain Song} 47 | */ 48 | String getArtist(); 49 | 50 | /** 51 | * An accessor for the album attribute of a {@linkplain Song} playable 52 | * entity in PlayerHater 53 | * 54 | * @return The name of the album on which the {@linkplain Song} is performed 55 | */ 56 | String getAlbumTitle(); 57 | 58 | /** 59 | * An accessor for the the album art attribute of a {@linkplain Song} 60 | * playable entity in PlayerHater 61 | * 62 | * @return The Uri representing the album art for this {@linkplain Song} 63 | */ 64 | Uri getAlbumArt(); 65 | 66 | /** 67 | * @see android.media.MediaPlayer#setDataSource(android.content.Context, 68 | * Uri) 69 | * @return A Uri which resolves to the {@linkplain Song}'s file, which can 70 | * be played using Android's 71 | * {@linkplain android.media.MediaPlayer#setDataSource(android.content.Context, Uri) 72 | * MediaPlayer} class. 73 | *

74 | * Ex. 75 | *

76 | * {@code http://www.example.com/track.mp3 } 77 | *

78 | * {@code content://com.example.app/clips/21 } 79 | */ 80 | Uri getUri(); 81 | 82 | /** 83 | * @return A Bundle whose meaning is user-defined. This is to enable easy 84 | * inter-process communication of additional data about Songs. 85 | *

86 | * PlayerHater will not do anything with this. 87 | */ 88 | Bundle getExtra(); 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/plugins/AudioFocusPlugin.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.plugins; 17 | 18 | import android.annotation.TargetApi; 19 | import android.content.ComponentName; 20 | import android.content.Context; 21 | import android.media.AudioManager; 22 | import android.os.Build; 23 | 24 | import org.prx.playerhater.BroadcastReceiver; 25 | import org.prx.playerhater.PlayerHater; 26 | import org.prx.playerhater.broadcast.OnAudioFocusChangedListener; 27 | import org.prx.playerhater.wrappers.ServicePlayerHater; 28 | 29 | @TargetApi(Build.VERSION_CODES.FROYO) 30 | public class AudioFocusPlugin extends AbstractPlugin { 31 | private AudioManager mAudioService; 32 | private OnAudioFocusChangedListener mAudioFocusChangeListener; 33 | private ComponentName mEventReceiver; 34 | 35 | public AudioFocusPlugin() { 36 | } 37 | 38 | @Override 39 | public void onPlayerHaterLoaded(Context context, PlayerHater playerHater) { 40 | super.onPlayerHaterLoaded(context, playerHater); 41 | if (!(playerHater instanceof ServicePlayerHater)) { 42 | throw new IllegalArgumentException( 43 | "AudioFocusPlugin must be run on the server side"); 44 | } 45 | mAudioFocusChangeListener = new OnAudioFocusChangedListener( 46 | (ServicePlayerHater) playerHater); 47 | } 48 | 49 | @Override 50 | public void onAudioStarted() { 51 | getAudioManager().requestAudioFocus(mAudioFocusChangeListener, 52 | AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); 53 | getAudioManager().registerMediaButtonEventReceiver(getEventReceiver()); 54 | } 55 | 56 | @Override 57 | public void onAudioStopped() { 58 | getAudioManager().abandonAudioFocus(mAudioFocusChangeListener); 59 | getAudioManager() 60 | .unregisterMediaButtonEventReceiver(getEventReceiver()); 61 | } 62 | 63 | protected AudioManager getAudioManager() { 64 | if (mAudioService == null) { 65 | mAudioService = (AudioManager) getContext().getSystemService( 66 | Context.AUDIO_SERVICE); 67 | } 68 | return mAudioService; 69 | } 70 | 71 | protected ComponentName getEventReceiver() { 72 | if (mEventReceiver == null) { 73 | mEventReceiver = new ComponentName(getContext(), 74 | BroadcastReceiver.class); 75 | } 76 | return mEventReceiver; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/broadcast/HeadphoneButtonGestureHelper.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.broadcast; 17 | 18 | import android.content.Context; 19 | import android.os.Handler; 20 | import android.os.Message; 21 | import android.view.KeyEvent; 22 | 23 | class HeadphoneButtonGestureHelper { 24 | 25 | private static final int BUTTON_PRESSED = 0; 26 | private static final int PLAY_PAUSE = 1; 27 | private static final int NEXT = 2; 28 | private static final int PREV = 3; 29 | private static final int MILISECONDS_DELAY = 250; 30 | public static final String TAG = "GESTURES"; 31 | 32 | private long mLastEventTime = 0; 33 | private int mCurrentAction = 1; 34 | private static Context lastContext; 35 | 36 | private final Handler mHandler = new ButtonHandler(this); 37 | private RemoteControlButtonReceiver mMediaButtonReceiver; 38 | 39 | public void onHeadsetButtonPressed(long eventTime, Context context) { 40 | if (eventTime - mLastEventTime <= MILISECONDS_DELAY + 100) { 41 | mCurrentAction += 1; 42 | if (mCurrentAction > 3) { 43 | mCurrentAction = 1; 44 | } 45 | mHandler.removeMessages(BUTTON_PRESSED); 46 | } 47 | lastContext = context; 48 | mLastEventTime = eventTime; 49 | mHandler.sendEmptyMessageDelayed(BUTTON_PRESSED, MILISECONDS_DELAY); 50 | } 51 | 52 | public void setReceiver(RemoteControlButtonReceiver receiver) { 53 | mMediaButtonReceiver = receiver; 54 | } 55 | 56 | private static class ButtonHandler extends Handler { 57 | 58 | private final HeadphoneButtonGestureHelper mButtonGestureHelper; 59 | 60 | private ButtonHandler(HeadphoneButtonGestureHelper ctx) { 61 | mButtonGestureHelper = ctx; 62 | } 63 | 64 | @Override 65 | public void dispatchMessage(Message message) { 66 | switch (mButtonGestureHelper.mCurrentAction) { 67 | 68 | case PLAY_PAUSE: 69 | mButtonGestureHelper.mMediaButtonReceiver 70 | .onRemoteControlButtonPressed( 71 | KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, lastContext); 72 | break; 73 | case NEXT: 74 | mButtonGestureHelper.mMediaButtonReceiver 75 | .onRemoteControlButtonPressed( 76 | KeyEvent.KEYCODE_MEDIA_NEXT, lastContext); 77 | break; 78 | case PREV: 79 | mButtonGestureHelper.mMediaButtonReceiver 80 | .onRemoteControlButtonPressed( 81 | KeyEvent.KEYCODE_MEDIA_PREVIOUS, lastContext); 82 | break; 83 | } 84 | mButtonGestureHelper.mLastEventTime = 0; 85 | mButtonGestureHelper.mCurrentAction = 1; 86 | } 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/songs/RemoteSong.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.songs; 17 | 18 | import org.prx.playerhater.Song; 19 | 20 | import android.net.Uri; 21 | import android.os.Bundle; 22 | import android.os.RemoteException; 23 | 24 | class RemoteSong implements Song { 25 | 26 | private static SongHost.Remote getRemote() { 27 | return SongHost.remote(); 28 | } 29 | 30 | private final int mTag; 31 | private Song mSong = null; 32 | private static final String remoteSongExceptionMessage = "Remote Process has died or become disconnected and song data has not been copied"; 33 | 34 | RemoteSong(int tag) { 35 | mTag = tag; 36 | } 37 | 38 | @Override 39 | public String getTitle() { 40 | try { 41 | return getRemote().getSongTitle(mTag); 42 | } catch (RemoteException e) { 43 | if (mSong != null) { 44 | return mSong.getTitle(); 45 | } 46 | throw new IllegalStateException(remoteSongExceptionMessage, e); 47 | } 48 | } 49 | 50 | @Override 51 | public String getArtist() { 52 | try { 53 | return getRemote().getSongArtist(mTag); 54 | } catch (RemoteException e) { 55 | if (mSong != null) { 56 | return mSong.getArtist(); 57 | } 58 | throw new IllegalStateException(remoteSongExceptionMessage, e); 59 | } 60 | } 61 | 62 | @Override 63 | public Uri getAlbumArt() { 64 | try { 65 | return getRemote().getSongAlbumArt(mTag); 66 | } catch (RemoteException e) { 67 | if (mSong != null) { 68 | return mSong.getAlbumArt(); 69 | } 70 | throw new IllegalStateException(remoteSongExceptionMessage, e); 71 | } 72 | } 73 | 74 | @Override 75 | public Uri getUri() { 76 | try { 77 | return getRemote().getSongUri(mTag); 78 | } catch (RemoteException e) { 79 | if (mSong != null) { 80 | return mSong.getUri(); 81 | } 82 | throw new IllegalStateException(remoteSongExceptionMessage, e); 83 | } 84 | } 85 | 86 | @Override 87 | public Bundle getExtra() { 88 | try { 89 | return getRemote().getSongExtra(mTag); 90 | } catch (RemoteException e) { 91 | if (mSong != null) { 92 | return mSong.getExtra(); 93 | } 94 | throw new IllegalStateException(remoteSongExceptionMessage, e); 95 | } 96 | } 97 | 98 | @Override 99 | public String getAlbumTitle() { 100 | try { 101 | return getRemote().getSongAlbumTitle(mTag); 102 | } catch (RemoteException e) { 103 | if (mSong != null) { 104 | return mSong.getAlbumTitle(); 105 | } 106 | throw new IllegalStateException(remoteSongExceptionMessage, e); 107 | } 108 | } 109 | 110 | void setSong(Song song) { 111 | mSong = song; 112 | } 113 | 114 | Song getSong() { 115 | return mSong; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/plugins/ExpandableNotificationPlugin.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.plugins; 17 | 18 | import android.annotation.TargetApi; 19 | import android.app.Notification; 20 | import android.net.Uri; 21 | import android.os.Build; 22 | import android.widget.RemoteViews; 23 | 24 | import org.prx.playerhater.R; 25 | 26 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 27 | public class ExpandableNotificationPlugin extends TouchableNotificationPlugin { 28 | 29 | private RemoteViews mExpandedView; 30 | 31 | @Override 32 | protected Notification getNotification() { 33 | if (mNotification == null) { 34 | mNotification = super.getNotification(); 35 | mNotification.bigContentView = getExpandedView(); 36 | } 37 | return mNotification; 38 | } 39 | 40 | private RemoteViews getExpandedView() { 41 | if (mExpandedView == null) { 42 | mExpandedView = new RemoteViews(getContext().getPackageName(), 43 | R.layout.zzz_ph_jbb_notification); 44 | setListeners(mExpandedView); 45 | mExpandedView.setTextViewText(R.id.zzz_ph_notification_title, 46 | mNotificationTitle); 47 | mExpandedView.setTextViewText(R.id.zzz_ph_notification_text, 48 | mNotificationText); 49 | mExpandedView.setImageViewUri(R.id.zzz_ph_notification_image, 50 | mNotificationImageUrl); 51 | } 52 | return mExpandedView; 53 | } 54 | 55 | @Override 56 | protected void setTextViewText(int viewId, String text) { 57 | super.setTextViewText(viewId, text); 58 | if (mExpandedView != null) { 59 | mExpandedView.setTextViewText(viewId, text); 60 | } 61 | } 62 | 63 | @Override 64 | protected void setViewEnabled(int viewId, boolean enabled) { 65 | if (mExpandedView != null) { 66 | mExpandedView.setBoolean(viewId, "setEnabled", enabled); 67 | } 68 | super.setViewEnabled(viewId, enabled); 69 | } 70 | 71 | @Override 72 | protected void setViewVisibility(int viewId, int visible) { 73 | if (mExpandedView != null) { 74 | mExpandedView.setViewVisibility(viewId, visible); 75 | } 76 | super.setViewVisibility(viewId, visible); 77 | } 78 | 79 | @Override 80 | protected void setImageViewResource(int viewId, int resourceId) { 81 | if (mExpandedView != null) { 82 | mExpandedView.setImageViewResource(viewId, resourceId); 83 | } 84 | super.setImageViewResource(viewId, resourceId); 85 | } 86 | 87 | @Override 88 | protected void setImageViewUri(int viewId, Uri contentUri) { 89 | super.setImageViewUri(viewId, contentUri); 90 | if (mExpandedView != null && contentUri != null) { 91 | mExpandedView.setImageViewUri(viewId, contentUri); 92 | } 93 | } 94 | 95 | @Override 96 | protected Notification buildNotification() { 97 | return getNotificationBuilder().build(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/wrappers/ServicePlayerHater.java: -------------------------------------------------------------------------------- 1 | package org.prx.playerhater.wrappers; 2 | 3 | import org.prx.playerhater.PlayerHater; 4 | import org.prx.playerhater.Song; 5 | import org.prx.playerhater.ipc.IPlayerHaterClient; 6 | import org.prx.playerhater.service.PlayerHaterService; 7 | 8 | import android.app.Notification; 9 | import android.app.PendingIntent; 10 | 11 | public class ServicePlayerHater extends PlayerHater { 12 | private final PlayerHaterService mService; 13 | 14 | public ServicePlayerHater(PlayerHaterService service) { 15 | mService = service; 16 | } 17 | 18 | @Override 19 | public boolean pause() { 20 | return mService.pause(); 21 | } 22 | 23 | @Override 24 | public boolean stop() { 25 | return mService.stop(); 26 | } 27 | 28 | @Override 29 | public boolean play() { 30 | return mService.play(); 31 | } 32 | 33 | @Override 34 | public boolean play(int startTime) { 35 | return mService.play(startTime); 36 | } 37 | 38 | @Override 39 | public boolean play(Song song) { 40 | return play(song, 0); 41 | } 42 | 43 | @Override 44 | public boolean play(Song song, int startTime) { 45 | return mService.play(song, startTime); 46 | } 47 | 48 | @Override 49 | public boolean seekTo(int startTime) { 50 | return mService.seekTo(startTime); 51 | } 52 | 53 | @Override 54 | public int enqueue(Song song) { 55 | return mService.enqueue(song); 56 | } 57 | 58 | @Override 59 | public void enqueue(int position, Song song) { 60 | mService.enqueue(position, song); 61 | } 62 | 63 | @Override 64 | public boolean skipTo(int position) { 65 | return mService.skipTo(position); 66 | } 67 | 68 | @Override 69 | public void skip() { 70 | mService.skip(); 71 | } 72 | 73 | @Override 74 | public void skipBack() { 75 | mService.skipBack(); 76 | } 77 | 78 | @Override 79 | public void emptyQueue() { 80 | mService.emptyQueue(); 81 | } 82 | 83 | @Override 84 | public int getCurrentPosition() { 85 | return mService.getCurrentPosition(); 86 | } 87 | 88 | @Override 89 | public int getDuration() { 90 | return mService.getDuration(); 91 | } 92 | 93 | @Override 94 | public Song nowPlaying() { 95 | return mService.nowPlaying(); 96 | } 97 | 98 | @Override 99 | public boolean isPlaying() { 100 | return mService.isPlaying(); 101 | } 102 | 103 | @Override 104 | public boolean isLoading() { 105 | return mService.isLoading(); 106 | } 107 | 108 | @Override 109 | public int getState() { 110 | return mService.getState(); 111 | } 112 | 113 | @Override 114 | public void setTransportControlFlags(int transportControlFlags) { 115 | mService.setTransportControlFlags(transportControlFlags); 116 | } 117 | 118 | @Override 119 | public int getQueueLength() { 120 | return mService.getQueueLength(); 121 | } 122 | 123 | @Override 124 | public int getQueuePosition() { 125 | return mService.getQueuePosition(); 126 | } 127 | 128 | @Override 129 | public boolean removeFromQueue(int position) { 130 | return mService.removeFromQueue(position); 131 | } 132 | 133 | @Override 134 | public void setPendingIntent(PendingIntent intent) { 135 | mService.setPendingIntent(intent); 136 | } 137 | 138 | /* 139 | * Service Specific stuff. 140 | */ 141 | 142 | public void setClient(IPlayerHaterClient client) { 143 | mService.setClient(client); 144 | } 145 | 146 | public void onRemoteControlButtonPressed(int keyCode) { 147 | mService.onRemoteControlButtonPressed(keyCode); 148 | } 149 | 150 | public void startForeground(int notificationNu, Notification notification) { 151 | mService.startForeground(notificationNu, notification); 152 | } 153 | 154 | public void stopForeground(boolean removeNotification) { 155 | mService.stopForeground(removeNotification); 156 | mService.quit(); 157 | } 158 | 159 | public void duck() { 160 | mService.duck(); 161 | } 162 | 163 | public void unduck() { 164 | mService.unduck(); 165 | } 166 | 167 | @Override 168 | public int getTransportControlFlags() { 169 | return mService.getTransportControlFlags(); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/broadcast/Receiver.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.broadcast; 17 | 18 | import org.prx.playerhater.PlayerHater; 19 | import org.prx.playerhater.ipc.IPlayerHaterServer; 20 | 21 | import android.annotation.SuppressLint; 22 | import android.content.BroadcastReceiver; 23 | import android.content.Context; 24 | import android.content.Intent; 25 | import android.media.AudioManager; 26 | import android.view.KeyEvent; 27 | 28 | public class Receiver extends BroadcastReceiver implements 29 | RemoteControlButtonReceiver { 30 | 31 | private static final HeadphoneButtonGestureHelper sGestureHelper = new HeadphoneButtonGestureHelper(); 32 | public static final String REMOTE_CONTROL_BUTTON = "org.prx.playerhater.REMOTE_CONTROL"; 33 | 34 | public Receiver() { 35 | super(); 36 | sGestureHelper.setReceiver(this); 37 | } 38 | 39 | @Override 40 | @SuppressLint("InlinedApi") 41 | public void onRemoteControlButtonPressed(int keyCode, Context context) { 42 | sendKeyCode(context, keyCode, keyCode != KeyEvent.KEYCODE_MEDIA_PAUSE); 43 | } 44 | 45 | @Override 46 | @SuppressLint("InlinedApi") 47 | public void onReceive(Context context, Intent intent) { 48 | int keyCode = -1; 49 | if (intent.getAction() != null) { 50 | if (intent.getIntExtra("state", 0) == 0) { 51 | if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) { 52 | keyCode = KeyEvent.KEYCODE_MEDIA_PAUSE; 53 | } else if (intent.getAction().equals( 54 | AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { 55 | keyCode = KeyEvent.KEYCODE_MEDIA_PAUSE; 56 | } 57 | } 58 | if (intent.getAction().equals(Intent.ACTION_MEDIA_BUTTON)) { 59 | KeyEvent event = (KeyEvent) intent 60 | .getParcelableExtra(Intent.EXTRA_KEY_EVENT); 61 | 62 | if (event.getAction() == KeyEvent.ACTION_DOWN) { 63 | return; 64 | } 65 | 66 | keyCode = event.getKeyCode(); 67 | 68 | if (KeyEvent.KEYCODE_HEADSETHOOK == keyCode) { 69 | sGestureHelper.onHeadsetButtonPressed(event.getEventTime(), 70 | context); 71 | } 72 | } 73 | 74 | if (keyCode != -1) { 75 | boolean autoStart = keyCode != KeyEvent.KEYCODE_MEDIA_PAUSE 76 | && keyCode != KeyEvent.KEYCODE_MEDIA_STOP; 77 | sendKeyCode(context, keyCode, autoStart); 78 | } 79 | } 80 | } 81 | 82 | private IPlayerHaterServer getService(Context context) { 83 | Intent intent = PlayerHater.buildServiceIntent(context); 84 | return IPlayerHaterServer.Stub 85 | .asInterface(peekService(context, intent)); 86 | } 87 | 88 | private void sendKeyCode(Context context, int keyCode, 89 | boolean startIfNecessary) { 90 | if (getService(context) != null) { 91 | try { 92 | getService(context).onRemoteControlButtonPressed(keyCode); 93 | } catch (Exception e) { 94 | if (startIfNecessary && context != null) { 95 | Intent intent = context 96 | .getPackageManager() 97 | .getLaunchIntentForPackage(context.getPackageName()); 98 | intent.putExtra(REMOTE_CONTROL_BUTTON, keyCode); 99 | context.startActivity(intent); 100 | } 101 | } 102 | 103 | } else if (startIfNecessary && context != null) { 104 | Intent intent = new Intent(PlayerHater.buildServiceIntent(context)); 105 | intent.putExtra(REMOTE_CONTROL_BUTTON, keyCode); 106 | context.startService(intent); 107 | } 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /legacyLibraryProject/PlayerHater/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 49 | 50 | 51 | 52 | 56 | 57 | 69 | 70 | 71 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/PlayerHater.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater; 17 | 18 | import org.prx.playerhater.wrappers.BoundPlayerHater; 19 | import org.prx.playerhater.util.Config; 20 | import org.prx.playerhater.util.IPlayerHater; 21 | 22 | import android.annotation.SuppressLint; 23 | import android.content.Context; 24 | import android.content.Intent; 25 | import android.media.RemoteControlClient; 26 | import android.util.Log; 27 | 28 | public abstract class PlayerHater implements IPlayerHater { 29 | 30 | @SuppressLint("InlinedApi") 31 | public static final int DEFAULT_TRANSPORT_CONTROL_FLAGS = RemoteControlClient.FLAG_KEY_MEDIA_NEXT 32 | | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE 33 | | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE 34 | | RemoteControlClient.FLAG_KEY_MEDIA_PLAY 35 | | RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS 36 | | RemoteControlClient.FLAG_KEY_MEDIA_STOP; 37 | 38 | /** 39 | * Releases the {@linkplain ServiceConnection} which this instance is using 40 | * if this instance is backed by a {@linkplain ServiceConnection} 41 | * 42 | * @return {@code true} if this instance was backed by a 43 | * {@linkplain ServiceConnection} and should not be used anymore, 44 | * {@code false} otherwise. 45 | */ 46 | public boolean release() { 47 | return false; 48 | } 49 | 50 | public boolean setLocalPlugin(PlayerHaterPlugin plugin) { 51 | return false; 52 | } 53 | 54 | /** 55 | * Constructs an {@linkplain Intent} which will start the appropriate 56 | * {@linkplain PlayerHaterService} as configured in the project's 57 | * AndroidManifest.xml file. 58 | * 59 | * @param context 60 | * @return An {@link Intent} which will start the correct service. 61 | * @throws IllegalArgumentException 62 | * if there is no appropriate service configured in 63 | * AndroidManifest.xml 64 | */ 65 | public static Intent buildServiceIntent(Context context) { 66 | Intent intent = new Intent("org.prx.playerhater.SERVICE"); 67 | intent.setPackage(context.getPackageName()); 68 | Config.attachToIntent(intent); 69 | 70 | if (context.getPackageManager().queryIntentServices(intent, 0).size() == 0) { 71 | intent = new Intent(context, PlaybackService.class); 72 | Config.attachToIntent(intent); 73 | if (context.getPackageManager().queryIntentServices(intent, 0) 74 | .size() == 0) { 75 | IllegalArgumentException e = new IllegalArgumentException( 76 | "No usable service found."); 77 | String tag = context.getPackageName() + "/PlayerHater"; 78 | String message = "Please define your Playback Service. For help, refer to: https://github.com/PRX/PlayerHater/wiki/Setting-Up-Your-Manifest"; 79 | Log.e(tag, message, e); 80 | throw e; 81 | } 82 | } 83 | 84 | return intent; 85 | } 86 | 87 | /** 88 | * Gets an instance of a {@linkplain BoundPlayerHater} which can be used to 89 | * interact with the playback service. 90 | * 91 | * Calling this method will also invoke 92 | * {@linkplain PlayerHater#configure(Context)} if it has not yet been 93 | * called. 94 | * 95 | * @since 2.1.0 96 | * 97 | * @param context 98 | * The context on which to bind the service. 99 | * @return an instance of PlayerHater which one can use to interact with the 100 | * Playback Service. 101 | */ 102 | public static PlayerHater bind(Context context) { 103 | return new BoundPlayerHater(context); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/res/layout-v11/zzz_ph_hc_notification.xml: -------------------------------------------------------------------------------- 1 | 16 | 20 | 21 | 29 | 30 | 42 | 43 | 54 | 55 | 65 | 66 | 72 | 73 | 86 | 87 | 88 | 89 | 90 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/util/PlaylistParser.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 | 17 | package org.prx.playerhater.util; 18 | 19 | import java.io.BufferedReader; 20 | import java.io.InputStream; 21 | import java.io.InputStreamReader; 22 | import java.util.ArrayList; 23 | 24 | import org.apache.http.Header; 25 | import org.apache.http.HttpEntity; 26 | import org.apache.http.HttpResponse; 27 | import org.apache.http.client.HttpClient; 28 | import org.apache.http.impl.client.DefaultHttpClient; 29 | import org.apache.http.client.methods.HttpGet; 30 | import org.apache.http.client.methods.HttpHead; 31 | 32 | import android.net.Uri; 33 | 34 | public final class PlaylistParser { 35 | 36 | private static String[] PLS_MIME_TYPES = new String[] { "audio/scpls", 37 | "audio/x-scpls" }; 38 | private static String[] M3U_MIME_TYPES = new String[] { "audio/x-mpegurl" }; 39 | 40 | public static Uri[] parsePlaylist(Uri uri) { 41 | try { 42 | HttpClient httpclient = new DefaultHttpClient(); 43 | HttpResponse response = httpclient.execute(new HttpHead(uri 44 | .toString())); 45 | Header contentType = response.getEntity().getContentType(); 46 | if (contentType != null) { 47 | String mimeType = contentType.getValue().split(";")[0].trim(); 48 | 49 | for (String plsMimeType : PLS_MIME_TYPES) { 50 | if (plsMimeType.equalsIgnoreCase(mimeType)) { 51 | return parsePls(uri); 52 | } 53 | } 54 | 55 | for (String m3uMimeType : M3U_MIME_TYPES) { 56 | if (m3uMimeType.equalsIgnoreCase(mimeType)) { 57 | return parseM3u(uri); 58 | } 59 | } 60 | } 61 | } catch (Exception e) {} 62 | return new Uri[] { uri }; 63 | } 64 | 65 | private static Uri[] parsePls(Uri uri) { 66 | try { 67 | HttpClient httpclient = new DefaultHttpClient(); 68 | HttpResponse response = httpclient.execute(new HttpGet(uri.toString())); 69 | HttpEntity entity = response.getEntity(); 70 | InputStream inputStream = entity.getContent(); 71 | BufferedReader reader = new BufferedReader(new InputStreamReader( 72 | inputStream)); 73 | String header = reader.readLine(); 74 | if (header.trim().equalsIgnoreCase("[playlist]")) { 75 | String line; 76 | ArrayList uriList = new ArrayList(); 77 | do { 78 | line = reader.readLine(); 79 | if (line != null) { 80 | if (line.startsWith("File")) { 81 | String fileName = line.substring( 82 | line.indexOf("=") + 1).trim(); 83 | uriList.add(Uri.parse(fileName)); 84 | } 85 | } 86 | } while (line != null); 87 | if (uriList.size() > 0) { 88 | Uri[] res = new Uri[uriList.size()]; 89 | return uriList.toArray(res); 90 | } 91 | } 92 | } catch (Exception e) { 93 | e.printStackTrace(); 94 | } 95 | return new Uri[] { uri }; 96 | } 97 | 98 | private static Uri[] parseM3u(Uri uri) { 99 | 100 | try { 101 | HttpClient httpclient = new DefaultHttpClient(); 102 | HttpResponse response = httpclient.execute(new HttpGet(uri 103 | .toString())); 104 | HttpEntity entity = response.getEntity(); 105 | InputStream inputStream = entity.getContent(); 106 | BufferedReader reader = new BufferedReader(new InputStreamReader( 107 | inputStream)); 108 | String line; 109 | ArrayList uriList = new ArrayList(); 110 | do { 111 | line = reader.readLine(); 112 | if (line != null) { 113 | if (!line.startsWith("#")) { 114 | uriList.add(Uri.parse(line.trim())); 115 | } 116 | } 117 | } while (line != null); 118 | if (uriList.size() > 0) { 119 | Uri[] res = new Uri[uriList.size()]; 120 | return uriList.toArray(res); 121 | } 122 | } catch (Exception e) { 123 | 124 | } 125 | return new Uri[] { uri }; 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/mediaplayer/Player.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.mediaplayer; 17 | 18 | import java.io.IOException; 19 | 20 | import android.content.Context; 21 | import android.media.MediaPlayer; 22 | import android.media.MediaPlayer.OnBufferingUpdateListener; 23 | import android.media.MediaPlayer.OnCompletionListener; 24 | import android.media.MediaPlayer.OnErrorListener; 25 | import android.media.MediaPlayer.OnInfoListener; 26 | import android.media.MediaPlayer.OnPreparedListener; 27 | import android.media.MediaPlayer.OnSeekCompleteListener; 28 | import android.net.Uri; 29 | 30 | public abstract class Player { 31 | 32 | public static interface StateChangeListener { 33 | public void onStateChanged(Player mediaPlayer, int state); 34 | } 35 | 36 | public abstract void reset(); 37 | 38 | public abstract void release(); 39 | 40 | public abstract void prepareAsync() throws IllegalStateException; 41 | 42 | public abstract void start() throws IllegalStateException; 43 | 44 | public abstract void pause() throws IllegalStateException; 45 | 46 | public abstract void stop() throws IllegalStateException; 47 | 48 | public abstract void seekTo(int msec); 49 | 50 | public abstract boolean isPlaying(); 51 | 52 | public abstract int getCurrentPosition(); 53 | 54 | public abstract int getDuration(); 55 | 56 | public abstract void setAudioStreamType(int streamtype); 57 | 58 | public abstract void setDataSource(Context context, Uri uri) 59 | throws IllegalStateException, IOException, 60 | IllegalArgumentException, SecurityException; 61 | 62 | public abstract void setOnErrorListener(OnErrorListener errorListener); 63 | 64 | public abstract void setOnPreparedListener( 65 | OnPreparedListener preparedListener); 66 | 67 | public abstract void setOnBufferingUpdateListener( 68 | OnBufferingUpdateListener bufferingUpdateListener); 69 | 70 | public abstract void setOnCompletionListener( 71 | OnCompletionListener completionListener); 72 | 73 | public abstract void setOnInfoListener(OnInfoListener infoListener); 74 | 75 | public abstract void setOnSeekCompleteListener( 76 | OnSeekCompleteListener seekCompleteListener); 77 | 78 | public abstract void setVolume(float leftVolume, float rightVolume); 79 | 80 | public abstract boolean equals(MediaPlayer mp); 81 | 82 | /* Stately API */ 83 | 84 | public int getState() { 85 | throw new IllegalStateException("This player isn't stately enough."); 86 | } 87 | 88 | public void setStateChangeListener(StateChangeListener listener) { 89 | throw new IllegalStateException("This player isn't stately enough."); 90 | } 91 | 92 | public String getStateName() { 93 | throw new IllegalStateException("This player isn't stately enough."); 94 | } 95 | 96 | /* Synchronous API */ 97 | 98 | public boolean prepare(Context context, Uri uri) { 99 | throw new IllegalStateException( 100 | "This player isn't down with synchronous calls."); 101 | } 102 | 103 | public boolean prepareAndPlay(Context applicationContext, Uri uri, 104 | int position) { 105 | throw new IllegalStateException( 106 | "This player isn't down with synchronous calls."); 107 | } 108 | 109 | public boolean conditionalPause() { 110 | throw new IllegalStateException( 111 | "This player isn't down with synchronous calls."); 112 | } 113 | 114 | public boolean conditionalStop() { 115 | throw new IllegalStateException( 116 | "This player isn't down with synchronous calls."); 117 | } 118 | 119 | public boolean conditionalPlay() { 120 | throw new IllegalStateException( 121 | "This player isn't down with synchronous calls."); 122 | } 123 | 124 | public boolean isWaitingToPlay() { 125 | return false; 126 | } 127 | 128 | public int getStateMask() { 129 | return getState(); 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/service/PlayerStateWatcher.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013, 2014 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.service; 17 | 18 | import org.prx.playerhater.PlayerHater; 19 | import org.prx.playerhater.mediaplayer.Player; 20 | import org.prx.playerhater.mediaplayer.Player.StateChangeListener; 21 | import org.prx.playerhater.mediaplayer.StatelyPlayer; 22 | 23 | public class PlayerStateWatcher implements StateChangeListener { 24 | private int mCurrentState = PlayerHater.STATE_IDLE; 25 | private int mCurrentDuration = 0; 26 | private PlayerHaterStateListener mListener; 27 | private Player mMediaPlayer; 28 | 29 | public PlayerStateWatcher() { 30 | this(null); 31 | } 32 | 33 | public PlayerStateWatcher(PlayerHaterStateListener listener) { 34 | setListener(listener); 35 | } 36 | 37 | public interface PlayerHaterStateListener { 38 | void onStateChanged(int state); 39 | 40 | void onDurationChanged(int duration); 41 | } 42 | 43 | public void setMediaPlayer(Player player) { 44 | if (mMediaPlayer != null) { 45 | mMediaPlayer.setStateChangeListener(null); 46 | } 47 | mMediaPlayer = player; 48 | if (mMediaPlayer != null) { 49 | mMediaPlayer.setStateChangeListener(this); 50 | onStateChanged(mMediaPlayer, mMediaPlayer.getStateMask()); 51 | } else { 52 | onStateChanged(null, StatelyPlayer.IDLE); 53 | } 54 | } 55 | 56 | public void setListener(PlayerHaterStateListener listener) { 57 | mListener = listener; 58 | notifyState(); 59 | } 60 | 61 | @Override 62 | public synchronized void onStateChanged(Player mediaPlayer, int state) { 63 | boolean willPlay = StatelyPlayer.willPlay(state); 64 | boolean seekable = StatelyPlayer.seekable(state); 65 | state = StatelyPlayer.mediaPlayerState(state); 66 | if (mediaPlayer != null) { 67 | setCurrentDuration(mediaPlayer.getDuration()); 68 | } else { 69 | setCurrentDuration(0); 70 | } 71 | 72 | switch (state) { 73 | case StatelyPlayer.END: 74 | setCurrentState(PlayerHater.STATE_IDLE); 75 | case StatelyPlayer.STOPPED: 76 | case StatelyPlayer.ERROR: 77 | case StatelyPlayer.IDLE: 78 | case StatelyPlayer.INITIALIZED: 79 | case StatelyPlayer.PLAYBACK_COMPLETED: 80 | case StatelyPlayer.PREPARING: 81 | case StatelyPlayer.PREPARED: 82 | if (willPlay) { 83 | setCurrentState(PlayerHater.STATE_LOADING); 84 | } else { 85 | setCurrentState(PlayerHater.STATE_IDLE); 86 | } 87 | break; 88 | case StatelyPlayer.PAUSED: 89 | setCurrentState(PlayerHater.STATE_PAUSED); 90 | break; 91 | case StatelyPlayer.STARTED: 92 | if (seekable) { 93 | setCurrentState(PlayerHater.STATE_PLAYING); 94 | } else { 95 | setCurrentState(PlayerHater.STATE_STREAMING); 96 | } 97 | break; 98 | default: 99 | throw new IllegalStateException("Illegal State: " + state); 100 | } 101 | } 102 | 103 | public synchronized int getCurrentState() { 104 | return mCurrentState; 105 | } 106 | 107 | private void setCurrentState(int currentState) { 108 | if (currentState != mCurrentState) { 109 | mCurrentState = currentState; 110 | notifyState(); 111 | } 112 | } 113 | 114 | private void setCurrentDuration(int duration) { 115 | if (mCurrentDuration != duration) { 116 | mCurrentDuration = duration; 117 | mListener.onDurationChanged(duration); 118 | } 119 | } 120 | 121 | private void notifyState() { 122 | if (mListener != null) { 123 | mListener.onStateChanged(mCurrentState); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/ipc/PlayerHaterClient.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.ipc; 17 | 18 | import org.prx.playerhater.PlayerHaterPlugin; 19 | import org.prx.playerhater.songs.SongHost; 20 | 21 | import android.app.PendingIntent; 22 | import android.net.Uri; 23 | import android.os.Bundle; 24 | import android.os.RemoteException; 25 | 26 | public class PlayerHaterClient extends IPlayerHaterClient.Stub { 27 | 28 | private final PlayerHaterPlugin mPlugin; 29 | 30 | public PlayerHaterClient(PlayerHaterPlugin plugin) { 31 | mPlugin = plugin; 32 | } 33 | 34 | @Override 35 | public void onSongChanged(int songTag) throws RemoteException { 36 | mPlugin.onSongChanged(SongHost.getSong(songTag)); 37 | } 38 | 39 | @Override 40 | public void onSongFinished(int songTag, int reason) throws RemoteException { 41 | mPlugin.onSongFinished(SongHost.getSong(songTag), reason); 42 | } 43 | 44 | @Override 45 | public void onDurationChanged(int duration) throws RemoteException { 46 | mPlugin.onDurationChanged(duration); 47 | } 48 | 49 | @Override 50 | public void onAudioLoading() throws RemoteException { 51 | mPlugin.onAudioLoading(); 52 | } 53 | 54 | @Override 55 | public void onAudioPaused() throws RemoteException { 56 | mPlugin.onAudioPaused(); 57 | } 58 | 59 | @Override 60 | public void onAudioResumed() throws RemoteException { 61 | mPlugin.onAudioResumed(); 62 | } 63 | 64 | @Override 65 | public void onAudioStarted() throws RemoteException { 66 | mPlugin.onAudioStarted(); 67 | } 68 | 69 | @Override 70 | public void onAudioStopped() throws RemoteException { 71 | mPlugin.onAudioStopped(); 72 | } 73 | 74 | @Override 75 | public void onTitleChanged(String title) throws RemoteException { 76 | mPlugin.onTitleChanged(title); 77 | } 78 | 79 | @Override 80 | public void onArtistChanged(String artist) throws RemoteException { 81 | mPlugin.onArtistChanged(artist); 82 | } 83 | 84 | @Override 85 | public void onAlbumTitleChanged(String albumTitle) throws RemoteException { 86 | mPlugin.onAlbumTitleChanged(albumTitle); 87 | } 88 | 89 | @Override 90 | public void onAlbumArtChanged(Uri uri) throws RemoteException { 91 | mPlugin.onAlbumArtChanged(uri); 92 | } 93 | 94 | @Override 95 | public void onTransportControlFlagsChanged(int transportControlFlags) 96 | throws RemoteException { 97 | mPlugin.onTransportControlFlagsChanged(transportControlFlags); 98 | } 99 | 100 | @Override 101 | public void onNextSongAvailable(int songTag) throws RemoteException { 102 | mPlugin.onNextSongAvailable(SongHost.getSong(songTag)); 103 | } 104 | 105 | @Override 106 | public void onNextSongUnavailable() throws RemoteException { 107 | mPlugin.onNextSongUnavailable(); 108 | } 109 | 110 | @Override 111 | public void onChangesComplete() throws RemoteException { 112 | mPlugin.onChangesComplete(); 113 | } 114 | 115 | @Override 116 | public void onIntentActivityChanged(PendingIntent intent) 117 | throws RemoteException { 118 | mPlugin.onPendingIntentChanged(intent); 119 | } 120 | 121 | @Override 122 | public String getSongTitle(int songTag) throws RemoteException { 123 | return SongHost.getLocalSong(songTag).getTitle(); 124 | } 125 | 126 | @Override 127 | public String getSongArtist(int songTag) throws RemoteException { 128 | return SongHost.getLocalSong(songTag).getArtist(); 129 | } 130 | 131 | @Override 132 | public String getSongAlbumTitle(int songTag) throws RemoteException { 133 | return SongHost.getLocalSong(songTag).getAlbumTitle(); 134 | } 135 | 136 | @Override 137 | public Uri getSongAlbumArt(int songTag) throws RemoteException { 138 | return SongHost.getLocalSong(songTag).getAlbumArt(); 139 | } 140 | 141 | @Override 142 | public Uri getSongUri(int songTag) throws RemoteException { 143 | return SongHost.getLocalSong(songTag).getUri(); 144 | } 145 | 146 | @Override 147 | public Bundle getSongExtra(int songTag) throws RemoteException { 148 | return SongHost.getLocalSong(songTag).getExtra(); 149 | } 150 | 151 | @Override 152 | public void onPlayerHaterShutdown() throws RemoteException { 153 | mPlugin.onPlayerHaterShutdown(); 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/mediaplayer/MediaPlayerPool.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.mediaplayer; 17 | 18 | import android.content.Context; 19 | import android.net.Uri; 20 | 21 | import java.lang.reflect.InvocationTargetException; 22 | import java.util.ArrayList; 23 | import java.util.HashMap; 24 | import java.util.HashSet; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.Set; 28 | 29 | import org.prx.playerhater.util.Log; 30 | 31 | public class MediaPlayerPool

{ 32 | 33 | private final Map mMediaPlayers = new HashMap(); 34 | private final Set

mIdlePlayers = new HashSet

(); 35 | private final List mRequests = new ArrayList(); 36 | private final Class mClass; 37 | 38 | public static MediaPlayerPool getInstance(Context context, 39 | Class klass) { 40 | return new MediaPlayerPool(context, klass); 41 | } 42 | 43 | public MediaPlayerPool(Context context, Class

mediaPlayerClass) { 44 | this(context, mediaPlayerClass, 3); 45 | } 46 | 47 | public MediaPlayerPool(Context context, Class

mediaPlayerClass, int size) { 48 | mClass = mediaPlayerClass; 49 | for (int i = 0; i < size; i++) { 50 | try { 51 | mIdlePlayers.add(mClass.getConstructor(Context.class).newInstance(context)); 52 | } catch (InstantiationException e) { 53 | throw new IllegalArgumentException(e); 54 | } catch (IllegalAccessException e) { 55 | throw new IllegalArgumentException(e); 56 | } catch (NoSuchMethodException e) { 57 | throw new IllegalArgumentException(e); 58 | } catch (InvocationTargetException e) { 59 | throw new IllegalArgumentException(e); 60 | } 61 | } 62 | } 63 | 64 | public synchronized void release() { 65 | for (SynchronousPlayer player : mIdlePlayers) { 66 | player.release(); 67 | } 68 | mIdlePlayers.clear(); 69 | for (Uri uri : mRequests) { 70 | mMediaPlayers.remove(uri).release(); 71 | } 72 | mRequests.clear(); 73 | for (SynchronousPlayer player : mMediaPlayers.values()) { 74 | player.release(); 75 | } 76 | mMediaPlayers.clear(); 77 | } 78 | 79 | public synchronized void prepare(Context context, Uri uri) { 80 | if (uri == null) { 81 | throw new IllegalArgumentException( 82 | "can't prepare a player for a null uri!"); 83 | } 84 | if (!mMediaPlayers.containsKey(uri)) { 85 | P player = getPlayer(); 86 | Log.d("Preparing " + player + " for " + uri); 87 | player.prepare(context, uri); 88 | addPlayer(player, uri); 89 | } 90 | } 91 | 92 | public synchronized P getPlayer(Context context, Uri uri) { 93 | Log.d("Getting player for " + uri); 94 | if (mMediaPlayers.containsKey(uri)) { 95 | mRequests.remove(uri); 96 | Log.d("Found one (" + mMediaPlayers.get(uri) + ")"); 97 | return mMediaPlayers.remove(uri); 98 | } else { 99 | P player = getPlayer(); 100 | player.prepare(context, uri); 101 | return player; 102 | } 103 | } 104 | 105 | public synchronized void recycle(P player) { 106 | if (player != null && player.getState() != StatelyPlayer.END) { 107 | player.reset(); 108 | mIdlePlayers.add(player); 109 | } 110 | } 111 | 112 | @SuppressWarnings("unchecked") 113 | private synchronized P getPlayer() { 114 | if (mIdlePlayers.size() > 0) { 115 | P player = (P) mIdlePlayers.toArray()[0]; 116 | mIdlePlayers.remove(player); 117 | Log.d("Getting idle player (" + player + ")"); 118 | return player; 119 | } else if (mRequests.size() > 0) { 120 | Uri leastRecentReq = mRequests.remove(mRequests.size() - 1); 121 | Log.d("Recycling the player that is prepared for " + leastRecentReq); 122 | P player = mMediaPlayers.remove(leastRecentReq); 123 | Log.d("Player: " + player); 124 | player.reset(); 125 | return player; 126 | } else { 127 | throw new IllegalStateException( 128 | "MediaPlayer resources exhausted. Are you sure you're #recycle()ing on time?"); 129 | } 130 | } 131 | 132 | private synchronized void addPlayer(P player, Uri uri) { 133 | mRequests.remove(uri); 134 | mRequests.add(0, uri); 135 | mMediaPlayers.put(uri, player); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/plugins/AbstractPlugin.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.plugins; 17 | 18 | import android.app.PendingIntent; 19 | import android.content.Context; 20 | import android.net.Uri; 21 | 22 | import org.prx.playerhater.PlayerHater; 23 | import org.prx.playerhater.PlayerHaterPlugin; 24 | import org.prx.playerhater.Song; 25 | 26 | /** 27 | * A simple helper for writing {@linkplain PlayerHaterPlugin}s 28 | *

29 | * Subclasses MUST implement a default no-argument constructor. 30 | * 31 | * @author Chris Rhoden 32 | * @version 2.1.0 33 | * @see {@link PlayerHaterPlugin} 34 | * @since 2.0.0 35 | */ 36 | public abstract class AbstractPlugin implements PlayerHaterPlugin { 37 | private PlayerHater mPlayerHater; 38 | private Context mContext; 39 | 40 | public AbstractPlugin() { 41 | 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | *

47 | * Overridden implementations should be sure to call 48 | * {@code super.onPlayerHaterLoaded} so that future calls to 49 | * {@link #getContext()} and {@link #getPlayerHater()} can succeed. 50 | */ 51 | @Override 52 | public synchronized void onPlayerHaterLoaded(Context context, PlayerHater playerHater) { 53 | mContext = context; 54 | mPlayerHater = playerHater; 55 | } 56 | 57 | @Override 58 | public void onAudioStarted() { 59 | } 60 | 61 | /** 62 | * {@inheritDoc} 63 | *

64 | * The default implementation forwards this method call to 65 | * {@link #onAudioStarted()} 66 | */ 67 | @Override 68 | public void onAudioResumed() { 69 | onAudioStarted(); 70 | } 71 | 72 | @Override 73 | public void onAudioStopped() { 74 | } 75 | 76 | @Override 77 | public void onTitleChanged(String title) { 78 | } 79 | 80 | @Override 81 | public void onArtistChanged(String artist) { 82 | } 83 | 84 | @Override 85 | public void onAlbumTitleChanged(String albumTitle) { 86 | } 87 | 88 | @Override 89 | public void onAlbumArtChanged(Uri uri) { 90 | } 91 | 92 | /** 93 | * {@inheritDoc} 94 | *

95 | * The default implementation will call {@link #onAlbumArtChanged(Uri)} 96 | * , {@link #onTitleChanged(String)}, and {@link #onArtistChanged(String)} 97 | */ 98 | @Override 99 | public void onSongChanged(Song song) { 100 | if (song != null) { 101 | onTitleChanged(song.getTitle()); 102 | onArtistChanged(song.getArtist()); 103 | onAlbumTitleChanged(song.getAlbumTitle()); 104 | onAlbumArtChanged(song.getAlbumArt()); 105 | } 106 | } 107 | 108 | @Override 109 | public void onDurationChanged(int duration) { 110 | } 111 | 112 | @Override 113 | public void onAudioLoading() { 114 | } 115 | 116 | @Override 117 | public void onAudioPaused() { 118 | } 119 | 120 | @Override 121 | public void onNextSongAvailable(Song nextSong) { 122 | } 123 | 124 | @Override 125 | public void onNextSongUnavailable() { 126 | } 127 | 128 | @Override 129 | public void onPendingIntentChanged(PendingIntent pending) { 130 | } 131 | 132 | @Override 133 | public void onSongFinished(Song song, int reason) { 134 | } 135 | 136 | @Override 137 | public void onChangesComplete() { 138 | } 139 | 140 | @Override 141 | public void onTransportControlFlagsChanged(int transportControlFlags) { 142 | } 143 | 144 | @Override 145 | public void onPlayerHaterShutdown() { 146 | mPlayerHater = null; 147 | mContext = null; 148 | } 149 | 150 | /** 151 | * Grants the plugin easy access to the instance of {@link PlayerHater} that 152 | * it is permitted to use. 153 | * 154 | * @return An instance of PlayerHater 155 | */ 156 | protected synchronized final PlayerHater getPlayerHater() { 157 | if (mPlayerHater == null) 158 | throw new IllegalStateException("PlayerHater is not loaded yet or has been shut down."); 159 | return mPlayerHater; 160 | } 161 | 162 | /** 163 | * A method providing simple access to the plugin's context without having 164 | * to override {@link #onPlayerHaterLoaded(Context, PlayerHater)} 165 | * 166 | * @return The {@link Context} in which the plugin is running. 167 | */ 168 | protected synchronized final Context getContext() { 169 | if (mContext == null) 170 | throw new IllegalStateException("PlayerHater is not loaded yet or has been shut down."); 171 | return mContext; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/main/res/layout-v16/zzz_ph_jbb_notification.xml: -------------------------------------------------------------------------------- 1 | 16 | 20 | 21 | 29 | 30 | 35 | 36 | 49 | 50 | 51 | 52 | 53 | 64 | 65 | 70 | 71 | 78 | 79 | 90 | 91 | 102 | 103 | 114 | 115 | 116 | 117 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/mediaplayer/SynchronousPlayer.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.mediaplayer; 17 | 18 | import android.content.Context; 19 | import android.media.MediaPlayer; 20 | import android.media.MediaPlayer.OnPreparedListener; 21 | import android.media.MediaPlayer.OnSeekCompleteListener; 22 | import android.net.Uri; 23 | 24 | public class SynchronousPlayer extends StatelyPlayer implements 25 | OnPreparedListener, OnSeekCompleteListener { 26 | 27 | private boolean mShouldPlayWhenPrepared; 28 | private int mShouldSkipWhenPrepared; 29 | private Uri mShouldSetDataSourceUri; 30 | private Context mShouldSetPrepareContext; 31 | 32 | public SynchronousPlayer(Context context) { 33 | super(context); 34 | } 35 | 36 | @Override 37 | public synchronized void onPrepared(MediaPlayer mp) { 38 | super.onPrepared(mp); 39 | startIfNecessary(); 40 | } 41 | 42 | @Override 43 | public synchronized void onSeekComplete(MediaPlayer mp) { 44 | super.onSeekComplete(mp); 45 | startIfNecessary(); 46 | } 47 | 48 | @Override 49 | public synchronized boolean prepare(Context context, Uri uri) { 50 | mShouldPlayWhenPrepared = false; 51 | mShouldSkipWhenPrepared = 0; 52 | mShouldSetDataSourceUri = null; 53 | mShouldSetPrepareContext = null; 54 | switch (getState()) { 55 | case IDLE: 56 | try { 57 | setDataSource(context, uri); 58 | } catch (Exception e) { 59 | return false; 60 | } 61 | case INITIALIZED: 62 | case STOPPED: 63 | case LOADING_CONTENT: 64 | prepareAsync(); 65 | break; 66 | case PREPARING: 67 | case PREPARING_CONTENT: 68 | mShouldSetDataSourceUri = uri; 69 | mShouldSetPrepareContext = context; 70 | break; 71 | default: 72 | reset(); 73 | try { 74 | setDataSource(context, uri); 75 | } catch (Exception e) { 76 | return false; 77 | } 78 | try { 79 | prepareAsync(); 80 | } catch (IllegalStateException e) { 81 | // TODO Auto-generated catch block 82 | e.printStackTrace(); 83 | } 84 | } 85 | return true; 86 | } 87 | 88 | @Override 89 | public synchronized boolean prepareAndPlay(Context context, Uri uri, 90 | int position) { 91 | if (prepare(context, uri)) { 92 | if (position != 0) { 93 | mShouldPlayWhenPrepared = true; 94 | seekTo(position); 95 | } else { 96 | start(); 97 | } 98 | return true; 99 | } else { 100 | return false; 101 | } 102 | } 103 | 104 | @Override 105 | public synchronized void start() { 106 | if (getState() == PREPARING) { 107 | mShouldPlayWhenPrepared = true; 108 | onStateChanged(); 109 | } else if (getState() == INITIALIZED || getState() == STOPPED) { 110 | mShouldPlayWhenPrepared = true; 111 | try { 112 | prepareAsync(); 113 | } catch (Exception e) {} 114 | } else { 115 | super.start(); 116 | } 117 | } 118 | 119 | @Override 120 | public synchronized void seekTo(int msec) { 121 | int state = getState(); 122 | if (state == PREPARING || state == INITIALIZED || state == STOPPED 123 | || state == LOADING_CONTENT || state == PREPARING_CONTENT) { 124 | mShouldSkipWhenPrepared = msec; 125 | if (state == INITIALIZED || state == STOPPED) { 126 | prepareAsync(); 127 | } 128 | } else if (state == PREPARED || state == PAUSED 129 | || state == PLAYBACK_COMPLETED) { 130 | super.seekTo(msec); 131 | } else if (state == STARTED) { 132 | super.pause(); 133 | mShouldPlayWhenPrepared = true; 134 | super.seekTo(msec); 135 | } 136 | } 137 | 138 | @Override 139 | public synchronized boolean conditionalPause() { 140 | if (mShouldPlayWhenPrepared) { 141 | mShouldPlayWhenPrepared = false; 142 | return true; 143 | } else if (getState() == STARTED) { 144 | pause(); 145 | return true; 146 | } 147 | return false; 148 | } 149 | 150 | @Override 151 | public synchronized boolean conditionalPlay() { 152 | try { 153 | start(); 154 | } catch (Exception e) { 155 | return false; 156 | } 157 | return true; 158 | } 159 | 160 | @Override 161 | public synchronized boolean conditionalStop() { 162 | if (mShouldPlayWhenPrepared) { 163 | mShouldPlayWhenPrepared = false; 164 | return true; 165 | } 166 | int state = getState(); 167 | if (state != PREPARED && state != STARTED && state != PAUSED 168 | && state != PLAYBACK_COMPLETED) { 169 | return false; 170 | } 171 | stop(); 172 | return true; 173 | } 174 | 175 | @Override 176 | public synchronized boolean isWaitingToPlay() { 177 | return super.isWaitingToPlay() 178 | || (mShouldSkipWhenPrepared != 0 | mShouldPlayWhenPrepared); 179 | } 180 | 181 | private void startIfNecessary() { 182 | if (mShouldSetDataSourceUri != null) { 183 | if (mShouldPlayWhenPrepared) { 184 | prepareAndPlay(mShouldSetPrepareContext, 185 | mShouldSetDataSourceUri, mShouldSkipWhenPrepared); 186 | } else { 187 | prepare(mShouldSetPrepareContext, mShouldSetDataSourceUri); 188 | } 189 | } else if (mShouldSkipWhenPrepared != 0) { 190 | seekTo(mShouldSkipWhenPrepared); 191 | mShouldSkipWhenPrepared = 0; 192 | } else if (mShouldPlayWhenPrepared) { 193 | start(); 194 | mShouldPlayWhenPrepared = false; 195 | } 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/plugins/NotificationPlugin.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.plugins; 17 | 18 | import android.annotation.TargetApi; 19 | import android.app.Notification; 20 | import android.app.NotificationManager; 21 | import android.app.PendingIntent; 22 | import android.content.Context; 23 | import android.content.Intent; 24 | import android.content.pm.PackageManager; 25 | import android.os.Build; 26 | 27 | import org.prx.playerhater.PlayerHater; 28 | import org.prx.playerhater.R; 29 | import org.prx.playerhater.wrappers.ServicePlayerHater; 30 | 31 | @TargetApi(Build.VERSION_CODES.CUPCAKE) 32 | public class NotificationPlugin extends AbstractPlugin { 33 | 34 | private static final int NOTIFICATION_NU = 0x974732; 35 | private NotificationManager mNotificationManager; 36 | protected PendingIntent mContentIntent; 37 | protected String mNotificationTitle = "PlayerHater"; 38 | protected String mNotificationText = "Version 0.1.0"; 39 | private boolean mIsVisible = false; 40 | protected Notification mNotification; 41 | private boolean mShouldBeVisible; 42 | 43 | public NotificationPlugin() { 44 | } 45 | 46 | @Override 47 | public void onPlayerHaterLoaded(Context context, PlayerHater playerHater) { 48 | super.onPlayerHaterLoaded(context, playerHater); 49 | if (!(playerHater instanceof ServicePlayerHater)) { 50 | throw new IllegalArgumentException( 51 | "NotificationPlugin must be run on the server side"); 52 | } 53 | 54 | PackageManager packageManager = context.getPackageManager(); 55 | mNotificationManager = (NotificationManager) context 56 | .getSystemService(Context.NOTIFICATION_SERVICE); 57 | 58 | Intent resumeActivityIntent = packageManager 59 | .getLaunchIntentForPackage(getContext().getPackageName()); 60 | resumeActivityIntent.addCategory(Intent.CATEGORY_LAUNCHER); 61 | resumeActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP 62 | | Intent.FLAG_ACTIVITY_SINGLE_TOP); 63 | mContentIntent = PendingIntent.getActivity(getContext(), 64 | NOTIFICATION_NU, resumeActivityIntent, 0); 65 | } 66 | 67 | @Override 68 | public void onAudioLoading() { 69 | mShouldBeVisible = true; 70 | } 71 | 72 | @Override 73 | public void onAudioStarted() { 74 | mShouldBeVisible = true; 75 | } 76 | 77 | @SuppressWarnings("deprecation") 78 | protected Notification getNotification() { 79 | if (mNotification == null) { 80 | mNotification = new Notification(R.drawable.zzz_ph_ic_notification, 81 | "Playing: " + mNotificationTitle, 0); 82 | } else { 83 | mNotification.tickerText = "Playing: " + mNotificationTitle; 84 | } 85 | mNotification.setLatestEventInfo(getContext(), mNotificationTitle, 86 | mNotificationText, mContentIntent); 87 | return mNotification; 88 | } 89 | 90 | @Override 91 | public void onAudioPaused() { 92 | onAudioStopped(); 93 | } 94 | 95 | @Override 96 | public void onAudioStopped() { 97 | mShouldBeVisible = false; 98 | mIsVisible = false; 99 | mNotificationManager.cancel(NOTIFICATION_NU); 100 | mNotification = null; 101 | ServicePlayerHater binder = getBinder(); 102 | if (binder != null) { 103 | binder.stopForeground(true); 104 | } 105 | } 106 | 107 | @Override 108 | public void onTitleChanged(String notificationTitle) { 109 | mNotificationTitle = notificationTitle; 110 | } 111 | 112 | public void onPendingIntentChanged(PendingIntent contentIntent) { 113 | mContentIntent = contentIntent; 114 | if (mNotification != null) { 115 | mNotification.contentIntent = mContentIntent; 116 | } 117 | } 118 | 119 | @Override 120 | public void onArtistChanged(String notificationText) { 121 | mNotificationText = notificationText; 122 | } 123 | 124 | @Override 125 | public void onChangesComplete() { 126 | ServicePlayerHater binder; 127 | if (mShouldBeVisible && !mIsVisible) { 128 | binder = getBinder(); 129 | if (binder != null) { 130 | binder.startForeground(NOTIFICATION_NU, getNotification()); 131 | } 132 | mIsVisible = true; 133 | } else if (mIsVisible && !mShouldBeVisible) { 134 | binder = getBinder(); 135 | if (binder != null) { 136 | binder.stopForeground(true); 137 | } 138 | } else if (mIsVisible && mShouldBeVisible) { 139 | updateNotification(); 140 | } 141 | } 142 | 143 | protected void updateNotification() { 144 | mNotificationManager.notify(NOTIFICATION_NU, getNotification()); 145 | } 146 | 147 | protected ServicePlayerHater getBinder() { 148 | try { 149 | return (ServicePlayerHater) getPlayerHater(); 150 | } catch (IllegalStateException exception) { 151 | return null; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/ipc/ClientPlugin.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.ipc; 17 | 18 | import org.prx.playerhater.PlayerHater; 19 | import org.prx.playerhater.PlayerHaterPlugin; 20 | import org.prx.playerhater.Song; 21 | import org.prx.playerhater.songs.SongHost; 22 | import org.prx.playerhater.util.Log; 23 | 24 | import android.app.PendingIntent; 25 | import android.content.Context; 26 | import android.net.Uri; 27 | import android.os.RemoteException; 28 | 29 | public class ClientPlugin implements PlayerHaterPlugin { 30 | 31 | private static final String CLIENT_ERROR = "ClientPlugin has gone away..."; 32 | 33 | private final IPlayerHaterClient mClient; 34 | 35 | public ClientPlugin(IPlayerHaterClient client) { 36 | mClient = client; 37 | } 38 | 39 | @Override 40 | public void onPlayerHaterLoaded(Context context, PlayerHater playerHater) { 41 | } 42 | 43 | @Override 44 | public void onSongChanged(Song song) { 45 | try { 46 | mClient.onSongChanged(SongHost.getTag(song)); 47 | } catch (RemoteException e) { 48 | Log.e(CLIENT_ERROR, e); 49 | throw new IllegalStateException(CLIENT_ERROR, e); 50 | } 51 | } 52 | 53 | @Override 54 | public void onSongFinished(Song song, int reason) { 55 | try { 56 | mClient.onSongFinished(SongHost.getTag(song), reason); 57 | } catch (RemoteException e) { 58 | Log.e(CLIENT_ERROR, e); 59 | throw new IllegalStateException(CLIENT_ERROR, e); 60 | } 61 | } 62 | 63 | @Override 64 | public void onDurationChanged(int duration) { 65 | try { 66 | mClient.onDurationChanged(duration); 67 | } catch (RemoteException e) { 68 | Log.e(CLIENT_ERROR, e); 69 | throw new IllegalStateException(CLIENT_ERROR, e); 70 | } 71 | } 72 | 73 | @Override 74 | public void onAudioLoading() { 75 | try { 76 | mClient.onAudioLoading(); 77 | } catch (RemoteException e) { 78 | Log.e(CLIENT_ERROR, e); 79 | throw new IllegalStateException(CLIENT_ERROR, e); 80 | } 81 | } 82 | 83 | @Override 84 | public void onAudioPaused() { 85 | try { 86 | mClient.onAudioPaused(); 87 | } catch (RemoteException e) { 88 | Log.e(CLIENT_ERROR, e); 89 | throw new IllegalStateException(CLIENT_ERROR, e); 90 | } 91 | } 92 | 93 | @Override 94 | public void onAudioResumed() { 95 | try { 96 | mClient.onAudioResumed(); 97 | } catch (RemoteException e) { 98 | Log.e(CLIENT_ERROR, e); 99 | throw new IllegalStateException(CLIENT_ERROR, e); 100 | } 101 | } 102 | 103 | @Override 104 | public void onAudioStarted() { 105 | try { 106 | mClient.onAudioStarted(); 107 | } catch (RemoteException e) { 108 | Log.e(CLIENT_ERROR, e); 109 | throw new IllegalStateException(CLIENT_ERROR, e); 110 | } 111 | } 112 | 113 | @Override 114 | public void onAudioStopped() { 115 | try { 116 | mClient.onAudioStopped(); 117 | } catch (RemoteException e) { 118 | Log.e(CLIENT_ERROR, e); 119 | throw new IllegalStateException(CLIENT_ERROR, e); 120 | } 121 | } 122 | 123 | @Override 124 | public void onTitleChanged(String title) { 125 | try { 126 | mClient.onTitleChanged(title); 127 | } catch (RemoteException e) { 128 | Log.e(CLIENT_ERROR, e); 129 | throw new IllegalStateException(CLIENT_ERROR, e); 130 | } 131 | } 132 | 133 | @Override 134 | public void onArtistChanged(String artist) { 135 | try { 136 | mClient.onArtistChanged(artist); 137 | } catch (RemoteException e) { 138 | Log.e(CLIENT_ERROR, e); 139 | throw new IllegalStateException(CLIENT_ERROR, e); 140 | } 141 | } 142 | 143 | @Override 144 | public void onAlbumArtChanged(Uri url) { 145 | try { 146 | mClient.onAlbumArtChanged(url); 147 | } catch (RemoteException e) { 148 | Log.e(CLIENT_ERROR, e); 149 | throw new IllegalStateException(CLIENT_ERROR, e); 150 | } 151 | } 152 | 153 | @Override 154 | public void onNextSongAvailable(Song nextTrack) { 155 | try { 156 | mClient.onNextSongAvailable(SongHost.getTag(nextTrack)); 157 | } catch (RemoteException e) { 158 | Log.e(CLIENT_ERROR, e); 159 | throw new IllegalStateException(CLIENT_ERROR, e); 160 | } 161 | } 162 | 163 | @Override 164 | public void onNextSongUnavailable() { 165 | try { 166 | mClient.onNextSongUnavailable(); 167 | } catch (RemoteException e) { 168 | Log.e(CLIENT_ERROR, e); 169 | throw new IllegalStateException(CLIENT_ERROR, e); 170 | } 171 | } 172 | 173 | @Override 174 | public void onTransportControlFlagsChanged(int transportControlFlags) { 175 | try { 176 | mClient.onTransportControlFlagsChanged(transportControlFlags); 177 | } catch (RemoteException e) { 178 | Log.e(CLIENT_ERROR, e); 179 | throw new IllegalStateException(CLIENT_ERROR, e); 180 | } 181 | } 182 | 183 | @Override 184 | public void onPendingIntentChanged(PendingIntent intent) { 185 | try { 186 | mClient.onIntentActivityChanged(intent); 187 | } catch (RemoteException e) { 188 | Log.e(CLIENT_ERROR, e); 189 | throw new IllegalStateException(CLIENT_ERROR, e); 190 | } 191 | } 192 | 193 | @Override 194 | public void onChangesComplete() { 195 | try { 196 | mClient.onChangesComplete(); 197 | } catch (RemoteException e) { 198 | Log.e(CLIENT_ERROR, e); 199 | throw new IllegalStateException(CLIENT_ERROR, e); 200 | } 201 | } 202 | 203 | @Override 204 | public void onAlbumTitleChanged(String albumTitle) { 205 | try { 206 | mClient.onAlbumTitleChanged(albumTitle); 207 | } catch (RemoteException e) { 208 | Log.e(CLIENT_ERROR, e); 209 | throw new IllegalStateException(CLIENT_ERROR, e); 210 | } 211 | } 212 | 213 | @Override public void onPlayerHaterShutdown() { 214 | try { 215 | mClient.onPlayerHaterShutdown(); 216 | } catch (RemoteException e) { 217 | Log.e(CLIENT_ERROR, e); 218 | throw new IllegalStateException(CLIENT_ERROR, e); 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/PlaybackService.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater; 17 | 18 | import org.prx.playerhater.mediaplayer.MediaPlayerPool; 19 | import org.prx.playerhater.mediaplayer.PlaylistSupportingPlayer; 20 | import org.prx.playerhater.service.PlayerHaterService; 21 | import org.prx.playerhater.songs.SongQueue; 22 | import org.prx.playerhater.songs.SongQueue.OnQueuedSongsChangedListener; 23 | 24 | import android.media.MediaPlayer; 25 | import android.media.MediaPlayer.OnCompletionListener; 26 | import android.media.MediaPlayer.OnErrorListener; 27 | 28 | public class PlaybackService extends PlayerHaterService implements 29 | OnQueuedSongsChangedListener, OnErrorListener, OnCompletionListener { 30 | 31 | private MediaPlayerPool mMediaPlayerPool; 32 | 33 | @Override 34 | public void onCreate() { 35 | super.onCreate(); 36 | mMediaPlayerPool = MediaPlayerPool 37 | .getInstance(getApplicationContext(), PlaylistSupportingPlayer.class); 38 | } 39 | 40 | @Override 41 | public void onDestroy() { 42 | mMediaPlayerPool.release(); 43 | super.onDestroy(); 44 | } 45 | 46 | @Override 47 | public boolean play(Song song, int startTime) { 48 | onSongFinished(nowPlaying(), PlayerHater.FINISH_SKIP_BUTTON); 49 | onNowPlayingChanged(song, nowPlaying()); 50 | getQueue().appendAndSkip(song); 51 | seekTo(startTime); 52 | return play(); 53 | } 54 | 55 | @Override 56 | public boolean seekTo(int startTime) { 57 | getMediaPlayer().seekTo(startTime); 58 | return true; 59 | } 60 | 61 | @Override 62 | public int enqueue(Song song) { 63 | int q = getQueue().appendSong(song); 64 | return q; 65 | } 66 | 67 | @Override 68 | public void enqueue(int position, Song song) { 69 | getQueue().addSongAtPosition(song, position); 70 | } 71 | 72 | @Override 73 | public boolean skipTo(int position) { 74 | startTransaction(); 75 | onSongFinished(nowPlaying(), PlayerHater.FINISH_SKIP_BUTTON); 76 | if (getQueue().skipTo(position)) { 77 | return true; 78 | } else { 79 | commitTransaction(); 80 | return false; 81 | } 82 | } 83 | 84 | @Override 85 | public void skip() { 86 | if (nextAllowed()) { 87 | startTransaction(); 88 | onSongFinished(nowPlaying(), PlayerHater.FINISH_SKIP_BUTTON); 89 | getQueue().next(); 90 | } 91 | } 92 | 93 | @Override 94 | public void skipBack() { 95 | if (previousAllowed()) { 96 | if (getCurrentPosition() < 2000) { 97 | startTransaction(); 98 | onSongFinished(nowPlaying(), PlayerHater.FINISH_SKIP_BUTTON); 99 | getQueue().back(); 100 | } else { 101 | seekTo(0); 102 | } 103 | } 104 | } 105 | 106 | @Override 107 | public void emptyQueue() { 108 | getQueue().empty(); 109 | } 110 | 111 | @Override 112 | public Song nowPlaying() { 113 | return getQueue().getNowPlaying(); 114 | } 115 | 116 | private SongQueue mQueue; 117 | 118 | private SongQueue getQueue() { 119 | if (mQueue == null) { 120 | mQueue = new SongQueue(); 121 | mQueue.setQueuedSongsChangedListener(this); 122 | } 123 | return mQueue; 124 | } 125 | 126 | @Override 127 | public int getQueueLength() { 128 | return getQueue().size(); 129 | } 130 | 131 | @Override 132 | public int getQueuePosition() { 133 | return getQueue().getPosition() - (peekMediaPlayer() != null && getCurrentPosition() > 0 ? 0 : 1); 134 | } 135 | 136 | @Override 137 | public boolean removeFromQueue(int position) { 138 | return getQueue().remove(position); 139 | } 140 | 141 | @Override 142 | public void onNowPlayingChanged(Song nowPlaying, Song was) { 143 | startTransaction(); 144 | mMediaPlayerPool.recycle(peekMediaPlayer()); 145 | if (nowPlaying == null) { 146 | setMediaPlayer(null); 147 | } else { 148 | setMediaPlayer(mMediaPlayerPool.getPlayer(getApplicationContext(), 149 | nowPlaying.getUri())); 150 | if (isPlaying()) { 151 | getMediaPlayer().start(); 152 | } else { 153 | 154 | } 155 | } 156 | commitTransaction(); 157 | onSongChanged(nowPlaying); 158 | } 159 | 160 | @Override 161 | public void onNextSongChanged(Song nextSong, Song was) { 162 | if (nextSong != null) { 163 | mMediaPlayerPool 164 | .prepare(getApplicationContext(), nextSong.getUri()); 165 | } 166 | onNextSongChanged(nextSong); 167 | } 168 | 169 | @Override 170 | public void onCompletion(MediaPlayer mp) { 171 | if (peekMediaPlayer() != null && peekMediaPlayer().equals(mp)) { 172 | startTransaction(); 173 | mMediaPlayerPool.recycle(peekMediaPlayer()); 174 | setMediaPlayer(null); 175 | onSongFinished(nowPlaying(), PlayerHater.FINISH_SONG_END); 176 | getQueue().next(); 177 | } 178 | } 179 | 180 | @Override 181 | public boolean onError(MediaPlayer mp, int what, int extra) { 182 | if (peekMediaPlayer() != null && peekMediaPlayer().equals(mp)) { 183 | startTransaction(); 184 | mMediaPlayerPool.recycle(peekMediaPlayer()); 185 | setMediaPlayer(null); 186 | onSongFinished(nowPlaying(), PlayerHater.FINISH_ERROR); 187 | getQueue().next(); 188 | return true; 189 | } 190 | return false; 191 | } 192 | 193 | @Override 194 | public Song getNextSong() { 195 | return getQueue().getNextPlaying(); 196 | } 197 | 198 | @Override 199 | protected synchronized void setMediaPlayer( 200 | PlaylistSupportingPlayer mediaPlayer) { 201 | PlaylistSupportingPlayer oldPlayer = peekMediaPlayer(); 202 | if (oldPlayer != null) { 203 | oldPlayer.setOnErrorListener(null); 204 | oldPlayer.setOnCompletionListener(null); 205 | } 206 | super.setMediaPlayer(mediaPlayer); 207 | if (mediaPlayer != null) { 208 | mediaPlayer.setOnErrorListener(this); 209 | mediaPlayer.setOnCompletionListener(this); 210 | } 211 | } 212 | 213 | @Override 214 | synchronized protected PlaylistSupportingPlayer getMediaPlayer() { 215 | if (peekMediaPlayer() == null) { 216 | PlaylistSupportingPlayer player = mMediaPlayerPool.getPlayer(getApplicationContext(), nowPlaying().getUri()); 217 | setMediaPlayer(player); 218 | } 219 | return peekMediaPlayer(); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/ipc/PlayerHaterServer.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 | 17 | package org.prx.playerhater.ipc; 18 | 19 | import org.prx.playerhater.service.PlayerHaterService; 20 | import org.prx.playerhater.songs.SongHost; 21 | import org.prx.playerhater.wrappers.ThreadsafeServicePlayerHater; 22 | 23 | import android.app.Notification; 24 | import android.app.PendingIntent; 25 | import android.net.Uri; 26 | import android.os.Bundle; 27 | import android.os.RemoteException; 28 | 29 | public class PlayerHaterServer extends IPlayerHaterServer.Stub { 30 | 31 | private final ThreadsafeServicePlayerHater mService; 32 | 33 | public PlayerHaterServer(PlayerHaterService service) { 34 | mService = new ThreadsafeServicePlayerHater(service); 35 | } 36 | 37 | public PlayerHaterServer(ThreadsafeServicePlayerHater playerHater) { 38 | mService = playerHater; 39 | } 40 | 41 | @Override 42 | public void setClient(IPlayerHaterClient client) throws RemoteException { 43 | mService.setClient(client); 44 | } 45 | 46 | @Override 47 | public void onRemoteControlButtonPressed(int keyCode) 48 | throws RemoteException { 49 | mService.onRemoteControlButtonPressed(keyCode); 50 | } 51 | 52 | @Override 53 | public void startForeground(int notificationNu, Notification notification) 54 | throws RemoteException { 55 | mService.startForeground(notificationNu, notification); 56 | } 57 | 58 | @Override 59 | public void stopForeground(boolean fact) throws RemoteException { 60 | mService.stopForeground(fact); 61 | } 62 | 63 | @Override 64 | public void duck() throws RemoteException { 65 | mService.duck(); 66 | } 67 | 68 | @Override 69 | public void unduck() throws RemoteException { 70 | mService.unduck(); 71 | } 72 | 73 | @Override 74 | public boolean pause() throws RemoteException { 75 | return mService.pause(true); 76 | } 77 | 78 | @Override 79 | public boolean stop() throws RemoteException { 80 | return mService.stop(); 81 | } 82 | 83 | @Override 84 | public boolean resume() throws RemoteException { 85 | return mService.play(); 86 | } 87 | 88 | @Override 89 | public boolean playAtTime(int startTime) throws RemoteException { 90 | return mService.play(startTime); 91 | } 92 | 93 | @Override 94 | public boolean play(int songTag, Bundle songData, int startTime) throws RemoteException { 95 | return mService.play(SongHost.getSong(songTag, songData), startTime); 96 | } 97 | 98 | @Override 99 | public boolean seekTo(int startTime) throws RemoteException { 100 | return mService.seekTo(startTime); 101 | } 102 | 103 | @Override 104 | public int enqueue(int songTag, Bundle songData) throws RemoteException { 105 | return mService.enqueue(SongHost.getSong(songTag, songData)); 106 | } 107 | 108 | @Override 109 | public void enqueueAtPosition(int position, int songTag, Bundle songData) 110 | throws RemoteException { 111 | mService.enqueue(position, SongHost.getSong(songTag, songData)); 112 | } 113 | 114 | @Override 115 | public boolean skipTo(int position) throws RemoteException { 116 | return mService.skipTo(position); 117 | } 118 | 119 | @Override 120 | public void skip() throws RemoteException { 121 | mService.skip(); 122 | } 123 | 124 | @Override 125 | public void skipBack() throws RemoteException { 126 | mService.skipBack(); 127 | } 128 | 129 | @Override 130 | public void emptyQueue() throws RemoteException { 131 | mService.emptyQueue(); 132 | } 133 | 134 | @Override 135 | public int getCurrentPosition() throws RemoteException { 136 | return mService.getCurrentPosition(); 137 | } 138 | 139 | @Override 140 | public int getDuration() throws RemoteException { 141 | return mService.getDuration(); 142 | } 143 | 144 | @Override 145 | public int nowPlaying() throws RemoteException { 146 | return SongHost.getTag(mService.nowPlaying()); 147 | } 148 | 149 | @Override 150 | public boolean isPlaying() throws RemoteException { 151 | return mService.isPlaying(); 152 | } 153 | 154 | @Override 155 | public boolean isLoading() throws RemoteException { 156 | return mService.isLoading(); 157 | } 158 | 159 | @Override 160 | public int getState() throws RemoteException { 161 | return mService.getState(); 162 | } 163 | 164 | @Override 165 | public void setTransportControlFlags(int transportControlFlags) 166 | throws RemoteException { 167 | mService.setTransportControlFlags(transportControlFlags); 168 | } 169 | 170 | @Override 171 | public int getQueueLength() throws RemoteException { 172 | return mService.getQueueLength(); 173 | } 174 | 175 | @Override 176 | public int getQueuePosition() throws RemoteException { 177 | return mService.getQueuePosition(); 178 | } 179 | 180 | @Override 181 | public boolean removeFromQueue(int position) throws RemoteException { 182 | return mService.removeFromQueue(position); 183 | } 184 | 185 | @Override 186 | public String getSongTitle(int songTag) throws RemoteException { 187 | return SongHost.getLocalSong(songTag).getTitle(); 188 | } 189 | 190 | @Override 191 | public String getSongArtist(int songTag) throws RemoteException { 192 | return SongHost.getLocalSong(songTag).getArtist(); 193 | } 194 | 195 | @Override 196 | public String getSongAlbumTitle(int songTag) throws RemoteException { 197 | return SongHost.getLocalSong(songTag).getAlbumTitle(); 198 | } 199 | 200 | @Override 201 | public Uri getSongAlbumArt(int songTag) throws RemoteException { 202 | return SongHost.getLocalSong(songTag).getAlbumArt(); 203 | } 204 | 205 | @Override 206 | public Uri getSongUri(int songTag) throws RemoteException { 207 | return SongHost.getLocalSong(songTag).getUri(); 208 | } 209 | 210 | @Override 211 | public Bundle getSongExtra(int songTag) throws RemoteException { 212 | return SongHost.getLocalSong(songTag).getExtra(); 213 | } 214 | 215 | @Override 216 | public void setPendingIntent(PendingIntent intent) throws RemoteException { 217 | mService.setPendingIntent(intent); 218 | } 219 | 220 | @Override 221 | public void slurp(int songTag, Bundle songData) throws RemoteException { 222 | SongHost.slurp(songTag, songData); 223 | } 224 | 225 | @Override 226 | public int getTransportControlFlags() throws RemoteException { 227 | return mService.getTransportControlFlags(); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/plugins/PlayerHaterListenerPlugin.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.plugins; 17 | 18 | import android.content.Context; 19 | import android.os.Handler; 20 | import android.os.Message; 21 | 22 | import org.prx.playerhater.PlayerHater; 23 | import org.prx.playerhater.PlayerHaterListener; 24 | import org.prx.playerhater.Song; 25 | 26 | import java.lang.ref.WeakReference; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | /** 31 | * A wrapper around the {@link PlayerHaterListener} interface allowing 32 | * {@linkplain PlayerHaterListener}s to be treated as plugins. 33 | *

34 | * All callbacks made to the underlying {@link PlayerHaterListener} will happen 35 | * in the same thread as this plugin's 36 | * {@link PlayerHaterPlugin#onPlayerHaterLoaded(Context, PlayerHater)} or 37 | * {@link PlayerHaterPlugin#onChangesComplete()}. In the default arrangement, 38 | * both of these callbacks are made on the UI thread. 39 | * 40 | * @author Chris Rhoden 41 | * @version 2.1.0 42 | * @since 2.1.0 43 | */ 44 | public class PlayerHaterListenerPlugin extends AbstractPlugin { 45 | 46 | private static final List> sInstances = new ArrayList>(); 47 | 48 | private static final int ADD = 1; 49 | private static final int REM = 2; 50 | private static final int TCK = 3; 51 | 52 | private synchronized static void addInstance( 53 | PlayerHaterListenerPlugin plugin) { 54 | WeakReference ref = new WeakReference( 55 | plugin); 56 | sHandler.obtainMessage(ADD, ref).sendToTarget(); 57 | } 58 | 59 | private static final Handler sHandler = new Handler() { 60 | 61 | @SuppressWarnings("unchecked") 62 | @Override 63 | public void handleMessage(Message msg) { 64 | switch (msg.what) { 65 | case ADD: 66 | sInstances 67 | .add((WeakReference) msg.obj); 68 | if (sInstances.size() == 1) { 69 | sendEmptyMessageDelayed(TCK, 500); 70 | } 71 | break; 72 | case REM: 73 | sInstances.remove(msg.obj); 74 | break; 75 | case TCK: 76 | for (WeakReference ref : sInstances) { 77 | if (ref.get() != null) { 78 | ref.get().onTick(); 79 | } else { 80 | obtainMessage(REM, ref).sendToTarget(); 81 | } 82 | } 83 | if (sInstances.size() >= 1) { 84 | sendEmptyMessageDelayed(TCK, 500); 85 | } 86 | } 87 | } 88 | 89 | }; 90 | 91 | private final PlayerHaterListener mListener; 92 | private final boolean mEcho; 93 | private Song mSong; 94 | 95 | /** 96 | * Instantiates a PlayerHaterListenerPlugin with echo enabled. 97 | * 98 | * @param listener The {@link PlayerHaterListener} that this plugin wraps. 99 | * @see {link PlayerHaterListenerPlugin} 100 | * @see {@link PlayerHaterListenerPlugin#PlayerHaterListenerPlugin(PlayerHaterListener, boolean)} 101 | */ 102 | public PlayerHaterListenerPlugin(PlayerHaterListener listener) { 103 | this(listener, true); 104 | } 105 | 106 | /** 107 | * Instantiates a PlayerHaterListenerPlugin with optional echo. 108 | * 109 | * @param listener {@link PlayerHaterListener} That this plugin wraps. 110 | * @param echo A flag to determine whether the most recent callback that 111 | * would have been called should be called as soon as this plugin 112 | * is attached. 113 | * @see {@link PlayerHaterListenerPlugin} 114 | * @see {@link PlayerHaterListenerPlugin#PlayerHaterListenerPlugin(PlayerHaterListener)} 115 | */ 116 | public PlayerHaterListenerPlugin(PlayerHaterListener listener, boolean echo) { 117 | mListener = listener; 118 | mEcho = echo; 119 | addInstance(this); 120 | } 121 | 122 | @Override 123 | public void onPlayerHaterLoaded(Context context, PlayerHater playerHater) { 124 | super.onPlayerHaterLoaded(context, playerHater); 125 | mSong = playerHater.nowPlaying(); 126 | if (mEcho) { 127 | onChangesComplete(); 128 | } 129 | } 130 | 131 | @Override 132 | public void onSongChanged(Song song) { 133 | mSong = song; 134 | } 135 | 136 | @Override 137 | public void onChangesComplete() { 138 | switch (getPlayerHater().getState()) { 139 | case PlayerHater.STATE_PLAYING: 140 | if (mSong != null) { 141 | mListener.onPlaying(mSong, getPlayerHater() 142 | .getCurrentPosition()); 143 | } 144 | break; 145 | case PlayerHater.STATE_STREAMING: 146 | if (mSong != null) { 147 | mListener.onStreaming(mSong); 148 | } 149 | break; 150 | case PlayerHater.STATE_LOADING: 151 | mListener.onLoading(mSong); 152 | break; 153 | default: 154 | if (mSong != null) { 155 | mListener.onPaused(mSong); 156 | } else { 157 | mListener.onStopped(); 158 | } 159 | } 160 | } 161 | 162 | private void onTick() { 163 | try { 164 | if (getPlayerHater().getState() == PlayerHater.STATE_PLAYING) { 165 | mListener.onPlaying(getPlayerHater().nowPlaying(), getPlayerHater() 166 | .getCurrentPosition()); 167 | } else if (getPlayerHater().getState() == PlayerHater.STATE_STREAMING) { 168 | mListener.onStreaming(getPlayerHater().nowPlaying()); 169 | } 170 | } catch (java.lang.IllegalStateException e) { 171 | // do nothing 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/util/Config.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.util; 17 | 18 | import java.util.Collections; 19 | import java.util.HashSet; 20 | import java.util.Set; 21 | 22 | import org.prx.playerhater.PlayerHater; 23 | import org.prx.playerhater.PlayerHaterPlugin; 24 | import org.prx.playerhater.R; 25 | import org.prx.playerhater.plugins.PluginCollection; 26 | 27 | import android.content.Context; 28 | import android.content.Intent; 29 | import android.content.pm.PackageManager; 30 | import android.content.pm.ServiceInfo; 31 | import android.content.pm.PackageManager.NameNotFoundException; 32 | import android.content.res.Resources; 33 | import android.content.res.XmlResourceParser; 34 | import android.content.res.Resources.NotFoundException; 35 | import android.os.Parcel; 36 | import android.os.Parcelable; 37 | 38 | public class Config implements Parcelable { 39 | private static final String EXTRA_CONFIG = "config"; 40 | private static Config sInstance; 41 | 42 | public static void attachToIntent(Intent intent) { 43 | if (sInstance != null) { 44 | intent.putExtra(EXTRA_CONFIG, sInstance); 45 | } 46 | } 47 | 48 | public static Config fromIntent(Intent intent) { 49 | return intent.getExtras().getParcelable(Config.EXTRA_CONFIG); 50 | } 51 | 52 | public static Config getInstance(Context context) { 53 | if (sInstance == null) { 54 | sInstance = new Config(context); 55 | } 56 | return sInstance; 57 | } 58 | 59 | public PluginCollection run(Context context, PlayerHater playerHater) { 60 | PluginCollection collection = new PluginCollection(); 61 | return run(context, playerHater, collection); 62 | } 63 | 64 | public PluginCollection run(Context context, PlayerHater playerHater, 65 | PluginCollection collection) { 66 | collection.writeLock(); 67 | for (Class pluginKlass : getPlugins()) { 68 | try { 69 | PlayerHaterPlugin plugin = pluginKlass.newInstance(); 70 | collection.add(plugin); 71 | } catch (Exception e) { 72 | Log.e("Could not instantiate plugin " 73 | + pluginKlass.getCanonicalName(), e); 74 | } 75 | } 76 | collection.onPlayerHaterLoaded(context, playerHater); 77 | collection.unWriteLock(); 78 | return collection; 79 | } 80 | 81 | private final Set mPlugins = new HashSet(); 82 | 83 | private Config(Context context) { 84 | XmlResourceParser parser = context.getResources().getXml( 85 | R.xml.zzz_ph_config_defaults); 86 | load(parser, context); 87 | try { 88 | ServiceInfo info = context.getPackageManager().getServiceInfo( 89 | PlayerHater.buildServiceIntent(context).getComponent(), 90 | PackageManager.GET_META_DATA); 91 | if (info != null && info.metaData != null) { 92 | int id = info.metaData.getInt("org.prx.playerhater.Config", 0); 93 | if (id != 0) { 94 | parser = context.getResources().getXml(id); 95 | load(parser, context); 96 | } 97 | } 98 | } catch (NameNotFoundException e) { 99 | // If this happens, we can just use the default configuration. 100 | } 101 | } 102 | 103 | private Set> getPlugins() { 104 | return getPlugins(mPlugins); 105 | } 106 | 107 | @SuppressWarnings("unchecked") 108 | private Set> getPlugins( 109 | Set strings) { 110 | Set> plugins = new HashSet>(); 111 | for (String pluginName : strings) { 112 | try { 113 | plugins.add((Class) Class 114 | .forName(pluginName)); 115 | } catch (Exception e) { 116 | Log.e("Can't load plugin " + pluginName, e); 117 | } 118 | } 119 | return plugins; 120 | } 121 | 122 | @Override 123 | public int describeContents() { 124 | return 0; 125 | } 126 | 127 | @Override 128 | public void writeToParcel(Parcel dest, int flags) { 129 | dest.writeStringArray(getPluginsArray()); 130 | } 131 | 132 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 133 | 134 | @Override 135 | public Config createFromParcel(Parcel in) { 136 | return new Config(in); 137 | } 138 | 139 | @Override 140 | public Config[] newArray(int size) { 141 | return new Config[size]; 142 | } 143 | }; 144 | 145 | private static final int PLUGIN = 1; 146 | private static final int INVALID_TAG = -1; 147 | 148 | private Config(Parcel in) { 149 | setPluginsArray(in.createStringArray()); 150 | } 151 | 152 | private String[] getPluginsArray() { 153 | return mPlugins.toArray(new String[mPlugins.size() - 1]); 154 | } 155 | 156 | private void setPluginsArray(String[] plugins) { 157 | setStringArray(plugins, mPlugins); 158 | } 159 | 160 | private void setStringArray(String[] stuff, Set in) { 161 | in.clear(); 162 | Collections.addAll(in, stuff); 163 | } 164 | 165 | private void load(XmlResourceParser parser, Context context) { 166 | Resources res = context.getResources(); 167 | try { 168 | parser.next(); 169 | int eventType = parser.getEventType(); 170 | boolean pluginEnabled = false; 171 | boolean pluginDisabled = false; 172 | String pluginName = null; 173 | int currentTagType = INVALID_TAG; 174 | 175 | while (eventType != XmlResourceParser.END_DOCUMENT) { 176 | if (eventType == XmlResourceParser.START_TAG) { 177 | if (parser.getName().equals("plugin")) { 178 | currentTagType = PLUGIN; 179 | } 180 | pluginEnabled = loadBooleanOrResourceBoolean(res, parser, 181 | "enabled", true); 182 | pluginDisabled = loadBooleanOrResourceBoolean(res, parser, 183 | "disabled", false); 184 | 185 | pluginName = parser.getAttributeValue(null, "name"); 186 | } else if (eventType == XmlResourceParser.END_TAG) { 187 | switch (currentTagType) { 188 | case PLUGIN: 189 | if (pluginEnabled && pluginName != null) { 190 | mPlugins.add(pluginName); 191 | } else if (pluginDisabled && pluginName != null) { 192 | mPlugins.remove(pluginName); 193 | } 194 | break; 195 | } 196 | } 197 | eventType = parser.next(); 198 | } 199 | } catch (Exception e) { 200 | 201 | } 202 | } 203 | 204 | private boolean loadBooleanOrResourceBoolean(Resources res, 205 | XmlResourceParser parser, String attrName, boolean def) { 206 | int id; 207 | boolean result = def; 208 | try { 209 | id = parser.getAttributeResourceValue(null, attrName, 0); 210 | if (id != 0) { 211 | try { 212 | result = res.getBoolean(id); 213 | } catch (NotFoundException e) { 214 | result = parser.getAttributeBooleanValue(null, attrName, 215 | def); 216 | } 217 | } else { 218 | result = parser.getAttributeBooleanValue(null, attrName, def); 219 | } 220 | } catch (Exception e) { 221 | return result; 222 | } 223 | return result; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/songs/SongHost.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.songs; 17 | 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | import org.prx.playerhater.Song; 22 | import org.prx.playerhater.ipc.IPlayerHaterClient; 23 | import org.prx.playerhater.ipc.IPlayerHaterServer; 24 | import android.net.Uri; 25 | import android.os.Bundle; 26 | import android.os.RemoteException; 27 | import android.util.SparseArray; 28 | 29 | public class SongHost { 30 | 31 | public static final int INVALID_TAG = -1; 32 | 33 | private static Remote sRemote; 34 | private static SparseArray sSongs; 35 | private static Map sTags; 36 | 37 | public static void setRemote(Remote remote) { 38 | sRemote = remote; 39 | } 40 | 41 | public static void setRemote(IPlayerHaterClient client) { 42 | sRemote = new ClientRemote(client); 43 | } 44 | 45 | public static void setRemote(IPlayerHaterServer server) { 46 | sRemote = new ServerRemote(server); 47 | } 48 | 49 | public static void slurp(int songTag, Bundle songData) { 50 | Song song = getSong(songTag); 51 | if (song instanceof RemoteSong) { 52 | ((RemoteSong) getSong(songTag)).setSong(Songs.fromBundle(songData)); 53 | } 54 | } 55 | 56 | public static SparseArray localSongs() { 57 | SparseArray data = new SparseArray(); 58 | for (Song song : getTags().keySet()) { 59 | if (!(song instanceof RemoteSong)) { 60 | data.put(getTag(song), Songs.toBundle(song)); 61 | } 62 | } 63 | return data; 64 | } 65 | 66 | static Remote remote() { 67 | if (sRemote == null) { 68 | return new NullRemote(); 69 | } 70 | return sRemote; 71 | } 72 | 73 | public static void clear() { 74 | sRemote = null; 75 | sSongs = null; 76 | sTags = null; 77 | } 78 | 79 | public static int getTag(Song song) { 80 | if (song == null) { 81 | return INVALID_TAG; 82 | } 83 | if (getTags().containsKey(song)) { 84 | return getTags().get(song); 85 | } else { 86 | int tag = song.hashCode(); 87 | getTags().put(song, tag); 88 | getSongs().put(tag, song); 89 | return tag; 90 | } 91 | } 92 | 93 | public static Song getSong(int tag) { 94 | if (tag == INVALID_TAG) { 95 | return null; 96 | } 97 | Song song = getSongs().get(tag); 98 | if (song != null) { 99 | return song; 100 | } else { 101 | song = new RemoteSong(tag); 102 | getTags().put(song, tag); 103 | getSongs().put(tag, song); 104 | return song; 105 | } 106 | } 107 | 108 | public static Song getSong(int tag, Bundle songData) { 109 | Song song = getSong(tag); 110 | if (song instanceof RemoteSong) { 111 | ((RemoteSong) song).setSong(Songs.fromBundle(songData)); 112 | } 113 | return song; 114 | } 115 | 116 | public static Song getLocalSong(int tag) { 117 | Song song = getSongs().get(tag); 118 | if (song instanceof RemoteSong) { 119 | song = ((RemoteSong) song).getSong(); 120 | } 121 | if (song == null) { 122 | throw new IllegalStateException("No locally stored song data available."); 123 | } 124 | return song; 125 | } 126 | 127 | private static SparseArray getSongs() { 128 | if (sSongs == null) { 129 | sSongs = new SparseArray(); 130 | } 131 | return sSongs; 132 | } 133 | 134 | private static Map getTags() { 135 | if (sTags == null) { 136 | sTags = new HashMap(); 137 | } 138 | return sTags; 139 | } 140 | 141 | static interface Remote { 142 | Uri getSongAlbumArt(int tag) throws RemoteException; 143 | 144 | Uri getSongUri(int tag) throws RemoteException; 145 | 146 | String getSongAlbumTitle(int tag) throws RemoteException; 147 | 148 | String getSongTitle(int tag) throws RemoteException; 149 | 150 | String getSongArtist(int tag) throws RemoteException; 151 | 152 | Bundle getSongExtra(int tag) throws RemoteException; 153 | } 154 | 155 | private static final class ClientRemote implements Remote { 156 | private final IPlayerHaterClient mClient; 157 | 158 | private ClientRemote(IPlayerHaterClient client) { 159 | mClient = client; 160 | } 161 | 162 | @Override 163 | public Uri getSongAlbumArt(int tag) throws RemoteException { 164 | return mClient.getSongAlbumArt(tag); 165 | } 166 | 167 | @Override 168 | public Uri getSongUri(int tag) throws RemoteException { 169 | return mClient.getSongUri(tag); 170 | } 171 | 172 | @Override 173 | public String getSongTitle(int tag) throws RemoteException { 174 | return mClient.getSongTitle(tag); 175 | } 176 | 177 | @Override 178 | public String getSongArtist(int tag) throws RemoteException { 179 | return mClient.getSongArtist(tag); 180 | } 181 | 182 | @Override 183 | public Bundle getSongExtra(int tag) throws RemoteException { 184 | return mClient.getSongExtra(tag); 185 | } 186 | 187 | @Override 188 | public String getSongAlbumTitle(int tag) throws RemoteException { 189 | return mClient.getSongAlbumTitle(tag); 190 | } 191 | } 192 | 193 | private static class ServerRemote implements Remote { 194 | private final IPlayerHaterServer mServer; 195 | 196 | private ServerRemote(IPlayerHaterServer server) { 197 | mServer = server; 198 | } 199 | 200 | @Override 201 | public Uri getSongAlbumArt(int tag) throws RemoteException { 202 | return mServer.getSongAlbumArt(tag); 203 | } 204 | 205 | @Override 206 | public Uri getSongUri(int tag) throws RemoteException { 207 | return mServer.getSongUri(tag); 208 | } 209 | 210 | @Override 211 | public String getSongTitle(int tag) throws RemoteException { 212 | return mServer.getSongTitle(tag); 213 | } 214 | 215 | @Override 216 | public String getSongArtist(int tag) throws RemoteException { 217 | return mServer.getSongArtist(tag); 218 | } 219 | 220 | @Override 221 | public Bundle getSongExtra(int tag) throws RemoteException { 222 | return mServer.getSongExtra(tag); 223 | } 224 | 225 | @Override 226 | public String getSongAlbumTitle(int tag) throws RemoteException { 227 | return mServer.getSongAlbumTitle(tag); 228 | } 229 | } 230 | 231 | private static class NullRemote implements Remote { 232 | 233 | @Override 234 | public Uri getSongAlbumArt(int tag) throws RemoteException { 235 | // TODO Auto-generated method stub 236 | return null; 237 | } 238 | 239 | @Override 240 | public Uri getSongUri(int tag) throws RemoteException { 241 | // TODO Auto-generated method stub 242 | return null; 243 | } 244 | 245 | @Override 246 | public String getSongAlbumTitle(int tag) throws RemoteException { 247 | // TODO Auto-generated method stub 248 | return null; 249 | } 250 | 251 | @Override 252 | public String getSongTitle(int tag) throws RemoteException { 253 | // TODO Auto-generated method stub 254 | return null; 255 | } 256 | 257 | @Override 258 | public String getSongArtist(int tag) throws RemoteException { 259 | // TODO Auto-generated method stub 260 | return null; 261 | } 262 | 263 | @Override 264 | public Bundle getSongExtra(int tag) throws RemoteException { 265 | // TODO Auto-generated method stub 266 | return null; 267 | } 268 | 269 | } 270 | } -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/plugins/LockScreenControlsPlugin.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013, 2014 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.plugins; 17 | 18 | import android.annotation.TargetApi; 19 | import android.app.PendingIntent; 20 | import android.content.Intent; 21 | import android.graphics.Bitmap; 22 | import android.graphics.BitmapFactory; 23 | import android.media.MediaMetadataRetriever; 24 | import android.media.RemoteControlClient; 25 | import android.net.Uri; 26 | import android.os.Build; 27 | 28 | import org.prx.playerhater.Song; 29 | 30 | import java.io.FileNotFoundException; 31 | import java.io.IOException; 32 | import java.io.InputStream; 33 | import java.net.MalformedURLException; 34 | import java.net.URL; 35 | 36 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 37 | public class LockScreenControlsPlugin extends AudioFocusPlugin { 38 | 39 | private RemoteControlClient mRemoteControlClient; 40 | private Bitmap mAlbumArt; 41 | private String mArtist; 42 | private String mTitle; 43 | 44 | private int mTransportControlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE 45 | | RemoteControlClient.FLAG_KEY_MEDIA_STOP 46 | | RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS 47 | | RemoteControlClient.FLAG_KEY_MEDIA_NEXT; 48 | 49 | @Override 50 | public void onAudioLoading() { 51 | super.onAudioLoading(); 52 | setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING); 53 | } 54 | 55 | @Override 56 | public void onAudioStarted() { 57 | super.onAudioStarted(); 58 | setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); 59 | } 60 | 61 | @Override 62 | public void onAudioPaused() { 63 | super.onAudioPaused(); 64 | setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); 65 | } 66 | 67 | @Override 68 | public void onDurationChanged(int duration) { 69 | getRemoteControlClient() 70 | .editMetadata(false) 71 | .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, duration) 72 | .apply(); 73 | } 74 | 75 | @Override 76 | public void onSongChanged(Song song) { 77 | if (song == null) { 78 | onAlbumArtChanged(null); 79 | onTitleChanged(null); 80 | onArtistChanged(null); 81 | return; 82 | } 83 | if (song.getAlbumArt() != null) { 84 | onAlbumArtChanged(song.getAlbumArt()); 85 | } 86 | 87 | onTitleChanged(song.getTitle()); 88 | onArtistChanged(song.getArtist()); 89 | } 90 | 91 | @Override 92 | public void onAudioStopped() { 93 | setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); 94 | super.onAudioStopped(); 95 | } 96 | 97 | @Override 98 | public void onTitleChanged(String title) { 99 | mTitle = title; 100 | } 101 | 102 | @Override 103 | public void onArtistChanged(String artist) { 104 | mArtist = artist; 105 | } 106 | 107 | @Override 108 | public void onAlbumArtChanged(Uri uri) { 109 | if (uri != null) { 110 | if (uri.getScheme().equals("android.resource") && uri.getLastPathSegment() != null) { 111 | mAlbumArt = BitmapFactory.decodeResource(getContext() 112 | .getResources(), Integer.valueOf(uri 113 | .getLastPathSegment())); 114 | } else if (uri.getScheme().equals("content")) { 115 | InputStream stream = null; 116 | try { 117 | stream = getContext().getContentResolver().openInputStream( 118 | uri); 119 | mAlbumArt = BitmapFactory.decodeStream(stream); 120 | } catch (FileNotFoundException e) { 121 | mAlbumArt = null; 122 | } finally { 123 | if (stream != null) { 124 | try { 125 | stream.close(); 126 | } catch (IOException e) { 127 | } 128 | } 129 | } 130 | 131 | } else { 132 | InputStream stream = null; 133 | try { 134 | stream = new URL(uri.toString()).openStream(); 135 | mAlbumArt = BitmapFactory.decodeStream(stream); 136 | } catch (MalformedURLException e) { 137 | mAlbumArt = null; 138 | } catch (IOException e) { 139 | mAlbumArt = null; 140 | } finally { 141 | if (stream != null) { 142 | try { 143 | stream.close(); 144 | } catch (IOException e) { 145 | } 146 | } 147 | } 148 | } 149 | } else { 150 | mAlbumArt = null; 151 | } 152 | getRemoteControlClient().editMetadata(false).putBitmap( 153 | RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, 154 | mAlbumArt); 155 | } 156 | 157 | @Override 158 | public void onTransportControlFlagsChanged(int transportControlFlags) { 159 | mTransportControlFlags = transportControlFlags; 160 | getRemoteControlClient() 161 | .setTransportControlFlags(transportControlFlags); 162 | } 163 | 164 | @Override 165 | public void onChangesComplete() { 166 | getRemoteControlClient().setTransportControlFlags( 167 | mTransportControlFlags); 168 | getRemoteControlClient() 169 | .editMetadata(false) 170 | .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, mTitle) 171 | .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, mArtist) 172 | .putBitmap( 173 | RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, 174 | mAlbumArt).apply(); 175 | } 176 | 177 | protected RemoteControlClient getRemoteControlClient() { 178 | if (mRemoteControlClient == null) { 179 | Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); 180 | mediaButtonIntent.setComponent(getEventReceiver()); 181 | PendingIntent pendingIntent = PendingIntent.getBroadcast( 182 | getContext(), 0, mediaButtonIntent, 183 | PendingIntent.FLAG_CANCEL_CURRENT); 184 | mRemoteControlClient = new RemoteControlClient(pendingIntent); 185 | } 186 | return mRemoteControlClient; 187 | } 188 | 189 | protected int getPlaybackState() { 190 | if (getPlayerHater().isLoading()) { 191 | return RemoteControlClient.PLAYSTATE_BUFFERING; 192 | } else if (getPlayerHater().isPlaying()) { 193 | return RemoteControlClient.PLAYSTATE_PLAYING; 194 | } 195 | return RemoteControlClient.PLAYSTATE_PAUSED; 196 | } 197 | 198 | protected void setPlaybackState(int playbackState) { 199 | getRemoteControlClient().setPlaybackState(playbackState); 200 | getAudioManager().registerRemoteControlClient(getRemoteControlClient()); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PlayerHater 2 | 3 | PlayerHater is an Android library for playing audio in the background of your app. You can use it for: 4 | 5 | * Podcasts! 6 | * Music! 7 | * Radio! 8 | 9 | You probably shouldn't use it for: 10 | 11 | * Fart sounds 12 | * Game sound effects 13 | 14 | There's a whole lot that PlayerHater does for you right out of the box and you can add your own functionality through the use of plugins. PlayerHater even ships with plugins that will do the following automatically: 15 | 16 | * Show a regular notification on on Android 2.3 or lower, a notification with player controls on Android 3.0+, and an expandable notification on Android 4.2+! 17 | * Handle audio focus changes in all of the weird and different ways for pre-2.2 and post-2.2 versions! 18 | * Add lock screen controls on Android 4.0+! 19 | * Pebble support! 20 | 21 | All of this is configurable if you like, and you can write your own plugins to run alongside these. 22 | 23 | But the best thing about PlayerHater is that there are no ServiceConnection callbacks, MediaPlayer state diagrams, or setup and teardown code for you to write. It's all taken care of in the library. 24 | 25 | ## Quick Start 26 | 27 | ### Important, temporary information 28 | 29 | Everything's going into Maven Central soon using the new aar format 30 | that's part of the new Android build system that uses Gradle. If 31 | you're not comfortable using the sonatpe OSS snapshot server (or if 32 | you don't know what that is) or you haven't moved to Gradle yet, you 33 | can check out the project repository and use the files in 34 | 35 | legacyLibraryProject/PlayerHater 36 | 37 | as if they were a normal android library project. They are, in fact 38 | symlinks. 39 | 40 | Note that doing this will require you to add entries to your 41 | AndroidManifest.xml file, while the new build system allows us to 42 | automatically do this for you if you're using Gradle. 43 | 44 | Once you've imported the project into your android application and added the `Service` and `BroadcastReceiver` to your `AndroidManifest.xml` file, getting a handle on the player is as easy as: 45 | 46 | ```java 47 | import org.prx.android.playerhater.PlayerHater; 48 | 49 | class MyApplicationActivity extends Activity { 50 | 51 | private PlayerHater mPlayerHater; 52 | 53 | @Override 54 | public void onResume(Bundle savedInstanceState) { 55 | super.onResume(savedInstanceState); 56 | mPlayerHater = PlayerHater.bind(this); 57 | } 58 | 59 | @Override 60 | public void onPause() { 61 | mPlayerHater.release(); 62 | } 63 | 64 | } 65 | ``` 66 | 67 | When you've bound to a `PlayerHater`, you're ready to start playing stuff - music, podcasts, or whatever new thing the kids are listening to these days. 68 | 69 | ### The PlayerHater API 70 | 71 | abstract void emptyQueue() 72 | Removes all songs from the queue. 73 | abstract int enqueue(Song song) 74 | Puts a song on the end of the play queue. 75 | abstract int getCurrentPosition() 76 | Gets the location of the playhead in milliseconds. 77 | abstract int getDuration() 78 | Gets the duration of the currently loaded Song in milliseconds. 79 | abstract int getQueueLength() 80 | Returns the number of items in the queue. 81 | abstract int getQueuePosition() 82 | Returns the number of clips in the queue which are at least partially behind the playhead. 83 | abstract int getState() 84 | Gets the state of the PlayerHater, represented as an int. 85 | abstract boolean isLoading() 86 | Checks to see if the player is currently loading audio. 87 | abstract boolean isPlaying() 88 | Checks to see if the player is currently playing back audio. 89 | abstract Song nowPlaying() 90 | Gets the Song representation of the track that is currently loaded in the player. 91 | abstract boolean pause() 92 | Pauses the player. 93 | abstract boolean play(Song song, int startTime) 94 | Begins playback of song at startTime 95 | abstract boolean play(Song song) 96 | Begins playback of a song at the beginning. 97 | abstract boolean play(int startTime) 98 | Begins playback of the currently loaded Song at startTime in the track. 99 | abstract boolean play() 100 | Begins playback of the currently loaded Song. 101 | abstract boolean removeFromQueue(int position) 102 | Removes the element at position from the play queue. 103 | abstract boolean seekTo(int startTime) 104 | Moves the playhead to startTime 105 | abstract void setPendingIntent(PendingIntent intent) 106 | Sets the intent to be used by the plugins. 107 | abstract void setTransportControlFlags(int transportControlFlags) 108 | Sets the visible buttons for plugins. 109 | abstract void skip() 110 | Moves to the next song in the play queue. 111 | abstract void skipBack() 112 | Moves back in the play queue. 113 | abstract boolean skipTo(int position) 114 | Moves to a new position in the play queue. 115 | abstract boolean stop() 116 | Stops the player. 117 | 118 | ## Changelog 119 | 120 | ### v0.3.0 121 | 122 | We refactored everything to make more sense and run faster. Large parts of the codebase have been written from scratch using knowledge we gained in versions 0.1.0 and 0.2.0 and many class and method names have been changed to make more sense. 123 | 124 | 0.3.0 is a breaking upgrade from 0.2.0 and is still considered beta software until a 1.0.0 release. 125 | 126 | #### Highlights 127 | 128 | * The Service is now aggressively bound whenever you are interacting with PlayerHater. It's still released when appropriate so that Android can GC the process automatically and it will not show up in the Android Running Services list. 129 | 130 | * getState() will now return one of `PlayerHater.IDLE`, `PlayerHater.PLAYING`, `PlayerHater.LOADING`, or `PlayerHater.PAUSED` instead of one of the lower-level MediaPlayer states. 131 | 132 | * `public Bundle getExtra()` and `public String getAlbumTitle()` have been added to the `Song` interface. `void onAlbumTitleChanged(String albumTitle)` has been added to the `PlayerHaterPlugin` interface. 133 | 134 | * `void onServiceStarted(IPlayerHaterBinder binder)` has been removed from the `PlayerHaterPlugin` interface. If you need access to Service-only methods, check to see if the `PlayerHater` passed to `onPlayerHaterLoaded()` is a `ServicePlayerHater`, cast, and then call the methods needed. 135 | 136 | * Song data is loaded just in time, even across the IPC boundary. This means that if you enqueue a `Song` and then the return values for the various Song methods change, those changes will be propagated. Please note that if your Activity-side application is terminated the Song data will be serialized at that moment and passed across the barier to permit garbage collection. 137 | 138 | ### v0.2.0 139 | 140 | * No service callbacks to deal with - Service is automatically started as needed when `play()` is called. You can immediately work with PlayerHater and all commands sent to it will be forwarded to the Service once it is started. 141 | 142 | * It is no longer the developer's responsibility to remember when to and when not to stop the service. 143 | 144 | * The service is now runnable on a separate Android process through the use of AIDL IPC. 145 | 146 | * Too many changes to enumerate. Refer to documentation. 147 | 148 | 0.2.0 is a breaking upgrade from 0.1.0 and is still considered beta software until a 1.0.0 release. 149 | 150 | License 151 | ------- 152 | 153 | Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 154 | 155 | Licensed under the Apache License, Version 2.0 (the "License"); 156 | you may not use this file except in compliance with the License. 157 | You may obtain a copy of the License at 158 | 159 | http://www.apache.org/licenses/LICENSE-2.0 160 | 161 | Unless required by applicable law or agreed to in writing, software 162 | distributed under the License is distributed on an "AS IS" BASIS, 163 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 164 | See the License for the specific language governing permissions and 165 | limitations under the License. 166 | -------------------------------------------------------------------------------- /src/main/java/org/prx/playerhater/songs/SongQueue.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange 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 org.prx.playerhater.songs; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | import org.prx.playerhater.Song; 22 | 23 | import android.os.Handler; 24 | import android.os.HandlerThread; 25 | import android.os.Message; 26 | 27 | public class SongQueue { 28 | 29 | private static Handler sHandler; 30 | private static final int CURRENT_SONG = 1; 31 | private static final int NEXT_SONG = 2; 32 | 33 | private static Handler getHandler() { 34 | if (sHandler == null) { 35 | HandlerThread thread = new HandlerThread("SongQueue"); 36 | thread.start(); 37 | sHandler = new Handler(thread.getLooper()) { 38 | 39 | @Override 40 | public void handleMessage(Message msg) { 41 | SongMessage m = (SongMessage) msg.obj; 42 | switch (msg.what) { 43 | case CURRENT_SONG: 44 | m.queue.sendSongChanged(m.song, m.oldSong); 45 | break; 46 | case NEXT_SONG: 47 | m.queue.sendNextSongChanged(m.song, m.oldSong); 48 | } 49 | } 50 | 51 | }; 52 | } 53 | return sHandler; 54 | } 55 | 56 | private static class SongMessage { 57 | private final SongQueue queue; 58 | private final Song song; 59 | private final Song oldSong; 60 | 61 | public SongMessage(SongQueue songQueue, Song newSong, Song lastSong) { 62 | queue = songQueue; 63 | song = newSong; 64 | oldSong = lastSong; 65 | } 66 | } 67 | 68 | public interface OnQueuedSongsChangedListener { 69 | public void onNowPlayingChanged(Song nowPlaying, Song nowPlayingWas); 70 | 71 | public void onNextSongChanged(Song nextSong, Song nextSongWas); 72 | } 73 | 74 | private int mPlayheadPosition = -1; 75 | private final List mSongs = new ArrayList(); 76 | 77 | private Song mNextSongWas = null; 78 | private Song mCurrentSongWas = null; 79 | private OnQueuedSongsChangedListener mListener; 80 | 81 | public synchronized void setQueuedSongsChangedListener( 82 | OnQueuedSongsChangedListener listener) { 83 | mListener = listener; 84 | } 85 | 86 | public synchronized int appendSong(Song song) { 87 | return addSongAtPosition(song, mSongs.size() + 1); 88 | } 89 | 90 | public synchronized int addSongAtPosition(Song song, int position) { 91 | if (position < 0) { 92 | throw new IllegalArgumentException("Illegal position: " + position); 93 | } else if (position == 0) { 94 | position = 1; 95 | } 96 | 97 | if (position <= getPlayheadPosition()) { 98 | setPlayheadPosition(getPlayheadPosition() + 1); 99 | } 100 | 101 | mSongs.add(position - 1, song); 102 | songOrderChanged(); 103 | return mSongs.size() - getPosition(); 104 | } 105 | 106 | public synchronized Song next() { 107 | setPlayheadPosition(getPlayheadPosition() + 1); 108 | if (getPlayheadPosition() > size()) { 109 | setPlayheadPosition(1); 110 | } 111 | songOrderChanged(); 112 | return getNowPlaying(); 113 | } 114 | 115 | public synchronized Song back() { 116 | setPlayheadPosition(getPlayheadPosition() - 1); 117 | if (getPlayheadPosition() <= 0) { 118 | setPlayheadPosition(1); 119 | } 120 | songOrderChanged(); 121 | return getNowPlaying(); 122 | } 123 | 124 | public synchronized void skipToEnd() { 125 | setPlayheadPosition(mSongs.size()); 126 | songOrderChanged(); 127 | } 128 | 129 | public synchronized boolean isAtLastSong() { 130 | return getPlayheadPosition() == mSongs.size(); 131 | } 132 | 133 | public synchronized Song getNowPlaying() { 134 | if (getPlayheadPosition() <= 0) { 135 | return null; 136 | } 137 | return mSongs.get(getPlayheadPosition() - 1); 138 | } 139 | 140 | public synchronized Song getNextPlaying() { 141 | return getNextSong(); 142 | } 143 | 144 | public synchronized void empty() { 145 | mSongs.clear(); 146 | setPlayheadPosition(-1); 147 | songOrderChanged(); 148 | } 149 | 150 | private void songOrderChanged() { 151 | songOrderChanged(true); 152 | } 153 | 154 | private void songOrderChanged(boolean notify) { 155 | songOrderChanged(notify, notify); 156 | } 157 | 158 | private void songOrderChanged(boolean notifyCurrent, boolean notifyNext) { 159 | if (mSongs.size() > 0) { 160 | if (getPlayheadPosition() == -1) { 161 | setPlayheadPosition(1); 162 | } 163 | 164 | if (mCurrentSongWas == null || mCurrentSongWas != getNowPlaying()) { 165 | currentSongChanged(notifyCurrent); 166 | } 167 | if (mSongs.size() > 1) { 168 | if (mNextSongWas == null || mNextSongWas != getNextSong()) { 169 | nextSongChanged(notifyNext); 170 | } 171 | } else if (mNextSongWas != null) { 172 | nextSongChanged(notifyNext); 173 | } 174 | } else { 175 | if (mCurrentSongWas != null) { 176 | currentSongChanged(notifyCurrent); 177 | } 178 | if (mNextSongWas != null) { 179 | nextSongChanged(notifyNext); 180 | } 181 | } 182 | } 183 | 184 | private void currentSongChanged(boolean notify) { 185 | if (notify && mListener != null) { 186 | getHandler().obtainMessage(CURRENT_SONG, 187 | new SongMessage(this, getNowPlaying(), mCurrentSongWas)) 188 | .sendToTarget(); 189 | } 190 | mCurrentSongWas = getNowPlaying(); 191 | } 192 | 193 | private void sendSongChanged(Song newSong, Song oldSong) { 194 | mListener.onNowPlayingChanged(newSong, oldSong); 195 | } 196 | 197 | private void nextSongChanged(boolean notify) { 198 | if (notify && mListener != null) { 199 | getHandler().obtainMessage(NEXT_SONG, 200 | new SongMessage(this, getNextSong(), mNextSongWas)) 201 | .sendToTarget(); 202 | } 203 | mNextSongWas = getNextSong(); 204 | } 205 | 206 | private void sendNextSongChanged(Song newNextSong, Song oldNextSong) { 207 | mListener.onNextSongChanged(newNextSong, oldNextSong); 208 | } 209 | 210 | private Song getNextSong() { 211 | if (getPlayheadPosition() >= mSongs.size() 212 | || getPlayheadPosition() <= 0) { 213 | return null; 214 | } else { 215 | return mSongs.get(getPlayheadPosition()); 216 | } 217 | } 218 | 219 | public synchronized boolean skipTo(int position) { 220 | if (position <= mSongs.size()) { 221 | if (position < 0) { 222 | return skipTo(mSongs.size() + position + 1); 223 | } 224 | setPlayheadPosition(position); 225 | songOrderChanged(); 226 | return true; 227 | } 228 | return false; 229 | } 230 | 231 | public synchronized int size() { 232 | return mSongs.size(); 233 | } 234 | 235 | public synchronized boolean remove(int position) { 236 | if (position < 1 || position > mSongs.size()) { 237 | return false; 238 | } else { 239 | mSongs.remove(position - 1); 240 | if (position < getPlayheadPosition()) { 241 | setPlayheadPosition(getPlayheadPosition() - 1); 242 | } 243 | if (getPlayheadPosition() >= mSongs.size()) { 244 | setPlayheadPosition(mSongs.size() >= 1 ? -1 : 1); 245 | } 246 | songOrderChanged(); 247 | return true; 248 | } 249 | } 250 | 251 | private int getPlayheadPosition() { 252 | return mPlayheadPosition; 253 | } 254 | 255 | private void setPlayheadPosition(int playheadPosition) { 256 | mPlayheadPosition = playheadPosition; 257 | } 258 | 259 | // XXX Should do some more here. 260 | public int getPosition() { 261 | if (getPlayheadPosition() < 0) { 262 | return 0; 263 | } else { 264 | return getPlayheadPosition(); 265 | } 266 | } 267 | 268 | public void appendAndSkip(Song song) { 269 | mSongs.add(mSongs.size(), song); 270 | setPlayheadPosition(mSongs.size()); 271 | songOrderChanged(false, true); 272 | } 273 | } 274 | --------------------------------------------------------------------------------