OSDN Git Service

c80e67b8fccea58273772b88e9fea308cbdd499c
[android-x86/packages-apps-Contacts.git] / src / com / android / contacts / TwelveKeyDialer.java
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.contacts;
18
19 import android.app.Activity;
20 import android.content.ActivityNotFoundException;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.res.Resources;
24 import android.database.Cursor;
25 import android.graphics.Bitmap;
26 import android.graphics.BitmapFactory;
27 import android.graphics.drawable.Drawable;
28 import android.media.AudioManager;
29 import android.media.ToneGenerator;
30 import android.net.Uri;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.os.Message;
34 import android.os.RemoteException;
35 import android.os.ServiceManager;
36 import android.os.SystemClock;
37 import android.provider.Contacts.Intents.Insert;
38 import android.provider.Contacts.People;
39 import android.provider.Contacts.Phones;
40 import android.provider.Contacts.PhonesColumns;
41 import android.provider.Settings;
42 import android.telephony.PhoneNumberFormattingTextWatcher;
43 import android.telephony.PhoneNumberUtils;
44 import android.telephony.PhoneStateListener;
45 import android.telephony.TelephonyManager;
46 import android.text.Editable;
47 import android.text.TextUtils;
48 import android.text.TextWatcher;
49 import android.text.method.DialerKeyListener;
50 import android.util.Log;
51 import android.view.KeyEvent;
52 import android.view.LayoutInflater;
53 import android.view.Menu;
54 import android.view.MenuItem;
55 import android.view.View;
56 import android.view.ViewConfiguration;
57 import android.view.ViewGroup;
58 import android.widget.AdapterView;
59 import android.widget.BaseAdapter;
60 import android.widget.EditText;
61 import android.widget.ImageView;
62 import android.widget.ListView;
63 import android.widget.TextView;
64
65 import com.android.internal.telephony.ITelephony;
66
67 /**
68  * Dialer activity that displays the typical twelve key interface.
69  */
70 public class TwelveKeyDialer extends Activity implements View.OnClickListener,
71         View.OnLongClickListener, View.OnKeyListener,
72         AdapterView.OnItemClickListener, TextWatcher {
73
74     private static final String TAG = "TwelveKeyDialer";
75     
76     private static final int STOP_TONE = 1;
77
78     /** The length of DTMF tones in milliseconds */
79     private static final int TONE_LENGTH_MS = 150;
80     
81     /** The DTMF tone volume relative to other sounds in the stream */
82     private static final int TONE_RELATIVE_VOLUME = 50;
83
84     private EditText mDigits;
85     private View mDelete;
86     private MenuItem mAddToContactMenuItem;
87     private ToneGenerator mToneGenerator;
88     private Object mToneGeneratorLock = new Object();
89     private Drawable mDigitsBackground;
90     private Drawable mDigitsEmptyBackground;
91     private Drawable mDeleteBackground;
92     private Drawable mDeleteEmptyBackground;
93     private View mDigitsAndBackspace;
94     private View mDialpad;
95     private ListView mDialpadChooser;
96     private DialpadChooserAdapter mDialpadChooserAdapter;
97
98     // determines if we want to playback local DTMF tones.
99     private boolean mDTMFToneEnabled;
100     
101     /** Identifier for the "Add Call" intent extra. */
102     static final String ADD_CALL_MODE_KEY = "add_call_mode";
103     /** Indicates if we are opening this dialer to add a call from the InCallScreen. */
104     private boolean mIsAddCallMode;
105
106     PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
107             /**
108              * Listen for phone state changes so that we can take down the
109              * "dialpad chooser" if the phone becomes idle while the
110              * chooser UI is visible.
111              */
112             @Override
113             public void onCallStateChanged(int state, String incomingNumber) {
114                 // Log.i(TAG, "PhoneStateListener.onCallStateChanged: "
115                 //       + state + ", '" + incomingNumber + "'");
116                 if ((state == TelephonyManager.CALL_STATE_IDLE) && dialpadChooserVisible()) {
117                     // Log.i(TAG, "Call ended with dialpad chooser visible!  Taking it down...");
118                     // Note there's a race condition in the UI here: the
119                     // dialpad chooser could conceivably disappear (on its
120                     // own) at the exact moment the user was trying to select
121                     // one of the choices, which would be confusing.  (But at
122                     // least that's better than leaving the dialpad chooser
123                     // onscreen, but useless...)
124                     showDialpadChooser(false);
125                 }
126             }
127         };
128
129     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
130         // Do nothing
131     }
132
133     public void onTextChanged(CharSequence input, int start, int before, int changeCount) {
134         // Do nothing
135         // DTMF Tones do not need to be played here any longer - 
136         // the DTMF dialer handles that functionality now.
137     }
138
139     public void afterTextChanged(Editable input) {
140         if (SpecialCharSequenceMgr.handleChars(this, input.toString(), mDigits)) {
141             // A special sequence was entered, clear the digits
142             mDigits.getText().clear();
143         }
144
145         // Set the proper background for the dial input area
146         if (mDigits.length() != 0) {
147             mDelete.setBackgroundDrawable(mDeleteBackground);
148             mDigits.setBackgroundDrawable(mDigitsBackground);
149             mDigits.setCompoundDrawablesWithIntrinsicBounds(
150                     getResources().getDrawable(R.drawable.ic_dial_number), null, null, null);
151         } else {
152             mDelete.setBackgroundDrawable(mDeleteEmptyBackground);
153             mDigits.setBackgroundDrawable(mDigitsEmptyBackground);
154             mDigits.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
155         }
156     }
157
158     @Override
159     protected void onCreate(Bundle icicle) {
160         super.onCreate(icicle);
161
162         // Set the content view
163         setContentView(getContentViewResource());
164
165         // Load up the resources for the text field and delete button
166         Resources r = getResources();
167         mDigitsBackground = r.getDrawable(R.drawable.btn_dial_textfield_active);
168         //mDigitsBackground.setDither(true);
169         mDigitsEmptyBackground = r.getDrawable(R.drawable.btn_dial_textfield);
170         //mDigitsEmptyBackground.setDither(true);
171         mDeleteBackground = r.getDrawable(R.drawable.btn_dial_delete_active);
172         //mDeleteBackground.setDither(true);
173         mDeleteEmptyBackground = r.getDrawable(R.drawable.btn_dial_delete);
174         //mDeleteEmptyBackground.setDither(true);
175
176         mDigits = (EditText) findViewById(R.id.digits);
177         mDigits.setKeyListener(DialerKeyListener.getInstance());
178         mDigits.setOnClickListener(this);
179         mDigits.setOnKeyListener(this);
180         maybeAddNumberFormatting();
181
182         // Check for the presence of the keypad
183         View view = findViewById(R.id.one);
184         if (view != null) {
185             setupKeypad();
186         }
187
188         view = findViewById(R.id.backspace);
189         view.setOnClickListener(this);
190         view.setOnLongClickListener(this);
191         mDelete = view;
192
193         mDigitsAndBackspace = (View) findViewById(R.id.digitsAndBackspace);
194         mDialpad = (View) findViewById(R.id.dialpad);  // This is null in landscape mode
195
196         // Set up the "dialpad chooser" UI; see showDialpadChooser().
197         mDialpadChooser = (ListView) findViewById(R.id.dialpadChooser);
198         mDialpadChooser.setOnItemClickListener(this);
199
200         if (!resolveIntent() && icicle != null) {
201             super.onRestoreInstanceState(icicle);
202         }
203
204         // If the mToneGenerator creation fails, just continue without it.  It is
205         // a local audio signal, and is not as important as the dtmf tone itself.
206         synchronized (mToneGeneratorLock) {
207             if (mToneGenerator == null) {
208                 try {
209                     mToneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 
210                             TONE_RELATIVE_VOLUME);
211                 } catch (RuntimeException e) {
212                     Log.w(TAG, "Exception caught while creating local tone generator: " + e);
213                     mToneGenerator = null;
214                 }
215             }
216         }
217     }
218
219     @Override
220     protected void onDestroy() {
221         super.onDestroy();
222         synchronized(mToneGeneratorLock) {
223             if (mToneGenerator != null) {
224                 mToneStopper.removeMessages(STOP_TONE);
225                 mToneGenerator.release();
226                 mToneGenerator = null;
227             }
228         }
229     }
230
231     @Override
232     protected void onRestoreInstanceState(Bundle icicle) {
233         // Do nothing, state is restored in onCreate() if needed
234     }
235     
236     protected void maybeAddNumberFormatting() {
237         mDigits.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
238     }
239     
240     /**
241      * Overridden by subclasses to control the resource used by the content view. 
242      */
243     protected int getContentViewResource() {
244         return R.layout.twelve_key_dialer;
245     }
246
247     private boolean resolveIntent() {
248         boolean ignoreState = false;
249
250         // Find the proper intent
251         final Intent intent;
252         if (isChild()) {
253             intent = getParent().getIntent();
254             ignoreState = intent.getBooleanExtra(DialtactsActivity.EXTRA_IGNORE_STATE, false);
255         } else {
256             intent = getIntent();
257         }
258         // Log.i(TAG, "==> resolveIntent(): intent: " + intent);
259
260         // by default we are not adding a call.
261         mIsAddCallMode = false;
262
263         // By default we don't show the "dialpad chooser" UI.
264         boolean needToShowDialpadChooser = false;
265
266         // Resolve the intent
267         final String action = intent.getAction();
268         if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
269             // see if we are "adding a call" from the InCallScreen; false by default.
270             mIsAddCallMode = intent.getBooleanExtra(ADD_CALL_MODE_KEY, false);
271             Uri uri = intent.getData();
272             if (uri != null) {
273                 if ("tel".equals(uri.getScheme())) {
274                     // Put the requested number into the input area
275                     String data = uri.getSchemeSpecificPart();
276                     setFormattedDigits(data);
277                 } else {
278                     String type = intent.getType();
279                     if (People.CONTENT_ITEM_TYPE.equals(type)
280                             || Phones.CONTENT_ITEM_TYPE.equals(type)) {
281                         // Query the phone number
282                         Cursor c = getContentResolver().query(intent.getData(),
283                                 new String[] {PhonesColumns.NUMBER}, null, null, null);
284                         if (c != null) {
285                             if (c.moveToFirst()) {
286                                 // Put the number into the input area
287                                 setFormattedDigits(c.getString(0));
288                             }
289                             c.close();
290                         }
291                     }
292                 }
293             }
294         } else if (Intent.ACTION_MAIN.equals(action)) {
295             // The MAIN action means we're bringing up a blank dialer
296             // (e.g. by selecting the Home shortcut, or tabbing over from
297             // Contacts or Call log.)
298             //
299             // At this point, IF there's already an active call, there's a
300             // good chance that the user got here accidentally (but really
301             // wanted the in-call dialpad instead).  So we bring up an
302             // intermediate UI to make the user confirm what they really
303             // want to do.
304             if (phoneIsInUse()) {
305                 // Log.i(TAG, "resolveIntent(): phone is in use; showing dialpad chooser!");
306                 needToShowDialpadChooser = true;
307             }
308         }
309
310         // Bring up the "dialpad chooser" IFF we need to make the user
311         // confirm which dialpad they really want.
312         showDialpadChooser(needToShowDialpadChooser);
313
314         return ignoreState;
315     }
316
317     protected void setFormattedDigits(String data) {
318         // strip the non-dialable numbers out of the data string.
319         String dialString = PhoneNumberUtils.extractNetworkPortion(data);
320         dialString = PhoneNumberUtils.formatNumber(dialString);
321         if (!TextUtils.isEmpty(dialString)) {
322             Editable digits = mDigits.getText();
323             digits.replace(0, digits.length(), dialString);
324             mDigits.setCompoundDrawablesWithIntrinsicBounds(
325                     getResources().getDrawable(R.drawable.ic_dial_number), null, null, null);
326         }
327     }
328
329     @Override
330     protected void onNewIntent(Intent newIntent) {
331         setIntent(newIntent);
332         resolveIntent();
333     }
334     
335     @Override
336     protected void onPostCreate(Bundle savedInstanceState) {
337         super.onPostCreate(savedInstanceState);
338
339         // This can't be done in onCreate(), since the auto-restoring of the digits
340         // will play DTMF tones for all the old digits if it is when onRestoreSavedInstanceState()
341         // is called. This method will be called every time the activity is created, and
342         // will always happen after onRestoreSavedInstanceState().
343         mDigits.addTextChangedListener(this);
344     }
345     
346     private void setupKeypad() {
347         // Setup the listeners for the buttons
348         View view = findViewById(R.id.one);
349         view.setOnClickListener(this);
350         view.setOnLongClickListener(this);
351
352         findViewById(R.id.two).setOnClickListener(this);
353         findViewById(R.id.three).setOnClickListener(this);
354         findViewById(R.id.four).setOnClickListener(this);
355         findViewById(R.id.five).setOnClickListener(this);
356         findViewById(R.id.six).setOnClickListener(this);
357         findViewById(R.id.seven).setOnClickListener(this);
358         findViewById(R.id.eight).setOnClickListener(this);
359         findViewById(R.id.nine).setOnClickListener(this);
360         findViewById(R.id.star).setOnClickListener(this);
361
362         view = findViewById(R.id.zero);
363         view.setOnClickListener(this);
364         view.setOnLongClickListener(this);
365
366         findViewById(R.id.pound).setOnClickListener(this);
367     }
368
369     @Override
370     protected void onResume() {
371         super.onResume();
372         
373         // retrieve the DTMF tone play back setting.
374         mDTMFToneEnabled = Settings.System.getInt(getContentResolver(),
375                 Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
376
377         // if the mToneGenerator creation fails, just continue without it.  It is 
378         // a local audio signal, and is not as important as the dtmf tone itself.
379         synchronized(mToneGeneratorLock) {
380             if (mToneGenerator == null) {
381                 try {
382                     mToneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 
383                             TONE_RELATIVE_VOLUME);
384                 } catch (RuntimeException e) {
385                     Log.w(TAG, "Exception caught while creating local tone generator: " + e);
386                     mToneGenerator = null;
387                 }
388             }
389         }
390         
391         Activity parent = getParent();
392         // See if we were invoked with a DIAL intent. If we were, fill in the appropriate
393         // digits in the dialer field.
394         if (parent != null && parent instanceof DialtactsActivity) {
395             Uri dialUri = ((DialtactsActivity) parent).getAndClearDialUri();
396             if (dialUri != null) {
397                 resolveIntent();
398             }
399         }
400
401         // While we're in the foreground, listen for phone state changes,
402         // purely so that we can take down the "dialpad chooser" if the
403         // phone becomes idle while the chooser UI is visible.
404         TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
405         telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
406
407         // Potentially show hint text in the mDigits field when the user
408         // hasn't typed any digits yet.  (If there's already an active call,
409         // this hint text will remind the user that he's about to add a new
410         // call.)
411         //
412         // TODO: consider adding better UI for the case where *both* lines
413         // are currently in use.  (Right now we let the user try to add
414         // another call, but that call is guaranteed to fail.  Perhaps the
415         // entire dialer UI should be disabled instead.)
416         if (phoneIsInUse()) {
417             mDigits.setHint(R.string.dialerDialpadHintText);
418         } else {
419             // Common case; no hint necessary.
420             mDigits.setHint(null);
421
422             // Also, a sanity-check: the "dialpad chooser" UI should NEVER
423             // be visible if the phone is idle!
424             showDialpadChooser(false);
425         }
426     }
427
428     @Override
429     protected void onPause() {
430         super.onPause();
431
432         // Stop listening for phone state changes.
433         TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
434         telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
435
436         synchronized(mToneGeneratorLock) {
437             if (mToneGenerator != null) {
438                 mToneStopper.removeMessages(STOP_TONE);
439                 mToneGenerator.release();
440                 mToneGenerator = null;
441             }
442         }
443     }
444
445     @Override
446     public boolean onCreateOptionsMenu(Menu menu) {
447         mAddToContactMenuItem = menu.add(0, 0, 0, R.string.recentCalls_addToContact)
448                 .setIcon(android.R.drawable.ic_menu_add);
449
450         return true;
451     }
452
453     @Override
454     public boolean onPrepareOptionsMenu(Menu menu) {
455         // We never show a menu if the "choose dialpad" UI is up.
456         if (dialpadChooserVisible()) {
457             return false;
458         }
459
460         CharSequence digits = mDigits.getText();
461         if (digits == null || !TextUtils.isGraphic(digits)) {
462             mAddToContactMenuItem.setVisible(false);
463         } else {
464             // Put the current digits string into an intent
465             Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
466             intent.putExtra(Insert.PHONE, mDigits.getText());
467             intent.setType(People.CONTENT_ITEM_TYPE);
468             mAddToContactMenuItem.setIntent(intent);
469             mAddToContactMenuItem.setVisible(true);
470         }
471         return true;
472     }
473
474     @Override
475     public boolean onKeyDown(int keyCode, KeyEvent event) {
476         switch (keyCode) {
477             case KeyEvent.KEYCODE_CALL: {
478                 long callPressDiff = SystemClock.uptimeMillis() - event.getDownTime();
479                 if (callPressDiff >= ViewConfiguration.getLongPressTimeout()) {
480                     // Launch voice dialer
481                     Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND);
482                     intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
483                     try {
484                         startActivity(intent);
485                     } catch (ActivityNotFoundException e) {
486                     }
487                 }
488                 return true;
489             }
490             case KeyEvent.KEYCODE_1: {
491                 long timeDiff = SystemClock.uptimeMillis() - event.getDownTime(); 
492                 if (timeDiff >= ViewConfiguration.getLongPressTimeout()) {
493                     // Long press detected, call voice mail
494                     callVoicemail();
495                 }
496                 return true;
497             }
498         }
499         return super.onKeyDown(keyCode, event);
500     }
501
502     @Override
503     public boolean onKeyUp(int keyCode, KeyEvent event) {
504         switch (keyCode) {
505             case KeyEvent.KEYCODE_CALL: {
506                 if (mIsAddCallMode && (TextUtils.isEmpty(mDigits.getText().toString()))) {
507                     // if we are adding a call from the InCallScreen and the phone
508                     // number entered is empty, we just close the dialer to expose
509                     // the InCallScreen under it.
510                     finish();
511                 } else {
512                     // otherwise, we place the call.
513                     placeCall();
514                 }
515                 return true;
516             }
517         }
518         return super.onKeyUp(keyCode, event);
519     }
520     
521     private void keyPressed(int keyCode) {
522         KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
523         mDigits.onKeyDown(keyCode, event);
524     }
525
526     public boolean onKey(View view, int keyCode, KeyEvent event) {
527         switch (view.getId()) {
528             case R.id.digits:
529                 if (keyCode == KeyEvent.KEYCODE_ENTER) {
530                     placeCall();
531                     return true;
532                 }
533                 break;
534         }
535         return false;
536     }
537
538     public void onClick(View view) {
539         switch (view.getId()) {
540             case R.id.one: {
541                 playTone(ToneGenerator.TONE_DTMF_1);
542                 keyPressed(KeyEvent.KEYCODE_1);
543                 return;
544             }
545             case R.id.two: {
546                 playTone(ToneGenerator.TONE_DTMF_2);
547                 keyPressed(KeyEvent.KEYCODE_2);
548                 return;
549             }
550             case R.id.three: {
551                 playTone(ToneGenerator.TONE_DTMF_3);
552                 keyPressed(KeyEvent.KEYCODE_3);
553                 return;
554             }
555             case R.id.four: {
556                 playTone(ToneGenerator.TONE_DTMF_4);
557                 keyPressed(KeyEvent.KEYCODE_4);
558                 return;
559             }
560             case R.id.five: {
561                 playTone(ToneGenerator.TONE_DTMF_5);
562                 keyPressed(KeyEvent.KEYCODE_5);
563                 return;
564             }
565             case R.id.six: {
566                 playTone(ToneGenerator.TONE_DTMF_6);
567                 keyPressed(KeyEvent.KEYCODE_6);
568                 return;
569             }
570             case R.id.seven: {
571                 playTone(ToneGenerator.TONE_DTMF_7);
572                 keyPressed(KeyEvent.KEYCODE_7);
573                 return;
574             }
575             case R.id.eight: {
576                 playTone(ToneGenerator.TONE_DTMF_8);
577                 keyPressed(KeyEvent.KEYCODE_8);
578                 return;
579             }
580             case R.id.nine: {
581                 playTone(ToneGenerator.TONE_DTMF_9);
582                 keyPressed(KeyEvent.KEYCODE_9);
583                 return;
584             }
585             case R.id.zero: {
586                 playTone(ToneGenerator.TONE_DTMF_0);
587                 keyPressed(KeyEvent.KEYCODE_0);
588                 return;
589             }
590             case R.id.pound: {
591                 playTone(ToneGenerator.TONE_DTMF_P);
592                 keyPressed(KeyEvent.KEYCODE_POUND);
593                 return;
594             }
595             case R.id.star: {
596                 playTone(ToneGenerator.TONE_DTMF_S);
597                 keyPressed(KeyEvent.KEYCODE_STAR);
598                 return;
599             }
600             case R.id.backspace: {
601                 keyPressed(KeyEvent.KEYCODE_DEL);
602                 return;
603             }
604             case R.id.digits: {
605                 placeCall();
606                 return;
607             }
608         }
609     }
610
611     public boolean onLongClick(View view) {
612         final Editable digits = mDigits.getText();
613         int id = view.getId();
614         switch (id) {
615             case R.id.backspace: {
616                 digits.clear();
617                 return true;
618             }
619             case R.id.one: {
620                 if (digits.length() == 0) {
621                     callVoicemail();
622                     return true;
623                 }
624                 return false;
625             }
626             case R.id.zero: {
627                 keyPressed(KeyEvent.KEYCODE_PLUS);
628                 return true;
629             }
630         }
631         return false;
632     }
633
634     void callVoicemail() {
635         Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
636                 Uri.fromParts("voicemail", "", null));
637         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
638         startActivity(intent);
639         mDigits.getText().clear();
640         finish();
641     }
642
643     void placeCall() {
644         final String number = mDigits.getText().toString();
645         if (number == null || !TextUtils.isGraphic(number)) {
646             // There is no number entered.
647             playTone(ToneGenerator.TONE_PROP_NACK);
648             return;
649         }
650         Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
651                 Uri.fromParts("tel", number, null));
652         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
653         startActivity(intent);
654         mDigits.getText().clear();
655         finish();
656     }
657
658     Handler mToneStopper = new Handler() {
659         @Override
660         public void handleMessage(Message msg) {
661             switch (msg.what) {
662                 case STOP_TONE:
663                     synchronized(mToneGeneratorLock) {
664                         if (mToneGenerator == null) {
665                             Log.w(TAG, "mToneStopper: mToneGenerator == null");
666                         } else {
667                             mToneGenerator.stopTone();
668                         }
669                     }
670                     break;
671             }
672         }
673     };
674
675     /**
676      * Play a tone for TONE_LENGTH_MS milliseconds.
677      * 
678      * @param tone a tone code from {@link ToneGenerator}
679      */
680     void playTone(int tone) {
681         // if local tone playback is disabled, just return.
682         if (!mDTMFToneEnabled) {
683             return;
684         }
685  
686         synchronized(mToneGeneratorLock) {
687             if (mToneGenerator == null) {
688                 Log.w(TAG, "playTone: mToneGenerator == null, tone: "+tone);
689                 return;
690             }
691             
692             // Remove pending STOP_TONE messages
693             mToneStopper.removeMessages(STOP_TONE);
694     
695             // Start the new tone (will stop any playing tone)
696             mToneGenerator.startTone(tone);
697             mToneStopper.sendEmptyMessageDelayed(STOP_TONE, TONE_LENGTH_MS);
698         }
699     }
700
701     /**
702      * Brings up the "dialpad chooser" UI in place of the usual Dialer
703      * elements (the textfield/button and the dialpad underneath).
704      *
705      * We show this UI if the user brings up the Dialer while a call is
706      * already in progress, since there's a good chance we got here
707      * accidentally (and the user really wanted the in-call dialpad instead).
708      * So in this situation we display an intermediate UI that lets the user
709      * explicitly choose between the in-call dialpad ("Use touch tone
710      * keypad") and the regular Dialer ("Add call").  (Or, the option "Return
711      * to call in progress" just goes back to the in-call UI with no dialpad
712      * at all.)
713      *
714      * @param enabled If true, show the "dialpad chooser" instead
715      *                of the regular Dialer UI
716      */
717     private void showDialpadChooser(boolean enabled) {
718         if (enabled) {
719             // Log.i(TAG, "Showing dialpad chooser!");
720             mDigitsAndBackspace.setVisibility(View.GONE);
721             if (mDialpad != null) mDialpad.setVisibility(View.GONE);
722             mDialpadChooser.setVisibility(View.VISIBLE);
723
724             // Instantiate the DialpadChooserAdapter and hook it up to the
725             // ListView.  We do this only once.
726             if (mDialpadChooserAdapter == null) {
727                 mDialpadChooserAdapter = new DialpadChooserAdapter(this);
728                 mDialpadChooser.setAdapter(mDialpadChooserAdapter);
729             }
730         } else {
731             // Log.i(TAG, "Displaying normal Dialer UI.");
732             mDigitsAndBackspace.setVisibility(View.VISIBLE);
733             if (mDialpad != null) mDialpad.setVisibility(View.VISIBLE);
734             mDialpadChooser.setVisibility(View.GONE);
735         }
736     }
737
738     /**
739      * @return true if we're currently showing the "dialpad chooser" UI.
740      */
741     private boolean dialpadChooserVisible() {
742         return mDialpadChooser.getVisibility() == View.VISIBLE;
743     }
744
745     /**
746      * Simple list adapter, binding to an icon + text label
747      * for each item in the "dialpad chooser" list.
748      */
749     private static class DialpadChooserAdapter extends BaseAdapter {
750         private LayoutInflater mInflater;
751
752         // Simple struct for a single "choice" item.
753         static class ChoiceItem {
754             String text;
755             Bitmap icon;
756             int id;
757
758             public ChoiceItem(String s, Bitmap b, int i) {
759                 text = s;
760                 icon = b;
761                 id = i;
762             }
763         }
764
765         // IDs for the possible "choices":
766         static final int DIALPAD_CHOICE_USE_DTMF_DIALPAD = 101;
767         static final int DIALPAD_CHOICE_RETURN_TO_CALL = 102;
768         static final int DIALPAD_CHOICE_ADD_NEW_CALL = 103;
769
770         private static final int NUM_ITEMS = 3;
771         private ChoiceItem mChoiceItems[] = new ChoiceItem[NUM_ITEMS];
772
773         public DialpadChooserAdapter(Context context) {
774             // Cache the LayoutInflate to avoid asking for a new one each time.
775             mInflater = LayoutInflater.from(context);
776
777             // Initialize the possible choices.
778             // TODO: could this be specified entirely in XML?
779
780             // - "Use touch tone keypad"
781             mChoiceItems[0] = new ChoiceItem(
782                     context.getString(R.string.dialer_useDtmfDialpad),
783                     BitmapFactory.decodeResource(context.getResources(),
784                                                  R.drawable.ic_dialer_fork_tt_keypad),
785                     DIALPAD_CHOICE_USE_DTMF_DIALPAD);
786
787             // - "Return to call in progress"
788             mChoiceItems[1] = new ChoiceItem(
789                     context.getString(R.string.dialer_returnToInCallScreen),
790                     BitmapFactory.decodeResource(context.getResources(),
791                                                  R.drawable.ic_dialer_fork_current_call),
792                     DIALPAD_CHOICE_RETURN_TO_CALL);
793
794             // - "Add call"
795             mChoiceItems[2] = new ChoiceItem(
796                     context.getString(R.string.dialer_addAnotherCall),
797                     BitmapFactory.decodeResource(context.getResources(),
798                                                  R.drawable.ic_dialer_fork_add_call),
799                     DIALPAD_CHOICE_ADD_NEW_CALL);
800         }
801
802         public int getCount() {
803             return NUM_ITEMS;
804         }
805
806         /**
807          * Return the ChoiceItem for a given position.
808          */
809         public Object getItem(int position) {
810             return mChoiceItems[position];
811         }
812
813         /**
814          * Return a unique ID for each possible choice.
815          */
816         public long getItemId(int position) {
817             return position;
818         }
819
820         /**
821          * Make a view for each row.
822          */
823         public View getView(int position, View convertView, ViewGroup parent) {
824             // When convertView is non-null, we can reuse it (there's no need
825             // to reinflate it.)
826             if (convertView == null) {
827                 convertView = mInflater.inflate(R.layout.dialpad_chooser_list_item, null);
828             }
829
830             TextView text = (TextView) convertView.findViewById(R.id.text);
831             text.setText(mChoiceItems[position].text);
832
833             ImageView icon = (ImageView) convertView.findViewById(R.id.icon);
834             icon.setImageBitmap(mChoiceItems[position].icon);
835
836             return convertView;
837         }
838     }
839
840     /**
841      * Handle clicks from the dialpad chooser.
842      */
843     public void onItemClick(AdapterView parent, View v, int position, long id) {
844         DialpadChooserAdapter.ChoiceItem item =
845                 (DialpadChooserAdapter.ChoiceItem) parent.getItemAtPosition(position);
846         int itemId = item.id;
847         switch (itemId) {
848             case DialpadChooserAdapter.DIALPAD_CHOICE_USE_DTMF_DIALPAD:
849                 // Log.i(TAG, "DIALPAD_CHOICE_USE_DTMF_DIALPAD");
850                 // Fire off an intent to go back to the in-call UI
851                 // with the dialpad visible.
852                 returnToInCallScreen(true);
853                 break;
854
855             case DialpadChooserAdapter.DIALPAD_CHOICE_RETURN_TO_CALL:
856                 // Log.i(TAG, "DIALPAD_CHOICE_RETURN_TO_CALL");
857                 // Fire off an intent to go back to the in-call UI
858                 // (with the dialpad hidden).
859                 returnToInCallScreen(false);
860                 break;
861
862             case DialpadChooserAdapter.DIALPAD_CHOICE_ADD_NEW_CALL:
863                 // Log.i(TAG, "DIALPAD_CHOICE_ADD_NEW_CALL");
864                 // Ok, guess the user really did want to be here (in the
865                 // regular Dialer) after all.  Bring back the normal Dialer UI.
866                 showDialpadChooser(false);
867                 break;
868
869             default:
870                 Log.w(TAG, "onItemClick: unexpected itemId: " + itemId);
871                 break;
872         }
873     }
874
875     /**
876      * Returns to the in-call UI (where there's presumably a call in
877      * progress) in response to the user selecting "use touch tone keypad"
878      * or "return to call" from the dialpad chooser.
879      */
880     private void returnToInCallScreen(boolean showDialpad) {
881         try {
882             ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
883             if (phone != null) phone.showCallScreenWithDialpad(showDialpad);
884         } catch (RemoteException e) {
885             Log.w(TAG, "phone.showCallScreenWithDialpad() failed", e);
886         }
887
888         // Finally, finish() ourselves so that we don't stay on the
889         // activity stack.
890         // Note that we do this whether or not the showCallScreenWithDialpad()
891         // call above had any effect or not!  (That call is a no-op if the
892         // phone is idle, which can happen if the current call ends while
893         // the dialpad chooser is up.  In this case we can't show the
894         // InCallScreen, and there's no point staying here in the Dialer,
895         // so we just take the user back where he came from...)
896         finish();
897     }
898
899     /**
900      * @return true if the phone is "in use", meaning that at least one line
901      *              is active (ie. off hook or ringing or dialing).
902      */
903     private boolean phoneIsInUse() {
904         boolean phoneInUse = false;
905         try {
906             ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
907             if (phone != null) phoneInUse = !phone.isIdle();
908         } catch (RemoteException e) {
909             Log.w(TAG, "phone.isIdle() failed", e);
910         }
911         return phoneInUse;
912     }
913 }