AudioSwitchPreferenceController.java revision 8276d966e9c39c70d6a84c724ef36bf26f298025
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
17package com.android.settings.sound;
18
19import static android.media.AudioManager.STREAM_DEVICES_CHANGED_ACTION;
20import static android.media.AudioManager.STREAM_MUSIC;
21import static android.media.AudioManager.STREAM_VOICE_CALL;
22import static android.media.AudioSystem.DEVICE_OUT_ALL_A2DP;
23import static android.media.AudioSystem.DEVICE_OUT_ALL_SCO;
24import static android.media.AudioSystem.DEVICE_OUT_HEARING_AID;
25import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
26
27import android.bluetooth.BluetoothDevice;
28import android.content.BroadcastReceiver;
29import android.content.Context;
30import android.content.Intent;
31import android.content.IntentFilter;
32import android.media.AudioDeviceCallback;
33import android.media.AudioDeviceInfo;
34import android.media.AudioManager;
35import android.media.MediaRouter;
36import android.media.MediaRouter.Callback;
37import android.os.Handler;
38import android.os.Looper;
39import android.support.v7.preference.ListPreference;
40import android.support.v7.preference.Preference;
41import android.support.v7.preference.PreferenceScreen;
42import android.text.TextUtils;
43import android.util.FeatureFlagUtils;
44
45import com.android.settings.R;
46import com.android.settings.bluetooth.Utils;
47import com.android.settings.core.BasePreferenceController;
48import com.android.settings.core.FeatureFlags;
49import com.android.settingslib.bluetooth.A2dpProfile;
50import com.android.settingslib.bluetooth.BluetoothCallback;
51import com.android.settingslib.bluetooth.CachedBluetoothDevice;
52import com.android.settingslib.bluetooth.HeadsetProfile;
53import com.android.settingslib.bluetooth.HearingAidProfile;
54import com.android.settingslib.bluetooth.LocalBluetoothManager;
55import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
56import com.android.settingslib.core.lifecycle.LifecycleObserver;
57import com.android.settingslib.core.lifecycle.events.OnStart;
58import com.android.settingslib.core.lifecycle.events.OnStop;
59
60import java.util.ArrayList;
61import java.util.List;
62
63/**
64 * Abstract class for audio switcher controller to notify subclass
65 * updating the current status of switcher entry. Subclasses must overwrite
66 * {@link #setActiveBluetoothDevice(BluetoothDevice)} to set the
67 * active device for corresponding profile.
68 */
69public abstract class AudioSwitchPreferenceController extends BasePreferenceController
70        implements Preference.OnPreferenceChangeListener, BluetoothCallback,
71        LifecycleObserver, OnStart, OnStop {
72
73    private static final int INVALID_INDEX = -1;
74
75    protected final List<BluetoothDevice> mConnectedDevices;
76    protected final AudioManager mAudioManager;
77    protected final MediaRouter mMediaRouter;
78    protected final LocalBluetoothProfileManager mProfileManager;
79    protected int mSelectedIndex;
80    protected Preference mPreference;
81
82    private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback;
83    private final LocalBluetoothManager mLocalBluetoothManager;
84    private final MediaRouterCallback mMediaRouterCallback;
85    private final WiredHeadsetBroadcastReceiver mReceiver;
86    private final Handler mHandler;
87
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        mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
93        mLocalBluetoothManager.setForegroundActivity(context);
94        mProfileManager = mLocalBluetoothManager.getProfileManager();
95        mHandler = new Handler(Looper.getMainLooper());
96        mAudioManagerAudioDeviceCallback = new AudioManagerAudioDeviceCallback();
97        mReceiver = new WiredHeadsetBroadcastReceiver();
98        mMediaRouterCallback = new MediaRouterCallback();
99        mConnectedDevices = new ArrayList<>();
100    }
101
102    /**
103     * Make this method as final, ensure that subclass will checking
104     * the feature flag and they could mistakenly break it via overriding.
105     */
106    @Override
107    public final int getAvailabilityStatus() {
108        return FeatureFlagUtils.isEnabled(mContext, FeatureFlags.AUDIO_SWITCHER_SETTINGS)
109                ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
110    }
111
112    @Override
113    public boolean onPreferenceChange(Preference preference, Object newValue) {
114        final String address = (String) newValue;
115        if (!(preference instanceof ListPreference)) {
116            return false;
117        }
118
119        final ListPreference listPreference = (ListPreference) preference;
120        if (TextUtils.equals(address, mContext.getText(R.string.media_output_default_summary))) {
121            // Switch to default device which address is device name
122            mSelectedIndex = getDefaultDeviceIndex();
123            setActiveBluetoothDevice(null);
124            listPreference.setSummary(mContext.getText(R.string.media_output_default_summary));
125        } else {
126            // Switch to BT device which address is hardware address
127            final int connectedDeviceIndex = getConnectedDeviceIndex(address);
128            if (connectedDeviceIndex == INVALID_INDEX) {
129                return false;
130            }
131            final BluetoothDevice btDevice = mConnectedDevices.get(connectedDeviceIndex);
132            mSelectedIndex = connectedDeviceIndex;
133            setActiveBluetoothDevice(btDevice);
134            listPreference.setSummary(btDevice.getName());
135        }
136        return true;
137    }
138
139    public abstract void setActiveBluetoothDevice(BluetoothDevice device);
140
141    @Override
142    public void displayPreference(PreferenceScreen screen) {
143        super.displayPreference(screen);
144        mPreference = screen.findPreference(mPreferenceKey);
145        mPreference.setVisible(false);
146    }
147
148    @Override
149    public void onStart() {
150        register();
151    }
152
153    @Override
154    public void onStop() {
155        unregister();
156    }
157
158    /**
159     * Only concerned about whether the local adapter is connected to any profile of any device and
160     * are not really concerned about which profile.
161     */
162    @Override
163    public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {
164        updateState(mPreference);
165    }
166
167    @Override
168    public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {
169        updateState(mPreference);
170    }
171
172    @Override
173    public void onAudioModeChanged() {
174        updateState(mPreference);
175    }
176
177    @Override
178    public void onBluetoothStateChanged(int bluetoothState) {
179    }
180
181    /**
182     * The local Bluetooth adapter has started the remote device discovery process.
183     */
184    @Override
185    public void onScanningStateChanged(boolean started) {
186    }
187
188    /**
189     * Indicates a change in the bond state of a remote
190     * device. For example, if a device is bonded (paired).
191     */
192    @Override
193    public void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
194        updateState(mPreference);
195    }
196
197    @Override
198    public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
199    }
200
201    @Override
202    public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
203    }
204
205    protected boolean isStreamFromOutputDevice(int streamType, int device) {
206        return (device & mAudioManager.getDevicesForStream(streamType)) != 0;
207    }
208
209    /**
210     * get hands free profile(HFP) connected device
211     */
212    protected List<BluetoothDevice> getConnectedHfpDevices() {
213        final List<BluetoothDevice> connectedDevices = new ArrayList<>();
214        final HeadsetProfile hfpProfile = mProfileManager.getHeadsetProfile();
215        if (hfpProfile == null) {
216            return connectedDevices;
217        }
218        final List<BluetoothDevice> devices = hfpProfile.getConnectedDevices();
219        for (BluetoothDevice device : devices) {
220            if (device.isConnected()) {
221                connectedDevices.add(device);
222            }
223        }
224        return connectedDevices;
225    }
226
227    /**
228     * get A2dp connected device
229     */
230    protected List<BluetoothDevice> getConnectedA2dpDevices() {
231        final List<BluetoothDevice> connectedDevices = new ArrayList<>();
232        final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
233        if (a2dpProfile == null) {
234            return connectedDevices;
235        }
236        final List<BluetoothDevice> devices = a2dpProfile.getConnectedDevices();
237        for (BluetoothDevice device : devices) {
238            if (device.isConnected()) {
239                connectedDevices.add(device);
240            }
241        }
242        return connectedDevices;
243    }
244
245    /**
246     * get hearing aid profile connected device, exclude other devices with same hiSyncId.
247     */
248    protected List<BluetoothDevice> getConnectedHearingAidDevices() {
249        final List<BluetoothDevice> connectedDevices = new ArrayList<>();
250        final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile();
251        if (hapProfile == null) {
252            return connectedDevices;
253        }
254        final List<Long> devicesHiSyncIds = new ArrayList<>();
255        final List<BluetoothDevice> devices = hapProfile.getConnectedDevices();
256        for (BluetoothDevice device : devices) {
257            final long hiSyncId = hapProfile.getHiSyncId(device);
258            // device with same hiSyncId should not be shown in the UI.
259            // So do not add it into connectedDevices.
260            if (!devicesHiSyncIds.contains(hiSyncId) && device.isConnected()) {
261                devicesHiSyncIds.add(hiSyncId);
262                connectedDevices.add(device);
263            }
264        }
265        return connectedDevices;
266    }
267
268    /**
269     * According to different stream and output device, find the active device from
270     * the corresponding profile. Hearing aid device could stream both STREAM_MUSIC
271     * and STREAM_VOICE_CALL.
272     *
273     * @param streamType the type of audio streams.
274     * @return the active device. Return null if the active device is current device
275     * or streamType is not STREAM_MUSIC or STREAM_VOICE_CALL.
276     */
277    protected BluetoothDevice findActiveDevice(int streamType) {
278        if (streamType != STREAM_MUSIC && streamType != STREAM_VOICE_CALL) {
279            return null;
280        }
281        if (isStreamFromOutputDevice(STREAM_MUSIC, DEVICE_OUT_ALL_A2DP)) {
282            return mProfileManager.getA2dpProfile().getActiveDevice();
283        } else if (isStreamFromOutputDevice(STREAM_VOICE_CALL, DEVICE_OUT_ALL_SCO)) {
284            return mProfileManager.getHeadsetProfile().getActiveDevice();
285        } else if (isStreamFromOutputDevice(streamType, DEVICE_OUT_HEARING_AID)) {
286            // The first element is the left active device; the second element is
287            // the right active device. And they will have same hiSyncId. If either
288            // or both side is not active, it will be null on that position.
289            List<BluetoothDevice> activeDevices =
290                    mProfileManager.getHearingAidProfile().getActiveDevices();
291            for (BluetoothDevice btDevice : activeDevices) {
292                if (btDevice != null && mConnectedDevices.contains(btDevice)) {
293                    // also need to check mConnectedDevices, because one of
294                    // the device(same hiSyncId) might not be shown in the UI.
295                    return btDevice;
296                }
297            }
298        }
299        return null;
300    }
301
302    int getDefaultDeviceIndex() {
303        // Default device is after all connected devices.
304        return mConnectedDevices.size();
305    }
306
307    void setupPreferenceEntries(CharSequence[] mediaOutputs, CharSequence[] mediaValues,
308            BluetoothDevice activeDevice) {
309        // default to current device
310        mSelectedIndex = getDefaultDeviceIndex();
311        // default device is after all connected devices.
312        mediaOutputs[mSelectedIndex] = mContext.getText(R.string.media_output_default_summary);
313        // use default device name as address
314        mediaValues[mSelectedIndex] = mContext.getText(R.string.media_output_default_summary);
315        for (int i = 0, size = mConnectedDevices.size(); i < size; i++) {
316            final BluetoothDevice btDevice = mConnectedDevices.get(i);
317            mediaOutputs[i] = btDevice.getName();
318            mediaValues[i] = btDevice.getAddress();
319            if (btDevice.equals(activeDevice)) {
320                // select the active connected device.
321                mSelectedIndex = i;
322            }
323        }
324    }
325
326    void setPreference(CharSequence[] mediaOutputs, CharSequence[] mediaValues,
327            Preference preference) {
328        final ListPreference listPreference = (ListPreference) preference;
329        listPreference.setEntries(mediaOutputs);
330        listPreference.setEntryValues(mediaValues);
331        listPreference.setValueIndex(mSelectedIndex);
332        listPreference.setSummary(mediaOutputs[mSelectedIndex]);
333    }
334
335    private int getConnectedDeviceIndex(String hardwareAddress) {
336        if (mConnectedDevices != null) {
337            for (int i = 0, size = mConnectedDevices.size(); i < size; i++) {
338                final BluetoothDevice btDevice = mConnectedDevices.get(i);
339                if (TextUtils.equals(btDevice.getAddress(), hardwareAddress)) {
340                    return i;
341                }
342            }
343        }
344        return INVALID_INDEX;
345    }
346
347    private void register() {
348        mLocalBluetoothManager.getEventManager().registerCallback(this);
349        mAudioManager.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler);
350        mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback);
351
352        // Register for misc other intent broadcasts.
353        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
354        intentFilter.addAction(STREAM_DEVICES_CHANGED_ACTION);
355        mContext.registerReceiver(mReceiver, intentFilter);
356    }
357
358    private void unregister() {
359        mLocalBluetoothManager.getEventManager().unregisterCallback(this);
360        mAudioManager.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback);
361        mMediaRouter.removeCallback(mMediaRouterCallback);
362        mContext.unregisterReceiver(mReceiver);
363    }
364
365    /** Notifications of audio device connection and disconnection events. */
366    private class AudioManagerAudioDeviceCallback extends AudioDeviceCallback {
367        @Override
368        public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
369            updateState(mPreference);
370        }
371
372        @Override
373        public void onAudioDevicesRemoved(AudioDeviceInfo[] devices) {
374            updateState(mPreference);
375        }
376    }
377
378    /** Receiver for wired headset plugged and unplugged events. */
379    private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver {
380        @Override
381        public void onReceive(Context context, Intent intent) {
382            final String action = intent.getAction();
383            if (AudioManager.ACTION_HEADSET_PLUG.equals(action) ||
384                    AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) {
385                updateState(mPreference);
386            }
387        }
388    }
389
390    /** Callback for cast device events. */
391    private class MediaRouterCallback extends Callback {
392        @Override
393        public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) {
394        }
395
396        @Override
397        public void onRouteUnselected(MediaRouter router, int type, MediaRouter.RouteInfo info) {
398        }
399
400        @Override
401        public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
402            if (info != null && !info.isDefault()) {
403                // cast mode
404                updateState(mPreference);
405            }
406        }
407
408        @Override
409        public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
410        }
411
412        @Override
413        public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
414            if (info != null && !info.isDefault()) {
415                // cast mode
416                updateState(mPreference);
417            }
418        }
419
420        @Override
421        public void onRouteGrouped(MediaRouter router, MediaRouter.RouteInfo info,
422                MediaRouter.RouteGroup group, int index) {
423        }
424
425        @Override
426        public void onRouteUngrouped(MediaRouter router, MediaRouter.RouteInfo info,
427                MediaRouter.RouteGroup group) {
428        }
429
430        @Override
431        public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo info) {
432        }
433    }
434}
435