• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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 static android.Manifest.permission.BLUETOOTH_CONNECT;
20 import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
21 import static android.bluetooth.BluetoothProfile.STATE_CONNECTED;
22 import static android.bluetooth.BluetoothProfile.STATE_CONNECTING;
23 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;
24 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING;
25 
26 import android.accounts.Account;
27 import android.bluetooth.BluetoothDevice;
28 import android.bluetooth.BluetoothPbapClient;
29 import android.bluetooth.BluetoothProfile;
30 import android.bluetooth.BluetoothUuid;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.os.Looper;
34 import android.os.Message;
35 import android.util.Log;
36 
37 import com.android.bluetooth.Utils;
38 import com.android.bluetooth.btservice.AdapterService;
39 import com.android.bluetooth.btservice.MetricsLogger;
40 import com.android.bluetooth.btservice.ProfileService;
41 import com.android.internal.annotations.VisibleForTesting;
42 import com.android.internal.util.State;
43 import com.android.internal.util.StateMachine;
44 import com.android.obex.ResponseCodes;
45 
46 import java.util.ArrayList;
47 import java.util.HashMap;
48 import java.util.List;
49 import java.util.Map;
50 
51 /**
52  * This object represents a connection over PBAP with a given remote device. It manages the account,
53  * SDP Record and PBAP OBEX Client for the remote device. It also uses the OBEX client to make
54  * simple requests, driving the overall contact download process.
55  */
56 class PbapClientStateMachine extends StateMachine {
57     private static final String TAG = PbapClientStateMachine.class.getSimpleName();
58 
59     // Messages for handling connect/disconnect requests.
60     private static final int MSG_CONNECT = 1;
61     private static final int MSG_DISCONNECT = 2;
62     private static final int MSG_SDP_COMPLETE = 3;
63     private static final int MSG_OBEX_CLIENT_CONNECTED = 4;
64     private static final int MSG_OBEX_CLIENT_DISCONNECTED = 5;
65     private static final int MSG_STORAGE_READY = 6;
66     private static final int MSG_ACCOUNT_ADDED = 7;
67     private static final int MSG_ACCOUNT_REMOVED = 8;
68     private static final int MSG_DOWNLOAD = 9;
69     private static final int MSG_PHONEBOOK_METADATA_RECEIVED = 10;
70     private static final int MSG_PHONEBOOK_CONTACTS_RECEIVED = 11;
71 
72     // Messages for handling error conditions.
73     public static final int MSG_CONNECT_TIMEOUT = 12;
74     public static final int MSG_DISCONNECT_TIMEOUT = 13;
75     private static final int MSG_SDP_FAILED = 14;
76 
77     // Configurable Timeouts
78     @VisibleForTesting static final int CONNECT_TIMEOUT_MS = 12000;
79     @VisibleForTesting static final int DISCONNECT_TIMEOUT_MS = 3000;
80 
81     // Constants for SDP. Note that these values come from the native stack, but no centralized
82     // constants exist for them as part of the various SDP APIs.
83     private static final int SDP_SUCCESS = 0;
84     private static final int SDP_FAILED = 1;
85     private static final int SDP_BUSY = 2;
86 
87     // Supported features of our OBEX client
88     private static final int LOCAL_SUPPORTED_FEATURES =
89             PbapSdpRecord.FEATURE_DOWNLOADING | PbapSdpRecord.FEATURE_DEFAULT_IMAGE_FORMAT;
90 
91     // Default configuration for VCard format -> prefer 3.0 to 2.1
92     private static final byte DEFAULT_VCARD_VERSION = PbapPhonebook.FORMAT_VCARD_30;
93 
94     // Default property filter for downloaded contacts
95     private static final long DEFAULT_PROPERTIES =
96             PbapApplicationParameters.PROPERTY_VERSION
97                     | PbapApplicationParameters.PROPERTY_FN
98                     | PbapApplicationParameters.PROPERTY_N
99                     | PbapApplicationParameters.PROPERTY_PHOTO
100                     | PbapApplicationParameters.PROPERTY_ADR
101                     | PbapApplicationParameters.PROPERTY_TEL
102                     | PbapApplicationParameters.PROPERTY_EMAIL
103                     | PbapApplicationParameters.PROPERTY_NICKNAME;
104 
105     // Our internal batch size when downloading contacts. Batches let us deliver contacts sooner to
106     // the UI and applications that want contacts, and make our individual download operations
107     // shorter running, but come with the trade off of a greater overall time to download.
108     private static final int CONTACT_DOWNLOAD_BATCH_SIZE = 250;
109 
110     /** A Callback interface for object creators to get events related to this state machine */
111     interface Callback {
112         /**
113          * Receive connection state changes for this state machines so you can know when to clean it
114          * up.
115          *
116          * @param oldState The old state of the device state machine
117          * @param newState The new state of the device state machine
118          */
onConnectionStateChanged(int oldState, int newState)119         void onConnectionStateChanged(int oldState, int newState);
120     }
121 
122     /**
123      * Internal Phonebook object to help drive downloads with batching and record download process
124      * metrics.
125      */
126     private static final class Phonebook {
127         private final String mName;
128         private PbapPhonebookMetadata mMetadata;
129         private int mNumDownloaded;
130 
Phonebook(String name)131         Phonebook(String name) {
132             mName = name;
133             mMetadata = null;
134             mNumDownloaded = 0;
135         }
136 
setMetadata(PbapPhonebookMetadata metadata)137         public void setMetadata(PbapPhonebookMetadata metadata) {
138             mMetadata = metadata;
139         }
140 
onContactsDownloaded(int numDownloaded)141         public void onContactsDownloaded(int numDownloaded) {
142             mNumDownloaded += numDownloaded;
143         }
144 
getTotalNumberOfContacts()145         public int getTotalNumberOfContacts() {
146             return (mMetadata == null || mMetadata.size() == PbapPhonebookMetadata.INVALID_SIZE)
147                     ? 0
148                     : mMetadata.size();
149         }
150 
getNumberOfContactsDownloaded()151         public int getNumberOfContactsDownloaded() {
152             return mNumDownloaded;
153         }
154 
155         @Override
156         @SuppressWarnings("ReferenceEquality") // equals() doesn't work because the constant is null
toString()157         public String toString() {
158             if (mMetadata == null) {
159                 return mName
160                         + " ["
161                         + getNumberOfContactsDownloaded()
162                         + "/ UNKNOWN] (db:UNKNOWN, pc:UNKNOWN, sc:UNKNOWN)";
163             }
164 
165             String databaseIdentifier = mMetadata.databaseIdentifier();
166             if (databaseIdentifier == PbapPhonebookMetadata.INVALID_DATABASE_IDENTIFIER) {
167                 databaseIdentifier = "UNKNOWN";
168             }
169 
170             String primaryVersionCounter = mMetadata.primaryVersionCounter();
171             if (primaryVersionCounter == PbapPhonebookMetadata.INVALID_VERSION_COUNTER) {
172                 primaryVersionCounter = "UNKNOWN";
173             }
174 
175             String secondaryVersionCounter = mMetadata.secondaryVersionCounter();
176             if (secondaryVersionCounter == PbapPhonebookMetadata.INVALID_VERSION_COUNTER) {
177                 secondaryVersionCounter = "UNKNOWN";
178             }
179 
180             String totalContactsExpected = "UNKNOWN";
181             if (mMetadata.size() != PbapPhonebookMetadata.INVALID_SIZE) {
182                 totalContactsExpected = Integer.toString(mMetadata.size());
183             }
184 
185             return mName
186                     + " ["
187                     + (getNumberOfContactsDownloaded() + "/" + totalContactsExpected)
188                     + "] ("
189                     + ("db:" + databaseIdentifier)
190                     + (", pc:" + primaryVersionCounter)
191                     + (", sc:" + secondaryVersionCounter)
192                     + ")";
193         }
194     }
195 
196     private final BluetoothDevice mDevice;
197     private final Context mContext;
198     private PbapSdpRecord mSdpRecord = null;
199     private final Account mAccount;
200     private final Map<String, Phonebook> mPhonebooks = new HashMap<String, Phonebook>();
201     private final PbapClientObexClient mObexClient;
202     private final PbapClientContactsStorage mContactsStorage;
203 
204     private final PbapClientContactsStorage.Callback mStorageCallback =
205             new PbapClientContactsStorage.Callback() {
206                 @Override
207                 public void onStorageReady() {
208                     onPbapClientStorageReady();
209                 }
210 
211                 @Override
212                 public void onStorageAccountsChanged(
213                         List<Account> oldAccounts, List<Account> newAccounts) {
214                     boolean inOld = oldAccounts.contains(mAccount);
215                     boolean inNew = newAccounts.contains(mAccount);
216                     if (!inOld && inNew) {
217                         Log.i(TAG, "Storage accounts changed, account added");
218                         onPbapClientAccountAdded();
219                     } else if (inOld && !inNew) {
220                         Log.i(TAG, "Storage accounts changed, account removed");
221                         onPbapClientAccountRemoved();
222                     } else {
223                         Log.i(TAG, "Storage accounts changed, but no impact to our account");
224                     }
225                 }
226             };
227 
228     private int mCurrentState = STATE_DISCONNECTED;
229     private State mDisconnected;
230     private State mConnecting;
231     private State mConnected;
232     private State mDownloading;
233     private State mDisconnecting;
234 
235     private final Callback mCallback;
236 
PbapClientStateMachine( BluetoothDevice device, PbapClientContactsStorage storage, Context context, Callback callback)237     PbapClientStateMachine(
238             BluetoothDevice device,
239             PbapClientContactsStorage storage,
240             Context context,
241             Callback callback) {
242         super(TAG);
243 
244         mDevice = device;
245         mContext = context;
246         mContactsStorage = storage;
247         mCallback = callback;
248         mAccount = mContactsStorage.getStorageAccountForDevice(mDevice);
249         mObexClient =
250                 new PbapClientObexClient(
251                         device, LOCAL_SUPPORTED_FEATURES, new PbapClientObexClientCallback());
252 
253         initializeStates();
254     }
255 
256     @VisibleForTesting
PbapClientStateMachine( BluetoothDevice device, PbapClientContactsStorage storage, Context context, Looper looper, Callback callback, PbapClientObexClient obexClient)257     PbapClientStateMachine(
258             BluetoothDevice device,
259             PbapClientContactsStorage storage,
260             Context context,
261             Looper looper,
262             Callback callback,
263             PbapClientObexClient obexClient) {
264         super(TAG, looper);
265 
266         mDevice = device;
267         mContext = context;
268         mContactsStorage = storage;
269         mCallback = callback;
270         mAccount = mContactsStorage.getStorageAccountForDevice(mDevice);
271         mObexClient = obexClient;
272 
273         initializeStates();
274     }
275 
initializeStates()276     private void initializeStates() {
277         mDisconnected = new Disconnected();
278         mConnecting = new Connecting();
279         mDisconnecting = new Disconnecting();
280         mConnected = new Connected();
281         mDownloading = new Downloading();
282 
283         addState(mDisconnected);
284         addState(mConnecting);
285         addState(mDisconnecting);
286         addState(mConnected);
287         addState(mDownloading, mConnected);
288 
289         setInitialState(mDisconnected);
290     }
291 
292     /** Request to connect the device this state machine represents */
connect()293     public void connect() {
294         debug("connect requested");
295         sendMessage(MSG_CONNECT);
296     }
297 
298     /** Request to disconnect the device this state machine represents */
disconnect()299     public void disconnect() {
300         debug("disconnect requested");
301         sendMessage(MSG_DISCONNECT);
302     }
303 
304     /** Request to start the contacts download procedure */
download()305     private void download() {
306         sendMessage(MSG_DOWNLOAD);
307     }
308 
309     /** Notify this device state machine of a newly received SDP record */
onSdpResultReceived(int status, PbapSdpRecord record)310     public void onSdpResultReceived(int status, PbapSdpRecord record) {
311         if (status != SDP_SUCCESS) {
312             sendMessage(MSG_SDP_FAILED, status);
313         } else {
314             sendMessage(MSG_SDP_COMPLETE, record);
315         }
316     }
317 
318     /** Notify this device state machine of a newly added device account */
onPbapClientStorageReady()319     private void onPbapClientStorageReady() {
320         obtainMessage(MSG_STORAGE_READY).sendToTarget();
321     }
322 
323     /** Notify this device state machine of a newly added device account */
onPbapClientAccountAdded()324     private void onPbapClientAccountAdded() {
325         obtainMessage(MSG_ACCOUNT_ADDED).sendToTarget();
326     }
327 
328     /** Notify this device state machine of that its device account was removed */
onPbapClientAccountRemoved()329     private void onPbapClientAccountRemoved() {
330         obtainMessage(MSG_ACCOUNT_REMOVED).sendToTarget();
331     }
332 
333     /** Notify this device state machine of downloaded metadata from our OBEX client */
onPhonebookMetadataReceived(PbapPhonebookMetadata metadata)334     private void onPhonebookMetadataReceived(PbapPhonebookMetadata metadata) {
335         obtainMessage(MSG_PHONEBOOK_METADATA_RECEIVED, metadata).sendToTarget();
336     }
337 
338     /** Notify this device state machine that a download metadata request failed */
onPhonebookMetadataDownloadFailed(String phonebook)339     private void onPhonebookMetadataDownloadFailed(String phonebook) {
340         PbapPhonebookMetadata emptyMetadata =
341                 new PbapPhonebookMetadata(phonebook, 0, null, null, null);
342         obtainMessage(MSG_PHONEBOOK_METADATA_RECEIVED, emptyMetadata).sendToTarget();
343     }
344 
345     /** Notify this device state machine of downloaded contacts from our OBEX client */
onPhonebookContactsReceived(PbapPhonebook contacts)346     private void onPhonebookContactsReceived(PbapPhonebook contacts) {
347         obtainMessage(MSG_PHONEBOOK_CONTACTS_RECEIVED, contacts).sendToTarget();
348     }
349 
350     /** Notify this device state machine that a download contacts request failed */
onPhonebookContactsDownloadFailed(String phonebook)351     private void onPhonebookContactsDownloadFailed(String phonebook) {
352         PbapPhonebook emptyContacts = new PbapPhonebook(phonebook);
353         obtainMessage(MSG_PHONEBOOK_CONTACTS_RECEIVED, emptyContacts).sendToTarget();
354     }
355 
356     /** Get the current connection state */
getConnectionState()357     public int getConnectionState() {
358         return mCurrentState;
359     }
360 
361     class Disconnected extends State {
362         @Override
enter()363         public void enter() {
364             debug("Disconnected: Enter, from=" + eventToString(getCurrentMessage().what));
365             if (mCurrentState != STATE_DISCONNECTED) {
366                 // Only broadcast a state change that came from something other than disconnected
367                 onConnectionStateChanged(STATE_DISCONNECTED);
368 
369                 // Quit processing on this handler. This makes this object one time use. The
370                 // connection state changed callback event will trigger the service to clean up
371                 // their state machine reference if they still have one.
372                 quit();
373             }
374         }
375 
376         @Override
processMessage(Message message)377         public boolean processMessage(Message message) {
378             debug("Disconnected: process message, what=" + eventToString(message.what));
379             switch (message.what) {
380                 case MSG_CONNECT:
381                     transitionTo(mConnecting);
382                     break;
383                 default:
384                     warn(
385                             "Disconnected: Received unhandled message, what="
386                                     + eventToString(message.what));
387                     return NOT_HANDLED;
388             }
389             return true;
390         }
391     }
392 
393     class Connecting extends State {
394         @Override
enter()395         public void enter() {
396             debug("Connecting: Enter from=" + eventToString(getCurrentMessage().what));
397             onConnectionStateChanged(STATE_CONNECTING);
398 
399             // We can't connect over OBEX until we known where/how to connect. We need the SDP
400             // record details to do this. Thus, being connected means we received a valid SDP record
401             // and properly connected our OBEX Client afterwards.
402             mDevice.sdpSearch(BluetoothUuid.PBAP_PSE);
403 
404             // Wait up to CONNECT_TIMEOUT for SDP to complete and our OBEX client to connect
405             sendMessageDelayed(MSG_CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS);
406         }
407 
408         @Override
processMessage(Message message)409         public boolean processMessage(Message message) {
410             debug("Connecting: process message, what=" + eventToString(message.what));
411             switch (message.what) {
412                 case MSG_DISCONNECT:
413                     transitionTo(mDisconnecting);
414                     break;
415 
416                 case MSG_OBEX_CLIENT_CONNECTED:
417                     transitionTo(mConnected);
418                     break;
419 
420                 case MSG_OBEX_CLIENT_DISCONNECTED:
421                 case MSG_CONNECT_TIMEOUT:
422                     transitionTo(mDisconnecting);
423                     break;
424 
425                 case MSG_SDP_FAILED:
426                     int failureCode = message.arg1;
427                     info("Connecting: SDP unsuccessful, code=" + sdpCodeToString(failureCode));
428                     if (failureCode == SDP_BUSY) {
429                         mDevice.sdpSearch(BluetoothUuid.PBAP_PSE);
430                     } else {
431                         transitionTo(mDisconnecting);
432                     }
433                     break;
434 
435                 case MSG_SDP_COMPLETE:
436                     mSdpRecord = (PbapSdpRecord) message.obj;
437 
438                     info("Connecting: received SDP record, record=" + mSdpRecord);
439 
440                     if (!mDevice.equals(mSdpRecord.getDevice())) {
441                         warn("Connecting: received SDP record for improper device. Ignoring.");
442                         return HANDLED;
443                     }
444 
445                     // Use SDP contents to determine whether we connect on L2CAP or RFCOMM
446                     if (mSdpRecord.getL2capPsm() != /* L2CAP_INVALID_PSM */ -1) {
447                         mObexClient.connectL2cap(mSdpRecord.getL2capPsm());
448                     } else if (mSdpRecord.getRfcommChannelNumber()
449                             != /* RFCOMM_INVALID_CHANNEL */ -1) {
450                         mObexClient.connectRfcomm(mSdpRecord.getRfcommChannelNumber());
451                     } else {
452                         error("Connecting: Record didn't contain a valid L2CAP PSM/RFCOMM channel");
453                         mDevice.sdpSearch(BluetoothUuid.PBAP_PSE);
454                     }
455 
456                     if (mSdpRecord.isRepositorySupported(PbapSdpRecord.REPOSITORY_FAVORITES)) {
457                         mPhonebooks.put(
458                                 PbapPhonebook.FAVORITES_PATH,
459                                 new Phonebook(PbapPhonebook.FAVORITES_PATH));
460                     }
461                     if (mSdpRecord.isRepositorySupported(
462                             PbapSdpRecord.REPOSITORY_LOCAL_PHONEBOOK)) {
463                         mPhonebooks.put(
464                                 PbapPhonebook.LOCAL_PHONEBOOK_PATH,
465                                 new Phonebook(PbapPhonebook.LOCAL_PHONEBOOK_PATH));
466                         mPhonebooks.put(
467                                 PbapPhonebook.MCH_PATH, new Phonebook(PbapPhonebook.MCH_PATH));
468                         mPhonebooks.put(
469                                 PbapPhonebook.ICH_PATH, new Phonebook(PbapPhonebook.ICH_PATH));
470                         mPhonebooks.put(
471                                 PbapPhonebook.OCH_PATH, new Phonebook(PbapPhonebook.OCH_PATH));
472                     }
473                     if (mSdpRecord.isRepositorySupported(PbapSdpRecord.REPOSITORY_SIM_CARD)) {
474                         mPhonebooks.put(
475                                 PbapPhonebook.SIM_PHONEBOOK_PATH,
476                                 new Phonebook(PbapPhonebook.SIM_PHONEBOOK_PATH));
477                         mPhonebooks.put(
478                                 PbapPhonebook.SIM_MCH_PATH,
479                                 new Phonebook(PbapPhonebook.SIM_MCH_PATH));
480                         mPhonebooks.put(
481                                 PbapPhonebook.SIM_ICH_PATH,
482                                 new Phonebook(PbapPhonebook.SIM_ICH_PATH));
483                         mPhonebooks.put(
484                                 PbapPhonebook.SIM_OCH_PATH,
485                                 new Phonebook(PbapPhonebook.SIM_OCH_PATH));
486                     }
487                     break;
488 
489                 default:
490                     warn(
491                             "Connecting: Received unhandled message, what="
492                                     + eventToString(message.what));
493                     return NOT_HANDLED;
494             }
495             return HANDLED;
496         }
497 
498         @Override
exit()499         public void exit() {
500             removeMessages(MSG_CONNECT_TIMEOUT);
501         }
502     }
503 
504     class Connected extends State {
505         private boolean mHasDownloaded = false;
506 
507         @Override
enter()508         public void enter() {
509             debug("Connected: Enter, from=" + eventToString(getCurrentMessage().what));
510             if (mCurrentState != STATE_CONNECTING) {
511                 return;
512             }
513 
514             onConnectionStateChanged(STATE_CONNECTED);
515 
516             mHasDownloaded = false;
517 
518             mContactsStorage.registerCallback(mStorageCallback);
519             if (mContactsStorage.isStorageReady()) {
520                 onPbapClientStorageReady();
521             } else {
522                 Log.i(TAG, "Awaiting storage to be ready");
523             }
524         }
525 
526         @Override
processMessage(Message message)527         public boolean processMessage(Message message) {
528             debug("Connected: process message, what=" + eventToString(message.what));
529             switch (message.what) {
530                 case MSG_OBEX_CLIENT_DISCONNECTED:
531                 case MSG_DISCONNECT:
532                     transitionTo(mDisconnecting);
533                     break;
534 
535                 case MSG_STORAGE_READY:
536                     if (mContactsStorage.getStorageAccounts().contains(mAccount)) {
537                         info("Connected: Account already exists, time to download");
538                         if (!mHasDownloaded) {
539                             download();
540                             mHasDownloaded = true;
541                         }
542                     } else {
543                         info("Connected: Account not found. Requesting to add it.");
544                         mContactsStorage.addAccount(mAccount);
545                     }
546                     break;
547 
548                 case MSG_ACCOUNT_ADDED:
549                     info("Connected: account was added, time to download");
550                     if (!mHasDownloaded) {
551                         download();
552                         mHasDownloaded = true;
553                     }
554                     break;
555 
556                 case MSG_ACCOUNT_REMOVED:
557                     info("Connected: account was removed, time to disconnect");
558                     transitionTo(mDisconnecting);
559                     break;
560 
561                 case MSG_DOWNLOAD:
562                     transitionTo(mDownloading);
563                     break;
564 
565                 default:
566                     warn(
567                             "Connected: received unhandled message, what="
568                                     + eventToString(message.what));
569                     return NOT_HANDLED;
570             }
571             return HANDLED;
572         }
573     }
574 
575     class Downloading extends State {
576         List<String> mPhonebooksToDownload = new ArrayList<String>();
577 
578         @Override
enter()579         public void enter() {
580 
581             info("Downloading: Start download process");
582 
583             // Initialize our list of phonebooks to download based on supported repositories
584             initializePhonebooksToDownload();
585 
586             String currentPhonebook = getCurrentPhonebook();
587             if (currentPhonebook != null) {
588                 downloadPhonebookMetadata(currentPhonebook);
589             } else {
590                 warn("Downloading: no supported repositories to download");
591                 transitionTo(mConnected);
592             }
593         }
594 
595         @Override
processMessage(Message message)596         public boolean processMessage(Message message) {
597             String currentPhonebook = getCurrentPhonebook();
598             String phonebook = null;
599             debug("Downloading: process message, what=" + eventToString(message.what));
600             switch (message.what) {
601                 case MSG_DISCONNECT:
602                     transitionTo(mDisconnecting);
603                     break;
604 
605                 case MSG_PHONEBOOK_METADATA_RECEIVED:
606                     PbapPhonebookMetadata metadata = (PbapPhonebookMetadata) message.obj;
607                     phonebook = metadata.phonebook();
608                     if (currentPhonebook != null && currentPhonebook.equals(phonebook)) {
609                         info("Downloading: received metadata=" + metadata);
610 
611                         // Process Metadata
612                         mPhonebooks.get(phonebook).setMetadata(metadata);
613 
614                         // If phonebook has contacts, begin downloading them
615                         if (metadata.size() > 0) {
616                             downloadPhonebook(currentPhonebook, 0, CONTACT_DOWNLOAD_BATCH_SIZE);
617                         } else {
618                             warn(
619                                     "Downloading: no contacts for phonebook="
620                                             + currentPhonebook
621                                             + ", skipping");
622                             setNextPhonebookOrComplete();
623                             break;
624                         }
625                     } else {
626                         warn(
627                                 "Downloading: dropped metadata event for phonebook="
628                                         + phonebook
629                                         + ", current="
630                                         + currentPhonebook);
631                     }
632                     break;
633 
634                 case MSG_PHONEBOOK_CONTACTS_RECEIVED:
635                     PbapPhonebook contacts = (PbapPhonebook) message.obj;
636                     phonebook = contacts.getPhonebook();
637                     if (currentPhonebook != null && currentPhonebook.equals(phonebook)) {
638                         int numReceived = contacts.getCount();
639                         mPhonebooks.get(phonebook).onContactsDownloaded(numReceived);
640                         int totalContactDownloaded =
641                                 mPhonebooks.get(phonebook).getNumberOfContactsDownloaded();
642                         int totalContactsExpected =
643                                 mPhonebooks.get(phonebook).getTotalNumberOfContacts();
644 
645                         info(
646                                 "Downloading: received contacts, phonebook="
647                                         + phonebook
648                                         + ", entries="
649                                         + numReceived
650                                         + ", total="
651                                         + totalContactDownloaded
652                                         + "/"
653                                         + totalContactsExpected);
654                         if (numReceived != 0) {
655                             storeDownloadedContacts(phonebook, contacts);
656                         } else {
657                             warn(
658                                     "Downloading: contacts empty for phonebook="
659                                             + phonebook
660                                             + ", proceed to next phonebook");
661                             setNextPhonebookOrComplete();
662                             break;
663                         }
664 
665                         if (totalContactDownloaded >= totalContactsExpected) {
666                             info("Downloading: download complete, phonebook=" + phonebook);
667                             setNextPhonebookOrComplete();
668                         } else {
669                             downloadPhonebook(
670                                     currentPhonebook,
671                                     totalContactDownloaded,
672                                     CONTACT_DOWNLOAD_BATCH_SIZE);
673                         }
674                     } else {
675                         warn("Downloading: dropped received contacts, phonebook=" + phonebook);
676                     }
677                     break;
678 
679                 default:
680                     debug(
681                             "Downloading: passing message to parent state, type="
682                                     + eventToString(message.what));
683                     return NOT_HANDLED;
684             }
685             return HANDLED;
686         }
687 
688         /* Initialize our prioritized list of phonebooks we want to download */
initializePhonebooksToDownload()689         private void initializePhonebooksToDownload() {
690             mPhonebooksToDownload.clear();
691 
692             if (mPhonebooks.containsKey(PbapPhonebook.FAVORITES_PATH)) {
693                 mPhonebooksToDownload.add(PbapPhonebook.FAVORITES_PATH);
694             }
695             if (mPhonebooks.containsKey(PbapPhonebook.LOCAL_PHONEBOOK_PATH)) {
696                 mPhonebooksToDownload.add(PbapPhonebook.LOCAL_PHONEBOOK_PATH);
697             }
698             if (mPhonebooks.containsKey(PbapPhonebook.SIM_PHONEBOOK_PATH)) {
699                 mPhonebooksToDownload.add(PbapPhonebook.SIM_PHONEBOOK_PATH);
700             }
701             if (mPhonebooks.containsKey(PbapPhonebook.MCH_PATH)) {
702                 mPhonebooksToDownload.add(PbapPhonebook.MCH_PATH);
703             }
704             if (mPhonebooks.containsKey(PbapPhonebook.ICH_PATH)) {
705                 mPhonebooksToDownload.add(PbapPhonebook.ICH_PATH);
706             }
707             if (mPhonebooks.containsKey(PbapPhonebook.OCH_PATH)) {
708                 mPhonebooksToDownload.add(PbapPhonebook.OCH_PATH);
709             }
710             if (mPhonebooks.containsKey(PbapPhonebook.SIM_MCH_PATH)) {
711                 mPhonebooksToDownload.add(PbapPhonebook.SIM_MCH_PATH);
712             }
713             if (mPhonebooks.containsKey(PbapPhonebook.SIM_ICH_PATH)) {
714                 mPhonebooksToDownload.add(PbapPhonebook.SIM_ICH_PATH);
715             }
716             if (mPhonebooks.containsKey(PbapPhonebook.SIM_OCH_PATH)) {
717                 mPhonebooksToDownload.add(PbapPhonebook.SIM_OCH_PATH);
718             }
719 
720             info("Downloading: initialized download process, phonebooks=" + mPhonebooksToDownload);
721         }
722 
723         /* Get the currently downloading/processing phonebook path */
getCurrentPhonebook()724         private String getCurrentPhonebook() {
725             return mPhonebooksToDownload.size() != 0 ? mPhonebooksToDownload.get(0) : null;
726         }
727 
728         /*
729          * Complete operation on one phonebook and update to the next one, if available.
730          *
731          * <p>If there's further phonebooks to download, this will trigger the process to download
732          * the next phonebook. If there are no more phonebooks to download, this will return us to
733          * the Connected state.
734          */
setNextPhonebookOrComplete()735         private void setNextPhonebookOrComplete() {
736             String currentPhonebook = getCurrentPhonebook();
737             if (currentPhonebook == null) {
738                 warn("Downloading: No phonebooks left to download");
739                 transitionTo(mConnected);
740                 return;
741             }
742 
743             mPhonebooksToDownload.remove(0);
744             if (mPhonebooksToDownload.size() != 0) {
745                 String nextPhonebook = getCurrentPhonebook();
746                 debug(
747                         "Downloading: Phonebook changed, old="
748                                 + currentPhonebook
749                                 + ", new="
750                                 + nextPhonebook);
751                 downloadPhonebookMetadata(nextPhonebook);
752             } else {
753                 info("Downloading: All phonebooks downloaded");
754                 transitionTo(mConnected);
755             }
756         }
757 
758         /*
759          * Request the size and version counters for a specific phonebook, by path.
760          *
761          * <p>Downloads are in two parts. First we get the metadata and then we use that to create
762          * batches to download. Downloaded contacts are handed to the Contacts Client for storage
763          */
downloadPhonebookMetadata(String path)764         private void downloadPhonebookMetadata(String path) {
765             info("Downloading: Request metadata, phonebook=" + path);
766             mObexClient.requestPhonebookMetadata(
767                     path,
768                     new PbapApplicationParameters(
769                             DEFAULT_PROPERTIES,
770                             DEFAULT_VCARD_VERSION,
771                             PbapApplicationParameters.RETURN_SIZE_ONLY,
772                             0));
773         }
774 
775         /*
776          * Download a specific phonebook, by path, using the given batching parameters
777          *
778          * <p>Downloads are in two parts. First we get the metadata and then we use that to create
779          * batches to download. Downloaded contacts are handed to the Contacts Client for storage
780          */
downloadPhonebook(String path, int batchStart, int numToFetch)781         private void downloadPhonebook(String path, int batchStart, int numToFetch) {
782             int batchEnd = (batchStart + numToFetch - 1);
783             info(
784                     "Downloading: Download contents, phonebook="
785                             + path
786                             + ", start="
787                             + batchStart
788                             + ", end="
789                             + batchEnd);
790 
791             PbapApplicationParameters params =
792                     new PbapApplicationParameters(
793                             DEFAULT_PROPERTIES, DEFAULT_VCARD_VERSION, numToFetch, batchStart);
794             mObexClient.requestDownloadPhonebook(mPhonebooksToDownload.get(0), params);
795         }
796     }
797 
798     class Disconnecting extends State {
799         @Override
enter()800         public void enter() {
801             debug("Disconnecting: Enter, from=" + eventToString(getCurrentMessage().what));
802             onConnectionStateChanged(STATE_DISCONNECTING);
803 
804             // Disconnect
805             if (mObexClient.getConnectionState() != STATE_DISCONNECTED) {
806                 mObexClient.disconnect();
807                 sendMessageDelayed(MSG_DISCONNECT_TIMEOUT, DISCONNECT_TIMEOUT_MS);
808             } else {
809                 transitionTo(mDisconnected);
810             }
811         }
812 
813         @Override
processMessage(Message message)814         public boolean processMessage(Message message) {
815             debug("Disconnecting: process message, what=" + eventToString(message.what));
816             switch (message.what) {
817                 case MSG_OBEX_CLIENT_DISCONNECTED:
818                     removeMessages(MSG_DISCONNECT_TIMEOUT);
819                     transitionTo(mDisconnected);
820                     break;
821 
822                 case MSG_DISCONNECT:
823                     deferMessage(message);
824                     break;
825 
826                 case MSG_DISCONNECT_TIMEOUT:
827                     warn("Disconnecting: Timeout, Forcing");
828                     mObexClient.close();
829                     transitionTo(mDisconnected);
830                     break;
831 
832                 default:
833                     warn(
834                             "Disconnecting: Received unhandled message, what="
835                                     + eventToString(message.what));
836                     return NOT_HANDLED;
837             }
838             return HANDLED;
839         }
840 
841         @Override
exit()842         public void exit() {
843             mContactsStorage.unregisterCallback(mStorageCallback);
844 
845             // Always remove data as a last step
846             cleanup();
847         }
848     }
849 
850     /*
851      * Force this state machine to stop immediately
852      *
853      * <p>This function quits the state machine operation by broadcasting the proper connection
854      * state changes and properly cleaning up data that may be exist.
855      */
856     @Override
onQuitting()857     protected void onQuitting() {
858         Log.d(TAG, "State machine is force quitting");
859         switch (mCurrentState) {
860             case STATE_CONNECTED:
861                 onConnectionStateChanged(STATE_DISCONNECTING);
862                 // intentional fallthrough-- we want to broadcast both state changes
863             case STATE_CONNECTING:
864             case STATE_DISCONNECTING:
865                 onConnectionStateChanged(STATE_DISCONNECTED);
866                 cleanup();
867                 break;
868             default:
869                 Log.i(TAG, "Force quit a disconnected state machine. No state to broadcast");
870         }
871     }
872 
cleanup()873     private void cleanup() {
874         info("cleanup: evaluate data to cleanup");
875         cleanupContactsDataAndAccounts();
876     }
877 
cleanupContactsDataAndAccounts()878     private void cleanupContactsDataAndAccounts() {
879         info("cleanupContactsDataAndAccounts: clear saved contacts, call history and account");
880         mContactsStorage.removeAllContacts(mAccount);
881         mContactsStorage.removeCallHistory(mAccount);
882         mContactsStorage.removeAccount(mAccount);
883     }
884 
885     /* Request to insert downloaded contacts into storage */
storeDownloadedContacts(String phonebook, PbapPhonebook contacts)886     private void storeDownloadedContacts(String phonebook, PbapPhonebook contacts) {
887         info("Request to store contacts for phonebook=" + phonebook);
888         if (phonebook.equals(PbapPhonebook.FAVORITES_PATH)) {
889             mContactsStorage.insertFavorites(mAccount, contacts.getList());
890         } else if (phonebook.equals(PbapPhonebook.LOCAL_PHONEBOOK_PATH)) {
891             mContactsStorage.insertLocalContacts(mAccount, contacts.getList());
892         } else if (phonebook.equals(PbapPhonebook.SIM_PHONEBOOK_PATH)) {
893             mContactsStorage.insertSimContacts(mAccount, contacts.getList());
894         } else if (phonebook.equals(PbapPhonebook.MCH_PATH)
895                 || phonebook.equals(PbapPhonebook.SIM_MCH_PATH)) {
896             mContactsStorage.insertMissedCallHistory(mAccount, contacts.getList());
897         } else if (phonebook.equals(PbapPhonebook.ICH_PATH)
898                 || phonebook.equals(PbapPhonebook.SIM_ICH_PATH)) {
899             mContactsStorage.insertIncomingCallHistory(mAccount, contacts.getList());
900         } else if (phonebook.equals(PbapPhonebook.OCH_PATH)
901                 || phonebook.equals(PbapPhonebook.SIM_OCH_PATH)) {
902             mContactsStorage.insertOutgoingCallHistory(mAccount, contacts.getList());
903         } else {
904             warn("Received unknown phonebook to store, phonebook=" + phonebook);
905         }
906     }
907 
onConnectionStateChanged(int state)908     private void onConnectionStateChanged(int state) {
909         int prevState = mCurrentState;
910 
911         Intent intent = new Intent(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED);
912         intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
913         intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
914         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
915         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
916 
917         // Update the state, notify our service, AdapterService, and send the broadcast all at once
918         mCurrentState = state;
919 
920         info("Connection state changed, prev=" + prevState + ", new=" + state);
921 
922         AdapterService adapterService = AdapterService.getAdapterService();
923         mCallback.onConnectionStateChanged(prevState, state);
924         if (adapterService != null) {
925             adapterService.updateProfileConnectionAdapterProperties(
926                     mDevice, BluetoothProfile.PBAP_CLIENT, state, prevState);
927         }
928         mContext.sendBroadcastMultiplePermissions(
929                 intent,
930                 new String[] {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED},
931                 Utils.getTempBroadcastOptions());
932     }
933 
934     /* Callback for getting events back from our OBEX Client */
935     class PbapClientObexClientCallback implements PbapClientObexClient.Callback {
936 
PbapClientObexClientCallback()937         PbapClientObexClientCallback() {}
938 
939         @Override
onConnectionStateChanged(int oldState, int newState)940         public void onConnectionStateChanged(int oldState, int newState) {
941             info("Obex client connection state changed: " + oldState + " -> " + newState);
942             if (newState == STATE_DISCONNECTED) {
943                 obtainMessage(MSG_OBEX_CLIENT_DISCONNECTED).sendToTarget();
944             } else if (newState == STATE_CONNECTED) {
945                 obtainMessage(MSG_OBEX_CLIENT_CONNECTED).sendToTarget();
946             }
947         }
948 
949         @Override
onGetPhonebookMetadataComplete( int responseCode, String phonebook, PbapPhonebookMetadata metadata)950         public void onGetPhonebookMetadataComplete(
951                 int responseCode, String phonebook, PbapPhonebookMetadata metadata) {
952             if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
953                 warn(
954                         "PullPhonebook for metadata failed, phonebook="
955                                 + phonebook
956                                 + ", code="
957                                 + responseCode);
958                 onPhonebookMetadataDownloadFailed(phonebook);
959                 return;
960             }
961             debug("Received phonebook metadata, phonebook=" + phonebook + ", metadata=" + metadata);
962             onPhonebookMetadataReceived(metadata);
963         }
964 
965         @Override
onPhonebookContactsDownloaded( int responseCode, String phonebook, PbapPhonebook contacts)966         public void onPhonebookContactsDownloaded(
967                 int responseCode, String phonebook, PbapPhonebook contacts) {
968             if (responseCode != ResponseCodes.OBEX_HTTP_OK) {
969                 warn("PullPhonebook failed, phonebook=" + phonebook + ", code=" + responseCode);
970                 onPhonebookContactsDownloadFailed(phonebook);
971                 return;
972             }
973             debug("Received contacts, phonebook=" + phonebook + ", count=" + contacts.getCount());
974             onPhonebookContactsReceived(contacts);
975         }
976     }
977 
eventToString(int message)978     private static String eventToString(int message) {
979         switch (message) {
980             case -2 /* Special, from StateMachine.java */:
981                 return "SM_INIT_CMD";
982             case -1 /* Special, from StateMachine.java */:
983                 return "SM_QUIT_CMD";
984             case MSG_CONNECT:
985                 return "MSG_CONNECT";
986             case MSG_DISCONNECT:
987                 return "MSG_DISCONNECT";
988             case MSG_SDP_COMPLETE:
989                 return "MSG_SDP_COMPLETE";
990             case MSG_SDP_FAILED:
991                 return "MSG_SDP_FAILED";
992             case MSG_OBEX_CLIENT_CONNECTED:
993                 return "MSG_OBEX_CLIENT_CONNECTED";
994             case MSG_OBEX_CLIENT_DISCONNECTED:
995                 return "MSG_OBEX_CLIENT_DISCONNECTED";
996             case MSG_STORAGE_READY:
997                 return "MSG_STORAGE_READY";
998             case MSG_ACCOUNT_ADDED:
999                 return "MSG_ACCOUNT_ADDED";
1000             case MSG_ACCOUNT_REMOVED:
1001                 return "MSG_ACCOUNT_REMOVED";
1002             case MSG_DOWNLOAD:
1003                 return "MSG_DOWNLOAD";
1004             case MSG_PHONEBOOK_METADATA_RECEIVED:
1005                 return "MSG_PHONEBOOK_METADATA_RECEIVED";
1006             case MSG_PHONEBOOK_CONTACTS_RECEIVED:
1007                 return "MSG_PHONEBOOK_CONTACTS_RECEIVED";
1008             case MSG_CONNECT_TIMEOUT:
1009                 return "MSG_CONNECT_TIMEOUT";
1010             case MSG_DISCONNECT_TIMEOUT:
1011                 return "MSG_DISCONNECT_TIMEOUT";
1012             default:
1013                 return "Unknown (" + message + ")";
1014         }
1015     }
1016 
sdpCodeToString(int code)1017     private static String sdpCodeToString(int code) {
1018         switch (code) {
1019             case SDP_SUCCESS:
1020                 return "SDP_SUCCESS";
1021             case SDP_FAILED:
1022                 return "SDP_FAILED";
1023             case SDP_BUSY:
1024                 return "SDP_BUSY";
1025             default:
1026                 return "Unknown (" + code + ")";
1027         }
1028     }
1029 
debug(String message)1030     private void debug(String message) {
1031         Log.d(TAG, "[" + mDevice + "] " + message);
1032     }
1033 
info(String message)1034     private void info(String message) {
1035         Log.i(TAG, "[" + mDevice + "] " + message);
1036     }
1037 
warn(String message)1038     private void warn(String message) {
1039         Log.w(TAG, "[" + mDevice + "] " + message);
1040     }
1041 
error(String message)1042     private void error(String message) {
1043         Log.e(TAG, "[" + mDevice + "] " + message);
1044     }
1045 
dump(StringBuilder sb)1046     public void dump(StringBuilder sb) {
1047         ProfileService.println(
1048                 sb,
1049                 "  mDevice: "
1050                         + mDevice.getAddress()
1051                         + "("
1052                         + Utils.getName(mDevice)
1053                         + ") "
1054                         + this.toString());
1055 
1056         if (mSdpRecord != null) {
1057             ProfileService.println(
1058                     sb,
1059                     "    Server Version: "
1060                             + PbapSdpRecord.versionToString(mSdpRecord.getProfileVersion()));
1061         } else {
1062             ProfileService.println(sb, "    Server Version: Unknown, no SDP record");
1063         }
1064 
1065         ProfileService.println(sb, "    OBEX Client: " + mObexClient);
1066 
1067         ProfileService.println(sb, "    Download Batch Size: " + CONTACT_DOWNLOAD_BATCH_SIZE);
1068 
1069         int totalContacts = 0;
1070         int totalContactDownloaded = 0;
1071         ProfileService.println(sb, "    Supported Repositories:");
1072         for (Phonebook pb : mPhonebooks.values()) {
1073             ProfileService.println(sb, "      " + pb);
1074             totalContacts += pb.getTotalNumberOfContacts();
1075             totalContactDownloaded += pb.getNumberOfContactsDownloaded();
1076         }
1077         ProfileService.println(sb, "    Total Contacts: " + totalContacts);
1078         ProfileService.println(
1079                 sb, "    Download Progress: " + totalContactDownloaded + "/" + totalContacts);
1080     }
1081 }
1082