Home | History | Annotate | Download | only in pbapclient
      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 
     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 
     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 
    114     public BluetoothDevice getDevice() {
    115         return mDevice;
    116     }
    117 
    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
    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
    145     public void onPhoneBookError() {
    146         if (DBG) {
    147             Log.d(TAG, "Error, mLastPull = "  + mLastPull);
    148         }
    149         processNextRequest();
    150     }
    151 
    152     @Override
    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 
    172         public ConnectionHandler(Looper looper) {
    173             super(looper);
    174         }
    175 
    176         @Override
    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 
    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 
    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 
    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 
    279     public void connect(BluetoothDevice device) {
    280         mConnectionHandler.obtainMessage(ConnectionHandler.EVENT_CONNECT,device).sendToTarget();
    281     }
    282 
    283     public void disconnect(BluetoothDevice device) {
    284         mConnectionHandler.obtainMessage(ConnectionHandler.EVENT_DISCONNECT,device).sendToTarget();
    285     }
    286 
    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 
    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 
    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 
    338     private void downloadPhoneBook() {
    339         // Download the phone book.
    340         PhonebookPullRequest pb = new PhonebookPullRequest(mContext, mAccount);
    341         addPullRequest(pb);
    342     }
    343 
    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 
    363         public ContactHandler(Looper looper) {
    364           super(looper);
    365         }
    366 
    367         @Override
    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 
    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 
    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 
    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