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