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