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
17package com.android.contacts;
18
19import com.android.internal.telephony.ITelephony;
20import com.android.phone.CallLogAsync;
21import com.android.phone.HapticFeedback;
22
23import android.app.Activity;
24import android.content.ActivityNotFoundException;
25import android.content.Context;
26import android.content.Intent;
27import android.content.res.Configuration;
28import android.content.res.Resources;
29import android.database.Cursor;
30import android.graphics.Bitmap;
31import android.graphics.BitmapFactory;
32import android.graphics.drawable.Drawable;
33import android.media.AudioManager;
34import android.media.ToneGenerator;
35import android.net.Uri;
36import android.os.Bundle;
37import android.os.RemoteException;
38import android.os.ServiceManager;
39import android.os.SystemClock;
40import android.provider.Settings;
41import android.provider.Contacts.People;
42import android.provider.Contacts.Phones;
43import android.provider.Contacts.PhonesColumns;
44import android.provider.Contacts.Intents.Insert;
45import android.telephony.PhoneNumberFormattingTextWatcher;
46import android.telephony.PhoneNumberUtils;
47import android.telephony.PhoneStateListener;
48import android.telephony.TelephonyManager;
49import android.text.Editable;
50import android.text.TextUtils;
51import android.text.TextWatcher;
52import android.text.method.DialerKeyListener;
53import android.util.Log;
54import android.view.KeyEvent;
55import android.view.LayoutInflater;
56import android.view.Menu;
57import android.view.MenuItem;
58import android.view.View;
59import android.view.ViewConfiguration;
60import android.view.ViewGroup;
61import android.view.Window;
62import android.view.inputmethod.InputMethodManager;
63import android.widget.AdapterView;
64import android.widget.BaseAdapter;
65import android.widget.EditText;
66import android.widget.ImageView;
67import android.widget.ListView;
68import android.widget.TextView;
69
70/**
71 * Dialer activity that displays the typical twelve key interface.
72 */
73@SuppressWarnings("deprecation")
74public class TwelveKeyDialer extends Activity implements View.OnClickListener,
75        View.OnLongClickListener, View.OnKeyListener,
76        AdapterView.OnItemClickListener, TextWatcher {
77    private static final String EMPTY_NUMBER = "";
78    private static final String TAG = "TwelveKeyDialer";
79
80    /** The length of DTMF tones in milliseconds */
81    private static final int TONE_LENGTH_MS = 150;
82
83    /** The DTMF tone volume relative to other sounds in the stream */
84    private static final int TONE_RELATIVE_VOLUME = 80;
85
86    /** Stream type used to play the DTMF tones off call, and mapped to the volume control keys */
87    private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_MUSIC;
88
89    private EditText mDigits;
90    private View mDelete;
91    private MenuItem mAddToContactMenuItem;
92    private ToneGenerator mToneGenerator;
93    private Object mToneGeneratorLock = new Object();
94    private Drawable mDigitsBackground;
95    private Drawable mDigitsEmptyBackground;
96    private View mDialpad;
97    private View mVoicemailDialAndDeleteRow;
98    private View mVoicemailButton;
99    private View mDialButton;
100    private ListView mDialpadChooser;
101    private DialpadChooserAdapter mDialpadChooserAdapter;
102    //Member variables for dialpad options
103    private MenuItem m2SecPauseMenuItem;
104    private MenuItem mWaitMenuItem;
105    private static final int MENU_ADD_CONTACTS = 1;
106    private static final int MENU_2S_PAUSE = 2;
107    private static final int MENU_WAIT = 3;
108
109    // Last number dialed, retrieved asynchronously from the call DB
110    // in onCreate. This number is displayed when the user hits the
111    // send key and cleared in onPause.
112    CallLogAsync mCallLog = new CallLogAsync();
113    private String mLastNumberDialed = EMPTY_NUMBER;
114
115    // determines if we want to playback local DTMF tones.
116    private boolean mDTMFToneEnabled;
117
118    // Vibration (haptic feedback) for dialer key presses.
119    private HapticFeedback mHaptic = new HapticFeedback();
120
121    /** Identifier for the "Add Call" intent extra. */
122    static final String ADD_CALL_MODE_KEY = "add_call_mode";
123
124    /**
125     * Identifier for intent extra for sending an empty Flash message for
126     * CDMA networks. This message is used by the network to simulate a
127     * press/depress of the "hookswitch" of a landline phone. Aka "empty flash".
128     *
129     * TODO: Using an intent extra to tell the phone to send this flash is a
130     * temporary measure. To be replaced with an ITelephony call in the future.
131     * TODO: Keep in sync with the string defined in OutgoingCallBroadcaster.java
132     * in Phone app until this is replaced with the ITelephony API.
133     */
134    static final String EXTRA_SEND_EMPTY_FLASH
135            = "com.android.phone.extra.SEND_EMPTY_FLASH";
136
137    /** Indicates if we are opening this dialer to add a call from the InCallScreen. */
138    private boolean mIsAddCallMode;
139
140    PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
141            /**
142             * Listen for phone state changes so that we can take down the
143             * "dialpad chooser" if the phone becomes idle while the
144             * chooser UI is visible.
145             */
146            @Override
147            public void onCallStateChanged(int state, String incomingNumber) {
148                // Log.i(TAG, "PhoneStateListener.onCallStateChanged: "
149                //       + state + ", '" + incomingNumber + "'");
150                if ((state == TelephonyManager.CALL_STATE_IDLE) && dialpadChooserVisible()) {
151                    // Log.i(TAG, "Call ended with dialpad chooser visible!  Taking it down...");
152                    // Note there's a race condition in the UI here: the
153                    // dialpad chooser could conceivably disappear (on its
154                    // own) at the exact moment the user was trying to select
155                    // one of the choices, which would be confusing.  (But at
156                    // least that's better than leaving the dialpad chooser
157                    // onscreen, but useless...)
158                    showDialpadChooser(false);
159                }
160            }
161        };
162
163    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
164        // Do nothing
165    }
166
167    public void onTextChanged(CharSequence input, int start, int before, int changeCount) {
168        // Do nothing
169        // DTMF Tones do not need to be played here any longer -
170        // the DTMF dialer handles that functionality now.
171    }
172
173    public void afterTextChanged(Editable input) {
174        if (SpecialCharSequenceMgr.handleChars(this, input.toString(), mDigits)) {
175            // A special sequence was entered, clear the digits
176            mDigits.getText().clear();
177        }
178
179        if (!isDigitsEmpty()) {
180            mDigits.setBackgroundDrawable(mDigitsBackground);
181        } else {
182            mDigits.setCursorVisible(false);
183            mDigits.setBackgroundDrawable(mDigitsEmptyBackground);
184        }
185
186        updateDialAndDeleteButtonEnabledState();
187    }
188
189    @Override
190    protected void onCreate(Bundle icicle) {
191        super.onCreate(icicle);
192
193        Resources r = getResources();
194        // Do not show title in the case the device is in carmode.
195        if ((r.getConfiguration().uiMode & Configuration.UI_MODE_TYPE_MASK) ==
196                Configuration.UI_MODE_TYPE_CAR) {
197            requestWindowFeature(Window.FEATURE_NO_TITLE);
198        }
199        // Set the content view
200        setContentView(getContentViewResource());
201
202        // Load up the resources for the text field.
203        mDigitsBackground = r.getDrawable(R.drawable.btn_dial_textfield_active);
204        mDigitsEmptyBackground = r.getDrawable(R.drawable.btn_dial_textfield);
205
206        mDigits = (EditText) findViewById(R.id.digits);
207        mDigits.setKeyListener(DialerKeyListener.getInstance());
208        mDigits.setOnClickListener(this);
209        mDigits.setOnKeyListener(this);
210
211        maybeAddNumberFormatting();
212
213        // Check for the presence of the keypad
214        View view = findViewById(R.id.one);
215        if (view != null) {
216            setupKeypad();
217        }
218
219        mVoicemailDialAndDeleteRow = findViewById(R.id.voicemailAndDialAndDelete);
220
221        initVoicemailButton();
222
223        // Check whether we should show the onscreen "Dial" button.
224        mDialButton = mVoicemailDialAndDeleteRow.findViewById(R.id.dialButton);
225
226        if (r.getBoolean(R.bool.config_show_onscreen_dial_button)) {
227            mDialButton.setOnClickListener(this);
228        } else {
229            mDialButton.setVisibility(View.GONE); // It's VISIBLE by default
230            mDialButton = null;
231        }
232
233        view = mVoicemailDialAndDeleteRow.findViewById(R.id.deleteButton);
234        view.setOnClickListener(this);
235        view.setOnLongClickListener(this);
236        mDelete = view;
237
238        mDialpad = findViewById(R.id.dialpad);  // This is null in landscape mode.
239
240        // In landscape we put the keyboard in phone mode.
241        // In portrait we prevent the soft keyboard to show since the
242        // dialpad acts as one already.
243        if (null == mDialpad) {
244            mDigits.setInputType(android.text.InputType.TYPE_CLASS_PHONE);
245        } else {
246            mDigits.setInputType(android.text.InputType.TYPE_NULL);
247        }
248
249        // Set up the "dialpad chooser" UI; see showDialpadChooser().
250        mDialpadChooser = (ListView) findViewById(R.id.dialpadChooser);
251        mDialpadChooser.setOnItemClickListener(this);
252
253        if (!resolveIntent() && icicle != null) {
254            super.onRestoreInstanceState(icicle);
255        }
256
257        try {
258            mHaptic.init(this, r.getBoolean(R.bool.config_enable_dialer_key_vibration));
259        } catch (Resources.NotFoundException nfe) {
260             Log.e(TAG, "Vibrate control bool missing.", nfe);
261        }
262
263    }
264
265    @Override
266    protected void onRestoreInstanceState(Bundle icicle) {
267        // Do nothing, state is restored in onCreate() if needed
268    }
269
270    protected void maybeAddNumberFormatting() {
271        mDigits.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
272    }
273
274    /**
275     * Overridden by subclasses to control the resource used by the content view.
276     */
277    protected int getContentViewResource() {
278        return R.layout.twelve_key_dialer;
279    }
280
281    private boolean resolveIntent() {
282        boolean ignoreState = false;
283
284        // Find the proper intent
285        final Intent intent;
286        if (isChild()) {
287            intent = getParent().getIntent();
288            ignoreState = intent.getBooleanExtra(DialtactsActivity.EXTRA_IGNORE_STATE, false);
289        } else {
290            intent = getIntent();
291        }
292        // Log.i(TAG, "==> resolveIntent(): intent: " + intent);
293
294        // by default we are not adding a call.
295        mIsAddCallMode = false;
296
297        // By default we don't show the "dialpad chooser" UI.
298        boolean needToShowDialpadChooser = false;
299
300        // Resolve the intent
301        final String action = intent.getAction();
302        if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
303            // see if we are "adding a call" from the InCallScreen; false by default.
304            mIsAddCallMode = intent.getBooleanExtra(ADD_CALL_MODE_KEY, false);
305
306            Uri uri = intent.getData();
307            if (uri != null) {
308                if ("tel".equals(uri.getScheme())) {
309                    // Put the requested number into the input area
310                    String data = uri.getSchemeSpecificPart();
311                    setFormattedDigits(data);
312                } else {
313                    String type = intent.getType();
314                    if (People.CONTENT_ITEM_TYPE.equals(type)
315                            || Phones.CONTENT_ITEM_TYPE.equals(type)) {
316                        // Query the phone number
317                        Cursor c = getContentResolver().query(intent.getData(),
318                                new String[] {PhonesColumns.NUMBER}, null, null, null);
319                        if (c != null) {
320                            if (c.moveToFirst()) {
321                                // Put the number into the input area
322                                setFormattedDigits(c.getString(0));
323                            }
324                            c.close();
325                        }
326                    }
327                }
328            } else {
329                // ACTION_DIAL or ACTION_VIEW with no data.
330                // This behaves basically like ACTION_MAIN: If there's
331                // already an active call, bring up an intermediate UI to
332                // make the user confirm what they really want to do.
333                // Be sure *not* to show the dialpad chooser if this is an
334                // explicit "Add call" action, though.
335                if (!mIsAddCallMode && phoneIsInUse()) {
336                    needToShowDialpadChooser = true;
337                }
338            }
339        } else if (Intent.ACTION_MAIN.equals(action)) {
340            // The MAIN action means we're bringing up a blank dialer
341            // (e.g. by selecting the Home shortcut, or tabbing over from
342            // Contacts or Call log.)
343            //
344            // At this point, IF there's already an active call, there's a
345            // good chance that the user got here accidentally (but really
346            // wanted the in-call dialpad instead).  So we bring up an
347            // intermediate UI to make the user confirm what they really
348            // want to do.
349            if (phoneIsInUse()) {
350                // Log.i(TAG, "resolveIntent(): phone is in use; showing dialpad chooser!");
351                needToShowDialpadChooser = true;
352            }
353        }
354
355        // Bring up the "dialpad chooser" IFF we need to make the user
356        // confirm which dialpad they really want.
357        showDialpadChooser(needToShowDialpadChooser);
358
359        return ignoreState;
360    }
361
362    protected void setFormattedDigits(String data) {
363        // strip the non-dialable numbers out of the data string.
364        String dialString = PhoneNumberUtils.extractNetworkPortion(data);
365        dialString = PhoneNumberUtils.formatNumber(dialString);
366        if (!TextUtils.isEmpty(dialString)) {
367            Editable digits = mDigits.getText();
368            digits.replace(0, digits.length(), dialString);
369            // for some reason this isn't getting called in the digits.replace call above..
370            // but in any case, this will make sure the background drawable looks right
371            afterTextChanged(digits);
372        }
373    }
374
375    @Override
376    protected void onNewIntent(Intent newIntent) {
377        setIntent(newIntent);
378        resolveIntent();
379    }
380
381    @Override
382    protected void onPostCreate(Bundle savedInstanceState) {
383        super.onPostCreate(savedInstanceState);
384
385        // This can't be done in onCreate(), since the auto-restoring of the digits
386        // will play DTMF tones for all the old digits if it is when onRestoreSavedInstanceState()
387        // is called. This method will be called every time the activity is created, and
388        // will always happen after onRestoreSavedInstanceState().
389        mDigits.addTextChangedListener(this);
390    }
391
392    private void setupKeypad() {
393        // Setup the listeners for the buttons
394        View view = findViewById(R.id.one);
395        view.setOnClickListener(this);
396        view.setOnLongClickListener(this);
397
398        findViewById(R.id.two).setOnClickListener(this);
399        findViewById(R.id.three).setOnClickListener(this);
400        findViewById(R.id.four).setOnClickListener(this);
401        findViewById(R.id.five).setOnClickListener(this);
402        findViewById(R.id.six).setOnClickListener(this);
403        findViewById(R.id.seven).setOnClickListener(this);
404        findViewById(R.id.eight).setOnClickListener(this);
405        findViewById(R.id.nine).setOnClickListener(this);
406        findViewById(R.id.star).setOnClickListener(this);
407
408        view = findViewById(R.id.zero);
409        view.setOnClickListener(this);
410        view.setOnLongClickListener(this);
411
412        findViewById(R.id.pound).setOnClickListener(this);
413    }
414
415    @Override
416    protected void onResume() {
417        super.onResume();
418
419        // Query the last dialed number. Do it first because hitting
420        // the DB is 'slow'. This call is asynchronous.
421        queryLastOutgoingCall();
422
423        // retrieve the DTMF tone play back setting.
424        mDTMFToneEnabled = Settings.System.getInt(getContentResolver(),
425                Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
426
427        // Retrieve the haptic feedback setting.
428        mHaptic.checkSystemSetting();
429
430        // if the mToneGenerator creation fails, just continue without it.  It is
431        // a local audio signal, and is not as important as the dtmf tone itself.
432        synchronized(mToneGeneratorLock) {
433            if (mToneGenerator == null) {
434                try {
435                    // we want the user to be able to control the volume of the dial tones
436                    // outside of a call, so we use the stream type that is also mapped to the
437                    // volume control keys for this activity
438                    mToneGenerator = new ToneGenerator(DIAL_TONE_STREAM_TYPE, TONE_RELATIVE_VOLUME);
439                    setVolumeControlStream(DIAL_TONE_STREAM_TYPE);
440                } catch (RuntimeException e) {
441                    Log.w(TAG, "Exception caught while creating local tone generator: " + e);
442                    mToneGenerator = null;
443                }
444            }
445        }
446
447        Activity parent = getParent();
448        // See if we were invoked with a DIAL intent. If we were, fill in the appropriate
449        // digits in the dialer field.
450        if (parent != null && parent instanceof DialtactsActivity) {
451            Uri dialUri = ((DialtactsActivity) parent).getAndClearDialUri();
452            if (dialUri != null) {
453                resolveIntent();
454            }
455        }
456
457        // While we're in the foreground, listen for phone state changes,
458        // purely so that we can take down the "dialpad chooser" if the
459        // phone becomes idle while the chooser UI is visible.
460        TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
461        telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
462
463        // Potentially show hint text in the mDigits field when the user
464        // hasn't typed any digits yet.  (If there's already an active call,
465        // this hint text will remind the user that he's about to add a new
466        // call.)
467        //
468        // TODO: consider adding better UI for the case where *both* lines
469        // are currently in use.  (Right now we let the user try to add
470        // another call, but that call is guaranteed to fail.  Perhaps the
471        // entire dialer UI should be disabled instead.)
472        if (phoneIsInUse()) {
473            mDigits.setHint(R.string.dialerDialpadHintText);
474        } else {
475            // Common case; no hint necessary.
476            mDigits.setHint(null);
477
478            // Also, a sanity-check: the "dialpad chooser" UI should NEVER
479            // be visible if the phone is idle!
480            showDialpadChooser(false);
481        }
482
483        updateDialAndDeleteButtonEnabledState();
484    }
485
486    @Override
487    public void onWindowFocusChanged(boolean hasFocus) {
488        if (hasFocus) {
489            // Hide soft keyboard, if visible (it's fugly over button dialer).
490            // The only known case where this will be true is when launching the dialer with
491            // ACTION_DIAL via a soft keyboard.  we dismiss it here because we don't
492            // have a window token yet in onCreate / onNewIntent
493            InputMethodManager inputMethodManager = (InputMethodManager)
494                    getSystemService(Context.INPUT_METHOD_SERVICE);
495            inputMethodManager.hideSoftInputFromWindow(mDigits.getWindowToken(), 0);
496        }
497    }
498
499    @Override
500    protected void onPause() {
501        super.onPause();
502
503        // Stop listening for phone state changes.
504        TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
505        telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
506
507        synchronized(mToneGeneratorLock) {
508            if (mToneGenerator != null) {
509                mToneGenerator.release();
510                mToneGenerator = null;
511            }
512        }
513        // TODO: I wonder if we should not check if the AsyncTask that
514        // lookup the last dialed number has completed.
515        mLastNumberDialed = EMPTY_NUMBER;  // Since we are going to query again, free stale number.
516    }
517
518    @Override
519    public boolean onCreateOptionsMenu(Menu menu) {
520        mAddToContactMenuItem = menu.add(0, MENU_ADD_CONTACTS, 0, R.string.recentCalls_addToContact)
521                .setIcon(android.R.drawable.ic_menu_add);
522        m2SecPauseMenuItem = menu.add(0, MENU_2S_PAUSE, 0, R.string.add_2sec_pause)
523                .setIcon(R.drawable.ic_menu_2sec_pause);
524        mWaitMenuItem = menu.add(0, MENU_WAIT, 0, R.string.add_wait)
525                .setIcon(R.drawable.ic_menu_wait);
526        return true;
527    }
528
529    @Override
530    public boolean onPrepareOptionsMenu(Menu menu) {
531        // We never show a menu if the "choose dialpad" UI is up.
532        if (dialpadChooserVisible()) {
533            return false;
534        }
535
536        if (isDigitsEmpty()) {
537            mAddToContactMenuItem.setVisible(false);
538            m2SecPauseMenuItem.setVisible(false);
539            mWaitMenuItem.setVisible(false);
540        } else {
541            CharSequence digits = mDigits.getText();
542
543            // Put the current digits string into an intent
544            Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
545            intent.putExtra(Insert.PHONE, digits);
546            intent.setType(People.CONTENT_ITEM_TYPE);
547            mAddToContactMenuItem.setIntent(intent);
548            mAddToContactMenuItem.setVisible(true);
549
550            // Check out whether to show Pause & Wait option menu items
551            int selectionStart;
552            int selectionEnd;
553            String strDigits = digits.toString();
554
555            selectionStart = mDigits.getSelectionStart();
556            selectionEnd = mDigits.getSelectionEnd();
557
558            if (selectionStart != -1) {
559                if (selectionStart > selectionEnd) {
560                    // swap it as we want start to be less then end
561                    int tmp = selectionStart;
562                    selectionStart = selectionEnd;
563                    selectionEnd = tmp;
564                }
565
566                if (selectionStart != 0) {
567                    // Pause can be visible if cursor is not in the begining
568                    m2SecPauseMenuItem.setVisible(true);
569
570                    // For Wait to be visible set of condition to meet
571                    mWaitMenuItem.setVisible(showWait(selectionStart,
572                                                      selectionEnd, strDigits));
573                } else {
574                    // cursor in the beginning both pause and wait to be invisible
575                    m2SecPauseMenuItem.setVisible(false);
576                    mWaitMenuItem.setVisible(false);
577                }
578            } else {
579                // cursor is not selected so assume new digit is added to the end
580                int strLength = strDigits.length();
581                mWaitMenuItem.setVisible(showWait(strLength,
582                                                      strLength, strDigits));
583            }
584        }
585        return true;
586    }
587
588    @Override
589    public boolean onKeyDown(int keyCode, KeyEvent event) {
590        switch (keyCode) {
591            case KeyEvent.KEYCODE_CALL: {
592                long callPressDiff = SystemClock.uptimeMillis() - event.getDownTime();
593                if (callPressDiff >= ViewConfiguration.getLongPressTimeout()) {
594                    // Launch voice dialer
595                    Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND);
596                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
597                    try {
598                        startActivity(intent);
599                    } catch (ActivityNotFoundException e) {
600                    }
601                }
602                return true;
603            }
604            case KeyEvent.KEYCODE_1: {
605                long timeDiff = SystemClock.uptimeMillis() - event.getDownTime();
606                if (timeDiff >= ViewConfiguration.getLongPressTimeout()) {
607                    // Long press detected, call voice mail
608                    callVoicemail();
609                }
610                return true;
611            }
612        }
613        return super.onKeyDown(keyCode, event);
614    }
615
616    @Override
617    public boolean onKeyUp(int keyCode, KeyEvent event) {
618        switch (keyCode) {
619            case KeyEvent.KEYCODE_CALL: {
620                // TODO: In dialButtonPressed we do some of these
621                // tests again. We should try to consolidate them in
622                // one place.
623                if (!phoneIsCdma() && mIsAddCallMode && isDigitsEmpty()) {
624                    // For CDMA phones, we always call
625                    // dialButtonPressed() because we may need to send
626                    // an empty flash command to the network.
627                    // Otherwise, if we are adding a call from the
628                    // InCallScreen and the phone number entered is
629                    // empty, we just close the dialer to expose the
630                    // InCallScreen under it.
631                    finish();
632                }
633
634                // If we're CDMA, regardless of where we are adding a call from (either
635                // InCallScreen or Dialtacts), the user may need to send an empty
636                // flash command to the network. So let's call dialButtonPressed() regardless
637                // and dialButtonPressed will handle this functionality for us.
638                // otherwise, we place the call.
639                dialButtonPressed();
640                return true;
641            }
642        }
643        return super.onKeyUp(keyCode, event);
644    }
645
646    private void keyPressed(int keyCode) {
647        mHaptic.vibrate();
648        KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
649        mDigits.onKeyDown(keyCode, event);
650    }
651
652    public boolean onKey(View view, int keyCode, KeyEvent event) {
653        switch (view.getId()) {
654            case R.id.digits:
655                if (keyCode == KeyEvent.KEYCODE_ENTER) {
656                    dialButtonPressed();
657                    return true;
658                }
659                break;
660        }
661        return false;
662    }
663
664    public void onClick(View view) {
665        switch (view.getId()) {
666            case R.id.one: {
667                playTone(ToneGenerator.TONE_DTMF_1);
668                keyPressed(KeyEvent.KEYCODE_1);
669                return;
670            }
671            case R.id.two: {
672                playTone(ToneGenerator.TONE_DTMF_2);
673                keyPressed(KeyEvent.KEYCODE_2);
674                return;
675            }
676            case R.id.three: {
677                playTone(ToneGenerator.TONE_DTMF_3);
678                keyPressed(KeyEvent.KEYCODE_3);
679                return;
680            }
681            case R.id.four: {
682                playTone(ToneGenerator.TONE_DTMF_4);
683                keyPressed(KeyEvent.KEYCODE_4);
684                return;
685            }
686            case R.id.five: {
687                playTone(ToneGenerator.TONE_DTMF_5);
688                keyPressed(KeyEvent.KEYCODE_5);
689                return;
690            }
691            case R.id.six: {
692                playTone(ToneGenerator.TONE_DTMF_6);
693                keyPressed(KeyEvent.KEYCODE_6);
694                return;
695            }
696            case R.id.seven: {
697                playTone(ToneGenerator.TONE_DTMF_7);
698                keyPressed(KeyEvent.KEYCODE_7);
699                return;
700            }
701            case R.id.eight: {
702                playTone(ToneGenerator.TONE_DTMF_8);
703                keyPressed(KeyEvent.KEYCODE_8);
704                return;
705            }
706            case R.id.nine: {
707                playTone(ToneGenerator.TONE_DTMF_9);
708                keyPressed(KeyEvent.KEYCODE_9);
709                return;
710            }
711            case R.id.zero: {
712                playTone(ToneGenerator.TONE_DTMF_0);
713                keyPressed(KeyEvent.KEYCODE_0);
714                return;
715            }
716            case R.id.pound: {
717                playTone(ToneGenerator.TONE_DTMF_P);
718                keyPressed(KeyEvent.KEYCODE_POUND);
719                return;
720            }
721            case R.id.star: {
722                playTone(ToneGenerator.TONE_DTMF_S);
723                keyPressed(KeyEvent.KEYCODE_STAR);
724                return;
725            }
726            case R.id.deleteButton: {
727                keyPressed(KeyEvent.KEYCODE_DEL);
728                return;
729            }
730            case R.id.dialButton: {
731                mHaptic.vibrate();  // Vibrate here too, just like we do for the regular keys
732                dialButtonPressed();
733                return;
734            }
735            case R.id.voicemailButton: {
736                callVoicemail();
737                mHaptic.vibrate();
738                return;
739            }
740            case R.id.digits: {
741                if (!isDigitsEmpty()) {
742                    mDigits.setCursorVisible(true);
743                }
744                return;
745            }
746        }
747    }
748
749    public boolean onLongClick(View view) {
750        final Editable digits = mDigits.getText();
751        int id = view.getId();
752        switch (id) {
753            case R.id.deleteButton: {
754                digits.clear();
755                // TODO: The framework forgets to clear the pressed
756                // status of disabled button. Until this is fixed,
757                // clear manually the pressed status. b/2133127
758                mDelete.setPressed(false);
759                return true;
760            }
761            case R.id.one: {
762                if (isDigitsEmpty()) {
763                    callVoicemail();
764                    return true;
765                }
766                return false;
767            }
768            case R.id.zero: {
769                keyPressed(KeyEvent.KEYCODE_PLUS);
770                return true;
771            }
772        }
773        return false;
774    }
775
776    void callVoicemail() {
777        StickyTabs.saveTab(this, getIntent());
778        Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
779                Uri.fromParts("voicemail", EMPTY_NUMBER, null));
780        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
781        startActivity(intent);
782        mDigits.getText().clear();
783        finish();
784    }
785
786    void dialButtonPressed() {
787        final String number = mDigits.getText().toString();
788        boolean sendEmptyFlash = false;
789        Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED);
790
791        if (isDigitsEmpty()) { // There is no number entered.
792            if (phoneIsCdma() && phoneIsOffhook()) {
793                // On CDMA phones, if we're already on a call, pressing
794                // the Dial button without entering any digits means "send
795                // an empty flash."
796                intent.setData(Uri.fromParts("tel", EMPTY_NUMBER, null));
797                intent.putExtra(EXTRA_SEND_EMPTY_FLASH, true);
798                sendEmptyFlash = true;
799            } else if (!TextUtils.isEmpty(mLastNumberDialed)) {
800                // Otherwise, pressing the Dial button without entering
801                // any digits means "recall the last number dialed".
802                mDigits.setText(mLastNumberDialed);
803                return;
804            } else {
805                // Rare case: there's no "last number dialed".  There's
806                // nothing useful for the Dial button to do in this case.
807                playTone(ToneGenerator.TONE_PROP_NACK);
808                return;
809            }
810        } else {  // There is a number.
811            intent.setData(Uri.fromParts("tel", number, null));
812        }
813
814        StickyTabs.saveTab(this, getIntent());
815        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
816        startActivity(intent);
817        mDigits.getText().clear();
818
819        // Don't finish TwelveKeyDialer yet if we're sending a blank flash for CDMA. CDMA
820        // networks use Flash messages when special processing needs to be done, mainly for
821        // 3-way or call waiting scenarios. Presumably, here we're in a special 3-way scenario
822        // where the network needs a blank flash before being able to add the new participant.
823        // (This is not the case with all 3-way calls, just certain CDMA infrastructures.)
824        if (!sendEmptyFlash) {
825            finish();
826        }
827    }
828
829
830    /**
831     * Plays the specified tone for TONE_LENGTH_MS milliseconds.
832     *
833     * The tone is played locally, using the audio stream for phone calls.
834     * Tones are played only if the "Audible touch tones" user preference
835     * is checked, and are NOT played if the device is in silent mode.
836     *
837     * @param tone a tone code from {@link ToneGenerator}
838     */
839    void playTone(int tone) {
840        // if local tone playback is disabled, just return.
841        if (!mDTMFToneEnabled) {
842            return;
843        }
844
845        // Also do nothing if the phone is in silent mode.
846        // We need to re-check the ringer mode for *every* playTone()
847        // call, rather than keeping a local flag that's updated in
848        // onResume(), since it's possible to toggle silent mode without
849        // leaving the current activity (via the ENDCALL-longpress menu.)
850        AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
851        int ringerMode = audioManager.getRingerMode();
852        if ((ringerMode == AudioManager.RINGER_MODE_SILENT)
853            || (ringerMode == AudioManager.RINGER_MODE_VIBRATE)) {
854            return;
855        }
856
857        synchronized(mToneGeneratorLock) {
858            if (mToneGenerator == null) {
859                Log.w(TAG, "playTone: mToneGenerator == null, tone: "+tone);
860                return;
861            }
862
863            // Start the new tone (will stop any playing tone)
864            mToneGenerator.startTone(tone, TONE_LENGTH_MS);
865        }
866    }
867
868    /**
869     * Brings up the "dialpad chooser" UI in place of the usual Dialer
870     * elements (the textfield/button and the dialpad underneath).
871     *
872     * We show this UI if the user brings up the Dialer while a call is
873     * already in progress, since there's a good chance we got here
874     * accidentally (and the user really wanted the in-call dialpad instead).
875     * So in this situation we display an intermediate UI that lets the user
876     * explicitly choose between the in-call dialpad ("Use touch tone
877     * keypad") and the regular Dialer ("Add call").  (Or, the option "Return
878     * to call in progress" just goes back to the in-call UI with no dialpad
879     * at all.)
880     *
881     * @param enabled If true, show the "dialpad chooser" instead
882     *                of the regular Dialer UI
883     */
884    private void showDialpadChooser(boolean enabled) {
885        if (enabled) {
886            // Log.i(TAG, "Showing dialpad chooser!");
887            mDigits.setVisibility(View.GONE);
888            if (mDialpad != null) mDialpad.setVisibility(View.GONE);
889            mVoicemailDialAndDeleteRow.setVisibility(View.GONE);
890            mDialpadChooser.setVisibility(View.VISIBLE);
891
892            // Instantiate the DialpadChooserAdapter and hook it up to the
893            // ListView.  We do this only once.
894            if (mDialpadChooserAdapter == null) {
895                mDialpadChooserAdapter = new DialpadChooserAdapter(this);
896                mDialpadChooser.setAdapter(mDialpadChooserAdapter);
897            }
898        } else {
899            // Log.i(TAG, "Displaying normal Dialer UI.");
900            mDigits.setVisibility(View.VISIBLE);
901            if (mDialpad != null) mDialpad.setVisibility(View.VISIBLE);
902            mVoicemailDialAndDeleteRow.setVisibility(View.VISIBLE);
903            mDialpadChooser.setVisibility(View.GONE);
904        }
905    }
906
907    /**
908     * @return true if we're currently showing the "dialpad chooser" UI.
909     */
910    private boolean dialpadChooserVisible() {
911        return mDialpadChooser.getVisibility() == View.VISIBLE;
912    }
913
914    /**
915     * Simple list adapter, binding to an icon + text label
916     * for each item in the "dialpad chooser" list.
917     */
918    private static class DialpadChooserAdapter extends BaseAdapter {
919        private LayoutInflater mInflater;
920
921        // Simple struct for a single "choice" item.
922        static class ChoiceItem {
923            String text;
924            Bitmap icon;
925            int id;
926
927            public ChoiceItem(String s, Bitmap b, int i) {
928                text = s;
929                icon = b;
930                id = i;
931            }
932        }
933
934        // IDs for the possible "choices":
935        static final int DIALPAD_CHOICE_USE_DTMF_DIALPAD = 101;
936        static final int DIALPAD_CHOICE_RETURN_TO_CALL = 102;
937        static final int DIALPAD_CHOICE_ADD_NEW_CALL = 103;
938
939        private static final int NUM_ITEMS = 3;
940        private ChoiceItem mChoiceItems[] = new ChoiceItem[NUM_ITEMS];
941
942        public DialpadChooserAdapter(Context context) {
943            // Cache the LayoutInflate to avoid asking for a new one each time.
944            mInflater = LayoutInflater.from(context);
945
946            // Initialize the possible choices.
947            // TODO: could this be specified entirely in XML?
948
949            // - "Use touch tone keypad"
950            mChoiceItems[0] = new ChoiceItem(
951                    context.getString(R.string.dialer_useDtmfDialpad),
952                    BitmapFactory.decodeResource(context.getResources(),
953                                                 R.drawable.ic_dialer_fork_tt_keypad),
954                    DIALPAD_CHOICE_USE_DTMF_DIALPAD);
955
956            // - "Return to call in progress"
957            mChoiceItems[1] = new ChoiceItem(
958                    context.getString(R.string.dialer_returnToInCallScreen),
959                    BitmapFactory.decodeResource(context.getResources(),
960                                                 R.drawable.ic_dialer_fork_current_call),
961                    DIALPAD_CHOICE_RETURN_TO_CALL);
962
963            // - "Add call"
964            mChoiceItems[2] = new ChoiceItem(
965                    context.getString(R.string.dialer_addAnotherCall),
966                    BitmapFactory.decodeResource(context.getResources(),
967                                                 R.drawable.ic_dialer_fork_add_call),
968                    DIALPAD_CHOICE_ADD_NEW_CALL);
969        }
970
971        public int getCount() {
972            return NUM_ITEMS;
973        }
974
975        /**
976         * Return the ChoiceItem for a given position.
977         */
978        public Object getItem(int position) {
979            return mChoiceItems[position];
980        }
981
982        /**
983         * Return a unique ID for each possible choice.
984         */
985        public long getItemId(int position) {
986            return position;
987        }
988
989        /**
990         * Make a view for each row.
991         */
992        public View getView(int position, View convertView, ViewGroup parent) {
993            // When convertView is non-null, we can reuse it (there's no need
994            // to reinflate it.)
995            if (convertView == null) {
996                convertView = mInflater.inflate(R.layout.dialpad_chooser_list_item, null);
997            }
998
999            TextView text = (TextView) convertView.findViewById(R.id.text);
1000            text.setText(mChoiceItems[position].text);
1001
1002            ImageView icon = (ImageView) convertView.findViewById(R.id.icon);
1003            icon.setImageBitmap(mChoiceItems[position].icon);
1004
1005            return convertView;
1006        }
1007    }
1008
1009    /**
1010     * Handle clicks from the dialpad chooser.
1011     */
1012    public void onItemClick(AdapterView parent, View v, int position, long id) {
1013        DialpadChooserAdapter.ChoiceItem item =
1014                (DialpadChooserAdapter.ChoiceItem) parent.getItemAtPosition(position);
1015        int itemId = item.id;
1016        switch (itemId) {
1017            case DialpadChooserAdapter.DIALPAD_CHOICE_USE_DTMF_DIALPAD:
1018                // Log.i(TAG, "DIALPAD_CHOICE_USE_DTMF_DIALPAD");
1019                // Fire off an intent to go back to the in-call UI
1020                // with the dialpad visible.
1021                returnToInCallScreen(true);
1022                break;
1023
1024            case DialpadChooserAdapter.DIALPAD_CHOICE_RETURN_TO_CALL:
1025                // Log.i(TAG, "DIALPAD_CHOICE_RETURN_TO_CALL");
1026                // Fire off an intent to go back to the in-call UI
1027                // (with the dialpad hidden).
1028                returnToInCallScreen(false);
1029                break;
1030
1031            case DialpadChooserAdapter.DIALPAD_CHOICE_ADD_NEW_CALL:
1032                // Log.i(TAG, "DIALPAD_CHOICE_ADD_NEW_CALL");
1033                // Ok, guess the user really did want to be here (in the
1034                // regular Dialer) after all.  Bring back the normal Dialer UI.
1035                showDialpadChooser(false);
1036                break;
1037
1038            default:
1039                Log.w(TAG, "onItemClick: unexpected itemId: " + itemId);
1040                break;
1041        }
1042    }
1043
1044    /**
1045     * Returns to the in-call UI (where there's presumably a call in
1046     * progress) in response to the user selecting "use touch tone keypad"
1047     * or "return to call" from the dialpad chooser.
1048     */
1049    private void returnToInCallScreen(boolean showDialpad) {
1050        try {
1051            ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
1052            if (phone != null) phone.showCallScreenWithDialpad(showDialpad);
1053        } catch (RemoteException e) {
1054            Log.w(TAG, "phone.showCallScreenWithDialpad() failed", e);
1055        }
1056
1057        // Finally, finish() ourselves so that we don't stay on the
1058        // activity stack.
1059        // Note that we do this whether or not the showCallScreenWithDialpad()
1060        // call above had any effect or not!  (That call is a no-op if the
1061        // phone is idle, which can happen if the current call ends while
1062        // the dialpad chooser is up.  In this case we can't show the
1063        // InCallScreen, and there's no point staying here in the Dialer,
1064        // so we just take the user back where he came from...)
1065        finish();
1066    }
1067
1068    /**
1069     * @return true if the phone is "in use", meaning that at least one line
1070     *              is active (ie. off hook or ringing or dialing).
1071     */
1072    private boolean phoneIsInUse() {
1073        boolean phoneInUse = false;
1074        try {
1075            ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
1076            if (phone != null) phoneInUse = !phone.isIdle();
1077        } catch (RemoteException e) {
1078            Log.w(TAG, "phone.isIdle() failed", e);
1079        }
1080        return phoneInUse;
1081    }
1082
1083    /**
1084     * @return true if the phone is a CDMA phone type
1085     */
1086    private boolean phoneIsCdma() {
1087        boolean isCdma = false;
1088        try {
1089            ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
1090            if (phone != null) {
1091                isCdma = (phone.getActivePhoneType() == TelephonyManager.PHONE_TYPE_CDMA);
1092            }
1093        } catch (RemoteException e) {
1094            Log.w(TAG, "phone.getActivePhoneType() failed", e);
1095        }
1096        return isCdma;
1097    }
1098
1099    /**
1100     * @return true if the phone state is OFFHOOK
1101     */
1102    private boolean phoneIsOffhook() {
1103        boolean phoneOffhook = false;
1104        try {
1105            ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
1106            if (phone != null) phoneOffhook = phone.isOffhook();
1107        } catch (RemoteException e) {
1108            Log.w(TAG, "phone.isOffhook() failed", e);
1109        }
1110        return phoneOffhook;
1111    }
1112
1113
1114    /**
1115     * Returns true whenever any one of the options from the menu is selected.
1116     * Code changes to support dialpad options
1117     */
1118    @Override
1119    public boolean onOptionsItemSelected(MenuItem item) {
1120        switch (item.getItemId()) {
1121            case MENU_2S_PAUSE:
1122                updateDialString(",");
1123                return true;
1124            case MENU_WAIT:
1125                updateDialString(";");
1126                return true;
1127        }
1128        return false;
1129    }
1130
1131    /**
1132     * Updates the dial string (mDigits) after inserting a Pause character (,)
1133     * or Wait character (;).
1134     */
1135    private void updateDialString(String newDigits) {
1136        int selectionStart;
1137        int selectionEnd;
1138
1139        // SpannableStringBuilder editable_text = new SpannableStringBuilder(mDigits.getText());
1140        int anchor = mDigits.getSelectionStart();
1141        int point = mDigits.getSelectionEnd();
1142
1143        selectionStart = Math.min(anchor, point);
1144        selectionEnd = Math.max(anchor, point);
1145
1146        Editable digits = mDigits.getText();
1147        if (selectionStart != -1 ) {
1148            if (selectionStart == selectionEnd) {
1149                // then there is no selection. So insert the pause at this
1150                // position and update the mDigits.
1151                digits.replace(selectionStart, selectionStart, newDigits);
1152            } else {
1153                digits.replace(selectionStart, selectionEnd, newDigits);
1154                // Unselect: back to a regular cursor, just pass the character inserted.
1155                mDigits.setSelection(selectionStart + 1);
1156            }
1157        } else {
1158            int len = mDigits.length();
1159            digits.replace(len, len, newDigits);
1160        }
1161    }
1162
1163    /**
1164     * Update the enabledness of the "Dial" and "Backspace" buttons if applicable.
1165     */
1166    private void updateDialAndDeleteButtonEnabledState() {
1167        final boolean digitsNotEmpty = !isDigitsEmpty();
1168
1169        if (mDialButton != null) {
1170            // On CDMA phones, if we're already on a call, we *always*
1171            // enable the Dial button (since you can press it without
1172            // entering any digits to send an empty flash.)
1173            if (phoneIsCdma() && phoneIsOffhook()) {
1174                mDialButton.setEnabled(true);
1175            } else {
1176                // Common case: GSM, or CDMA but not on a call.
1177                // Enable the Dial button if some digits have
1178                // been entered, or if there is a last dialed number
1179                // that could be redialed.
1180                mDialButton.setEnabled(digitsNotEmpty ||
1181                                       !TextUtils.isEmpty(mLastNumberDialed));
1182            }
1183        }
1184        mDelete.setEnabled(digitsNotEmpty);
1185    }
1186
1187
1188    /**
1189     * Check if voicemail is enabled/accessible.
1190     */
1191    private void initVoicemailButton() {
1192        boolean hasVoicemail = false;
1193        try {
1194            hasVoicemail = TelephonyManager.getDefault().getVoiceMailNumber() != null;
1195        } catch (SecurityException se) {
1196            // Possibly no READ_PHONE_STATE privilege.
1197        }
1198
1199        mVoicemailButton = mVoicemailDialAndDeleteRow.findViewById(R.id.voicemailButton);
1200        if (hasVoicemail) {
1201            mVoicemailButton.setOnClickListener(this);
1202        } else {
1203            mVoicemailButton.setEnabled(false);
1204        }
1205    }
1206
1207    /**
1208     * This function return true if Wait menu item can be shown
1209     * otherwise returns false. Assumes the passed string is non-empty
1210     * and the 0th index check is not required.
1211     */
1212    private boolean showWait(int start, int end, String digits) {
1213        if (start == end) {
1214            // visible false in this case
1215            if (start > digits.length()) return false;
1216
1217            // preceding char is ';', so visible should be false
1218            if (digits.charAt(start-1) == ';') return false;
1219
1220            // next char is ';', so visible should be false
1221            if ((digits.length() > start) && (digits.charAt(start) == ';')) return false;
1222        } else {
1223            // visible false in this case
1224            if (start > digits.length() || end > digits.length()) return false;
1225
1226            // In this case we need to just check for ';' preceding to start
1227            // or next to end
1228            if (digits.charAt(start-1) == ';') return false;
1229        }
1230        return true;
1231    }
1232
1233    /**
1234     * @return true if the widget with the phone number digits is empty.
1235     */
1236    private boolean isDigitsEmpty() {
1237        return mDigits.length() == 0;
1238    }
1239
1240    /**
1241     * Starts the asyn query to get the last dialed/outgoing
1242     * number. When the background query finishes, mLastNumberDialed
1243     * is set to the last dialed number or an empty string if none
1244     * exists yet.
1245     */
1246    private void queryLastOutgoingCall() {
1247        mLastNumberDialed = EMPTY_NUMBER;
1248        CallLogAsync.GetLastOutgoingCallArgs lastCallArgs =
1249                new CallLogAsync.GetLastOutgoingCallArgs(
1250                    this,
1251                    new CallLogAsync.OnLastOutgoingCallComplete() {
1252                        public void lastOutgoingCall(String number) {
1253                            // TODO: Filter out emergency numbers if
1254                            // the carrier does not want redial for
1255                            // these.
1256                            mLastNumberDialed = number;
1257                            updateDialAndDeleteButtonEnabledState();
1258                        }
1259                    });
1260        mCallLog.getLastOutgoingCall(lastCallArgs);
1261    }
1262
1263    @Override
1264    public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
1265            boolean globalSearch) {
1266        if (globalSearch) {
1267            super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
1268        } else {
1269            ContactsSearchManager.startSearch(this, initialQuery);
1270        }
1271    }
1272}
1273