blob: ebb76c0d7d7ae01e81343966d788a4ae00e85d3a [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";
Eric Laurentd9efc872009-07-17 11:52:06 -070078
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -080079 /** The length of DTMF tones in milliseconds */
80 private static final int TONE_LENGTH_MS = 150;
Eric Laurentd9efc872009-07-17 11:52:06 -070081
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -080082 /** The DTMF tone volume relative to other sounds in the stream */
83 private static final int TONE_RELATIVE_VOLUME = 50;
84
85 private EditText mDigits;
86 private View mDelete;
87 private MenuItem mAddToContactMenuItem;
88 private ToneGenerator mToneGenerator;
89 private Object mToneGeneratorLock = new Object();
90 private Drawable mDigitsBackground;
91 private Drawable mDigitsEmptyBackground;
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -080092 private View mDialpad;
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -070093 private View mVoicemailDialAndDeleteRow;
Nicolas Catania80bda0f2009-09-19 09:17:14 -070094 private View mVoicemailButton;
95 private View mDialButton;
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -080096 private ListView mDialpadChooser;
97 private DialpadChooserAdapter mDialpadChooserAdapter;
Reli Talc2a2a512009-06-10 16:48:00 -040098 //Member variables for dialpad options
99 private MenuItem m2SecPauseMenuItem;
100 private MenuItem mWaitMenuItem;
101 private static final int MENU_ADD_CONTACTS = 1;
102 private static final int MENU_2S_PAUSE = 2;
103 private static final int MENU_WAIT = 3;
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800104
105 // determines if we want to playback local DTMF tones.
106 private boolean mDTMFToneEnabled;
David Brownc29c7ab2009-07-07 16:00:18 -0700107
108 // Vibration (haptic feedback) for dialer key presses.
109 private Vibrator mVibrator;
110 private boolean mVibrateOn;
111 private long mVibrateDuration;
112
Eric Laurentd9efc872009-07-17 11:52:06 -0700113
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800114 /** Identifier for the "Add Call" intent extra. */
115 static final String ADD_CALL_MODE_KEY = "add_call_mode";
116 /** Indicates if we are opening this dialer to add a call from the InCallScreen. */
117 private boolean mIsAddCallMode;
118
119 PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
120 /**
121 * Listen for phone state changes so that we can take down the
122 * "dialpad chooser" if the phone becomes idle while the
123 * chooser UI is visible.
124 */
125 @Override
126 public void onCallStateChanged(int state, String incomingNumber) {
127 // Log.i(TAG, "PhoneStateListener.onCallStateChanged: "
128 // + state + ", '" + incomingNumber + "'");
129 if ((state == TelephonyManager.CALL_STATE_IDLE) && dialpadChooserVisible()) {
130 // Log.i(TAG, "Call ended with dialpad chooser visible! Taking it down...");
131 // Note there's a race condition in the UI here: the
132 // dialpad chooser could conceivably disappear (on its
133 // own) at the exact moment the user was trying to select
134 // one of the choices, which would be confusing. (But at
135 // least that's better than leaving the dialpad chooser
136 // onscreen, but useless...)
137 showDialpadChooser(false);
138 }
139 }
140 };
141
142 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
143 // Do nothing
144 }
145
146 public void onTextChanged(CharSequence input, int start, int before, int changeCount) {
147 // Do nothing
Eric Laurentd9efc872009-07-17 11:52:06 -0700148 // DTMF Tones do not need to be played here any longer -
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800149 // the DTMF dialer handles that functionality now.
150 }
151
152 public void afterTextChanged(Editable input) {
153 if (SpecialCharSequenceMgr.handleChars(this, input.toString(), mDigits)) {
154 // A special sequence was entered, clear the digits
155 mDigits.getText().clear();
156 }
157
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -0700158 updateDialAndDeleteButtonStateEnabledAttr();
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800159 }
160
161 @Override
162 protected void onCreate(Bundle icicle) {
163 super.onCreate(icicle);
164
165 // Set the content view
166 setContentView(getContentViewResource());
167
168 // Load up the resources for the text field and delete button
169 Resources r = getResources();
170 mDigitsBackground = r.getDrawable(R.drawable.btn_dial_textfield_active);
171 //mDigitsBackground.setDither(true);
172 mDigitsEmptyBackground = r.getDrawable(R.drawable.btn_dial_textfield);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800173
174 mDigits = (EditText) findViewById(R.id.digits);
175 mDigits.setKeyListener(DialerKeyListener.getInstance());
176 mDigits.setOnClickListener(this);
177 mDigits.setOnKeyListener(this);
178 maybeAddNumberFormatting();
179
180 // Check for the presence of the keypad
181 View view = findViewById(R.id.one);
182 if (view != null) {
183 setupKeypad();
184 }
185
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -0700186 mVoicemailDialAndDeleteRow = findViewById(R.id.voicemailAndDialAndDelete);
Nicolas Cataniadea164e2009-09-18 06:26:16 -0700187
Nicolas Catania80bda0f2009-09-19 09:17:14 -0700188 initVoicemailButton();
189
David Brown3d07e6d2009-08-04 20:30:09 -0700190 // Check whether we should show the onscreen "Dial" button.
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -0700191 mDialButton = mVoicemailDialAndDeleteRow.findViewById(R.id.dialButton);
Nicolas Cataniadea164e2009-09-18 06:26:16 -0700192
David Brown3d07e6d2009-08-04 20:30:09 -0700193 if (r.getBoolean(R.bool.config_show_onscreen_dial_button)) {
David Brown3d07e6d2009-08-04 20:30:09 -0700194 mDialButton.setOnClickListener(this);
Nicolas Cataniadea164e2009-09-18 06:26:16 -0700195 } else {
196 mDialButton.setVisibility(View.GONE); // It's VISIBLE by default
197 mDialButton = null;
David Brown3d07e6d2009-08-04 20:30:09 -0700198 }
199
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -0700200 view = mVoicemailDialAndDeleteRow.findViewById(R.id.deleteButton);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800201 view.setOnClickListener(this);
202 view.setOnLongClickListener(this);
203 mDelete = view;
204
Dmitri Plotnikov032bb362009-05-06 17:05:39 -0700205 mDialpad = findViewById(R.id.dialpad); // This is null in landscape mode
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800206
207 // Set up the "dialpad chooser" UI; see showDialpadChooser().
208 mDialpadChooser = (ListView) findViewById(R.id.dialpadChooser);
209 mDialpadChooser.setOnItemClickListener(this);
210
211 if (!resolveIntent() && icicle != null) {
212 super.onRestoreInstanceState(icicle);
213 }
214
David Brownc29c7ab2009-07-07 16:00:18 -0700215 // Initialize vibration parameters.
216 // TODO: We might eventually need to make mVibrateOn come from a
217 // user preference rather than a per-platform resource, in which
218 // case we would need to update it in onResume() rather than here.
219 mVibrateOn = r.getBoolean(R.bool.config_enable_dialer_key_vibration);
220 mVibrateDuration = (long) r.getInteger(R.integer.config_dialer_key_vibrate_duration);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800221 }
222
223 @Override
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800224 protected void onRestoreInstanceState(Bundle icicle) {
225 // Do nothing, state is restored in onCreate() if needed
226 }
Eric Laurentd9efc872009-07-17 11:52:06 -0700227
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800228 protected void maybeAddNumberFormatting() {
229 mDigits.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
230 }
Eric Laurentd9efc872009-07-17 11:52:06 -0700231
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800232 /**
Eric Laurentd9efc872009-07-17 11:52:06 -0700233 * Overridden by subclasses to control the resource used by the content view.
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800234 */
235 protected int getContentViewResource() {
236 return R.layout.twelve_key_dialer;
237 }
238
239 private boolean resolveIntent() {
240 boolean ignoreState = false;
241
242 // Find the proper intent
243 final Intent intent;
244 if (isChild()) {
245 intent = getParent().getIntent();
246 ignoreState = intent.getBooleanExtra(DialtactsActivity.EXTRA_IGNORE_STATE, false);
247 } else {
248 intent = getIntent();
249 }
250 // Log.i(TAG, "==> resolveIntent(): intent: " + intent);
251
252 // by default we are not adding a call.
253 mIsAddCallMode = false;
254
255 // By default we don't show the "dialpad chooser" UI.
256 boolean needToShowDialpadChooser = false;
257
258 // Resolve the intent
259 final String action = intent.getAction();
260 if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
261 // see if we are "adding a call" from the InCallScreen; false by default.
262 mIsAddCallMode = intent.getBooleanExtra(ADD_CALL_MODE_KEY, false);
263 Uri uri = intent.getData();
264 if (uri != null) {
265 if ("tel".equals(uri.getScheme())) {
266 // Put the requested number into the input area
267 String data = uri.getSchemeSpecificPart();
268 setFormattedDigits(data);
269 } else {
270 String type = intent.getType();
271 if (People.CONTENT_ITEM_TYPE.equals(type)
272 || Phones.CONTENT_ITEM_TYPE.equals(type)) {
273 // Query the phone number
274 Cursor c = getContentResolver().query(intent.getData(),
275 new String[] {PhonesColumns.NUMBER}, null, null, null);
276 if (c != null) {
277 if (c.moveToFirst()) {
278 // Put the number into the input area
279 setFormattedDigits(c.getString(0));
280 }
281 c.close();
282 }
283 }
284 }
285 }
286 } else if (Intent.ACTION_MAIN.equals(action)) {
287 // The MAIN action means we're bringing up a blank dialer
288 // (e.g. by selecting the Home shortcut, or tabbing over from
289 // Contacts or Call log.)
290 //
291 // At this point, IF there's already an active call, there's a
292 // good chance that the user got here accidentally (but really
293 // wanted the in-call dialpad instead). So we bring up an
294 // intermediate UI to make the user confirm what they really
295 // want to do.
296 if (phoneIsInUse()) {
297 // Log.i(TAG, "resolveIntent(): phone is in use; showing dialpad chooser!");
298 needToShowDialpadChooser = true;
299 }
300 }
301
302 // Bring up the "dialpad chooser" IFF we need to make the user
303 // confirm which dialpad they really want.
304 showDialpadChooser(needToShowDialpadChooser);
305
306 return ignoreState;
307 }
308
309 protected void setFormattedDigits(String data) {
310 // strip the non-dialable numbers out of the data string.
311 String dialString = PhoneNumberUtils.extractNetworkPortion(data);
312 dialString = PhoneNumberUtils.formatNumber(dialString);
313 if (!TextUtils.isEmpty(dialString)) {
314 Editable digits = mDigits.getText();
315 digits.replace(0, digits.length(), dialString);
Karl Rosaenf46bc312009-03-24 18:20:48 -0700316 // for some reason this isn't getting called in the digits.replace call above..
317 // but in any case, this will make sure the background drawable looks right
318 afterTextChanged(digits);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800319 }
320 }
321
322 @Override
323 protected void onNewIntent(Intent newIntent) {
324 setIntent(newIntent);
325 resolveIntent();
326 }
Eric Laurentd9efc872009-07-17 11:52:06 -0700327
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800328 @Override
329 protected void onPostCreate(Bundle savedInstanceState) {
330 super.onPostCreate(savedInstanceState);
331
332 // This can't be done in onCreate(), since the auto-restoring of the digits
333 // will play DTMF tones for all the old digits if it is when onRestoreSavedInstanceState()
334 // is called. This method will be called every time the activity is created, and
335 // will always happen after onRestoreSavedInstanceState().
336 mDigits.addTextChangedListener(this);
337 }
Eric Laurentd9efc872009-07-17 11:52:06 -0700338
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800339 private void setupKeypad() {
340 // Setup the listeners for the buttons
341 View view = findViewById(R.id.one);
342 view.setOnClickListener(this);
343 view.setOnLongClickListener(this);
344
345 findViewById(R.id.two).setOnClickListener(this);
346 findViewById(R.id.three).setOnClickListener(this);
347 findViewById(R.id.four).setOnClickListener(this);
348 findViewById(R.id.five).setOnClickListener(this);
349 findViewById(R.id.six).setOnClickListener(this);
350 findViewById(R.id.seven).setOnClickListener(this);
351 findViewById(R.id.eight).setOnClickListener(this);
352 findViewById(R.id.nine).setOnClickListener(this);
353 findViewById(R.id.star).setOnClickListener(this);
354
355 view = findViewById(R.id.zero);
356 view.setOnClickListener(this);
357 view.setOnLongClickListener(this);
358
359 findViewById(R.id.pound).setOnClickListener(this);
360 }
361
362 @Override
363 protected void onResume() {
364 super.onResume();
David Brownc29c7ab2009-07-07 16:00:18 -0700365
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800366 // retrieve the DTMF tone play back setting.
367 mDTMFToneEnabled = Settings.System.getInt(getContentResolver(),
368 Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
369
Eric Laurentd9efc872009-07-17 11:52:06 -0700370 // if the mToneGenerator creation fails, just continue without it. It is
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800371 // a local audio signal, and is not as important as the dtmf tone itself.
372 synchronized(mToneGeneratorLock) {
373 if (mToneGenerator == null) {
374 try {
Eric Laurentd9efc872009-07-17 11:52:06 -0700375 mToneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF,
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800376 TONE_RELATIVE_VOLUME);
377 } catch (RuntimeException e) {
378 Log.w(TAG, "Exception caught while creating local tone generator: " + e);
379 mToneGenerator = null;
380 }
381 }
382 }
Eric Laurentd9efc872009-07-17 11:52:06 -0700383
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800384 Activity parent = getParent();
385 // See if we were invoked with a DIAL intent. If we were, fill in the appropriate
386 // digits in the dialer field.
387 if (parent != null && parent instanceof DialtactsActivity) {
388 Uri dialUri = ((DialtactsActivity) parent).getAndClearDialUri();
389 if (dialUri != null) {
390 resolveIntent();
391 }
392 }
393
394 // While we're in the foreground, listen for phone state changes,
395 // purely so that we can take down the "dialpad chooser" if the
396 // phone becomes idle while the chooser UI is visible.
397 TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
398 telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
399
400 // Potentially show hint text in the mDigits field when the user
401 // hasn't typed any digits yet. (If there's already an active call,
402 // this hint text will remind the user that he's about to add a new
403 // call.)
404 //
405 // TODO: consider adding better UI for the case where *both* lines
406 // are currently in use. (Right now we let the user try to add
407 // another call, but that call is guaranteed to fail. Perhaps the
408 // entire dialer UI should be disabled instead.)
409 if (phoneIsInUse()) {
410 mDigits.setHint(R.string.dialerDialpadHintText);
411 } else {
412 // Common case; no hint necessary.
413 mDigits.setHint(null);
414
415 // Also, a sanity-check: the "dialpad chooser" UI should NEVER
416 // be visible if the phone is idle!
417 showDialpadChooser(false);
418 }
Nicolas Cataniadea164e2009-09-18 06:26:16 -0700419
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -0700420 updateDialAndDeleteButtonStateEnabledAttr();
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800421 }
422
423 @Override
Karl Rosaenf46bc312009-03-24 18:20:48 -0700424 public void onWindowFocusChanged(boolean hasFocus) {
425 if (hasFocus) {
426 // Hide soft keyboard, if visible (it's fugly over button dialer).
427 // The only known case where this will be true is when launching the dialer with
428 // ACTION_DIAL via a soft keyboard. we dismiss it here because we don't
429 // have a window token yet in onCreate / onNewIntent
430 InputMethodManager inputMethodManager = (InputMethodManager)
431 getSystemService(Context.INPUT_METHOD_SERVICE);
Eric Laurentd9efc872009-07-17 11:52:06 -0700432 inputMethodManager.hideSoftInputFromWindow(mDigits.getWindowToken(), 0);
Karl Rosaenf46bc312009-03-24 18:20:48 -0700433 }
434 }
435
436 @Override
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800437 protected void onPause() {
438 super.onPause();
439
440 // Stop listening for phone state changes.
441 TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
442 telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
443
444 synchronized(mToneGeneratorLock) {
445 if (mToneGenerator != null) {
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800446 mToneGenerator.release();
447 mToneGenerator = null;
448 }
449 }
450 }
451
452 @Override
453 public boolean onCreateOptionsMenu(Menu menu) {
Reli Talc2a2a512009-06-10 16:48:00 -0400454 mAddToContactMenuItem = menu.add(0, MENU_ADD_CONTACTS, 0, R.string.recentCalls_addToContact)
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800455 .setIcon(android.R.drawable.ic_menu_add);
Reli Talc2a2a512009-06-10 16:48:00 -0400456 m2SecPauseMenuItem = menu.add(0, MENU_2S_PAUSE, 0, R.string.add_2sec_pause)
457 .setIcon(R.drawable.ic_menu_2sec_pause);
458 mWaitMenuItem = menu.add(0, MENU_WAIT, 0, R.string.add_wait)
459 .setIcon(R.drawable.ic_menu_wait);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800460 return true;
461 }
462
463 @Override
464 public boolean onPrepareOptionsMenu(Menu menu) {
465 // We never show a menu if the "choose dialpad" UI is up.
466 if (dialpadChooserVisible()) {
467 return false;
468 }
469
470 CharSequence digits = mDigits.getText();
471 if (digits == null || !TextUtils.isGraphic(digits)) {
472 mAddToContactMenuItem.setVisible(false);
Reli Talc2a2a512009-06-10 16:48:00 -0400473 m2SecPauseMenuItem.setVisible(false);
474 mWaitMenuItem.setVisible(false);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800475 } else {
476 // Put the current digits string into an intent
477 Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
478 intent.putExtra(Insert.PHONE, mDigits.getText());
479 intent.setType(People.CONTENT_ITEM_TYPE);
480 mAddToContactMenuItem.setIntent(intent);
481 mAddToContactMenuItem.setVisible(true);
Reli Talc2a2a512009-06-10 16:48:00 -0400482
483 // Check out whether to show Pause & Wait option menu items
484 int selectionStart;
485 int selectionEnd;
486 String strDigits = digits.toString();
487
488 selectionStart = mDigits.getSelectionStart();
489 selectionEnd = mDigits.getSelectionEnd();
490
491 if (selectionStart != -1) {
492 if (selectionStart > selectionEnd) {
493 // swap it as we want start to be less then end
494 int tmp = selectionStart;
495 selectionStart = selectionEnd;
496 selectionEnd = tmp;
497 }
498
499 if (selectionStart != 0) {
500 // Pause can be visible if cursor is not in the begining
501 m2SecPauseMenuItem.setVisible(true);
502
503 // For Wait to be visible set of condition to meet
504 mWaitMenuItem.setVisible(showWait(selectionStart,
505 selectionEnd, strDigits));
506 } else {
507 // cursor in the beginning both pause and wait to be invisible
508 m2SecPauseMenuItem.setVisible(false);
509 mWaitMenuItem.setVisible(false);
510 }
511 } else {
512 // cursor is not selected so assume new digit is added to the end
513 int strLength = strDigits.length();
514 mWaitMenuItem.setVisible(showWait(strLength,
515 strLength, strDigits));
516 }
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800517 }
518 return true;
519 }
520
521 @Override
522 public boolean onKeyDown(int keyCode, KeyEvent event) {
523 switch (keyCode) {
524 case KeyEvent.KEYCODE_CALL: {
525 long callPressDiff = SystemClock.uptimeMillis() - event.getDownTime();
526 if (callPressDiff >= ViewConfiguration.getLongPressTimeout()) {
527 // Launch voice dialer
528 Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND);
529 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
530 try {
531 startActivity(intent);
532 } catch (ActivityNotFoundException e) {
533 }
534 }
535 return true;
536 }
537 case KeyEvent.KEYCODE_1: {
Eric Laurentd9efc872009-07-17 11:52:06 -0700538 long timeDiff = SystemClock.uptimeMillis() - event.getDownTime();
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800539 if (timeDiff >= ViewConfiguration.getLongPressTimeout()) {
540 // Long press detected, call voice mail
541 callVoicemail();
542 }
543 return true;
544 }
545 }
546 return super.onKeyDown(keyCode, event);
547 }
548
549 @Override
550 public boolean onKeyUp(int keyCode, KeyEvent event) {
551 switch (keyCode) {
552 case KeyEvent.KEYCODE_CALL: {
553 if (mIsAddCallMode && (TextUtils.isEmpty(mDigits.getText().toString()))) {
554 // if we are adding a call from the InCallScreen and the phone
555 // number entered is empty, we just close the dialer to expose
556 // the InCallScreen under it.
557 finish();
558 } else {
559 // otherwise, we place the call.
560 placeCall();
561 }
562 return true;
563 }
564 }
565 return super.onKeyUp(keyCode, event);
566 }
Eric Laurentd9efc872009-07-17 11:52:06 -0700567
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800568 private void keyPressed(int keyCode) {
David Brownc29c7ab2009-07-07 16:00:18 -0700569 vibrate();
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800570 KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
571 mDigits.onKeyDown(keyCode, event);
572 }
573
574 public boolean onKey(View view, int keyCode, KeyEvent event) {
575 switch (view.getId()) {
576 case R.id.digits:
577 if (keyCode == KeyEvent.KEYCODE_ENTER) {
578 placeCall();
579 return true;
580 }
581 break;
582 }
583 return false;
584 }
585
586 public void onClick(View view) {
587 switch (view.getId()) {
588 case R.id.one: {
589 playTone(ToneGenerator.TONE_DTMF_1);
590 keyPressed(KeyEvent.KEYCODE_1);
591 return;
592 }
593 case R.id.two: {
594 playTone(ToneGenerator.TONE_DTMF_2);
595 keyPressed(KeyEvent.KEYCODE_2);
596 return;
597 }
598 case R.id.three: {
599 playTone(ToneGenerator.TONE_DTMF_3);
600 keyPressed(KeyEvent.KEYCODE_3);
601 return;
602 }
603 case R.id.four: {
604 playTone(ToneGenerator.TONE_DTMF_4);
605 keyPressed(KeyEvent.KEYCODE_4);
606 return;
607 }
608 case R.id.five: {
609 playTone(ToneGenerator.TONE_DTMF_5);
610 keyPressed(KeyEvent.KEYCODE_5);
611 return;
612 }
613 case R.id.six: {
614 playTone(ToneGenerator.TONE_DTMF_6);
615 keyPressed(KeyEvent.KEYCODE_6);
616 return;
617 }
618 case R.id.seven: {
619 playTone(ToneGenerator.TONE_DTMF_7);
620 keyPressed(KeyEvent.KEYCODE_7);
621 return;
622 }
623 case R.id.eight: {
624 playTone(ToneGenerator.TONE_DTMF_8);
625 keyPressed(KeyEvent.KEYCODE_8);
626 return;
627 }
628 case R.id.nine: {
629 playTone(ToneGenerator.TONE_DTMF_9);
630 keyPressed(KeyEvent.KEYCODE_9);
631 return;
632 }
633 case R.id.zero: {
634 playTone(ToneGenerator.TONE_DTMF_0);
635 keyPressed(KeyEvent.KEYCODE_0);
636 return;
637 }
638 case R.id.pound: {
639 playTone(ToneGenerator.TONE_DTMF_P);
640 keyPressed(KeyEvent.KEYCODE_POUND);
641 return;
642 }
643 case R.id.star: {
644 playTone(ToneGenerator.TONE_DTMF_S);
645 keyPressed(KeyEvent.KEYCODE_STAR);
646 return;
647 }
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -0700648 case R.id.deleteButton: {
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800649 keyPressed(KeyEvent.KEYCODE_DEL);
650 return;
651 }
David Brown3d07e6d2009-08-04 20:30:09 -0700652 case R.id.dialButton:
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800653 case R.id.digits: {
David Brownc29c7ab2009-07-07 16:00:18 -0700654 vibrate(); // Vibrate here too, just like we do for the regular keys
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800655 placeCall();
656 return;
657 }
Nicolas Catania80bda0f2009-09-19 09:17:14 -0700658 case R.id.voicemailButton: {
659 callVoicemail();
660 vibrate();
661 return;
662 }
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800663 }
664 }
665
666 public boolean onLongClick(View view) {
667 final Editable digits = mDigits.getText();
668 int id = view.getId();
669 switch (id) {
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -0700670 case R.id.deleteButton: {
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800671 digits.clear();
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -0700672 // TODO: The framework forgets to clear the pressed
673 // status of disabled button. Until this is fixed,
674 // clear manually the pressed status. b/2133127
675 mDelete.setPressed(false);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800676 return true;
677 }
678 case R.id.one: {
679 if (digits.length() == 0) {
680 callVoicemail();
681 return true;
682 }
683 return false;
684 }
685 case R.id.zero: {
686 keyPressed(KeyEvent.KEYCODE_PLUS);
687 return true;
688 }
689 }
690 return false;
691 }
692
693 void callVoicemail() {
694 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
695 Uri.fromParts("voicemail", "", null));
696 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
697 startActivity(intent);
698 mDigits.getText().clear();
699 finish();
700 }
701
702 void placeCall() {
703 final String number = mDigits.getText().toString();
704 if (number == null || !TextUtils.isGraphic(number)) {
705 // There is no number entered.
706 playTone(ToneGenerator.TONE_PROP_NACK);
707 return;
708 }
709 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
710 Uri.fromParts("tel", number, null));
711 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
712 startActivity(intent);
713 mDigits.getText().clear();
714 finish();
715 }
716
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800717
718 /**
David Brown22f615f2009-06-25 16:19:19 -0700719 * Plays the specified tone for TONE_LENGTH_MS milliseconds.
720 *
721 * The tone is played locally, using the audio stream for phone calls.
722 * Tones are played only if the "Audible touch tones" user preference
723 * is checked, and are NOT played if the device is in silent mode.
724 *
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800725 * @param tone a tone code from {@link ToneGenerator}
726 */
727 void playTone(int tone) {
728 // if local tone playback is disabled, just return.
729 if (!mDTMFToneEnabled) {
730 return;
731 }
David Brown22f615f2009-06-25 16:19:19 -0700732
733 // Also do nothing if the phone is in silent mode.
734 // We need to re-check the ringer mode for *every* playTone()
735 // call, rather than keeping a local flag that's updated in
736 // onResume(), since it's possible to toggle silent mode without
737 // leaving the current activity (via the ENDCALL-longpress menu.)
738 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
David Brownd5a15302009-07-20 16:39:47 -0700739 int ringerMode = audioManager.getRingerMode();
740 if ((ringerMode == AudioManager.RINGER_MODE_SILENT)
741 || (ringerMode == AudioManager.RINGER_MODE_VIBRATE)) {
David Brown22f615f2009-06-25 16:19:19 -0700742 return;
743 }
744
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800745 synchronized(mToneGeneratorLock) {
746 if (mToneGenerator == null) {
747 Log.w(TAG, "playTone: mToneGenerator == null, tone: "+tone);
748 return;
749 }
Eric Laurentd9efc872009-07-17 11:52:06 -0700750
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800751 // Start the new tone (will stop any playing tone)
Eric Laurent8487fed2009-09-07 08:45:14 -0700752 mToneGenerator.startTone(tone, TONE_LENGTH_MS);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800753 }
754 }
755
756 /**
757 * Brings up the "dialpad chooser" UI in place of the usual Dialer
758 * elements (the textfield/button and the dialpad underneath).
759 *
760 * We show this UI if the user brings up the Dialer while a call is
761 * already in progress, since there's a good chance we got here
762 * accidentally (and the user really wanted the in-call dialpad instead).
763 * So in this situation we display an intermediate UI that lets the user
764 * explicitly choose between the in-call dialpad ("Use touch tone
765 * keypad") and the regular Dialer ("Add call"). (Or, the option "Return
766 * to call in progress" just goes back to the in-call UI with no dialpad
767 * at all.)
768 *
769 * @param enabled If true, show the "dialpad chooser" instead
770 * of the regular Dialer UI
771 */
772 private void showDialpadChooser(boolean enabled) {
773 if (enabled) {
774 // Log.i(TAG, "Showing dialpad chooser!");
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -0700775 mDigits.setVisibility(View.GONE);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800776 if (mDialpad != null) mDialpad.setVisibility(View.GONE);
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -0700777 mVoicemailDialAndDeleteRow.setVisibility(View.GONE);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800778 mDialpadChooser.setVisibility(View.VISIBLE);
779
780 // Instantiate the DialpadChooserAdapter and hook it up to the
781 // ListView. We do this only once.
782 if (mDialpadChooserAdapter == null) {
783 mDialpadChooserAdapter = new DialpadChooserAdapter(this);
784 mDialpadChooser.setAdapter(mDialpadChooserAdapter);
785 }
786 } else {
787 // Log.i(TAG, "Displaying normal Dialer UI.");
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -0700788 mDigits.setVisibility(View.VISIBLE);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800789 if (mDialpad != null) mDialpad.setVisibility(View.VISIBLE);
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -0700790 mVoicemailDialAndDeleteRow.setVisibility(View.VISIBLE);
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -0800791 mDialpadChooser.setVisibility(View.GONE);
792 }
793 }
794
795 /**
796 * @return true if we're currently showing the "dialpad chooser" UI.
797 */
798 private boolean dialpadChooserVisible() {
799 return mDialpadChooser.getVisibility() == View.VISIBLE;
800 }
801
802 /**
803 * Simple list adapter, binding to an icon + text label
804 * for each item in the "dialpad chooser" list.
805 */
806 private static class DialpadChooserAdapter extends BaseAdapter {
807 private LayoutInflater mInflater;
808
809 // Simple struct for a single "choice" item.
810 static class ChoiceItem {
811 String text;
812 Bitmap icon;
813 int id;
814
815 public ChoiceItem(String s, Bitmap b, int i) {
816 text = s;
817 icon = b;
818 id = i;
819 }
820 }
821
822 // IDs for the possible "choices":
823 static final int DIALPAD_CHOICE_USE_DTMF_DIALPAD = 101;
824 static final int DIALPAD_CHOICE_RETURN_TO_CALL = 102;
825 static final int DIALPAD_CHOICE_ADD_NEW_CALL = 103;
826
827 private static final int NUM_ITEMS = 3;
828 private ChoiceItem mChoiceItems[] = new ChoiceItem[NUM_ITEMS];
829
830 public DialpadChooserAdapter(Context context) {
831 // Cache the LayoutInflate to avoid asking for a new one each time.
832 mInflater = LayoutInflater.from(context);
833
834 // Initialize the possible choices.
835 // TODO: could this be specified entirely in XML?
836
837 // - "Use touch tone keypad"
838 mChoiceItems[0] = new ChoiceItem(
839 context.getString(R.string.dialer_useDtmfDialpad),
840 BitmapFactory.decodeResource(context.getResources(),
841 R.drawable.ic_dialer_fork_tt_keypad),
842 DIALPAD_CHOICE_USE_DTMF_DIALPAD);
843
844 // - "Return to call in progress"
845 mChoiceItems[1] = new ChoiceItem(
846 context.getString(R.string.dialer_returnToInCallScreen),
847 BitmapFactory.decodeResource(context.getResources(),
848 R.drawable.ic_dialer_fork_current_call),
849 DIALPAD_CHOICE_RETURN_TO_CALL);
850
851 // - "Add call"
852 mChoiceItems[2] = new ChoiceItem(
853 context.getString(R.string.dialer_addAnotherCall),
854 BitmapFactory.decodeResource(context.getResources(),
855 R.drawable.ic_dialer_fork_add_call),
856 DIALPAD_CHOICE_ADD_NEW_CALL);
857 }
858
859 public int getCount() {
860 return NUM_ITEMS;
861 }
862
863 /**
864 * Return the ChoiceItem for a given position.
865 */
866 public Object getItem(int position) {
867 return mChoiceItems[position];
868 }
869
870 /**
871 * Return a unique ID for each possible choice.
872 */
873 public long getItemId(int position) {
874 return position;
875 }
876
877 /**
878 * Make a view for each row.
879 */
880 public View getView(int position, View convertView, ViewGroup parent) {
881 // When convertView is non-null, we can reuse it (there's no need
882 // to reinflate it.)
883 if (convertView == null) {
884 convertView = mInflater.inflate(R.layout.dialpad_chooser_list_item, null);
885 }
886
887 TextView text = (TextView) convertView.findViewById(R.id.text);
888 text.setText(mChoiceItems[position].text);
889
890 ImageView icon = (ImageView) convertView.findViewById(R.id.icon);
891 icon.setImageBitmap(mChoiceItems[position].icon);
892
893 return convertView;
894 }
895 }
896
897 /**
898 * Handle clicks from the dialpad chooser.
899 */
900 public void onItemClick(AdapterView parent, View v, int position, long id) {
901 DialpadChooserAdapter.ChoiceItem item =
902 (DialpadChooserAdapter.ChoiceItem) parent.getItemAtPosition(position);
903 int itemId = item.id;
904 switch (itemId) {
905 case DialpadChooserAdapter.DIALPAD_CHOICE_USE_DTMF_DIALPAD:
906 // Log.i(TAG, "DIALPAD_CHOICE_USE_DTMF_DIALPAD");
907 // Fire off an intent to go back to the in-call UI
908 // with the dialpad visible.
909 returnToInCallScreen(true);
910 break;
911
912 case DialpadChooserAdapter.DIALPAD_CHOICE_RETURN_TO_CALL:
913 // Log.i(TAG, "DIALPAD_CHOICE_RETURN_TO_CALL");
914 // Fire off an intent to go back to the in-call UI
915 // (with the dialpad hidden).
916 returnToInCallScreen(false);
917 break;
918
919 case DialpadChooserAdapter.DIALPAD_CHOICE_ADD_NEW_CALL:
920 // Log.i(TAG, "DIALPAD_CHOICE_ADD_NEW_CALL");
921 // Ok, guess the user really did want to be here (in the
922 // regular Dialer) after all. Bring back the normal Dialer UI.
923 showDialpadChooser(false);
924 break;
925
926 default:
927 Log.w(TAG, "onItemClick: unexpected itemId: " + itemId);
928 break;
929 }
930 }
931
932 /**
933 * Returns to the in-call UI (where there's presumably a call in
934 * progress) in response to the user selecting "use touch tone keypad"
935 * or "return to call" from the dialpad chooser.
936 */
937 private void returnToInCallScreen(boolean showDialpad) {
938 try {
939 ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
940 if (phone != null) phone.showCallScreenWithDialpad(showDialpad);
941 } catch (RemoteException e) {
942 Log.w(TAG, "phone.showCallScreenWithDialpad() failed", e);
943 }
944
945 // Finally, finish() ourselves so that we don't stay on the
946 // activity stack.
947 // Note that we do this whether or not the showCallScreenWithDialpad()
948 // call above had any effect or not! (That call is a no-op if the
949 // phone is idle, which can happen if the current call ends while
950 // the dialpad chooser is up. In this case we can't show the
951 // InCallScreen, and there's no point staying here in the Dialer,
952 // so we just take the user back where he came from...)
953 finish();
954 }
955
956 /**
957 * @return true if the phone is "in use", meaning that at least one line
958 * is active (ie. off hook or ringing or dialing).
959 */
960 private boolean phoneIsInUse() {
961 boolean phoneInUse = false;
962 try {
963 ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
964 if (phone != null) phoneInUse = !phone.isIdle();
965 } catch (RemoteException e) {
966 Log.w(TAG, "phone.isIdle() failed", e);
967 }
968 return phoneInUse;
969 }
David Brownc29c7ab2009-07-07 16:00:18 -0700970
971 /**
972 * Triggers haptic feedback (if enabled) for dialer key presses.
973 */
974 private synchronized void vibrate() {
975 if (!mVibrateOn) {
976 return;
977 }
978 if (mVibrator == null) {
979 mVibrator = new Vibrator();
980 }
981 mVibrator.vibrate(mVibrateDuration);
982 }
Reli Talc2a2a512009-06-10 16:48:00 -0400983
984 /**
985 * Returns true whenever any one of the options from the menu is selected.
986 * Code changes to support dialpad options
987 */
988 @Override
989 public boolean onOptionsItemSelected(MenuItem item) {
990 switch (item.getItemId()) {
991 case MENU_2S_PAUSE:
992 updateDialString(",");
993 return true;
994 case MENU_WAIT:
995 updateDialString(";");
996 return true;
997 }
998 return false;
999 }
1000
1001 /**
1002 * Updates the dial string (mDigits) after inserting a Pause character (,)
1003 * or Wait character (;).
1004 */
1005 private void updateDialString(String newDigits) {
1006 int selectionStart;
1007 int selectionEnd;
1008
1009 // SpannableStringBuilder editable_text = new SpannableStringBuilder(mDigits.getText());
Eric Fischer686782e2009-09-10 17:57:45 -07001010 int anchor = mDigits.getSelectionStart();
1011 int point = mDigits.getSelectionEnd();
1012
1013 selectionStart = Math.min(anchor, point);
1014 selectionEnd = Math.max(anchor, point);
Reli Talc2a2a512009-06-10 16:48:00 -04001015
1016 Editable digits = mDigits.getText();
1017 if (selectionStart != -1 ) {
1018 if (selectionStart == selectionEnd) {
1019 // then there is no selection. So insert the pause at this
1020 // position and update the mDigits.
1021 digits.replace(selectionStart, selectionStart, newDigits);
1022 } else {
Eric Fischer1e2d3a22009-09-17 10:53:10 -07001023 digits.replace(selectionStart, selectionEnd, newDigits);
Reli Talc2a2a512009-06-10 16:48:00 -04001024 }
1025 } else {
1026 int len = mDigits.length();
1027 digits.replace(len, len, newDigits);
1028 }
1029 }
1030
1031 /**
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -07001032 * Update the enabledness of the "Dial" and "Backspace" buttons if applicable.
Nicolas Cataniadea164e2009-09-18 06:26:16 -07001033 */
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -07001034 private void updateDialAndDeleteButtonStateEnabledAttr() {
1035 final boolean notEmpty = mDigits.length() != 0;
1036
Nicolas Cataniadea164e2009-09-18 06:26:16 -07001037 if (mDialButton != null) {
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -07001038 mDialButton.setEnabled(notEmpty);
Nicolas Cataniadea164e2009-09-18 06:26:16 -07001039 }
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -07001040 mDelete.setEnabled(notEmpty);
Nicolas Cataniadea164e2009-09-18 06:26:16 -07001041 }
1042
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -07001043
Nicolas Cataniadea164e2009-09-18 06:26:16 -07001044 /**
Nicolas Catania80bda0f2009-09-19 09:17:14 -07001045 * Check if voicemail is enabled/accessible.
1046 */
1047 private void initVoicemailButton() {
1048 boolean hasVoicemail = false;
1049 try {
1050 hasVoicemail = TelephonyManager.getDefault().getVoiceMailNumber() != null;
1051 } catch (SecurityException se) {
1052 // Possibly no READ_PHONE_STATE privilege.
1053 }
1054
Nicolas Cataniaa7e5a5b2009-09-20 10:56:40 -07001055 mVoicemailButton = mVoicemailDialAndDeleteRow.findViewById(R.id.voicemailButton);
Nicolas Catania80bda0f2009-09-19 09:17:14 -07001056 if (hasVoicemail) {
1057 mVoicemailButton.setOnClickListener(this);
1058 } else {
1059 mVoicemailButton.setEnabled(false);
1060 }
1061 }
1062
1063 /**
Reli Talc2a2a512009-06-10 16:48:00 -04001064 * This function return true if Wait menu item can be shown
1065 * otherwise returns false. Assumes the passed string is non-empty
1066 * and the 0th index check is not required.
1067 */
1068 private boolean showWait(int start, int end, String digits) {
1069 if (start == end) {
1070 // visible false in this case
1071 if (start > digits.length()) return false;
1072
1073 // preceding char is ';', so visible should be false
1074 if (digits.charAt(start-1) == ';') return false;
1075
1076 // next char is ';', so visible should be false
1077 if ((digits.length() > start) && (digits.charAt(start) == ';')) return false;
1078 } else {
1079 // visible false in this case
1080 if (start > digits.length() || end > digits.length()) return false;
1081
1082 // In this case we need to just check for ';' preceding to start
1083 // or next to end
1084 if (digits.charAt(start-1) == ';') return false;
1085 }
1086 return true;
1087 }
The Android Open Source Project7aa0e4c2009-03-03 19:32:21 -08001088}