blob: 7dcba10ebc037133ef855647fa9e10b7fedff206 [file] [log] [blame]
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -08001/*
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 android.app.Activity;
20import android.content.ActivityNotFoundException;
21import android.content.Context;
22import android.content.Intent;
23import android.content.res.Resources;
24import android.database.Cursor;
25import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.graphics.drawable.Drawable;
28import android.media.AudioManager;
29import android.media.ToneGenerator;
30import android.net.Uri;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.Message;
34import android.os.RemoteException;
35import android.os.ServiceManager;
36import android.os.SystemClock;
David Brownc29c7ab2009-07-07 16:00:18 -070037import android.os.Vibrator;
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -080038import android.provider.Contacts.Intents.Insert;
39import android.provider.Contacts.People;
40import android.provider.Contacts.Phones;
41import android.provider.Contacts.PhonesColumns;
42import android.provider.Settings;
43import android.telephony.PhoneNumberFormattingTextWatcher;
44import android.telephony.PhoneNumberUtils;
45import android.telephony.PhoneStateListener;
46import android.telephony.TelephonyManager;
47import android.text.Editable;
Reli Talc2a2a512009-06-10 16:48:00 -040048import android.text.SpannableStringBuilder;
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -080049import android.text.TextUtils;
50import android.text.TextWatcher;
51import android.text.method.DialerKeyListener;
52import android.util.Log;
53import android.view.KeyEvent;
54import android.view.LayoutInflater;
55import android.view.Menu;
56import android.view.MenuItem;
57import android.view.View;
58import android.view.ViewConfiguration;
59import android.view.ViewGroup;
Karl Rosaenf46bc312009-03-24 18:20:48 -070060import android.view.inputmethod.InputMethodManager;
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -080061import android.widget.AdapterView;
62import android.widget.BaseAdapter;
63import android.widget.EditText;
64import android.widget.ImageView;
65import android.widget.ListView;
66import android.widget.TextView;
67
68import com.android.internal.telephony.ITelephony;
69
70/**
71 * Dialer activity that displays the typical twelve key interface.
72 */
73public class TwelveKeyDialer extends Activity implements View.OnClickListener,
74 View.OnLongClickListener, View.OnKeyListener,
75 AdapterView.OnItemClickListener, TextWatcher {
76
77 private static final String TAG = "TwelveKeyDialer";
78
79 private static final int STOP_TONE = 1;
80
81 /** The length of DTMF tones in milliseconds */
82 private static final int TONE_LENGTH_MS = 150;
83
84 /** The DTMF tone volume relative to other sounds in the stream */
85 private static final int TONE_RELATIVE_VOLUME = 50;
86
87 private EditText mDigits;
88 private View mDelete;
89 private MenuItem mAddToContactMenuItem;
90 private ToneGenerator mToneGenerator;
91 private Object mToneGeneratorLock = new Object();
92 private Drawable mDigitsBackground;
93 private Drawable mDigitsEmptyBackground;
94 private Drawable mDeleteBackground;
95 private Drawable mDeleteEmptyBackground;
96 private View mDigitsAndBackspace;
97 private View mDialpad;
98 private ListView mDialpadChooser;
99 private DialpadChooserAdapter mDialpadChooserAdapter;
Reli Talc2a2a512009-06-10 16:48:00 -0400100 //Member variables for dialpad options
101 private MenuItem m2SecPauseMenuItem;
102 private MenuItem mWaitMenuItem;
103 private static final int MENU_ADD_CONTACTS = 1;
104 private static final int MENU_2S_PAUSE = 2;
105 private static final int MENU_WAIT = 3;
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800106
107 // determines if we want to playback local DTMF tones.
108 private boolean mDTMFToneEnabled;
David Brownc29c7ab2009-07-07 16:00:18 -0700109
110 // Vibration (haptic feedback) for dialer key presses.
111 private Vibrator mVibrator;
112 private boolean mVibrateOn;
113 private long mVibrateDuration;
114
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800115 /** Identifier for the "Add Call" intent extra. */
116 static final String ADD_CALL_MODE_KEY = "add_call_mode";
117 /** Indicates if we are opening this dialer to add a call from the InCallScreen. */
118 private boolean mIsAddCallMode;
119
120 PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
121 /**
122 * Listen for phone state changes so that we can take down the
123 * "dialpad chooser" if the phone becomes idle while the
124 * chooser UI is visible.
125 */
126 @Override
127 public void onCallStateChanged(int state, String incomingNumber) {
128 // Log.i(TAG, "PhoneStateListener.onCallStateChanged: "
129 // + state + ", '" + incomingNumber + "'");
130 if ((state == TelephonyManager.CALL_STATE_IDLE) && dialpadChooserVisible()) {
131 // Log.i(TAG, "Call ended with dialpad chooser visible! Taking it down...");
132 // Note there's a race condition in the UI here: the
133 // dialpad chooser could conceivably disappear (on its
134 // own) at the exact moment the user was trying to select
135 // one of the choices, which would be confusing. (But at
136 // least that's better than leaving the dialpad chooser
137 // onscreen, but useless...)
138 showDialpadChooser(false);
139 }
140 }
141 };
142
143 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
144 // Do nothing
145 }
146
147 public void onTextChanged(CharSequence input, int start, int before, int changeCount) {
148 // Do nothing
149 // DTMF Tones do not need to be played here any longer -
150 // the DTMF dialer handles that functionality now.
151 }
152
153 public void afterTextChanged(Editable input) {
154 if (SpecialCharSequenceMgr.handleChars(this, input.toString(), mDigits)) {
155 // A special sequence was entered, clear the digits
156 mDigits.getText().clear();
157 }
158
159 // Set the proper background for the dial input area
160 if (mDigits.length() != 0) {
161 mDelete.setBackgroundDrawable(mDeleteBackground);
162 mDigits.setBackgroundDrawable(mDigitsBackground);
163 mDigits.setCompoundDrawablesWithIntrinsicBounds(
164 getResources().getDrawable(R.drawable.ic_dial_number), null, null, null);
165 } else {
166 mDelete.setBackgroundDrawable(mDeleteEmptyBackground);
167 mDigits.setBackgroundDrawable(mDigitsEmptyBackground);
168 mDigits.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
169 }
170 }
171
172 @Override
173 protected void onCreate(Bundle icicle) {
174 super.onCreate(icicle);
175
176 // Set the content view
177 setContentView(getContentViewResource());
178
179 // Load up the resources for the text field and delete button
180 Resources r = getResources();
181 mDigitsBackground = r.getDrawable(R.drawable.btn_dial_textfield_active);
182 //mDigitsBackground.setDither(true);
183 mDigitsEmptyBackground = r.getDrawable(R.drawable.btn_dial_textfield);
184 //mDigitsEmptyBackground.setDither(true);
185 mDeleteBackground = r.getDrawable(R.drawable.btn_dial_delete_active);
186 //mDeleteBackground.setDither(true);
187 mDeleteEmptyBackground = r.getDrawable(R.drawable.btn_dial_delete);
188 //mDeleteEmptyBackground.setDither(true);
189
190 mDigits = (EditText) findViewById(R.id.digits);
191 mDigits.setKeyListener(DialerKeyListener.getInstance());
192 mDigits.setOnClickListener(this);
193 mDigits.setOnKeyListener(this);
194 maybeAddNumberFormatting();
195
196 // Check for the presence of the keypad
197 View view = findViewById(R.id.one);
198 if (view != null) {
199 setupKeypad();
200 }
201
202 view = findViewById(R.id.backspace);
203 view.setOnClickListener(this);
204 view.setOnLongClickListener(this);
205 mDelete = view;
206
207 mDigitsAndBackspace = (View) findViewById(R.id.digitsAndBackspace);
208 mDialpad = (View) findViewById(R.id.dialpad); // This is null in landscape mode
209
210 // Set up the "dialpad chooser" UI; see showDialpadChooser().
211 mDialpadChooser = (ListView) findViewById(R.id.dialpadChooser);
212 mDialpadChooser.setOnItemClickListener(this);
213
214 if (!resolveIntent() && icicle != null) {
215 super.onRestoreInstanceState(icicle);
216 }
217
218 // If the mToneGenerator creation fails, just continue without it. It is
219 // a local audio signal, and is not as important as the dtmf tone itself.
220 synchronized (mToneGeneratorLock) {
221 if (mToneGenerator == null) {
222 try {
223 mToneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL,
224 TONE_RELATIVE_VOLUME);
225 } catch (RuntimeException e) {
226 Log.w(TAG, "Exception caught while creating local tone generator: " + e);
227 mToneGenerator = null;
228 }
229 }
230 }
David Brownc29c7ab2009-07-07 16:00:18 -0700231
232 // Initialize vibration parameters.
233 // TODO: We might eventually need to make mVibrateOn come from a
234 // user preference rather than a per-platform resource, in which
235 // case we would need to update it in onResume() rather than here.
236 mVibrateOn = r.getBoolean(R.bool.config_enable_dialer_key_vibration);
237 mVibrateDuration = (long) r.getInteger(R.integer.config_dialer_key_vibrate_duration);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800238 }
239
240 @Override
241 protected void onDestroy() {
242 super.onDestroy();
243 synchronized(mToneGeneratorLock) {
244 if (mToneGenerator != null) {
245 mToneStopper.removeMessages(STOP_TONE);
246 mToneGenerator.release();
247 mToneGenerator = null;
248 }
249 }
250 }
251
252 @Override
253 protected void onRestoreInstanceState(Bundle icicle) {
254 // Do nothing, state is restored in onCreate() if needed
255 }
256
257 protected void maybeAddNumberFormatting() {
258 mDigits.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
259 }
260
261 /**
262 * Overridden by subclasses to control the resource used by the content view.
263 */
264 protected int getContentViewResource() {
265 return R.layout.twelve_key_dialer;
266 }
267
268 private boolean resolveIntent() {
269 boolean ignoreState = false;
270
271 // Find the proper intent
272 final Intent intent;
273 if (isChild()) {
274 intent = getParent().getIntent();
275 ignoreState = intent.getBooleanExtra(DialtactsActivity.EXTRA_IGNORE_STATE, false);
276 } else {
277 intent = getIntent();
278 }
279 // Log.i(TAG, "==> resolveIntent(): intent: " + intent);
280
281 // by default we are not adding a call.
282 mIsAddCallMode = false;
283
284 // By default we don't show the "dialpad chooser" UI.
285 boolean needToShowDialpadChooser = false;
286
287 // Resolve the intent
288 final String action = intent.getAction();
289 if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
290 // see if we are "adding a call" from the InCallScreen; false by default.
291 mIsAddCallMode = intent.getBooleanExtra(ADD_CALL_MODE_KEY, false);
292 Uri uri = intent.getData();
293 if (uri != null) {
294 if ("tel".equals(uri.getScheme())) {
295 // Put the requested number into the input area
296 String data = uri.getSchemeSpecificPart();
297 setFormattedDigits(data);
298 } else {
299 String type = intent.getType();
300 if (People.CONTENT_ITEM_TYPE.equals(type)
301 || Phones.CONTENT_ITEM_TYPE.equals(type)) {
302 // Query the phone number
303 Cursor c = getContentResolver().query(intent.getData(),
304 new String[] {PhonesColumns.NUMBER}, null, null, null);
305 if (c != null) {
306 if (c.moveToFirst()) {
307 // Put the number into the input area
308 setFormattedDigits(c.getString(0));
309 }
310 c.close();
311 }
312 }
313 }
314 }
315 } else if (Intent.ACTION_MAIN.equals(action)) {
316 // The MAIN action means we're bringing up a blank dialer
317 // (e.g. by selecting the Home shortcut, or tabbing over from
318 // Contacts or Call log.)
319 //
320 // At this point, IF there's already an active call, there's a
321 // good chance that the user got here accidentally (but really
322 // wanted the in-call dialpad instead). So we bring up an
323 // intermediate UI to make the user confirm what they really
324 // want to do.
325 if (phoneIsInUse()) {
326 // Log.i(TAG, "resolveIntent(): phone is in use; showing dialpad chooser!");
327 needToShowDialpadChooser = true;
328 }
329 }
330
331 // Bring up the "dialpad chooser" IFF we need to make the user
332 // confirm which dialpad they really want.
333 showDialpadChooser(needToShowDialpadChooser);
334
335 return ignoreState;
336 }
337
338 protected void setFormattedDigits(String data) {
339 // strip the non-dialable numbers out of the data string.
340 String dialString = PhoneNumberUtils.extractNetworkPortion(data);
341 dialString = PhoneNumberUtils.formatNumber(dialString);
342 if (!TextUtils.isEmpty(dialString)) {
343 Editable digits = mDigits.getText();
344 digits.replace(0, digits.length(), dialString);
Karl Rosaenf46bc312009-03-24 18:20:48 -0700345 // for some reason this isn't getting called in the digits.replace call above..
346 // but in any case, this will make sure the background drawable looks right
347 afterTextChanged(digits);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800348 }
349 }
350
351 @Override
352 protected void onNewIntent(Intent newIntent) {
353 setIntent(newIntent);
354 resolveIntent();
355 }
356
357 @Override
358 protected void onPostCreate(Bundle savedInstanceState) {
359 super.onPostCreate(savedInstanceState);
360
361 // This can't be done in onCreate(), since the auto-restoring of the digits
362 // will play DTMF tones for all the old digits if it is when onRestoreSavedInstanceState()
363 // is called. This method will be called every time the activity is created, and
364 // will always happen after onRestoreSavedInstanceState().
365 mDigits.addTextChangedListener(this);
366 }
367
368 private void setupKeypad() {
369 // Setup the listeners for the buttons
370 View view = findViewById(R.id.one);
371 view.setOnClickListener(this);
372 view.setOnLongClickListener(this);
373
374 findViewById(R.id.two).setOnClickListener(this);
375 findViewById(R.id.three).setOnClickListener(this);
376 findViewById(R.id.four).setOnClickListener(this);
377 findViewById(R.id.five).setOnClickListener(this);
378 findViewById(R.id.six).setOnClickListener(this);
379 findViewById(R.id.seven).setOnClickListener(this);
380 findViewById(R.id.eight).setOnClickListener(this);
381 findViewById(R.id.nine).setOnClickListener(this);
382 findViewById(R.id.star).setOnClickListener(this);
383
384 view = findViewById(R.id.zero);
385 view.setOnClickListener(this);
386 view.setOnLongClickListener(this);
387
388 findViewById(R.id.pound).setOnClickListener(this);
389 }
390
391 @Override
392 protected void onResume() {
393 super.onResume();
David Brownc29c7ab2009-07-07 16:00:18 -0700394
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800395 // retrieve the DTMF tone play back setting.
396 mDTMFToneEnabled = Settings.System.getInt(getContentResolver(),
397 Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
398
399 // if the mToneGenerator creation fails, just continue without it. It is
400 // a local audio signal, and is not as important as the dtmf tone itself.
401 synchronized(mToneGeneratorLock) {
402 if (mToneGenerator == null) {
403 try {
404 mToneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL,
405 TONE_RELATIVE_VOLUME);
406 } catch (RuntimeException e) {
407 Log.w(TAG, "Exception caught while creating local tone generator: " + e);
408 mToneGenerator = null;
409 }
410 }
411 }
412
413 Activity parent = getParent();
414 // See if we were invoked with a DIAL intent. If we were, fill in the appropriate
415 // digits in the dialer field.
416 if (parent != null && parent instanceof DialtactsActivity) {
417 Uri dialUri = ((DialtactsActivity) parent).getAndClearDialUri();
418 if (dialUri != null) {
419 resolveIntent();
420 }
421 }
422
423 // While we're in the foreground, listen for phone state changes,
424 // purely so that we can take down the "dialpad chooser" if the
425 // phone becomes idle while the chooser UI is visible.
426 TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
427 telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
428
429 // Potentially show hint text in the mDigits field when the user
430 // hasn't typed any digits yet. (If there's already an active call,
431 // this hint text will remind the user that he's about to add a new
432 // call.)
433 //
434 // TODO: consider adding better UI for the case where *both* lines
435 // are currently in use. (Right now we let the user try to add
436 // another call, but that call is guaranteed to fail. Perhaps the
437 // entire dialer UI should be disabled instead.)
438 if (phoneIsInUse()) {
439 mDigits.setHint(R.string.dialerDialpadHintText);
440 } else {
441 // Common case; no hint necessary.
442 mDigits.setHint(null);
443
444 // Also, a sanity-check: the "dialpad chooser" UI should NEVER
445 // be visible if the phone is idle!
446 showDialpadChooser(false);
447 }
448 }
449
450 @Override
Karl Rosaenf46bc312009-03-24 18:20:48 -0700451 public void onWindowFocusChanged(boolean hasFocus) {
452 if (hasFocus) {
453 // Hide soft keyboard, if visible (it's fugly over button dialer).
454 // The only known case where this will be true is when launching the dialer with
455 // ACTION_DIAL via a soft keyboard. we dismiss it here because we don't
456 // have a window token yet in onCreate / onNewIntent
457 InputMethodManager inputMethodManager = (InputMethodManager)
458 getSystemService(Context.INPUT_METHOD_SERVICE);
459 inputMethodManager.hideSoftInputFromWindow(mDigits.getWindowToken(), 0);
460 }
461 }
462
463 @Override
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800464 protected void onPause() {
465 super.onPause();
466
467 // Stop listening for phone state changes.
468 TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
469 telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
470
471 synchronized(mToneGeneratorLock) {
472 if (mToneGenerator != null) {
473 mToneStopper.removeMessages(STOP_TONE);
474 mToneGenerator.release();
475 mToneGenerator = null;
476 }
477 }
478 }
479
480 @Override
481 public boolean onCreateOptionsMenu(Menu menu) {
Reli Talc2a2a512009-06-10 16:48:00 -0400482 mAddToContactMenuItem = menu.add(0, MENU_ADD_CONTACTS, 0, R.string.recentCalls_addToContact)
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800483 .setIcon(android.R.drawable.ic_menu_add);
Reli Talc2a2a512009-06-10 16:48:00 -0400484 m2SecPauseMenuItem = menu.add(0, MENU_2S_PAUSE, 0, R.string.add_2sec_pause)
485 .setIcon(R.drawable.ic_menu_2sec_pause);
486 mWaitMenuItem = menu.add(0, MENU_WAIT, 0, R.string.add_wait)
487 .setIcon(R.drawable.ic_menu_wait);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800488 return true;
489 }
490
491 @Override
492 public boolean onPrepareOptionsMenu(Menu menu) {
493 // We never show a menu if the "choose dialpad" UI is up.
494 if (dialpadChooserVisible()) {
495 return false;
496 }
497
498 CharSequence digits = mDigits.getText();
499 if (digits == null || !TextUtils.isGraphic(digits)) {
500 mAddToContactMenuItem.setVisible(false);
Reli Talc2a2a512009-06-10 16:48:00 -0400501 m2SecPauseMenuItem.setVisible(false);
502 mWaitMenuItem.setVisible(false);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800503 } else {
504 // Put the current digits string into an intent
505 Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
506 intent.putExtra(Insert.PHONE, mDigits.getText());
507 intent.setType(People.CONTENT_ITEM_TYPE);
508 mAddToContactMenuItem.setIntent(intent);
509 mAddToContactMenuItem.setVisible(true);
Reli Talc2a2a512009-06-10 16:48:00 -0400510
511 // Check out whether to show Pause & Wait option menu items
512 int selectionStart;
513 int selectionEnd;
514 String strDigits = digits.toString();
515
516 selectionStart = mDigits.getSelectionStart();
517 selectionEnd = mDigits.getSelectionEnd();
518
519 if (selectionStart != -1) {
520 if (selectionStart > selectionEnd) {
521 // swap it as we want start to be less then end
522 int tmp = selectionStart;
523 selectionStart = selectionEnd;
524 selectionEnd = tmp;
525 }
526
527 if (selectionStart != 0) {
528 // Pause can be visible if cursor is not in the begining
529 m2SecPauseMenuItem.setVisible(true);
530
531 // For Wait to be visible set of condition to meet
532 mWaitMenuItem.setVisible(showWait(selectionStart,
533 selectionEnd, strDigits));
534 } else {
535 // cursor in the beginning both pause and wait to be invisible
536 m2SecPauseMenuItem.setVisible(false);
537 mWaitMenuItem.setVisible(false);
538 }
539 } else {
540 // cursor is not selected so assume new digit is added to the end
541 int strLength = strDigits.length();
542 mWaitMenuItem.setVisible(showWait(strLength,
543 strLength, strDigits));
544 }
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800545 }
546 return true;
547 }
548
549 @Override
550 public boolean onKeyDown(int keyCode, KeyEvent event) {
551 switch (keyCode) {
552 case KeyEvent.KEYCODE_CALL: {
553 long callPressDiff = SystemClock.uptimeMillis() - event.getDownTime();
554 if (callPressDiff >= ViewConfiguration.getLongPressTimeout()) {
555 // Launch voice dialer
556 Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND);
557 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
558 try {
559 startActivity(intent);
560 } catch (ActivityNotFoundException e) {
561 }
562 }
563 return true;
564 }
565 case KeyEvent.KEYCODE_1: {
566 long timeDiff = SystemClock.uptimeMillis() - event.getDownTime();
567 if (timeDiff >= ViewConfiguration.getLongPressTimeout()) {
568 // Long press detected, call voice mail
569 callVoicemail();
570 }
571 return true;
572 }
573 }
574 return super.onKeyDown(keyCode, event);
575 }
576
577 @Override
578 public boolean onKeyUp(int keyCode, KeyEvent event) {
579 switch (keyCode) {
580 case KeyEvent.KEYCODE_CALL: {
581 if (mIsAddCallMode && (TextUtils.isEmpty(mDigits.getText().toString()))) {
582 // if we are adding a call from the InCallScreen and the phone
583 // number entered is empty, we just close the dialer to expose
584 // the InCallScreen under it.
585 finish();
586 } else {
587 // otherwise, we place the call.
588 placeCall();
589 }
590 return true;
591 }
592 }
593 return super.onKeyUp(keyCode, event);
594 }
595
596 private void keyPressed(int keyCode) {
David Brownc29c7ab2009-07-07 16:00:18 -0700597 vibrate();
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800598 KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
599 mDigits.onKeyDown(keyCode, event);
600 }
601
602 public boolean onKey(View view, int keyCode, KeyEvent event) {
603 switch (view.getId()) {
604 case R.id.digits:
605 if (keyCode == KeyEvent.KEYCODE_ENTER) {
606 placeCall();
607 return true;
608 }
609 break;
610 }
611 return false;
612 }
613
614 public void onClick(View view) {
615 switch (view.getId()) {
616 case R.id.one: {
617 playTone(ToneGenerator.TONE_DTMF_1);
618 keyPressed(KeyEvent.KEYCODE_1);
619 return;
620 }
621 case R.id.two: {
622 playTone(ToneGenerator.TONE_DTMF_2);
623 keyPressed(KeyEvent.KEYCODE_2);
624 return;
625 }
626 case R.id.three: {
627 playTone(ToneGenerator.TONE_DTMF_3);
628 keyPressed(KeyEvent.KEYCODE_3);
629 return;
630 }
631 case R.id.four: {
632 playTone(ToneGenerator.TONE_DTMF_4);
633 keyPressed(KeyEvent.KEYCODE_4);
634 return;
635 }
636 case R.id.five: {
637 playTone(ToneGenerator.TONE_DTMF_5);
638 keyPressed(KeyEvent.KEYCODE_5);
639 return;
640 }
641 case R.id.six: {
642 playTone(ToneGenerator.TONE_DTMF_6);
643 keyPressed(KeyEvent.KEYCODE_6);
644 return;
645 }
646 case R.id.seven: {
647 playTone(ToneGenerator.TONE_DTMF_7);
648 keyPressed(KeyEvent.KEYCODE_7);
649 return;
650 }
651 case R.id.eight: {
652 playTone(ToneGenerator.TONE_DTMF_8);
653 keyPressed(KeyEvent.KEYCODE_8);
654 return;
655 }
656 case R.id.nine: {
657 playTone(ToneGenerator.TONE_DTMF_9);
658 keyPressed(KeyEvent.KEYCODE_9);
659 return;
660 }
661 case R.id.zero: {
662 playTone(ToneGenerator.TONE_DTMF_0);
663 keyPressed(KeyEvent.KEYCODE_0);
664 return;
665 }
666 case R.id.pound: {
667 playTone(ToneGenerator.TONE_DTMF_P);
668 keyPressed(KeyEvent.KEYCODE_POUND);
669 return;
670 }
671 case R.id.star: {
672 playTone(ToneGenerator.TONE_DTMF_S);
673 keyPressed(KeyEvent.KEYCODE_STAR);
674 return;
675 }
676 case R.id.backspace: {
677 keyPressed(KeyEvent.KEYCODE_DEL);
678 return;
679 }
680 case R.id.digits: {
David Brownc29c7ab2009-07-07 16:00:18 -0700681 vibrate(); // Vibrate here too, just like we do for the regular keys
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800682 placeCall();
683 return;
684 }
685 }
686 }
687
688 public boolean onLongClick(View view) {
689 final Editable digits = mDigits.getText();
690 int id = view.getId();
691 switch (id) {
692 case R.id.backspace: {
693 digits.clear();
694 return true;
695 }
696 case R.id.one: {
697 if (digits.length() == 0) {
698 callVoicemail();
699 return true;
700 }
701 return false;
702 }
703 case R.id.zero: {
704 keyPressed(KeyEvent.KEYCODE_PLUS);
705 return true;
706 }
707 }
708 return false;
709 }
710
711 void callVoicemail() {
712 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
713 Uri.fromParts("voicemail", "", null));
714 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
715 startActivity(intent);
716 mDigits.getText().clear();
717 finish();
718 }
719
720 void placeCall() {
721 final String number = mDigits.getText().toString();
722 if (number == null || !TextUtils.isGraphic(number)) {
723 // There is no number entered.
724 playTone(ToneGenerator.TONE_PROP_NACK);
725 return;
726 }
727 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
728 Uri.fromParts("tel", number, null));
729 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
730 startActivity(intent);
731 mDigits.getText().clear();
732 finish();
733 }
734
735 Handler mToneStopper = new Handler() {
736 @Override
737 public void handleMessage(Message msg) {
738 switch (msg.what) {
739 case STOP_TONE:
740 synchronized(mToneGeneratorLock) {
741 if (mToneGenerator == null) {
742 Log.w(TAG, "mToneStopper: mToneGenerator == null");
743 } else {
744 mToneGenerator.stopTone();
745 }
746 }
747 break;
748 }
749 }
750 };
751
752 /**
David Brown22f615f2009-06-25 16:19:19 -0700753 * Plays the specified tone for TONE_LENGTH_MS milliseconds.
754 *
755 * The tone is played locally, using the audio stream for phone calls.
756 * Tones are played only if the "Audible touch tones" user preference
757 * is checked, and are NOT played if the device is in silent mode.
758 *
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800759 * @param tone a tone code from {@link ToneGenerator}
760 */
761 void playTone(int tone) {
762 // if local tone playback is disabled, just return.
763 if (!mDTMFToneEnabled) {
764 return;
765 }
David Brown22f615f2009-06-25 16:19:19 -0700766
767 // Also do nothing if the phone is in silent mode.
768 // We need to re-check the ringer mode for *every* playTone()
769 // call, rather than keeping a local flag that's updated in
770 // onResume(), since it's possible to toggle silent mode without
771 // leaving the current activity (via the ENDCALL-longpress menu.)
772 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
David Brownd5a15302009-07-20 16:39:47 -0700773 int ringerMode = audioManager.getRingerMode();
774 if ((ringerMode == AudioManager.RINGER_MODE_SILENT)
775 || (ringerMode == AudioManager.RINGER_MODE_VIBRATE)) {
David Brown22f615f2009-06-25 16:19:19 -0700776 return;
777 }
778
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800779 synchronized(mToneGeneratorLock) {
780 if (mToneGenerator == null) {
781 Log.w(TAG, "playTone: mToneGenerator == null, tone: "+tone);
782 return;
783 }
784
785 // Remove pending STOP_TONE messages
786 mToneStopper.removeMessages(STOP_TONE);
787
788 // Start the new tone (will stop any playing tone)
789 mToneGenerator.startTone(tone);
790 mToneStopper.sendEmptyMessageDelayed(STOP_TONE, TONE_LENGTH_MS);
791 }
792 }
793
794 /**
795 * Brings up the "dialpad chooser" UI in place of the usual Dialer
796 * elements (the textfield/button and the dialpad underneath).
797 *
798 * We show this UI if the user brings up the Dialer while a call is
799 * already in progress, since there's a good chance we got here
800 * accidentally (and the user really wanted the in-call dialpad instead).
801 * So in this situation we display an intermediate UI that lets the user
802 * explicitly choose between the in-call dialpad ("Use touch tone
803 * keypad") and the regular Dialer ("Add call"). (Or, the option "Return
804 * to call in progress" just goes back to the in-call UI with no dialpad
805 * at all.)
806 *
807 * @param enabled If true, show the "dialpad chooser" instead
808 * of the regular Dialer UI
809 */
810 private void showDialpadChooser(boolean enabled) {
811 if (enabled) {
812 // Log.i(TAG, "Showing dialpad chooser!");
813 mDigitsAndBackspace.setVisibility(View.GONE);
814 if (mDialpad != null) mDialpad.setVisibility(View.GONE);
815 mDialpadChooser.setVisibility(View.VISIBLE);
816
817 // Instantiate the DialpadChooserAdapter and hook it up to the
818 // ListView. We do this only once.
819 if (mDialpadChooserAdapter == null) {
820 mDialpadChooserAdapter = new DialpadChooserAdapter(this);
821 mDialpadChooser.setAdapter(mDialpadChooserAdapter);
822 }
823 } else {
824 // Log.i(TAG, "Displaying normal Dialer UI.");
825 mDigitsAndBackspace.setVisibility(View.VISIBLE);
826 if (mDialpad != null) mDialpad.setVisibility(View.VISIBLE);
827 mDialpadChooser.setVisibility(View.GONE);
828 }
829 }
830
831 /**
832 * @return true if we're currently showing the "dialpad chooser" UI.
833 */
834 private boolean dialpadChooserVisible() {
835 return mDialpadChooser.getVisibility() == View.VISIBLE;
836 }
837
838 /**
839 * Simple list adapter, binding to an icon + text label
840 * for each item in the "dialpad chooser" list.
841 */
842 private static class DialpadChooserAdapter extends BaseAdapter {
843 private LayoutInflater mInflater;
844
845 // Simple struct for a single "choice" item.
846 static class ChoiceItem {
847 String text;
848 Bitmap icon;
849 int id;
850
851 public ChoiceItem(String s, Bitmap b, int i) {
852 text = s;
853 icon = b;
854 id = i;
855 }
856 }
857
858 // IDs for the possible "choices":
859 static final int DIALPAD_CHOICE_USE_DTMF_DIALPAD = 101;
860 static final int DIALPAD_CHOICE_RETURN_TO_CALL = 102;
861 static final int DIALPAD_CHOICE_ADD_NEW_CALL = 103;
862
863 private static final int NUM_ITEMS = 3;
864 private ChoiceItem mChoiceItems[] = new ChoiceItem[NUM_ITEMS];
865
866 public DialpadChooserAdapter(Context context) {
867 // Cache the LayoutInflate to avoid asking for a new one each time.
868 mInflater = LayoutInflater.from(context);
869
870 // Initialize the possible choices.
871 // TODO: could this be specified entirely in XML?
872
873 // - "Use touch tone keypad"
874 mChoiceItems[0] = new ChoiceItem(
875 context.getString(R.string.dialer_useDtmfDialpad),
876 BitmapFactory.decodeResource(context.getResources(),
877 R.drawable.ic_dialer_fork_tt_keypad),
878 DIALPAD_CHOICE_USE_DTMF_DIALPAD);
879
880 // - "Return to call in progress"
881 mChoiceItems[1] = new ChoiceItem(
882 context.getString(R.string.dialer_returnToInCallScreen),
883 BitmapFactory.decodeResource(context.getResources(),
884 R.drawable.ic_dialer_fork_current_call),
885 DIALPAD_CHOICE_RETURN_TO_CALL);
886
887 // - "Add call"
888 mChoiceItems[2] = new ChoiceItem(
889 context.getString(R.string.dialer_addAnotherCall),
890 BitmapFactory.decodeResource(context.getResources(),
891 R.drawable.ic_dialer_fork_add_call),
892 DIALPAD_CHOICE_ADD_NEW_CALL);
893 }
894
895 public int getCount() {
896 return NUM_ITEMS;
897 }
898
899 /**
900 * Return the ChoiceItem for a given position.
901 */
902 public Object getItem(int position) {
903 return mChoiceItems[position];
904 }
905
906 /**
907 * Return a unique ID for each possible choice.
908 */
909 public long getItemId(int position) {
910 return position;
911 }
912
913 /**
914 * Make a view for each row.
915 */
916 public View getView(int position, View convertView, ViewGroup parent) {
917 // When convertView is non-null, we can reuse it (there's no need
918 // to reinflate it.)
919 if (convertView == null) {
920 convertView = mInflater.inflate(R.layout.dialpad_chooser_list_item, null);
921 }
922
923 TextView text = (TextView) convertView.findViewById(R.id.text);
924 text.setText(mChoiceItems[position].text);
925
926 ImageView icon = (ImageView) convertView.findViewById(R.id.icon);
927 icon.setImageBitmap(mChoiceItems[position].icon);
928
929 return convertView;
930 }
931 }
932
933 /**
934 * Handle clicks from the dialpad chooser.
935 */
936 public void onItemClick(AdapterView parent, View v, int position, long id) {
937 DialpadChooserAdapter.ChoiceItem item =
938 (DialpadChooserAdapter.ChoiceItem) parent.getItemAtPosition(position);
939 int itemId = item.id;
940 switch (itemId) {
941 case DialpadChooserAdapter.DIALPAD_CHOICE_USE_DTMF_DIALPAD:
942 // Log.i(TAG, "DIALPAD_CHOICE_USE_DTMF_DIALPAD");
943 // Fire off an intent to go back to the in-call UI
944 // with the dialpad visible.
945 returnToInCallScreen(true);
946 break;
947
948 case DialpadChooserAdapter.DIALPAD_CHOICE_RETURN_TO_CALL:
949 // Log.i(TAG, "DIALPAD_CHOICE_RETURN_TO_CALL");
950 // Fire off an intent to go back to the in-call UI
951 // (with the dialpad hidden).
952 returnToInCallScreen(false);
953 break;
954
955 case DialpadChooserAdapter.DIALPAD_CHOICE_ADD_NEW_CALL:
956 // Log.i(TAG, "DIALPAD_CHOICE_ADD_NEW_CALL");
957 // Ok, guess the user really did want to be here (in the
958 // regular Dialer) after all. Bring back the normal Dialer UI.
959 showDialpadChooser(false);
960 break;
961
962 default:
963 Log.w(TAG, "onItemClick: unexpected itemId: " + itemId);
964 break;
965 }
966 }
967
968 /**
969 * Returns to the in-call UI (where there's presumably a call in
970 * progress) in response to the user selecting "use touch tone keypad"
971 * or "return to call" from the dialpad chooser.
972 */
973 private void returnToInCallScreen(boolean showDialpad) {
974 try {
975 ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
976 if (phone != null) phone.showCallScreenWithDialpad(showDialpad);
977 } catch (RemoteException e) {
978 Log.w(TAG, "phone.showCallScreenWithDialpad() failed", e);
979 }
980
981 // Finally, finish() ourselves so that we don't stay on the
982 // activity stack.
983 // Note that we do this whether or not the showCallScreenWithDialpad()
984 // call above had any effect or not! (That call is a no-op if the
985 // phone is idle, which can happen if the current call ends while
986 // the dialpad chooser is up. In this case we can't show the
987 // InCallScreen, and there's no point staying here in the Dialer,
988 // so we just take the user back where he came from...)
989 finish();
990 }
991
992 /**
993 * @return true if the phone is "in use", meaning that at least one line
994 * is active (ie. off hook or ringing or dialing).
995 */
996 private boolean phoneIsInUse() {
997 boolean phoneInUse = false;
998 try {
999 ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
1000 if (phone != null) phoneInUse = !phone.isIdle();
1001 } catch (RemoteException e) {
1002 Log.w(TAG, "phone.isIdle() failed", e);
1003 }
1004 return phoneInUse;
1005 }
David Brownc29c7ab2009-07-07 16:00:18 -07001006
1007 /**
1008 * Triggers haptic feedback (if enabled) for dialer key presses.
1009 */
1010 private synchronized void vibrate() {
1011 if (!mVibrateOn) {
1012 return;
1013 }
1014 if (mVibrator == null) {
1015 mVibrator = new Vibrator();
1016 }
1017 mVibrator.vibrate(mVibrateDuration);
1018 }
Reli Talc2a2a512009-06-10 16:48:00 -04001019
1020 /**
1021 * Returns true whenever any one of the options from the menu is selected.
1022 * Code changes to support dialpad options
1023 */
1024 @Override
1025 public boolean onOptionsItemSelected(MenuItem item) {
1026 switch (item.getItemId()) {
1027 case MENU_2S_PAUSE:
1028 updateDialString(",");
1029 return true;
1030 case MENU_WAIT:
1031 updateDialString(";");
1032 return true;
1033 }
1034 return false;
1035 }
1036
1037 /**
1038 * Updates the dial string (mDigits) after inserting a Pause character (,)
1039 * or Wait character (;).
1040 */
1041 private void updateDialString(String newDigits) {
1042 int selectionStart;
1043 int selectionEnd;
1044
1045 // SpannableStringBuilder editable_text = new SpannableStringBuilder(mDigits.getText());
1046 selectionStart = mDigits.getSelectionStart();
1047 selectionEnd = mDigits.getSelectionEnd();
1048
1049 Editable digits = mDigits.getText();
1050 if (selectionStart != -1 ) {
1051 if (selectionStart == selectionEnd) {
1052 // then there is no selection. So insert the pause at this
1053 // position and update the mDigits.
1054 digits.replace(selectionStart, selectionStart, newDigits);
1055 } else {
1056 digits.delete(selectionStart, selectionEnd);
1057 digits.replace(selectionStart, selectionStart, newDigits);
1058 }
1059 } else {
1060 int len = mDigits.length();
1061 digits.replace(len, len, newDigits);
1062 }
1063 }
1064
1065 /**
1066 * This function return true if Wait menu item can be shown
1067 * otherwise returns false. Assumes the passed string is non-empty
1068 * and the 0th index check is not required.
1069 */
1070 private boolean showWait(int start, int end, String digits) {
1071 if (start == end) {
1072 // visible false in this case
1073 if (start > digits.length()) return false;
1074
1075 // preceding char is ';', so visible should be false
1076 if (digits.charAt(start-1) == ';') return false;
1077
1078 // next char is ';', so visible should be false
1079 if ((digits.length() > start) && (digits.charAt(start) == ';')) return false;
1080 } else {
1081 // visible false in this case
1082 if (start > digits.length() || end > digits.length()) return false;
1083
1084 // In this case we need to just check for ';' preceding to start
1085 // or next to end
1086 if (digits.charAt(start-1) == ';') return false;
1087 }
1088 return true;
1089 }
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -08001090}