diff options
8 files changed, 333 insertions, 13 deletions
diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java index 2d3ef19f8..11e952cbc 100644 --- a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java +++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java @@ -39,6 +39,7 @@ import com.android.dialer.spam.SpamComponent; import com.android.dialer.speeddial.loader.UiItemLoaderComponent; import com.android.dialer.storage.StorageComponent; import com.android.dialer.strictmode.StrictModeComponent; +import com.android.incallui.audiomode.BluetoothDeviceProviderComponent; import com.android.incallui.calllocation.CallLocationComponent; import com.android.incallui.maps.MapsComponent; import com.android.incallui.speakeasy.SpeakEasyComponent; @@ -49,7 +50,8 @@ import com.android.voicemail.VoicemailComponent; * from this component. */ public interface BaseDialerRootComponent - extends BubbleComponent.HasComponent, + extends BluetoothDeviceProviderComponent.HasComponent, + BubbleComponent.HasComponent, CallLocationComponent.HasComponent, CallLogComponent.HasComponent, CallLogConfigComponent.HasComponent, diff --git a/java/com/android/incallui/AndroidManifest.xml b/java/com/android/incallui/AndroidManifest.xml index 9a762feea..832a5e874 100644 --- a/java/com/android/incallui/AndroidManifest.xml +++ b/java/com/android/incallui/AndroidManifest.xml @@ -40,6 +40,9 @@ <!-- Testing location --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> + <!-- Set Bluetooth device --> + <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> + <!-- Set android:taskAffinity="com.android.incallui" for all activities to ensure proper navigation. Otherwise system could bring up DialtactsActivity instead, e.g. when user unmerge a call. diff --git a/java/com/android/incallui/InCallServiceImpl.java b/java/com/android/incallui/InCallServiceImpl.java index 6b463cef6..29a65b925 100644 --- a/java/com/android/incallui/InCallServiceImpl.java +++ b/java/com/android/incallui/InCallServiceImpl.java @@ -26,6 +26,7 @@ import android.telecom.InCallService; import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; import com.android.dialer.feedback.FeedbackComponent; import com.android.incallui.audiomode.AudioModeProvider; +import com.android.incallui.audiomode.BluetoothDeviceProviderComponent; import com.android.incallui.call.CallList; import com.android.incallui.call.ExternalCallList; import com.android.incallui.call.TelecomAdapter; @@ -97,6 +98,7 @@ public class InCallServiceImpl extends InCallService { final Context context = getApplicationContext(); final ContactInfoCache contactInfoCache = ContactInfoCache.getInstance(context); AudioModeProvider.getInstance().initializeAudioState(this); + BluetoothDeviceProviderComponent.get(context).bluetoothDeviceProvider().setUp(); InCallPresenter.getInstance() .setUp( context, @@ -141,6 +143,7 @@ public class InCallServiceImpl extends InCallService { // Tear down the InCall system InCallPresenter.getInstance().tearDown(); TelecomAdapter.getInstance().clearInCallService(); + BluetoothDeviceProviderComponent.get(this).bluetoothDeviceProvider().tearDown(); if (returnToCallController != null) { returnToCallController.tearDown(); returnToCallController = null; diff --git a/java/com/android/incallui/audiomode/BluetoothDeviceProvider.java b/java/com/android/incallui/audiomode/BluetoothDeviceProvider.java new file mode 100644 index 000000000..1aa1c20a8 --- /dev/null +++ b/java/com/android/incallui/audiomode/BluetoothDeviceProvider.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.incallui.audiomode; + +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothProfile; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.ArraySet; +import com.android.dialer.common.LogUtil; +import com.android.dialer.inject.ApplicationContext; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Set; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** Proxy class for getting and setting connected/active Bluetooth devices. */ +@Singleton +public final class BluetoothDeviceProvider extends BroadcastReceiver { + + // TODO(yueg): use BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED when possible + private static final String ACTION_ACTIVE_DEVICE_CHANGED = + "android.bluetooth.headset.profile.action.ACTIVE_DEVICE_CHANGED"; + + private final Context appContext; + private final BluetoothProfileServiceListener bluetoothProfileServiceListener = + new BluetoothProfileServiceListener(); + + private final Set<BluetoothDevice> connectedBluetoothDeviceSet = new ArraySet<>(); + + private BluetoothDevice activeBluetoothDevice; + private BluetoothHeadset bluetoothHeadset; + private boolean isSetUp; + + @Inject + public BluetoothDeviceProvider(@ApplicationContext Context appContext) { + this.appContext = appContext; + } + + public void setUp() { + if (BluetoothAdapter.getDefaultAdapter() == null) { + // Bluetooth is not supported on this hardware platform + return; + } + // Get Bluetooth service including the initial connected device list (should only contain one + // device) + BluetoothAdapter.getDefaultAdapter() + .getProfileProxy(appContext, bluetoothProfileServiceListener, BluetoothProfile.HEADSET); + // Get notified of Bluetooth device update + IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(ACTION_ACTIVE_DEVICE_CHANGED); + appContext.registerReceiver(this, filter); + + isSetUp = true; + } + + public void tearDown() { + if (!isSetUp) { + return; + } + appContext.unregisterReceiver(this); + if (bluetoothHeadset != null) { + BluetoothAdapter.getDefaultAdapter() + .closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset); + } + } + + public Set<BluetoothDevice> getConnectedBluetoothDeviceSet() { + return connectedBluetoothDeviceSet; + } + + public BluetoothDevice getActiveBluetoothDevice() { + return activeBluetoothDevice; + } + + @SuppressLint("PrivateApi") + public void setActiveBluetoothDevice(BluetoothDevice bluetoothDevice) { + if (!connectedBluetoothDeviceSet.contains(bluetoothDevice)) { + LogUtil.e("BluetoothProfileServiceListener.setActiveBluetoothDevice", "device is not in set"); + return; + } + // TODO(yueg): use BluetoothHeadset.setActiveDevice() when possible + try { + Method getActiveDeviceMethod = + bluetoothHeadset.getClass().getDeclaredMethod("setActiveDevice", BluetoothDevice.class); + getActiveDeviceMethod.setAccessible(true); + getActiveDeviceMethod.invoke(bluetoothHeadset, bluetoothDevice); + } catch (Exception e) { + LogUtil.e( + "BluetoothProfileServiceListener.setActiveBluetoothDevice", + "failed to call setActiveDevice", + e); + } + } + + @Override + public void onReceive(Context context, Intent intent) { + if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { + handleActionConnectionStateChanged(intent); + } else if (ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) { + handleActionActiveDeviceChanged(intent); + } + } + + private void handleActionConnectionStateChanged(Intent intent) { + if (!intent.hasExtra(BluetoothDevice.EXTRA_DEVICE)) { + LogUtil.i( + "BluetoothDeviceProvider.handleActionConnectionStateChanged", + "extra BluetoothDevice.EXTRA_DEVICE not found"); + return; + } + BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (bluetoothDevice == null) { + return; + } + + int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); + if (state == BluetoothProfile.STATE_DISCONNECTED) { + connectedBluetoothDeviceSet.remove(bluetoothDevice); + LogUtil.i("BluetoothDeviceProvider.handleActionConnectionStateChanged", "device removed"); + } else if (state == BluetoothProfile.STATE_CONNECTED) { + connectedBluetoothDeviceSet.add(bluetoothDevice); + LogUtil.i("BluetoothDeviceProvider.handleActionConnectionStateChanged", "device added"); + } + } + + private void handleActionActiveDeviceChanged(Intent intent) { + if (!intent.hasExtra(BluetoothDevice.EXTRA_DEVICE)) { + LogUtil.i( + "BluetoothDeviceProvider.handleActionActiveDeviceChanged", + "extra BluetoothDevice.EXTRA_DEVICE not found"); + return; + } + activeBluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + LogUtil.i( + "BluetoothDeviceProvider.handleActionActiveDeviceChanged", + (activeBluetoothDevice == null ? "null" : "")); + } + + private final class BluetoothProfileServiceListener implements BluetoothProfile.ServiceListener { + @Override + @SuppressLint("PrivateApi") + public void onServiceConnected(int profile, BluetoothProfile bluetoothProfile) { + if (profile != BluetoothProfile.HEADSET) { + return; + } + // Get initial connected device list + bluetoothHeadset = (BluetoothHeadset) bluetoothProfile; + List<BluetoothDevice> devices = bluetoothProfile.getConnectedDevices(); + for (BluetoothDevice device : devices) { + connectedBluetoothDeviceSet.add(device); + LogUtil.i( + "BluetoothProfileServiceListener.onServiceConnected", "get initial connected device"); + } + + // Get initial active device + // TODO(yueg): use BluetoothHeadset.getActiveDevice() when possible + try { + Method getActiveDeviceMethod = + bluetoothHeadset.getClass().getDeclaredMethod("getActiveDevice"); + getActiveDeviceMethod.setAccessible(true); + activeBluetoothDevice = (BluetoothDevice) getActiveDeviceMethod.invoke(bluetoothHeadset); + LogUtil.i( + "BluetoothProfileServiceListener.onServiceConnected", + "get initial active device" + ((activeBluetoothDevice == null) ? " null" : "")); + } catch (Exception e) { + LogUtil.e( + "BluetoothProfileServiceListener.onServiceConnected", + "failed to call getAcitveDevice", + e); + } + } + + @Override + public void onServiceDisconnected(int profile) { + LogUtil.enterBlock("BluetoothProfileServiceListener.onServiceDisconnected"); + if (profile == BluetoothProfile.HEADSET) { + bluetoothHeadset = null; + } + } + } +} diff --git a/java/com/android/incallui/audiomode/BluetoothDeviceProviderComponent.java b/java/com/android/incallui/audiomode/BluetoothDeviceProviderComponent.java new file mode 100644 index 000000000..9cd926835 --- /dev/null +++ b/java/com/android/incallui/audiomode/BluetoothDeviceProviderComponent.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.incallui.audiomode; + +import android.content.Context; +import com.android.dialer.inject.HasRootComponent; +import dagger.Subcomponent; + +/** Dagger component for the Bluetooth device provider. */ +@Subcomponent +public abstract class BluetoothDeviceProviderComponent { + + public abstract BluetoothDeviceProvider bluetoothDeviceProvider(); + + public static BluetoothDeviceProviderComponent get(Context context) { + return ((BluetoothDeviceProviderComponent.HasComponent) + ((HasRootComponent) context.getApplicationContext()).component()) + .bluetoothDeviceProviderComponent(); + } + + /** Used to refer to the root application component. */ + public interface HasComponent { + BluetoothDeviceProviderComponent bluetoothDeviceProviderComponent(); + } +} diff --git a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java index d481f4339..79cae098d 100644 --- a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java +++ b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java @@ -17,6 +17,7 @@ package com.android.incallui.audioroute; import android.app.Dialog; +import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.DialogInterface; import android.content.res.ColorStateList; @@ -29,9 +30,12 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.widget.LinearLayout; import android.widget.TextView; import com.android.dialer.common.FragmentUtils; import com.android.dialer.common.LogUtil; +import com.android.incallui.audiomode.BluetoothDeviceProviderComponent; +import java.util.Set; /** Shows picker for audio routes */ public class AudioRouteSelectorDialogFragment extends BottomSheetDialogFragment { @@ -75,10 +79,22 @@ public class AudioRouteSelectorDialogFragment extends BottomSheetDialogFragment View view = layoutInflater.inflate(R.layout.audioroute_selector, viewGroup, false); CallAudioState audioState = getArguments().getParcelable(ARG_AUDIO_STATE); - initItem( - (TextView) view.findViewById(R.id.audioroute_bluetooth), - CallAudioState.ROUTE_BLUETOOTH, - audioState); + Set<BluetoothDevice> bluetoothDeviceSet = + BluetoothDeviceProviderComponent.get(getContext()) + .bluetoothDeviceProvider() + .getConnectedBluetoothDeviceSet(); + for (BluetoothDevice device : bluetoothDeviceSet) { + boolean selected = + (audioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) + && (bluetoothDeviceSet.size() == 1 + || device.equals( + BluetoothDeviceProviderComponent.get(getContext()) + .bluetoothDeviceProvider() + .getActiveBluetoothDevice())); + TextView textView = createBluetoothItem(device, selected); + ((LinearLayout) view).addView(textView, 0); + } + initItem( (TextView) view.findViewById(R.id.audioroute_speaker), CallAudioState.ROUTE_SPEAKER, @@ -121,4 +137,30 @@ public class AudioRouteSelectorDialogFragment extends BottomSheetDialogFragment .onAudioRouteSelected(itemRoute); }); } + + private TextView createBluetoothItem(BluetoothDevice bluetoothDevice, boolean selected) { + int selectedColor = getResources().getColor(R.color.dialer_theme_color); + TextView textView = + (TextView) getLayoutInflater().inflate(R.layout.audioroute_item, null, false); + textView.setText(bluetoothDevice.getName()); + if (selected) { + textView.setTextColor(selectedColor); + textView.setCompoundDrawableTintList(ColorStateList.valueOf(selectedColor)); + textView.setCompoundDrawableTintMode(Mode.SRC_ATOP); + } + textView.setOnClickListener( + (v) -> { + dismiss(); + // Set Bluetooth audio route + FragmentUtils.getParentUnsafe( + AudioRouteSelectorDialogFragment.this, AudioRouteSelectorPresenter.class) + .onAudioRouteSelected(CallAudioState.ROUTE_BLUETOOTH); + // Set active Bluetooth device + BluetoothDeviceProviderComponent.get(getContext()) + .bluetoothDeviceProvider() + .setActiveBluetoothDevice(bluetoothDevice); + }); + + return textView; + } } diff --git a/java/com/android/incallui/audioroute/res/layout/audioroute_item.xml b/java/com/android/incallui/audioroute/res/layout/audioroute_item.xml new file mode 100644 index 000000000..66c83f67e --- /dev/null +++ b/java/com/android/incallui/audioroute/res/layout/audioroute_item.xml @@ -0,0 +1,21 @@ +<!-- +~ Copyright (C) 2018 The Android Open Source Project +~ +~ Licensed under the Apache License, Version 2.0 (the "License"); +~ you may not use this file except in compliance with the License. +~ You may obtain a copy of the License at +~ +~ http://www.apache.org/licenses/LICENSE-2.0 +~ +~ Unless required by applicable law or agreed to in writing, software +~ distributed under the License is distributed on an "AS IS" BASIS, +~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +~ See the License for the specific language governing permissions and +~ limitations under the License +--> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + style="@style/AudioRouteItem" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:drawableStart="@drawable/quantum_ic_bluetooth_audio_vd_theme_24" + android:drawableTint="@color/material_grey_600"/>
\ No newline at end of file diff --git a/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml b/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml index ef2220e8f..145101dd1 100644 --- a/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml +++ b/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml @@ -1,4 +1,18 @@ -<?xml version="1.0" encoding="utf-8"?> +<!-- +~ Copyright (C) 2018 The Android Open Source Project +~ +~ Licensed under the Apache License, Version 2.0 (the "License"); +~ you may not use this file except in compliance with the License. +~ You may obtain a copy of the License at +~ +~ http://www.apache.org/licenses/LICENSE-2.0 +~ +~ Unless required by applicable law or agreed to in writing, software +~ distributed under the License is distributed on an "AS IS" BASIS, +~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +~ See the License for the specific language governing permissions and +~ limitations under the License +--> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" @@ -6,13 +20,6 @@ android:orientation="vertical" tools:layout_gravity="bottom"> <TextView - android:id="@+id/audioroute_bluetooth" - style="@style/AudioRouteItem" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:drawableStart="@drawable/quantum_ic_bluetooth_audio_grey600_24" - android:text="@string/audioroute_bluetooth"/> - <TextView android:id="@+id/audioroute_speaker" style="@style/AudioRouteItem" android:layout_width="match_parent" |