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