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