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