1 /* 2 * Copyright (C) 2015 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 package com.android.car.dialer; 17 18 import android.content.ContentResolver; 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Color; 22 import android.os.Bundle; 23 import android.os.Handler; 24 import android.support.car.ui.CircleBitmapDrawable; 25 import android.support.car.ui.FabDrawable; 26 import android.support.v4.app.Fragment; 27 import android.telecom.Call; 28 import android.telecom.CallAudioState; 29 import android.text.TextUtils; 30 import android.text.format.DateUtils; 31 import android.util.Log; 32 import android.view.KeyEvent; 33 import android.view.LayoutInflater; 34 import android.view.MotionEvent; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.view.animation.AccelerateDecelerateInterpolator; 38 import android.view.animation.AccelerateInterpolator; 39 import android.view.animation.Animation; 40 import android.view.animation.Interpolator; 41 import android.view.animation.Transformation; 42 import android.widget.ImageButton; 43 import android.widget.ImageView; 44 import android.widget.LinearLayout; 45 import android.widget.TextView; 46 import com.android.car.dialer.bluetooth.UiBluetoothMonitor; 47 import com.android.car.dialer.telecom.TelecomUtils; 48 import com.android.car.dialer.telecom.UiCall; 49 import com.android.car.dialer.telecom.UiCallManager; 50 import com.android.car.dialer.telecom.UiCallManager.CallListener; 51 52 import java.util.Arrays; 53 import java.util.HashMap; 54 import java.util.List; 55 56 public class OngoingCallFragment extends Fragment { 57 private static final String TAG = "Em.OngoingCall"; 58 private static final HashMap<Integer, Character> mDialpadButtonMap = new HashMap<>(); 59 60 static { mDialpadButtonMap.put(R.id.one, '1')61 mDialpadButtonMap.put(R.id.one, '1'); mDialpadButtonMap.put(R.id.two, '2')62 mDialpadButtonMap.put(R.id.two, '2'); mDialpadButtonMap.put(R.id.three, '3')63 mDialpadButtonMap.put(R.id.three, '3'); mDialpadButtonMap.put(R.id.four, '4')64 mDialpadButtonMap.put(R.id.four, '4'); mDialpadButtonMap.put(R.id.five, '5')65 mDialpadButtonMap.put(R.id.five, '5'); mDialpadButtonMap.put(R.id.six, '6')66 mDialpadButtonMap.put(R.id.six, '6'); mDialpadButtonMap.put(R.id.seven, '7')67 mDialpadButtonMap.put(R.id.seven, '7'); mDialpadButtonMap.put(R.id.eight, '8')68 mDialpadButtonMap.put(R.id.eight, '8'); mDialpadButtonMap.put(R.id.nine, '9')69 mDialpadButtonMap.put(R.id.nine, '9'); mDialpadButtonMap.put(R.id.zero, '0')70 mDialpadButtonMap.put(R.id.zero, '0'); mDialpadButtonMap.put(R.id.star, '*')71 mDialpadButtonMap.put(R.id.star, '*'); mDialpadButtonMap.put(R.id.pound, '#')72 mDialpadButtonMap.put(R.id.pound, '#'); 73 } 74 75 private UiCall mPrimaryCall; 76 private UiCall mSecondaryCall; 77 private UiCall mLastRemovedCall; 78 private UiCallManager mUiCallManager; 79 private Handler mHandler; 80 private View mRingingCallControls; 81 private View mActiveCallControls; 82 private ImageButton mEndCallButton; 83 private ImageButton mUnholdCallButton; 84 private ImageButton mMuteButton; 85 private ImageButton mToggleDialpadButton; 86 private ImageButton mSwapButton; 87 private ImageButton mMergeButton; 88 private ImageButton mAnswerCallButton; 89 private ImageButton mRejectCallButton; 90 private TextView mNameTextView; 91 private TextView mSecondaryNameTextView; 92 private TextView mStateTextView; 93 private TextView mSecondaryStateTextView; 94 private ImageView mLargeContactPhotoView; 95 private ImageView mSmallContactPhotoView; 96 private View mDialpadContainer; 97 private View mSecondaryCallContainer; 98 private View mSecondaryCallControls; 99 private LinearLayout mRotaryDialpad; 100 private List<View> mDialpadViews; 101 private String mLoadedNumber; 102 private CharSequence mCallInfoLabel; 103 private boolean mIsHfpConnected; 104 private UiBluetoothMonitor mUiBluetoothMonitor; 105 106 private final Interpolator 107 mAccelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator(); 108 private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator(10); 109 110 @Override onCreate(Bundle savedInstanceState)111 public void onCreate(Bundle savedInstanceState) { 112 super.onCreate(savedInstanceState); 113 mUiCallManager = UiCallManager.getInstance(getContext()); 114 mUiBluetoothMonitor = UiBluetoothMonitor.getInstance(); 115 mHandler = new Handler(); 116 } 117 118 @Override onDestroy()119 public void onDestroy() { 120 super.onDestroy(); 121 mHandler.removeCallbacks(mUpdateDurationRunnable); 122 mHandler.removeCallbacks(mStopDtmfToneRunnable); 123 mHandler = null; 124 mUiCallManager = null; 125 mLoadedNumber = null; 126 mUiBluetoothMonitor = null; 127 } 128 129 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)130 public View onCreateView(LayoutInflater inflater, ViewGroup container, 131 Bundle savedInstanceState) { 132 View view = inflater.inflate(R.layout.ongoing_call, container, false); 133 mRingingCallControls = view.findViewById(R.id.ringing_call_controls); 134 mActiveCallControls = view.findViewById(R.id.active_call_controls); 135 mEndCallButton = (ImageButton) view.findViewById(R.id.end_call); 136 mUnholdCallButton = (ImageButton) view.findViewById(R.id.unhold_call); 137 mMuteButton = (ImageButton) view.findViewById(R.id.mute); 138 mToggleDialpadButton = (ImageButton) view.findViewById(R.id.toggle_dialpad); 139 mDialpadContainer = view.findViewById(R.id.dialpad_container); 140 mNameTextView = (TextView) view.findViewById(R.id.name); 141 mSecondaryNameTextView = (TextView) view.findViewById(R.id.name_secondary); 142 mStateTextView = (TextView) view.findViewById(R.id.info); 143 mSecondaryStateTextView = (TextView) view.findViewById(R.id.info_secondary); 144 mLargeContactPhotoView = (ImageView) view.findViewById(R.id.large_contact_photo); 145 mSmallContactPhotoView = (ImageView) view.findViewById(R.id.small_contact_photo); 146 mSecondaryCallContainer = view.findViewById(R.id.secondary_call_container); 147 mSecondaryCallControls = view.findViewById(R.id.secondary_call_controls); 148 mRotaryDialpad = (LinearLayout) view.findViewById(R.id.rotary_dialpad); 149 mSwapButton = (ImageButton) view.findViewById(R.id.swap); 150 mMergeButton = (ImageButton) view.findViewById(R.id.merge); 151 mAnswerCallButton = (ImageButton) view.findViewById(R.id.answer_call_button); 152 mRejectCallButton = (ImageButton) view.findViewById(R.id.reject_call_button); 153 154 boolean hasTouch = getResources().getBoolean(R.bool.has_touch); 155 View dialPadContainer = hasTouch ? mDialpadContainer : mRotaryDialpad; 156 mDialpadViews = Arrays.asList( 157 dialPadContainer.findViewById(R.id.one), 158 dialPadContainer.findViewById(R.id.two), 159 dialPadContainer.findViewById(R.id.three), 160 dialPadContainer.findViewById(R.id.four), 161 dialPadContainer.findViewById(R.id.five), 162 dialPadContainer.findViewById(R.id.six), 163 dialPadContainer.findViewById(R.id.seven), 164 dialPadContainer.findViewById(R.id.eight), 165 dialPadContainer.findViewById(R.id.nine), 166 dialPadContainer.findViewById(R.id.zero), 167 dialPadContainer.findViewById(R.id.pound), 168 dialPadContainer.findViewById(R.id.star) 169 ); 170 if (hasTouch) { 171 // In touch screen, we need to adjust the InCall card for the narrow screen to show the 172 // full dial pad. 173 for (View dialpadView : mDialpadViews) { 174 dialpadView.setOnTouchListener(mDialpadTouchListener); 175 dialpadView.setOnKeyListener(mDialpadKeyListener); 176 } 177 } else { 178 for (View dialpadView : mDialpadViews) { 179 dialpadView.setOnKeyListener(mDialpadKeyListener); 180 } 181 mToggleDialpadButton.setImageResource(R.drawable.ic_rotary_dialpad); 182 } 183 setDialPadFocusability(!hasTouch); 184 setInCallControllerFocusability(!hasTouch); 185 186 mAnswerCallButton.setOnClickListener((unusedView) -> { 187 UiCall call = mUiCallManager.getCallWithState(Call.STATE_RINGING); 188 if (call == null) { 189 Log.w(TAG, "There is no incoming call to answer."); 190 return; 191 } 192 mUiCallManager.answerCall(call); 193 }); 194 Context context = getContext(); 195 FabDrawable answerCallDrawable = new FabDrawable(context); 196 answerCallDrawable.setFabAndStrokeColor(getResources().getColor(R.color.phone_call)); 197 mAnswerCallButton.setBackground(answerCallDrawable); 198 199 mRejectCallButton.setOnClickListener((unusedView) -> { 200 UiCall call = mUiCallManager.getCallWithState(Call.STATE_RINGING); 201 if (call == null) { 202 Log.w(TAG, "There is no incoming call to reject."); 203 return; 204 } 205 mUiCallManager.rejectCall(call, false, null); 206 }); 207 208 mEndCallButton.setOnClickListener((unusedView) -> { 209 UiCall call = mUiCallManager.getPrimaryCall(); 210 if (call == null) { 211 Log.w(TAG, "There is no active call to end."); 212 return; 213 } 214 mUiCallManager.disconnectCall(call); 215 }); 216 FabDrawable endCallDrawable = new FabDrawable(context); 217 endCallDrawable.setFabAndStrokeColor(getResources().getColor(R.color.phone_end_call)); 218 mEndCallButton.setBackground(endCallDrawable); 219 220 mUnholdCallButton.setOnClickListener((unusedView) -> { 221 UiCall call = mUiCallManager.getPrimaryCall(); 222 if (call == null) { 223 Log.w(TAG, "There is no active call to unhold."); 224 return; 225 } 226 mUiCallManager.unholdCall(call); 227 }); 228 FabDrawable unholdCallDrawable = new FabDrawable(context); 229 unholdCallDrawable.setFabAndStrokeColor(getResources().getColor(R.color.phone_call)); 230 mUnholdCallButton.setBackground(unholdCallDrawable); 231 232 mMuteButton.setOnClickListener((unusedView) -> { 233 if (mUiCallManager.getMuted()) { 234 mUiCallManager.setMuted(false); 235 } else { 236 mUiCallManager.setMuted(true); 237 } 238 }); 239 240 mSwapButton.setOnClickListener((unusedView) -> { 241 UiCall call = mUiCallManager.getPrimaryCall(); 242 if (call == null) { 243 Log.w(TAG, "There is no active call to hold."); 244 return; 245 } 246 if (call.getState() == Call.STATE_HOLDING) { 247 mUiCallManager.unholdCall(call); 248 } else { 249 mUiCallManager.holdCall(call); 250 } 251 }); 252 253 mMergeButton.setOnClickListener((unusedView) -> { 254 UiCall call = mUiCallManager.getPrimaryCall(); 255 UiCall secondarycall = mUiCallManager.getSecondaryCall(); 256 if (call == null || mSecondaryCall == null) { 257 Log.w(TAG, "There aren't two call to merge."); 258 return; 259 } 260 261 mUiCallManager.conference(call, secondarycall); 262 }); 263 264 mToggleDialpadButton.setOnClickListener((unusedView) -> { 265 if (mToggleDialpadButton.isActivated()) { 266 closeDialpad(); 267 } else { 268 openDialpad(true /*animate*/); 269 } 270 }); 271 272 mUiCallManager.addListener(mCallListener); 273 274 // These must be called after the views are inflated because they have the side affect 275 // of updating the ui. 276 mUiBluetoothMonitor.addListener(mBluetoothListener); 277 mBluetoothListener.onStateChanged(); // Trigger state change to set initial state. 278 279 updateCalls(); 280 updateRotaryFocus(); 281 282 return view; 283 } 284 285 @Override onDestroyView()286 public void onDestroyView() { 287 super.onDestroyView(); 288 mUiCallManager.removeListener(mCallListener); 289 mUiBluetoothMonitor.removeListener(mBluetoothListener); 290 } 291 292 @Override onStart()293 public void onStart() { 294 super.onStart(); 295 trySpeakerAudioRouteIfNecessary(); 296 } 297 rebindViews()298 private void rebindViews() { 299 mHandler.removeCallbacks(mUpdateDurationRunnable); 300 301 // Toggle the visibility between the active call controls, ringing call controls, 302 // and no controls. 303 CharSequence disconnectCauseLabel = mLastRemovedCall == null ? 304 null : mLastRemovedCall.getDisconnectClause(); 305 if (mPrimaryCall == null && !TextUtils.isEmpty(disconnectCauseLabel)) { 306 closeDialpad(); 307 setStateText(disconnectCauseLabel); 308 return; 309 } else if (mPrimaryCall == null || mPrimaryCall.getState() == Call.STATE_DISCONNECTED) { 310 closeDialpad(); 311 setStateText(getString(R.string.call_state_call_ended)); 312 mRingingCallControls.setVisibility(View.GONE); 313 mActiveCallControls.setVisibility(View.GONE); 314 return; 315 } else if (mPrimaryCall.getState() == Call.STATE_RINGING) { 316 mRingingCallControls.setVisibility(View.VISIBLE); 317 mActiveCallControls.setVisibility(View.GONE); 318 } else { 319 mRingingCallControls.setVisibility(View.GONE); 320 mActiveCallControls.setVisibility(View.VISIBLE); 321 } 322 323 // Show the primary contact photo in the large ImageView on the right if there is no 324 // secondary call. Otherwise, show it in the small ImageView that is inside the card. 325 Context context = getContext(); 326 final ContentResolver cr = context.getContentResolver(); 327 final String primaryNumber = mPrimaryCall.getNumber(); 328 // Don't reload the image if the number is the same. 329 if ((primaryNumber != null && !primaryNumber.equals(mLoadedNumber)) 330 || (primaryNumber == null && mLoadedNumber != null)) { 331 BitmapWorkerTask.BitmapRunnable runnable = new BitmapWorkerTask.BitmapRunnable() { 332 @Override 333 public void run() { 334 if (mBitmap != null) { 335 Resources r = mSmallContactPhotoView.getResources(); 336 mSmallContactPhotoView.setImageDrawable( 337 new CircleBitmapDrawable(r, mBitmap)); 338 mLargeContactPhotoView.setImageBitmap(mBitmap); 339 mLargeContactPhotoView.clearColorFilter(); 340 } else { 341 mSmallContactPhotoView.setImageResource(R.drawable.logo_avatar); 342 mLargeContactPhotoView.setImageResource(R.drawable.ic_avatar_bg); 343 } 344 345 if (mSecondaryCall != null) { 346 BitmapWorkerTask.BitmapRunnable secondCallContactPhotoHandler = 347 new BitmapWorkerTask.BitmapRunnable() { 348 @Override 349 public void run() { 350 if (mBitmap != null) { 351 mLargeContactPhotoView.setImageBitmap(mBitmap); 352 } else { 353 mLargeContactPhotoView.setImageResource( 354 R.drawable.logo_avatar); 355 } 356 } 357 }; 358 359 BitmapWorkerTask.loadBitmap( 360 cr, mLargeContactPhotoView, mSecondaryCall.getNumber(), 361 secondCallContactPhotoHandler); 362 363 int scrimColor = getResources().getColor( 364 R.color.phone_secondary_call_scrim); 365 mLargeContactPhotoView.setColorFilter(scrimColor); 366 } 367 mLoadedNumber = primaryNumber; 368 } 369 }; 370 BitmapWorkerTask.loadBitmap(cr, mLargeContactPhotoView, primaryNumber, runnable); 371 } 372 373 if (mSecondaryCall != null) { 374 mSecondaryCallContainer.setVisibility(View.VISIBLE); 375 if (mPrimaryCall.getState() == Call.STATE_ACTIVE 376 && mSecondaryCall.getState() == Call.STATE_HOLDING) { 377 mSecondaryCallControls.setVisibility(View.VISIBLE); 378 } else { 379 mSecondaryCallControls.setVisibility(View.GONE); 380 } 381 } else { 382 mSecondaryCallContainer.setVisibility(View.GONE); 383 mSecondaryCallControls.setVisibility(View.GONE); 384 } 385 386 String displayName = TelecomUtils.getDisplayName(context, mPrimaryCall); 387 mNameTextView.setText(displayName); 388 mNameTextView.setVisibility(TextUtils.isEmpty(displayName) ? View.GONE : View.VISIBLE); 389 390 if (mSecondaryCall != null) { 391 mSecondaryNameTextView.setText( 392 TelecomUtils.getDisplayName(context, mSecondaryCall)); 393 } 394 395 switch (mPrimaryCall.getState()) { 396 case Call.STATE_NEW: 397 // Since the content resolver call is only cached when a contact is found, 398 // this should only be called once on a new call to avoid jank. 399 // TODO: consider moving TelecomUtils.getTypeFromNumber into a CursorLoader 400 String number = mPrimaryCall.getNumber(); 401 mCallInfoLabel = TelecomUtils.getTypeFromNumber(context, number); 402 case Call.STATE_CONNECTING: 403 case Call.STATE_DIALING: 404 case Call.STATE_SELECT_PHONE_ACCOUNT: 405 case Call.STATE_HOLDING: 406 case Call.STATE_DISCONNECTED: 407 mHandler.removeCallbacks(mUpdateDurationRunnable); 408 String callInfoText = TelecomUtils.getCallInfoText(context, 409 mPrimaryCall, mCallInfoLabel); 410 setStateText(callInfoText); 411 break; 412 case Call.STATE_ACTIVE: 413 if (mIsHfpConnected) { 414 mHandler.post(mUpdateDurationRunnable); 415 } 416 break; 417 case Call.STATE_RINGING: 418 Log.w(TAG, "There should not be a ringing call in the ongoing call fragment."); 419 break; 420 default: 421 Log.w(TAG, "Unhandled call state: " + mPrimaryCall.getState()); 422 } 423 424 if (mSecondaryCall != null) { 425 mSecondaryStateTextView.setText( 426 TelecomUtils.callStateToUiString(context, mSecondaryCall.getState())); 427 } 428 429 // If it is a voicemail call, open the dialpad (with no animation). 430 if (primaryNumber != null && primaryNumber.equals( 431 TelecomUtils.getVoicemailNumber(context))) { 432 if (getResources().getBoolean(R.bool.has_touch)) { 433 openDialpad(false /*animate*/); 434 mToggleDialpadButton.setVisibility(View.GONE); 435 } else { 436 mToggleDialpadButton.setVisibility(View.VISIBLE); 437 mToggleDialpadButton.requestFocus(); 438 } 439 } else { 440 mToggleDialpadButton.setVisibility(View.VISIBLE); 441 } 442 443 // Handle the holding case. 444 if (mPrimaryCall.getState() == Call.STATE_HOLDING) { 445 mEndCallButton.setVisibility(View.GONE); 446 mUnholdCallButton.setVisibility(View.VISIBLE); 447 mMuteButton.setVisibility(View.INVISIBLE); 448 mToggleDialpadButton.setVisibility(View.INVISIBLE); 449 } else { 450 mEndCallButton.setVisibility(View.VISIBLE); 451 mUnholdCallButton.setVisibility(View.GONE); 452 mMuteButton.setVisibility(View.VISIBLE); 453 mToggleDialpadButton.setVisibility(View.VISIBLE); 454 } 455 } 456 setStateText(CharSequence stateText)457 private void setStateText(CharSequence stateText) { 458 mStateTextView.setText(stateText); 459 mStateTextView.setVisibility(TextUtils.isEmpty(stateText) ? View.GONE : View.VISIBLE); 460 } 461 updateCalls()462 private void updateCalls() { 463 mPrimaryCall = mUiCallManager.getPrimaryCall(); 464 if (mPrimaryCall != null && mPrimaryCall.getState() == Call.STATE_RINGING) { 465 // TODO: update when notifications will work 466 } 467 mSecondaryCall = mUiCallManager.getSecondaryCall(); 468 if (Log.isLoggable(TAG, Log.DEBUG)) { 469 Log.d(TAG, "Primary call: " + mPrimaryCall + "\tSecondary call:" + mSecondaryCall); 470 } 471 rebindViews(); 472 } 473 474 /** 475 * If the phone is using bluetooth: 476 * * Do nothing 477 * If the phone is not using bluetooth: 478 * * If the phone supports bluetooth, use it. 479 * * If the phone doesn't support bluetooth and support speaker, use speaker 480 * * Otherwise, do nothing. Hopefully no phones won't have bt or speaker. 481 */ trySpeakerAudioRouteIfNecessary()482 private void trySpeakerAudioRouteIfNecessary() { 483 if (mUiCallManager == null) { 484 return; 485 } 486 487 int supportedAudioRouteMask = mUiCallManager.getSupportedAudioRouteMask(); 488 boolean supportsBluetooth = (supportedAudioRouteMask & CallAudioState.ROUTE_BLUETOOTH) != 0; 489 boolean supportsSpeaker = (supportedAudioRouteMask & CallAudioState.ROUTE_SPEAKER) != 0; 490 boolean isUsingBluetooth = 491 mUiCallManager.getAudioRoute() == CallAudioState.ROUTE_BLUETOOTH; 492 493 if (supportsBluetooth && !isUsingBluetooth) { 494 mUiCallManager.setAudioRoute(CallAudioState.ROUTE_BLUETOOTH); 495 } else if (!supportsBluetooth && supportsSpeaker) { 496 mUiCallManager.setAudioRoute(CallAudioState.ROUTE_SPEAKER); 497 } 498 } 499 openDialpad(boolean animate)500 private void openDialpad(boolean animate) { 501 if (mToggleDialpadButton.isActivated()) { 502 return; 503 } 504 mToggleDialpadButton.setActivated(true); 505 if (getResources().getBoolean(R.bool.has_touch)) { 506 // This array of of size 2 because getLocationOnScreen returns (x,y) coordinates. 507 int[] location = new int[2]; 508 mToggleDialpadButton.getLocationOnScreen(location); 509 510 // The dialpad should be aligned with the right edge of mToggleDialpadButton. 511 int startingMargin = location[1] + mToggleDialpadButton.getWidth(); 512 513 ViewGroup.MarginLayoutParams layoutParams = 514 (ViewGroup.MarginLayoutParams) mDialpadContainer.getLayoutParams(); 515 516 if (layoutParams.getMarginStart() != startingMargin) { 517 layoutParams.setMarginStart(startingMargin); 518 mDialpadContainer.setLayoutParams(layoutParams); 519 } 520 521 Animation anim = new DialpadAnimation(getContext(), false /* reverse */, animate); 522 mDialpadContainer.startAnimation(anim); 523 } else { 524 final int toggleButtonImageOffset = getResources().getDimensionPixelSize( 525 R.dimen.in_call_toggle_button_image_offset); 526 final int muteButtonLeftMargin = 527 ((LinearLayout.LayoutParams) mMuteButton.getLayoutParams()).leftMargin; 528 529 mEndCallButton.animate() 530 .alpha(0) 531 .setStartDelay(0) 532 .setDuration(384) 533 .setInterpolator(mAccelerateDecelerateInterpolator) 534 .withEndAction(() -> { 535 mEndCallButton.setVisibility(View.INVISIBLE); 536 mEndCallButton.setFocusable(false); 537 }).start(); 538 mMuteButton.animate() 539 .alpha(0) 540 .setStartDelay(0) 541 .setDuration(240) 542 .setInterpolator(mAccelerateDecelerateInterpolator) 543 .withEndAction(() -> { 544 mMuteButton.setVisibility(View.INVISIBLE); 545 mMuteButton.setFocusable(false); 546 }).start(); 547 mToggleDialpadButton.animate() 548 .setStartDelay(0) 549 .translationX(-(mEndCallButton.getWidth() + muteButtonLeftMargin 550 + mMuteButton.getWidth() + toggleButtonImageOffset)) 551 .setDuration(480) 552 .setInterpolator(mAccelerateDecelerateInterpolator) 553 .start(); 554 555 mRotaryDialpad.setTranslationX( 556 -(mEndCallButton.getWidth() + muteButtonLeftMargin + toggleButtonImageOffset)); 557 mRotaryDialpad.animate() 558 .translationX(-(mEndCallButton.getWidth() + muteButtonLeftMargin 559 + mMuteButton.getWidth() + toggleButtonImageOffset)) 560 .setDuration(320) 561 .setInterpolator(mAccelerateDecelerateInterpolator) 562 .setStartDelay(240) 563 .withStartAction(() -> { 564 mRotaryDialpad.setVisibility(View.VISIBLE); 565 int delay = 0; 566 for (View dialpadView : mDialpadViews) { 567 dialpadView.setAlpha(0); 568 dialpadView.animate() 569 .alpha(1) 570 .setDuration(160) 571 .setStartDelay(delay) 572 .setInterpolator(mAccelerateInterpolator) 573 .start(); 574 delay += 10; 575 } 576 }).start(); 577 } 578 } 579 closeDialpad()580 private void closeDialpad() { 581 if (!mToggleDialpadButton.isActivated()) { 582 return; 583 } 584 mToggleDialpadButton.setActivated(false); 585 if (getResources().getBoolean(R.bool.has_touch)) { 586 Animation anim = new DialpadAnimation(getContext(), true /* reverse */); 587 mDialpadContainer.startAnimation(anim); 588 } else { 589 final int toggleButtonImageOffset = getResources().getDimensionPixelSize( 590 R.dimen.in_call_toggle_button_image_offset); 591 final int muteButtonLeftMargin = 592 ((LinearLayout.LayoutParams) mMuteButton.getLayoutParams()).leftMargin; 593 594 mRotaryDialpad.animate() 595 .setStartDelay(0) 596 .translationX(-(mEndCallButton.getWidth() 597 + muteButtonLeftMargin + toggleButtonImageOffset)) 598 .setDuration(320) 599 .setInterpolator(mAccelerateDecelerateInterpolator) 600 .withStartAction(() -> { 601 int delay = 0; 602 for (int i = mDialpadViews.size() - 1; i >= 0; i--) { 603 View dialpadView = mDialpadViews.get(i); 604 dialpadView.animate() 605 .alpha(0) 606 .setDuration(160) 607 .setStartDelay(delay) 608 .setInterpolator(mAccelerateInterpolator) 609 .start(); 610 delay += 10; 611 } 612 }).withEndAction(() -> { 613 mRotaryDialpad.setVisibility(View.GONE); 614 mRotaryDialpad.setTranslationX(0); 615 }).start(); 616 mToggleDialpadButton.animate() 617 .translationX(0) 618 .setDuration(480) 619 .setStartDelay(80) 620 .setInterpolator(mAccelerateDecelerateInterpolator) 621 .start(); 622 mMuteButton.animate() 623 .alpha(1) 624 .setDuration(176) 625 .setInterpolator(mAccelerateDecelerateInterpolator) 626 .setStartDelay(384) 627 .withStartAction(() -> { 628 mMuteButton.setVisibility(View.VISIBLE); 629 mMuteButton.setFocusable(true); 630 }).start(); 631 mEndCallButton.animate() 632 .alpha(1) 633 .setDuration(320) 634 .setInterpolator(mAccelerateDecelerateInterpolator) 635 .setStartDelay(240) 636 .withStartAction(() -> { 637 mEndCallButton.setVisibility(View.VISIBLE); 638 mEndCallButton.setFocusable(true); 639 }).start(); 640 } 641 } 642 updateRotaryFocus()643 private void updateRotaryFocus() { 644 boolean hasTouch = getResources().getBoolean(R.bool.has_touch); 645 if (mPrimaryCall != null && !hasTouch) { 646 if (mPrimaryCall.getState() == Call.STATE_RINGING) { 647 mRingingCallControls.requestFocus(); 648 } else { 649 mActiveCallControls.requestFocus(); 650 } 651 } 652 } 653 setInCallControllerFocusability(boolean focusable)654 private void setInCallControllerFocusability(boolean focusable) { 655 mSwapButton.setFocusable(focusable); 656 mMergeButton.setFocusable(focusable); 657 658 mAnswerCallButton.setFocusable(focusable); 659 mRejectCallButton.setFocusable(focusable); 660 661 mEndCallButton.setFocusable(focusable); 662 mUnholdCallButton.setFocusable(focusable); 663 mMuteButton.setFocusable(focusable); 664 mToggleDialpadButton.setFocusable(focusable); 665 } 666 setDialPadFocusability(boolean focusable)667 private void setDialPadFocusability(boolean focusable) { 668 for (View dialPadView : mDialpadViews) { 669 dialPadView.setFocusable(focusable); 670 } 671 } 672 673 private final View.OnTouchListener mDialpadTouchListener = new View.OnTouchListener() { 674 675 @Override 676 public boolean onTouch(View v, MotionEvent event) { 677 Character digit = mDialpadButtonMap.get(v.getId()); 678 if (digit == null) { 679 Log.w(TAG, "Unknown dialpad button pressed."); 680 return false; 681 } 682 if (event.getAction() == MotionEvent.ACTION_DOWN) { 683 v.setPressed(true); 684 mUiCallManager.playDtmfTone(mPrimaryCall, digit); 685 return true; 686 } else if (event.getAction() == MotionEvent.ACTION_UP) { 687 v.setPressed(false); 688 v.performClick(); 689 mUiCallManager.stopDtmfTone(mPrimaryCall); 690 return true; 691 } 692 693 return false; 694 } 695 }; 696 697 private final View.OnKeyListener mDialpadKeyListener = new View.OnKeyListener() { 698 @Override 699 public boolean onKey(View v, int keyCode, KeyEvent event) { 700 Character digit = mDialpadButtonMap.get(v.getId()); 701 if (digit == null) { 702 Log.w(TAG, "Unknown dialpad button pressed."); 703 return false; 704 } 705 706 if (event.getKeyCode() != KeyEvent.KEYCODE_DPAD_CENTER) { 707 return false; 708 } 709 710 if (event.getAction() == KeyEvent.ACTION_DOWN) { 711 v.setPressed(true); 712 mUiCallManager.playDtmfTone(mPrimaryCall, digit); 713 return true; 714 } else if (event.getAction() == KeyEvent.ACTION_UP) { 715 v.setPressed(false); 716 mUiCallManager.stopDtmfTone(mPrimaryCall); 717 return true; 718 } 719 720 return false; 721 } 722 }; 723 724 private final Runnable mUpdateDurationRunnable = new Runnable() { 725 @Override 726 public void run() { 727 if (mPrimaryCall.getState() != Call.STATE_ACTIVE) { 728 return; 729 } 730 String callInfoText = TelecomUtils.getCallInfoText(getContext(), 731 mPrimaryCall, mCallInfoLabel); 732 setStateText(callInfoText); 733 mHandler.postDelayed(this /* runnable */, DateUtils.SECOND_IN_MILLIS); 734 } 735 }; 736 737 private final Runnable mStopDtmfToneRunnable = new Runnable() { 738 @Override 739 public void run() { 740 mUiCallManager.stopDtmfTone(mPrimaryCall); 741 } 742 }; 743 744 private final class DialpadAnimation extends Animation { 745 private static final int DURATION = 300; 746 private static final float MAX_SCRIM_ALPHA = 0.6f; 747 748 private final int mStartingTranslation; 749 private final int mScrimColor; 750 private final boolean mReverse; 751 DialpadAnimation(Context context, boolean reverse)752 public DialpadAnimation(Context context, boolean reverse) { 753 this(context, reverse, true); 754 } 755 DialpadAnimation(Context context, boolean reverse, boolean animate)756 public DialpadAnimation(Context context, boolean reverse, boolean animate) { 757 setDuration(animate ? DURATION : 0); 758 setInterpolator(new AccelerateDecelerateInterpolator()); 759 Resources res = context.getResources(); 760 mStartingTranslation = 761 res.getDimensionPixelOffset(R.dimen.in_call_card_dialpad_translation_x); 762 mScrimColor = res.getColor(R.color.phone_theme); 763 mReverse = reverse; 764 } 765 766 @Override applyTransformation(float interpolatedTime, Transformation t)767 protected void applyTransformation(float interpolatedTime, Transformation t) { 768 if (mReverse) { 769 interpolatedTime = 1f - interpolatedTime; 770 } 771 int translationX = (int) (mStartingTranslation * (1f - interpolatedTime)); 772 mDialpadContainer.setTranslationX(translationX); 773 mDialpadContainer.setAlpha(interpolatedTime); 774 if (interpolatedTime == 0f) { 775 mDialpadContainer.setVisibility(View.GONE); 776 } else { 777 mDialpadContainer.setVisibility(View.VISIBLE); 778 } 779 float alpha = 255f * interpolatedTime * MAX_SCRIM_ALPHA; 780 mLargeContactPhotoView.setColorFilter(Color.argb((int) alpha, Color.red(mScrimColor), 781 Color.green(mScrimColor), Color.blue(mScrimColor))); 782 783 mSecondaryNameTextView.setAlpha(1f - interpolatedTime); 784 mSecondaryStateTextView.setAlpha(1f - interpolatedTime); 785 } 786 } 787 788 private final CallListener mCallListener = new CallListener() { 789 790 @Override 791 public void onCallAdded(UiCall call) { 792 if (Log.isLoggable(TAG, Log.DEBUG)) { 793 Log.d(TAG, "on call added"); 794 } 795 updateCalls(); 796 trySpeakerAudioRouteIfNecessary(); 797 } 798 799 @Override 800 public void onCallRemoved(UiCall call) { 801 if (Log.isLoggable(TAG, Log.DEBUG)) { 802 Log.d(TAG, "on call removed"); 803 } 804 mLastRemovedCall = call; 805 updateCalls(); 806 } 807 808 @Override 809 public void onAudioStateChanged(boolean isMuted, int audioRoute, 810 int supportedAudioRouteMask) { 811 if (Log.isLoggable(TAG, Log.DEBUG)) { 812 Log.d(TAG, "on audio state changed"); 813 } 814 mMuteButton.setActivated(isMuted); 815 trySpeakerAudioRouteIfNecessary(); 816 } 817 818 @Override 819 public void onStateChanged(UiCall call, int state) { 820 if (Log.isLoggable(TAG, Log.DEBUG)) { 821 Log.d(TAG, "onStateChanged"); 822 } 823 updateCalls(); 824 // this will reset the focus if any state of any call changes on pure rotary devices. 825 updateRotaryFocus(); 826 } 827 828 @Override 829 public void onCallUpdated(UiCall call) { 830 if (Log.isLoggable(TAG, Log.DEBUG)) { 831 Log.d(TAG, "onCallUpdated"); 832 } 833 updateCalls(); 834 } 835 }; 836 837 private final UiBluetoothMonitor.Listener mBluetoothListener = () -> { 838 OngoingCallFragment.this.mIsHfpConnected = 839 UiBluetoothMonitor.getInstance().isHfpConnected(); 840 }; 841 } 842