• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.incallui.incall.impl;
18 
19 import android.Manifest.permission;
20 import android.content.Context;
21 import android.content.pm.PackageManager;
22 import android.os.Build.VERSION;
23 import android.os.Build.VERSION_CODES;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.support.annotation.NonNull;
27 import android.support.annotation.Nullable;
28 import android.support.v4.app.Fragment;
29 import android.support.v4.app.FragmentTransaction;
30 import android.support.v4.content.ContextCompat;
31 import android.telecom.CallAudioState;
32 import android.telephony.TelephonyManager;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.View.OnClickListener;
36 import android.view.ViewGroup;
37 import android.view.accessibility.AccessibilityEvent;
38 import android.widget.ImageView;
39 import android.widget.RelativeLayout;
40 import android.widget.Toast;
41 import com.android.dialer.common.Assert;
42 import com.android.dialer.common.FragmentUtils;
43 import com.android.dialer.common.LogUtil;
44 import com.android.dialer.multimedia.MultimediaData;
45 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment;
46 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter;
47 import com.android.incallui.contactgrid.ContactGridManager;
48 import com.android.incallui.hold.OnHoldFragment;
49 import com.android.incallui.incall.impl.ButtonController.SpeakerButtonController;
50 import com.android.incallui.incall.impl.InCallButtonGridFragment.OnButtonGridCreatedListener;
51 import com.android.incallui.incall.protocol.InCallButtonIds;
52 import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
53 import com.android.incallui.incall.protocol.InCallButtonUi;
54 import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
55 import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
56 import com.android.incallui.incall.protocol.InCallScreen;
57 import com.android.incallui.incall.protocol.InCallScreenDelegate;
58 import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
59 import com.android.incallui.incall.protocol.PrimaryCallState;
60 import com.android.incallui.incall.protocol.PrimaryInfo;
61 import com.android.incallui.incall.protocol.SecondaryInfo;
62 import java.util.ArrayList;
63 import java.util.List;
64 
65 /** Fragment that shows UI for an ongoing voice call. */
66 public 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 
isSupportedButton(@nCallButtonIds int id)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
onAttach(Context context)112   public void onAttach(Context context) {
113     super.onAttach(context);
114     if (savedSecondaryInfo != null) {
115       setSecondary(savedSecondaryInfo);
116     }
117   }
118 
119   @Override
onCreate(Bundle savedInstanceState)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
onCreateView( @onNull LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle)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
onResume()172   public void onResume() {
173     super.onResume();
174     inCallButtonUiDelegate.refreshMuteState();
175     inCallScreenDelegate.onInCallScreenResumed();
176   }
177 
178   @Override
onViewCreated(@onNull View view, @Nullable Bundle bundle)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
onPause()205   public void onPause() {
206     super.onPause();
207     inCallScreenDelegate.onInCallScreenPaused();
208   }
209 
210   @Override
onDestroyView()211   public void onDestroyView() {
212     super.onDestroyView();
213     inCallScreenDelegate.onInCallScreenUnready();
214   }
215 
216   @Override
onSaveInstanceState(Bundle outState)217   public void onSaveInstanceState(Bundle outState) {
218     super.onSaveInstanceState(outState);
219     inCallButtonUiDelegate.onSaveInstanceState(outState);
220   }
221 
222   @Override
onClick(View view)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
setPrimary(@onNull PrimaryInfo primaryInfo)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 
setAdapterMedia(MultimediaData multimediaData)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
setSecondary(@onNull SecondaryInfo secondaryInfo)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
setCallState(@onNull PrimaryCallState primaryCallState)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
setEndCallButtonEnabled(boolean enabled, boolean animate)321   public void setEndCallButtonEnabled(boolean enabled, boolean animate) {
322     if (endCallButton != null) {
323       endCallButton.setEnabled(enabled);
324     }
325   }
326 
327   @Override
showManageConferenceCallButton(boolean visible)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
isManageConferenceVisible()335   public boolean isManageConferenceVisible() {
336     return getButtonController(InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE).isAllowed();
337   }
338 
339   @Override
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)340   public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
341     contactGridManager.dispatchPopulateAccessibilityEvent(event);
342   }
343 
344   @Override
showNoteSentToast()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
updateInCallScreenColors()351   public void updateInCallScreenColors() {}
352 
353   @Override
onInCallScreenDialpadVisibilityChange(boolean isShowing)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
getAnswerAndDialpadContainerResourceId()369   public int getAnswerAndDialpadContainerResourceId() {
370     return R.id.incall_dialpad_container;
371   }
372 
373   @Override
getInCallScreenFragment()374   public Fragment getInCallScreenFragment() {
375     return this;
376   }
377 
378   @Override
showButton(@nCallButtonIds int buttonId, boolean show)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
enableButton(@nCallButtonIds int buttonId, boolean enable)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
setEnabled(boolean enabled)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
setHold(boolean value)411   public void setHold(boolean value) {
412     getButtonController(InCallButtonIds.BUTTON_HOLD).setChecked(value);
413   }
414 
415   @Override
setCameraSwitched(boolean isBackFacingCamera)416   public void setCameraSwitched(boolean isBackFacingCamera) {}
417 
418   @Override
setVideoPaused(boolean isPaused)419   public void setVideoPaused(boolean isPaused) {}
420 
421   @Override
setAudioState(CallAudioState audioState)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
updateButtonStates()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
updateInCallButtonUiColors()458   public void updateInCallButtonUiColors() {}
459 
460   @Override
getInCallButtonUiFragment()461   public Fragment getInCallButtonUiFragment() {
462     return this;
463   }
464 
465   @Override
showAudioRouteSelector()466   public void showAudioRouteSelector() {
467     AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState())
468         .show(getChildFragmentManager(), null);
469   }
470 
471   @Override
onAudioRouteSelected(int audioRoute)472   public void onAudioRouteSelected(int audioRoute) {
473     inCallButtonUiDelegate.setAudioRoute(audioRoute);
474   }
475 
476   @NonNull
477   @Override
getButtonController(@nCallButtonIds int id)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
onButtonGridCreated(InCallButtonGridFragment inCallButtonGridFragment)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
onButtonGridDestroyed()497   public void onButtonGridDestroyed() {
498     LogUtil.i("InCallFragment.onButtonGridCreated", "InCallUiUnready");
499     inCallButtonUiDelegate.onInCallButtonUiUnready();
500     this.inCallButtonGridFragment = null;
501   }
502 
503   @Override
isShowingLocationUi()504   public boolean isShowingLocationUi() {
505     Fragment fragment = getLocationFragment();
506     return fragment != null && fragment.isVisible();
507   }
508 
509   @Override
showLocationUi(@ullable Fragment locationUi)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
onMultiWindowModeChanged(boolean isInMultiWindowMode)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 
getLocationFragment()537   private Fragment getLocationFragment() {
538     return getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
539   }
540 }
541