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
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
181
182
183
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
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
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
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
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
261 if (canHighlight()) {
262 owner.setTooltip(R.string.select_some, this, true);
263 mode = Mode.SELECTION;
264 prevKeyCode = -1;
265 }
266
267
268 return false;
269 } else if (prevKeyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
270
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
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
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
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
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
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
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 }