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