(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 extends P> 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 extends PlayerHaterPlugin> 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 extends PlayerHaterPlugin>) 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 |
--------------------------------------------------------------------------------