1/* 2 * Copyright (C) 2016 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.incall.impl; 18 19import android.Manifest.permission; 20import android.content.Context; 21import android.content.pm.PackageManager; 22import android.os.Build.VERSION; 23import android.os.Build.VERSION_CODES; 24import android.os.Bundle; 25import android.os.Handler; 26import android.support.annotation.ColorInt; 27import android.support.annotation.NonNull; 28import android.support.annotation.Nullable; 29import android.support.v4.app.Fragment; 30import android.support.v4.app.FragmentTransaction; 31import android.support.v4.content.ContextCompat; 32import android.telecom.CallAudioState; 33import android.telephony.TelephonyManager; 34import android.view.LayoutInflater; 35import android.view.View; 36import android.view.View.OnClickListener; 37import android.view.ViewGroup; 38import android.view.accessibility.AccessibilityEvent; 39import android.widget.ImageView; 40import android.widget.RelativeLayout; 41import android.widget.Toast; 42import com.android.dialer.common.Assert; 43import com.android.dialer.common.FragmentUtils; 44import com.android.dialer.common.LogUtil; 45import com.android.dialer.compat.ActivityCompat; 46import com.android.dialer.logging.DialerImpression; 47import com.android.dialer.logging.Logger; 48import com.android.dialer.multimedia.MultimediaData; 49import com.android.dialer.strictmode.StrictModeUtils; 50import com.android.dialer.util.ViewUtil; 51import com.android.dialer.widget.LockableViewPager; 52import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment; 53import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter; 54import com.android.incallui.contactgrid.ContactGridManager; 55import com.android.incallui.hold.OnHoldFragment; 56import com.android.incallui.incall.impl.ButtonController.SpeakerButtonController; 57import com.android.incallui.incall.impl.InCallButtonGridFragment.OnButtonGridCreatedListener; 58import com.android.incallui.incall.protocol.InCallButtonIds; 59import com.android.incallui.incall.protocol.InCallButtonIdsExtension; 60import com.android.incallui.incall.protocol.InCallButtonUi; 61import com.android.incallui.incall.protocol.InCallButtonUiDelegate; 62import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory; 63import com.android.incallui.incall.protocol.InCallScreen; 64import com.android.incallui.incall.protocol.InCallScreenDelegate; 65import com.android.incallui.incall.protocol.InCallScreenDelegateFactory; 66import com.android.incallui.incall.protocol.PrimaryCallState; 67import com.android.incallui.incall.protocol.PrimaryCallState.ButtonState; 68import com.android.incallui.incall.protocol.PrimaryInfo; 69import com.android.incallui.incall.protocol.SecondaryInfo; 70import java.util.ArrayList; 71import java.util.List; 72 73/** Fragment that shows UI for an ongoing voice call. */ 74public class InCallFragment extends Fragment 75 implements InCallScreen, 76 InCallButtonUi, 77 OnClickListener, 78 AudioRouteSelectorPresenter, 79 OnButtonGridCreatedListener { 80 81 private List<ButtonController> buttonControllers = new ArrayList<>(); 82 private View endCallButton; 83 private InCallPaginator paginator; 84 private LockableViewPager pager; 85 private InCallPagerAdapter adapter; 86 private ContactGridManager contactGridManager; 87 private InCallScreenDelegate inCallScreenDelegate; 88 private InCallButtonUiDelegate inCallButtonUiDelegate; 89 private InCallButtonGridFragment inCallButtonGridFragment; 90 @Nullable private ButtonChooser buttonChooser; 91 private SecondaryInfo savedSecondaryInfo; 92 private int voiceNetworkType; 93 private int phoneType; 94 private boolean stateRestored; 95 96 // Add animation to educate users. If a call has enriched calling attachments then we'll 97 // initially show the attachment page. After a delay seconds we'll animate to the button grid. 98 private final Handler handler = new Handler(); 99 private final Runnable pagerRunnable = 100 new Runnable() { 101 @Override 102 public void run() { 103 pager.setCurrentItem(adapter.getButtonGridPosition()); 104 } 105 }; 106 107 private static boolean isSupportedButton(@InCallButtonIds int id) { 108 return id == InCallButtonIds.BUTTON_AUDIO 109 || id == InCallButtonIds.BUTTON_MUTE 110 || id == InCallButtonIds.BUTTON_DIALPAD 111 || id == InCallButtonIds.BUTTON_HOLD 112 || id == InCallButtonIds.BUTTON_SWAP 113 || id == InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO 114 || id == InCallButtonIds.BUTTON_ADD_CALL 115 || id == InCallButtonIds.BUTTON_MERGE 116 || id == InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE 117 || id == InCallButtonIds.BUTTON_SWAP_SIM; 118 } 119 120 @Override 121 public void onAttach(Context context) { 122 super.onAttach(context); 123 if (savedSecondaryInfo != null) { 124 setSecondary(savedSecondaryInfo); 125 } 126 } 127 128 @Override 129 public void onCreate(Bundle savedInstanceState) { 130 super.onCreate(savedInstanceState); 131 inCallButtonUiDelegate = 132 FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class) 133 .newInCallButtonUiDelegate(); 134 if (savedInstanceState != null) { 135 inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState); 136 stateRestored = true; 137 } 138 } 139 140 @Nullable 141 @Override 142 public View onCreateView( 143 @NonNull LayoutInflater layoutInflater, 144 @Nullable ViewGroup viewGroup, 145 @Nullable Bundle bundle) { 146 LogUtil.i("InCallFragment.onCreateView", null); 147 // Bypass to avoid StrictModeResourceMismatchViolation 148 final View view = 149 StrictModeUtils.bypass( 150 () -> layoutInflater.inflate(R.layout.frag_incall_voice, viewGroup, false)); 151 contactGridManager = 152 new ContactGridManager( 153 view, 154 (ImageView) view.findViewById(R.id.contactgrid_avatar), 155 getResources().getDimensionPixelSize(R.dimen.incall_avatar_size), 156 true /* showAnonymousAvatar */); 157 contactGridManager.onMultiWindowModeChanged(ActivityCompat.isInMultiWindowMode(getActivity())); 158 159 paginator = (InCallPaginator) view.findViewById(R.id.incall_paginator); 160 pager = (LockableViewPager) view.findViewById(R.id.incall_pager); 161 pager.setOnTouchListener( 162 (v, event) -> { 163 handler.removeCallbacks(pagerRunnable); 164 return false; 165 }); 166 167 endCallButton = view.findViewById(R.id.incall_end_call); 168 endCallButton.setOnClickListener(this); 169 170 if (ContextCompat.checkSelfPermission(getContext(), permission.READ_PHONE_STATE) 171 != PackageManager.PERMISSION_GRANTED) { 172 voiceNetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN; 173 } else { 174 175 voiceNetworkType = 176 VERSION.SDK_INT >= VERSION_CODES.N 177 ? getContext().getSystemService(TelephonyManager.class).getVoiceNetworkType() 178 : TelephonyManager.NETWORK_TYPE_UNKNOWN; 179 } 180 // TODO(a bug): Change to use corresponding phone type used for current call. 181 phoneType = getContext().getSystemService(TelephonyManager.class).getPhoneType(); 182 View space = view.findViewById(R.id.navigation_bar_background); 183 space.getLayoutParams().height = ViewUtil.getNavigationBarHeight(getContext()); 184 185 return view; 186 } 187 188 @Override 189 public void onResume() { 190 super.onResume(); 191 inCallButtonUiDelegate.refreshMuteState(); 192 inCallScreenDelegate.onInCallScreenResumed(); 193 } 194 195 @Override 196 public void onViewCreated(@NonNull View view, @Nullable Bundle bundle) { 197 LogUtil.i("InCallFragment.onViewCreated", null); 198 super.onViewCreated(view, bundle); 199 inCallScreenDelegate = 200 FragmentUtils.getParent(this, InCallScreenDelegateFactory.class).newInCallScreenDelegate(); 201 Assert.isNotNull(inCallScreenDelegate); 202 203 buttonControllers.add(new ButtonController.MuteButtonController(inCallButtonUiDelegate)); 204 buttonControllers.add(new ButtonController.SpeakerButtonController(inCallButtonUiDelegate)); 205 buttonControllers.add(new ButtonController.DialpadButtonController(inCallButtonUiDelegate)); 206 buttonControllers.add(new ButtonController.HoldButtonController(inCallButtonUiDelegate)); 207 buttonControllers.add(new ButtonController.AddCallButtonController(inCallButtonUiDelegate)); 208 buttonControllers.add(new ButtonController.SwapButtonController(inCallButtonUiDelegate)); 209 buttonControllers.add(new ButtonController.MergeButtonController(inCallButtonUiDelegate)); 210 buttonControllers.add(new ButtonController.SwapSimButtonController(inCallButtonUiDelegate)); 211 buttonControllers.add( 212 new ButtonController.UpgradeToVideoButtonController(inCallButtonUiDelegate)); 213 buttonControllers.add( 214 new ButtonController.ManageConferenceButtonController(inCallScreenDelegate)); 215 buttonControllers.add( 216 new ButtonController.SwitchToSecondaryButtonController(inCallScreenDelegate)); 217 218 inCallScreenDelegate.onInCallScreenDelegateInit(this); 219 inCallScreenDelegate.onInCallScreenReady(); 220 } 221 222 @Override 223 public void onPause() { 224 super.onPause(); 225 inCallScreenDelegate.onInCallScreenPaused(); 226 } 227 228 @Override 229 public void onDestroyView() { 230 super.onDestroyView(); 231 inCallScreenDelegate.onInCallScreenUnready(); 232 } 233 234 @Override 235 public void onSaveInstanceState(Bundle outState) { 236 super.onSaveInstanceState(outState); 237 inCallButtonUiDelegate.onSaveInstanceState(outState); 238 } 239 240 @Override 241 public void onClick(View view) { 242 if (view == endCallButton) { 243 LogUtil.i("InCallFragment.onClick", "end call button clicked"); 244 Logger.get(getContext()) 245 .logImpression(DialerImpression.Type.IN_CALL_DIALPAD_HANG_UP_BUTTON_PRESSED); 246 inCallScreenDelegate.onEndCallClicked(); 247 } else { 248 LogUtil.e("InCallFragment.onClick", "unknown view: " + view); 249 Assert.fail(); 250 } 251 } 252 253 @Override 254 public void setPrimary(@NonNull PrimaryInfo primaryInfo) { 255 LogUtil.i("InCallFragment.setPrimary", primaryInfo.toString()); 256 setAdapterMedia(primaryInfo.multimediaData(), primaryInfo.showInCallButtonGrid()); 257 contactGridManager.setPrimary(primaryInfo); 258 259 if (primaryInfo.shouldShowLocation()) { 260 // Hide the avatar to make room for location 261 contactGridManager.setAvatarHidden(true); 262 263 // Need to let the dialpad move up a little further when location info is being shown 264 View dialpadView = getView().findViewById(R.id.incall_dialpad_container); 265 ViewGroup.LayoutParams params = dialpadView.getLayoutParams(); 266 if (params instanceof RelativeLayout.LayoutParams) { 267 ((RelativeLayout.LayoutParams) params).removeRule(RelativeLayout.BELOW); 268 } 269 dialpadView.setLayoutParams(params); 270 } 271 } 272 273 private void setAdapterMedia(MultimediaData multimediaData, boolean showInCallButtonGrid) { 274 if (adapter == null) { 275 adapter = 276 new InCallPagerAdapter(getChildFragmentManager(), multimediaData, showInCallButtonGrid); 277 pager.setAdapter(adapter); 278 } else { 279 adapter.setAttachments(multimediaData); 280 } 281 282 if (adapter.getCount() > 1 && getResources().getInteger(R.integer.incall_num_rows) > 1) { 283 paginator.setVisibility(View.VISIBLE); 284 paginator.setupWithViewPager(pager); 285 pager.setSwipingLocked(false); 286 if (!stateRestored) { 287 handler.postDelayed(pagerRunnable, 4_000); 288 } else { 289 pager.setCurrentItem(adapter.getButtonGridPosition(), false /* animateScroll */); 290 } 291 } else { 292 paginator.setVisibility(View.GONE); 293 } 294 } 295 296 @Override 297 public void setSecondary(@NonNull SecondaryInfo secondaryInfo) { 298 LogUtil.i("InCallFragment.setSecondary", secondaryInfo.toString()); 299 updateButtonStates(); 300 301 if (!isAdded()) { 302 savedSecondaryInfo = secondaryInfo; 303 return; 304 } 305 savedSecondaryInfo = null; 306 FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); 307 Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.incall_on_hold_banner); 308 if (secondaryInfo.shouldShow()) { 309 transaction.replace(R.id.incall_on_hold_banner, OnHoldFragment.newInstance(secondaryInfo)); 310 } else { 311 if (oldBanner != null) { 312 transaction.remove(oldBanner); 313 } 314 } 315 transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top); 316 transaction.commitNowAllowingStateLoss(); 317 } 318 319 @Override 320 public void setCallState(@NonNull PrimaryCallState primaryCallState) { 321 LogUtil.i("InCallFragment.setCallState", primaryCallState.toString()); 322 contactGridManager.setCallState(primaryCallState); 323 getButtonController(InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) 324 .setAllowed(primaryCallState.swapToSecondaryButtonState() != ButtonState.NOT_SUPPORT); 325 getButtonController(InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) 326 .setEnabled(primaryCallState.swapToSecondaryButtonState() == ButtonState.ENABLED); 327 buttonChooser = 328 ButtonChooserFactory.newButtonChooser( 329 voiceNetworkType, primaryCallState.isWifi(), phoneType); 330 updateButtonStates(); 331 } 332 333 @Override 334 public void setEndCallButtonEnabled(boolean enabled, boolean animate) { 335 if (endCallButton != null) { 336 endCallButton.setEnabled(enabled); 337 } 338 } 339 340 @Override 341 public void showManageConferenceCallButton(boolean visible) { 342 getButtonController(InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE).setAllowed(visible); 343 getButtonController(InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE).setEnabled(visible); 344 updateButtonStates(); 345 } 346 347 @Override 348 public boolean isManageConferenceVisible() { 349 return getButtonController(InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE).isAllowed(); 350 } 351 352 @Override 353 public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 354 contactGridManager.dispatchPopulateAccessibilityEvent(event); 355 } 356 357 @Override 358 public void showNoteSentToast() { 359 LogUtil.i("InCallFragment.showNoteSentToast", null); 360 Toast.makeText(getContext(), R.string.incall_note_sent, Toast.LENGTH_LONG).show(); 361 } 362 363 @Override 364 public void updateInCallScreenColors() {} 365 366 @Override 367 public void onInCallScreenDialpadVisibilityChange(boolean isShowing) { 368 LogUtil.i("InCallFragment.onInCallScreenDialpadVisibilityChange", "isShowing: " + isShowing); 369 // Take note that the dialpad button isShowing 370 getButtonController(InCallButtonIds.BUTTON_DIALPAD).setChecked(isShowing); 371 372 // This check is needed because there is a race condition where we attempt to update 373 // ButtonGridFragment before it is ready, so we check whether it is ready first and once it is 374 // ready, #onButtonGridCreated will mark the dialpad button as isShowing. 375 if (inCallButtonGridFragment != null) { 376 // Update the Android Button's state to isShowing. 377 inCallButtonGridFragment.onInCallScreenDialpadVisibilityChange(isShowing); 378 } 379 } 380 381 @Override 382 public int getAnswerAndDialpadContainerResourceId() { 383 return R.id.incall_dialpad_container; 384 } 385 386 @Override 387 public Fragment getInCallScreenFragment() { 388 return this; 389 } 390 391 @Override 392 public void showButton(@InCallButtonIds int buttonId, boolean show) { 393 LogUtil.v( 394 "InCallFragment.showButton", 395 "buttionId: %s, show: %b", 396 InCallButtonIdsExtension.toString(buttonId), 397 show); 398 if (isSupportedButton(buttonId)) { 399 getButtonController(buttonId).setAllowed(show); 400 if (buttonId == InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO && show) { 401 Logger.get(getContext()) 402 .logImpression(DialerImpression.Type.UPGRADE_TO_VIDEO_CALL_BUTTON_SHOWN); 403 } 404 } 405 } 406 407 @Override 408 public void enableButton(@InCallButtonIds int buttonId, boolean enable) { 409 LogUtil.v( 410 "InCallFragment.enableButton", 411 "buttonId: %s, enable: %b", 412 InCallButtonIdsExtension.toString(buttonId), 413 enable); 414 if (isSupportedButton(buttonId)) { 415 getButtonController(buttonId).setEnabled(enable); 416 } 417 } 418 419 @Override 420 public void setEnabled(boolean enabled) { 421 LogUtil.v("InCallFragment.setEnabled", "enabled: " + enabled); 422 for (ButtonController buttonController : buttonControllers) { 423 buttonController.setEnabled(enabled); 424 } 425 } 426 427 @Override 428 public void setHold(boolean value) { 429 getButtonController(InCallButtonIds.BUTTON_HOLD).setChecked(value); 430 } 431 432 @Override 433 public void setCameraSwitched(boolean isBackFacingCamera) {} 434 435 @Override 436 public void setVideoPaused(boolean isPaused) {} 437 438 @Override 439 public void setAudioState(CallAudioState audioState) { 440 LogUtil.i("InCallFragment.setAudioState", "audioState: " + audioState); 441 ((SpeakerButtonController) getButtonController(InCallButtonIds.BUTTON_AUDIO)) 442 .setAudioState(audioState); 443 getButtonController(InCallButtonIds.BUTTON_MUTE).setChecked(audioState.isMuted()); 444 } 445 446 @Override 447 public void updateButtonStates() { 448 // When the incall screen is ready, this method is called from #setSecondary, even though the 449 // incall button ui is not ready yet. This method is called again once the incall button ui is 450 // ready though, so this operation is safe and will be executed asap. 451 if (inCallButtonGridFragment == null) { 452 return; 453 } 454 int numVisibleButtons = 455 inCallButtonGridFragment.updateButtonStates( 456 buttonControllers, buttonChooser, voiceNetworkType, phoneType); 457 458 int visibility = numVisibleButtons == 0 ? View.GONE : View.VISIBLE; 459 pager.setVisibility(visibility); 460 if (adapter != null 461 && adapter.getCount() > 1 462 && getResources().getInteger(R.integer.incall_num_rows) > 1) { 463 paginator.setVisibility(View.VISIBLE); 464 pager.setSwipingLocked(false); 465 } else { 466 paginator.setVisibility(View.GONE); 467 if (adapter != null) { 468 pager.setSwipingLocked(true); 469 pager.setCurrentItem(adapter.getButtonGridPosition()); 470 } 471 } 472 } 473 474 @Override 475 public void updateInCallButtonUiColors(@ColorInt int color) { 476 inCallButtonGridFragment.updateButtonColor(color); 477 } 478 479 @Override 480 public Fragment getInCallButtonUiFragment() { 481 return this; 482 } 483 484 @Override 485 public void showAudioRouteSelector() { 486 AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState()) 487 .show(getChildFragmentManager(), null); 488 } 489 490 @Override 491 public void onAudioRouteSelected(int audioRoute) { 492 inCallButtonUiDelegate.setAudioRoute(audioRoute); 493 } 494 495 @Override 496 public void onAudioRouteSelectorDismiss() {} 497 498 @NonNull 499 @Override 500 public ButtonController getButtonController(@InCallButtonIds int id) { 501 for (ButtonController buttonController : buttonControllers) { 502 if (buttonController.getInCallButtonId() == id) { 503 return buttonController; 504 } 505 } 506 Assert.fail(); 507 return null; 508 } 509 510 @Override 511 public void onButtonGridCreated(InCallButtonGridFragment inCallButtonGridFragment) { 512 LogUtil.i("InCallFragment.onButtonGridCreated", "InCallUiReady"); 513 this.inCallButtonGridFragment = inCallButtonGridFragment; 514 inCallButtonUiDelegate.onInCallButtonUiReady(this); 515 updateButtonStates(); 516 } 517 518 @Override 519 public void onButtonGridDestroyed() { 520 LogUtil.i("InCallFragment.onButtonGridCreated", "InCallUiUnready"); 521 inCallButtonUiDelegate.onInCallButtonUiUnready(); 522 this.inCallButtonGridFragment = null; 523 } 524 525 @Override 526 public boolean isShowingLocationUi() { 527 Fragment fragment = getLocationFragment(); 528 return fragment != null && fragment.isVisible(); 529 } 530 531 @Override 532 public void showLocationUi(@Nullable Fragment locationUi) { 533 boolean isVisible = isShowingLocationUi(); 534 if (locationUi != null && !isVisible) { 535 // Show the location fragment. 536 getChildFragmentManager() 537 .beginTransaction() 538 .replace(R.id.incall_location_holder, locationUi) 539 .commitAllowingStateLoss(); 540 } else if (locationUi == null && isVisible) { 541 // Hide the location fragment 542 getChildFragmentManager() 543 .beginTransaction() 544 .remove(getLocationFragment()) 545 .commitAllowingStateLoss(); 546 } 547 } 548 549 @Override 550 public void onMultiWindowModeChanged(boolean isInMultiWindowMode) { 551 super.onMultiWindowModeChanged(isInMultiWindowMode); 552 if (isInMultiWindowMode == isShowingLocationUi()) { 553 LogUtil.i("InCallFragment.onMultiWindowModeChanged", "hide = " + isInMultiWindowMode); 554 // Need to show or hide location 555 showLocationUi(isInMultiWindowMode ? null : getLocationFragment()); 556 } 557 contactGridManager.onMultiWindowModeChanged(isInMultiWindowMode); 558 } 559 560 private Fragment getLocationFragment() { 561 return getChildFragmentManager().findFragmentById(R.id.incall_location_holder); 562 } 563} 564