PhonePolicy.java revision 3cc3ca76069837340db03751e64b6df230e5d8cc
1/*
2 * Copyright (C) 2017 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.bluetooth.btservice;
18
19import android.bluetooth.BluetoothA2dp;
20import android.bluetooth.BluetoothAdapter;
21import android.bluetooth.BluetoothDevice;
22import android.bluetooth.BluetoothHeadset;
23import android.bluetooth.BluetoothProfile;
24import android.bluetooth.BluetoothUuid;
25import android.content.BroadcastReceiver;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.os.Handler;
30import android.os.Looper;
31import android.os.Message;
32import android.os.ParcelUuid;
33import android.os.Parcelable;
34import android.support.annotation.VisibleForTesting;
35import android.util.Log;
36
37import com.android.bluetooth.a2dp.A2dpService;
38import com.android.bluetooth.hearingaid.HearingAidService;
39import com.android.bluetooth.hfp.HeadsetService;
40import com.android.bluetooth.hid.HidHostService;
41import com.android.bluetooth.pan.PanService;
42import com.android.internal.R;
43
44import java.util.HashSet;
45import java.util.List;
46
47// Describes the phone policy
48//
49// The policy should be as decoupled from the stack as possible. In an ideal world we should not
50// need to have this policy talk with any non-public APIs and one way to enforce that would be to
51// keep this file outside the Bluetooth process. Unfortunately, keeping a separate process alive is
52// an expensive and a tedious task.
53//
54// Best practices:
55// a) PhonePolicy should be ALL private methods
56//    -- Use broadcasts which can be listened in on the BroadcastReceiver
57// b) NEVER call from the PhonePolicy into the Java stack, unless public APIs. It is OK to call into
58// the non public versions as long as public versions exist (so that a 3rd party policy can mimick)
59// us.
60//
61// Policy description:
62//
63// Policies are usually governed by outside events that may warrant an action. We talk about various
64// events and the resulting outcome from this policy:
65//
66// 1. Adapter turned ON: At this point we will try to auto-connect the (device, profile) pairs which
67// have PRIORITY_AUTO_CONNECT. The fact that we *only* auto-connect Headset and A2DP is something
68// that is hardcoded and specific to phone policy (see autoConnect() function)
69// 2. When the profile connection-state changes: At this point if a new profile gets CONNECTED we
70// will try to connect other profiles on the same device. This is to avoid collision if devices
71// somehow end up trying to connect at same time or general connection issues.
72class PhonePolicy {
73    private static final boolean DBG = true;
74    private static final String TAG = "BluetoothPhonePolicy";
75
76    // Message types for the handler (internal messages generated by intents or timeouts)
77    private static final int MESSAGE_PROFILE_CONNECTION_STATE_CHANGED = 1;
78    private static final int MESSAGE_PROFILE_INIT_PRIORITIES = 2;
79    private static final int MESSAGE_CONNECT_OTHER_PROFILES = 3;
80    private static final int MESSAGE_ADAPTER_STATE_TURNED_ON = 4;
81
82    // Timeouts
83    @VisibleForTesting
84    static int sConnectOtherProfilesTimeoutMillis = 6000; // 6s
85
86    private final AdapterService mAdapterService;
87    private final ServiceFactory mFactory;
88    private final Handler mHandler;
89    private final HashSet<BluetoothDevice> mHeadsetRetrySet = new HashSet<>();
90    private final HashSet<BluetoothDevice> mA2dpRetrySet = new HashSet<>();
91    private final HashSet<BluetoothDevice> mConnectOtherProfilesDeviceSet = new HashSet<>();
92
93    // Broadcast receiver for all changes to states of various profiles
94    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
95        @Override
96        public void onReceive(Context context, Intent intent) {
97            String action = intent.getAction();
98            if (action == null) {
99                errorLog("Received intent with null action");
100                return;
101            }
102            switch (action) {
103                case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED:
104                    mHandler.obtainMessage(MESSAGE_PROFILE_CONNECTION_STATE_CHANGED,
105                            BluetoothProfile.HEADSET, -1, // No-op argument
106                            intent).sendToTarget();
107                    break;
108                case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
109                    mHandler.obtainMessage(MESSAGE_PROFILE_CONNECTION_STATE_CHANGED,
110                            BluetoothProfile.A2DP, -1, // No-op argument
111                            intent).sendToTarget();
112                    break;
113                case BluetoothAdapter.ACTION_STATE_CHANGED:
114                    // Only pass the message on if the adapter has actually changed state from
115                    // non-ON to ON. NOTE: ON is the state depicting BREDR ON and not just BLE ON.
116                    int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
117                    if (newState == BluetoothAdapter.STATE_ON) {
118                        mHandler.obtainMessage(MESSAGE_ADAPTER_STATE_TURNED_ON).sendToTarget();
119                    }
120                    break;
121                case BluetoothDevice.ACTION_UUID:
122                    mHandler.obtainMessage(MESSAGE_PROFILE_INIT_PRIORITIES, intent).sendToTarget();
123                    break;
124                default:
125                    Log.e(TAG, "Received unexpected intent, action=" + action);
126                    break;
127            }
128        }
129    };
130
131    @VisibleForTesting
132    BroadcastReceiver getBroadcastReceiver() {
133        return mReceiver;
134    }
135
136    // Handler to handoff intents to class thread
137    class PhonePolicyHandler extends Handler {
138        PhonePolicyHandler(Looper looper) {
139            super(looper);
140        }
141
142        @Override
143        public void handleMessage(Message msg) {
144            switch (msg.what) {
145                case MESSAGE_PROFILE_INIT_PRIORITIES: {
146                    Intent intent = (Intent) msg.obj;
147                    BluetoothDevice device =
148                            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
149                    Parcelable[] uuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
150                    debugLog("Received ACTION_UUID for device " + device);
151                    if (uuids != null) {
152                        ParcelUuid[] uuidsToSend = new ParcelUuid[uuids.length];
153                        for (int i = 0; i < uuidsToSend.length; i++) {
154                            uuidsToSend[i] = (ParcelUuid) uuids[i];
155                            debugLog("index=" + i + "uuid=" + uuidsToSend[i]);
156                        }
157                        processInitProfilePriorities(device, uuidsToSend);
158                    }
159                }
160                break;
161
162                case MESSAGE_PROFILE_CONNECTION_STATE_CHANGED: {
163                    Intent intent = (Intent) msg.obj;
164                    BluetoothDevice device =
165                            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
166                    int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
167                    int nextState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
168                    processProfileStateChanged(device, msg.arg1, nextState, prevState);
169                }
170                break;
171
172                case MESSAGE_CONNECT_OTHER_PROFILES: {
173                    // Called when we try connect some profiles in processConnectOtherProfiles but
174                    // we send a delayed message to try connecting the remaining profiles
175                    BluetoothDevice device = (BluetoothDevice) msg.obj;
176                    processConnectOtherProfiles(device);
177                    mConnectOtherProfilesDeviceSet.remove(device);
178                    break;
179                }
180                case MESSAGE_ADAPTER_STATE_TURNED_ON:
181                    // Call auto connect when adapter switches state to ON
182                    resetStates();
183                    autoConnect();
184                    break;
185            }
186        }
187    }
188
189    ;
190
191    // Policy API functions for lifecycle management (protected)
192    protected void start() {
193        IntentFilter filter = new IntentFilter();
194        filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
195        filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
196        filter.addAction(BluetoothDevice.ACTION_UUID);
197        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
198        mAdapterService.registerReceiver(mReceiver, filter);
199    }
200
201    protected void cleanup() {
202        mAdapterService.unregisterReceiver(mReceiver);
203        resetStates();
204    }
205
206    PhonePolicy(AdapterService service, ServiceFactory factory) {
207        mAdapterService = service;
208        mFactory = factory;
209        mHandler = new PhonePolicyHandler(service.getMainLooper());
210    }
211
212    // Policy implementation, all functions MUST be private
213    private void processInitProfilePriorities(BluetoothDevice device, ParcelUuid[] uuids) {
214        debugLog("processInitProfilePriorities() - device " + device);
215        HidHostService hidService = mFactory.getHidHostService();
216        A2dpService a2dpService = mFactory.getA2dpService();
217        HeadsetService headsetService = mFactory.getHeadsetService();
218        PanService panService = mFactory.getPanService();
219        HearingAidService hearingAidService = mFactory.getHearingAidService();
220
221        // Set profile priorities only for the profiles discovered on the remote device.
222        // This avoids needless auto-connect attempts to profiles non-existent on the remote device
223        if ((hidService != null) && (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hid)
224                || BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp)) && (
225                hidService.getPriority(device) == BluetoothProfile.PRIORITY_UNDEFINED)) {
226            hidService.setPriority(device, BluetoothProfile.PRIORITY_ON);
227        }
228
229        // If we do not have a stored priority for HFP/A2DP (all roles) then default to on.
230        if ((headsetService != null) && ((BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HSP)
231                || BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Handsfree)) && (
232                headsetService.getPriority(device) == BluetoothProfile.PRIORITY_UNDEFINED))) {
233            headsetService.setPriority(device, BluetoothProfile.PRIORITY_ON);
234        }
235
236        if ((a2dpService != null) && (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.AudioSink)
237                || BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.AdvAudioDist)) && (
238                a2dpService.getPriority(device) == BluetoothProfile.PRIORITY_UNDEFINED)) {
239            a2dpService.setPriority(device, BluetoothProfile.PRIORITY_ON);
240        }
241
242        if ((panService != null) && (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.PANU) && (
243                panService.getPriority(device) == BluetoothProfile.PRIORITY_UNDEFINED)
244                && mAdapterService.getResources()
245                .getBoolean(R.bool.config_bluetooth_pan_enable_autoconnect))) {
246            panService.setPriority(device, BluetoothProfile.PRIORITY_ON);
247        }
248
249        if ((hearingAidService != null)
250                && BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HearingAid)
251                && (hearingAidService.getPriority(device) == BluetoothProfile.PRIORITY_UNDEFINED)) {
252            debugLog("setting hearing aid profile priority for device " + device);
253            hearingAidService.setPriority(device, BluetoothProfile.PRIORITY_ON);
254        }
255    }
256
257    private void processProfileStateChanged(BluetoothDevice device, int profileId, int nextState,
258            int prevState) {
259        debugLog("processProfileStateChanged, device=" + device + ", profile=" + profileId + ", "
260                + prevState + " -> " + nextState);
261        if (((profileId == BluetoothProfile.A2DP) || (profileId == BluetoothProfile.HEADSET))) {
262            if (nextState == BluetoothProfile.STATE_CONNECTED) {
263                switch (profileId) {
264                    case BluetoothProfile.A2DP:
265                        mA2dpRetrySet.remove(device);
266                        break;
267                    case BluetoothProfile.HEADSET:
268                        mHeadsetRetrySet.remove(device);
269                        break;
270                }
271                connectOtherProfile(device);
272                setProfileAutoConnectionPriority(device, profileId, true);
273            }
274            if (prevState == BluetoothProfile.STATE_CONNECTING
275                    && nextState == BluetoothProfile.STATE_DISCONNECTED) {
276                setProfileAutoConnectionPriority(device, profileId, false);
277            }
278
279        }
280    }
281
282    private void resetStates() {
283        mHeadsetRetrySet.clear();
284        mA2dpRetrySet.clear();
285    }
286
287    private void autoConnect() {
288        if (mAdapterService.getState() != BluetoothAdapter.STATE_ON) {
289            errorLog("autoConnect() - BT is not ON. Exiting autoConnect");
290            return;
291        }
292
293        if (!mAdapterService.isQuietModeEnabled()) {
294            debugLog("autoConnect() - Initiate auto connection on BT on...");
295            // Phone profiles.
296            autoConnectHeadset();
297            autoConnectA2dp();
298        } else {
299            debugLog("autoConnect() - BT is in quiet mode. Not initiating auto connections");
300        }
301    }
302
303    private void autoConnectHeadset() {
304        final HeadsetService hsService = mFactory.getHeadsetService();
305        if (hsService == null) {
306            errorLog("autoConnectHeadset, service is null");
307            return;
308        }
309        final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
310        if (bondedDevices == null) {
311            errorLog("autoConnectHeadset, bondedDevices are null");
312            return;
313        }
314        for (BluetoothDevice device : bondedDevices) {
315            int priority = hsService.getPriority(device);
316            debugLog("autoConnectHeadset, attempt auto-connect with device " + device
317                     + " priority " + priority);
318            if (priority == BluetoothProfile.PRIORITY_AUTO_CONNECT) {
319                debugLog("autoConnectHeadset, Connecting HFP with " + device);
320                hsService.connect(device);
321            } else {
322                debugLog("autoConnectHeadset, skipped auto-connect with device " + device
323                         + " priority " + priority);
324            }
325        }
326    }
327
328    private void autoConnectA2dp() {
329        final A2dpService a2dpService = mFactory.getA2dpService();
330        if (a2dpService == null) {
331            errorLog("autoConnectA2dp, service is null");
332            return;
333        }
334        final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
335        if (bondedDevices == null) {
336            errorLog("autoConnectA2dp, bondedDevices are null");
337            return;
338        }
339        for (BluetoothDevice device : bondedDevices) {
340            int priority = a2dpService.getPriority(device);
341            debugLog("autoConnectA2dp, attempt auto-connect with device " + device
342                     + " priority " + priority);
343            if (priority == BluetoothProfile.PRIORITY_AUTO_CONNECT) {
344                debugLog("autoConnectA2dp, connecting A2DP with " + device);
345                a2dpService.connect(device);
346            } else {
347                debugLog("autoConnectA2dp, skipped auto-connect with device " + device
348                         + " priority " + priority);
349            }
350        }
351    }
352
353    private void connectOtherProfile(BluetoothDevice device) {
354        if (mAdapterService.isQuietModeEnabled()) {
355            debugLog("connectOtherProfile: in quiet mode, skip connect other profile " + device);
356            return;
357        }
358        if (mConnectOtherProfilesDeviceSet.contains(device)) {
359            debugLog("connectOtherProfile: already scheduled callback for " + device);
360            return;
361        }
362        mConnectOtherProfilesDeviceSet.add(device);
363        Message m = mHandler.obtainMessage(MESSAGE_CONNECT_OTHER_PROFILES);
364        m.obj = device;
365        mHandler.sendMessageDelayed(m, sConnectOtherProfilesTimeoutMillis);
366    }
367
368    // This function is called whenever a profile is connected.  This allows any other bluetooth
369    // profiles which are not already connected or in the process of connecting to attempt to
370    // connect to the device that initiated the connection.  In the event that this function is
371    // invoked and there are no current bluetooth connections no new profiles will be connected.
372    private void processConnectOtherProfiles(BluetoothDevice device) {
373        debugLog("processConnectOtherProfiles, device=" + device);
374        if (mAdapterService.getState() != BluetoothAdapter.STATE_ON) {
375            warnLog("processConnectOtherProfiles, adapter is not ON " + mAdapterService.getState());
376            return;
377        }
378        HeadsetService hsService = mFactory.getHeadsetService();
379        A2dpService a2dpService = mFactory.getA2dpService();
380        PanService panService = mFactory.getPanService();
381
382        boolean atLeastOneProfileConnectedForDevice = false;
383        boolean allProfilesEmpty = true;
384        List<BluetoothDevice> a2dpConnDevList = null;
385        List<BluetoothDevice> hsConnDevList = null;
386        List<BluetoothDevice> panConnDevList = null;
387
388        if (hsService != null) {
389            hsConnDevList = hsService.getConnectedDevices();
390            allProfilesEmpty &= hsConnDevList.isEmpty();
391            atLeastOneProfileConnectedForDevice |= hsConnDevList.contains(device);
392        }
393        if (a2dpService != null) {
394            a2dpConnDevList = a2dpService.getConnectedDevices();
395            allProfilesEmpty &= a2dpConnDevList.isEmpty();
396            atLeastOneProfileConnectedForDevice |= a2dpConnDevList.contains(device);
397        }
398        if (panService != null) {
399            panConnDevList = panService.getConnectedDevices();
400            allProfilesEmpty &= panConnDevList.isEmpty();
401            atLeastOneProfileConnectedForDevice |= panConnDevList.contains(device);
402        }
403
404        if (!atLeastOneProfileConnectedForDevice) {
405            // Consider this device as fully disconnected, don't bother connecting others
406            debugLog("processConnectOtherProfiles, all profiles disconnected for " + device);
407            mHeadsetRetrySet.remove(device);
408            mA2dpRetrySet.remove(device);
409            if (allProfilesEmpty) {
410                debugLog("processConnectOtherProfiles, all profiles disconnected for all devices");
411                // reset retry status so that in the next round we can start retrying connections
412                resetStates();
413            }
414            return;
415        }
416
417        if (hsService != null) {
418            if (!mHeadsetRetrySet.contains(device) && (hsService.getPriority(device)
419                    >= BluetoothProfile.PRIORITY_ON) && (hsService.getConnectionState(device)
420                    == BluetoothProfile.STATE_DISCONNECTED)) {
421                debugLog("Retrying connection to Headset with device " + device);
422                mHeadsetRetrySet.add(device);
423                hsService.connect(device);
424            }
425        }
426        if (a2dpService != null) {
427            if (!mA2dpRetrySet.contains(device) && (a2dpService.getPriority(device)
428                    >= BluetoothProfile.PRIORITY_ON) && (a2dpService.getConnectionState(device)
429                    == BluetoothProfile.STATE_DISCONNECTED)) {
430                debugLog("Retrying connection to A2DP with device " + device);
431                mA2dpRetrySet.add(device);
432                a2dpService.connect(device);
433            }
434        }
435        if (panService != null) {
436            // TODO: the panConnDevList.isEmpty() check below should be removed once
437            // Multi-PAN is supported.
438            if (panConnDevList.isEmpty() && (panService.getPriority(device)
439                    >= BluetoothProfile.PRIORITY_ON) && (panService.getConnectionState(device)
440                    == BluetoothProfile.STATE_DISCONNECTED)) {
441                debugLog("Retrying connection to PAN with device " + device);
442                panService.connect(device);
443            }
444        }
445    }
446
447    private void setProfileAutoConnectionPriority(BluetoothDevice device, int profileId,
448            boolean autoConnect) {
449        debugLog("setProfileAutoConnectionPriority: device=" + device + ", profile=" + profileId
450                + ", autoConnect=" + autoConnect);
451        switch (profileId) {
452            case BluetoothProfile.HEADSET: {
453                HeadsetService hsService = mFactory.getHeadsetService();
454                if (hsService == null) {
455                    warnLog("setProfileAutoConnectionPriority: HEADSET service is null");
456                    break;
457                }
458                removeAutoConnectFromDisconnectedHeadsets(hsService);
459                if (autoConnect) {
460                    hsService.setPriority(device, BluetoothProfile.PRIORITY_AUTO_CONNECT);
461                }
462                break;
463            }
464            case BluetoothProfile.A2DP: {
465                A2dpService a2dpService = mFactory.getA2dpService();
466                if (a2dpService == null) {
467                    warnLog("setProfileAutoConnectionPriority: A2DP service is null");
468                    break;
469                }
470                removeAutoConnectFromDisconnectedA2dpSinks(a2dpService);
471                if (autoConnect) {
472                    a2dpService.setPriority(device, BluetoothProfile.PRIORITY_AUTO_CONNECT);
473                }
474                break;
475            }
476            default:
477                Log.w(TAG, "Tried to set AutoConnect priority on invalid profile " + profileId);
478                break;
479        }
480    }
481
482    private void removeAutoConnectFromDisconnectedHeadsets(HeadsetService hsService) {
483        List<BluetoothDevice> connectedDeviceList = hsService.getConnectedDevices();
484        for (BluetoothDevice device : mAdapterService.getBondedDevices()) {
485            if (hsService.getPriority(device) >= BluetoothProfile.PRIORITY_AUTO_CONNECT
486                    && !connectedDeviceList.contains(device)) {
487                debugLog("removeAutoConnectFromDisconnectedHeadsets, device " + device
488                        + " PRIORITY_ON");
489                hsService.setPriority(device, BluetoothProfile.PRIORITY_ON);
490            }
491        }
492    }
493
494    private void removeAutoConnectFromDisconnectedA2dpSinks(A2dpService a2dpService) {
495        List<BluetoothDevice> connectedDeviceList = a2dpService.getConnectedDevices();
496        for (BluetoothDevice device : mAdapterService.getBondedDevices()) {
497            if (a2dpService.getPriority(device) >= BluetoothProfile.PRIORITY_AUTO_CONNECT
498                    && !connectedDeviceList.contains(device)) {
499                debugLog("removeAutoConnectFromDisconnectedA2dpSinks, device " + device
500                        + " PRIORITY_ON");
501                a2dpService.setPriority(device, BluetoothProfile.PRIORITY_ON);
502            }
503        }
504    }
505
506    private static void debugLog(String msg) {
507        if (DBG) {
508            Log.i(TAG, msg);
509        }
510    }
511
512    private static void warnLog(String msg) {
513        Log.w(TAG, msg);
514    }
515
516    private static void errorLog(String msg) {
517        Log.e(TAG, msg);
518    }
519}
520