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