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