1 /*
2  * Copyright (C) 2019 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.car.developeroptions.sound;
18 
19 import static android.media.AudioManager.STREAM_DEVICES_CHANGED_ACTION;
20 import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
21 
22 import android.bluetooth.BluetoothDevice;
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.content.pm.PackageManager;
28 import android.media.AudioDeviceCallback;
29 import android.media.AudioDeviceInfo;
30 import android.media.AudioManager;
31 import android.media.MediaRouter;
32 import android.media.MediaRouter.Callback;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.util.FeatureFlagUtils;
36 import android.util.Log;
37 
38 import androidx.preference.ListPreference;
39 import androidx.preference.Preference;
40 import androidx.preference.PreferenceScreen;
41 
42 import com.android.car.developeroptions.bluetooth.Utils;
43 import com.android.car.developeroptions.core.BasePreferenceController;
44 import com.android.car.developeroptions.core.FeatureFlags;
45 import com.android.settingslib.bluetooth.A2dpProfile;
46 import com.android.settingslib.bluetooth.BluetoothCallback;
47 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
48 import com.android.settingslib.bluetooth.HeadsetProfile;
49 import com.android.settingslib.bluetooth.HearingAidProfile;
50 import com.android.settingslib.bluetooth.LocalBluetoothManager;
51 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
52 import com.android.settingslib.core.lifecycle.LifecycleObserver;
53 import com.android.settingslib.core.lifecycle.events.OnStart;
54 import com.android.settingslib.core.lifecycle.events.OnStop;
55 
56 import java.util.ArrayList;
57 import java.util.List;
58 import java.util.concurrent.ExecutionException;
59 import java.util.concurrent.FutureTask;
60 
61 /**
62  * Abstract class for audio switcher controller to notify subclass
63  * updating the current status of switcher entry. Subclasses must overwrite
64  */
65 public abstract class AudioSwitchPreferenceController extends BasePreferenceController
66         implements BluetoothCallback, LifecycleObserver, OnStart, OnStop {
67 
68     private static final String TAG = "AudioSwitchPrefCtrl";
69 
70     protected final List<BluetoothDevice> mConnectedDevices;
71     protected final AudioManager mAudioManager;
72     protected final MediaRouter mMediaRouter;
73     protected int mSelectedIndex;
74     protected Preference mPreference;
75     protected LocalBluetoothProfileManager mProfileManager;
76     protected AudioSwitchCallback mAudioSwitchPreferenceCallback;
77 
78     private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback;
79     private final MediaRouterCallback mMediaRouterCallback;
80     private final WiredHeadsetBroadcastReceiver mReceiver;
81     private final Handler mHandler;
82     private LocalBluetoothManager mLocalBluetoothManager;
83 
84     public interface AudioSwitchCallback {
onPreferenceDataChanged(ListPreference preference)85         void onPreferenceDataChanged(ListPreference preference);
86     }
87 
AudioSwitchPreferenceController(Context context, String preferenceKey)88     public AudioSwitchPreferenceController(Context context, String preferenceKey) {
89         super(context, preferenceKey);
90         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
91         mMediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
92         mHandler = new Handler(Looper.getMainLooper());
93         mAudioManagerAudioDeviceCallback = new AudioManagerAudioDeviceCallback();
94         mReceiver = new WiredHeadsetBroadcastReceiver();
95         mMediaRouterCallback = new MediaRouterCallback();
96         mConnectedDevices = new ArrayList<>();
97         final FutureTask<LocalBluetoothManager> localBtManagerFutureTask = new FutureTask<>(
98                 // Avoid StrictMode ThreadPolicy violation
99                 () -> Utils.getLocalBtManager(mContext));
100         try {
101             localBtManagerFutureTask.run();
102             mLocalBluetoothManager = localBtManagerFutureTask.get();
103         } catch (InterruptedException | ExecutionException e) {
104             Log.w(TAG, "Error getting LocalBluetoothManager.", e);
105             return;
106         }
107         if (mLocalBluetoothManager == null) {
108             Log.e(TAG, "Bluetooth is not supported on this device");
109             return;
110         }
111         mProfileManager = mLocalBluetoothManager.getProfileManager();
112     }
113 
114     /**
115      * Make this method as final, ensure that subclass will checking
116      * the feature flag and they could mistakenly break it via overriding.
117      */
118     @Override
getAvailabilityStatus()119     public final int getAvailabilityStatus() {
120         return FeatureFlagUtils.isEnabled(mContext, FeatureFlags.AUDIO_SWITCHER_SETTINGS) &&
121                 mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
122                 ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
123     }
124 
125     @Override
displayPreference(PreferenceScreen screen)126     public void displayPreference(PreferenceScreen screen) {
127         super.displayPreference(screen);
128         mPreference = screen.findPreference(mPreferenceKey);
129         mPreference.setVisible(false);
130     }
131 
132     @Override
onStart()133     public void onStart() {
134         if (mLocalBluetoothManager == null) {
135             Log.e(TAG, "Bluetooth is not supported on this device");
136             return;
137         }
138         mLocalBluetoothManager.setForegroundActivity(mContext);
139         register();
140     }
141 
142     @Override
onStop()143     public void onStop() {
144         if (mLocalBluetoothManager == null) {
145             Log.e(TAG, "Bluetooth is not supported on this device");
146             return;
147         }
148         mLocalBluetoothManager.setForegroundActivity(null);
149         unregister();
150     }
151 
152     @Override
onBluetoothStateChanged(int bluetoothState)153     public void onBluetoothStateChanged(int bluetoothState) {
154         // To handle the case that Bluetooth on and no connected devices
155         updateState(mPreference);
156     }
157 
158     @Override
onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile)159     public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {
160         updateState(mPreference);
161     }
162 
163     @Override
onAudioModeChanged()164     public void onAudioModeChanged() {
165         updateState(mPreference);
166     }
167 
168     @Override
onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, int bluetoothProfile)169     public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state,
170             int bluetoothProfile) {
171         updateState(mPreference);
172     }
173 
174     /**
175      * Indicates a change in the bond state of a remote
176      * device. For example, if a device is bonded (paired).
177      */
178     @Override
onDeviceAdded(CachedBluetoothDevice cachedDevice)179     public void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
180         updateState(mPreference);
181     }
182 
setCallback(AudioSwitchCallback callback)183     public void setCallback(AudioSwitchCallback callback) {
184         mAudioSwitchPreferenceCallback = callback;
185     }
186 
isStreamFromOutputDevice(int streamType, int device)187     protected boolean isStreamFromOutputDevice(int streamType, int device) {
188         return (device & mAudioManager.getDevicesForStream(streamType)) != 0;
189     }
190 
191     /**
192      * get hands free profile(HFP) connected device
193      */
getConnectedHfpDevices()194     protected List<BluetoothDevice> getConnectedHfpDevices() {
195         final List<BluetoothDevice> connectedDevices = new ArrayList<>();
196         final HeadsetProfile hfpProfile = mProfileManager.getHeadsetProfile();
197         if (hfpProfile == null) {
198             return connectedDevices;
199         }
200         final List<BluetoothDevice> devices = hfpProfile.getConnectedDevices();
201         for (BluetoothDevice device : devices) {
202             if (device.isConnected()) {
203                 connectedDevices.add(device);
204             }
205         }
206         return connectedDevices;
207     }
208 
209     /**
210      * get A2dp devices on all states
211      * (STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED,  STATE_DISCONNECTING)
212      */
getConnectableA2dpDevices()213     protected List<BluetoothDevice> getConnectableA2dpDevices() {
214         final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
215         if (a2dpProfile == null) {
216             return new ArrayList<>();
217         }
218         return a2dpProfile.getConnectableDevices();
219     }
220 
221     /**
222      * get hearing aid profile connected device, exclude other devices with same hiSyncId.
223      */
getConnectedHearingAidDevices()224     protected List<BluetoothDevice> getConnectedHearingAidDevices() {
225         final List<BluetoothDevice> connectedDevices = new ArrayList<>();
226         final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile();
227         if (hapProfile == null) {
228             return connectedDevices;
229         }
230         final List<Long> devicesHiSyncIds = new ArrayList<>();
231         final List<BluetoothDevice> devices = hapProfile.getConnectedDevices();
232         for (BluetoothDevice device : devices) {
233             final long hiSyncId = hapProfile.getHiSyncId(device);
234             // device with same hiSyncId should not be shown in the UI.
235             // So do not add it into connectedDevices.
236             if (!devicesHiSyncIds.contains(hiSyncId) && device.isConnected()) {
237                 devicesHiSyncIds.add(hiSyncId);
238                 connectedDevices.add(device);
239             }
240         }
241         return connectedDevices;
242     }
243 
244     /**
245      * get hearing aid profile devices on all states
246      * (STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED,  STATE_DISCONNECTING)
247      * exclude other devices with same hiSyncId.
248      */
getConnectableHearingAidDevices()249     protected List<BluetoothDevice> getConnectableHearingAidDevices() {
250         final List<BluetoothDevice> connectedDevices = new ArrayList<>();
251         final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile();
252         if (hapProfile == null) {
253             return connectedDevices;
254         }
255         final List<Long> devicesHiSyncIds = new ArrayList<>();
256         final List<BluetoothDevice> devices = hapProfile.getConnectableDevices();
257         for (BluetoothDevice device : devices) {
258             final long hiSyncId = hapProfile.getHiSyncId(device);
259             // device with same hiSyncId should not be shown in the UI.
260             // So do not add it into connectedDevices.
261             if (!devicesHiSyncIds.contains(hiSyncId)) {
262                 devicesHiSyncIds.add(hiSyncId);
263                 connectedDevices.add(device);
264             }
265         }
266         return connectedDevices;
267     }
268 
269     /**
270      * Find active hearing aid device
271      */
findActiveHearingAidDevice()272     protected BluetoothDevice findActiveHearingAidDevice() {
273         final HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
274 
275         if (hearingAidProfile != null) {
276             // The first element is the left active device; the second element is
277             // the right active device. And they will have same hiSyncId. If either
278             // or both side is not active, it will be null on that position.
279             List<BluetoothDevice> activeDevices = hearingAidProfile.getActiveDevices();
280             for (BluetoothDevice btDevice : activeDevices) {
281                 if (btDevice != null && mConnectedDevices.contains(btDevice)) {
282                     // also need to check mConnectedDevices, because one of
283                     // the device(same hiSyncId) might not be shown in the UI.
284                     return btDevice;
285                 }
286             }
287         }
288         return null;
289     }
290 
291     /**
292      * Find the active device from the corresponding profile.
293      *
294      * @return the active device. Return null if the
295      * corresponding profile don't have active device.
296      */
findActiveDevice()297     public abstract BluetoothDevice findActiveDevice();
298 
register()299     private void register() {
300         mLocalBluetoothManager.getEventManager().registerCallback(this);
301         mAudioManager.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler);
302         mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback);
303 
304         // Register for misc other intent broadcasts.
305         IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
306         intentFilter.addAction(STREAM_DEVICES_CHANGED_ACTION);
307         mContext.registerReceiver(mReceiver, intentFilter);
308     }
309 
unregister()310     private void unregister() {
311         mLocalBluetoothManager.getEventManager().unregisterCallback(this);
312         mAudioManager.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback);
313         mMediaRouter.removeCallback(mMediaRouterCallback);
314         mContext.unregisterReceiver(mReceiver);
315     }
316 
317     /** Notifications of audio device connection and disconnection events. */
318     private class AudioManagerAudioDeviceCallback extends AudioDeviceCallback {
319         @Override
onAudioDevicesAdded(AudioDeviceInfo[] addedDevices)320         public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
321             updateState(mPreference);
322         }
323 
324         @Override
onAudioDevicesRemoved(AudioDeviceInfo[] devices)325         public void onAudioDevicesRemoved(AudioDeviceInfo[] devices) {
326             updateState(mPreference);
327         }
328     }
329 
330     /** Receiver for wired headset plugged and unplugged events. */
331     private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver {
332         @Override
onReceive(Context context, Intent intent)333         public void onReceive(Context context, Intent intent) {
334             final String action = intent.getAction();
335             if (AudioManager.ACTION_HEADSET_PLUG.equals(action) ||
336                     AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) {
337                 updateState(mPreference);
338             }
339         }
340     }
341 
342     /** Callback for cast device events. */
343     private class MediaRouterCallback extends Callback {
344         @Override
onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info)345         public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) {
346         }
347 
348         @Override
onRouteUnselected(MediaRouter router, int type, MediaRouter.RouteInfo info)349         public void onRouteUnselected(MediaRouter router, int type, MediaRouter.RouteInfo info) {
350         }
351 
352         @Override
onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info)353         public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
354             if (info != null && !info.isDefault()) {
355                 // cast mode
356                 updateState(mPreference);
357             }
358         }
359 
360         @Override
onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info)361         public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
362         }
363 
364         @Override
onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info)365         public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
366             if (info != null && !info.isDefault()) {
367                 // cast mode
368                 updateState(mPreference);
369             }
370         }
371 
372         @Override
onRouteGrouped(MediaRouter router, MediaRouter.RouteInfo info, MediaRouter.RouteGroup group, int index)373         public void onRouteGrouped(MediaRouter router, MediaRouter.RouteInfo info,
374                 MediaRouter.RouteGroup group, int index) {
375         }
376 
377         @Override
onRouteUngrouped(MediaRouter router, MediaRouter.RouteInfo info, MediaRouter.RouteGroup group)378         public void onRouteUngrouped(MediaRouter router, MediaRouter.RouteInfo info,
379                 MediaRouter.RouteGroup group) {
380         }
381 
382         @Override
onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo info)383         public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo info) {
384         }
385     }
386 }
387