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