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