View Javadoc

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  				// do nothing, we are going to throw this media player away
86  				// anyway
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 			// try to save the application state
120 			AmbientApplication.getInstance().getStateHandler().saveState();
121 			AmbientApplication.getInstance().getNotificator()
122 					.cancelNotifications();
123 			// free all resources
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 				// check if the file exists and is readable
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 				// It seems that MediaPlayer randomly reports
234 				// IllegalStateException. Throw this instance away.
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 		// ignore
319 	}
320 
321 	public void onBufferingUpdate(MediaPlayer mp, int percent) {
322 		getInvocator().buffered((byte) percent);
323 	}
324 }