PhonePolicy.java revision d94f6ddab1f088340d197086949409c144ec2b0a
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.BluetoothSap;
25import android.bluetooth.BluetoothUuid;
26import android.bluetooth.IBluetooth;
27import android.content.BroadcastReceiver;
28import android.content.Context;
29import android.content.Intent;
30import android.content.IntentFilter;
31import android.os.Handler;
32import android.os.Looper;
33import android.os.Message;
34import android.os.Parcelable;
35import android.os.ParcelUuid;
36import android.util.Log;
37
38import com.android.bluetooth.a2dp.A2dpService;
39import com.android.bluetooth.hid.HidService;
40import com.android.bluetooth.hfp.HeadsetService;
41import com.android.bluetooth.pan.PanService;
42import com.android.internal.R;
43
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    final private static boolean DBG = true;
73    final private static String TAG = "BluetoothPhonePolicy";
74
75    // Message types for the handler (internal messages generated by intents or timeouts)
76    final private static int MESSAGE_PROFILE_CONNECTION_STATE_CHANGED = 1;
77    final private static int MESSAGE_PROFILE_INIT_PRIORITIES = 2;
78    final private static int MESSAGE_CONNECT_OTHER_PROFILES = 3;
79    final private static int MESSAGE_ADAPTER_STATE_TURNED_ON = 4;
80
81    // Timeouts
82    final private static int CONNECT_OTHER_PROFILES_TIMEOUT = 6000; // 6s
83
84    final private AdapterService mAdapterService;
85    final private ServiceFactory mFactory;
86    final private Handler mHandler;
87
88    // Broadcast receiver for all changes to states of various profiles
89    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
90        @Override
91        public void onReceive(Context context, Intent intent) {
92            String action = intent.getAction();
93            if (action == null) {
94                errorLog("Received intent with null action");
95                return;
96            }
97            switch (action) {
98                case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED:
99                    mHandler.obtainMessage(MESSAGE_PROFILE_CONNECTION_STATE_CHANGED,
100                                    BluetoothProfile.HEADSET,
101                                    -1, // No-op argument
102                                    intent)
103                            .sendToTarget();
104                    break;
105                case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
106                    mHandler.obtainMessage(MESSAGE_PROFILE_CONNECTION_STATE_CHANGED,
107                                    BluetoothProfile.A2DP,
108                                    -1, // No-op argument
109                                    intent)
110                            .sendToTarget();
111                    break;
112                case BluetoothAdapter.ACTION_STATE_CHANGED:
113                    // Only pass the message on if the adapter has actually changed state from
114                    // non-ON to ON. NOTE: ON is the state depicting BREDR ON and not just BLE ON.
115                    int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
116                    if (newState == BluetoothAdapter.STATE_ON) {
117                        mHandler.obtainMessage(MESSAGE_ADAPTER_STATE_TURNED_ON).sendToTarget();
118                    }
119                    break;
120                case BluetoothDevice.ACTION_UUID:
121                    mHandler.obtainMessage(MESSAGE_PROFILE_INIT_PRIORITIES, intent).sendToTarget();
122                    break;
123                default:
124                    Log.e(TAG, "Received unexpected intent, action=" + action);
125                    break;
126            }
127        }
128    };
129
130    // ONLY for testing
131    public BroadcastReceiver getBroadcastReceiver() {
132        return mReceiver;
133    }
134
135    // Handler to handoff intents to class thread
136    class PhonePolicyHandler extends Handler {
137        PhonePolicyHandler(Looper looper) {
138            super(looper);
139        }
140
141        @Override
142        public void handleMessage(Message msg) {
143            switch (msg.what) {
144                case MESSAGE_PROFILE_INIT_PRIORITIES: {
145                    Intent intent = (Intent) msg.obj;
146                    BluetoothDevice device =
147                            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
148                    Parcelable[] uuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
149                    debugLog("Received ACTION_UUID for device " + device);
150                    if (uuids != null) {
151                        ParcelUuid[] uuidsToSend = new ParcelUuid[uuids.length];
152                        for (int i = 0; i < uuidsToSend.length; i++) {
153                            uuidsToSend[i] = (ParcelUuid) uuids[i];
154                            debugLog("index=" + i + "uuid=" + uuidsToSend[i]);
155                        }
156                        processInitProfilePriorities(device, uuidsToSend);
157                    }
158                } break;
159
160                case MESSAGE_PROFILE_CONNECTION_STATE_CHANGED: {
161                    Intent intent = (Intent) msg.obj;
162                    BluetoothDevice device =
163                            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
164                    int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
165                    int nextState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
166                    processProfileStateChanged(device, msg.arg1, nextState, prevState);
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                    autoConnect();
178                    break;
179            }
180        }
181    };
182
183    // Policy API functions for lifecycle management (protected)
184    protected void start() {
185        IntentFilter filter = new IntentFilter();
186        filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
187        filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
188        filter.addAction(BluetoothDevice.ACTION_UUID);
189        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
190        mAdapterService.registerReceiver(mReceiver, filter);
191    }
192    protected void cleanup() {
193        mAdapterService.unregisterReceiver(mReceiver);
194    }
195
196    PhonePolicy(AdapterService service, ServiceFactory factory) {
197        mAdapterService = service;
198        mFactory = factory;
199        mHandler = new PhonePolicyHandler(service.getMainLooper());
200    }
201
202    // Policy implementation, all functions MUST be private
203    private void processInitProfilePriorities(BluetoothDevice device, ParcelUuid[] uuids) {
204        debugLog("processInitProfilePriorities() - device " + device);
205        HidService hidService = mFactory.getHidService();
206        A2dpService a2dpService = mFactory.getA2dpService();
207        HeadsetService headsetService = mFactory.getHeadsetService();
208        PanService panService = mFactory.getPanService();
209
210        // Set profile priorities only for the profiles discovered on the remote device.
211        // This avoids needless auto-connect attempts to profiles non-existent on the remote device
212        if ((hidService != null)
213                && (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hid)
214                           || BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hogp))
215                && (hidService.getPriority(device) == BluetoothProfile.PRIORITY_UNDEFINED)) {
216            hidService.setPriority(device, BluetoothProfile.PRIORITY_ON);
217        }
218
219        // If we do not have a stored priority for HFP/A2DP (all roles) then default to on.
220        if ((headsetService != null)
221                && ((BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HSP)
222                            || BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Handsfree))
223                           && (headsetService.getPriority(device)
224                                      == BluetoothProfile.PRIORITY_UNDEFINED))) {
225            headsetService.setPriority(device, BluetoothProfile.PRIORITY_ON);
226        }
227
228        if ((a2dpService != null)
229                && (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)
236                && (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.PANU)
237                           && (panService.getPriority(device)
238                                      == BluetoothProfile.PRIORITY_UNDEFINED)
239                           && mAdapterService.getResources().getBoolean(
240                                      R.bool.config_bluetooth_pan_enable_autoconnect))) {
241            panService.setPriority(device, BluetoothProfile.PRIORITY_ON);
242        }
243    }
244
245    private void processProfileStateChanged(
246            BluetoothDevice device, int profileId, int nextState, int prevState) {
247        debugLog("processProfileStateChanged, device=" + device + ", profile=" + profileId + ", "
248                + prevState + " -> " + nextState);
249        if (((profileId == BluetoothProfile.A2DP) || (profileId == BluetoothProfile.HEADSET))
250                && (nextState == BluetoothProfile.STATE_CONNECTED)) {
251            connectOtherProfile(device);
252            setProfileAutoConnectionPriority(device, profileId);
253        }
254    }
255
256    private void autoConnect() {
257        if (mAdapterService.getState() != BluetoothAdapter.STATE_ON) {
258            errorLog("autoConnect() - BT is not ON. Exiting autoConnect");
259            return;
260        }
261
262        if (!mAdapterService.isQuietModeEnabled()) {
263            debugLog("autoConnect() - Initiate auto connection on BT on...");
264            // Phone profiles.
265            autoConnectHeadset();
266            autoConnectA2dp();
267        } else {
268            debugLog("autoConnect() - BT is in quiet mode. Not initiating auto connections");
269        }
270    }
271
272    private void autoConnectHeadset() {
273        final HeadsetService hsService = mFactory.getHeadsetService();
274        if (hsService == null) {
275            errorLog("autoConnectHeadset, service is null");
276            return;
277        }
278        final BluetoothDevice bondedDevices[] = mAdapterService.getBondedDevices();
279        if (bondedDevices == null) {
280            errorLog("autoConnectHeadset, bondedDevices are null");
281            return;
282        }
283        for (BluetoothDevice device : bondedDevices) {
284            debugLog("autoConnectHeadset, attempt auto-connect with device " + device);
285            if (hsService.getPriority(device) == BluetoothProfile.PRIORITY_AUTO_CONNECT) {
286                debugLog("autoConnectHeadset, Connecting HFP with " + device);
287                hsService.connect(device);
288            }
289        }
290    }
291
292    private void autoConnectA2dp() {
293        final A2dpService a2dpService = mFactory.getA2dpService();
294        if (a2dpService == null) {
295            errorLog("autoConnectA2dp, service is null");
296            return;
297        }
298        final BluetoothDevice bondedDevices[] = mAdapterService.getBondedDevices();
299        if (bondedDevices == null) {
300            errorLog("autoConnectA2dp, bondedDevices are null");
301            return;
302        }
303        for (BluetoothDevice device : bondedDevices) {
304            debugLog("autoConnectA2dp, attempt auto-connect with device " + device);
305            if (a2dpService.getPriority(device) == BluetoothProfile.PRIORITY_AUTO_CONNECT) {
306                debugLog("autoConnectA2dp, connecting A2DP with " + device);
307                a2dpService.connect(device);
308            }
309        }
310    }
311
312    private void connectOtherProfile(BluetoothDevice device) {
313        if ((!mHandler.hasMessages(MESSAGE_CONNECT_OTHER_PROFILES))
314                && (!mAdapterService.isQuietModeEnabled())) {
315            Message m = mHandler.obtainMessage(MESSAGE_CONNECT_OTHER_PROFILES);
316            m.obj = device;
317            mHandler.sendMessageDelayed(m, CONNECT_OTHER_PROFILES_TIMEOUT);
318        }
319    }
320
321    // This function is called whenever a profile is connected.  This allows any other bluetooth
322    // profiles which are not already connected or in the process of connecting to attempt to
323    // connect to the device that initiated the connection.  In the event that this function is
324    // invoked and there are no current bluetooth connections no new profiles will be connected.
325    private void processConnectOtherProfiles(BluetoothDevice device) {
326        debugLog("processConnectOtherProfiles, device=" + device);
327        if (mAdapterService.getState() != BluetoothAdapter.STATE_ON) {
328            warnLog("processConnectOtherProfiles, adapter is not ON " + mAdapterService.getState());
329            return;
330        }
331        HeadsetService hsService = mFactory.getHeadsetService();
332        A2dpService a2dpService = mFactory.getA2dpService();
333        PanService panService = mFactory.getPanService();
334
335        boolean allProfilesEmpty = true;
336        List<BluetoothDevice> a2dpConnDevList = null;
337        List<BluetoothDevice> hsConnDevList = null;
338        List<BluetoothDevice> panConnDevList = null;
339
340        if (hsService != null) {
341            hsConnDevList = hsService.getConnectedDevices();
342            allProfilesEmpty = allProfilesEmpty && hsConnDevList.isEmpty();
343        }
344        if (a2dpService != null) {
345            a2dpConnDevList = a2dpService.getConnectedDevices();
346            allProfilesEmpty = allProfilesEmpty && a2dpConnDevList.isEmpty();
347        }
348        if (panService != null) {
349            panConnDevList = panService.getConnectedDevices();
350            allProfilesEmpty = allProfilesEmpty && panConnDevList.isEmpty();
351        }
352
353        if (allProfilesEmpty) {
354            debugLog("processConnectOtherProfiles, all profiles disconnected for " + device);
355            // must have connected then disconnected, don't bother connecting others.
356            return;
357        }
358
359        if (hsService != null) {
360            if (hsConnDevList.isEmpty()
361                    && (hsService.getPriority(device) >= BluetoothProfile.PRIORITY_ON)
362                    && (hsService.getConnectionState(device)
363                               == BluetoothProfile.STATE_DISCONNECTED)) {
364                debugLog("Retrying connection to Headset with device " + device);
365                hsService.connect(device);
366            }
367        }
368        if (a2dpService != null) {
369            if (a2dpConnDevList.isEmpty()
370                    && (a2dpService.getPriority(device) >= BluetoothProfile.PRIORITY_ON)
371                    && (a2dpService.getConnectionState(device)
372                               == BluetoothProfile.STATE_DISCONNECTED)) {
373                debugLog("Retrying connection to A2DP with device " + device);
374                a2dpService.connect(device);
375            }
376        }
377        if (panService != null) {
378            if (panConnDevList.isEmpty()
379                    && (panService.getPriority(device) >= BluetoothProfile.PRIORITY_ON)
380                    && (panService.getConnectionState(device)
381                               == BluetoothProfile.STATE_DISCONNECTED)) {
382                debugLog("Retrying connection to PAN with device " + device);
383                panService.connect(device);
384            }
385        }
386    }
387
388    private void setProfileAutoConnectionPriority(BluetoothDevice device, int profileId) {
389        switch (profileId) {
390            case BluetoothProfile.HEADSET:
391                HeadsetService hsService = mFactory.getHeadsetService();
392                if ((hsService != null)
393                        && (BluetoothProfile.PRIORITY_AUTO_CONNECT
394                                   != hsService.getPriority(device))) {
395                    List<BluetoothDevice> deviceList = hsService.getConnectedDevices();
396                    adjustOtherHeadsetPriorities(hsService, deviceList);
397                    hsService.setPriority(device, BluetoothProfile.PRIORITY_AUTO_CONNECT);
398                }
399                break;
400
401            case BluetoothProfile.A2DP:
402                A2dpService a2dpService = mFactory.getA2dpService();
403                if ((a2dpService != null) && (BluetoothProfile.PRIORITY_AUTO_CONNECT
404                                                     != a2dpService.getPriority(device))) {
405                    adjustOtherSinkPriorities(a2dpService, device);
406                    a2dpService.setPriority(device, BluetoothProfile.PRIORITY_AUTO_CONNECT);
407                }
408                break;
409
410            default:
411                Log.w(TAG, "Tried to set AutoConnect priority on invalid profile " + profileId);
412                break;
413        }
414    }
415
416    private void adjustOtherHeadsetPriorities(
417            HeadsetService hsService, List<BluetoothDevice> connectedDeviceList) {
418        for (BluetoothDevice device : mAdapterService.getBondedDevices()) {
419            if (hsService.getPriority(device) >= BluetoothProfile.PRIORITY_AUTO_CONNECT
420                    && !connectedDeviceList.contains(device)) {
421                hsService.setPriority(device, BluetoothProfile.PRIORITY_ON);
422            }
423        }
424    }
425
426    private void adjustOtherSinkPriorities(
427            A2dpService a2dpService, BluetoothDevice connectedDevice) {
428        for (BluetoothDevice device : mAdapterService.getBondedDevices()) {
429            if (a2dpService.getPriority(device) >= BluetoothProfile.PRIORITY_AUTO_CONNECT
430                    && !device.equals(connectedDevice)) {
431                a2dpService.setPriority(device, BluetoothProfile.PRIORITY_ON);
432            }
433        }
434    }
435
436    private static void debugLog(String msg) {
437        if (DBG) Log.d(TAG, msg);
438    }
439
440    private static void warnLog(String msg) {
441        Log.w(TAG, msg);
442    }
443
444    private static void errorLog(String msg) {
445        Log.e(TAG, msg);
446    }
447}
448