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