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.audioroute;
18 
19 import android.annotation.SuppressLint;
20 import android.app.Dialog;
21 import android.bluetooth.BluetoothDevice;
22 import android.content.Context;
23 import android.content.DialogInterface;
24 import android.content.res.ColorStateList;
25 import android.graphics.PorterDuff.Mode;
26 import android.os.Bundle;
27 import android.provider.Settings;
28 import android.support.annotation.Nullable;
29 import android.support.design.widget.BottomSheetDialogFragment;
30 import android.support.v4.os.BuildCompat;
31 import android.telecom.CallAudioState;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.view.WindowManager;
36 import android.widget.LinearLayout;
37 import android.widget.TextView;
38 import com.android.dialer.common.FragmentUtils;
39 import com.android.dialer.common.LogUtil;
40 import com.android.dialer.logging.DialerImpression;
41 import com.android.dialer.logging.Logger;
42 import com.android.dialer.theme.base.ThemeComponent;
43 import com.android.incallui.call.CallList;
44 import com.android.incallui.call.DialerCall;
45 import com.android.incallui.call.TelecomAdapter;
46 import java.lang.reflect.InvocationTargetException;
47 import java.lang.reflect.Method;
48 import java.util.Collection;
49 
50 /** Shows picker for audio routes */
51 public class AudioRouteSelectorDialogFragment extends BottomSheetDialogFragment {
52 
53   public static final String TAG = "AudioRouteSelectorDialogFragment";
54   private static final String ARG_AUDIO_STATE = "audio_state";
55 
56   /** Called when an audio route is picked */
57   public interface AudioRouteSelectorPresenter {
onAudioRouteSelected(int audioRoute)58     void onAudioRouteSelected(int audioRoute);
59 
onAudioRouteSelectorDismiss()60     void onAudioRouteSelectorDismiss();
61   }
62 
newInstance(CallAudioState audioState)63   public static AudioRouteSelectorDialogFragment newInstance(CallAudioState audioState) {
64     AudioRouteSelectorDialogFragment fragment = new AudioRouteSelectorDialogFragment();
65     Bundle args = new Bundle();
66     args.putParcelable(ARG_AUDIO_STATE, audioState);
67     fragment.setArguments(args);
68     return fragment;
69   }
70 
71   @Override
onAttach(Context context)72   public void onAttach(Context context) {
73     super.onAttach(context);
74     FragmentUtils.checkParent(this, AudioRouteSelectorPresenter.class);
75   }
76 
77   @Override
onCreateDialog(final Bundle savedInstanceState)78   public Dialog onCreateDialog(final Bundle savedInstanceState) {
79     LogUtil.i("AudioRouteSelectorDialogFragment.onCreateDialog", null);
80     Dialog dialog = super.onCreateDialog(savedInstanceState);
81     dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
82     if (Settings.canDrawOverlays(getContext())) {
83       dialog
84           .getWindow()
85           .setType(
86               BuildCompat.isAtLeastO()
87                   ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
88                   : WindowManager.LayoutParams.TYPE_PHONE);
89     }
90     return dialog;
91   }
92 
93   @Nullable
94   @Override
95   @SuppressLint("NewApi")
onCreateView( LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle)96   public View onCreateView(
97       LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
98     View view = layoutInflater.inflate(R.layout.audioroute_selector, viewGroup, false);
99     CallAudioState audioState = getArguments().getParcelable(ARG_AUDIO_STATE);
100 
101     if (BuildCompat.isAtLeastP()) {
102       // Create items for all connected Bluetooth devices
103       Collection<BluetoothDevice> bluetoothDeviceSet = audioState.getSupportedBluetoothDevices();
104       for (BluetoothDevice device : bluetoothDeviceSet) {
105         boolean selected =
106             (audioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH)
107                 && (bluetoothDeviceSet.size() == 1
108                     || device.equals(audioState.getActiveBluetoothDevice()));
109         TextView textView = createBluetoothItem(device, selected);
110         ((LinearLayout) view).addView(textView, 0);
111       }
112     } else {
113       // Only create Bluetooth audio route
114       TextView textView =
115           (TextView) getLayoutInflater().inflate(R.layout.audioroute_item, null, false);
116       textView.setText(getString(R.string.audioroute_bluetooth));
117       initItem(
118           textView,
119           CallAudioState.ROUTE_BLUETOOTH,
120           audioState,
121           DialerImpression.Type.IN_CALL_SWITCH_AUDIO_ROUTE_BLUETOOTH);
122       ((LinearLayout) view).addView(textView, 0);
123     }
124 
125     initItem(
126         (TextView) view.findViewById(R.id.audioroute_speaker),
127         CallAudioState.ROUTE_SPEAKER,
128         audioState,
129         DialerImpression.Type.IN_CALL_SWITCH_AUDIO_ROUTE_SPEAKER);
130     initItem(
131         (TextView) view.findViewById(R.id.audioroute_headset),
132         CallAudioState.ROUTE_WIRED_HEADSET,
133         audioState,
134         DialerImpression.Type.IN_CALL_SWITCH_AUDIO_ROUTE_WIRED_HEADSET);
135     initItem(
136         (TextView) view.findViewById(R.id.audioroute_earpiece),
137         CallAudioState.ROUTE_EARPIECE,
138         audioState,
139         DialerImpression.Type.IN_CALL_SWITCH_AUDIO_ROUTE_EARPIECE);
140 
141     // TODO(a bug): set peak height correctly to fully expand it in landscape mode.
142     return view;
143   }
144 
145   @Override
onCancel(DialogInterface dialogInterface)146   public void onCancel(DialogInterface dialogInterface) {
147     super.onCancel(dialogInterface);
148     FragmentUtils.getParentUnsafe(
149             AudioRouteSelectorDialogFragment.this, AudioRouteSelectorPresenter.class)
150         .onAudioRouteSelectorDismiss();
151   }
152 
initItem( TextView item, final int itemRoute, CallAudioState audioState, DialerImpression.Type impressionType)153   private void initItem(
154       TextView item,
155       final int itemRoute,
156       CallAudioState audioState,
157       DialerImpression.Type impressionType) {
158     int selectedColor = ThemeComponent.get(getContext()).theme().getColorPrimary();
159     if ((audioState.getSupportedRouteMask() & itemRoute) == 0) {
160       item.setVisibility(View.GONE);
161     } else if (audioState.getRoute() == itemRoute) {
162       item.setSelected(true);
163       item.setTextColor(selectedColor);
164       item.setCompoundDrawableTintList(ColorStateList.valueOf(selectedColor));
165       item.setCompoundDrawableTintMode(Mode.SRC_ATOP);
166     }
167     item.setOnClickListener(
168         (v) -> {
169           logCallAudioRouteImpression(impressionType);
170           FragmentUtils.getParentUnsafe(
171                   AudioRouteSelectorDialogFragment.this, AudioRouteSelectorPresenter.class)
172               .onAudioRouteSelected(itemRoute);
173           dismiss();
174         });
175   }
176 
createBluetoothItem(BluetoothDevice bluetoothDevice, boolean selected)177   private TextView createBluetoothItem(BluetoothDevice bluetoothDevice, boolean selected) {
178     int selectedColor = ThemeComponent.get(getContext()).theme().getColorPrimary();
179     TextView textView =
180         (TextView) getLayoutInflater().inflate(R.layout.audioroute_item, null, false);
181     textView.setText(getAliasName(bluetoothDevice));
182     if (selected) {
183       textView.setSelected(true);
184       textView.setTextColor(selectedColor);
185       textView.setCompoundDrawableTintList(ColorStateList.valueOf(selectedColor));
186       textView.setCompoundDrawableTintMode(Mode.SRC_ATOP);
187     }
188     textView.setOnClickListener(
189         (v) -> {
190           logCallAudioRouteImpression(DialerImpression.Type.IN_CALL_SWITCH_AUDIO_ROUTE_BLUETOOTH);
191           // Set Bluetooth audio route
192           FragmentUtils.getParentUnsafe(
193                   AudioRouteSelectorDialogFragment.this, AudioRouteSelectorPresenter.class)
194               .onAudioRouteSelected(CallAudioState.ROUTE_BLUETOOTH);
195           // Set active Bluetooth device
196           TelecomAdapter.getInstance().requestBluetoothAudio(bluetoothDevice);
197           dismiss();
198         });
199 
200     return textView;
201   }
202 
203   @SuppressLint("PrivateApi")
getAliasName(BluetoothDevice bluetoothDevice)204   private String getAliasName(BluetoothDevice bluetoothDevice) {
205     try {
206       Method getActiveDeviceMethod = bluetoothDevice.getClass().getDeclaredMethod("getAliasName");
207       getActiveDeviceMethod.setAccessible(true);
208       return (String) getActiveDeviceMethod.invoke(bluetoothDevice);
209     } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
210       e.printStackTrace();
211       return bluetoothDevice.getName();
212     }
213   }
214 
logCallAudioRouteImpression(DialerImpression.Type impressionType)215   private void logCallAudioRouteImpression(DialerImpression.Type impressionType) {
216     DialerCall dialerCall = CallList.getInstance().getOutgoingCall();
217     if (dialerCall == null) {
218       dialerCall = CallList.getInstance().getActiveOrBackgroundCall();
219     }
220 
221     if (dialerCall != null) {
222       Logger.get(getContext())
223           .logCallImpression(
224               impressionType, dialerCall.getUniqueCallId(), dialerCall.getTimeAddedMs());
225     } else {
226       Logger.get(getContext()).logImpression(impressionType);
227     }
228   }
229 }
230