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  
19  package sk.baka.ambient;
20  
21  import java.text.ParseException;
22  
23  import sk.baka.ambient.activity.ErrorActivity;
24  import sk.baka.ambient.activity.main.MainActivity;
25  import sk.baka.ambient.collection.TrackMetadataBean;
26  import sk.baka.ambient.collection.TrackOriginEnum;
27  import sk.baka.ambient.commons.Interval;
28  import sk.baka.ambient.commons.MiscUtils;
29  import sk.baka.ambient.commons.TagFormatter;
30  import sk.baka.ambient.playerservice.IPlayerListener;
31  import sk.baka.ambient.playerservice.PlayerStateEnum;
32  import sk.baka.ambient.playlist.PlaylistItem;
33  import sk.baka.ambient.playlist.Random;
34  import sk.baka.ambient.playlist.Repeat;
35  import android.app.Notification;
36  import android.app.NotificationManager;
37  import android.app.PendingIntent;
38  import android.content.Context;
39  import android.content.Intent;
40  import android.graphics.Typeface;
41  import android.text.SpannableStringBuilder;
42  import android.text.Spanned;
43  import android.text.style.StyleSpan;
44  import android.widget.Toast;
45  
46  /***
47   * Controls the notification shown when a track is being played.
48   * 
49   * @author Martin Vysny
50   */
51  public final class PlaybackNotificator {
52  	/***
53  	 * The ID of the notification.
54  	 */
55  	public static final int NOTIFICATION_ID = 23847;
56  
57  	/***
58  	 * The ID of the error notification.
59  	 */
60  	public static final int ERROR_NOTIFICATION_ID = 23848;
61  
62  	private final AmbientApplication app;
63  
64  	/***
65  	 * Creates new notification controller.
66  	 * 
67  	 * @param app
68  	 *            the application instance.
69  	 */
70  	PlaybackNotificator(final AmbientApplication app) {
71  		super();
72  		this.app = app;
73  		app.getBus().addHandler(listener);
74  		final boolean isPlaying = AmbientApplication.getInstance()
75  				.getPlaylist().getPlaybackState() == PlayerStateEnum.Playing;
76  		final TrackMetadataBean track = AmbientApplication.getInstance()
77  				.getPlaylist().getCurrentlyPlayingTrack();
78  		if ((track != null) && isPlaying) {
79  			createNotification(track);
80  		}
81  	}
82  
83  	private final Listener listener = new Listener();
84  
85  	private void createNotification(final TrackMetadataBean track) {
86  		if (app.isShutdown()) {
87  			cancelNotifications();
88  			return;
89  		}
90  		final NotificationManager nm = (NotificationManager) app
91  				.getSystemService(Context.NOTIFICATION_SERVICE);
92  		final SpannableStringBuilder text = new SpannableStringBuilder();
93  		if (!MiscUtils.isEmptyOrWhitespace(track.getAlbum())) {
94  			text.append(track.getAlbum());
95  		}
96  		if (!MiscUtils.isEmptyOrWhitespace(track.getArtist())) {
97  			if (text.length() != 0) {
98  				text.append(", ");
99  				text.setSpan(new StyleSpan(Typeface.ITALIC), text.length() - 2,
100 						text.length(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
101 				text.append(track.getArtist());
102 			}
103 		}
104 		final Notification notification = new Notification(R.drawable.icon,
105 				null, 0);
106 		final PendingIntent contentIntent = PendingIntent.getActivity(app, 0,
107 				new Intent(app, MainActivity.class), 0);
108 		final StringBuilder trackName = new StringBuilder(track.getDisplayableName());
109 		if (track.getLength() > 0) {
110 			trackName.append(" (");
111 			track.appendDisplayableLength(trackName, false);
112 			trackName.append(')');
113 		}
114 		notification.setLatestEventInfo(app, trackName, text,
115 				contentIntent);
116 		notification.flags = Notification.FLAG_NO_CLEAR
117 				| Notification.FLAG_ONGOING_EVENT;
118 		nm.notify(NOTIFICATION_ID, notification);
119 	}
120 
121 	/***
122 	 * Cancels all notifications.
123 	 */
124 	public void cancelNotifications() {
125 		final NotificationManager nm = (NotificationManager) app
126 				.getSystemService(Context.NOTIFICATION_SERVICE);
127 		nm.cancel(NOTIFICATION_ID);
128 		nm.cancel(ERROR_NOTIFICATION_ID);
129 	}
130 
131 	/***
132 	 * Cancels the playback notification.
133 	 */
134 	public void cancelPlaybackNotification() {
135 		final NotificationManager nm = (NotificationManager) app
136 				.getSystemService(Context.NOTIFICATION_SERVICE);
137 		nm.cancel(NOTIFICATION_ID);
138 	}
139 
140 	private final class Listener implements IPlaylistPlayerListener,
141 			IPlayerListener {
142 
143 		public void playbackStateChanged(PlayerStateEnum state) {
144 			// handle the Android Notification
145 			if (state != PlayerStateEnum.Playing) {
146 				cancelPlaybackNotification();
147 			} else {
148 				final TrackMetadataBean track = AmbientApplication
149 						.getInstance().getPlaylist().getCurrentlyPlayingTrack();
150 				if (track != null) {
151 					createNotification(track);
152 				}
153 			}
154 		}
155 
156 		public void playlistChanged(Interval target) {
157 			// ignore
158 		}
159 
160 		public void randomChanged(Random random) {
161 			// ignore
162 		}
163 
164 		public void repeatChanged(Repeat repeat) {
165 			// ignore
166 		}
167 
168 		public void trackChanged(PlaylistItem track, boolean play,
169 				int positionMillis) {
170 			// update the Android Notification
171 			final boolean isPlaying = AmbientApplication.getInstance()
172 					.getPlaylist().getPlaybackState() == PlayerStateEnum.Playing;
173 			if ((track != null) && isPlaying) {
174 				createNotification(track.getTrack());
175 			}
176 			// notify the user with the Toast
177 			if (track == null) {
178 				return;
179 			}
180 			if (!play) {
181 				return;
182 			}
183 			if (app.getStateHandler().getConfig().notifyWhenMinimizedOnly
184 					&& app.isMainActivityVisible()) {
185 				return;
186 			}
187 			notifyOnNextTrack(track.getTrack());
188 		}
189 
190 		public void trackPositionChanged(int position, boolean playing) {
191 			// ignore
192 		}
193 
194 		public void buffered(byte percent) {
195 			// ignore
196 		}
197 
198 		public void radioNewTrack(String name) {
199 			// update the Android Notification
200 			final TrackMetadataBean.Builder b = new TrackMetadataBean.Builder();
201 			createNotification(b.setOrigin(TrackOriginEnum.Shoutcast)
202 					.setLocation("Shoutcast radio").setTitle(name).build(-1));
203 			// notify the user with the Toast
204 			final TrackMetadataBean bean = TrackMetadataBean.newBuilder()
205 					.setLocation(name).setTitle(name).setOrigin(
206 							TrackOriginEnum.Shoutcast).build(-1);
207 			notifyOnNextTrack(bean);
208 
209 		}
210 
211 		public void started(String file, int duration, int currentPosition) {
212 			// ignore this low-level event, we are primarily capturing
213 			// higher-level
214 			// events
215 		}
216 
217 		public void stopped(String error, boolean errorMissing,
218 				TrackOriginEnum origin) {
219 			// ignore this low-level event, we are primarily capturing
220 			// higher-level
221 			// events
222 		}
223 	}
224 
225 	/***
226 	 * Shows a notification that
227 	 * 
228 	 * @param bean
229 	 *            the bean to format
230 	 */
231 	private synchronized void notifyOnNextTrack(final TrackMetadataBean bean) {
232 		notifyOnNextTrack(bean, app.getStateHandler().getConfig());
233 	}
234 
235 	/***
236 	 * Forces the application to show a notification based on given
237 	 * configuration.
238 	 * 
239 	 * @param bean
240 	 *            the bean to format
241 	 * @param config
242 	 *            the configuration to use
243 	 */
244 	public synchronized void notifyOnNextTrack(final TrackMetadataBean bean,
245 			final ConfigurationBean config) {
246 		if (config.notificationType == ConfigurationBean.TrackChangeNotifEnum.None) {
247 			return;
248 		}
249 		// format the notification message
250 		final boolean shortNotif = config.notificationType == ConfigurationBean.TrackChangeNotifEnum.Short;
251 		final String format = shortNotif ? config.shortNotificationFormat
252 				: config.longNotificationFormat;
253 		try {
254 			if ((notificationFormatter == null)
255 					|| !notificationFormatter.formatString.equals(format)) {
256 				notificationFormatter = new TagFormatter(format);
257 			}
258 		} catch (ParseException e) {
259 			throw new RuntimeException(e);
260 		}
261 		final String formatted = notificationFormatter.format(bean);
262 		// notify
263 		final Toast toast = Toast.makeText(app, formatted,
264 				config.notificationType.viewDuration);
265 		toast.setGravity(config.notificationLocation.gravity, 0, 0);
266 		toast.show();
267 	}
268 
269 	/***
270 	 * Cached notification formatter instance.
271 	 */
272 	private TagFormatter notificationFormatter;
273 
274 	/***
275 	 * Destroys the notificator, removing it from the bus.
276 	 */
277 	void destroy() {
278 		app.getBus().removeHandler(listener);
279 	}
280 
281 	/***
282 	 * Updates the error notification.
283 	 * 
284 	 * @param errorCount
285 	 *            the error/warning count. If zero the notification is hidden.
286 	 * @param last
287 	 *            last error/warning or <code>null</code> if no errors are
288 	 *            present.
289 	 */
290 	void updateErrorNotification(final int errorCount,
291 			final ErrorHandler.ErrorInfo last) {
292 		final NotificationManager nm = (NotificationManager) app
293 				.getSystemService(Context.NOTIFICATION_SERVICE);
294 		if (errorCount <= 0) {
295 			nm.cancel(ERROR_NOTIFICATION_ID);
296 			return;
297 		}
298 		final Notification notification = new Notification(R.drawable.quit, null,
299 				last.timestamp);
300 		final PendingIntent contentIntent = PendingIntent.getActivity(app, 0,
301 				new Intent(app, ErrorActivity.class), 0);
302 		notification.setLatestEventInfo(app, app
303 				.getString(R.string.ambientError), last.message, contentIntent);
304 		notification.number = errorCount;
305 		nm.notify(ERROR_NOTIFICATION_ID, notification);
306 	}
307 }