diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c88729 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.classpath +.project +bin +gen diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..7344eb6 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/assets/.gitkeep b/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/gen/.gitkeep b/gen/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/libs/.gitkeep b/libs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/proguard-project.txt b/proguard-project.txt new file mode 100644 index 0000000..f2fe155 --- /dev/null +++ b/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/project.properties b/project.properties new file mode 100644 index 0000000..484dab0 --- /dev/null +++ b/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 +android.library=true diff --git a/res/drawable-hdpi-v11/zzz_ph_ic_notification.png b/res/drawable-hdpi-v11/zzz_ph_ic_notification.png new file mode 100755 index 0000000..15dfa8b Binary files /dev/null and b/res/drawable-hdpi-v11/zzz_ph_ic_notification.png differ diff --git a/res/drawable-hdpi-v9/zzz_ph_ic_notification.png b/res/drawable-hdpi-v9/zzz_ph_ic_notification.png new file mode 100755 index 0000000..aa92ccd Binary files /dev/null and b/res/drawable-hdpi-v9/zzz_ph_ic_notification.png differ diff --git a/res/drawable-hdpi/ic_launcher.png b/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..96a442e Binary files /dev/null and b/res/drawable-hdpi/ic_launcher.png differ diff --git a/res/drawable-hdpi/zzz_ph_bt_back_disabled.png b/res/drawable-hdpi/zzz_ph_bt_back_disabled.png new file mode 100644 index 0000000..b24e6e8 Binary files /dev/null and b/res/drawable-hdpi/zzz_ph_bt_back_disabled.png differ diff --git a/res/drawable-hdpi/zzz_ph_bt_back_enabled.png b/res/drawable-hdpi/zzz_ph_bt_back_enabled.png new file mode 100644 index 0000000..74397e2 Binary files /dev/null and b/res/drawable-hdpi/zzz_ph_bt_back_enabled.png differ diff --git a/res/drawable-hdpi/zzz_ph_bt_pause_disabled.png b/res/drawable-hdpi/zzz_ph_bt_pause_disabled.png new file mode 100644 index 0000000..0f7c1f4 Binary files /dev/null and b/res/drawable-hdpi/zzz_ph_bt_pause_disabled.png differ diff --git a/res/drawable-hdpi/zzz_ph_bt_pause_enabled.png b/res/drawable-hdpi/zzz_ph_bt_pause_enabled.png new file mode 100644 index 0000000..4f5eee2 Binary files /dev/null and b/res/drawable-hdpi/zzz_ph_bt_pause_enabled.png differ diff --git a/res/drawable-hdpi/zzz_ph_bt_play_disabled.png b/res/drawable-hdpi/zzz_ph_bt_play_disabled.png new file mode 100644 index 0000000..ae99245 Binary files /dev/null and b/res/drawable-hdpi/zzz_ph_bt_play_disabled.png differ diff --git a/res/drawable-hdpi/zzz_ph_bt_play_enabled.png b/res/drawable-hdpi/zzz_ph_bt_play_enabled.png new file mode 100644 index 0000000..3d28fcd Binary files /dev/null and b/res/drawable-hdpi/zzz_ph_bt_play_enabled.png differ diff --git a/res/drawable-hdpi/zzz_ph_bt_skip_disabled.png b/res/drawable-hdpi/zzz_ph_bt_skip_disabled.png new file mode 100644 index 0000000..3d922c0 Binary files /dev/null and b/res/drawable-hdpi/zzz_ph_bt_skip_disabled.png differ diff --git a/res/drawable-hdpi/zzz_ph_bt_skip_enabled.png b/res/drawable-hdpi/zzz_ph_bt_skip_enabled.png new file mode 100755 index 0000000..738aae1 Binary files /dev/null and b/res/drawable-hdpi/zzz_ph_bt_skip_enabled.png differ diff --git a/res/drawable-hdpi/zzz_ph_bt_stop.png b/res/drawable-hdpi/zzz_ph_bt_stop.png new file mode 100644 index 0000000..ea75fe7 Binary files /dev/null and b/res/drawable-hdpi/zzz_ph_bt_stop.png differ diff --git a/res/drawable-hdpi/zzz_ph_ic_notification.png b/res/drawable-hdpi/zzz_ph_ic_notification.png new file mode 100755 index 0000000..7dc90bc Binary files /dev/null and b/res/drawable-hdpi/zzz_ph_ic_notification.png differ diff --git a/res/drawable-ldpi-v11/zzz_ph_ic_notification.png b/res/drawable-ldpi-v11/zzz_ph_ic_notification.png new file mode 100755 index 0000000..faaa57c Binary files /dev/null and b/res/drawable-ldpi-v11/zzz_ph_ic_notification.png differ diff --git a/res/drawable-ldpi-v9/zzz_ph_ic_notification.png b/res/drawable-ldpi-v9/zzz_ph_ic_notification.png new file mode 100755 index 0000000..97e812b Binary files /dev/null and b/res/drawable-ldpi-v9/zzz_ph_ic_notification.png differ diff --git a/res/drawable-mdpi-v11/zzz_ph_ic_notification.png b/res/drawable-mdpi-v11/zzz_ph_ic_notification.png new file mode 100755 index 0000000..754dab1 Binary files /dev/null and b/res/drawable-mdpi-v11/zzz_ph_ic_notification.png differ diff --git a/res/drawable-mdpi-v9/zzz_ph_ic_notification.png b/res/drawable-mdpi-v9/zzz_ph_ic_notification.png new file mode 100755 index 0000000..94d43f4 Binary files /dev/null and b/res/drawable-mdpi-v9/zzz_ph_ic_notification.png differ diff --git a/res/drawable-mdpi/ic_launcher.png b/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..359047d Binary files /dev/null and b/res/drawable-mdpi/ic_launcher.png differ diff --git a/res/drawable-mdpi/zzz_ph_bt_back_disabled.png b/res/drawable-mdpi/zzz_ph_bt_back_disabled.png new file mode 100644 index 0000000..f9dcbd6 Binary files /dev/null and b/res/drawable-mdpi/zzz_ph_bt_back_disabled.png differ diff --git a/res/drawable-mdpi/zzz_ph_bt_back_enabled.png b/res/drawable-mdpi/zzz_ph_bt_back_enabled.png new file mode 100644 index 0000000..22a5b50 Binary files /dev/null and b/res/drawable-mdpi/zzz_ph_bt_back_enabled.png differ diff --git a/res/drawable-mdpi/zzz_ph_bt_pause_disabled.png b/res/drawable-mdpi/zzz_ph_bt_pause_disabled.png new file mode 100644 index 0000000..07bc3a9 Binary files /dev/null and b/res/drawable-mdpi/zzz_ph_bt_pause_disabled.png differ diff --git a/res/drawable-mdpi/zzz_ph_bt_pause_enabled.png b/res/drawable-mdpi/zzz_ph_bt_pause_enabled.png new file mode 100755 index 0000000..a5aee6f Binary files /dev/null and b/res/drawable-mdpi/zzz_ph_bt_pause_enabled.png differ diff --git a/res/drawable-mdpi/zzz_ph_bt_play_disabled.png b/res/drawable-mdpi/zzz_ph_bt_play_disabled.png new file mode 100644 index 0000000..da2b488 Binary files /dev/null and b/res/drawable-mdpi/zzz_ph_bt_play_disabled.png differ diff --git a/res/drawable-mdpi/zzz_ph_bt_play_enabled.png b/res/drawable-mdpi/zzz_ph_bt_play_enabled.png new file mode 100755 index 0000000..6a40cd5 Binary files /dev/null and b/res/drawable-mdpi/zzz_ph_bt_play_enabled.png differ diff --git a/res/drawable-mdpi/zzz_ph_bt_skip_disabled.png b/res/drawable-mdpi/zzz_ph_bt_skip_disabled.png new file mode 100644 index 0000000..1e0bad4 Binary files /dev/null and b/res/drawable-mdpi/zzz_ph_bt_skip_disabled.png differ diff --git a/res/drawable-mdpi/zzz_ph_bt_skip_enabled.png b/res/drawable-mdpi/zzz_ph_bt_skip_enabled.png new file mode 100755 index 0000000..28e8137 Binary files /dev/null and b/res/drawable-mdpi/zzz_ph_bt_skip_enabled.png differ diff --git a/res/drawable-mdpi/zzz_ph_ic_notification.png b/res/drawable-mdpi/zzz_ph_ic_notification.png new file mode 100755 index 0000000..d9e8099 Binary files /dev/null and b/res/drawable-mdpi/zzz_ph_ic_notification.png differ diff --git a/res/drawable-xhdpi-v11/zzz_ph_ic_notification.png b/res/drawable-xhdpi-v11/zzz_ph_ic_notification.png new file mode 100755 index 0000000..59ccc2d Binary files /dev/null and b/res/drawable-xhdpi-v11/zzz_ph_ic_notification.png differ diff --git a/res/drawable-xhdpi-v9/zzz_ph_ic_notification.png b/res/drawable-xhdpi-v9/zzz_ph_ic_notification.png new file mode 100755 index 0000000..e236d3e Binary files /dev/null and b/res/drawable-xhdpi-v9/zzz_ph_ic_notification.png differ diff --git a/res/drawable-xhdpi/ic_launcher.png b/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..71c6d76 Binary files /dev/null and b/res/drawable-xhdpi/ic_launcher.png differ diff --git a/res/drawable-xhdpi/zzz_ph_bt_back_disabled.png b/res/drawable-xhdpi/zzz_ph_bt_back_disabled.png new file mode 100644 index 0000000..d9b3598 Binary files /dev/null and b/res/drawable-xhdpi/zzz_ph_bt_back_disabled.png differ diff --git a/res/drawable-xhdpi/zzz_ph_bt_back_enabled.png b/res/drawable-xhdpi/zzz_ph_bt_back_enabled.png new file mode 100644 index 0000000..d0d894a Binary files /dev/null and b/res/drawable-xhdpi/zzz_ph_bt_back_enabled.png differ diff --git a/res/drawable-xhdpi/zzz_ph_bt_pause_disabled.png b/res/drawable-xhdpi/zzz_ph_bt_pause_disabled.png new file mode 100644 index 0000000..1484b5e Binary files /dev/null and b/res/drawable-xhdpi/zzz_ph_bt_pause_disabled.png differ diff --git a/res/drawable-xhdpi/zzz_ph_bt_pause_enabled.png b/res/drawable-xhdpi/zzz_ph_bt_pause_enabled.png new file mode 100755 index 0000000..333c1b2 Binary files /dev/null and b/res/drawable-xhdpi/zzz_ph_bt_pause_enabled.png differ diff --git a/res/drawable-xhdpi/zzz_ph_bt_play_disabled.png b/res/drawable-xhdpi/zzz_ph_bt_play_disabled.png new file mode 100644 index 0000000..18ff47b Binary files /dev/null and b/res/drawable-xhdpi/zzz_ph_bt_play_disabled.png differ diff --git a/res/drawable-xhdpi/zzz_ph_bt_play_enabled.png b/res/drawable-xhdpi/zzz_ph_bt_play_enabled.png new file mode 100755 index 0000000..5112499 Binary files /dev/null and b/res/drawable-xhdpi/zzz_ph_bt_play_enabled.png differ diff --git a/res/drawable-xhdpi/zzz_ph_bt_skip_disabled.png b/res/drawable-xhdpi/zzz_ph_bt_skip_disabled.png new file mode 100644 index 0000000..2bfd25d Binary files /dev/null and b/res/drawable-xhdpi/zzz_ph_bt_skip_disabled.png differ diff --git a/res/drawable-xhdpi/zzz_ph_bt_skip_enabled.png b/res/drawable-xhdpi/zzz_ph_bt_skip_enabled.png new file mode 100755 index 0000000..fe6b558 Binary files /dev/null and b/res/drawable-xhdpi/zzz_ph_bt_skip_enabled.png differ diff --git a/res/drawable-xhdpi/zzz_ph_ic_notification.png b/res/drawable-xhdpi/zzz_ph_ic_notification.png new file mode 100755 index 0000000..8f41397 Binary files /dev/null and b/res/drawable-xhdpi/zzz_ph_ic_notification.png differ diff --git a/res/drawable/zzz_ph_bt_back.xml b/res/drawable/zzz_ph_bt_back.xml new file mode 100644 index 0000000..55e5897 --- /dev/null +++ b/res/drawable/zzz_ph_bt_back.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/res/drawable/zzz_ph_bt_pause.xml b/res/drawable/zzz_ph_bt_pause.xml new file mode 100644 index 0000000..798c8b4 --- /dev/null +++ b/res/drawable/zzz_ph_bt_pause.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/res/drawable/zzz_ph_bt_play.xml b/res/drawable/zzz_ph_bt_play.xml new file mode 100644 index 0000000..e4ae652 --- /dev/null +++ b/res/drawable/zzz_ph_bt_play.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/res/drawable/zzz_ph_bt_skip.xml b/res/drawable/zzz_ph_bt_skip.xml new file mode 100644 index 0000000..cb05273 --- /dev/null +++ b/res/drawable/zzz_ph_bt_skip.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/res/layout-v11/zzz_ph_hc_notification.xml b/res/layout-v11/zzz_ph_hc_notification.xml new file mode 100644 index 0000000..f959c19 --- /dev/null +++ b/res/layout-v11/zzz_ph_hc_notification.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout-v16/zzz_ph_jb_land_notification.xml b/res/layout-v16/zzz_ph_jb_land_notification.xml new file mode 100644 index 0000000..ec19991 --- /dev/null +++ b/res/layout-v16/zzz_ph_jb_land_notification.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout-v16/zzz_ph_jbb_notification.xml b/res/layout-v16/zzz_ph_jbb_notification.xml new file mode 100644 index 0000000..d32f60e --- /dev/null +++ b/res/layout-v16/zzz_ph_jbb_notification.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values-land/player_hater_flags.xml b/res/values-land/player_hater_flags.xml new file mode 100644 index 0000000..89edd2b --- /dev/null +++ b/res/values-land/player_hater_flags.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/res/values-v11/player_hater_flags.xml b/res/values-v11/player_hater_flags.xml new file mode 100644 index 0000000..242c898 --- /dev/null +++ b/res/values-v11/player_hater_flags.xml @@ -0,0 +1,20 @@ + + + + false + true + diff --git a/res/values-v11/styles.xml b/res/values-v11/styles.xml new file mode 100644 index 0000000..3c02242 --- /dev/null +++ b/res/values-v11/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/res/values-v14/player_hater_flags.xml b/res/values-v14/player_hater_flags.xml new file mode 100644 index 0000000..7e7d9b9 --- /dev/null +++ b/res/values-v14/player_hater_flags.xml @@ -0,0 +1,20 @@ + + + + false + true + diff --git a/res/values-v14/styles.xml b/res/values-v14/styles.xml new file mode 100644 index 0000000..a91fd03 --- /dev/null +++ b/res/values-v14/styles.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/res/values-v16/player_hater_flags.xml b/res/values-v16/player_hater_flags.xml new file mode 100644 index 0000000..81dd6d2 --- /dev/null +++ b/res/values-v16/player_hater_flags.xml @@ -0,0 +1,21 @@ + + + + false + false + true + diff --git a/res/values-v8/player_hater_flags.xml b/res/values-v8/player_hater_flags.xml new file mode 100644 index 0000000..4f9e273 --- /dev/null +++ b/res/values-v8/player_hater_flags.xml @@ -0,0 +1,19 @@ + + + + true + diff --git a/res/values/player_hater_flags.xml b/res/values/player_hater_flags.xml new file mode 100644 index 0000000..1e372bd --- /dev/null +++ b/res/values/player_hater_flags.xml @@ -0,0 +1,35 @@ + + + + + true + false + false + false + false + + Stop + Previous + Album Art + Skip + Toggle Playback + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 0000000..65d354e --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,5 @@ + + + PlayerHater + + diff --git a/res/values/styles.xml b/res/values/styles.xml new file mode 100644 index 0000000..6ce89c7 --- /dev/null +++ b/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/res/xml/zzz_ph_config_defaults.xml b/res/xml/zzz_ph_config_defaults.xml new file mode 100644 index 0000000..06a59f3 --- /dev/null +++ b/res/xml/zzz_ph_config_defaults.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/src/org/prx/playerhater/BroadcastReceiver.java b/src/org/prx/playerhater/BroadcastReceiver.java new file mode 100644 index 0000000..2c40d2a --- /dev/null +++ b/src/org/prx/playerhater/BroadcastReceiver.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.prx.playerhater; + +public class BroadcastReceiver extends org.prx.playerhater.broadcast.Receiver {} diff --git a/src/org/prx/playerhater/PlaybackService.java b/src/org/prx/playerhater/PlaybackService.java new file mode 100644 index 0000000..3f1df6d --- /dev/null +++ b/src/org/prx/playerhater/PlaybackService.java @@ -0,0 +1,5 @@ +package org.prx.playerhater; + +public class PlaybackService { + +} diff --git a/src/org/prx/playerhater/PlayerHater.java b/src/org/prx/playerhater/PlayerHater.java new file mode 100644 index 0000000..bbab83f --- /dev/null +++ b/src/org/prx/playerhater/PlayerHater.java @@ -0,0 +1,266 @@ +package org.prx.playerhater; + +import org.prx.playerhater.util.Config; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public abstract class PlayerHater { + /** + * Pauses the player. + * + * @return true if false if the player is not playing. + */ + abstract public boolean pause(); + + /** + * Stops the player. + * + * @return {@code true} if successful, {@code false} if the player is not + * playing or paused. + */ + abstract public boolean stop(); + + /** + * Begins playback of the currently loaded {@linkplain Song}. + * + * @return {@code true} if successful, {@code false} if there is no + * {@linkplain Song} loaded. + */ + abstract public boolean play(); + + /** + * Begins playback of the currently loaded {@linkplain Song} at + * {@code startTime} in the track. + * + * @see {@link IPlayerHater#seekTo(int) seekTo(int)} + * @see {@link IPlayerHater#play() play()} + * @param startTime + * The time in milliseconds at which to begin playback + * @return {@code true} if successful, {@code false} if there is no track + * loaded. + */ + abstract public boolean play(int startTime); + + /** + * Begins playback of a song at the beginning. + * + * @param song + * A {@linkplain Song} to play back. + * @return {@code true} if successful, {@code false} if there is a problem. + */ + abstract public boolean play(Song song); + + /** + * Begins playback of {@code song} at {@code startTime} + * + * @see {@link PlayerHater#play(int)}, {@link PlayerHater#play(Song)} + * @return {@code true} if successful, {@code false} if there is a problem. + */ + abstract public boolean play(Song song, int startTime); + + /** + * Moves the playhead to {@code startTime} + * + * @param startTime + * The time (in milliseconds) to move the playhead to. + * @see {@link PlayerHater#play(int)} + * @return {@code true} if successful, {@code false} if there is no song + * loaded in the player. + */ + abstract public boolean seekTo(int startTime); + + /** + * Puts a song on the end of the play queue. + * + * @param song + * The {@linkplain Song} to add to the end of the queue. + * @return the queue position of the song, in relation to the playhead. If + * this song has been loaded into the now playing position, this + * will return 0. If the song will be played next, it will return 1, + * and so on. + */ + abstract public int enqueue(Song song); + + /** + * Moves to a new position in the play queue. + * + * @param position + * The position in the queue (1-indexed) to skip to. + * @return {@code true} if successful, {@code false} if the {@code position} + * requested was invalid. + */ + abstract public boolean skipTo(int position); + + /** + * Moves to the next song in the play queue. + *

+ * If the player is already playing, it will continue to play with the new + * track. If it is not playing, it will move to the next track without + * playing. + *

+ * If the player has loaded last track, the player will be moved to the + * first song in the queue and playback will be stopped, regardless of + * whether or not the player is already playing. + */ + abstract public void skip(); + + /** + * Moves back in the play queue. + *

+ * If the player is already playing, it will continue to play with the new + * track. If it is not playing, it will skip without playing. + *

+ * If the playhead is currently fewer than 2 seconds from the beginning of + * the track, it will skip to the previous track. Otherwise, it will skip to + * the beginning of the currently loaded track. + *

+ * If the player has loaded the first track in the queue, it will skip to + * the beginning of that track regardless of the playhead's position. + */ + abstract public void skipBack(); + + /** + * Removes all songs from the queue. + *

+ * If the player is currently playing, only the currently playing song will + * be preserved. All tracks, including those before and after the current + * track, will be removed from the queue. + *

+ * If the player is not playing, all tracks, including the currently loaded + * one, will be removed. + */ + abstract public void emptyQueue(); + + /** + * Gets the location of the playhead in milliseconds. + * + * @return the current position of the play head in milliseconds + */ + abstract public int getCurrentPosition(); + + /** + * Gets the duration of the currently loaded Song in milliseconds. + * + * @return the duration of the currently loaded Song in milliseconds. + */ + abstract public int getDuration(); + + /** + * Gets the {@linkplain Song} representation of the track that is currently + * loaded in the player. + * + * @return the {@linkplain Song} of the track that is currently loaded in + * the player. + */ + abstract public Song nowPlaying(); + + /** + * Checks to see if the player is currently playing back audio. + * + * @return {@code true} if the player is currently playing, {@code false} + * otherwise. + */ + abstract public boolean isPlaying(); + + /** + * Checks to see if the player is currently loading audio. + * + * @return {@code true} if the player is currently loading, {@code false} + * otherwise. + */ + abstract public boolean isLoading(); + + /** + * Gets the state of the currently loaded + * {@linkplain android.media.MediaPlayer MediaPlayer}, represented as an + * int. + * + * @return One of: {@link MediaPlayerWithState#IDLE IDLE}, + * {@link MediaPlayerWithState#INITIALIZED INITIALIZED}, + * {@link MediaPlayerWithState#PREPARING PREPARING}, + * {@link MediaPlayerWithState#PREPARED PREPARED}, + * {@link MediaPlayerWithState#STARTED STARTED}, + * {@link MediaPlayerWithState#PAUSED PAUSED}, + * {@link MediaPlayerWithState#PLAYBACK_COMPLETED + * PLAYBACK_COMPLETED}, {@link MediaPlayerWithState#STOPPED STOPPED} + * , {@link MediaPlayerWithState#END END}, or + * {@link MediaPlayerWithState#ERROR ERROR} + */ + abstract public int getState(); + + /** + * Sets the visible buttons for plugins. It is up to the implementation of + * the plugin to honor these settings. + * + * @see {@link android.media.RemoteControlClient#setTransportControlFlags(int) } + * @param transportControlFlags + */ + abstract public void setTransportControlFlags(int transportControlFlags); + + /** + * Returns the number of items in the queue. + * + * @return the number of items currently in the queue. + */ + abstract public int getQueueLength(); + + /** + * Returns the number of clips in the queue which are at least partially + * behind the playhead. + *

+ * If the currently enqueued track is stopped at the beginning of the track, + * it is not considered in this calculation. If the player is paused or + * playing or the playhead is partially over the "now playing" track, it + * will be considered as part of this calculation. + * + * @return the number of clips in the queue with are at least partially + * behind the playhead. + */ + abstract public int getQueuePosition(); + + /** + * Removes the element at {@code position} from the play queue. + * + * @param position + * the (non-zero-indexed) position of the item in the queue to + * remove. + * @return true if the operation was successful, false if there are fewer + * than {@code position} items in the play queue. + */ + abstract public boolean removeFromQueue(int position); + + /** + * Constructs an {@linkplain Intent} which will start the appropriate + * {@linkplain PlayerHaterService} as configured in the project's + * AndroidManifest.xml file. + * + * @param context + * @return An {@link Intent} which will start the correct service. + * @throws IllegalArgumentException + * if there is no appropriate service configured in + * AndroidManifest.xml + */ + public static Intent buildServiceIntent(Context context) { + Intent intent = new Intent("org.prx.playerhater.SERVICE"); + intent.setPackage(context.getPackageName()); + Config.attachToIntent(intent); + + if (context.getPackageManager().queryIntentServices(intent, 0).size() == 0) { + intent = new Intent(context, PlaybackService.class); + Config.attachToIntent(intent); + if (context.getPackageManager().queryIntentServices(intent, 0) + .size() == 0) { + IllegalArgumentException e = new IllegalArgumentException( + "No usable service found."); + String tag = context.getPackageName() + "/PlayerHater"; + String message = "Please define your Playback Service. For help, refer to: https://github.com/PRX/PlayerHater/wiki/Setting-Up-Your-Manifest"; + Log.e(tag, message, e); + throw e; + } + } + + return intent; + } +} diff --git a/src/org/prx/playerhater/PlayerHaterPlugin.java b/src/org/prx/playerhater/PlayerHaterPlugin.java new file mode 100644 index 0000000..368808e --- /dev/null +++ b/src/org/prx/playerhater/PlayerHaterPlugin.java @@ -0,0 +1,317 @@ +/******************************************************************************* + * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.prx.playerhater; + +import org.prx.playerhater.PlayerHater; +import org.prx.playerhater.Song; + +import android.app.PendingIntent; +import android.content.Context; +import android.net.Uri; + +/** + * An interface that plugins for PlayerHater must implement. + *

+ * Plugins must implement a default constructor that takes no arguments. + *

+ * Most implementations should consider extending {@linkplain AbstractPlugin} + * instead of implementing this interface. + * + * @since 2.0.0 + * @version 2.1.0 + * + * @author Chris Rhoden + */ +public interface PlayerHaterPlugin { + + /** + * Called when the plugin can start doing work against PlayerHater. + * + * @param context + * A {@link Context} that the plugin can use to interact with the + * application. This will either be the + * {@link ApplicationContext} associated with the + * {@link Application} running the plugin, or it will be the + * context that is bound when this plugin was attached using + * {@link BoundPlayerHater#setBoundPlugin(PlayerHaterPlugin)} + * @param playerHater + * A instance of one of the subclasses of {@link PlayerHater}. + * This is the plugin's handle to PlayerHater, and it should not + * attempt to access it in any other way. + *

+ * If this plugin is loaded in the standard way, this will be an + * instance of {@link ServicePlayerHater}. Otherwise, it will be + * a {@link BoundPlayerHater} or a {@link BinderPlayerHater}, + * depending on the current status of the Service. + */ + void onPlayerHaterLoaded(Context context, PlayerHater playerHater); + + /** + * Called when the song that is loaded into the player has changed. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param song + * The Song which is now loaded into the player. + */ + void onSongChanged(Song song); + + /** + * Called when the song that was loaded into the player has completed. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param song + * The Song that has finished playing. + * @param reason + * The reason that the song reached the end. One of + * {@link PlayerHater#SKIP_BUTTON } or + * {@link PlayerHater#TRACK_END } + */ + void onSongFinished(Song song, int reason); + + /** + * Called when the duration of the currently loaded track has changed. + * Typically called whenever {@link PlayerHaterPlugin#onSongChanged(Song)} + * has been called, but provided as a convenience for plugins that do not + * care about other metdata. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param duration + * The new duration of the currently loaded Song in milliseconds. + */ + void onDurationChanged(int duration); + + /** + * Called when the player has started preparing or buffering the Song loaded + * into it. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + */ + void onAudioLoading(); + + /** + * Called when playback has been paused. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + */ + void onAudioPaused(); + + /** + * Called when playback has been resumed after being paused. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + */ + void onAudioResumed(); + + /** + * Called when audio begins playback for the first time. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + */ + void onAudioStarted(); + + /** + * Called when the audio has been stopped without the possibility of + * resuming. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + */ + void onAudioStopped(); + + /** + * Called when the title of the currently playing track has changed. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param title + * The new title of the currently playing track. + */ + void onTitleChanged(String title); + + /** + * Called when the artist of the currently playing track has changed. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param artist + * The new artist of the currently playing track. + */ + void onArtistChanged(String artist); + + /** + * Called when the album art of the currently playing track has changed. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param url + * The URI of the new album art for the currently playing track. + */ + void onAlbumArtChanged(Uri url); + + /** + * Called when there is a song "on deck" to play next. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param nextTrack + * The next song which will be played. + */ + void onNextSongAvailable(Song nextTrack); + + /** + * Called when there is no song "on deck" to play next. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + */ + void onNextSongUnavailable(); + + /** + * Called when the requested Transport Control Flags have changed. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @see {@link android.media.RemoteControlClient#setTransportControlFlags(int)} + * + * @param transportControlFlags + * A bitmask representing the transport control flags requested + */ + void onTransportControlFlagsChanged(int transportControlFlags); + + /** + * Called when the requested pending intent for resuming the host + * application has changed. + *

+ * NOTE: This method, by default, is not guaranteed to run on the UI + * thread. If you need it to run on the UI Thread, you should ensure that + * your plugin is run in the context of a BackgroundedPlugin with the + * correct flags set. + *

+ * If you just want a method which is guaranteed to run on the UI thread, + * {@link #onChangesComplete()} is guaranteed to be called shortly after any + * changes, and will always be run on the UI thread by default. + * + * @param intent + * A pending intent that the plugin should use if it wants to + * resume the hosting application. + */ + void onIntentActivityChanged(PendingIntent intent); + + /** + * Called after one or more state-change callbacks have completed. This + * method is guaranteed, by default, to run on the UI thread. + */ + void onChangesComplete(); +} diff --git a/src/org/prx/playerhater/Song.java b/src/org/prx/playerhater/Song.java new file mode 100644 index 0000000..e90415a --- /dev/null +++ b/src/org/prx/playerhater/Song.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.prx.playerhater; + +import android.net.Uri; +import android.os.Bundle; + +/** + * An interface which can be used for playback in {@linkplain PlayerHater}. This + * interface is primarily for use by {@linkplain PlayerHaterPlugin}s, which use + * the metadata provided to display notifications, widgets, and lock screen + * controls. + * + * @see PlayerHater#play(Song) + * @version 3.0.0 + * @author Chris Rhoden + * @since 2.0.0 + */ +public interface Song { + + /** + * An accessor for the title attribute of a {@linkplain Song} playable + * entity in PlayerHater + * + * @return The title of the {@linkplain Song} + */ + String getTitle(); + + /** + * An accessor for the artist attribute of a {@linkplain Song} playable + * entity in PlayerHater + * + * @return The name of the artist performing the {@linkplain Song} + */ + String getArtist(); + + /** + * An accessor for the album attribute of a {@linkplain Song} playable + * entity in PlayerHater + * + * @return The name of the album on which the {@linkplain Song} is performed + */ + String getAlbumTitle(); + + /** + * An accessor for the the album art attribute of a {@linkplain Song} + * playable entity in PlayerHater + * + * @return The Uri representing the album art for this {@linkplain Song} + */ + Uri getAlbumArt(); + + /** + * @see android.media.MediaPlayer#setDataSource(android.content.Context, + * Uri) + * @return A Uri which resolves to the {@linkplain Song}'s file, which can + * be played using Android's + * {@linkplain android.media.MediaPlayer#setDataSource(android.content.Context, Uri) + * MediaPlayer} class. + *

+ * Ex. + *

+ * {@code http://www.example.com/track.mp3 } + *

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

+ * PlayerHater will not do anything with this. + */ + Bundle getExtra(); +} diff --git a/src/org/prx/playerhater/broadcast/HeadphoneButtonGestureHelper.java b/src/org/prx/playerhater/broadcast/HeadphoneButtonGestureHelper.java new file mode 100644 index 0000000..353d549 --- /dev/null +++ b/src/org/prx/playerhater/broadcast/HeadphoneButtonGestureHelper.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.prx.playerhater.broadcast; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.view.KeyEvent; + +class HeadphoneButtonGestureHelper { + + private static final int BUTTON_PRESSED = 0; + private static final int PLAY_PAUSE = 1; + private static final int NEXT = 2; + private static final int PREV = 3; + private static final int MILISECONDS_DELAY = 250; + public static final String TAG = "GESTURES"; + + + private long mLastEventTime = 0; + private int mCurrentAction = 1; + private static Context lastContext; + + private final Handler mHandler = new ButtonHandler(this); + private RemoteControlButtonReceiver mMediaButtonReceiver; + + public void onHeadsetButtonPressed(long eventTime, Context context) { + if (eventTime - mLastEventTime <= MILISECONDS_DELAY + 100) { + mCurrentAction += 1; + if (mCurrentAction > 3) { + mCurrentAction = 1; + } + mHandler.removeMessages(BUTTON_PRESSED); + } + lastContext = context; + mLastEventTime = eventTime; + mHandler.sendEmptyMessageDelayed(BUTTON_PRESSED, MILISECONDS_DELAY); + } + + public void setReceiver(RemoteControlButtonReceiver receiver) { + mMediaButtonReceiver = receiver; + } + + private static class ButtonHandler extends Handler { + + private final HeadphoneButtonGestureHelper mButtonGestureHelper; + + private ButtonHandler(HeadphoneButtonGestureHelper ctx) { + mButtonGestureHelper = ctx; + } + + @Override + public void dispatchMessage(Message message) { + switch (mButtonGestureHelper.mCurrentAction) { + + case PLAY_PAUSE: + mButtonGestureHelper.mMediaButtonReceiver + .onRemoteControlButtonPressed(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, lastContext); + break; + case NEXT: + mButtonGestureHelper.mMediaButtonReceiver + .onRemoteControlButtonPressed(KeyEvent.KEYCODE_MEDIA_NEXT, lastContext); + break; + case PREV: + mButtonGestureHelper.mMediaButtonReceiver + .onRemoteControlButtonPressed(KeyEvent.KEYCODE_MEDIA_PREVIOUS, lastContext); + break; + } + mButtonGestureHelper.mLastEventTime = 0; + mButtonGestureHelper.mCurrentAction = 1; + } + } + +} diff --git a/src/org/prx/playerhater/broadcast/Receiver.java b/src/org/prx/playerhater/broadcast/Receiver.java new file mode 100644 index 0000000..8bde716 --- /dev/null +++ b/src/org/prx/playerhater/broadcast/Receiver.java @@ -0,0 +1,107 @@ +package org.prx.playerhater.broadcast; + +import org.prx.playerhater.PlayerHater; +import org.prx.playerhater.ipc.IPlayerHaterServer; + +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.view.KeyEvent; + +public class Receiver extends BroadcastReceiver implements + RemoteControlButtonReceiver { + + public static final String REMOTE_CONTROL_BUTTON = "org.prx.playerhater.REMOTE_CONTROL"; + + private static final HeadphoneButtonGestureHelper sGestureHelper = new HeadphoneButtonGestureHelper(); + + public Receiver() { + super(); + } + + public Receiver(Context context) { + super(); + sGestureHelper.setReceiver(this); + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_HEADSET_PLUG); + filter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); + filter.addAction(Intent.ACTION_MEDIA_BUTTON); + filter.setPriority(10000); + context.registerReceiver(this, filter); + } + + @Override + @SuppressLint("InlinedApi") + public void onRemoteControlButtonPressed(int keyCode, Context context) { + sendKeyCode(context, keyCode, keyCode != KeyEvent.KEYCODE_MEDIA_PAUSE); + } + + @Override + @SuppressLint("InlinedApi") + public void onReceive(Context context, Intent intent) { + int keyCode = -1; + if (intent.getAction() != null) { + if (intent.getIntExtra("state", 0) == 0) { + if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) { + keyCode = KeyEvent.KEYCODE_MEDIA_PAUSE; + } else if (intent.getAction().equals( + AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { + keyCode = KeyEvent.KEYCODE_MEDIA_PAUSE; + } + } + if (intent.getAction().equals(Intent.ACTION_MEDIA_BUTTON)) { + KeyEvent event = (KeyEvent) intent + .getParcelableExtra(Intent.EXTRA_KEY_EVENT); + + if (event.getAction() == KeyEvent.ACTION_DOWN) { + return; + } + + keyCode = event.getKeyCode(); + + if (KeyEvent.KEYCODE_HEADSETHOOK == keyCode) { + sGestureHelper.onHeadsetButtonPressed(event.getEventTime(), + context); + } + } + + if (keyCode != -1) { + boolean autoStart = keyCode != KeyEvent.KEYCODE_MEDIA_PAUSE + && keyCode != KeyEvent.KEYCODE_MEDIA_STOP; + sendKeyCode(context, keyCode, autoStart); + } + } + } + + private IPlayerHaterServer getService(Context context) { + Intent intent = PlayerHater.buildServiceIntent(context); + return IPlayerHaterServer.Stub + .asInterface(peekService(context, intent)); + } + + private void sendKeyCode(Context context, int keyCode, + boolean startIfNecessary) { + if (getService(context) != null) { + try { + getService(context).onRemoteControlButtonPressed(keyCode); + } catch (Exception e) { + if (startIfNecessary && context != null) { + Intent intent = context + .getPackageManager() + .getLaunchIntentForPackage(context.getPackageName()); + intent.putExtra(REMOTE_CONTROL_BUTTON, keyCode); + context.startActivity(intent); + } + } + + } else if (startIfNecessary && context != null) { + Intent intent = new Intent(PlayerHater.buildServiceIntent(context)); + intent.putExtra(REMOTE_CONTROL_BUTTON, keyCode); + context.startService(intent); + } + } + +} diff --git a/src/org/prx/playerhater/broadcast/RemoteControlButtonReceiver.java b/src/org/prx/playerhater/broadcast/RemoteControlButtonReceiver.java new file mode 100644 index 0000000..1b76fa2 --- /dev/null +++ b/src/org/prx/playerhater/broadcast/RemoteControlButtonReceiver.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.prx.playerhater.broadcast; + +import android.content.Context; + +interface RemoteControlButtonReceiver { + public void onRemoteControlButtonPressed(int keyCode, Context context); +} diff --git a/src/org/prx/playerhater/ipc/Client.java b/src/org/prx/playerhater/ipc/Client.java new file mode 100644 index 0000000..4fd8cf0 --- /dev/null +++ b/src/org/prx/playerhater/ipc/Client.java @@ -0,0 +1,187 @@ +package org.prx.playerhater.ipc; + +import org.prx.playerhater.PlayerHater; +import org.prx.playerhater.PlayerHaterPlugin; +import org.prx.playerhater.Song; +import org.prx.playerhater.songs.SongHost; +import org.prx.playerhater.util.Log; + +import android.app.PendingIntent; +import android.content.Context; +import android.net.Uri; +import android.os.RemoteException; + +public class Client implements PlayerHaterPlugin { + + private static final String CLIENT_ERROR = "Client has gone away..."; + + private final IPlayerHaterClient mClient; + + public Client(IPlayerHaterClient client) { + mClient = client; + } + + @Override + public void onPlayerHaterLoaded(Context context, PlayerHater playerHater) { + } + + @Override + public void onSongChanged(Song song) { + try { + mClient.onSongChanged(SongHost.getTag(song)); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onSongFinished(Song song, int reason) { + try { + mClient.onSongFinished(SongHost.getTag(song), reason); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onDurationChanged(int duration) { + try { + mClient.onDurationChanged(duration); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onAudioLoading() { + try { + mClient.onAudioLoading(); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onAudioPaused() { + try { + mClient.onAudioPaused(); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onAudioResumed() { + try { + mClient.onAudioResumed(); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onAudioStarted() { + try { + mClient.onAudioStarted(); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onAudioStopped() { + try { + mClient.onAudioStopped(); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onTitleChanged(String title) { + try { + mClient.onTitleChanged(title); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onArtistChanged(String artist) { + try { + mClient.onArtistChanged(artist); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onAlbumArtChanged(Uri url) { + try { + mClient.onAlbumArtChanged(url); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onNextSongAvailable(Song nextTrack) { + try { + mClient.onNextSongAvailable(SongHost.getTag(nextTrack)); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onNextSongUnavailable() { + try { + mClient.onNextSongUnavailable(); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onTransportControlFlagsChanged(int transportControlFlags) { + try { + mClient.onTransportControlFlagsChanged(transportControlFlags); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onIntentActivityChanged(PendingIntent intent) { + try { + mClient.onIntentActivityChanged(intent); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } + + @Override + public void onChangesComplete() { + try { + mClient.onChangesComplete(); + } catch (RemoteException e) { + Log.e(CLIENT_ERROR, e); + throw new IllegalStateException(CLIENT_ERROR, e); + } + } +} diff --git a/src/org/prx/playerhater/ipc/IPlayerHaterClient.aidl b/src/org/prx/playerhater/ipc/IPlayerHaterClient.aidl new file mode 100644 index 0000000..e3d126e --- /dev/null +++ b/src/org/prx/playerhater/ipc/IPlayerHaterClient.aidl @@ -0,0 +1,38 @@ +package org.prx.playerhater.ipc; + +import android.net.Uri; +import android.app.PendingIntent; + +interface IPlayerHaterClient { + + /** + * Plugin Methods + */ + void onSongChanged(int songTag); + void onSongFinished(int songTag, int reason); + void onDurationChanged(int duration); + void onAudioLoading(); + void onAudioPaused(); + void onAudioResumed(); + void onAudioStarted(); + void onAudioStopped(); + void onTitleChanged(String title); + void onArtistChanged(String artist); + void onAlbumTitleChanged(String albumTitle); + void onAlbumArtChanged(in Uri uri); + void onTransportControlFlagsChanged(int transportControlFlags); + void onNextSongAvailable(int songTag); + void onNextSongUnavailable(); + void onChangesComplete(); + void onIntentActivityChanged(in PendingIntent intent); + + /** + * SongHost Methods + */ + String getSongTitle(int songTag); + String getSongArtist(int songTag); + String getSongAlbumTitle(int songTag); + Uri getSongAlbumArt(int songTag); + Uri getSongUri(int songTag); + Bundle getSongExtra(int songTag); +} \ No newline at end of file diff --git a/src/org/prx/playerhater/ipc/IPlayerHaterServer.aidl b/src/org/prx/playerhater/ipc/IPlayerHaterServer.aidl new file mode 100644 index 0000000..a45f08c --- /dev/null +++ b/src/org/prx/playerhater/ipc/IPlayerHaterServer.aidl @@ -0,0 +1,53 @@ +package org.prx.playerhater.ipc; + +import android.net.Uri; +import org.prx.playerhater.ipc.IPlayerHaterClient; +import android.app.Notification; + +interface IPlayerHaterServer { + + /** + * Server-specific methods + */ + void setClient(IPlayerHaterClient client); + void onRemoteControlButtonPressed(int keyCode); + void startForeground(int notificationNu, in Notification notification); + void stopForeground(boolean fact); + void duck(); + void unduck(); + + /** + * PlayerHater Methods + */ + boolean pause(); + boolean stop(); + boolean resume(); + boolean playAtTime(int startTime); + boolean play(int songTag, int startTime); + boolean seekTo(int startTime); + int enqueue(int songTag); + boolean skipTo(int position); + void skip(); + void skipBack(); + void emptyQueue(); + int getCurrentPosition(); + int getDuration(); + int nowPlaying(); + boolean isPlaying(); + boolean isLoading(); + int getState(); + void setTransportControlFlags(int transportControlFlags); + int getQueueLength(); + int getQueuePosition(); + boolean removeFromQueue(int position); + + /** + * SongHost Methods + */ + String getSongTitle(int songTag); + String getSongArtist(int songTag); + String getSongAlbumTitle(int songTag); + Uri getSongAlbumArt(int songTag); + Uri getSongUri(int songTag); + Bundle getSongExtra(int songTag); +} \ No newline at end of file diff --git a/src/org/prx/playerhater/ipc/Server.java b/src/org/prx/playerhater/ipc/Server.java new file mode 100644 index 0000000..5264255 --- /dev/null +++ b/src/org/prx/playerhater/ipc/Server.java @@ -0,0 +1,240 @@ +package org.prx.playerhater.ipc; + +import org.prx.playerhater.PlayerHater; +import org.prx.playerhater.Song; +import org.prx.playerhater.songs.SongHost; +import org.prx.playerhater.util.Log; + +import android.os.RemoteException; + +public class Server extends PlayerHater { + + private static final String SERVER_ERROR = "Server has gone away..."; + + private final IPlayerHaterServer mServer; + + public Server(IPlayerHaterServer server) { + mServer = server; + } + + @Override + public boolean pause() { + try { + return mServer.pause(); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public boolean stop() { + try { + return mServer.stop(); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public boolean play() { + try { + return mServer.resume(); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public boolean play(int startTime) { + try { + return mServer.playAtTime(startTime); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public boolean play(Song song) { + try { + return mServer.play(SongHost.getTag(song), 0); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public boolean play(Song song, int startTime) { + try { + return mServer.play(SongHost.getTag(song), 0); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public boolean seekTo(int startTime) { + try { + return mServer.seekTo(startTime); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public int enqueue(Song song) { + try { + return mServer.enqueue(SongHost.getTag(song)); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public boolean skipTo(int position) { + try { + return mServer.skipTo(position); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public void skip() { + try { + mServer.skip(); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public void skipBack() { + try { + mServer.skipBack(); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public void emptyQueue() { + try { + mServer.emptyQueue(); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public int getCurrentPosition() { + try { + return mServer.getCurrentPosition(); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public int getDuration() { + try { + return mServer.getDuration(); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public Song nowPlaying() { + try { + return SongHost.getSong(mServer.nowPlaying()); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public boolean isPlaying() { + try { + return mServer.isPlaying(); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public boolean isLoading() { + try { + return mServer.isLoading(); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public int getState() { + try { + return mServer.getState(); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public void setTransportControlFlags(int transportControlFlags) { + try { + mServer.setTransportControlFlags(transportControlFlags); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public int getQueueLength() { + try { + return mServer.getQueueLength(); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public int getQueuePosition() { + try { + return mServer.getQueuePosition(); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + + @Override + public boolean removeFromQueue(int position) { + try { + return mServer.removeFromQueue(position); + } catch (RemoteException e) { + Log.e(SERVER_ERROR, e); + throw new IllegalStateException(SERVER_ERROR, e); + } + } + +} diff --git a/src/org/prx/playerhater/songs/RemoteSong.java b/src/org/prx/playerhater/songs/RemoteSong.java new file mode 100644 index 0000000..adfb023 --- /dev/null +++ b/src/org/prx/playerhater/songs/RemoteSong.java @@ -0,0 +1,201 @@ +/******************************************************************************* + * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.prx.playerhater.songs; + +import org.prx.playerhater.Song; +import org.prx.playerhater.ipc.IPlayerHaterClient; +import org.prx.playerhater.ipc.IPlayerHaterServer; + +import android.net.Uri; +import android.os.Bundle; +import android.os.RemoteException; + +public class RemoteSong implements Song { + + private static SongHost sSongHost; + + public static interface SongHost { + Uri getSongAlbumArt(int tag) throws RemoteException; + + Uri getSongUri(int tag) throws RemoteException; + + String getSongAlbumTitle(int tag) throws RemoteException; + + String getSongTitle(int tag) throws RemoteException; + + String getSongArtist(int tag) throws RemoteException; + + Bundle getSongExtra(int tag) throws RemoteException; + } + + private static final class ClientSongHost implements SongHost { + private final IPlayerHaterClient mClient; + + private ClientSongHost(IPlayerHaterClient client) { + mClient = client; + } + + @Override + public Uri getSongAlbumArt(int tag) throws RemoteException { + return mClient.getSongAlbumArt(tag); + } + + @Override + public Uri getSongUri(int tag) throws RemoteException { + return mClient.getSongUri(tag); + } + + @Override + public String getSongTitle(int tag) throws RemoteException { + return mClient.getSongTitle(tag); + } + + @Override + public String getSongArtist(int tag) throws RemoteException { + return mClient.getSongArtist(tag); + } + + @Override + public Bundle getSongExtra(int tag) throws RemoteException { + return mClient.getSongExtra(tag); + } + + @Override + public String getSongAlbumTitle(int tag) throws RemoteException { + return mClient.getSongAlbumTitle(tag); + } + } + + private static class ServerSongHost implements SongHost { + private final IPlayerHaterServer mServer; + + private ServerSongHost(IPlayerHaterServer server) { + mServer = server; + } + + @Override + public Uri getSongAlbumArt(int tag) throws RemoteException { + return mServer.getSongAlbumArt(tag); + } + + @Override + public Uri getSongUri(int tag) throws RemoteException { + return mServer.getSongUri(tag); + } + + @Override + public String getSongTitle(int tag) throws RemoteException { + return mServer.getSongTitle(tag); + } + + @Override + public String getSongArtist(int tag) throws RemoteException { + return mServer.getSongArtist(tag); + } + + @Override + public Bundle getSongExtra(int tag) throws RemoteException { + return mServer.getSongExtra(tag); + } + + @Override + public String getSongAlbumTitle(int tag) throws RemoteException { + return mServer.getSongAlbumTitle(tag); + } + } + + public static void setSongHost(SongHost host) { + sSongHost = host; + } + + public static void setSongHost(IPlayerHaterClient client) { + sSongHost = new ClientSongHost(client); + } + + public static void setSongHost(IPlayerHaterServer server) { + sSongHost = new ServerSongHost(server); + } + + private static SongHost getSongHost() { + return sSongHost; + } + + private final int mTag; + + RemoteSong(int tag) { + mTag = tag; + } + + @Override + public String getTitle() { + try { + return getSongHost().getSongTitle(mTag); + } catch (RemoteException e) { + throw new IllegalStateException( + "Remote Process has died or become disconnected", e); + } + } + + @Override + public String getArtist() { + try { + return getSongHost().getSongArtist(mTag); + } catch (RemoteException e) { + throw new IllegalStateException( + "Remote Process has died or become disconnected", e); + } + } + + @Override + public Uri getAlbumArt() { + try { + return getSongHost().getSongAlbumArt(mTag); + } catch (RemoteException e) { + throw new IllegalStateException( + "Remote Process has died or become disconnected", e); + } + } + + @Override + public Uri getUri() { + try { + return getSongHost().getSongUri(mTag); + } catch (RemoteException e) { + throw new IllegalStateException( + "Remote Process has died or become disconnected", e); + } + } + + @Override + public Bundle getExtra() { + try { + return getSongHost().getSongExtra(mTag); + } catch (RemoteException e) { + throw new IllegalStateException( + "Remote Process has died or become disconnected", e); + } + } + + @Override + public String getAlbumTitle() { + try { + return getSongHost().getSongAlbumTitle(mTag); + } catch (RemoteException e) { + throw new IllegalStateException( + "Remote Process has died or become disconnected", e); + } + } +} diff --git a/src/org/prx/playerhater/songs/SongHost.java b/src/org/prx/playerhater/songs/SongHost.java new file mode 100644 index 0000000..e172e8c --- /dev/null +++ b/src/org/prx/playerhater/songs/SongHost.java @@ -0,0 +1,39 @@ +package org.prx.playerhater.songs; + +import java.util.HashMap; +import java.util.Map; + +import org.prx.playerhater.Song; + +import android.util.SparseArray; + +public class SongHost { + + private static final SparseArray sSongs = new SparseArray(); + private static final Map sTags = new HashMap(); + + + public static int getTag(Song song) { + if (sTags.containsKey(song)) { + return sTags.get(song); + } else { + int tag = song.hashCode(); + sTags.put(song, tag); + sSongs.put(tag, song); + return tag; + } + } + + public static Song getSong(int tag) { + Song song = sSongs.get(tag); + if (song != null) { + return song; + } else { + song = new RemoteSong(tag); + sTags.put(song, tag); + sSongs.put(tag, song); + return song; + } + } + +} diff --git a/src/org/prx/playerhater/util/Config.java b/src/org/prx/playerhater/util/Config.java new file mode 100644 index 0000000..764f93f --- /dev/null +++ b/src/org/prx/playerhater/util/Config.java @@ -0,0 +1,214 @@ +package org.prx.playerhater.util; + +import java.util.HashSet; +import java.util.Set; + +import org.prx.playerhater.PlayerHater; +import org.prx.playerhater.PlayerHaterPlugin; +import org.prx.playerhater.R; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.content.res.Resources.NotFoundException; +import android.os.Parcel; +import android.os.Parcelable; + +public class Config implements Parcelable { + public static final String EXTRA_CONFIG = "config"; + public static Config sInstance; + + public static void attachToIntent(Intent intent) { + if (sInstance != null) { + intent.putExtra(EXTRA_CONFIG, sInstance); + } + } + + public static Config getInstance(Context context) { + if (sInstance == null) { + sInstance = new Config(context); + } + return sInstance; + } + + private final Set mPlugins = new HashSet(); + private final Set mPreboundPlugins = new HashSet(); + + private Config(Context context) { + XmlResourceParser parser = context.getResources().getXml( + R.xml.zzz_ph_config_defaults); + load(parser, context); + try { + ServiceInfo info = context.getPackageManager().getServiceInfo( + PlayerHater.buildServiceIntent(context).getComponent(), + PackageManager.GET_META_DATA); + if (info != null && info.metaData != null) { + int id = info.metaData.getInt("org.prx.playerhater.Config", 0); + if (id != 0) { + parser = context.getResources().getXml(id); + load(parser, context); + } + } + } catch (NameNotFoundException e) { + // If this happens, we can just use the default configuration. + } + } + + public Set> getPrebindPlugins() { + return getPlugins(mPreboundPlugins); + } + + public Set> getServicePlugins() { + for (String plugin : mPlugins) { + Log.d(plugin); + } + return getPlugins(mPlugins); + } + + @SuppressWarnings("unchecked") + private Set> getPlugins( + Set strings) { + Set> plugins = new HashSet>(); + for (String pluginName : strings) { + try { + plugins.add((Class) Class + .forName(pluginName)); + } catch (Exception e) { + Log.e("Can't load plugin " + pluginName, e); + } + } + return plugins; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStringArray(getPluginsArray()); + dest.writeStringArray(getPreboundPluginsArray()); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + + @Override + public Config createFromParcel(Parcel in) { + return new Config(in); + } + + @Override + public Config[] newArray(int size) { + return new Config[size]; + } + }; + + private static final int PLUGIN = 1; + private static final int INVALID_TAG = -1; + + private Config(Parcel in) { + setPluginsArray(in.createStringArray()); + setPreboundPluginsArray(in.createStringArray()); + } + + private String[] getPluginsArray() { + return mPlugins.toArray(new String[mPlugins.size() - 1]); + } + + private String[] getPreboundPluginsArray() { + return mPreboundPlugins.toArray(new String[mPlugins.size() - 1]); + } + + private void setPluginsArray(String[] plugins) { + setStringArray(plugins, mPlugins); + } + + private void setPreboundPluginsArray(String[] plugins) { + setStringArray(plugins, mPreboundPlugins); + } + + private void setStringArray(String[] stuff, Set in) { + in.clear(); + for (String plugin : stuff) { + in.add(plugin); + } + } + + private void load(XmlResourceParser parser, Context context) { + Resources res = context.getResources(); + try { + parser.next(); + int eventType = parser.getEventType(); + boolean pluginEnabled = false; + boolean prebindPlugin = false; + boolean pluginDisabled = false; + String pluginName = null; + int currentTagType = INVALID_TAG; + + while (eventType != XmlResourceParser.END_DOCUMENT) { + if (eventType == XmlResourceParser.START_DOCUMENT) { + // + } else if (eventType == XmlResourceParser.START_TAG) { + if (parser.getName().equals("plugin")) { + currentTagType = PLUGIN; + } + pluginEnabled = loadBooleanOrResourceBoolean(res, parser, + "enabled", true); + prebindPlugin = loadBooleanOrResourceBoolean(res, parser, + "prebind", false); + pluginDisabled = loadBooleanOrResourceBoolean(res, parser, + "disabled", false); + + pluginName = parser.getAttributeValue(null, "name"); + } else if (eventType == XmlResourceParser.END_TAG) { + switch (currentTagType) { + case PLUGIN: + if (pluginEnabled && pluginName != null) { + if (prebindPlugin) { + mPreboundPlugins.add(pluginName); + } else { + mPlugins.add(pluginName); + } + } else if (pluginDisabled && pluginName != null) { + mPlugins.remove(pluginName); + mPreboundPlugins.remove(pluginName); + } + break; + } + } else if (eventType == XmlResourceParser.TEXT) { + // NOTHING + } + eventType = parser.next(); + } + } catch (Exception e) { + + } + } + + private boolean loadBooleanOrResourceBoolean(Resources res, + XmlResourceParser parser, String attrName, boolean def) { + int id; + boolean result = def; + try { + id = parser.getAttributeResourceValue(null, attrName, 0); + if (id != 0) { + try { + result = res.getBoolean(id); + } catch (NotFoundException e) { + result = parser.getAttributeBooleanValue(null, attrName, + def); + } + } else { + result = parser.getAttributeBooleanValue(null, attrName, def); + } + } catch (Exception e) { + return result; + } + return result; + } +} diff --git a/src/org/prx/playerhater/util/Log.java b/src/org/prx/playerhater/util/Log.java new file mode 100644 index 0000000..ea833d8 --- /dev/null +++ b/src/org/prx/playerhater/util/Log.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright 2013 Chris Rhoden, Rebecca Nesson, Public Radio Exchange + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.prx.playerhater.util; + +import org.prx.playerhater.BuildConfig; + +public class Log { + + public static String TAG = "PlayerHater"; + + public static void v(String msg) { + if (BuildConfig.DEBUG) { + android.util.Log.v(TAG, msg); + } + } + + public static void d(String msg) { + if (BuildConfig.DEBUG) { + android.util.Log.d(TAG, msg); + } + } + + public static void e(String string, Exception e) { + android.util.Log.e(TAG, string, e); + } + +}