• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2016 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 
17 package com.android.bluetooth.pbapclient;
18 
19 import android.accounts.Account;
20 import android.accounts.AccountManager;
21 import android.annotation.RequiresPermission;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothHeadsetClient;
24 import android.bluetooth.BluetoothProfile;
25 import android.bluetooth.IBluetoothPbapClient;
26 import android.content.Attributable;
27 import android.content.AttributionSource;
28 import android.content.BroadcastReceiver;
29 import android.content.ComponentName;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.IntentFilter;
33 import android.provider.CallLog;
34 import android.util.Log;
35 
36 import com.android.bluetooth.R;
37 import com.android.bluetooth.Utils;
38 import com.android.bluetooth.btservice.AdapterService;
39 import com.android.bluetooth.btservice.ProfileService;
40 import com.android.bluetooth.btservice.storage.DatabaseManager;
41 import com.android.bluetooth.hfpclient.connserv.HfpClientConnectionService;
42 import com.android.bluetooth.sdp.SdpManager;
43 
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.Objects;
48 import java.util.concurrent.ConcurrentHashMap;
49 
50 /**
51  * Provides Bluetooth Phone Book Access Profile Client profile.
52  *
53  * @hide
54  */
55 public class PbapClientService extends ProfileService {
56     private static final boolean DBG = com.android.bluetooth.pbapclient.Utils.DBG;
57     private static final boolean VDBG = com.android.bluetooth.pbapclient.Utils.VDBG;
58 
59     private static final String TAG = "PbapClientService";
60     private static final String SERVICE_NAME = "Phonebook Access PCE";
61     // MAXIMUM_DEVICES set to 10 to prevent an excessive number of simultaneous devices.
62     private static final int MAXIMUM_DEVICES = 10;
63     private Map<BluetoothDevice, PbapClientStateMachine> mPbapClientStateMachineMap =
64             new ConcurrentHashMap<>();
65     private static PbapClientService sPbapClientService;
66     private PbapBroadcastReceiver mPbapBroadcastReceiver = new PbapBroadcastReceiver();
67     private int mSdpHandle = -1;
68 
69     private DatabaseManager mDatabaseManager;
70 
71     @Override
initBinder()72     public IProfileServiceBinder initBinder() {
73         return new BluetoothPbapClientBinder(this);
74     }
75 
76     @Override
start()77     protected boolean start() {
78         if (VDBG) {
79             Log.v(TAG, "onStart");
80         }
81 
82         mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(),
83                 "DatabaseManager cannot be null when PbapClientService starts");
84 
85         IntentFilter filter = new IntentFilter();
86         filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
87         // delay initial download until after the user is unlocked to add an account.
88         filter.addAction(Intent.ACTION_USER_UNLOCKED);
89         // To remove call logs when PBAP was never connected while calls were made,
90         // we also listen for HFP to become disconnected.
91         filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
92         try {
93             registerReceiver(mPbapBroadcastReceiver, filter);
94         } catch (Exception e) {
95             Log.w(TAG, "Unable to register pbapclient receiver", e);
96         }
97 
98         removeUncleanAccounts();
99         registerSdpRecord();
100         setPbapClientService(this);
101         return true;
102     }
103 
104     @Override
stop()105     protected boolean stop() {
106         setPbapClientService(null);
107         cleanUpSdpRecord();
108         try {
109             unregisterReceiver(mPbapBroadcastReceiver);
110         } catch (Exception e) {
111             Log.w(TAG, "Unable to unregister pbapclient receiver", e);
112         }
113         for (PbapClientStateMachine pbapClientStateMachine : mPbapClientStateMachineMap.values()) {
114             pbapClientStateMachine.doQuit();
115         }
116         removeUncleanAccounts();
117         return true;
118     }
119 
cleanupDevice(BluetoothDevice device)120     void cleanupDevice(BluetoothDevice device) {
121         if (DBG) Log.d(TAG, "Cleanup device: " + device);
122         synchronized (mPbapClientStateMachineMap) {
123             PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device);
124             if (pbapClientStateMachine != null) {
125                 mPbapClientStateMachineMap.remove(device);
126             }
127         }
128     }
129 
removeUncleanAccounts()130     private void removeUncleanAccounts() {
131         // Find all accounts that match the type "pbap" and delete them.
132         AccountManager accountManager = AccountManager.get(this);
133         Account[] accounts =
134                 accountManager.getAccountsByType(getString(R.string.pbap_account_type));
135         if (VDBG) Log.v(TAG, "Found " + accounts.length + " unclean accounts");
136         for (Account acc : accounts) {
137             Log.w(TAG, "Deleting " + acc);
138             try {
139                 getContentResolver().delete(CallLog.Calls.CONTENT_URI,
140                         CallLog.Calls.PHONE_ACCOUNT_ID + "=?", new String[]{acc.name});
141             } catch (IllegalArgumentException e) {
142                 Log.w(TAG, "Call Logs could not be deleted, they may not exist yet.");
143             }
144             // The device ID is the name of the account.
145             accountManager.removeAccountExplicitly(acc);
146         }
147     }
148 
removeHfpCallLog(String accountName, Context context)149     private void removeHfpCallLog(String accountName, Context context) {
150         if (DBG) Log.d(TAG, "Removing call logs from " + accountName);
151         // Delete call logs belonging to accountName==BD_ADDR that also match
152         // component name "hfpclient".
153         ComponentName componentName = new ComponentName(context, HfpClientConnectionService.class);
154         String selectionFilter = CallLog.Calls.PHONE_ACCOUNT_ID + "=? AND "
155                 + CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME + "=?";
156         String[] selectionArgs = new String[]{accountName, componentName.flattenToString()};
157         try {
158             getContentResolver().delete(CallLog.Calls.CONTENT_URI, selectionFilter, selectionArgs);
159         } catch (IllegalArgumentException e) {
160             Log.w(TAG, "Call Logs could not be deleted, they may not exist yet.");
161         }
162     }
163 
registerSdpRecord()164     private void registerSdpRecord() {
165         SdpManager sdpManager = SdpManager.getDefaultManager();
166         if (sdpManager == null) {
167             Log.e(TAG, "SdpManager is null");
168             return;
169         }
170         mSdpHandle = sdpManager.createPbapPceRecord(SERVICE_NAME,
171                 PbapClientConnectionHandler.PBAP_V1_2);
172     }
173 
cleanUpSdpRecord()174     private void cleanUpSdpRecord() {
175         if (mSdpHandle < 0) {
176             Log.e(TAG, "cleanUpSdpRecord, SDP record never created");
177             return;
178         }
179         int sdpHandle = mSdpHandle;
180         mSdpHandle = -1;
181         SdpManager sdpManager = SdpManager.getDefaultManager();
182         if (sdpManager == null) {
183             Log.e(TAG, "cleanUpSdpRecord failed, sdpManager is null, sdpHandle=" + sdpHandle);
184             return;
185         }
186         Log.i(TAG, "cleanUpSdpRecord, mSdpHandle=" + sdpHandle);
187         if (!sdpManager.removeSdpRecord(sdpHandle)) {
188             Log.e(TAG, "cleanUpSdpRecord, removeSdpRecord failed, sdpHandle=" + sdpHandle);
189         }
190     }
191 
192 
193     private class PbapBroadcastReceiver extends BroadcastReceiver {
194         @Override
onReceive(Context context, Intent intent)195         public void onReceive(Context context, Intent intent) {
196             String action = intent.getAction();
197             if (DBG) Log.v(TAG, "onReceive" + action);
198             if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
199                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
200                 if (getConnectionState(device) == BluetoothProfile.STATE_CONNECTED) {
201                     disconnect(device);
202                 }
203             } else if (action.equals(Intent.ACTION_USER_UNLOCKED)) {
204                 for (PbapClientStateMachine stateMachine : mPbapClientStateMachineMap.values()) {
205                     stateMachine.resumeDownload();
206                 }
207             } else if (action.equals(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED)) {
208                 // PbapClientConnectionHandler has code to remove calllogs when PBAP disconnects.
209                 // However, if PBAP was never connected/enabled in the first place, and calls are
210                 // made over HFP, these calllogs will not be removed when the device disconnects.
211                 // This code ensures callogs are still removed in this case.
212                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
213                 int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
214 
215                 if (newState == BluetoothProfile.STATE_DISCONNECTED) {
216                     if (DBG) {
217                         Log.d(TAG, "Received intent to disconnect HFP with " + device);
218                     }
219                     // HFP client stores entries in calllog.db by BD_ADDR and component name
220                     removeHfpCallLog(device.getAddress(), context);
221                 }
222             }
223         }
224     }
225 
226     /**
227      * Handler for incoming service calls
228      */
229     private static class BluetoothPbapClientBinder extends IBluetoothPbapClient.Stub
230             implements IProfileServiceBinder {
231         private PbapClientService mService;
232 
BluetoothPbapClientBinder(PbapClientService svc)233         BluetoothPbapClientBinder(PbapClientService svc) {
234             mService = svc;
235         }
236 
237         @Override
cleanup()238         public void cleanup() {
239             mService = null;
240         }
241 
242         @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getService(AttributionSource source)243         private PbapClientService getService(AttributionSource source) {
244             if (!Utils.checkCallerIsSystemOrActiveUser(TAG)
245                     || !Utils.checkServiceAvailable(mService, TAG)
246                     || !Utils.checkConnectPermissionForDataDelivery(mService, source, TAG)) {
247                 return null;
248             }
249             return mService;
250         }
251 
252         @Override
connect(BluetoothDevice device, AttributionSource source)253         public boolean connect(BluetoothDevice device, AttributionSource source) {
254             Attributable.setAttributionSource(device, source);
255             PbapClientService service = getService(source);
256             if (DBG) {
257                 Log.d(TAG, "PbapClient Binder connect ");
258             }
259             if (service == null) {
260                 Log.e(TAG, "PbapClient Binder connect no service");
261                 return false;
262             }
263             return service.connect(device);
264         }
265 
266         @Override
disconnect(BluetoothDevice device, AttributionSource source)267         public boolean disconnect(BluetoothDevice device, AttributionSource source) {
268             Attributable.setAttributionSource(device, source);
269             PbapClientService service = getService(source);
270             if (service == null) {
271                 return false;
272             }
273             return service.disconnect(device);
274         }
275 
276         @Override
getConnectedDevices(AttributionSource source)277         public List<BluetoothDevice> getConnectedDevices(AttributionSource source) {
278             PbapClientService service = getService(source);
279             if (service == null) {
280                 return new ArrayList<BluetoothDevice>(0);
281             }
282             return service.getConnectedDevices();
283         }
284 
285         @Override
getDevicesMatchingConnectionStates(int[] states, AttributionSource source)286         public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states,
287                 AttributionSource source) {
288             PbapClientService service = getService(source);
289             if (service == null) {
290                 return new ArrayList<BluetoothDevice>(0);
291             }
292             return service.getDevicesMatchingConnectionStates(states);
293         }
294 
295         @Override
getConnectionState(BluetoothDevice device, AttributionSource source)296         public int getConnectionState(BluetoothDevice device, AttributionSource source) {
297             Attributable.setAttributionSource(device, source);
298             PbapClientService service = getService(source);
299             if (service == null) {
300                 return BluetoothProfile.STATE_DISCONNECTED;
301             }
302             return service.getConnectionState(device);
303         }
304 
305         @Override
setConnectionPolicy(BluetoothDevice device, int connectionPolicy, AttributionSource source)306         public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy,
307                 AttributionSource source) {
308             Attributable.setAttributionSource(device, source);
309             PbapClientService service = getService(source);
310             if (service == null) {
311                 return false;
312             }
313             return service.setConnectionPolicy(device, connectionPolicy);
314         }
315 
316         @Override
getConnectionPolicy(BluetoothDevice device, AttributionSource source)317         public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) {
318             Attributable.setAttributionSource(device, source);
319             PbapClientService service = getService(source);
320             if (service == null) {
321                 return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
322             }
323             return service.getConnectionPolicy(device);
324         }
325 
326 
327     }
328 
329     // API methods
getPbapClientService()330     public static synchronized PbapClientService getPbapClientService() {
331         if (sPbapClientService == null) {
332             Log.w(TAG, "getPbapClientService(): service is null");
333             return null;
334         }
335         if (!sPbapClientService.isAvailable()) {
336             Log.w(TAG, "getPbapClientService(): service is not available");
337             return null;
338         }
339         return sPbapClientService;
340     }
341 
setPbapClientService(PbapClientService instance)342     private static synchronized void setPbapClientService(PbapClientService instance) {
343         if (VDBG) {
344             Log.v(TAG, "setPbapClientService(): set to: " + instance);
345         }
346         sPbapClientService = instance;
347     }
348 
349     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
connect(BluetoothDevice device)350     public boolean connect(BluetoothDevice device) {
351         if (device == null) {
352             throw new IllegalArgumentException("Null device");
353         }
354         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
355                 "Need BLUETOOTH_PRIVILEGED permission");
356         if (DBG) Log.d(TAG, "Received request to ConnectPBAPPhonebook " + device.getAddress());
357         if (getConnectionPolicy(device) <= BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
358             return false;
359         }
360         synchronized (mPbapClientStateMachineMap) {
361             PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device);
362             if (pbapClientStateMachine == null
363                     && mPbapClientStateMachineMap.size() < MAXIMUM_DEVICES) {
364                 pbapClientStateMachine = new PbapClientStateMachine(this, device);
365                 pbapClientStateMachine.start();
366                 mPbapClientStateMachineMap.put(device, pbapClientStateMachine);
367                 return true;
368             } else {
369                 Log.w(TAG, "Received connect request while already connecting/connected.");
370                 return false;
371             }
372         }
373     }
374 
375     /**
376      * Disconnects the pbap client profile from the passed in device
377      *
378      * @param device is the device with which we will disconnect the pbap client profile
379      * @return true if we disconnected the pbap client profile, false otherwise
380      */
381     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
disconnect(BluetoothDevice device)382     public boolean disconnect(BluetoothDevice device) {
383         if (device == null) {
384             throw new IllegalArgumentException("Null device");
385         }
386         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
387                 "Need BLUETOOTH_PRIVILEGED permission");
388         PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device);
389         if (pbapClientStateMachine != null) {
390             pbapClientStateMachine.disconnect(device);
391             return true;
392 
393         } else {
394             Log.w(TAG, "disconnect() called on unconnected device.");
395             return false;
396         }
397     }
398 
getConnectedDevices()399     public List<BluetoothDevice> getConnectedDevices() {
400         int[] desiredStates = {BluetoothProfile.STATE_CONNECTED};
401         return getDevicesMatchingConnectionStates(desiredStates);
402     }
403 
getDevicesMatchingConnectionStates(int[] states)404     private List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
405         List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>(0);
406         for (Map.Entry<BluetoothDevice, PbapClientStateMachine> stateMachineEntry :
407                 mPbapClientStateMachineMap
408                 .entrySet()) {
409             int currentDeviceState = stateMachineEntry.getValue().getConnectionState();
410             for (int state : states) {
411                 if (currentDeviceState == state) {
412                     deviceList.add(stateMachineEntry.getKey());
413                     break;
414                 }
415             }
416         }
417         return deviceList;
418     }
419 
420     /**
421      * Get the current connection state of the profile
422      *
423      * @param device is the remote bluetooth device
424      * @return {@link BluetoothProfile#STATE_DISCONNECTED} if this profile is disconnected,
425      * {@link BluetoothProfile#STATE_CONNECTING} if this profile is being connected,
426      * {@link BluetoothProfile#STATE_CONNECTED} if this profile is connected, or
427      * {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected
428      */
getConnectionState(BluetoothDevice device)429     public int getConnectionState(BluetoothDevice device) {
430         if (device == null) {
431             throw new IllegalArgumentException("Null device");
432         }
433         PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device);
434         if (pbapClientStateMachine == null) {
435             return BluetoothProfile.STATE_DISCONNECTED;
436         } else {
437             return pbapClientStateMachine.getConnectionState(device);
438         }
439     }
440 
441     /**
442      * Set connection policy of the profile and connects it if connectionPolicy is
443      * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is
444      * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
445      *
446      * <p> The device should already be paired.
447      * Connection policy can be one of:
448      * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
449      * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
450      * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
451      *
452      * @param device Paired bluetooth device
453      * @param connectionPolicy is the connection policy to set to for this profile
454      * @return true if connectionPolicy is set, false on error
455      */
456     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
setConnectionPolicy(BluetoothDevice device, int connectionPolicy)457     public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
458         if (device == null) {
459             throw new IllegalArgumentException("Null device");
460         }
461         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
462                 "Need BLUETOOTH_PRIVILEGED permission");
463         if (DBG) {
464             Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy);
465         }
466 
467         if (!mDatabaseManager.setProfileConnectionPolicy(device, BluetoothProfile.PBAP_CLIENT,
468                   connectionPolicy)) {
469             return false;
470         }
471         if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
472             connect(device);
473         } else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
474             disconnect(device);
475         }
476         return true;
477     }
478 
479     /**
480      * Get the connection policy of the profile.
481      *
482      * <p> The connection policy can be any of:
483      * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
484      * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
485      * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
486      *
487      * @param device Bluetooth device
488      * @return connection policy of the device
489      * @hide
490      */
491     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
getConnectionPolicy(BluetoothDevice device)492     public int getConnectionPolicy(BluetoothDevice device) {
493         if (device == null) {
494             throw new IllegalArgumentException("Null device");
495         }
496         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
497                 "Need BLUETOOTH_PRIVILEGED permission");
498         return mDatabaseManager
499                 .getProfileConnectionPolicy(device, BluetoothProfile.PBAP_CLIENT);
500     }
501 
502     @Override
dump(StringBuilder sb)503     public void dump(StringBuilder sb) {
504         super.dump(sb);
505         for (PbapClientStateMachine stateMachine : mPbapClientStateMachineMap.values()) {
506             stateMachine.dump(sb);
507         }
508     }
509 }
510