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