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.BluetoothAdapter;
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.BluetoothSocket;
23 import android.bluetooth.BluetoothUuid;
24 import android.bluetooth.SdpPseRecord;
25 import android.content.Context;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.os.Message;
29 import android.provider.CallLog;
30 import android.provider.CallLog.Calls;
31 import android.util.Log;
32 
33 import com.android.bluetooth.BluetoothObexTransport;
34 import com.android.bluetooth.R;
35 import com.android.vcard.VCardEntry;
36 
37 import java.io.IOException;
38 import java.util.ArrayList;
39 import java.util.HashMap;
40 
41 import javax.obex.ClientSession;
42 import javax.obex.HeaderSet;
43 import javax.obex.ResponseCodes;
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     private static final int L2CAP_INVALID_PSM = -1;
107 
108     public static final String PB_PATH = "telecom/pb.vcf";
109     public static final String FAV_PATH = "telecom/fav.vcf";
110     public static final String MCH_PATH = "telecom/mch.vcf";
111     public static final String ICH_PATH = "telecom/ich.vcf";
112     public static final String OCH_PATH = "telecom/och.vcf";
113     public static final String SIM_PB_PATH = "SIM1/telecom/pb.vcf";
114     public static final String SIM_MCH_PATH = "SIM1/telecom/mch.vcf";
115     public static final String SIM_ICH_PATH = "SIM1/telecom/ich.vcf";
116     public static final String SIM_OCH_PATH = "SIM1/telecom/och.vcf";
117 
118     // PBAP v1.2.3 Sec. 7.1.2
119     private static final int SUPPORTED_REPOSITORIES_LOCALPHONEBOOK = 1 << 0;
120     private static final int SUPPORTED_REPOSITORIES_SIMCARD = 1 << 1;
121     private static final int SUPPORTED_REPOSITORIES_FAVORITES = 1 << 3;
122 
123     public static final int PBAP_V1_2 = 0x0102;
124     public static final byte VCARD_TYPE_21 = 0;
125     public static final byte VCARD_TYPE_30 = 1;
126 
127     private Account mAccount;
128     private AccountManager mAccountManager;
129     private BluetoothSocket mSocket;
130     private final BluetoothAdapter mAdapter;
131     private final BluetoothDevice mDevice;
132     // PSE SDP Record for current device.
133     private SdpPseRecord mPseRec = null;
134     private ClientSession mObexSession;
135     private Context mContext;
136     private BluetoothPbapObexAuthenticator mAuth = null;
137     private final PbapClientStateMachine mPbapClientStateMachine;
138     private boolean mAccountCreated;
139 
PbapClientConnectionHandler(Looper looper, Context context, PbapClientStateMachine stateMachine, BluetoothDevice device)140     PbapClientConnectionHandler(Looper looper, Context context, PbapClientStateMachine stateMachine,
141             BluetoothDevice device) {
142         super(looper);
143         mAdapter = BluetoothAdapter.getDefaultAdapter();
144         mDevice = device;
145         mContext = context;
146         mPbapClientStateMachine = stateMachine;
147         mAuth = new BluetoothPbapObexAuthenticator(this);
148         mAccountManager = AccountManager.get(mPbapClientStateMachine.getContext());
149         mAccount =
150                 new Account(mDevice.getAddress(), mContext.getString(R.string.pbap_account_type));
151     }
152 
153     /**
154      * Constructs PCEConnectionHandler object
155      *
156      * @param Builder To build  BluetoothPbapClientHandler Instance.
157      */
PbapClientConnectionHandler(Builder pceHandlerbuild)158     PbapClientConnectionHandler(Builder pceHandlerbuild) {
159         super(pceHandlerbuild.mLooper);
160         mAdapter = BluetoothAdapter.getDefaultAdapter();
161         mDevice = pceHandlerbuild.mDevice;
162         mContext = pceHandlerbuild.mContext;
163         mPbapClientStateMachine = pceHandlerbuild.mClientStateMachine;
164         mAuth = new BluetoothPbapObexAuthenticator(this);
165         mAccountManager = AccountManager.get(mPbapClientStateMachine.getContext());
166         mAccount =
167                 new Account(mDevice.getAddress(), mContext.getString(R.string.pbap_account_type));
168     }
169 
170     public static class Builder {
171 
172         private Looper mLooper;
173         private Context mContext;
174         private BluetoothDevice mDevice;
175         private PbapClientStateMachine mClientStateMachine;
176 
setLooper(Looper loop)177         public Builder setLooper(Looper loop) {
178             this.mLooper = loop;
179             return this;
180         }
181 
setClientSM(PbapClientStateMachine clientStateMachine)182         public Builder setClientSM(PbapClientStateMachine clientStateMachine) {
183             this.mClientStateMachine = clientStateMachine;
184             return this;
185         }
186 
setRemoteDevice(BluetoothDevice device)187         public Builder setRemoteDevice(BluetoothDevice device) {
188             this.mDevice = device;
189             return this;
190         }
191 
setContext(Context context)192         public Builder setContext(Context context) {
193             this.mContext = context;
194             return this;
195         }
196 
build()197         public PbapClientConnectionHandler build() {
198             PbapClientConnectionHandler pbapClientHandler = new PbapClientConnectionHandler(this);
199             return pbapClientHandler;
200         }
201 
202     }
203 
204     @Override
handleMessage(Message msg)205     public void handleMessage(Message msg) {
206         if (DBG) {
207             Log.d(TAG, "Handling Message = " + msg.what);
208         }
209         switch (msg.what) {
210             case MSG_CONNECT:
211                 mPseRec = (SdpPseRecord) msg.obj;
212                 /* To establish a connection, first open a socket and then create an OBEX session */
213                 if (connectSocket()) {
214                     if (DBG) {
215                         Log.d(TAG, "Socket connected");
216                     }
217                 } else {
218                     Log.w(TAG, "Socket CONNECT Failure ");
219                     mPbapClientStateMachine.sendMessage(
220                             PbapClientStateMachine.MSG_CONNECTION_FAILED);
221                     return;
222                 }
223 
224                 if (connectObexSession()) {
225                     mPbapClientStateMachine.sendMessage(
226                             PbapClientStateMachine.MSG_CONNECTION_COMPLETE);
227                 } else {
228                     mPbapClientStateMachine.sendMessage(
229                             PbapClientStateMachine.MSG_CONNECTION_FAILED);
230                 }
231                 break;
232 
233             case MSG_DISCONNECT:
234                 if (DBG) {
235                     Log.d(TAG, "Starting Disconnect");
236                 }
237                 try {
238                     if (mObexSession != null) {
239                         if (DBG) {
240                             Log.d(TAG, "obexSessionDisconnect" + mObexSession);
241                         }
242                         mObexSession.disconnect(null);
243                         mObexSession.close();
244                     }
245 
246                     if (DBG) {
247                         Log.d(TAG, "Closing Socket");
248                     }
249                     closeSocket();
250                 } catch (IOException e) {
251                     Log.w(TAG, "DISCONNECT Failure ", e);
252                 }
253                 if (DBG) {
254                     Log.d(TAG, "Completing Disconnect");
255                 }
256                 removeAccount(mAccount);
257                 removeCallLog(mAccount);
258 
259                 mPbapClientStateMachine.sendMessage(PbapClientStateMachine.MSG_CONNECTION_CLOSED);
260                 break;
261 
262             case MSG_DOWNLOAD:
263                 mAccountCreated = addAccount(mAccount);
264                 if (!mAccountCreated) {
265                     Log.e(TAG, "Account creation failed.");
266                     return;
267                 }
268                 if (isRepositorySupported(SUPPORTED_REPOSITORIES_FAVORITES)) {
269                     downloadContacts(FAV_PATH);
270                 }
271                 if (isRepositorySupported(SUPPORTED_REPOSITORIES_LOCALPHONEBOOK)) {
272                     downloadContacts(PB_PATH);
273                 }
274                 if (isRepositorySupported(SUPPORTED_REPOSITORIES_SIMCARD)) {
275                     downloadContacts(SIM_PB_PATH);
276                 }
277 
278                 HashMap<String, Integer> callCounter = new HashMap<>();
279                 downloadCallLog(MCH_PATH, callCounter);
280                 downloadCallLog(ICH_PATH, callCounter);
281                 downloadCallLog(OCH_PATH, callCounter);
282                 break;
283 
284             default:
285                 Log.w(TAG, "Received Unexpected Message");
286         }
287         return;
288     }
289 
290     /* Utilize SDP, if available, to create a socket connection over L2CAP, RFCOMM specified
291      * channel, or RFCOMM default channel. */
connectSocket()292     private synchronized boolean connectSocket() {
293         try {
294             /* Use BluetoothSocket to connect */
295             if (mPseRec == null) {
296                 // BackWardCompatability: Fall back to create RFCOMM through UUID.
297                 if (VDBG) Log.v(TAG, "connectSocket: UUID: " + BluetoothUuid.PBAP_PSE.getUuid());
298                 mSocket =
299                         mDevice.createRfcommSocketToServiceRecord(BluetoothUuid.PBAP_PSE.getUuid());
300             } else if (mPseRec.getL2capPsm() != L2CAP_INVALID_PSM) {
301                 if (VDBG) Log.v(TAG, "connectSocket: PSM: " + mPseRec.getL2capPsm());
302                 mSocket = mDevice.createL2capSocket(mPseRec.getL2capPsm());
303             } else {
304                 if (VDBG) Log.v(TAG, "connectSocket: channel: " + mPseRec.getRfcommChannelNumber());
305                 mSocket = mDevice.createRfcommSocket(mPseRec.getRfcommChannelNumber());
306             }
307 
308             if (mSocket != null) {
309                 mSocket.connect();
310                 return true;
311             } else {
312                 Log.w(TAG, "Could not create socket");
313             }
314         } catch (IOException e) {
315             Log.e(TAG, "Error while connecting socket", e);
316         }
317         return false;
318     }
319 
320     /* Connect an OBEX session over the already connected socket.  First establish an OBEX Transport
321      * abstraction, then establish a Bluetooth Authenticator, and finally issue the connect call */
connectObexSession()322     private boolean connectObexSession() {
323         boolean connectionSuccessful = false;
324 
325         try {
326             if (VDBG) {
327                 Log.v(TAG, "Start Obex Client Session");
328             }
329             BluetoothObexTransport transport = new BluetoothObexTransport(mSocket);
330             mObexSession = new ClientSession(transport);
331             mObexSession.setAuthenticator(mAuth);
332 
333             HeaderSet connectionRequest = new HeaderSet();
334             connectionRequest.setHeader(HeaderSet.TARGET, PBAP_TARGET);
335 
336             if (mPseRec != null) {
337                 if (DBG) {
338                     Log.d(TAG, "Remote PbapSupportedFeatures " + mPseRec.getSupportedFeatures());
339                 }
340 
341                 ObexAppParameters oap = new ObexAppParameters();
342 
343                 if (mPseRec.getProfileVersion() >= PBAP_V1_2) {
344                     oap.add(BluetoothPbapRequest.OAP_TAGID_PBAP_SUPPORTED_FEATURES,
345                             PBAP_SUPPORTED_FEATURE);
346                 }
347 
348                 oap.addToHeaderSet(connectionRequest);
349             }
350             HeaderSet connectionResponse = mObexSession.connect(connectionRequest);
351 
352             connectionSuccessful =
353                     (connectionResponse.getResponseCode() == ResponseCodes.OBEX_HTTP_OK);
354             if (DBG) {
355                 Log.d(TAG, "Success = " + Boolean.toString(connectionSuccessful));
356             }
357         } catch (IOException | NullPointerException e) {
358             // Will get NPE if a null mSocket is passed to BluetoothObexTransport.
359             // mSocket can be set to null if an abort() --> closeSocket() was called between
360             // the calls to connectSocket() and connectObexSession().
361             Log.w(TAG, "CONNECT Failure " + e.toString());
362             closeSocket();
363         }
364         return connectionSuccessful;
365     }
366 
abort()367     public void abort() {
368         // Perform forced cleanup, it is ok if the handler throws an exception this will free the
369         // handler to complete what it is doing and finish with cleanup.
370         closeSocket();
371         this.getLooper().getThread().interrupt();
372     }
373 
closeSocket()374     private synchronized void closeSocket() {
375         try {
376             if (mSocket != null) {
377                 if (DBG) {
378                     Log.d(TAG, "Closing socket" + mSocket);
379                 }
380                 mSocket.close();
381                 mSocket = null;
382             }
383         } catch (IOException e) {
384             Log.e(TAG, "Error when closing socket", e);
385             mSocket = null;
386         }
387     }
388 
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 
downloadCallLog(String path, HashMap<String, Integer> callCounter)442     void downloadCallLog(String path, HashMap<String, Integer> callCounter) {
443         try {
444             BluetoothPbapRequestPullPhoneBook request =
445                     new BluetoothPbapRequestPullPhoneBook(path, mAccount, 0, VCARD_TYPE_30, 0, 0);
446             request.execute(mObexSession);
447             CallLogPullRequest processor =
448                     new CallLogPullRequest(mPbapClientStateMachine.getContext(), path,
449                         callCounter, mAccount);
450             processor.setResults(request.getList());
451             processor.onPullComplete();
452         } catch (IOException e) {
453             Log.w(TAG, "Download call log failure");
454         }
455     }
456 
addAccount(Account account)457     private boolean addAccount(Account account) {
458         if (mAccountManager.addAccountExplicitly(account, null, null)) {
459             if (DBG) {
460                 Log.d(TAG, "Added account " + mAccount);
461             }
462             return true;
463         }
464         return false;
465     }
466 
removeAccount(Account account)467     private void removeAccount(Account account) {
468         if (mAccountManager.removeAccountExplicitly(account)) {
469             if (DBG) {
470                 Log.d(TAG, "Removed account " + account);
471             }
472         } else {
473             Log.e(TAG, "Failed to remove account " + mAccount);
474         }
475     }
476 
removeCallLog(Account account)477     private void removeCallLog(Account account) {
478         try {
479             // need to check call table is exist ?
480             if (mContext.getContentResolver() == null) {
481                 if (DBG) {
482                     Log.d(TAG, "CallLog ContentResolver is not found");
483                 }
484                 return;
485             }
486             mContext.getContentResolver().delete(CallLog.Calls.CONTENT_URI,
487                     Calls.PHONE_ACCOUNT_ID + "=?", new String[]{mAccount.name});
488         } catch (IllegalArgumentException e) {
489             Log.d(TAG, "Call Logs could not be deleted, they may not exist yet.");
490         }
491     }
492 
isRepositorySupported(int mask)493     private boolean isRepositorySupported(int mask) {
494         if (mPseRec == null) {
495             if (VDBG) Log.v(TAG, "No PBAP Server SDP Record");
496             return false;
497         }
498         return (mask & mPseRec.getSupportedRepositories()) != 0;
499     }
500 }
501