CallCardFragment.java revision 1771e5a1295eac5ad0d59fe2ff750ab450eab90e
1/* 2 * Copyright (C) 2013 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.incallui; 18 19import android.animation.LayoutTransition; 20import android.content.Context; 21import android.graphics.Bitmap; 22import android.graphics.drawable.BitmapDrawable; 23import android.graphics.drawable.Drawable; 24import android.os.Bundle; 25import android.telephony.DisconnectCause; 26import android.text.TextUtils; 27import android.view.Gravity; 28import android.view.LayoutInflater; 29import android.view.View; 30import android.view.View.OnClickListener; 31import android.view.ViewGroup; 32import android.view.ViewStub; 33import android.view.accessibility.AccessibilityEvent; 34import android.widget.Button; 35import android.widget.ImageView; 36import android.widget.TextView; 37 38import java.util.List; 39 40/** 41 * Fragment for call card. 42 */ 43public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPresenter.CallCardUi> 44 implements CallCardPresenter.CallCardUi { 45 46 // Primary caller info 47 private TextView mPhoneNumber; 48 private TextView mNumberLabel; 49 private TextView mPrimaryName; 50 private TextView mCallServiceLabel; 51 private TextView mCallStateLabel; 52 private TextView mCallTypeLabel; 53 private ImageView mPhoto; 54 private TextView mElapsedTime; 55 private ViewGroup mSupplementaryInfoContainer; 56 private Button mConnectionHandoffButton; 57 58 // Secondary caller info 59 private ViewStub mSecondaryCallInfo; 60 private TextView mSecondaryCallName; 61 private ImageView mSecondaryPhoto; 62 private View mSecondaryPhotoOverlay; 63 64 // Cached DisplayMetrics density. 65 private float mDensity; 66 67 @Override 68 CallCardPresenter.CallCardUi getUi() { 69 return this; 70 } 71 72 @Override 73 CallCardPresenter createPresenter() { 74 return new CallCardPresenter(); 75 } 76 77 @Override 78 public void onCreate(Bundle savedInstanceState) { 79 super.onCreate(savedInstanceState); 80 } 81 82 83 @Override 84 public void onActivityCreated(Bundle savedInstanceState) { 85 super.onActivityCreated(savedInstanceState); 86 87 final CallList calls = CallList.getInstance(); 88 final Call call = calls.getFirstCall(); 89 getPresenter().init(getActivity(), call); 90 } 91 92 @Override 93 public View onCreateView(LayoutInflater inflater, ViewGroup container, 94 Bundle savedInstanceState) { 95 super.onCreateView(inflater, container, savedInstanceState); 96 97 mDensity = getResources().getDisplayMetrics().density; 98 99 return inflater.inflate(R.layout.call_card, container, false); 100 } 101 102 @Override 103 public void onViewCreated(View view, Bundle savedInstanceState) { 104 super.onViewCreated(view, savedInstanceState); 105 106 mPhoneNumber = (TextView) view.findViewById(R.id.phoneNumber); 107 mPrimaryName = (TextView) view.findViewById(R.id.name); 108 mNumberLabel = (TextView) view.findViewById(R.id.label); 109 mSecondaryCallInfo = (ViewStub) view.findViewById(R.id.secondary_call_info); 110 mPhoto = (ImageView) view.findViewById(R.id.photo); 111 mCallServiceLabel = (TextView) view.findViewById(R.id.callServiceLabel); 112 mCallStateLabel = (TextView) view.findViewById(R.id.callStateLabel); 113 mCallTypeLabel = (TextView) view.findViewById(R.id.callTypeLabel); 114 mElapsedTime = (TextView) view.findViewById(R.id.elapsedTime); 115 mSupplementaryInfoContainer = 116 (ViewGroup) view.findViewById(R.id.supplementary_info_container); 117 mConnectionHandoffButton = (Button) view.findViewById(R.id.connectionHandoffButton); 118 mConnectionHandoffButton.setOnClickListener(new OnClickListener() { 119 @Override 120 public void onClick(View v) { 121 getPresenter().connectionHandoffClicked(); 122 } 123 }); 124 } 125 126 @Override 127 public void setVisible(boolean on) { 128 if (on) { 129 getView().setVisibility(View.VISIBLE); 130 } else { 131 getView().setVisibility(View.INVISIBLE); 132 } 133 } 134 135 @Override 136 public void setPrimaryName(String name, boolean nameIsNumber) { 137 if (TextUtils.isEmpty(name)) { 138 mPrimaryName.setText(""); 139 } else { 140 mPrimaryName.setText(name); 141 142 // Set direction of the name field 143 int nameDirection = View.TEXT_DIRECTION_INHERIT; 144 if (nameIsNumber) { 145 nameDirection = View.TEXT_DIRECTION_LTR; 146 } 147 mPrimaryName.setTextDirection(nameDirection); 148 } 149 } 150 151 @Override 152 public void setPrimaryImage(Drawable image) { 153 if (image != null) { 154 setDrawableToImageView(mPhoto, image); 155 } 156 } 157 158 @Override 159 public void setPrimaryPhoneNumber(String number) { 160 // Set the number 161 if (TextUtils.isEmpty(number)) { 162 mPhoneNumber.setText(""); 163 mPhoneNumber.setVisibility(View.GONE); 164 } else { 165 mPhoneNumber.setText(number); 166 mPhoneNumber.setVisibility(View.VISIBLE); 167 mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR); 168 } 169 } 170 171 @Override 172 public void setPrimaryLabel(String label) { 173 if (!TextUtils.isEmpty(label)) { 174 mNumberLabel.setText(label); 175 mNumberLabel.setVisibility(View.VISIBLE); 176 } else { 177 mNumberLabel.setVisibility(View.GONE); 178 } 179 180 } 181 182 @Override 183 public void setPrimary(String number, String name, boolean nameIsNumber, String label, 184 Drawable photo, boolean isConference, boolean isGeneric, boolean isSipCall) { 185 Log.d(this, "Setting primary call"); 186 187 if (isConference) { 188 name = getConferenceString(isGeneric); 189 photo = getConferencePhoto(isGeneric); 190 nameIsNumber = false; 191 } 192 193 setPrimaryPhoneNumber(number); 194 195 // set the name field. 196 setPrimaryName(name, nameIsNumber); 197 198 // Set the label (Mobile, Work, etc) 199 setPrimaryLabel(label); 200 201 showInternetCallLabel(isSipCall); 202 203 setDrawableToImageView(mPhoto, photo); 204 } 205 206 @Override 207 public void setSecondary(boolean show, String name, boolean nameIsNumber, String label, 208 Drawable photo, boolean isConference, boolean isGeneric) { 209 210 if (show) { 211 if (isConference) { 212 name = getConferenceString(isGeneric); 213 photo = getConferencePhoto(isGeneric); 214 nameIsNumber = false; 215 } 216 217 showAndInitializeSecondaryCallInfo(); 218 mSecondaryCallName.setText(name); 219 220 int nameDirection = View.TEXT_DIRECTION_INHERIT; 221 if (nameIsNumber) { 222 nameDirection = View.TEXT_DIRECTION_LTR; 223 } 224 mSecondaryCallName.setTextDirection(nameDirection); 225 226 setDrawableToImageView(mSecondaryPhoto, photo); 227 } else { 228 mSecondaryCallInfo.setVisibility(View.GONE); 229 } 230 } 231 232 @Override 233 public void setSecondaryImage(Drawable image) { 234 if (image != null) { 235 setDrawableToImageView(mSecondaryPhoto, image); 236 } 237 } 238 239 @Override 240 public void setCallState(int state, int cause, boolean bluetoothOn, String gatewayLabel, 241 String gatewayNumber, boolean isWiFi, boolean isHandoffCapable, 242 boolean isHandoffPending) { 243 String callStateText = null; 244 245 if (Call.State.isDialing(state) && !TextUtils.isEmpty(gatewayLabel)) { 246 // Provider info: (e.g. "Calling via <gatewayLabel>") 247 callStateText = gatewayLabel; 248 } else { 249 callStateText = getCallStateLabelFromState(state, cause); 250 } 251 252 // Only show call service related text if call state is not being displayed. 253 String callServiceText = null; 254 if (TextUtils.isEmpty(callStateText)) { 255 if (isHandoffPending) { 256 callServiceText = getResources().getString(R.string.handoff_status_pending); 257 } else if (isWiFi) { 258 callServiceText = getResources().getString(R.string.in_call_wifi_connected); 259 } 260 } 261 262 Log.v(this, "setCallState " + callStateText); 263 Log.v(this, "DisconnectCause " + DisconnectCause.toString(cause)); 264 Log.v(this, "bluetooth on " + bluetoothOn); 265 Log.v(this, "gateway " + gatewayLabel + gatewayNumber); 266 267 // There are cases where we totally skip the animation, in which case remove the transition 268 // animation here and restore it afterwards. 269 final boolean skipAnimation = (Call.State.isDialing(state) 270 || state == Call.State.DISCONNECTED || state == Call.State.DISCONNECTING); 271 LayoutTransition transition = null; 272 if (skipAnimation) { 273 transition = mSupplementaryInfoContainer.getLayoutTransition(); 274 mSupplementaryInfoContainer.setLayoutTransition(null); 275 } 276 277 updateCallServiceLabel(isWiFi, callServiceText); 278 updateCallStateLabel(state, bluetoothOn, callStateText); 279 280 // Only show the handoff button if call state is not being displayed. 281 boolean showHandoffButton = isHandoffCapable && TextUtils.isEmpty(callStateText); 282 mConnectionHandoffButton.setVisibility(showHandoffButton ? View.VISIBLE : View.GONE); 283 mConnectionHandoffButton.setEnabled(!isHandoffPending); 284 285 // Background color. 286 if (isWiFi) { 287 mSupplementaryInfoContainer.setBackgroundResource(R.color.wifi_connected_background); 288 } else { 289 mSupplementaryInfoContainer.setBackgroundResource( 290 R.color.incall_secondary_info_background); 291 } 292 293 // Restore the animation. 294 if (skipAnimation) { 295 mSupplementaryInfoContainer.setLayoutTransition(transition); 296 } 297 } 298 299 private void updateCallServiceLabel(boolean isWiFi, String text) { 300 if (!TextUtils.isEmpty(text)) { 301 mCallServiceLabel.setText(text); 302 if (isWiFi) { 303 mCallServiceLabel.setCompoundDrawablesRelativeWithIntrinsicBounds( 304 R.drawable.ic_in_call_wifi, 0, 0, 0); 305 mCallServiceLabel.setCompoundDrawablePadding(5); 306 mCallServiceLabel.setCompoundDrawablePadding((int) (mDensity * 5)); 307 } else { 308 mCallServiceLabel.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); 309 } 310 mCallServiceLabel.setVisibility(View.VISIBLE); 311 } else { 312 mCallServiceLabel.setText(""); 313 mCallServiceLabel.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); 314 mCallServiceLabel.setVisibility(View.GONE); 315 } 316 } 317 318 private void updateCallStateLabel(int state, boolean bluetoothOn, String text) { 319 if (!TextUtils.isEmpty(text)) { 320 mCallStateLabel.setVisibility(View.VISIBLE); 321 mCallStateLabel.setText(text); 322 323 if (Call.State.INCOMING == state) { 324 setBluetoothOn(bluetoothOn); 325 } 326 } else { 327 mCallStateLabel.setVisibility(View.GONE); 328 // Gravity is aligned left when receiving an incoming call in landscape. 329 // In that rare case, the gravity needs to be reset to the right. 330 // Also, setText("") is used since there is a delay in making the view GONE, 331 // so the user will otherwise see the text jump to the right side before disappearing. 332 if(mCallStateLabel.getGravity() != Gravity.END) { 333 mCallStateLabel.setText(""); 334 mCallStateLabel.setGravity(Gravity.END); 335 } 336 } 337 } 338 339 private void showInternetCallLabel(boolean show) { 340 if (show) { 341 final String label = getView().getContext().getString( 342 R.string.incall_call_type_label_sip); 343 mCallTypeLabel.setVisibility(View.VISIBLE); 344 mCallTypeLabel.setText(label); 345 } else { 346 mCallTypeLabel.setVisibility(View.GONE); 347 } 348 } 349 350 @Override 351 public void setPrimaryCallElapsedTime(boolean show, String callTimeElapsed) { 352 if (show) { 353 if (mElapsedTime.getVisibility() != View.VISIBLE) { 354 AnimationUtils.Fade.show(mElapsedTime); 355 } 356 mElapsedTime.setText(callTimeElapsed); 357 } else { 358 // hide() animation has no effect if it is already hidden. 359 AnimationUtils.Fade.hide(mElapsedTime, View.INVISIBLE); 360 } 361 } 362 363 private void setDrawableToImageView(ImageView view, Drawable photo) { 364 if (photo == null) { 365 photo = view.getResources().getDrawable(R.drawable.picture_unknown); 366 } 367 368 final Drawable current = view.getDrawable(); 369 if (current == null) { 370 view.setImageDrawable(photo); 371 AnimationUtils.Fade.show(view); 372 } else { 373 AnimationUtils.startCrossFade(view, current, photo); 374 view.setVisibility(View.VISIBLE); 375 } 376 } 377 378 private String getConferenceString(boolean isGeneric) { 379 Log.v(this, "isGenericString: " + isGeneric); 380 final int resId = isGeneric ? R.string.card_title_in_call : R.string.card_title_conf_call; 381 return getView().getResources().getString(resId); 382 } 383 384 private Drawable getConferencePhoto(boolean isGeneric) { 385 Log.v(this, "isGenericPhoto: " + isGeneric); 386 final int resId = isGeneric ? R.drawable.picture_dialing : R.drawable.picture_conference; 387 return getView().getResources().getDrawable(resId); 388 } 389 390 private void setBluetoothOn(boolean onOff) { 391 // Also, display a special icon (alongside the "Incoming call" 392 // label) if there's an incoming call and audio will be routed 393 // to bluetooth when you answer it. 394 final int bluetoothIconId = R.drawable.ic_in_call_bt_dk; 395 396 if (onOff) { 397 mCallStateLabel.setCompoundDrawablesWithIntrinsicBounds(bluetoothIconId, 0, 0, 0); 398 mCallStateLabel.setCompoundDrawablePadding((int) (mDensity * 5)); 399 } else { 400 // Clear out any icons 401 mCallStateLabel.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); 402 } 403 } 404 405 /** 406 * Gets the call state label based on the state of the call and 407 * cause of disconnect 408 */ 409 private String getCallStateLabelFromState(int state, int cause) { 410 final Context context = getView().getContext(); 411 String callStateLabel = null; // Label to display as part of the call banner 412 413 if (Call.State.IDLE == state) { 414 // "Call state" is meaningless in this state. 415 416 } else if (Call.State.ACTIVE == state) { 417 // We normally don't show a "call state label" at all in 418 // this state (but see below for some special cases). 419 420 } else if (Call.State.ONHOLD == state) { 421 callStateLabel = context.getString(R.string.card_title_on_hold); 422 } else if (Call.State.DIALING == state) { 423 callStateLabel = context.getString(R.string.card_title_dialing); 424 } else if (Call.State.REDIALING == state) { 425 callStateLabel = context.getString(R.string.card_title_redialing); 426 } else if (Call.State.INCOMING == state || Call.State.CALL_WAITING == state) { 427 callStateLabel = context.getString(R.string.card_title_incoming_call); 428 429 } else if (Call.State.DISCONNECTING == state) { 430 // While in the DISCONNECTING state we display a "Hanging up" 431 // message in order to make the UI feel more responsive. (In 432 // GSM it's normal to see a delay of a couple of seconds while 433 // negotiating the disconnect with the network, so the "Hanging 434 // up" state at least lets the user know that we're doing 435 // something. This state is currently not used with CDMA.) 436 callStateLabel = context.getString(R.string.card_title_hanging_up); 437 438 } else if (Call.State.DISCONNECTED == state) { 439 callStateLabel = getCallFailedString(cause); 440 441 } else { 442 Log.wtf(this, "updateCallStateWidgets: unexpected call: " + state); 443 } 444 445 return callStateLabel; 446 } 447 448 /** 449 * Maps the disconnect cause to a resource string. 450 * 451 * @param cause disconnect cause as defined in {@link DisconnectCause} 452 */ 453 private String getCallFailedString(int cause) { 454 int resID = R.string.card_title_call_ended; 455 456 // TODO: The card *title* should probably be "Call ended" in all 457 // cases, but if the DisconnectCause was an error condition we should 458 // probably also display the specific failure reason somewhere... 459 460 switch (cause) { 461 case DisconnectCause.BUSY: 462 resID = R.string.callFailed_userBusy; 463 break; 464 465 case DisconnectCause.CONGESTION: 466 resID = R.string.callFailed_congestion; 467 break; 468 469 case DisconnectCause.TIMED_OUT: 470 resID = R.string.callFailed_timedOut; 471 break; 472 473 case DisconnectCause.SERVER_UNREACHABLE: 474 resID = R.string.callFailed_server_unreachable; 475 break; 476 477 case DisconnectCause.NUMBER_UNREACHABLE: 478 resID = R.string.callFailed_number_unreachable; 479 break; 480 481 case DisconnectCause.INVALID_CREDENTIALS: 482 resID = R.string.callFailed_invalid_credentials; 483 break; 484 485 case DisconnectCause.SERVER_ERROR: 486 resID = R.string.callFailed_server_error; 487 break; 488 489 case DisconnectCause.OUT_OF_NETWORK: 490 resID = R.string.callFailed_out_of_network; 491 break; 492 493 case DisconnectCause.LOST_SIGNAL: 494 case DisconnectCause.CDMA_DROP: 495 resID = R.string.callFailed_noSignal; 496 break; 497 498 case DisconnectCause.LIMIT_EXCEEDED: 499 resID = R.string.callFailed_limitExceeded; 500 break; 501 502 case DisconnectCause.POWER_OFF: 503 resID = R.string.callFailed_powerOff; 504 break; 505 506 case DisconnectCause.ICC_ERROR: 507 resID = R.string.callFailed_simError; 508 break; 509 510 case DisconnectCause.OUT_OF_SERVICE: 511 resID = R.string.callFailed_outOfService; 512 break; 513 514 case DisconnectCause.INVALID_NUMBER: 515 case DisconnectCause.UNOBTAINABLE_NUMBER: 516 resID = R.string.callFailed_unobtainable_number; 517 break; 518 519 default: 520 resID = R.string.card_title_call_ended; 521 break; 522 } 523 return this.getView().getContext().getString(resID); 524 } 525 526 private void showAndInitializeSecondaryCallInfo() { 527 mSecondaryCallInfo.setVisibility(View.VISIBLE); 528 529 // mSecondaryCallName is initialized here (vs. onViewCreated) because it is inaccesible 530 // until mSecondaryCallInfo is inflated in the call above. 531 if (mSecondaryCallName == null) { 532 mSecondaryCallName = (TextView) getView().findViewById(R.id.secondaryCallName); 533 } 534 if (mSecondaryPhoto == null) { 535 mSecondaryPhoto = (ImageView) getView().findViewById(R.id.secondaryCallPhoto); 536 } 537 538 if (mSecondaryPhotoOverlay == null) { 539 mSecondaryPhotoOverlay = getView().findViewById(R.id.dim_effect_for_secondary_photo); 540 mSecondaryPhotoOverlay.setOnClickListener(new OnClickListener() { 541 @Override 542 public void onClick(View v) { 543 getPresenter().secondaryPhotoClicked(); 544 } 545 }); 546 mSecondaryPhotoOverlay.setOnTouchListener(new SmallerHitTargetTouchListener()); 547 } 548 } 549 550 public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 551 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 552 dispatchPopulateAccessibilityEvent(event, mPrimaryName); 553 dispatchPopulateAccessibilityEvent(event, mPhoneNumber); 554 return; 555 } 556 dispatchPopulateAccessibilityEvent(event, mCallStateLabel); 557 dispatchPopulateAccessibilityEvent(event, mPrimaryName); 558 dispatchPopulateAccessibilityEvent(event, mPhoneNumber); 559 dispatchPopulateAccessibilityEvent(event, mCallTypeLabel); 560 dispatchPopulateAccessibilityEvent(event, mSecondaryCallName); 561 562 return; 563 } 564 565 private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) { 566 if (view == null) return; 567 final List<CharSequence> eventText = event.getText(); 568 int size = eventText.size(); 569 view.dispatchPopulateAccessibilityEvent(event); 570 // if no text added write null to keep relative position 571 if (size == eventText.size()) { 572 eventText.add(null); 573 } 574 } 575} 576