| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.incallui.incall.impl; |
| |
| import android.Manifest.permission; |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.os.Build.VERSION; |
| import android.os.Build.VERSION_CODES; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.support.annotation.ColorInt; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.support.v4.app.Fragment; |
| import android.support.v4.app.FragmentTransaction; |
| import android.support.v4.content.ContextCompat; |
| import android.telecom.CallAudioState; |
| import android.telephony.TelephonyManager; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.ViewGroup; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.widget.ImageView; |
| import android.widget.RelativeLayout; |
| import android.widget.Toast; |
| import com.android.dialer.common.Assert; |
| import com.android.dialer.common.FragmentUtils; |
| import com.android.dialer.common.LogUtil; |
| import com.android.dialer.logging.DialerImpression; |
| import com.android.dialer.logging.Logger; |
| import com.android.dialer.multimedia.MultimediaData; |
| import com.android.dialer.strictmode.StrictModeUtils; |
| import com.android.dialer.util.ViewUtil; |
| import com.android.dialer.widget.LockableViewPager; |
| import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment; |
| import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter; |
| import com.android.incallui.contactgrid.ContactGridManager; |
| import com.android.incallui.hold.OnHoldFragment; |
| import com.android.incallui.incall.impl.ButtonController.SpeakerButtonController; |
| import com.android.incallui.incall.impl.InCallButtonGridFragment.OnButtonGridCreatedListener; |
| import com.android.incallui.incall.protocol.InCallButtonIds; |
| import com.android.incallui.incall.protocol.InCallButtonIdsExtension; |
| import com.android.incallui.incall.protocol.InCallButtonUi; |
| import com.android.incallui.incall.protocol.InCallButtonUiDelegate; |
| import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory; |
| import com.android.incallui.incall.protocol.InCallScreen; |
| import com.android.incallui.incall.protocol.InCallScreenDelegate; |
| import com.android.incallui.incall.protocol.InCallScreenDelegateFactory; |
| import com.android.incallui.incall.protocol.PrimaryCallState; |
| import com.android.incallui.incall.protocol.PrimaryCallState.ButtonState; |
| import com.android.incallui.incall.protocol.PrimaryInfo; |
| import com.android.incallui.incall.protocol.SecondaryInfo; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** Fragment that shows UI for an ongoing voice call. */ |
| public class InCallFragment extends Fragment |
| implements InCallScreen, |
| InCallButtonUi, |
| OnClickListener, |
| AudioRouteSelectorPresenter, |
| OnButtonGridCreatedListener { |
| |
| private List<ButtonController> buttonControllers = new ArrayList<>(); |
| private View endCallButton; |
| private InCallPaginator paginator; |
| private LockableViewPager pager; |
| private InCallPagerAdapter adapter; |
| private ContactGridManager contactGridManager; |
| private InCallScreenDelegate inCallScreenDelegate; |
| private InCallButtonUiDelegate inCallButtonUiDelegate; |
| private InCallButtonGridFragment inCallButtonGridFragment; |
| @Nullable private ButtonChooser buttonChooser; |
| private SecondaryInfo savedSecondaryInfo; |
| private int voiceNetworkType; |
| private int phoneType; |
| private boolean stateRestored; |
| |
| // Add animation to educate users. If a call has enriched calling attachments then we'll |
| // initially show the attachment page. After a delay seconds we'll animate to the button grid. |
| private final Handler handler = new Handler(); |
| private final Runnable pagerRunnable = |
| new Runnable() { |
| @Override |
| public void run() { |
| pager.setCurrentItem(adapter.getButtonGridPosition()); |
| } |
| }; |
| |
| private static boolean isSupportedButton(@InCallButtonIds int id) { |
| return id == InCallButtonIds.BUTTON_AUDIO |
| || id == InCallButtonIds.BUTTON_MUTE |
| || id == InCallButtonIds.BUTTON_DIALPAD |
| || id == InCallButtonIds.BUTTON_HOLD |
| || id == InCallButtonIds.BUTTON_SWAP |
| || id == InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO |
| || id == InCallButtonIds.BUTTON_ADD_CALL |
| || id == InCallButtonIds.BUTTON_MERGE |
| || id == InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE |
| || id == InCallButtonIds.BUTTON_SWAP_SIM; |
| } |
| |
| @Override |
| public void onAttach(Context context) { |
| super.onAttach(context); |
| if (savedSecondaryInfo != null) { |
| setSecondary(savedSecondaryInfo); |
| } |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| inCallButtonUiDelegate = |
| FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class) |
| .newInCallButtonUiDelegate(); |
| if (savedInstanceState != null) { |
| inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState); |
| stateRestored = true; |
| } |
| } |
| |
| @Nullable |
| @Override |
| public View onCreateView( |
| @NonNull LayoutInflater layoutInflater, |
| @Nullable ViewGroup viewGroup, |
| @Nullable Bundle bundle) { |
| LogUtil.i("InCallFragment.onCreateView", null); |
| // Bypass to avoid StrictModeResourceMismatchViolation |
| final View view = |
| StrictModeUtils.bypass( |
| () -> layoutInflater.inflate(R.layout.frag_incall_voice, viewGroup, false)); |
| contactGridManager = |
| new ContactGridManager( |
| view, |
| (ImageView) view.findViewById(R.id.contactgrid_avatar), |
| getResources().getDimensionPixelSize(R.dimen.incall_avatar_size), |
| true /* showAnonymousAvatar */); |
| |
| paginator = (InCallPaginator) view.findViewById(R.id.incall_paginator); |
| pager = (LockableViewPager) view.findViewById(R.id.incall_pager); |
| pager.setOnTouchListener( |
| (v, event) -> { |
| handler.removeCallbacks(pagerRunnable); |
| return false; |
| }); |
| |
| endCallButton = view.findViewById(R.id.incall_end_call); |
| endCallButton.setOnClickListener(this); |
| |
| if (ContextCompat.checkSelfPermission(getContext(), permission.READ_PHONE_STATE) |
| != PackageManager.PERMISSION_GRANTED) { |
| voiceNetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN; |
| } else { |
| |
| voiceNetworkType = |
| VERSION.SDK_INT >= VERSION_CODES.N |
| ? getContext().getSystemService(TelephonyManager.class).getVoiceNetworkType() |
| : TelephonyManager.NETWORK_TYPE_UNKNOWN; |
| } |
| phoneType = getContext().getSystemService(TelephonyManager.class).getPhoneType(); |
| View space = view.findViewById(R.id.navigation_bar_background); |
| space.getLayoutParams().height = ViewUtil.getNavigationBarHeight(getContext()); |
| |
| return view; |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| inCallButtonUiDelegate.refreshMuteState(); |
| inCallScreenDelegate.onInCallScreenResumed(); |
| } |
| |
| @Override |
| public void onViewCreated(@NonNull View view, @Nullable Bundle bundle) { |
| LogUtil.i("InCallFragment.onViewCreated", null); |
| super.onViewCreated(view, bundle); |
| inCallScreenDelegate = |
| FragmentUtils.getParent(this, InCallScreenDelegateFactory.class).newInCallScreenDelegate(); |
| Assert.isNotNull(inCallScreenDelegate); |
| |
| buttonControllers.add(new ButtonController.MuteButtonController(inCallButtonUiDelegate)); |
| buttonControllers.add(new ButtonController.SpeakerButtonController(inCallButtonUiDelegate)); |
| buttonControllers.add(new ButtonController.DialpadButtonController(inCallButtonUiDelegate)); |
| buttonControllers.add(new ButtonController.HoldButtonController(inCallButtonUiDelegate)); |
| buttonControllers.add(new ButtonController.AddCallButtonController(inCallButtonUiDelegate)); |
| buttonControllers.add(new ButtonController.SwapButtonController(inCallButtonUiDelegate)); |
| buttonControllers.add(new ButtonController.MergeButtonController(inCallButtonUiDelegate)); |
| buttonControllers.add(new ButtonController.SwapSimButtonController(inCallButtonUiDelegate)); |
| buttonControllers.add( |
| new ButtonController.UpgradeToVideoButtonController(inCallButtonUiDelegate)); |
| buttonControllers.add( |
| new ButtonController.ManageConferenceButtonController(inCallScreenDelegate)); |
| buttonControllers.add( |
| new ButtonController.SwitchToSecondaryButtonController(inCallScreenDelegate)); |
| |
| inCallScreenDelegate.onInCallScreenDelegateInit(this); |
| inCallScreenDelegate.onInCallScreenReady(); |
| } |
| |
| @Override |
| public void onPause() { |
| super.onPause(); |
| inCallScreenDelegate.onInCallScreenPaused(); |
| } |
| |
| @Override |
| public void onDestroyView() { |
| super.onDestroyView(); |
| inCallScreenDelegate.onInCallScreenUnready(); |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| inCallButtonUiDelegate.onSaveInstanceState(outState); |
| } |
| |
| @Override |
| public void onClick(View view) { |
| if (view == endCallButton) { |
| LogUtil.i("InCallFragment.onClick", "end call button clicked"); |
| Logger.get(getContext()) |
| .logImpression(DialerImpression.Type.IN_CALL_DIALPAD_HANG_UP_BUTTON_PRESSED); |
| inCallScreenDelegate.onEndCallClicked(); |
| } else { |
| LogUtil.e("InCallFragment.onClick", "unknown view: " + view); |
| Assert.fail(); |
| } |
| } |
| |
| @Override |
| public void setPrimary(@NonNull PrimaryInfo primaryInfo) { |
| LogUtil.i("InCallFragment.setPrimary", primaryInfo.toString()); |
| setAdapterMedia(primaryInfo.multimediaData, primaryInfo.showInCallButtonGrid); |
| contactGridManager.setPrimary(primaryInfo); |
| |
| if (primaryInfo.shouldShowLocation) { |
| // Hide the avatar to make room for location |
| contactGridManager.setAvatarHidden(true); |
| |
| // Need to widen the contact grid to fit location information |
| View contactGridView = getView().findViewById(R.id.incall_contact_grid); |
| ViewGroup.LayoutParams params = contactGridView.getLayoutParams(); |
| if (params instanceof ViewGroup.MarginLayoutParams) { |
| ((ViewGroup.MarginLayoutParams) params).setMarginStart(0); |
| ((ViewGroup.MarginLayoutParams) params).setMarginEnd(0); |
| } |
| contactGridView.setLayoutParams(params); |
| |
| // Need to let the dialpad move up a little further when location info is being shown |
| View dialpadView = getView().findViewById(R.id.incall_dialpad_container); |
| params = dialpadView.getLayoutParams(); |
| if (params instanceof RelativeLayout.LayoutParams) { |
| ((RelativeLayout.LayoutParams) params).removeRule(RelativeLayout.BELOW); |
| } |
| dialpadView.setLayoutParams(params); |
| } |
| } |
| |
| private void setAdapterMedia(MultimediaData multimediaData, boolean showInCallButtonGrid) { |
| if (adapter == null) { |
| adapter = |
| new InCallPagerAdapter(getChildFragmentManager(), multimediaData, showInCallButtonGrid); |
| pager.setAdapter(adapter); |
| } else { |
| adapter.setAttachments(multimediaData); |
| } |
| |
| if (adapter.getCount() > 1 && getResources().getInteger(R.integer.incall_num_rows) > 1) { |
| paginator.setVisibility(View.VISIBLE); |
| paginator.setupWithViewPager(pager); |
| pager.setSwipingLocked(false); |
| if (!stateRestored) { |
| handler.postDelayed(pagerRunnable, 4_000); |
| } else { |
| pager.setCurrentItem(adapter.getButtonGridPosition(), false /* animateScroll */); |
| } |
| } else { |
| paginator.setVisibility(View.GONE); |
| } |
| } |
| |
| @Override |
| public void setSecondary(@NonNull SecondaryInfo secondaryInfo) { |
| LogUtil.i("InCallFragment.setSecondary", secondaryInfo.toString()); |
| updateButtonStates(); |
| |
| if (!isAdded()) { |
| savedSecondaryInfo = secondaryInfo; |
| return; |
| } |
| savedSecondaryInfo = null; |
| FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); |
| Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.incall_on_hold_banner); |
| if (secondaryInfo.shouldShow) { |
| transaction.replace(R.id.incall_on_hold_banner, OnHoldFragment.newInstance(secondaryInfo)); |
| } else { |
| if (oldBanner != null) { |
| transaction.remove(oldBanner); |
| } |
| } |
| transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top); |
| transaction.commitAllowingStateLoss(); |
| } |
| |
| @Override |
| public void setCallState(@NonNull PrimaryCallState primaryCallState) { |
| LogUtil.i("InCallFragment.setCallState", primaryCallState.toString()); |
| contactGridManager.setCallState(primaryCallState); |
| getButtonController(InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) |
| .setAllowed(primaryCallState.swapToSecondaryButtonState != ButtonState.NOT_SUPPORT); |
| getButtonController(InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) |
| .setEnabled(primaryCallState.swapToSecondaryButtonState == ButtonState.ENABLED); |
| buttonChooser = |
| ButtonChooserFactory.newButtonChooser(voiceNetworkType, primaryCallState.isWifi, phoneType); |
| updateButtonStates(); |
| } |
| |
| @Override |
| public void setEndCallButtonEnabled(boolean enabled, boolean animate) { |
| if (endCallButton != null) { |
| endCallButton.setEnabled(enabled); |
| } |
| } |
| |
| @Override |
| public void showManageConferenceCallButton(boolean visible) { |
| getButtonController(InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE).setAllowed(visible); |
| getButtonController(InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE).setEnabled(visible); |
| updateButtonStates(); |
| } |
| |
| @Override |
| public boolean isManageConferenceVisible() { |
| return getButtonController(InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE).isAllowed(); |
| } |
| |
| @Override |
| public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { |
| contactGridManager.dispatchPopulateAccessibilityEvent(event); |
| } |
| |
| @Override |
| public void showNoteSentToast() { |
| LogUtil.i("InCallFragment.showNoteSentToast", null); |
| Toast.makeText(getContext(), R.string.incall_note_sent, Toast.LENGTH_LONG).show(); |
| } |
| |
| @Override |
| public void updateInCallScreenColors() {} |
| |
| @Override |
| public void onInCallScreenDialpadVisibilityChange(boolean isShowing) { |
| LogUtil.i("InCallFragment.onInCallScreenDialpadVisibilityChange", "isShowing: " + isShowing); |
| // Take note that the dialpad button isShowing |
| getButtonController(InCallButtonIds.BUTTON_DIALPAD).setChecked(isShowing); |
| |
| // This check is needed because there is a race condition where we attempt to update |
| // ButtonGridFragment before it is ready, so we check whether it is ready first and once it is |
| // ready, #onButtonGridCreated will mark the dialpad button as isShowing. |
| if (inCallButtonGridFragment != null) { |
| // Update the Android Button's state to isShowing. |
| inCallButtonGridFragment.onInCallScreenDialpadVisibilityChange(isShowing); |
| } |
| } |
| |
| @Override |
| public int getAnswerAndDialpadContainerResourceId() { |
| return R.id.incall_dialpad_container; |
| } |
| |
| @Override |
| public Fragment getInCallScreenFragment() { |
| return this; |
| } |
| |
| @Override |
| public void showButton(@InCallButtonIds int buttonId, boolean show) { |
| LogUtil.v( |
| "InCallFragment.showButton", |
| "buttionId: %s, show: %b", |
| InCallButtonIdsExtension.toString(buttonId), |
| show); |
| if (isSupportedButton(buttonId)) { |
| getButtonController(buttonId).setAllowed(show); |
| if (buttonId == InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO && show) { |
| Logger.get(getContext()) |
| .logImpression(DialerImpression.Type.UPGRADE_TO_VIDEO_CALL_BUTTON_SHOWN); |
| } |
| } |
| } |
| |
| @Override |
| public void enableButton(@InCallButtonIds int buttonId, boolean enable) { |
| LogUtil.v( |
| "InCallFragment.enableButton", |
| "buttonId: %s, enable: %b", |
| InCallButtonIdsExtension.toString(buttonId), |
| enable); |
| if (isSupportedButton(buttonId)) { |
| getButtonController(buttonId).setEnabled(enable); |
| } |
| } |
| |
| @Override |
| public void setEnabled(boolean enabled) { |
| LogUtil.v("InCallFragment.setEnabled", "enabled: " + enabled); |
| for (ButtonController buttonController : buttonControllers) { |
| buttonController.setEnabled(enabled); |
| } |
| } |
| |
| @Override |
| public void setHold(boolean value) { |
| getButtonController(InCallButtonIds.BUTTON_HOLD).setChecked(value); |
| } |
| |
| @Override |
| public void setCameraSwitched(boolean isBackFacingCamera) {} |
| |
| @Override |
| public void setVideoPaused(boolean isPaused) {} |
| |
| @Override |
| public void setAudioState(CallAudioState audioState) { |
| LogUtil.i("InCallFragment.setAudioState", "audioState: " + audioState); |
| ((SpeakerButtonController) getButtonController(InCallButtonIds.BUTTON_AUDIO)) |
| .setAudioState(audioState); |
| getButtonController(InCallButtonIds.BUTTON_MUTE).setChecked(audioState.isMuted()); |
| } |
| |
| @Override |
| public void updateButtonStates() { |
| // When the incall screen is ready, this method is called from #setSecondary, even though the |
| // incall button ui is not ready yet. This method is called again once the incall button ui is |
| // ready though, so this operation is safe and will be executed asap. |
| if (inCallButtonGridFragment == null) { |
| return; |
| } |
| int numVisibleButtons = |
| inCallButtonGridFragment.updateButtonStates( |
| buttonControllers, buttonChooser, voiceNetworkType, phoneType); |
| |
| int visibility = numVisibleButtons == 0 ? View.GONE : View.VISIBLE; |
| pager.setVisibility(visibility); |
| if (adapter != null |
| && adapter.getCount() > 1 |
| && getResources().getInteger(R.integer.incall_num_rows) > 1) { |
| paginator.setVisibility(View.VISIBLE); |
| pager.setSwipingLocked(false); |
| } else { |
| paginator.setVisibility(View.GONE); |
| if (adapter != null) { |
| pager.setSwipingLocked(true); |
| pager.setCurrentItem(adapter.getButtonGridPosition()); |
| } |
| } |
| } |
| |
| @Override |
| public void updateInCallButtonUiColors(@ColorInt int color) { |
| inCallButtonGridFragment.updateButtonColor(color); |
| } |
| |
| @Override |
| public Fragment getInCallButtonUiFragment() { |
| return this; |
| } |
| |
| @Override |
| public void showAudioRouteSelector() { |
| AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState()) |
| .show(getChildFragmentManager(), null); |
| } |
| |
| @Override |
| public void onAudioRouteSelected(int audioRoute) { |
| inCallButtonUiDelegate.setAudioRoute(audioRoute); |
| } |
| |
| @Override |
| public void onAudioRouteSelectorDismiss() {} |
| |
| @NonNull |
| @Override |
| public ButtonController getButtonController(@InCallButtonIds int id) { |
| for (ButtonController buttonController : buttonControllers) { |
| if (buttonController.getInCallButtonId() == id) { |
| return buttonController; |
| } |
| } |
| Assert.fail(); |
| return null; |
| } |
| |
| @Override |
| public void onButtonGridCreated(InCallButtonGridFragment inCallButtonGridFragment) { |
| LogUtil.i("InCallFragment.onButtonGridCreated", "InCallUiReady"); |
| this.inCallButtonGridFragment = inCallButtonGridFragment; |
| inCallButtonUiDelegate.onInCallButtonUiReady(this); |
| updateButtonStates(); |
| } |
| |
| @Override |
| public void onButtonGridDestroyed() { |
| LogUtil.i("InCallFragment.onButtonGridCreated", "InCallUiUnready"); |
| inCallButtonUiDelegate.onInCallButtonUiUnready(); |
| this.inCallButtonGridFragment = null; |
| } |
| |
| @Override |
| public boolean isShowingLocationUi() { |
| Fragment fragment = getLocationFragment(); |
| return fragment != null && fragment.isVisible(); |
| } |
| |
| @Override |
| public void showLocationUi(@Nullable Fragment locationUi) { |
| boolean isVisible = isShowingLocationUi(); |
| if (locationUi != null && !isVisible) { |
| // Show the location fragment. |
| getChildFragmentManager() |
| .beginTransaction() |
| .replace(R.id.incall_location_holder, locationUi) |
| .commitAllowingStateLoss(); |
| } else if (locationUi == null && isVisible) { |
| // Hide the location fragment |
| getChildFragmentManager() |
| .beginTransaction() |
| .remove(getLocationFragment()) |
| .commitAllowingStateLoss(); |
| } |
| } |
| |
| @Override |
| public void onMultiWindowModeChanged(boolean isInMultiWindowMode) { |
| super.onMultiWindowModeChanged(isInMultiWindowMode); |
| if (isInMultiWindowMode == isShowingLocationUi()) { |
| LogUtil.i("InCallFragment.onMultiWindowModeChanged", "hide = " + isInMultiWindowMode); |
| // Need to show or hide location |
| showLocationUi(isInMultiWindowMode ? null : getLocationFragment()); |
| } |
| } |
| |
| private Fragment getLocationFragment() { |
| return getChildFragmentManager().findFragmentById(R.id.incall_location_holder); |
| } |
| } |