1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.incallui; 18 19 import com.android.services.telephony.common.Call; 20 import com.android.services.telephony.common.Call.State; 21 22 import android.app.Activity; 23 import android.app.AlertDialog; 24 import android.app.FragmentTransaction; 25 import android.content.DialogInterface; 26 import android.content.DialogInterface.OnClickListener; 27 import android.content.DialogInterface.OnCancelListener; 28 import android.content.Intent; 29 import android.content.res.Configuration; 30 import android.os.Bundle; 31 import android.view.KeyEvent; 32 import android.view.View; 33 import android.view.Window; 34 import android.view.WindowManager; 35 import android.view.accessibility.AccessibilityEvent; 36 import android.widget.Toast; 37 38 /** 39 * Phone app "in call" screen. 40 */ 41 public class InCallActivity extends Activity { 42 43 public static final String SHOW_DIALPAD_EXTRA = "InCallActivity.show_dialpad"; 44 45 private static final int INVALID_RES_ID = -1; 46 47 private CallButtonFragment mCallButtonFragment; 48 private CallCardFragment mCallCardFragment; 49 private AnswerFragment mAnswerFragment; 50 private DialpadFragment mDialpadFragment; 51 private ConferenceManagerFragment mConferenceManagerFragment; 52 private boolean mIsForegroundActivity; 53 private AlertDialog mDialog; 54 55 /** Use to pass 'showDialpad' from {@link #onNewIntent} to {@link #onResume} */ 56 private boolean mShowDialpadRequested; 57 58 @Override 59 protected void onCreate(Bundle icicle) { 60 Log.d(this, "onCreate()... this = " + this); 61 62 super.onCreate(icicle); 63 64 // set this flag so this activity will stay in front of the keyguard 65 // Have the WindowManager filter out touch events that are "too fat". 66 int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 67 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON 68 | WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; 69 70 getWindow().addFlags(flags); 71 72 requestWindowFeature(Window.FEATURE_NO_TITLE); 73 74 // TODO(klp): Do we need to add this back when prox sensor is not available? 75 // lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; 76 77 // Inflate everything in incall_screen.xml and add it to the screen. 78 setContentView(R.layout.incall_screen); 79 80 initializeInCall(); 81 Log.d(this, "onCreate(): exit"); 82 } 83 84 @Override 85 protected void onStart() { 86 Log.d(this, "onStart()..."); 87 super.onStart(); 88 89 // setting activity should be last thing in setup process 90 InCallPresenter.getInstance().setActivity(this); 91 } 92 93 @Override 94 protected void onResume() { 95 Log.i(this, "onResume()..."); 96 super.onResume(); 97 98 mIsForegroundActivity = true; 99 InCallPresenter.getInstance().onUiShowing(true); 100 101 if (mShowDialpadRequested) { 102 mCallButtonFragment.displayDialpad(true); 103 mShowDialpadRequested = false; 104 } 105 } 106 107 // onPause is guaranteed to be called when the InCallActivity goes 108 // in the background. 109 @Override 110 protected void onPause() { 111 Log.d(this, "onPause()..."); 112 super.onPause(); 113 114 mIsForegroundActivity = false; 115 116 mDialpadFragment.onDialerKeyUp(null); 117 118 InCallPresenter.getInstance().onUiShowing(false); 119 } 120 121 @Override 122 protected void onStop() { 123 Log.d(this, "onStop()..."); 124 super.onStop(); 125 } 126 127 @Override 128 protected void onDestroy() { 129 Log.d(this, "onDestroy()... this = " + this); 130 131 InCallPresenter.getInstance().setActivity(null); 132 133 super.onDestroy(); 134 } 135 136 /** 137 * Returns true when theActivity is in foreground (between onResume and onPause). 138 */ 139 /* package */ boolean isForegroundActivity() { 140 return mIsForegroundActivity; 141 } 142 143 private boolean hasPendingErrorDialog() { 144 return mDialog != null; 145 } 146 /** 147 * Dismisses the in-call screen. 148 * 149 * We never *really* finish() the InCallActivity, since we don't want to get destroyed and then 150 * have to be re-created from scratch for the next call. Instead, we just move ourselves to the 151 * back of the activity stack. 152 * 153 * This also means that we'll no longer be reachable via the BACK button (since moveTaskToBack() 154 * puts us behind the Home app, but the home app doesn't allow the BACK key to move you any 155 * farther down in the history stack.) 156 * 157 * (Since the Phone app itself is never killed, this basically means that we'll keep a single 158 * InCallActivity instance around for the entire uptime of the device. This noticeably improves 159 * the UI responsiveness for incoming calls.) 160 */ 161 @Override 162 public void finish() { 163 Log.i(this, "finish(). Dialog showing: " + (mDialog != null)); 164 165 // skip finish if we are still showing a dialog. 166 if (!hasPendingErrorDialog() && !mAnswerFragment.hasPendingDialogs()) { 167 super.finish(); 168 } 169 } 170 171 @Override 172 protected void onNewIntent(Intent intent) { 173 Log.d(this, "onNewIntent: intent = " + intent); 174 175 // We're being re-launched with a new Intent. Since it's possible for a 176 // single InCallActivity instance to persist indefinitely (even if we 177 // finish() ourselves), this sequence can potentially happen any time 178 // the InCallActivity needs to be displayed. 179 180 // Stash away the new intent so that we can get it in the future 181 // by calling getIntent(). (Otherwise getIntent() will return the 182 // original Intent from when we first got created!) 183 setIntent(intent); 184 185 // Activities are always paused before receiving a new intent, so 186 // we can count on our onResume() method being called next. 187 188 // Just like in onCreate(), handle the intent. 189 internalResolveIntent(intent); 190 } 191 192 @Override 193 public void onBackPressed() { 194 Log.d(this, "onBackPressed()..."); 195 196 // BACK is also used to exit out of any "special modes" of the 197 // in-call UI: 198 199 if (mDialpadFragment.isVisible()) { 200 mCallButtonFragment.displayDialpad(false); // do the "closing" animation 201 return; 202 } else if (mConferenceManagerFragment.isVisible()) { 203 mConferenceManagerFragment.setVisible(false); 204 return; 205 } 206 207 // Always disable the Back key while an incoming call is ringing 208 final Call call = CallList.getInstance().getIncomingCall(); 209 if (call != null) { 210 Log.d(this, "Consume Back press for an inconing call"); 211 return; 212 } 213 214 // Nothing special to do. Fall back to the default behavior. 215 super.onBackPressed(); 216 } 217 218 @Override 219 public boolean onKeyUp(int keyCode, KeyEvent event) { 220 // push input to the dialer. 221 if ((mDialpadFragment.isVisible()) && (mDialpadFragment.onDialerKeyUp(event))){ 222 return true; 223 } else if (keyCode == KeyEvent.KEYCODE_CALL) { 224 // Always consume CALL to be sure the PhoneWindow won't do anything with it 225 return true; 226 } 227 return super.onKeyUp(keyCode, event); 228 } 229 230 @Override 231 public boolean onKeyDown(int keyCode, KeyEvent event) { 232 switch (keyCode) { 233 case KeyEvent.KEYCODE_CALL: 234 boolean handled = InCallPresenter.getInstance().handleCallKey(); 235 if (!handled) { 236 Log.w(this, "InCallActivity should always handle KEYCODE_CALL in onKeyDown"); 237 } 238 // Always consume CALL to be sure the PhoneWindow won't do anything with it 239 return true; 240 241 // Note there's no KeyEvent.KEYCODE_ENDCALL case here. 242 // The standard system-wide handling of the ENDCALL key 243 // (see PhoneWindowManager's handling of KEYCODE_ENDCALL) 244 // already implements exactly what the UI spec wants, 245 // namely (1) "hang up" if there's a current active call, 246 // or (2) "don't answer" if there's a current ringing call. 247 248 case KeyEvent.KEYCODE_CAMERA: 249 // Disable the CAMERA button while in-call since it's too 250 // easy to press accidentally. 251 return true; 252 253 case KeyEvent.KEYCODE_VOLUME_UP: 254 case KeyEvent.KEYCODE_VOLUME_DOWN: 255 case KeyEvent.KEYCODE_VOLUME_MUTE: 256 // Ringer silencing handled by PhoneWindowManager. 257 break; 258 259 case KeyEvent.KEYCODE_MUTE: 260 // toggle mute 261 CallCommandClient.getInstance().mute(!AudioModeProvider.getInstance().getMute()); 262 return true; 263 264 // Various testing/debugging features, enabled ONLY when VERBOSE == true. 265 case KeyEvent.KEYCODE_SLASH: 266 if (Log.VERBOSE) { 267 Log.v(this, "----------- InCallActivity View dump --------------"); 268 // Dump starting from the top-level view of the entire activity: 269 Window w = this.getWindow(); 270 View decorView = w.getDecorView(); 271 decorView.debug(); 272 return true; 273 } 274 break; 275 case KeyEvent.KEYCODE_EQUALS: 276 // TODO: Dump phone state? 277 break; 278 } 279 280 if (event.getRepeatCount() == 0 && handleDialerKeyDown(keyCode, event)) { 281 return true; 282 } 283 284 return super.onKeyDown(keyCode, event); 285 } 286 287 private boolean handleDialerKeyDown(int keyCode, KeyEvent event) { 288 Log.v(this, "handleDialerKeyDown: keyCode " + keyCode + ", event " + event + "..."); 289 290 // As soon as the user starts typing valid dialable keys on the 291 // keyboard (presumably to type DTMF tones) we start passing the 292 // key events to the DTMFDialer's onDialerKeyDown. 293 if (mDialpadFragment.isVisible()) { 294 return mDialpadFragment.onDialerKeyDown(event); 295 296 // TODO: If the dialpad isn't currently visible, maybe 297 // consider automatically bringing it up right now? 298 // (Just to make sure the user sees the digits widget...) 299 // But this probably isn't too critical since it's awkward to 300 // use the hard keyboard while in-call in the first place, 301 // especially now that the in-call UI is portrait-only... 302 } 303 304 return false; 305 } 306 307 @Override 308 public void onConfigurationChanged(Configuration config) { 309 InCallPresenter.getInstance().getProximitySensor().onConfigurationChanged(config); 310 } 311 312 public CallButtonFragment getCallButtonFragment() { 313 return mCallButtonFragment; 314 } 315 316 private void internalResolveIntent(Intent intent) { 317 final String action = intent.getAction(); 318 319 if (action.equals(intent.ACTION_MAIN)) { 320 // This action is the normal way to bring up the in-call UI. 321 // 322 // But we do check here for one extra that can come along with the 323 // ACTION_MAIN intent: 324 325 if (intent.hasExtra(SHOW_DIALPAD_EXTRA)) { 326 // SHOW_DIALPAD_EXTRA can be used here to specify whether the DTMF 327 // dialpad should be initially visible. If the extra isn't 328 // present at all, we just leave the dialpad in its previous state. 329 330 final boolean showDialpad = intent.getBooleanExtra(SHOW_DIALPAD_EXTRA, false); 331 Log.d(this, "- internalResolveIntent: SHOW_DIALPAD_EXTRA: " + showDialpad); 332 333 relaunchedFromDialer(showDialpad); 334 } 335 336 return; 337 } 338 } 339 340 private void relaunchedFromDialer(boolean showDialpad) { 341 mShowDialpadRequested = showDialpad; 342 343 if (mShowDialpadRequested) { 344 // If there's only one line in use, AND it's on hold, then we're sure the user 345 // wants to use the dialpad toward the exact line, so un-hold the holding line. 346 final Call call = CallList.getInstance().getActiveOrBackgroundCall(); 347 if (call != null && call.getState() == State.ONHOLD) { 348 CallCommandClient.getInstance().hold(call.getCallId(), false); 349 } 350 } 351 } 352 353 private void initializeInCall() { 354 if (mCallButtonFragment == null) { 355 mCallButtonFragment = (CallButtonFragment) getFragmentManager() 356 .findFragmentById(R.id.callButtonFragment); 357 mCallButtonFragment.getView().setVisibility(View.INVISIBLE); 358 } 359 360 if (mCallCardFragment == null) { 361 mCallCardFragment = (CallCardFragment) getFragmentManager() 362 .findFragmentById(R.id.callCardFragment); 363 } 364 365 if (mAnswerFragment == null) { 366 mAnswerFragment = (AnswerFragment) getFragmentManager() 367 .findFragmentById(R.id.answerFragment); 368 } 369 370 if (mDialpadFragment == null) { 371 mDialpadFragment = (DialpadFragment) getFragmentManager() 372 .findFragmentById(R.id.dialpadFragment); 373 getFragmentManager().beginTransaction().hide(mDialpadFragment).commit(); 374 } 375 376 if (mConferenceManagerFragment == null) { 377 mConferenceManagerFragment = (ConferenceManagerFragment) getFragmentManager() 378 .findFragmentById(R.id.conferenceManagerFragment); 379 mConferenceManagerFragment.getView().setVisibility(View.INVISIBLE); 380 } 381 } 382 383 private void toast(String text) { 384 final Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT); 385 386 toast.show(); 387 } 388 389 /** 390 * Simulates a user click to hide the dialpad. This will update the UI to show the call card, 391 * update the checked state of the dialpad button, and update the proximity sensor state. 392 */ 393 public void hideDialpadForDisconnect() { 394 mCallButtonFragment.displayDialpad(false); 395 } 396 397 public void dismissKeyguard(boolean dismiss) { 398 if (dismiss) { 399 getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); 400 } else { 401 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); 402 } 403 } 404 405 public void displayDialpad(boolean showDialpad) { 406 final FragmentTransaction ft = getFragmentManager().beginTransaction(); 407 if (showDialpad) { 408 ft.setCustomAnimations(R.anim.incall_dialpad_slide_in, 0); 409 ft.show(mDialpadFragment); 410 } else { 411 ft.setCustomAnimations(0, R.anim.incall_dialpad_slide_out); 412 ft.hide(mDialpadFragment); 413 } 414 ft.commitAllowingStateLoss(); 415 416 InCallPresenter.getInstance().getProximitySensor().onDialpadVisible(showDialpad); 417 } 418 419 public boolean isDialpadVisible() { 420 return mDialpadFragment.isVisible(); 421 } 422 423 public void displayManageConferencePanel(boolean showPanel) { 424 if (showPanel) { 425 mConferenceManagerFragment.setVisible(true); 426 } 427 } 428 429 public void showPostCharWaitDialog(int callId, String chars) { 430 final PostCharDialogFragment fragment = new PostCharDialogFragment(callId, chars); 431 fragment.show(getFragmentManager(), "postCharWait"); 432 } 433 434 @Override 435 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 436 if (mCallCardFragment != null) { 437 mCallCardFragment.dispatchPopulateAccessibilityEvent(event); 438 } 439 return super.dispatchPopulateAccessibilityEvent(event); 440 } 441 442 public void maybeShowErrorDialogOnDisconnect(Call.DisconnectCause cause) { 443 Log.d(this, "maybeShowErrorDialogOnDisconnect"); 444 445 if (!isFinishing()) { 446 final int resId = getResIdForDisconnectCause(cause); 447 if (resId != INVALID_RES_ID) { 448 showErrorDialog(resId); 449 } 450 } 451 } 452 453 public void dismissPendingDialogs() { 454 if (mDialog != null) { 455 mDialog.dismiss(); 456 mDialog = null; 457 } 458 mAnswerFragment.dismissPendingDialogues(); 459 } 460 461 /** 462 * Utility function to bring up a generic "error" dialog. 463 */ 464 private void showErrorDialog(int resId) { 465 final CharSequence msg = getResources().getText(resId); 466 Log.i(this, "Show Dialog: " + msg); 467 468 dismissPendingDialogs(); 469 470 mDialog = new AlertDialog.Builder(this) 471 .setMessage(msg) 472 .setPositiveButton(R.string.ok, new OnClickListener() { 473 @Override 474 public void onClick(DialogInterface dialog, int which) { 475 onDialogDismissed(); 476 }}) 477 .setOnCancelListener(new OnCancelListener() { 478 @Override 479 public void onCancel(DialogInterface dialog) { 480 onDialogDismissed(); 481 }}) 482 .create(); 483 484 mDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); 485 mDialog.show(); 486 } 487 488 private int getResIdForDisconnectCause(Call.DisconnectCause cause) { 489 int resId = INVALID_RES_ID; 490 491 if (cause == Call.DisconnectCause.CALL_BARRED) { 492 resId = R.string.callFailed_cb_enabled; 493 } else if (cause == Call.DisconnectCause.FDN_BLOCKED) { 494 resId = R.string.callFailed_fdn_only; 495 } else if (cause == Call.DisconnectCause.CS_RESTRICTED) { 496 resId = R.string.callFailed_dsac_restricted; 497 } else if (cause == Call.DisconnectCause.CS_RESTRICTED_EMERGENCY) { 498 resId = R.string.callFailed_dsac_restricted_emergency; 499 } else if (cause == Call.DisconnectCause.CS_RESTRICTED_NORMAL) { 500 resId = R.string.callFailed_dsac_restricted_normal; 501 } 502 503 return resId; 504 } 505 506 private void onDialogDismissed() { 507 mDialog = null; 508 InCallPresenter.getInstance().onDismissDialog(); 509 } 510 } 511