PageRenderTime 71ms CodeModel.GetById 9ms app.highlight 55ms RepoModel.GetById 1ms app.codeStats 0ms

/talkingdialer/src/com/google/marvin/talkingdialer/ContactsView.java

http://eyes-free.googlecode.com/
Java | 982 lines | 815 code | 98 blank | 69 comment | 147 complexity | ada9f2e180642291e185e31b9df437e5 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.talkingdialer;
 18
 19import android.content.Context;
 20import android.content.Intent;
 21import android.database.Cursor;
 22import android.database.CursorIndexOutOfBoundsException;
 23import android.database.StaleDataException;
 24import android.graphics.Canvas;
 25import android.graphics.Color;
 26import android.graphics.Paint;
 27import android.graphics.Typeface;
 28import android.net.Uri;
 29import android.os.Vibrator;
 30import android.provider.ContactsContract.CommonDataKinds.Phone;
 31import android.speech.tts.TextToSpeech;
 32import android.text.TextUtils;
 33import android.util.Log;
 34import android.view.KeyEvent;
 35import android.view.MotionEvent;
 36import android.widget.TextView;
 37
 38import com.google.marvin.talkingdialer.ShakeDetector.ShakeListener;
 39import com.googlecode.eyesfree.utils.compat.MotionEventCompatUtils;
 40
 41import java.lang.reflect.InvocationTargetException;
 42import java.lang.reflect.Method;
 43import java.util.ArrayList;
 44
 45/**
 46 * Allows the user to select the contact they wish to call by moving through
 47 * their phonebook. The contact name being spoken is actually the contact's
 48 * ringtone. By setting the ringtone to an audio file that is the same as the
 49 * contact's name, the user gets a talking caller ID feature automatically
 50 * without needing any additional code.
 51 *
 52 * @author clchen@google.com (Charles L. Chen)
 53 */
 54public class ContactsView extends TextView {
 55    // BEGIN OF WORKAROUND FOR DONUT COMPATIBILITY
 56    private static Method MotionEvent_getX;
 57
 58    private static Method MotionEvent_getY;
 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            /* success, this is a newer device */
 68        } catch (NoSuchMethodException nsme) {
 69            /* failure, must be older device */
 70        }
 71    }
 72
 73    private static float getX(MotionEvent event, int x) {
 74        try {
 75            Object retobj = MotionEvent_getX.invoke(event, x);
 76            return (Float) retobj;
 77        } catch (IllegalAccessException ie) {
 78            System.err.println("unexpected " + ie);
 79        } catch (IllegalArgumentException e) {
 80            // TODO Auto-generated catch block
 81            e.printStackTrace();
 82        } catch (InvocationTargetException e) {
 83            // TODO Auto-generated catch block
 84            e.printStackTrace();
 85        }
 86        return -1;
 87    }
 88
 89    private static float getY(MotionEvent event, int y) {
 90        try {
 91            Object retobj = MotionEvent_getY.invoke(event, y);
 92            return (Float) retobj;
 93        } catch (IllegalAccessException ie) {
 94            System.err.println("unexpected " + ie);
 95        } catch (IllegalArgumentException e) {
 96            // TODO Auto-generated catch block
 97            e.printStackTrace();
 98        } catch (InvocationTargetException e) {
 99            // TODO Auto-generated catch block
100            e.printStackTrace();
101        }
102        return -1;
103    }
104
105    // END OF WORKAROUND FOR DONUT COMPATIBILITY
106
107    private static final long[] PATTERN = { 0, 1, 40, 41 };
108
109    private static final int NAME = 0;
110    private static final int NUMBER = 1;
111    private static final int TYPE = 2;
112    private static final int PERSON_ID = 3;
113    private static final int AE = 0;
114    private static final int IM = 1;
115    private static final int QU = 2;
116    private static final int Y = 4;
117    private static final int NONE = 5;
118
119    private static final int LONG_PRESS_THRESHOLD = 2000;
120
121    private final double left = 0;
122    private final double upleft = Math.PI * .25;
123    private final double up = Math.PI * .5;
124    private final double upright = Math.PI * .75;
125    private final double downright = -Math.PI * .75;
126    private final double down = -Math.PI * .5;
127    private final double downleft = -Math.PI * .25;
128    private final double right = Math.PI;
129    private final double rightWrap = -Math.PI;
130
131    // An array specifying which columns to return.
132    private static final String[] PROJECTION = new String[] {
133            Phone.DISPLAY_NAME, Phone.NUMBER, Phone.TYPE, Phone.RAW_CONTACT_ID };
134
135    private SlideDial parent;
136
137    private Cursor managedCursor;
138
139    private FilterableContactsList filteredContacts;
140
141    private boolean confirmed;
142
143    private double downX;
144    private double downY;
145    private double lastX;
146    private double lastY;
147
148    private float p2DownX;
149    private float p2DownY;
150
151    private int currentValue;
152
153    private boolean screenIsBeingTouched;
154
155    private Vibrator vibe;
156
157    private int currentWheel;
158
159    private String currentCharacter;
160
161    private String currentString;
162
163    private String currentContact;
164
165    private int trackballTimeout = 500;
166
167    private boolean trackballEnabled = true;
168
169    private ShakeDetector shakeDetector;
170
171    private LongPressDetector longPress = null;
172
173    private boolean inDPadMode = false;
174
175    public ContactsView(Context context) {
176        super(context);
177
178        parent = ((SlideDial) context);
179
180        vibe = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
181
182        // Get the base URI for People table in Contacts content provider.
183        // ie. content://contacts/people/
184        Uri mContacts = Phone.CONTENT_URI;
185
186        // Best way to retrieve a query; returns a managed query.
187        managedCursor = parent.managedQuery(mContacts, PROJECTION, // Which
188        // columns
189                // to return.
190                null, // WHERE clause--we won't specify.
191                null, // no selection args
192                Phone.DISPLAY_NAME + " ASC"); // Order-by clause.
193        boolean moveSucceeded = managedCursor.moveToFirst();
194
195        ArrayList<String> contactNames = new ArrayList<String>();
196        while (moveSucceeded) {
197            contactNames.add(managedCursor.getString(NAME));
198            moveSucceeded = managedCursor.moveToNext();
199        }
200        filteredContacts = new FilterableContactsList(contactNames);
201        currentString = "";
202        currentContact = "";
203
204        setClickable(true);
205        setFocusable(true);
206        setFocusableInTouchMode(true);
207        requestFocus();
208
209        shakeDetector = new ShakeDetector(context, new ShakeListener() {
210            @Override
211            public void onShakeDetected() {
212                backspace();
213            }
214        });
215    }
216
217    public void shutdown() {
218        shakeDetector.shutdown();
219    }
220
221    @Override
222    public boolean onTrackballEvent(MotionEvent event) {
223        int action = event.getAction();
224        if (trackballEnabled == false) {
225            return true;
226        }
227        Log.i("Motion", Float.toString(event.getY()));
228        if (event.getY() > .16) {
229            trackballEnabled = false;
230            (new Thread(new trackballTimeout())).start();
231            nextContact();
232        }
233        if (event.getY() < -.16) {
234            trackballEnabled = false;
235            (new Thread(new trackballTimeout())).start();
236            prevContact();
237        }
238
239        if (action == MotionEvent.ACTION_DOWN) {
240            if (longPress == null) {
241                longPress = new LongPressDetector();
242            }
243            postDelayed(longPress, LONG_PRESS_THRESHOLD);
244            return true;
245        } else if (action == MotionEvent.ACTION_UP) {
246            if (longPress != null) {
247                removeCallbacks(longPress);
248            }
249            return true;
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    private void resetContactList() {
267        currentString = "";
268        if (managedCursor.getCount() > 0) {
269            managedCursor.moveToFirst();
270            if (!managedCursor.isBeforeFirst() && !managedCursor.isAfterLast()) {
271                // Keep going if the entry doesn't have a name
272                String name = managedCursor.getString(NAME);
273                if (name == null) {
274                    nextContact();
275                    return;
276                }
277            }
278        }
279    }
280
281    public void nextContact() {
282        currentString = "";
283        // Make sure we don't try to act on an empty table
284        if (managedCursor.getCount() > 0) {
285            boolean moveSucceeded = managedCursor.moveToNext();
286            if (!moveSucceeded) {
287                managedCursor.moveToFirst();
288            }
289            // Keep going if the entry doesn't have a name
290            String name = managedCursor.getString(NAME);
291            if (name == null) {
292                nextContact();
293                return;
294            }
295            vibe.vibrate(PATTERN, -1);
296            speakCurrentContact(true);
297        }
298    }
299
300    public void prevContact() {
301        currentString = "";
302        // Make sure we don't try to act on an empty table
303        if (managedCursor.getCount() > 0) {
304            boolean moveSucceeded = managedCursor.moveToPrevious();
305            if (!moveSucceeded) {
306                managedCursor.moveToLast();
307            }
308            // Keep going if the entry doesn't have a name
309            String name = managedCursor.getString(NAME);
310            if (name == null) {
311                prevContact();
312                return;
313            }
314            vibe.vibrate(PATTERN, -1);
315            speakCurrentContact(true);
316        }
317    }
318
319    private void jumpToFirstFilteredResult() {
320        ContactEntry entry = filteredContacts.next();
321        if (entry == null) {
322            parent.tts.playEarcon(parent.getString(R.string.earcon_tock), 0, null);
323            if (currentString.length() > 0) {
324                currentString = currentString.substring(0, currentString.length() - 1);
325                if (currentString.length() > 0) {
326                    filteredContacts.filter(currentString);
327                    jumpToFirstFilteredResult();
328                } else {
329                    parent.tts.speak(parent.getString(R.string.no_contacts_found), 0, null);
330                }
331            }
332            return;
333        }
334        managedCursor.moveToPosition(entry.index);
335        speakCurrentContact(true);
336    }
337
338    /**
339     * @return a string representing the currently selected contact
340     */
341    private String getCurrentContact() {
342        final StringBuilder contact = new StringBuilder();
343        String name = null;
344
345        try {
346            name = managedCursor.getString(NAME);
347        } catch (CursorIndexOutOfBoundsException e) {
348            e.printStackTrace();
349        } catch (StaleDataException e) {
350            e.printStackTrace();
351        }
352
353        if (TextUtils.isEmpty(name)) {
354            return null;
355        } else {
356            contact.append(name);
357        }
358
359        final int phoneType = Integer.parseInt(managedCursor.getString(TYPE));
360        int typeRes = -1;
361
362        switch (phoneType) {
363            case Phone.TYPE_HOME:
364                typeRes = R.string.home;
365                break;
366            case Phone.TYPE_MOBILE:
367                typeRes = R.string.cell;
368                break;
369            case Phone.TYPE_WORK:
370                typeRes = R.string.work;
371                break;
372        }
373
374        if (typeRes >= 0) {
375            contact.append(' ');
376            contact.append(getContext().getString(typeRes));
377        }
378
379        return contact.toString();
380    }
381
382    /**
383     * Speaks the currently selected contact and sets the internal current
384     * contact.
385     *
386     * @param interrupt Set to {@code true} to flush queued speech and speak
387     *            immediately.
388     */
389    private void speakCurrentContact(boolean interrupt) {
390        final String contact = getCurrentContact();
391
392        if (TextUtils.isEmpty(contact)) {
393            return;
394        }
395
396        final int mode = interrupt ? TextToSpeech.QUEUE_FLUSH : TextToSpeech.QUEUE_ADD;
397
398        parent.tts.speak(contact, mode, null);
399
400        currentContact = contact;
401
402        invalidate();
403    }
404
405    public void dialActionHandler() {
406        if (!confirmed) {
407            if (!TextUtils.isEmpty(currentContact)) {
408                if (parent.contactsPickerMode) {
409                    parent.tts.speak(
410                            parent.getString(R.string.you_have_selected, currentContact), 0, null);
411                } else {
412                    parent.tts.speak(
413                            parent.getString(R.string.you_are_about_to_dial, currentContact), 0,
414                            null);
415                }
416                confirmed = true;
417            } else {
418                // If the user attempts to dial with no contact selected, switch
419                // to dialing view.
420                parent.switchToDialingView();
421            }
422        } else {
423            parent.returnResults(managedCursor.getString(NUMBER), currentContact);
424        }
425    }
426
427    @Override
428    public boolean onKeyDown(int keyCode, KeyEvent event) {
429        final char keyLabel = event.getDisplayLabel();
430
431        if (Character.isLetterOrDigit(keyLabel)) {
432            currentString = currentString + keyLabel;
433            filteredContacts.filter(currentString);
434            jumpToFirstFilteredResult();
435            return true;
436        }
437
438        switch (keyCode) {
439            case KeyEvent.KEYCODE_DPAD_DOWN:
440                nextContact();
441                currentString = "";
442                return true;
443            case KeyEvent.KEYCODE_DPAD_UP:
444                prevContact();
445                currentString = "";
446                return true;
447            case KeyEvent.KEYCODE_ENTER:
448            case KeyEvent.KEYCODE_SEARCH:
449            case KeyEvent.KEYCODE_CALL:
450                dialActionHandler();
451                return true;
452            case KeyEvent.KEYCODE_MENU:
453                parent.switchToDialingView();
454                return true;
455            case KeyEvent.KEYCODE_DEL:
456                backspace();
457                return true;
458        }
459
460        confirmed = false;
461        return false;
462    }
463
464    private void confirmEntry() {
465        screenIsBeingTouched = false;
466        int prevVal = currentValue;
467        currentValue = evalMotion(lastX, lastY);
468        // Do some correction if the user lifts up on deadspace
469        if (currentValue == -1) {
470            currentValue = prevVal;
471        }
472        // The user never got a number that wasn't deadspace,
473        // so assume 5.
474        if (currentValue == -1) {
475            currentValue = 5;
476        }
477        currentCharacter = getCharacter(currentWheel, currentValue);
478        if (currentCharacter.equals("<-")) {
479            currentContact = "";
480            initiateMotion(lastX, lastY);
481            backspace();
482            return;
483        } else {
484            currentString = currentString + currentCharacter;
485        }
486        parent.tts.speak(currentCharacter, 0, null);
487        currentContact = "";
488
489        invalidate();
490        initiateMotion(lastX, lastY);
491
492        currentString = currentString + currentCharacter;
493        filteredContacts.filter(currentString);
494        jumpToFirstFilteredResult();
495    }
496
497    private void initiateMotion(double x, double y) {
498        downX = x;
499        downY = y;
500        lastX = x;
501        lastY = y;
502        currentValue = -1;
503        currentWheel = NONE;
504        currentCharacter = "";
505    }
506
507    public boolean onHoverEvent(MotionEvent event) {
508        return onTouchEvent(event);
509    }
510
511    @Override
512    public boolean onTouchEvent(MotionEvent event) {
513        int action = event.getAction();
514        float x = event.getX();
515        float y = event.getY();
516
517        // Treat the screen as a dpad
518        if (action == 261) { // 261 == ACTION_POINTER_2_DOWN - using number for
519                             // Android 1.6 compat
520            removeCallbacks(longPress);
521            inDPadMode = true;
522            screenIsBeingTouched = false;
523            p2DownX = getX(event, 1);
524            p2DownY = getY(event, 1);
525            vibe.vibrate(PATTERN, -1);
526            invalidate();
527        } else if (action == 262) { // 262 == MotionEvent.ACTION_POINTER_2_UP -
528                                    // using number for Android 1.6 compat
529            float p2DeltaX = getX(event, 1) - p2DownX;
530            float p2DeltaY = getY(event, 1) - p2DownY;
531            if (Math.abs(p2DeltaX) > Math.abs(p2DeltaY)) {
532                if (p2DeltaX < -200) {
533                    backspace();
534                }
535            } else {
536                if (p2DeltaY < -100) {
537                    prevContact();
538                } else if (p2DeltaY > 100) {
539                    nextContact();
540                }
541            }
542        }
543
544        if ((action == MotionEvent.ACTION_UP)
545                || (action == MotionEventCompatUtils.ACTION_HOVER_EXIT)) {
546            if (x > 650) {
547                nextContact();
548                return true;
549            }
550            if (x < 100) {
551                prevContact();
552                return true;
553            }
554        } else {
555            if (x > 650) {
556                return true;
557            }
558            if (x < 100) {
559                return true;
560            }
561        }
562
563        if ((action == MotionEvent.ACTION_DOWN)
564                || (action == MotionEventCompatUtils.ACTION_HOVER_ENTER)) {
565            initiateMotion(x, y);
566            if (longPress == null) {
567                longPress = new LongPressDetector();
568            }
569            postDelayed(longPress, LONG_PRESS_THRESHOLD);
570            return true;
571        } else if ((action == MotionEvent.ACTION_UP)
572                || (action == MotionEventCompatUtils.ACTION_HOVER_EXIT)) {
573            if (inDPadMode == false) {
574                confirmEntry();
575            } else {
576                inDPadMode = false;
577            }
578            if (longPress != null) {
579                removeCallbacks(longPress);
580            }
581            return true;
582        } else {
583            if (!inDPadMode) {
584                screenIsBeingTouched = true;
585                lastX = x;
586                lastY = y;
587                int prevVal = currentValue;
588                currentValue = evalMotion(x, y);
589                // Do nothing since we want a deadzone here;
590                // restore the state to the previous value.
591                if (currentValue == -1) {
592                    currentValue = prevVal;
593                    return true;
594                }
595                // There is a wheel that is active
596                if (currentValue != 5) {
597                    if (currentWheel == NONE) {
598                        currentWheel = getWheel(currentValue);
599                        // User has entered a wheel so invalidate the long press
600                        // callback.
601                        if (longPress != null) {
602                            removeCallbacks(longPress);
603                        }
604                    }
605                    currentCharacter = getCharacter(currentWheel, currentValue);
606                } else {
607                    currentCharacter = "";
608                }
609                invalidate();
610                if (prevVal != currentValue) {
611                    if (currentCharacter.equals("")) {
612                        parent.tts.playEarcon(parent.getString(R.string.earcon_tock), 0, null);
613                    } else {
614                        if (currentCharacter.equals(".")) {
615                            parent.tts.speak(parent.getString(R.string.period), 0, null);
616                        } else if (currentCharacter.equals("!")) {
617                            parent.tts.speak(parent.getString(R.string.exclamation_point), 0, null);
618                        } else if (currentCharacter.equals("?")) {
619                            parent.tts.speak(parent.getString(R.string.question_mark), 0, null);
620                        } else if (currentCharacter.equals(",")) {
621                            parent.tts.speak(parent.getString(R.string.comma), 0, null);
622                        } else if (currentCharacter.equals("<-")) {
623                            parent.tts.speak(parent.getString(R.string.backspace), 0, null);
624                        } else {
625                            parent.tts.speak(currentCharacter, 0, null);
626                        }
627                    }
628                }
629                vibe.vibrate(PATTERN, -1);
630            }
631        }
632        return true;
633    }
634
635    public int getWheel(int value) {
636        switch (value) {
637            case 1:
638                return AE;
639            case 2:
640                return IM;
641            case 3:
642                return QU;
643            case 4:
644                return Y;
645            case 5:
646                return NONE;
647            case 6:
648                return Y;
649            case 7:
650                return QU;
651            case 8:
652                return IM;
653            case 9:
654                return AE;
655            default:
656                return NONE;
657        }
658    }
659
660    public String getCharacter(int wheel, int value) {
661        switch (wheel) {
662            case AE:
663                switch (value) {
664                    case 1:
665                        return "A";
666                    case 2:
667                        return "B";
668                    case 3:
669                        return "C";
670                    case 4:
671                        return "H";
672                    case 5:
673                        return "";
674                    case 6:
675                        return "D";
676                    case 7:
677                        return "G";
678                    case 8:
679                        return "F";
680                    case 9:
681                        return "E";
682                    default:
683                        return "";
684                }
685            case IM:
686                switch (value) {
687                    case 1:
688                        return "P";
689                    case 2:
690                        return "I";
691                    case 3:
692                        return "J";
693                    case 4:
694                        return "O";
695                    case 5:
696                        return "";
697                    case 6:
698                        return "K";
699                    case 7:
700                        return "N";
701                    case 8:
702                        return "M";
703                    case 9:
704                        return "L";
705                    default:
706                        return "";
707                }
708            case QU:
709                switch (value) {
710                    case 1:
711                        return "W";
712                    case 2:
713                        return "X";
714                    case 3:
715                        return "Q";
716                    case 4:
717                        return "V";
718                    case 5:
719                        return "";
720                    case 6:
721                        return "R";
722                    case 7:
723                        return "U";
724                    case 8:
725                        return "T";
726                    case 9:
727                        return "S";
728                    default:
729                        return "";
730                }
731            case Y:
732                switch (value) {
733                    case 1:
734                        return ",";
735                    case 2:
736                        return "!";
737                    case 3:
738                        return ""; // return "MODE";
739                    case 4:
740                        return "<-";
741                    case 5:
742                        return "";
743                    case 6:
744                        return "Y";
745                    case 7:
746                        return ".";
747                    case 8:
748                        return "?";
749                    case 9:
750                        return "Z";
751                    default:
752                        return "";
753                }
754            default:
755                return "";
756        }
757    }
758
759    public int evalMotion(double x, double y) {
760        float rTolerance = 25;
761        double thetaTolerance = (Math.PI / 16);
762
763        double r = Math.sqrt(((downX - x) * (downX - x)) + ((downY - y) * (downY - y)));
764
765        if (r < rTolerance) {
766            return 5;
767        }
768
769        double theta = Math.atan2(downY - y, downX - x);
770
771        if (Math.abs(theta - left) < thetaTolerance) {
772            return 4;
773        } else if (Math.abs(theta - upleft) < thetaTolerance) {
774            return 1;
775        } else if (Math.abs(theta - up) < thetaTolerance) {
776            return 2;
777        } else if (Math.abs(theta - upright) < thetaTolerance) {
778            return 3;
779        } else if (Math.abs(theta - downright) < thetaTolerance) {
780            return 9;
781        } else if (Math.abs(theta - down) < thetaTolerance) {
782            return 8;
783        } else if (Math.abs(theta - downleft) < thetaTolerance) {
784            return 7;
785        } else if ((theta > right - thetaTolerance) || (theta < rightWrap + thetaTolerance)) {
786            return 6;
787        }
788
789        // Off by more than the threshold, so it doesn't count
790        return -1;
791    }
792
793    @Override
794    public void onDraw(Canvas canvas) {
795        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
796        paint.setColor(Color.WHITE);
797        paint.setTextAlign(Paint.Align.CENTER);
798        paint.setTypeface(Typeface.DEFAULT_BOLD);
799
800        int x = 5;
801        int y = 50;
802        paint.setTextSize(50);
803        paint.setTextAlign(Paint.Align.LEFT);
804        y -= paint.ascent() / 2;
805
806        canvas.drawText(currentString, x, y, paint);
807
808        if (currentContact.length() > 0) {
809            y = 140;
810            paint.setTextSize(45);
811            String[] lines = currentContact.split(" ");
812            for (int i = 0; i < lines.length; i++) {
813                canvas.drawText(lines[i], x, y + (i * 45), paint);
814            }
815        }
816        paint.setTextSize(50);
817
818        if (!screenIsBeingTouched) {
819            x = 5;
820            y = getHeight() - 260;
821            paint.setTextSize(20);
822            paint.setTextAlign(Paint.Align.LEFT);
823            y -= paint.ascent() / 2;
824            canvas.drawText("Press MENU for dialing mode.", x, y, paint);
825
826            x = 5;
827            y = getHeight() - 240;
828            paint.setTextSize(20);
829            paint.setTextAlign(Paint.Align.LEFT);
830            y -= paint.ascent() / 2;
831            canvas.drawText("Scroll contacts with trackball.", x, y, paint);
832
833            x = 5;
834            y = getHeight() - 220;
835            paint.setTextSize(20);
836            paint.setTextAlign(Paint.Align.LEFT);
837            y -= paint.ascent() / 2;
838            canvas.drawText("Press CALL twice to confirm.", x, y, paint);
839        } else {
840            int offset = 90;
841
842            int x1 = (int) downX - offset;
843            int y1 = (int) downY - offset;
844            int x2 = (int) downX;
845            int y2 = (int) downY - offset;
846            int x3 = (int) downX + offset;
847            int y3 = (int) downY - offset;
848            int x4 = (int) downX - offset;
849            int y4 = (int) downY;
850            int x6 = (int) downX + offset;
851            int y6 = (int) downY;
852            int x7 = (int) downX - offset;
853            int y7 = (int) downY + offset;
854            int x8 = (int) downX;
855            int y8 = (int) downY + offset;
856            int x9 = (int) downX + offset;
857            int y9 = (int) downY + offset;
858
859            y1 -= paint.ascent() / 2;
860            y2 -= paint.ascent() / 2;
861            y3 -= paint.ascent() / 2;
862            y4 -= paint.ascent() / 2;
863            y6 -= paint.ascent() / 2;
864            y7 -= paint.ascent() / 2;
865            y8 -= paint.ascent() / 2;
866            y9 -= paint.ascent() / 2;
867
868            switch (currentWheel) {
869                case AE:
870                    paint.setColor(Color.RED);
871                    drawCharacter("A", x1, y1, canvas, paint, currentCharacter.equals("A"));
872                    drawCharacter("B", x2, y2, canvas, paint, currentCharacter.equals("B"));
873                    drawCharacter("C", x3, y3, canvas, paint, currentCharacter.equals("C"));
874                    drawCharacter("H", x4, y4, canvas, paint, currentCharacter.equals("H"));
875                    drawCharacter("D", x6, y6, canvas, paint, currentCharacter.equals("D"));
876                    drawCharacter("G", x7, y7, canvas, paint, currentCharacter.equals("G"));
877                    drawCharacter("F", x8, y8, canvas, paint, currentCharacter.equals("F"));
878                    drawCharacter("E", x9, y9, canvas, paint, currentCharacter.equals("E"));
879                    break;
880                case IM:
881                    paint.setColor(Color.BLUE);
882                    drawCharacter("P", x1, y1, canvas, paint, currentCharacter.equals("P"));
883                    drawCharacter("I", x2, y2, canvas, paint, currentCharacter.equals("I"));
884                    drawCharacter("J", x3, y3, canvas, paint, currentCharacter.equals("J"));
885                    drawCharacter("O", x4, y4, canvas, paint, currentCharacter.equals("O"));
886                    drawCharacter("K", x6, y6, canvas, paint, currentCharacter.equals("K"));
887                    drawCharacter("N", x7, y7, canvas, paint, currentCharacter.equals("N"));
888                    drawCharacter("M", x8, y8, canvas, paint, currentCharacter.equals("M"));
889                    drawCharacter("L", x9, y9, canvas, paint, currentCharacter.equals("L"));
890                    break;
891                case QU:
892                    paint.setColor(Color.GREEN);
893                    drawCharacter("W", x1, y1, canvas, paint, currentCharacter.equals("W"));
894                    drawCharacter("X", x2, y2, canvas, paint, currentCharacter.equals("X"));
895                    drawCharacter("Q", x3, y3, canvas, paint, currentCharacter.equals("Q"));
896                    drawCharacter("V", x4, y4, canvas, paint, currentCharacter.equals("V"));
897                    drawCharacter("R", x6, y6, canvas, paint, currentCharacter.equals("R"));
898                    drawCharacter("U", x7, y7, canvas, paint, currentCharacter.equals("U"));
899                    drawCharacter("T", x8, y8, canvas, paint, currentCharacter.equals("T"));
900                    drawCharacter("S", x9, y9, canvas, paint, currentCharacter.equals("S"));
901                    break;
902                case Y:
903                    paint.setColor(Color.YELLOW);
904                    drawCharacter(",", x1, y1, canvas, paint, currentCharacter.equals(","));
905                    drawCharacter("!", x2, y2, canvas, paint, currentCharacter.equals("!"));
906                    // drawCharacter("MODE", x3, y3, canvas, paint,
907                    // currentCharacter.equals("MODE"));
908                    drawCharacter("<-", x4, y4, canvas, paint, currentCharacter.equals("<-"));
909                    drawCharacter("Y", x6, y6, canvas, paint, currentCharacter.equals("Y"));
910                    drawCharacter(".", x7, y7, canvas, paint, currentCharacter.equals("."));
911                    drawCharacter("?", x8, y8, canvas, paint, currentCharacter.equals("?"));
912                    drawCharacter("Z", x9, y9, canvas, paint, currentCharacter.equals("Z"));
913                    break;
914                default:
915                    paint.setColor(Color.RED);
916                    canvas.drawText("A", x1, y1, paint);
917                    canvas.drawText("E", x9, y9, paint);
918                    paint.setColor(Color.BLUE);
919                    canvas.drawText("I", x2, y2, paint);
920                    canvas.drawText("M", x8, y8, paint);
921                    paint.setColor(Color.GREEN);
922                    canvas.drawText("Q", x3, y3, paint);
923                    canvas.drawText("U", x7, y7, paint);
924                    paint.setColor(Color.YELLOW);
925                    canvas.drawText("Y", x6, y6, paint);
926                    canvas.drawText("<-", x4, y4, paint);
927                    break;
928            }
929        }
930    }
931
932    private void drawCharacter(
933            String character, int x, int y, Canvas canvas, Paint paint, boolean isSelected) {
934        int regSize = 50;
935        int selectedSize = regSize * 2;
936        if (isSelected) {
937            paint.setTextSize(selectedSize);
938        } else {
939            paint.setTextSize(regSize);
940        }
941        canvas.drawText(character, x, y, paint);
942    }
943
944    public void backspace() {
945        confirmed = false;
946        String deletedCharacter = "";
947        if (currentString.length() > 0) {
948            deletedCharacter = "" + currentString.charAt(currentString.length() - 1);
949            currentString = currentString.substring(0, currentString.length() - 1);
950            if (currentString.length() > 0) {
951                filteredContacts.filter(currentString);
952                jumpToFirstFilteredResult();
953            } else {
954                resetContactList();
955            }
956        } else {
957            parent.tts.playEarcon(parent.getString(R.string.earcon_tock), 0, null);
958            parent.tts.playEarcon(parent.getString(R.string.earcon_tock), 1, null);
959        }
960        invalidate();
961    }
962
963    public void displayContactDetails() {
964        if (!managedCursor.isAfterLast()) {
965            final String text = parent.getString(
966                    R.string.load_detail, managedCursor.getString(NAME));
967            parent.tts.speak(text, TextToSpeech.QUEUE_FLUSH, null);
968            final String uri = parent.getString(
969                    R.string.people_uri, managedCursor.getString(PERSON_ID));
970            final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
971            parent.startActivity(intent);
972        }
973    }
974
975    private class LongPressDetector implements Runnable {
976        @Override
977        public void run() {
978            invalidate();
979            displayContactDetails();
980        }
981    }
982}