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