PageRenderTime 109ms CodeModel.GetById 34ms app.highlight 68ms RepoModel.GetById 1ms app.codeStats 0ms

/shell/src/com/google/marvin/shell/AppChooserView.java

http://eyes-free.googlecode.com/
Java | 966 lines | 801 code | 103 blank | 62 comment | 137 complexity | 92673ea6f3dd5e2a0b8d2f93416609c2 MD5 | raw file
  1/*
  2 * Copyright (C) 2008 Google Inc.
  3 *
  4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5 * use this file except in compliance with the License. You may obtain a copy of
  6 * the License at
  7 *
  8 * http://www.apache.org/licenses/LICENSE-2.0
  9 *
 10 * Unless required by applicable law or agreed to in writing, software
 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 13 * License for the specific language governing permissions and limitations under
 14 * the License.
 15 */
 16
 17package com.google.marvin.shell;
 18
 19import android.content.ActivityNotFoundException;
 20import android.content.Context;
 21import android.content.Intent;
 22import android.graphics.Canvas;
 23import android.graphics.Color;
 24import android.graphics.Paint;
 25import android.graphics.Typeface;
 26import android.net.Uri;
 27import android.speech.tts.TextToSpeech;
 28import android.util.AttributeSet;
 29import android.view.KeyCharacterMap;
 30import android.view.KeyEvent;
 31import android.view.MotionEvent;
 32import android.view.View;
 33import android.view.accessibility.AccessibilityManager;
 34import android.widget.TextView;
 35
 36import com.google.marvin.shell.ShakeDetector.ShakeListener;
 37import com.googlecode.eyesfree.utils.FeedbackController;
 38import com.googlecode.eyesfree.utils.compat.MotionEventCompatUtils;
 39
 40import java.lang.reflect.InvocationTargetException;
 41import java.lang.reflect.Method;
 42import java.util.ArrayList;
 43import java.util.Collections;
 44
 45/**
 46 * Allows the user to select the application they wish to start by moving
 47 * through the list of applications.
 48 *
 49 * @author clchen@google.com (Charles L. Chen)
 50 */
 51public class AppChooserView extends TextView {
 52    // BEGIN OF WORKAROUND FOR BACKWARDS COMPATIBILITY (up to Donut)
 53    private static Method MotionEvent_getX;
 54
 55    private static Method MotionEvent_getY;
 56
 57    private static Method AccessibilityManager_isTouchExplorationEnabled;
 58
 59    static {
 60        initCompatibility();
 61    }
 62
 63    private static void initCompatibility() {
 64        try {
 65            MotionEvent_getX = MotionEvent.class.getMethod("getX", new Class[] { Integer.TYPE });
 66            MotionEvent_getY = MotionEvent.class.getMethod("getY", new Class[] { Integer.TYPE });
 67            AccessibilityManager_isTouchExplorationEnabled = AccessibilityManager.class.getMethod(
 68                    "isTouchExplorationEnabled");
 69            /* success, this is a newer device */
 70        } catch (NoSuchMethodException nsme) {
 71            /* failure, must be older device */
 72        }
 73    }
 74
 75    private static float getX(MotionEvent event, int x) {
 76        try {
 77            Object retobj = MotionEvent_getX.invoke(event, x);
 78            return (Float) retobj;
 79        } catch (IllegalAccessException ie) {
 80            System.err.println("unexpected " + ie);
 81        } catch (IllegalArgumentException e) {
 82            // TODO Auto-generated catch block
 83            e.printStackTrace();
 84        } catch (InvocationTargetException e) {
 85            // TODO Auto-generated catch block
 86            e.printStackTrace();
 87        }
 88        return -1;
 89    }
 90
 91    private static float getY(MotionEvent event, int y) {
 92        try {
 93            Object retobj = MotionEvent_getY.invoke(event, y);
 94            return (Float) retobj;
 95        } catch (IllegalAccessException ie) {
 96            System.err.println("unexpected " + ie);
 97        } catch (IllegalArgumentException e) {
 98            // TODO Auto-generated catch block
 99            e.printStackTrace();
100        } catch (InvocationTargetException e) {
101            // TODO Auto-generated catch block
102            e.printStackTrace();
103        }
104        return -1;
105    }
106
107    private static boolean isTouchExplorationEnabled(AccessibilityManager am) {
108        try {
109            if (AccessibilityManager_isTouchExplorationEnabled != null) {
110                Object retobj = AccessibilityManager_isTouchExplorationEnabled.invoke(am);
111                return (Boolean) retobj;
112            }
113        } catch (IllegalAccessException ie) {
114            System.err.println("unexpected " + ie);
115        } catch (IllegalArgumentException e) {
116            // TODO Auto-generated catch block
117            e.printStackTrace();
118        } catch (InvocationTargetException e) {
119            // TODO Auto-generated catch block
120            e.printStackTrace();
121        }
122        return false;
123    }
124
125    // END OF WORKAROUND FOR DONUT COMPATIBILITY
126
127    private static final char alphabet[] = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
128            'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };
129
130    private static final int AE = 0;
131
132    private static final int IM = 1;
133
134    private static final int QU = 2;
135
136    private static final int Y = 4;
137
138    private static final int NONE = 5;
139
140    private final double left = 0;
141
142    private final double upleft = Math.PI * .25;
143
144    private final double up = Math.PI * .5;
145
146    private final double upright = Math.PI * .75;
147
148    private final double downright = -Math.PI * .75;
149
150    private final double down = -Math.PI * .5;
151
152    private final double downleft = -Math.PI * .25;
153
154    private final double right = Math.PI;
155
156    private final double rightWrap = -Math.PI;
157
158    private MarvinShell parent;
159
160    private ArrayList<AppInfo> appList;
161
162    private int appListIndex;
163
164    private double downX;
165
166    private double downY;
167
168    private double lastX;
169
170    private double lastY;
171
172    private int currentValue;
173
174    private boolean screenIsBeingTouched;
175
176    private boolean gestureHadSecondPoint;
177
178    private int currentWheel;
179
180    private String currentCharacter;
181
182    private String currentString;
183
184    private int trackballTimeout = 500;
185
186    private boolean trackballEnabled = true;
187
188    private boolean inDPadMode = false;
189
190    private float p2DownX;
191
192    private float p2DownY;
193
194    private ShakeDetector shakeDetector;
195
196    private long backKeyTimeDown;
197
198    private long lastTapTime = 0;
199
200    private long doubleTapWindow = 700;
201
202    private AccessibilityManager accessibilityManager;
203
204    private FeedbackController feedbackController;
205
206    // Change this to true to enable shake to erase
207    private static final boolean useShake = false;
208
209    public AppChooserView(Context context, AttributeSet attrs) {
210        super(context, attrs);
211
212        parent = ((MarvinShell) context);
213        feedbackController = FeedbackController.getInstance(context);
214        accessibilityManager = (AccessibilityManager) context.getSystemService(
215                Context.ACCESSIBILITY_SERVICE);
216
217        // Build app list here
218        appListIndex = 0;
219        currentString = "";
220
221        setClickable(true);
222        setFocusable(true);
223        setFocusableInTouchMode(true);
224        requestFocus();
225    }
226
227    public void setAppList(ArrayList<AppInfo> installedApps) {
228        appList = installedApps;
229    }
230
231    @Override
232    public boolean onTrackballEvent(MotionEvent event) {
233        if (trackballEnabled == false) {
234            return true;
235        }
236        // Log.i("Motion", Float.toString(event.getY()));
237        if (event.getAction() == MotionEvent.ACTION_DOWN) {
238            startActionHandler();
239            return true;
240        } else if (event.getY() > .16) {
241            trackballEnabled = false;
242            (new Thread(new trackballTimeout())).start();
243            nextApp();
244            currentString = "";
245        } else if (event.getY() < -.16) {
246            trackballEnabled = false;
247            (new Thread(new trackballTimeout())).start();
248            prevApp();
249            currentString = "";
250        }
251        return true;
252    }
253
254    class trackballTimeout implements Runnable {
255        @Override
256        public void run() {
257            try {
258                Thread.sleep(trackballTimeout);
259                trackballEnabled = true;
260            } catch (InterruptedException e) {
261                e.printStackTrace();
262            }
263        }
264    }
265
266    public void nextApp() {
267        appListIndex++;
268        if (appListIndex >= appList.size()) {
269            appListIndex = 0;
270        }
271        feedbackController.playVibration(R.array.pattern_app_chooser);
272        speakCurrentApp(true);
273    }
274
275    public void prevApp() {
276        appListIndex--;
277        if (appListIndex < 0) {
278            appListIndex = appList.size() - 1;
279        }
280        feedbackController.playVibration(R.array.pattern_app_chooser);
281        speakCurrentApp(true);
282    }
283
284    private void jumpToFirstMatchingApp() {
285        int index = 0;
286        boolean foundMatch = false;
287        while (!foundMatch && (index < appList.size())) {
288            String title = appList.get(index).getTitle().toLowerCase();
289            if (title.startsWith(currentString.toLowerCase())) {
290                appListIndex = index - 1;
291                foundMatch = true;
292            }
293            index++;
294        }
295        if (!foundMatch) {
296            parent.tts.playEarcon(parent.getString(R.string.earcon_tock), 0, null);
297            if (currentString.length() > 0) {
298                currentString = currentString.substring(0, currentString.length() - 1);
299            }
300            speakCurrentApp(true);
301            return;
302        } else {
303            nextApp();
304        }
305    }
306
307    public void resetListState() {
308        appListIndex = 0;
309        currentString = "";
310    }
311
312    public void speakCurrentApp(boolean interrupt) {
313        String name = appList.get(appListIndex).getTitle();
314        if (interrupt) {
315            parent.tts.speak(name, TextToSpeech.QUEUE_FLUSH, null);
316        } else {
317            parent.tts.speak(name, TextToSpeech.QUEUE_ADD, null);
318        }
319        invalidate();
320    }
321
322    public void addApplication(AppInfo app) {
323        synchronized (appList) {
324            appList.add(app);
325            Collections.sort(appList);
326        }
327        appListIndex = 0;
328        currentString = "";
329    }
330
331    public void removePackage(String packageName) {
332        synchronized (appList) {
333            for (int i = 0; i < appList.size(); ++i) {
334                if (appList.get(i).getPackageName().equals(packageName)) {
335                    appList.remove(i);
336                    i--;
337                }
338            }
339        }
340        appListIndex = 0;
341        currentString = "";
342    }
343
344    public boolean applicationExists(AppInfo app) {
345        return appList.contains(app);
346    }
347
348    public void startActionHandler() {
349        currentString = "";
350        // Launch app here
351        parent.onAppSelected(appList.get(appListIndex));
352    }
353
354    @Override
355    public boolean onKeyDown(int keyCode, KeyEvent event) {
356        String input = "";
357        switch (keyCode) {
358            case KeyEvent.KEYCODE_MENU:
359                return false;
360            case KeyEvent.KEYCODE_DPAD_DOWN:
361                nextApp();
362                currentString = "";
363                return true;
364            case KeyEvent.KEYCODE_DPAD_UP:
365                prevApp();
366                currentString = "";
367                return true;
368            case KeyEvent.KEYCODE_DPAD_CENTER:
369            case KeyEvent.KEYCODE_ENTER:
370                startActionHandler();
371                return true;
372            case KeyEvent.KEYCODE_SEARCH:
373                startActionHandler();
374                return true;
375            case KeyEvent.KEYCODE_CALL:
376                startActionHandler();
377                return true;
378            case KeyEvent.KEYCODE_BACK:
379                if (backKeyTimeDown == -1) {
380                    backKeyTimeDown = System.currentTimeMillis();
381                    class QuitCommandWatcher implements Runnable {
382                        @Override
383                        public void run() {
384                            try {
385                                Thread.sleep(3000);
386                                if ((backKeyTimeDown > 0)
387                                        && (System.currentTimeMillis() - backKeyTimeDown > 2500)) {
388                                    parent.startActivity(parent.getSystemHomeIntent());
389                                    parent.finish();
390                                }
391                            } catch (InterruptedException e) {
392                                e.printStackTrace();
393                            }
394                        }
395                    }
396                    new Thread(new QuitCommandWatcher()).start();
397                }
398                return true;
399            case KeyEvent.KEYCODE_DEL:
400                backspace();
401                return true;
402            default:
403                KeyCharacterMap keyMap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
404                input = keyMap.getMatch(keyCode, alphabet) + "";
405        }
406        if (input.length() > 0) {
407            currentString = currentString + input;
408            jumpToFirstMatchingApp();
409        }
410        return false;
411    }
412
413    @Override
414    public boolean onKeyUp(int keyCode, KeyEvent event) {
415        switch (keyCode) {
416            case KeyEvent.KEYCODE_BACK:
417                backKeyTimeDown = -1;
418                parent.switchToMainView();
419                return true;
420        }
421        return false;
422    }
423
424    private void confirmEntry() {
425        screenIsBeingTouched = false;
426        int prevVal = currentValue;
427        currentValue = evalMotion(lastX, lastY);
428        // Do some correction if the user lifts up on deadspace
429        if (currentValue == -1) {
430            currentValue = prevVal;
431        }
432        // The user never got a number that wasn't deadspace,
433        // so assume 5.
434        if (currentValue == -1) {
435            currentValue = 5;
436        }
437        currentCharacter = getCharacter(currentWheel, currentValue);
438        if (currentCharacter.equals("<-")) {
439            currentCharacter = "";
440            backspace();
441        } else if (currentCharacter.equals("TAP")) {
442            if (lastTapTime + doubleTapWindow > System.currentTimeMillis()) {
443                startActionHandler();
444                return;
445            } else {
446                lastTapTime = System.currentTimeMillis();
447            }
448        } else {
449            if (currentCharacter.equals("SPACE")) {
450                currentString = currentString + " ";
451            }
452            /*
453             * else if (currentCharacter.equals("MODE")) { // Do nothing }
454             */
455            else {
456                currentString = currentString + currentCharacter;
457            }
458            parent.tts.speak(currentCharacter, 0, null);
459            jumpToFirstMatchingApp();
460        }
461        invalidate();
462        initiateMotion(lastX, lastY);
463    }
464
465    private void initiateMotion(double x, double y) {
466        downX = x;
467        downY = y;
468        lastX = x;
469        lastY = y;
470        currentValue = -1;
471        currentWheel = NONE;
472        currentCharacter = "";
473    }
474
475    public boolean onHoverEvent(MotionEvent event) {
476        return onTouchEvent(event);
477    }
478
479    @Override
480    public boolean onTouchEvent(MotionEvent event) {
481        boolean touchExploring = isTouchExplorationEnabled(accessibilityManager);
482
483        int action = event.getAction();
484        float x = event.getX();
485        float y = event.getY();
486
487        int secondFingerId = 1;
488        if (touchExploring) {
489            secondFingerId = 0;
490        }
491
492        // Treat the screen as a dpad
493        if ((action == 261) || (touchExploring && action == MotionEvent.ACTION_DOWN)) {
494            // 261 == ACTION_POINTER_2_DOWN - using number for Android 1.6
495            // compat
496            // Track the starting location of the second touch point.
497            inDPadMode = false;
498            gestureHadSecondPoint = true;
499            screenIsBeingTouched = false;
500            currentWheel = NONE;
501            p2DownX = getX(event, secondFingerId);
502            p2DownY = getY(event, secondFingerId);
503            feedbackController.playVibration(R.array.pattern_app_chooser);
504            invalidate();
505        } else if ((action == 262) || (touchExploring && action == MotionEvent.ACTION_UP)) {
506            // 262 == MotionEvent.ACTION_POINTER_2_UP - using number for Android
507            // 1.6 compat
508            // Second touch point has lifted, so process the gesture.
509            float p2DeltaX = getX(event, secondFingerId) - p2DownX;
510            float p2DeltaY = getY(event, secondFingerId) - p2DownY;
511            if (Math.abs(p2DeltaX) > Math.abs(p2DeltaY)) {
512                if (p2DeltaX < -200) {
513                    backspace();
514                }
515            } else {
516                if (p2DeltaY < -100) {
517                    prevApp();
518                } else if (p2DeltaY > 100) {
519                    nextApp();
520                }
521            }
522        }
523
524        if ((action == MotionEvent.ACTION_DOWN)
525                || (action == MotionEventCompatUtils.ACTION_HOVER_ENTER)) {
526            initiateMotion(x, y);
527            return true;
528        } else if ((action == MotionEvent.ACTION_UP)
529                || (action == MotionEventCompatUtils.ACTION_HOVER_EXIT)) {
530            if (gestureHadSecondPoint == false) {
531                if (inDPadMode == false) {
532                    confirmEntry();
533                } else {
534                    inDPadMode = false;
535                }
536            } else {
537                // Full multi-touch gesture completed, so reset the state.
538                gestureHadSecondPoint = false;
539            }
540            return true;
541        } else {
542            if (gestureHadSecondPoint == false) {
543                screenIsBeingTouched = true;
544                lastX = x;
545                lastY = y;
546                int prevVal = currentValue;
547                currentValue = evalMotion(x, y);
548                // Do nothing since we want a deadzone here;
549                // restore the state to the previous value.
550                if (currentValue == -1) {
551                    currentValue = prevVal;
552                    return true;
553                }
554                // There is a wheel that is active
555                if (currentValue != 5) {
556                    if (currentWheel == NONE) {
557                        currentWheel = getWheel(currentValue);
558                    }
559                    currentCharacter = getCharacter(currentWheel, currentValue);
560                } else {
561                    currentCharacter = "";
562                }
563                invalidate();
564                if (prevVal != currentValue) {
565                    if (currentCharacter.equals("")) {
566                        parent.tts.playEarcon(parent.getString(R.string.earcon_tock), 0, null);
567                    } else {
568                        if (currentCharacter.equals(".")) {
569                            parent.tts.speak(parent.getString(R.string.period), 0, null);
570                        } else if (currentCharacter.equals("!")) {
571                            parent.tts.speak(parent.getString(R.string.exclamation_point), 0, null);
572                        } else if (currentCharacter.equals("?")) {
573                            parent.tts.speak(parent.getString(R.string.question_mark), 0, null);
574                        } else if (currentCharacter.equals(",")) {
575                            parent.tts.speak(parent.getString(R.string.comma), 0, null);
576                        } else if (currentCharacter.equals("<-")) {
577                            parent.tts.speak(parent.getString(R.string.backspace), 0, null);
578                        } else {
579                            parent.tts.speak(currentCharacter, 0, null);
580                        }
581                    }
582                }
583                feedbackController.playVibration(R.array.pattern_app_chooser);
584            }
585        }
586        return true;
587    }
588
589    public int getWheel(int value) {
590        switch (value) {
591            case 1:
592                return AE;
593            case 2:
594                return IM;
595            case 3:
596                return QU;
597            case 4:
598                return Y;
599            case 5:
600                return NONE;
601            case 6:
602                return Y;
603            case 7:
604                return QU;
605            case 8:
606                return IM;
607            case 9:
608                return AE;
609            default:
610                return NONE;
611        }
612    }
613
614    public String getCharacter(int wheel, int value) {
615        switch (wheel) {
616            case AE:
617                switch (value) {
618                    case 1:
619                        return "A";
620                    case 2:
621                        return "B";
622                    case 3:
623                        return "C";
624                    case 4:
625                        return "H";
626                    case 5:
627                        return "";
628                    case 6:
629                        return "D";
630                    case 7:
631                        return "G";
632                    case 8:
633                        return "F";
634                    case 9:
635                        return "E";
636                    default:
637                        return "";
638                }
639            case IM:
640                switch (value) {
641                    case 1:
642                        return "P";
643                    case 2:
644                        return "I";
645                    case 3:
646                        return "J";
647                    case 4:
648                        return "O";
649                    case 5:
650                        return "";
651                    case 6:
652                        return "K";
653                    case 7:
654                        return "N";
655                    case 8:
656                        return "M";
657                    case 9:
658                        return "L";
659                    default:
660                        return "";
661                }
662            case QU:
663                switch (value) {
664                    case 1:
665                        return "W";
666                    case 2:
667                        return "X";
668                    case 3:
669                        return "Q";
670                    case 4:
671                        return "V";
672                    case 5:
673                        return "";
674                    case 6:
675                        return "R";
676                    case 7:
677                        return "U";
678                    case 8:
679                        return "T";
680                    case 9:
681                        return "S";
682                    default:
683                        return "";
684                }
685            case Y:
686                switch (value) {
687                    case 1:
688                        return ",";
689                    case 2:
690                        return "!";
691                    case 3:
692                        return "SPACE"; // return "MODE";
693                    case 4:
694                        return "<-";
695                    case 5:
696                        return "";
697                    case 6:
698                        return "Y";
699                    case 7:
700                        return ".";
701                    case 8:
702                        return "?";
703                    case 9:
704                        return "Z";
705                    default:
706                        return "";
707                }
708            default:
709                return "TAP";
710        }
711    }
712
713    public int evalMotion(double x, double y) {
714        float rTolerance = 25;
715        double thetaTolerance = (Math.PI / 16);
716
717        double r = Math.sqrt(((downX - x) * (downX - x)) + ((downY - y) * (downY - y)));
718
719        if (r < rTolerance) {
720            return 5;
721        }
722
723        double theta = Math.atan2(downY - y, downX - x);
724
725        if (Math.abs(theta - left) < thetaTolerance) {
726            return 4;
727        } else if (Math.abs(theta - upleft) < thetaTolerance) {
728            return 1;
729        } else if (Math.abs(theta - up) < thetaTolerance) {
730            return 2;
731        } else if (Math.abs(theta - upright) < thetaTolerance) {
732            return 3;
733        } else if (Math.abs(theta - downright) < thetaTolerance) {
734            return 9;
735        } else if (Math.abs(theta - down) < thetaTolerance) {
736            return 8;
737        } else if (Math.abs(theta - downleft) < thetaTolerance) {
738            return 7;
739        } else if ((theta > right - thetaTolerance) || (theta < rightWrap + thetaTolerance)) {
740            return 6;
741        }
742
743        // Off by more than the threshold, so it doesn't count
744        return -1;
745    }
746
747    @Override
748    public void onDraw(Canvas canvas) {
749        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
750        paint.setColor(Color.WHITE);
751        paint.setTextAlign(Paint.Align.CENTER);
752        paint.setTypeface(Typeface.DEFAULT_BOLD);
753
754        int x = 5;
755        int y = 50;
756        paint.setTextSize(50);
757        paint.setTextAlign(Paint.Align.LEFT);
758        y -= paint.ascent() / 2;
759
760        canvas.drawText(currentString, x, y, paint);
761
762        String currentTitle = appList.get(appListIndex).getTitle();
763
764        if (currentTitle.length() > 0) {
765            y = 140;
766            paint.setTextSize(45);
767            String[] lines = currentTitle.split(" ");
768            for (int i = 0; i < lines.length; i++) {
769                canvas.drawText(lines[i], x, y + (i * 45), paint);
770            }
771        }
772        paint.setTextSize(50);
773
774        if (!screenIsBeingTouched) {
775            x = 5;
776            y = getHeight() - 40;
777            paint.setTextSize(20);
778            paint.setTextAlign(Paint.Align.LEFT);
779            y -= paint.ascent() / 2;
780            canvas.drawText("Scroll apps with trackball.", x, y, paint);
781
782            x = 5;
783            y = getHeight() - 20;
784            paint.setTextSize(20);
785            paint.setTextAlign(Paint.Align.LEFT);
786            y -= paint.ascent() / 2;
787            canvas.drawText("Press CALL to launch app.", x, y, paint);
788        } else {
789            int offset = 90;
790
791            int x1 = (int) downX - offset;
792            int y1 = (int) downY - offset;
793            int x2 = (int) downX;
794            int y2 = (int) downY - offset;
795            int x3 = (int) downX + offset;
796            int y3 = (int) downY - offset;
797            int x4 = (int) downX - offset;
798            int y4 = (int) downY;
799            int x6 = (int) downX + offset;
800            int y6 = (int) downY;
801            int x7 = (int) downX - offset;
802            int y7 = (int) downY + offset;
803            int x8 = (int) downX;
804            int y8 = (int) downY + offset;
805            int x9 = (int) downX + offset;
806            int y9 = (int) downY + offset;
807
808            y1 -= paint.ascent() / 2;
809            y2 -= paint.ascent() / 2;
810            y3 -= paint.ascent() / 2;
811            y4 -= paint.ascent() / 2;
812            y6 -= paint.ascent() / 2;
813            y7 -= paint.ascent() / 2;
814            y8 -= paint.ascent() / 2;
815            y9 -= paint.ascent() / 2;
816
817            switch (currentWheel) {
818                case AE:
819                    paint.setColor(Color.RED);
820                    drawCharacter("A", x1, y1, canvas, paint, currentCharacter.equals("A"));
821                    drawCharacter("B", x2, y2, canvas, paint, currentCharacter.equals("B"));
822                    drawCharacter("C", x3, y3, canvas, paint, currentCharacter.equals("C"));
823                    drawCharacter("H", x4, y4, canvas, paint, currentCharacter.equals("H"));
824                    drawCharacter("D", x6, y6, canvas, paint, currentCharacter.equals("D"));
825                    drawCharacter("G", x7, y7, canvas, paint, currentCharacter.equals("G"));
826                    drawCharacter("F", x8, y8, canvas, paint, currentCharacter.equals("F"));
827                    drawCharacter("E", x9, y9, canvas, paint, currentCharacter.equals("E"));
828                    break;
829                case IM:
830                    paint.setColor(Color.BLUE);
831                    drawCharacter("P", x1, y1, canvas, paint, currentCharacter.equals("P"));
832                    drawCharacter("I", x2, y2, canvas, paint, currentCharacter.equals("I"));
833                    drawCharacter("J", x3, y3, canvas, paint, currentCharacter.equals("J"));
834                    drawCharacter("O", x4, y4, canvas, paint, currentCharacter.equals("O"));
835                    drawCharacter("K", x6, y6, canvas, paint, currentCharacter.equals("K"));
836                    drawCharacter("N", x7, y7, canvas, paint, currentCharacter.equals("N"));
837                    drawCharacter("M", x8, y8, canvas, paint, currentCharacter.equals("M"));
838                    drawCharacter("L", x9, y9, canvas, paint, currentCharacter.equals("L"));
839                    break;
840                case QU:
841                    paint.setColor(Color.GREEN);
842                    drawCharacter("W", x1, y1, canvas, paint, currentCharacter.equals("W"));
843                    drawCharacter("X", x2, y2, canvas, paint, currentCharacter.equals("X"));
844                    drawCharacter("Q", x3, y3, canvas, paint, currentCharacter.equals("Q"));
845                    drawCharacter("V", x4, y4, canvas, paint, currentCharacter.equals("V"));
846                    drawCharacter("R", x6, y6, canvas, paint, currentCharacter.equals("R"));
847                    drawCharacter("U", x7, y7, canvas, paint, currentCharacter.equals("U"));
848                    drawCharacter("T", x8, y8, canvas, paint, currentCharacter.equals("T"));
849                    drawCharacter("S", x9, y9, canvas, paint, currentCharacter.equals("S"));
850                    break;
851                case Y:
852                    paint.setColor(Color.YELLOW);
853                    drawCharacter(",", x1, y1, canvas, paint, currentCharacter.equals(","));
854                    drawCharacter("!", x2, y2, canvas, paint, currentCharacter.equals("!"));
855                    drawCharacter("SPACE", x3, y3, canvas, paint, currentCharacter.equals("SPACE"));
856                    drawCharacter("<-", x4, y4, canvas, paint, currentCharacter.equals("<-"));
857                    drawCharacter("Y", x6, y6, canvas, paint, currentCharacter.equals("Y"));
858                    drawCharacter(".", x7, y7, canvas, paint, currentCharacter.equals("."));
859                    drawCharacter("?", x8, y8, canvas, paint, currentCharacter.equals("?"));
860                    drawCharacter("Z", x9, y9, canvas, paint, currentCharacter.equals("Z"));
861                    break;
862                default:
863                    paint.setColor(Color.RED);
864                    canvas.drawText("A", x1, y1, paint);
865                    canvas.drawText("E", x9, y9, paint);
866                    paint.setColor(Color.BLUE);
867                    canvas.drawText("I", x2, y2, paint);
868                    canvas.drawText("M", x8, y8, paint);
869                    paint.setColor(Color.GREEN);
870                    canvas.drawText("Q", x3, y3, paint);
871                    canvas.drawText("U", x7, y7, paint);
872                    paint.setColor(Color.YELLOW);
873                    canvas.drawText("Y", x6, y6, paint);
874                    canvas.drawText("<-", x4, y4, paint);
875                    break;
876            }
877        }
878    }
879
880    private void drawCharacter(
881            String character, int x, int y, Canvas canvas, Paint paint, boolean isSelected) {
882        int regSize = 50;
883        int selectedSize = regSize * 2;
884        if (isSelected) {
885            paint.setTextSize(selectedSize);
886        } else {
887            paint.setTextSize(regSize);
888        }
889        canvas.drawText(character, x, y, paint);
890    }
891
892    public void backspace() {
893        String deletedCharacter = "";
894        if (currentString.length() > 0) {
895            deletedCharacter = "" + currentString.charAt(currentString.length() - 1);
896            currentString = currentString.substring(0, currentString.length() - 1);
897        }
898        if (!deletedCharacter.equals("")) {
899            // parent.tts.speak(deletedCharacter, 0, new String[]
900            // {TTSParams.VOICE_ROBOT.toString()});
901            parent.tts.speak(parent.getString(R.string.deleted, deletedCharacter), 0, null);
902        } else {
903            parent.tts.playEarcon(parent.getString(R.string.earcon_tock), 0, null);
904            parent.tts.playEarcon(parent.getString(R.string.earcon_tock), 1, null);
905        }
906        if (currentString.length() > 0) {
907            jumpToFirstMatchingApp();
908        }
909        invalidate();
910    }
911
912    @Override
913    protected void onWindowVisibilityChanged(int visibility) {
914        if (shakeDetector != null) {
915            shakeDetector.shutdown();
916            shakeDetector = null;
917        }
918        if (visibility == View.VISIBLE) {
919            shakeDetector = new ShakeDetector(parent, new ShakeListener() {
920                @Override
921                public void onShakeDetected() {
922                    if (useShake) {
923                        backspace();
924                    }
925                }
926            });
927        }
928        super.onWindowVisibilityChanged(visibility);
929    }
930
931    public void uninstallCurrentApp() {
932        String targetPackageName = appList.get(appListIndex).getPackageName();
933        Intent i = new Intent();
934        i.setAction("android.intent.action.DELETE");
935        i.setData(Uri.parse("package:" + targetPackageName));
936        parent.startActivity(i);
937    }
938
939    public void showCurrentAppInfo() {
940        String targetPackageName = appList.get(appListIndex).getPackageName();
941        Intent i = new Intent();
942        try {
943            // android.settings.APPLICATION_DETAILS_SETTINGS is the correct, SDK
944            // blessed way of doing this - but it is only available starting in
945            // GINGERBREAD.
946            i.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
947            i.setData(Uri.parse("package:" + targetPackageName));
948            parent.startActivity(i);
949        } catch (ActivityNotFoundException e){
950            try {
951                // If it isn't possible to use android.settings.APPLICATION_DETAILS_SETTINGS,
952                // try it again with the "pkg" magic key. See ManageApplications.APP_PKG_NAME in:
953                // src/com/android/settings/ManageApplications.java
954                // under http://android.git.kernel.org/?p=platform/packages/apps/Settings.git
955                i.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails");
956                i.putExtra("pkg", targetPackageName);
957                parent.startActivity(i);
958            } catch (ActivityNotFoundException e2) {
959            }
960        }
961    }
962
963    public String getCurrentAppTitle() {
964        return appList.get(appListIndex).getTitle();
965    }
966}