1 /***
2 * Ambient - A music player for the Android platform
3 Copyright (C) 2007 Martin Vysny
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18 package sk.baka.ambient.playerservice;
19
20 import java.io.IOException;
21 import java.net.InetAddress;
22 import java.net.URL;
23 import java.util.Map;
24
25 import sk.baka.ambient.AmbientApplication;
26 import sk.baka.ambient.R;
27 import sk.baka.ambient.collection.AbstractAudio;
28 import sk.baka.ambient.collection.TrackOriginEnum;
29 import sk.baka.ambient.stream.shoutcast.IShoutcastListener;
30 import android.app.Service;
31 import android.content.Intent;
32 import android.media.MediaPlayer;
33 import android.media.MediaPlayer.OnBufferingUpdateListener;
34 import android.media.MediaPlayer.OnCompletionListener;
35 import android.media.MediaPlayer.OnErrorListener;
36 import android.media.MediaPlayer.OnPreparedListener;
37 import android.net.Uri;
38 import android.os.IBinder;
39 import android.util.Log;
40
41 /***
42 * <p>
43 * Simply plays required audio files. Invokes {@link IPlayerListener} handler
44 * when the playback is finished.
45 * </p>
46 * <p>
47 * This service mainly prevents Android to kill the application when no
48 * activities are shown on screen.
49 * </p>
50 *
51 * @author Martin Vysny
52 */
53 public final class PlayerService extends Service implements
54 OnCompletionListener, OnErrorListener, OnPreparedListener,
55 IShoutcastListener, OnBufferingUpdateListener {
56
57 /***
58 * The player.
59 */
60 private MediaPlayer player;
61
62 /***
63 * The streamer server.
64 */
65 private final StreamerServer streamerServer = new StreamerServer(this);
66
67 @Override
68 public void onCreate() {
69 super.onCreate();
70 setForeground(true);
71 newMediaPlayer();
72 try {
73 streamerServer.start(StreamerServer.PORT, InetAddress
74 .getLocalHost());
75 } catch (IOException e) {
76 throw new RuntimeException(e);
77 }
78 }
79
80 private void newMediaPlayer() {
81 if (player != null) {
82 try {
83 player.release();
84 } catch (Throwable e) {
85
86
87 }
88 }
89 player = new MediaPlayer();
90 player.setOnCompletionListener(this);
91 player.setOnErrorListener(this);
92 player.setOnPreparedListener(this);
93 player.setOnBufferingUpdateListener(this);
94 }
95
96 /***
97 * The file being played.
98 */
99 private String file;
100
101 /***
102 * The track origin.
103 */
104 private TrackOriginEnum origin;
105
106 /***
107 * The playback state.
108 */
109 volatile PlayerStateEnum playerState = PlayerStateEnum.Stopped;
110
111 /***
112 * Log events with this tag.
113 */
114 private final static String TAG = PlayerService.class.getSimpleName();
115
116 @Override
117 public void onDestroy() {
118 try {
119
120 AmbientApplication.getInstance().getStateHandler().saveState();
121 AmbientApplication.getInstance().getNotificator()
122 .cancelNotifications();
123
124 try {
125 stop(true);
126 } catch (Exception ex) {
127 Log.e(TAG, ex.getMessage(), ex);
128 }
129 streamerServer.stop();
130 } finally {
131 super.onDestroy();
132 }
133 }
134
135 private final IBinder binder = new IPlayer.Stub() {
136 public int getPlaybackState() {
137 return playerState.ordinal();
138 }
139
140 @SuppressWarnings("incomplete-switch")
141 public void pause() {
142 switch (playerState) {
143 case Paused: {
144 player.start();
145 playerState = PlayerStateEnum.Playing;
146 }
147 break;
148 case Playing: {
149 player.pause();
150 playerState = PlayerStateEnum.Paused;
151 }
152 break;
153 }
154 }
155
156 public void play(String file, int o, int startSeekMillis) {
157 PlayerService.this.file = file;
158 PlayerService.this.origin = TrackOriginEnum.values()[o];
159 if (!origin.online) {
160
161 final AbstractAudio audio = AbstractAudio.fromUri(file);
162 if (!audio.exists()) {
163 invokeError(getString(R.string.file_not_found) + ": "
164 + file, null, true);
165 return;
166 }
167 if (!audio.isReadable()) {
168 invokeError(getString(R.string.file_not_readable) + ": "
169 + file, null, true);
170 return;
171 }
172 }
173 getInvocator().buffered((byte) 0);
174 try {
175 String url = file;
176 if (origin == TrackOriginEnum.Shoutcast) {
177 url = StreamerServer.getShoutcastStream(new URL(url));
178 }
179 PlayerService.this.stop(false);
180 PlayerService.this.startSeekMillis = origin.seekable ? startSeekMillis
181 : 0;
182 if (origin == TrackOriginEnum.LocalFs
183 && url.startsWith("content://")) {
184 player.setDataSource(PlayerService.this, Uri.parse(url));
185 } else {
186 player.setDataSource(url);
187 }
188 player.prepareAsync();
189 } catch (final Exception ex) {
190 invokeError(ex.getMessage(), ex, false);
191 }
192 }
193
194 public boolean seek(int position) {
195 if (playerState == PlayerStateEnum.Stopped) {
196 return false;
197 }
198 if (!origin.seekable) {
199 return false;
200 }
201 player.seekTo(position);
202 return true;
203 }
204
205 public void stop() {
206 PlayerService.this.stop(false);
207 getInvocator().buffered((byte) 0);
208 }
209
210 public int getPosition() {
211 if (playerState == PlayerStateEnum.Stopped)
212 return 0;
213 return player.getCurrentPosition();
214 }
215 };
216
217 /***
218 * Used only in {@link #onPrepared(MediaPlayer)} - states the playback start
219 * time.
220 */
221 private int startSeekMillis = 0;
222
223 private void stop(boolean release) {
224 if (playerState != PlayerStateEnum.Stopped) {
225 player.stop();
226 }
227 if (release) {
228 player.release();
229 } else {
230 try {
231 player.reset();
232 } catch (IllegalStateException ex) {
233
234
235 Log.e(TAG, "MediaPlayer failed to complete the reset() "
236 + "operation, throwing away", ex);
237 newMediaPlayer();
238 }
239 }
240 playerState = PlayerStateEnum.Stopped;
241 }
242
243 @Override
244 public IBinder onBind(Intent intent) {
245 return binder;
246 }
247
248 private final AmbientApplication getApp() {
249 return (AmbientApplication) getApplication();
250 }
251
252 /***
253 * Returns a proxy which posts events using the <code>SimpleBus</code>
254 * object.
255 *
256 * @return the proxy instance.
257 */
258 IPlayerListener getInvocator() {
259 return getApp().getBus().getInvocator(IPlayerListener.class, true);
260 }
261
262 public void onCompletion(MediaPlayer arg0) {
263 playerState = PlayerStateEnum.Stopped;
264 getInvocator().stopped(null, false, origin);
265 }
266
267 /***
268 * An error occurred. The player is stopped and a stop event is generated.
269 *
270 * @param message
271 * the error message.
272 * @param ex
273 * the error cause, may be <code>null</code> if not known
274 * @param missing
275 * if <code>true</code> then this error was caused by a file
276 * missing.
277 */
278 void invokeError(final String message, final Throwable ex,
279 final boolean missing) {
280 if (ex == null) {
281 Log.e(TAG, message);
282 } else {
283 Log.e(TAG, message, ex);
284 }
285 Log.e(TAG, "File: " + file);
286 stop(false);
287 playerState = PlayerStateEnum.Stopped;
288 getInvocator().stopped(message, missing, origin);
289 }
290
291 public boolean onError(MediaPlayer player, int what, int extra) {
292 invokeError(MediaPlayerErrors.getMessage(what, extra, this), null,
293 false);
294 return true;
295 }
296
297 public void onPrepared(MediaPlayer p) {
298 try {
299 player.start();
300 if (startSeekMillis > 0) {
301 player.seekTo(startSeekMillis);
302 }
303 playerState = PlayerStateEnum.Playing;
304 getInvocator().started(file,
305 origin.endless ? 0 : player.getDuration(),
306 origin.endless ? 0 : player.getCurrentPosition());
307 } catch (final Exception ex) {
308 invokeError(ex.getMessage(), ex, false);
309 }
310 }
311
312 public void metadataReceived(String title, Map<String, String> metadata) {
313 getInvocator().radioNewTrack(title);
314 }
315
316 public void opened(String name, String genre, boolean hasMetaint,
317 Map<String, String> metadata) {
318
319 }
320
321 public void onBufferingUpdate(MediaPlayer mp, int percent) {
322 getInvocator().buffered((byte) percent);
323 }
324 }