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.app.Service;
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothProfile;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.os.AsyncTask;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.os.HandlerThread;
31 import android.os.IBinder;
32 import android.os.Looper;
33 import android.os.Message;
34 import android.os.Process;
35 import android.net.Uri;
36 import android.provider.CallLog;
37 import android.provider.ContactsContract;
38 import android.util.Log;
39 import android.util.Pair;
40 
41 import com.android.vcard.VCardEntry;
42 import com.android.bluetooth.btservice.ProfileService;
43 import com.android.bluetooth.R;
44 
45 import java.util.ArrayDeque;
46 import java.util.ArrayList;
47 import java.util.List;
48 import java.util.Queue;
49 import java.lang.InterruptedException;
50 import java.lang.Thread;
51 /**
52  * These are the possible paths that can be pulled:
53  *       BluetoothPbapClient.PB_PATH;
54  *       BluetoothPbapClient.SIM_PB_PATH;
55  *       BluetoothPbapClient.ICH_PATH;
56  *       BluetoothPbapClient.SIM_ICH_PATH;
57  *       BluetoothPbapClient.OCH_PATH;
58  *       BluetoothPbapClient.SIM_OCH_PATH;
59  *       BluetoothPbapClient.MCH_PATH;
60  *       BluetoothPbapClient.SIM_MCH_PATH;
61  */
62 public class PbapPCEClient  implements PbapHandler.PbapListener {
63     private static final String TAG = "PbapPCEClient";
64     private static final boolean DBG = false;
65     private final Queue<PullRequest> mPendingRequests = new ArrayDeque<PullRequest>();
66     private BluetoothDevice mDevice;
67     private BluetoothPbapClient mClient;
68     private boolean mClientConnected = false;
69     private PbapHandler mHandler;
70     private ConnectionHandler mConnectionHandler;
71     private PullRequest mLastPull;
72     private HandlerThread mContactHandlerThread;
73     private Handler mContactHandler;
74     private Account mAccount = null;
75     private Context mContext = null;
76     private AccountManager mAccountManager;
77 
PbapPCEClient(Context context)78     PbapPCEClient(Context context) {
79         mContext = context;
80         mConnectionHandler = new ConnectionHandler(mContext.getMainLooper());
81         mHandler = new PbapHandler(this);
82         mAccountManager = AccountManager.get(mContext);
83         mContactHandlerThread = new HandlerThread("PBAP contact handler",
84                 Process.THREAD_PRIORITY_BACKGROUND);
85         mContactHandlerThread.start();
86         mContactHandler = new ContactHandler(mContactHandlerThread.getLooper());
87     }
88 
getConnectionState()89     public int getConnectionState() {
90         if (mDevice == null) {
91             return BluetoothProfile.STATE_DISCONNECTED;
92         }
93         BluetoothPbapClient.ConnectionState currentState = mClient.getState();
94         int bluetoothConnectionState;
95         switch(currentState) {
96           case DISCONNECTED:
97               bluetoothConnectionState = BluetoothProfile.STATE_DISCONNECTED;
98               break;
99           case CONNECTING:
100               bluetoothConnectionState = BluetoothProfile.STATE_CONNECTING;
101               break;
102           case CONNECTED:
103               bluetoothConnectionState = BluetoothProfile.STATE_CONNECTED;
104               break;
105           case DISCONNECTING:
106               bluetoothConnectionState = BluetoothProfile.STATE_DISCONNECTING;
107               break;
108           default:
109               bluetoothConnectionState = BluetoothProfile.STATE_DISCONNECTED;
110         }
111         return bluetoothConnectionState;
112     }
113 
getDevice()114     public BluetoothDevice getDevice() {
115         return mDevice;
116     }
117 
processNextRequest()118     private boolean processNextRequest() {
119         if (DBG) {
120             Log.d(TAG,"processNextRequest()");
121         }
122         if (mPendingRequests.isEmpty()) {
123             return false;
124         }
125         if (mClient != null  && mClient.getState() ==
126                 BluetoothPbapClient.ConnectionState.CONNECTED) {
127             mLastPull = mPendingRequests.remove();
128             if (DBG) {
129                 Log.d(TAG, "Pulling phone book from: " + mLastPull.path);
130             }
131             return mClient.pullPhoneBook(mLastPull.path);
132         }
133         return false;
134     }
135 
136 
137     @Override
onPhoneBookPullDone(List<VCardEntry> entries)138     public void onPhoneBookPullDone(List<VCardEntry> entries) {
139         mLastPull.setResults(entries);
140         mContactHandler.obtainMessage(ContactHandler.EVENT_ADD_CONTACTS,mLastPull).sendToTarget();
141         processNextRequest();
142     }
143 
144     @Override
onPhoneBookError()145     public void onPhoneBookError() {
146         if (DBG) {
147             Log.d(TAG, "Error, mLastPull = "  + mLastPull);
148         }
149         processNextRequest();
150     }
151 
152     @Override
onPbapClientConnected(boolean status)153     public synchronized void onPbapClientConnected(boolean status) {
154         mClientConnected = status;
155         if (mClientConnected == false) {
156             // If we are disconnected then whatever the current device is we should simply clean up.
157             onConnectionStateChanged(mDevice, BluetoothProfile.STATE_CONNECTING,
158                     BluetoothProfile.STATE_DISCONNECTED);
159             disconnect(null);
160         }
161         if (mClientConnected == true) {
162             onConnectionStateChanged(mDevice, BluetoothProfile.STATE_CONNECTING,
163                     BluetoothProfile.STATE_CONNECTED);
164             processNextRequest();
165         }
166     }
167 
168     private class ConnectionHandler extends Handler {
169         public static final int EVENT_CONNECT = 1;
170         public static final int EVENT_DISCONNECT = 2;
171 
ConnectionHandler(Looper looper)172         public ConnectionHandler(Looper looper) {
173             super(looper);
174         }
175 
176         @Override
handleMessage(Message msg)177         public void handleMessage(Message msg) {
178             if (DBG) {
179                 Log.d(TAG, "Connection Handler Message " + msg.what + " with " + msg.obj);
180             }
181             switch (msg.what) {
182                 case EVENT_CONNECT:
183                     if (msg.obj instanceof BluetoothDevice) {
184                         BluetoothDevice device = (BluetoothDevice) msg.obj;
185                         int oldState = getConnectionState();
186                         if (oldState != BluetoothProfile.STATE_DISCONNECTED) {
187                             return;
188                         }
189                         handleConnect(device);
190                     } else {
191                         Log.e(TAG, "Invalid instance in Connection Handler:Connect");
192                     }
193                     break;
194 
195                 case EVENT_DISCONNECT:
196                     if (mDevice == null) {
197                         return;
198                     }
199                     if (msg.obj == null || msg.obj instanceof BluetoothDevice) {
200                         BluetoothDevice device = (BluetoothDevice) msg.obj;
201                         if (!mDevice.equals(device)) {
202                             return;
203                         }
204                         int oldState = getConnectionState();
205                         handleDisconnect(device);
206                         int newState = getConnectionState();
207                         if (device != null) {
208                             onConnectionStateChanged(device, oldState, newState);
209                         }
210                     } else {
211                         Log.e(TAG, "Invalid instance in Connection Handler:Disconnect");
212                     }
213                     break;
214 
215                 default:
216                     Log.e(TAG, "Unknown Request to Connection Handler");
217                     break;
218             }
219         }
220 
handleConnect(BluetoothDevice device)221         private void handleConnect(BluetoothDevice device) {
222           Log.d(TAG,"HANDLECONNECT" + device);
223             if (device == null) {
224                 throw new IllegalStateException(TAG + ":Connect with null device!");
225             } else if (mDevice != null && !mDevice.equals(device)) {
226                 // Check that we are not already connected to an existing different device.
227                 // Since the device can be connected to multiple external devices -- we use the honor
228                 // protocol and only accept the first connecting device.
229                 Log.e(TAG, ":Got a connected event when connected to a different device. " +
230                       "existing = " + mDevice + " new = " + device);
231                 return;
232             } else if (device.equals(mDevice)) {
233                 Log.w(TAG, "Got a connected event for the same device. Ignoring!");
234                 return;
235             }
236             // Update the device.
237             mDevice = device;
238             onConnectionStateChanged(mDevice,BluetoothProfile.STATE_DISCONNECTED,
239                     BluetoothProfile.STATE_CONNECTING);
240             // Add the account. This should give us a place to stash the data.
241             mAccount = new Account(device.getAddress(),
242                     mContext.getString(R.string.pbap_account_type));
243             mContactHandler.obtainMessage(ContactHandler.EVENT_ADD_ACCOUNT, mAccount)
244                     .sendToTarget();
245             mClient = new BluetoothPbapClient(mDevice, mAccount, mHandler);
246             downloadPhoneBook();
247             downloadCallLogs();
248             mClient.connect();
249         }
250 
handleDisconnect(BluetoothDevice device)251         private void handleDisconnect(BluetoothDevice device) {
252             Log.w(TAG, "pbap disconnecting from = " + device);
253 
254             if (device == null) {
255                 // If we have a null device then disconnect the current device.
256                 device = mDevice;
257             } else if (mDevice == null) {
258                 Log.w(TAG, "No existing device connected to service - ignoring device = " + device);
259                 return;
260             } else if (!mDevice.equals(device)) {
261                 Log.w(TAG, "Existing device different from disconnected device. existing = " + mDevice +
262                            " disconnecting device = " + device);
263                 return;
264             }
265             resetState();
266         }
267     }
268 
onConnectionStateChanged(BluetoothDevice device, int prevState, int state)269     private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) {
270         Intent intent = new Intent(android.bluetooth.BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED);
271         intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
272         intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
273         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
274         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
275         mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
276         Log.d(TAG,"Connection state " + device + ": " + prevState + "->" + state);
277     }
278 
connect(BluetoothDevice device)279     public void connect(BluetoothDevice device) {
280         mConnectionHandler.obtainMessage(ConnectionHandler.EVENT_CONNECT,device).sendToTarget();
281     }
282 
disconnect(BluetoothDevice device)283     public void disconnect(BluetoothDevice device) {
284         mConnectionHandler.obtainMessage(ConnectionHandler.EVENT_DISCONNECT,device).sendToTarget();
285     }
286 
start()287     public void start() {
288         if (mDevice != null) {
289             // We are already connected -Ignore.
290             Log.w(TAG, "Already started, ignoring request to start again.");
291             return;
292         }
293         // Device is NULL, we go on remove any unclean shutdown accounts.
294         mContactHandler.obtainMessage(ContactHandler.EVENT_CLEANUP).sendToTarget();
295     }
296 
resetState()297     private void resetState() {
298         if (DBG) {
299             Log.d(TAG,"resetState()");
300         }
301         if (mClient != null) {
302             // This should abort any inflight messages.
303             mClient.disconnect();
304         }
305         mClient = null;
306         mClientConnected = false;
307 
308         mContactHandler.removeCallbacksAndMessages(null);
309         mContactHandlerThread.interrupt();
310         mContactHandler.obtainMessage(ContactHandler.EVENT_CLEANUP).sendToTarget();
311 
312         mDevice = null;
313         mAccount = null;
314         mPendingRequests.clear();
315         if (DBG) {
316             Log.d(TAG,"resetState Complete");
317         }
318 
319     }
320 
downloadCallLogs()321     private void downloadCallLogs() {
322         // Download Incoming Call Logs.
323         CallLogPullRequest ichCallLog =
324                 new CallLogPullRequest(mContext, BluetoothPbapClient.ICH_PATH);
325         addPullRequest(ichCallLog);
326 
327         // Downoad Outgoing Call Logs.
328         CallLogPullRequest ochCallLog =
329                 new CallLogPullRequest(mContext, BluetoothPbapClient.OCH_PATH);
330         addPullRequest(ochCallLog);
331 
332         // Downoad Missed Call Logs.
333         CallLogPullRequest mchCallLog =
334                 new CallLogPullRequest(mContext, BluetoothPbapClient.MCH_PATH);
335         addPullRequest(mchCallLog);
336     }
337 
downloadPhoneBook()338     private void downloadPhoneBook() {
339         // Download the phone book.
340         PhonebookPullRequest pb = new PhonebookPullRequest(mContext, mAccount);
341         addPullRequest(pb);
342     }
343 
addPullRequest(PullRequest r)344     private void addPullRequest(PullRequest r) {
345         if (DBG) {
346             Log.d(TAG, "pull request mClient=" + mClient + " connected= " +
347                     mClientConnected + " mDevice=" + mDevice + " path= " + r.path);
348         }
349         if (mClient == null || mDevice == null) {
350             // It seems we want to pull but the bt connection isn't up, fail it
351             // immediately.
352             Log.w(TAG, "aborting pull request.");
353             return;
354         }
355         mPendingRequests.add(r);
356     }
357 
358     private class ContactHandler extends Handler {
359         public static final int EVENT_ADD_ACCOUNT = 1;
360         public static final int EVENT_ADD_CONTACTS = 2;
361         public static final int EVENT_CLEANUP = 3;
362 
ContactHandler(Looper looper)363         public ContactHandler(Looper looper) {
364           super(looper);
365         }
366 
367         @Override
handleMessage(Message msg)368         public void handleMessage(Message msg) {
369             if (DBG) {
370                 Log.d(TAG, "Contact Handler Message " + msg.what + " with " + msg.obj);
371             }
372             switch (msg.what) {
373                 case EVENT_ADD_ACCOUNT:
374                     if (msg.obj instanceof Account) {
375                         Account account = (Account) msg.obj;
376                         addAccount(account);
377                     } else {
378                         Log.e(TAG, "invalid Instance in Contact Handler: Add Account");
379                     }
380                     break;
381 
382                 case EVENT_ADD_CONTACTS:
383                     if (msg.obj instanceof PullRequest) {
384                         PullRequest req = (PullRequest) msg.obj;
385                         req.onPullComplete();
386                     } else {
387                         Log.e(TAG, "invalid Instance in Contact Handler: Add Contacts");
388                     }
389                     break;
390 
391                 case EVENT_CLEANUP:
392                     Thread.currentThread().interrupted();  //clear state of interrupt.
393                     removeUncleanAccounts();
394                     mContext.getContentResolver().delete(CallLog.Calls.CONTENT_URI, null, null);
395                     if (DBG) {
396                         Log.d(TAG, "Call logs deleted.");
397                     }
398                     break;
399 
400                 default:
401                     Log.e(TAG, "Unknown Request to Contact Handler");
402                     break;
403             }
404         }
405 
removeUncleanAccounts()406         private void removeUncleanAccounts() {
407             // Find all accounts that match the type "pbap" and delete them. This section is
408             // executed only if the device was shut down in an unclean state and contacts persisted.
409             Account[] accounts =
410                 mAccountManager.getAccountsByType(mContext.getString(R.string.pbap_account_type));
411             Log.w(TAG, "Found " + accounts.length + " unclean accounts");
412             for (Account acc : accounts) {
413                 Log.w(TAG, "Deleting " + acc);
414                 // The device ID is the name of the account.
415                 removeAccount(acc);
416             }
417         }
418 
addAccount(Account account)419         private boolean addAccount(Account account) {
420             if (mAccountManager.addAccountExplicitly(account, null, null)) {
421                 if (DBG) {
422                     Log.d(TAG, "Added account " + mAccount);
423                 }
424                 return true;
425             }
426             return false;
427         }
428 
removeAccount(Account acc)429         private boolean removeAccount(Account acc) {
430             if (mAccountManager.removeAccountExplicitly(acc)) {
431                 if (DBG) {
432                     Log.d(TAG, "Removed account " + acc);
433                 }
434                 return true;
435             }
436             Log.e(TAG, "Failed to remove account " + mAccount);
437             return false;
438         }
439    }
440 }
441