• 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 package com.android.bluetooth.pbapclient;
17 
18 import android.accounts.Account;
19 import android.annotation.SuppressLint;
20 import android.bluetooth.BluetoothDevice;
21 import android.bluetooth.BluetoothSocket;
22 import android.bluetooth.BluetoothUuid;
23 import android.os.Handler;
24 import android.os.Looper;
25 import android.os.Message;
26 import android.provider.CallLog;
27 import android.provider.CallLog.Calls;
28 import android.util.Log;
29 
30 import com.android.bluetooth.BluetoothObexTransport;
31 import com.android.bluetooth.ObexAppParameters;
32 import com.android.bluetooth.R;
33 import com.android.bluetooth.flags.Flags;
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.obex.ClientSession;
36 import com.android.obex.HeaderSet;
37 import com.android.obex.ResponseCodes;
38 import com.android.vcard.VCardEntry;
39 
40 import java.io.IOException;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44 
45 /* Bluetooth/pbapclient/PbapClientConnectionHandler is responsible
46  * for connecting, disconnecting and downloading contacts from the
47  * PBAP PSE when commanded. It receives all direction from the
48  * controlling state machine.
49  */
50 class PbapClientConnectionHandler extends Handler {
51     private static final String TAG = PbapClientConnectionHandler.class.getSimpleName();
52 
53     // Tradeoff: larger BATCH_SIZE leads to faster download rates, while smaller
54     // BATCH_SIZE is less prone to IO Exceptions if there is a download in
55     // progress when Bluetooth stack is torn down.
56     private static final int DEFAULT_BATCH_SIZE = 250;
57 
58     static final int MSG_CONNECT = 1;
59     static final int MSG_DISCONNECT = 2;
60     static final int MSG_DOWNLOAD = 3;
61 
62     static final int L2CAP_INVALID_PSM = -1;
63     static final int RFCOMM_INVALID_CHANNEL_ID = -1;
64 
65     // The following constants are pulled from the Bluetooth Phone Book Access Profile specification
66     // 1.1
67     private static final byte[] PBAP_TARGET =
68             new byte[] {
69                 0x79,
70                 0x61,
71                 0x35,
72                 (byte) 0xf0,
73                 (byte) 0xf0,
74                 (byte) 0xc5,
75                 0x11,
76                 (byte) 0xd8,
77                 0x09,
78                 0x66,
79                 0x08,
80                 0x00,
81                 0x20,
82                 0x0c,
83                 (byte) 0x9a,
84                 0x66
85             };
86 
87     private final Account mAccount;
88     private BluetoothSocket mSocket;
89     private final BluetoothDevice mDevice;
90     private final int mLocalSupportedFeatures;
91     // PSE SDP Record for current device.
92     private PbapSdpRecord mPseRec = null;
93     private ClientSession mObexSession;
94     private final PbapClientService mService;
95     private PbapClientObexAuthenticator mAuth = null;
96     private final PbapClientStateMachineOld mPbapClientStateMachine;
97     private boolean mAccountCreated;
98 
99     /**
100      * Constructs PCEConnectionHandler object
101      *
102      * @param pceHandlerbuild To build PbapClientConnectionHandler Instance.
103      */
PbapClientConnectionHandler(Builder pceHandlerbuild)104     PbapClientConnectionHandler(Builder pceHandlerbuild) {
105         super(pceHandlerbuild.mLooper);
106 
107         if (Flags.pbapClientStorageRefactor()) {
108             Log.w(TAG, "This object is no longer used in this configuration");
109         }
110 
111         mDevice = pceHandlerbuild.mDevice;
112         mLocalSupportedFeatures = pceHandlerbuild.mLocalSupportedFeatures;
113         mService = pceHandlerbuild.mService;
114         mPbapClientStateMachine = pceHandlerbuild.mClientStateMachine;
115         mAuth = new PbapClientObexAuthenticator();
116         mAccount =
117                 new Account(
118                         mDevice.getAddress(),
119                         mService.getString(R.string.pbap_client_account_type));
120     }
121 
122     public static class Builder {
123 
124         private Looper mLooper;
125         private PbapClientService mService;
126         private BluetoothDevice mDevice;
127         private int mLocalSupportedFeatures;
128         private PbapClientStateMachineOld mClientStateMachine;
129 
setLooper(Looper loop)130         public Builder setLooper(Looper loop) {
131             this.mLooper = loop;
132             return this;
133         }
134 
setLocalSupportedFeatures(int features)135         public Builder setLocalSupportedFeatures(int features) {
136             this.mLocalSupportedFeatures = features;
137             return this;
138         }
139 
setClientSM(PbapClientStateMachineOld clientStateMachine)140         public Builder setClientSM(PbapClientStateMachineOld clientStateMachine) {
141             this.mClientStateMachine = clientStateMachine;
142             return this;
143         }
144 
setRemoteDevice(BluetoothDevice device)145         public Builder setRemoteDevice(BluetoothDevice device) {
146             this.mDevice = device;
147             return this;
148         }
149 
setService(PbapClientService service)150         public Builder setService(PbapClientService service) {
151             this.mService = service;
152             return this;
153         }
154 
build()155         public PbapClientConnectionHandler build() {
156             PbapClientConnectionHandler pbapClientHandler = new PbapClientConnectionHandler(this);
157             return pbapClientHandler;
158         }
159     }
160 
161     @Override
handleMessage(Message msg)162     public void handleMessage(Message msg) {
163         Log.d(TAG, "Handling Message = " + msg.what);
164         switch (msg.what) {
165             case MSG_CONNECT:
166                 mPseRec = (PbapSdpRecord) msg.obj;
167 
168                 /* To establish a connection, first open a socket and then create an OBEX session */
169                 if (connectSocket()) {
170                     Log.d(TAG, "Socket connected");
171                 } else {
172                     Log.w(TAG, "Socket CONNECT Failure ");
173                     mPbapClientStateMachine.sendMessage(
174                             PbapClientStateMachineOld.MSG_CONNECTION_FAILED);
175                     return;
176                 }
177 
178                 if (connectObexSession()) {
179                     mPbapClientStateMachine.sendMessage(
180                             PbapClientStateMachineOld.MSG_CONNECTION_COMPLETE);
181                 } else {
182                     mPbapClientStateMachine.sendMessage(
183                             PbapClientStateMachineOld.MSG_CONNECTION_FAILED);
184                 }
185                 break;
186 
187             case MSG_DISCONNECT:
188                 Log.d(TAG, "Starting Disconnect");
189                 try {
190                     if (mObexSession != null) {
191                         Log.d(TAG, "obexSessionDisconnect" + mObexSession);
192                         mObexSession.disconnect(null);
193                         mObexSession.close();
194                     }
195                 } catch (IOException e) {
196                     Log.w(TAG, "DISCONNECT Failure ", e);
197                 } finally {
198                     Log.d(TAG, "Closing Socket");
199                     closeSocket();
200                 }
201                 Log.d(TAG, "Completing Disconnect");
202                 if (mAccountCreated) {
203                     removeAccount();
204                 }
205                 removeCallLog();
206 
207                 mPbapClientStateMachine.sendMessage(
208                         PbapClientStateMachineOld.MSG_CONNECTION_CLOSED);
209                 break;
210 
211             case MSG_DOWNLOAD:
212                 mAccountCreated = addAccount();
213                 if (!mAccountCreated) {
214                     Log.e(TAG, "Account creation failed.");
215                     return;
216                 }
217                 if (mPseRec.isRepositorySupported(PbapSdpRecord.REPOSITORY_FAVORITES)) {
218                     downloadContacts(PbapPhonebook.FAVORITES_PATH);
219                 }
220                 if (mPseRec.isRepositorySupported(PbapSdpRecord.REPOSITORY_LOCAL_PHONEBOOK)) {
221                     downloadContacts(PbapPhonebook.LOCAL_PHONEBOOK_PATH);
222                 }
223                 if (mPseRec.isRepositorySupported(PbapSdpRecord.REPOSITORY_SIM_CARD)) {
224                     downloadContacts(PbapPhonebook.SIM_PHONEBOOK_PATH);
225                 }
226 
227                 Map<String, Integer> callCounter = new HashMap<>();
228                 downloadCallLog(PbapPhonebook.MCH_PATH, callCounter);
229                 downloadCallLog(PbapPhonebook.ICH_PATH, callCounter);
230                 downloadCallLog(PbapPhonebook.OCH_PATH, callCounter);
231                 break;
232 
233             default:
234                 Log.w(TAG, "Received Unexpected Message");
235         }
236     }
237 
238     @VisibleForTesting
setPseRecord(PbapSdpRecord record)239     synchronized void setPseRecord(PbapSdpRecord record) {
240         mPseRec = record;
241     }
242 
243     @VisibleForTesting
getSocket()244     synchronized BluetoothSocket getSocket() {
245         return mSocket;
246     }
247 
248     /* Utilize SDP, if available, to create a socket connection over L2CAP, RFCOMM specified
249      * channel, or RFCOMM default channel. */
250     @VisibleForTesting
251     @SuppressLint("AndroidFrameworkRequiresPermission") // TODO: b/350563786
connectSocket()252     synchronized boolean connectSocket() {
253         try {
254             /* Use BluetoothSocket to connect */
255             if (mPseRec == null) {
256                 // BackWardCompatibility: Fall back to create RFCOMM through UUID.
257                 Log.v(TAG, "connectSocket: UUID: " + BluetoothUuid.PBAP_PSE.getUuid());
258                 mSocket =
259                         mDevice.createRfcommSocketToServiceRecord(BluetoothUuid.PBAP_PSE.getUuid());
260             } else if (mPseRec.getL2capPsm() != L2CAP_INVALID_PSM) {
261                 Log.v(TAG, "connectSocket: PSM: " + mPseRec.getL2capPsm());
262                 mSocket = mDevice.createL2capSocket(mPseRec.getL2capPsm());
263             } else if (mPseRec.getRfcommChannelNumber() != RFCOMM_INVALID_CHANNEL_ID) {
264                 Log.v(TAG, "connectSocket: channel: " + mPseRec.getRfcommChannelNumber());
265                 mSocket = mDevice.createRfcommSocket(mPseRec.getRfcommChannelNumber());
266             } else {
267                 Log.w(TAG, "connectSocket: transport PSM or channel ID not specified");
268                 return false;
269             }
270 
271             if (mSocket != null) {
272                 mSocket.connect();
273                 return true;
274             } else {
275                 Log.w(TAG, "Could not create socket");
276             }
277         } catch (IOException e) {
278             Log.e(TAG, "Error while connecting socket", e);
279         }
280         return false;
281     }
282 
283     /* Connect an OBEX session over the already connected socket.  First establish an OBEX Transport
284      * abstraction, then establish a Bluetooth Authenticator, and finally issue the connect call */
285     @VisibleForTesting
connectObexSession()286     boolean connectObexSession() {
287         boolean connectionSuccessful = false;
288 
289         try {
290             Log.v(TAG, "Start Obex Client Session");
291             BluetoothObexTransport transport = new BluetoothObexTransport(mSocket);
292             mObexSession = new ClientSession(transport);
293             mObexSession.setAuthenticator(mAuth);
294 
295             HeaderSet connectionRequest = new HeaderSet();
296             connectionRequest.setHeader(HeaderSet.TARGET, PBAP_TARGET);
297 
298             if (mPseRec != null) {
299                 Log.d(TAG, "Remote PbapSupportedFeatures " + mPseRec.getSupportedFeatures());
300 
301                 ObexAppParameters oap = new ObexAppParameters();
302 
303                 if (mPseRec.getProfileVersion() >= PbapSdpRecord.VERSION_1_2) {
304                     oap.add(
305                             PbapApplicationParameters.OAP_PBAP_SUPPORTED_FEATURES,
306                             mLocalSupportedFeatures);
307                 }
308 
309                 oap.addToHeaderSet(connectionRequest);
310             }
311             HeaderSet connectionResponse = mObexSession.connect(connectionRequest);
312 
313             connectionSuccessful =
314                     (connectionResponse.getResponseCode() == ResponseCodes.OBEX_HTTP_OK);
315             Log.d(TAG, "Success = " + Boolean.toString(connectionSuccessful));
316         } catch (IOException | NullPointerException e) {
317             // Will get NPE if a null mSocket is passed to BluetoothObexTransport.
318             // mSocket can be set to null if an abort() --> closeSocket() was called between
319             // the calls to connectSocket() and connectObexSession().
320             Log.w(TAG, "CONNECT Failure ", e);
321             closeSocket();
322         }
323         return connectionSuccessful;
324     }
325 
abort()326     void abort() {
327         // Perform forced cleanup, it is ok if the handler throws an exception this will free the
328         // handler to complete what it is doing and finish with cleanup.
329         closeSocket();
330         this.getLooper().getThread().interrupt();
331     }
332 
closeSocket()333     private synchronized void closeSocket() {
334         try {
335             if (mSocket != null) {
336                 Log.d(TAG, "Closing socket" + mSocket);
337                 mSocket.close();
338                 mSocket = null;
339             }
340         } catch (IOException e) {
341             Log.e(TAG, "Error when closing socket", e);
342             mSocket = null;
343         }
344     }
345 
346     @VisibleForTesting
downloadContacts(String path)347     void downloadContacts(String path) {
348         try {
349             PhonebookPullRequest processor =
350                     new PhonebookPullRequest(mPbapClientStateMachine.getContext());
351 
352             PbapApplicationParameters params =
353                     new PbapApplicationParameters(
354                             PbapApplicationParameters.PROPERTIES_ALL,
355                             /* format, unused */ (byte) 0,
356                             PbapApplicationParameters.RETURN_SIZE_ONLY,
357                             /* list startOffset, start from beginning */ 0);
358 
359             // Download contacts in batches of size DEFAULT_BATCH_SIZE
360             RequestPullPhonebookMetadata requestPbSize =
361                     new RequestPullPhonebookMetadata(path, params);
362             requestPbSize.execute(mObexSession);
363 
364             int numberOfContactsRemaining = requestPbSize.getMetadata().size();
365             int startOffset = 0;
366             if (PbapPhonebook.LOCAL_PHONEBOOK_PATH.equals(path)) {
367                 // PBAP v1.2.3, Sec 3.1.5. The first contact in pb is owner card 0.vcf, which we
368                 // do not want to download. The other phonebook objects (e.g., fav) don't have an
369                 // owner card, so they don't need an offset.
370                 startOffset = 1;
371                 // "-1" because Owner Card 0.vcf is also included in /pb, but not in /fav.
372                 numberOfContactsRemaining -= 1;
373             }
374 
375             while ((numberOfContactsRemaining > 0)
376                     && (startOffset <= PbapApplicationParameters.MAX_PHONEBOOK_SIZE)) {
377                 int numberOfContactsToDownload =
378                         Math.min(
379                                 Math.min(DEFAULT_BATCH_SIZE, numberOfContactsRemaining),
380                                 PbapApplicationParameters.MAX_PHONEBOOK_SIZE - startOffset + 1);
381 
382                 params =
383                         new PbapApplicationParameters(
384                                 PbapApplicationParameters.PROPERTIES_ALL,
385                                 PbapPhonebook.FORMAT_VCARD_30,
386                                 numberOfContactsToDownload,
387                                 startOffset);
388 
389                 RequestPullPhonebook request = new RequestPullPhonebook(path, params);
390                 request.execute(mObexSession);
391                 List<VCardEntry> vcards = request.getList();
392                 for (VCardEntry v : vcards) {
393                     v.setAccount(mAccount);
394 
395                     // mark each vcard as a favorite
396                     if (PbapPhonebook.FAVORITES_PATH.equals(path)) {
397                         v.setStarred(true);
398                     }
399                 }
400                 processor.setResults(vcards);
401                 processor.onPullComplete();
402 
403                 startOffset += numberOfContactsToDownload;
404                 numberOfContactsRemaining -= numberOfContactsToDownload;
405             }
406             if ((startOffset > PbapApplicationParameters.MAX_PHONEBOOK_SIZE)
407                     && (numberOfContactsRemaining > 0)) {
408                 Log.w(TAG, "Download contacts incomplete, index exceeded upper limit.");
409             }
410         } catch (IOException e) {
411             Log.e(TAG, "Download contacts failure", e);
412         } catch (IllegalArgumentException e) {
413             Log.e(TAG, "Download contacts failure: " + e.getMessage(), e);
414         }
415     }
416 
417     @VisibleForTesting
downloadCallLog(String path, Map<String, Integer> callCounter)418     void downloadCallLog(String path, Map<String, Integer> callCounter) {
419         try {
420             PbapApplicationParameters params =
421                     new PbapApplicationParameters(
422                             /* properties, unused for call logs */ 0,
423                             PbapPhonebook.FORMAT_VCARD_30,
424                             0,
425                             0);
426 
427             RequestPullPhonebook request = new RequestPullPhonebook(path, params);
428             request.execute(mObexSession);
429             CallLogPullRequest processor =
430                     new CallLogPullRequest(
431                             mPbapClientStateMachine.getContext(), path, callCounter, mAccount);
432             processor.setResults(request.getList());
433             processor.onPullComplete();
434         } catch (IOException e) {
435             Log.e(TAG, "Download call log failure", e);
436         } catch (IllegalArgumentException e) {
437             Log.e(TAG, "Download call log failure: " + e.getMessage(), e);
438         }
439     }
440 
441     @VisibleForTesting
addAccount()442     boolean addAccount() {
443         if (mService.addAccount(mAccount)) {
444             Log.d(TAG, "Added account " + mAccount);
445             return true;
446         } else {
447             Log.e(TAG, "Failed to add account " + mAccount);
448         }
449         return false;
450     }
451 
452     @VisibleForTesting
removeAccount()453     void removeAccount() {
454         if (mService.removeAccount(mAccount)) {
455             Log.d(TAG, "Removed account " + mAccount);
456         } else {
457             Log.e(TAG, "Failed to remove account " + mAccount);
458         }
459     }
460 
461     @VisibleForTesting
removeCallLog()462     void removeCallLog() {
463         try {
464             // need to check call table is exist ?
465             if (mService.getContentResolver() == null) {
466                 Log.d(TAG, "CallLog ContentResolver is not found");
467                 return;
468             }
469             mService.getContentResolver()
470                     .delete(
471                             CallLog.Calls.CONTENT_URI,
472                             Calls.PHONE_ACCOUNT_ID + "=?",
473                             new String[] {mAccount.name});
474         } catch (IllegalArgumentException e) {
475             Log.d(TAG, "Call Logs could not be deleted, they may not exist yet.");
476         }
477     }
478 }
479