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