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.views.gesturelist.keypad;
20  
21  import java.util.Collections;
22  import java.util.List;
23  import java.util.concurrent.Callable;
24  
25  import sk.baka.ambient.AmbientApplication;
26  import sk.baka.ambient.R;
27  import sk.baka.ambient.collection.TrackMetadataBean;
28  import sk.baka.ambient.commons.Interval;
29  import sk.baka.ambient.commons.MiscUtils;
30  import sk.baka.ambient.views.gesturelist.GesturesListView;
31  import sk.baka.ambient.views.gesturelist.TrackListClipboardObject;
32  import android.view.KeyEvent;
33  
34  /***
35   * Controls the {@link GesturesListView} component via the keypad.
36   * 
37   * @author Martin Vysny
38   */
39  public final class KeypadController extends AbstractKeypadHandler {
40  	@Override
41  	public boolean isActivatedByKey(int keyCode, final KeyEvent event) {
42  		return keyCode == KeyEvent.KEYCODE_DPAD_LEFT
43  				|| keyCode == KeyEvent.KEYCODE_DPAD_RIGHT;
44  	}
45  
46  	@Override
47  	public boolean isStarted() {
48  		return mode != Mode.NORMAL || initialItem != -1 || prevKeyCode != -1;
49  	}
50  
51  	@Override
52  	public void start() {
53  		initialItem = -1;
54  		prevKeyCode = -1;
55  		mode = Mode.NORMAL;
56  	}
57  
58  	@Override
59  	public synchronized void stop() {
60  		initialItem = -1;
61  		prevKeyCode = -1;
62  		mode = Mode.NORMAL;
63  		clearMode();
64  	}
65  
66  	/***
67  	 * Creates new controller instance.
68  	 * 
69  	 * @param ownerView
70  	 *            the owning view.
71  	 */
72  	public KeypadController(final GesturesListView ownerView) {
73  		super(ownerView);
74  	}
75  
76  	/***
77  	 * The mode we are currently in.
78  	 * 
79  	 * @author Martin Vysny
80  	 */
81  	private static enum Mode {
82  		/***
83  		 * Normal mode.
84  		 */
85  		NORMAL,
86  		/***
87  		 * Selection mode.Items are selected by pressing the U , D arrow keys.
88  		 * To leave this mode, press Center button. Pressing the Center button
89  		 * copies selected items into the clipboard.
90  		 */
91  		SELECTION,
92  		/***
93  		 * Selected items are moved up/downwards the playlist as the U and D
94  		 * buttons are pressed. To leave this mode, press Center button.
95  		 */
96  		MOVE;
97  	}
98  
99  	/***
100 	 * Current working mode.
101 	 */
102 	private Mode mode = Mode.NORMAL;
103 
104 	/***
105 	 * Previous key event. Serves for remembering gesture combo.
106 	 */
107 	private int prevKeyCode = -1;
108 
109 	/***
110 	 * Initial index of the item when selecting items.
111 	 */
112 	private int initialItem = -1;
113 
114 	private boolean isReadOnly() {
115 		if (!owner.listener.isReadOnly()) {
116 			return false;
117 		}
118 		owner.setTooltip(R.string.cannotModify, this, false);
119 		initialItem = -1;
120 		prevKeyCode = -1;
121 		mode = Mode.NORMAL;
122 		return true;
123 	}
124 
125 	private boolean canHighlight() {
126 		if (owner.listener.canHighlight()) {
127 			return true;
128 		}
129 		owner.setTooltip(R.string.cannotSelect, this, false);
130 		initialItem = -1;
131 		prevKeyCode = -1;
132 		mode = Mode.NORMAL;
133 		return false;
134 	}
135 
136 	@Override
137 	protected boolean onKey(int keyCode, int count, KeyEvent event) {
138 		switch (mode) {
139 		case NORMAL:
140 			return onKeyNormalMode(keyCode);
141 		case SELECTION:
142 			return onKeySelectionMode(keyCode);
143 		case MOVE:
144 			return onKeyMoveMode(keyCode);
145 		}
146 		throw new IllegalStateException();
147 	}
148 
149 	private boolean onKeyMoveMode(int keyCode) {
150 		switch (keyCode) {
151 		case KeyEvent.KEYCODE_DPAD_CENTER:
152 		case KeyEvent.KEYCODE_ENTER:
153 			clearMode();
154 			return true;
155 		case KeyEvent.KEYCODE_DPAD_UP: {
156 			final Interval newInterval = moveItems(false);
157 			owner.getModel().highlight(newInterval);
158 			owner.getModel().notifyModified();
159 			owner.setSelection(newInterval.start);
160 			return true;
161 		}
162 		case KeyEvent.KEYCODE_DPAD_DOWN: {
163 			final Interval newInterval = moveItems(true);
164 			owner.getModel().highlight(newInterval);
165 			owner.getModel().notifyModified();
166 			owner.setSelection(newInterval.end);
167 			return true;
168 		}
169 		case KeyEvent.KEYCODE_BACK:
170 			// cancel
171 			clearMode();
172 			owner.getModel().highlight(null);
173 			owner.getModel().notifyModified();
174 			return true;
175 		}
176 		return false;
177 	}
178 
179 	private boolean onKeySelectionMode(int keyCode) {
180 		// we do not handle UP/DOWN keys - we'll handle the selectionChanged
181 		// event instead. The only button we handle is the Center button,
182 		// which leaves this mode, and the R button, which selects all
183 		// items.
184 		switch (keyCode) {
185 		case KeyEvent.KEYCODE_DPAD_CENTER:
186 		case KeyEvent.KEYCODE_ENTER:
187 			owner.setTooltip(R.string.copyToClipboard, this, false);
188 			copy(owner.getModel().getHighlight(initialItem));
189 			clearMode(false);
190 			return true;
191 		case KeyEvent.KEYCODE_DPAD_RIGHT:
192 			owner.getModel().highlight(owner.getModel().getAllItems());
193 			owner.getModel().notifyModified();
194 			return true;
195 		case KeyEvent.KEYCODE_BACK:
196 			// cancel
197 			clearMode();
198 			owner.getModel().highlight(null);
199 			owner.getModel().notifyModified();
200 			return true;
201 		}
202 		return false;
203 	}
204 
205 	private boolean onKeyNormalMode(final int keyCode) {
206 		final int index = owner.getSelectedItemPosition();
207 		final boolean isEOP = owner.isEOP(index);
208 		switch (keyCode) {
209 		case KeyEvent.KEYCODE_DPAD_CENTER:
210 		case KeyEvent.KEYCODE_ENTER:
211 			boolean clearHint = true;
212 			if (index >= 0) {
213 				if (prevKeyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
214 					// the LC gesture. paste
215 					clearHint = false;
216 					if (!isReadOnly()) {
217 						final List<TrackMetadataBean> tracks = getClipboard();
218 						if (!tracks.isEmpty()) {
219 							owner.setTooltip(R.string.paste, this, false);
220 							owner.listener.dropItems(tracks, -1, -1, index);
221 						} else {
222 							owner.setTooltip(R.string.clipboardEmpty, this, false);
223 						}
224 					}
225 				} else if (prevKeyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
226 					// copy to clipboard
227 					if (owner.listener.canComputeItems()) {
228 						clearHint = false;
229 						owner.setTooltip(R.string.copyToClipboard, this, false);
230 						copy(owner.getModel().getHighlight(initialItem));
231 					}
232 				}
233 			}
234 			clearMode(clearHint);
235 			return true;
236 		case KeyEvent.KEYCODE_DPAD_RIGHT:
237 			if (prevKeyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
238 				// RR gesture - enter selection mode
239 				if (canHighlight()) {
240 					owner.getModel().highlight(owner.getModel().getAllItems());
241 					owner.getModel().notifyModified();
242 					mode = Mode.SELECTION;
243 					prevKeyCode = -1;
244 					owner.setTooltip(R.string.select_some, this, true);
245 				}
246 				return true;
247 			} else if ((prevKeyCode == -1) && (index >= 0)) {
248 				if (canHighlight()) {
249 					prevKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
250 					owner.setTooltip(R.string.selection, this, true);
251 					owner.getModel().highlight(Interval.fromItem(index));
252 					owner.getModel().notifyModified();
253 					initialItem = index;
254 				}
255 				return true;
256 			}
257 			break;
258 		case KeyEvent.KEYCODE_DPAD_DOWN:
259 			if (prevKeyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
260 				// RD - enter selection mode
261 				if (canHighlight()) {
262 					owner.setTooltip(R.string.select_some, this, true);
263 					mode = Mode.SELECTION;
264 					prevKeyCode = -1;
265 				}
266 				// fall back to the original implementation, to fire
267 				// selectionChanged event
268 				return false;
269 			} else if (prevKeyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
270 				// LD - move down
271 				if (!isReadOnly()) {
272 					enterItemMoveMode(isEOP, true);
273 				}
274 				return false;
275 			}
276 			break;
277 		case KeyEvent.KEYCODE_DPAD_UP:
278 			if (prevKeyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
279 				// RU - clear selection
280 				owner.setTooltip(R.string.deselect_all, KeypadController.this,
281 						false);
282 				owner.getModel().highlight(Interval.EMPTY);
283 				owner.getModel().notifyModified();
284 				clearMode(false);
285 				return true;
286 			} else if (prevKeyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
287 				// LU - move up
288 				if (!isReadOnly()) {
289 					enterItemMoveMode(isEOP, false);
290 				}
291 				return false;
292 			}
293 			break;
294 		case KeyEvent.KEYCODE_DPAD_LEFT:
295 			if (prevKeyCode == -1) {
296 				prevKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
297 				owner.setTooltip(owner.hintDeleteMovePasteId, this, true);
298 				return true;
299 			} else if (prevKeyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
300 				// LL - delete highlight
301 				owner.setTooltip(owner.hintDeleteId, this, false);
302 				owner.listener.removeItems(owner.getModel().getHighlight(true));
303 				if (!owner.getHighlight().isEmpty()) {
304 					owner.getModel().highlight(Interval.EMPTY);
305 					owner.getModel().notifyModified();
306 				}
307 				clearMode(false);
308 				return true;
309 			}
310 			break;
311 		case KeyEvent.KEYCODE_BACK:
312 			// cancel
313 			clearMode();
314 			owner.getModel().highlight(null);
315 			owner.getModel().notifyModified();
316 			return true;
317 		default:
318 			clearMode();
319 		}
320 		return false;
321 	}
322 
323 	private Interval enterItemMoveMode(final boolean isEOP, final boolean down) {
324 		if (!owner.canMove() || isEOP) {
325 			clearMode(true);
326 			return owner.getHighlight();
327 		}
328 		mode = Mode.MOVE;
329 		owner.setTooltip(R.string.move, this, true);
330 		final Interval result = moveItems(down);
331 		owner.getModel().notifyModified();
332 		return result;
333 	}
334 
335 	private Interval moveItems(final boolean down) {
336 		return owner.listener.moveItemsByOne(owner.getModel()
337 				.getHighlight(true), down);
338 	}
339 
340 	private void clearMode() {
341 		clearMode(true);
342 	}
343 
344 	private void clearMode(final boolean clearModeHint) {
345 		if (clearModeHint) {
346 			owner.clearTooltip(this);
347 		}
348 		initialItem = -1;
349 		prevKeyCode = -1;
350 		mode = Mode.NORMAL;
351 	}
352 
353 	@Override
354 	public void selectionChanged() {
355 		switch (mode) {
356 		case NORMAL:
357 			// do nothing
358 			return;
359 		case SELECTION:
360 			final int select = owner.getSelectedItemPosition();
361 			if (select >= 0) {
362 				owner.getModel().highlight(
363 						Interval.fromRange(select, initialItem));
364 				owner.getModel().notifyModified();
365 			}
366 			return;
367 		case MOVE:
368 			return;
369 		}
370 	}
371 
372 	/***
373 	 * Copies given interval into the clipboard. The operation does not block -
374 	 * it may retrieve the tracks in the background
375 	 * 
376 	 * @param i
377 	 *            the interval to copy.
378 	 */
379 	public void copy(final Interval i) {
380 		final boolean isLongOp = owner.listener.isComputeTracksLong(i);
381 		final boolean isOnlineOp = owner.listener.isComputeTracksOnlineOp(i);
382 		final Callable<Void> copyOp = new Callable<Void>() {
383 			/***
384 			 * Retrieved tracks.
385 			 */
386 			private volatile List<TrackMetadataBean> tracks = null;
387 
388 			public Void call() {
389 				if (tracks == null) {
390 					tracks = owner.listener.computeTracks(i);
391 					if (Thread.currentThread().isInterrupted()) {
392 						return null;
393 					}
394 					if ((tracks == null) || tracks.isEmpty()) {
395 						return null;
396 					}
397 					// prepare a thread-safe list
398 					tracks = Collections.synchronizedList(tracks);
399 					AmbientApplication.getHandler().post(
400 							MiscUtils.toRunnable(this));
401 				} else {
402 					if (owner.dragDropViews.isEmpty()) {
403 						return null;
404 					}
405 					final TrackListClipboardObject obj = new TrackListClipboardObject(
406 							tracks, owner.dragDropViews);
407 					owner.listener.setClipboard(obj);
408 				}
409 				return null;
410 			}
411 		};
412 		if (isLongOp) {
413 			AmbientApplication.getInstance().getBackgroundTasks()
414 					.schedule(
415 							copyOp,
416 							GesturesListView.class,
417 							isOnlineOp,
418 							owner.getResources().getString(
419 									R.string.copyingToClipboard));
420 		} else {
421 			try {
422 				copyOp.call();
423 			} catch (final Exception ex) {
424 				if (!Thread.currentThread().isInterrupted()) {
425 					final AmbientApplication app = AmbientApplication
426 							.getInstance();
427 					app.error(KeypadController.class, true, app
428 							.getString(R.string.error), ex);
429 				}
430 			}
431 		}
432 	}
433 
434 	/***
435 	 * Returns the contents of the clipboard as a list of tracks.
436 	 * 
437 	 * @return list of tracks, never <code>null</code>, may be empty.
438 	 */
439 	public List<TrackMetadataBean> getClipboard() {
440 		final TrackListClipboardObject obj = owner.getClipboard();
441 		if (obj == null) {
442 			return Collections.emptyList();
443 		}
444 		if (!obj.isTarget(owner)) {
445 			return Collections.emptyList();
446 		}
447 		return obj.getObjects();
448 	}
449 }