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