/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth.pbapclient; import android.accounts.Account; import android.accounts.AccountManager; import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.Process; import android.net.Uri; import android.provider.CallLog; import android.provider.ContactsContract; import android.util.Log; import android.util.Pair; import com.android.vcard.VCardEntry; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.R; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.lang.InterruptedException; import java.lang.Thread; /** * These are the possible paths that can be pulled: * BluetoothPbapClient.PB_PATH; * BluetoothPbapClient.SIM_PB_PATH; * BluetoothPbapClient.ICH_PATH; * BluetoothPbapClient.SIM_ICH_PATH; * BluetoothPbapClient.OCH_PATH; * BluetoothPbapClient.SIM_OCH_PATH; * BluetoothPbapClient.MCH_PATH; * BluetoothPbapClient.SIM_MCH_PATH; */ public class PbapPCEClient implements PbapHandler.PbapListener { private static final String TAG = "PbapPCEClient"; private static final boolean DBG = false; private final Queue mPendingRequests = new ArrayDeque(); private BluetoothDevice mDevice; private BluetoothPbapClient mClient; private boolean mClientConnected = false; private PbapHandler mHandler; private ConnectionHandler mConnectionHandler; private PullRequest mLastPull; private HandlerThread mContactHandlerThread; private Handler mContactHandler; private Account mAccount = null; private Context mContext = null; private AccountManager mAccountManager; PbapPCEClient(Context context) { mContext = context; mConnectionHandler = new ConnectionHandler(mContext.getMainLooper()); mHandler = new PbapHandler(this); mAccountManager = AccountManager.get(mContext); mContactHandlerThread = new HandlerThread("PBAP contact handler", Process.THREAD_PRIORITY_BACKGROUND); mContactHandlerThread.start(); mContactHandler = new ContactHandler(mContactHandlerThread.getLooper()); } public int getConnectionState() { if (mDevice == null) { return BluetoothProfile.STATE_DISCONNECTED; } BluetoothPbapClient.ConnectionState currentState = mClient.getState(); int bluetoothConnectionState; switch(currentState) { case DISCONNECTED: bluetoothConnectionState = BluetoothProfile.STATE_DISCONNECTED; break; case CONNECTING: bluetoothConnectionState = BluetoothProfile.STATE_CONNECTING; break; case CONNECTED: bluetoothConnectionState = BluetoothProfile.STATE_CONNECTED; break; case DISCONNECTING: bluetoothConnectionState = BluetoothProfile.STATE_DISCONNECTING; break; default: bluetoothConnectionState = BluetoothProfile.STATE_DISCONNECTED; } return bluetoothConnectionState; } public BluetoothDevice getDevice() { return mDevice; } private boolean processNextRequest() { if (DBG) { Log.d(TAG,"processNextRequest()"); } if (mPendingRequests.isEmpty()) { return false; } if (mClient != null && mClient.getState() == BluetoothPbapClient.ConnectionState.CONNECTED) { mLastPull = mPendingRequests.remove(); if (DBG) { Log.d(TAG, "Pulling phone book from: " + mLastPull.path); } return mClient.pullPhoneBook(mLastPull.path); } return false; } @Override public void onPhoneBookPullDone(List entries) { mLastPull.setResults(entries); mContactHandler.obtainMessage(ContactHandler.EVENT_ADD_CONTACTS,mLastPull).sendToTarget(); processNextRequest(); } @Override public void onPhoneBookError() { if (DBG) { Log.d(TAG, "Error, mLastPull = " + mLastPull); } processNextRequest(); } @Override public synchronized void onPbapClientConnected(boolean status) { mClientConnected = status; if (mClientConnected == false) { // If we are disconnected then whatever the current device is we should simply clean up. onConnectionStateChanged(mDevice, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED); disconnect(null); } if (mClientConnected == true) { onConnectionStateChanged(mDevice, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_CONNECTED); processNextRequest(); } } private class ConnectionHandler extends Handler { public static final int EVENT_CONNECT = 1; public static final int EVENT_DISCONNECT = 2; public ConnectionHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { if (DBG) { Log.d(TAG, "Connection Handler Message " + msg.what + " with " + msg.obj); } switch (msg.what) { case EVENT_CONNECT: if (msg.obj instanceof BluetoothDevice) { BluetoothDevice device = (BluetoothDevice) msg.obj; int oldState = getConnectionState(); if (oldState != BluetoothProfile.STATE_DISCONNECTED) { return; } handleConnect(device); } else { Log.e(TAG, "Invalid instance in Connection Handler:Connect"); } break; case EVENT_DISCONNECT: if (mDevice == null) { return; } if (msg.obj == null || msg.obj instanceof BluetoothDevice) { BluetoothDevice device = (BluetoothDevice) msg.obj; if (!mDevice.equals(device)) { return; } int oldState = getConnectionState(); handleDisconnect(device); int newState = getConnectionState(); if (device != null) { onConnectionStateChanged(device, oldState, newState); } } else { Log.e(TAG, "Invalid instance in Connection Handler:Disconnect"); } break; default: Log.e(TAG, "Unknown Request to Connection Handler"); break; } } private void handleConnect(BluetoothDevice device) { Log.d(TAG,"HANDLECONNECT" + device); if (device == null) { throw new IllegalStateException(TAG + ":Connect with null device!"); } else if (mDevice != null && !mDevice.equals(device)) { // Check that we are not already connected to an existing different device. // Since the device can be connected to multiple external devices -- we use the honor // protocol and only accept the first connecting device. Log.e(TAG, ":Got a connected event when connected to a different device. " + "existing = " + mDevice + " new = " + device); return; } else if (device.equals(mDevice)) { Log.w(TAG, "Got a connected event for the same device. Ignoring!"); return; } // Update the device. mDevice = device; onConnectionStateChanged(mDevice,BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); // Add the account. This should give us a place to stash the data. mAccount = new Account(device.getAddress(), mContext.getString(R.string.pbap_account_type)); mContactHandler.obtainMessage(ContactHandler.EVENT_ADD_ACCOUNT, mAccount) .sendToTarget(); mClient = new BluetoothPbapClient(mDevice, mAccount, mHandler); downloadPhoneBook(); downloadCallLogs(); mClient.connect(); } private void handleDisconnect(BluetoothDevice device) { Log.w(TAG, "pbap disconnecting from = " + device); if (device == null) { // If we have a null device then disconnect the current device. device = mDevice; } else if (mDevice == null) { Log.w(TAG, "No existing device connected to service - ignoring device = " + device); return; } else if (!mDevice.equals(device)) { Log.w(TAG, "Existing device different from disconnected device. existing = " + mDevice + " disconnecting device = " + device); return; } resetState(); } } private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) { Intent intent = new Intent(android.bluetooth.BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); intent.putExtra(BluetoothProfile.EXTRA_STATE, state); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); Log.d(TAG,"Connection state " + device + ": " + prevState + "->" + state); } public void connect(BluetoothDevice device) { mConnectionHandler.obtainMessage(ConnectionHandler.EVENT_CONNECT,device).sendToTarget(); } public void disconnect(BluetoothDevice device) { mConnectionHandler.obtainMessage(ConnectionHandler.EVENT_DISCONNECT,device).sendToTarget(); } public void start() { if (mDevice != null) { // We are already connected -Ignore. Log.w(TAG, "Already started, ignoring request to start again."); return; } // Device is NULL, we go on remove any unclean shutdown accounts. mContactHandler.obtainMessage(ContactHandler.EVENT_CLEANUP).sendToTarget(); } private void resetState() { if (DBG) { Log.d(TAG,"resetState()"); } if (mClient != null) { // This should abort any inflight messages. mClient.disconnect(); } mClient = null; mClientConnected = false; mContactHandler.removeCallbacksAndMessages(null); mContactHandlerThread.interrupt(); mContactHandler.obtainMessage(ContactHandler.EVENT_CLEANUP).sendToTarget(); mDevice = null; mAccount = null; mPendingRequests.clear(); if (DBG) { Log.d(TAG,"resetState Complete"); } } private void downloadCallLogs() { // Download Incoming Call Logs. CallLogPullRequest ichCallLog = new CallLogPullRequest(mContext, BluetoothPbapClient.ICH_PATH); addPullRequest(ichCallLog); // Downoad Outgoing Call Logs. CallLogPullRequest ochCallLog = new CallLogPullRequest(mContext, BluetoothPbapClient.OCH_PATH); addPullRequest(ochCallLog); // Downoad Missed Call Logs. CallLogPullRequest mchCallLog = new CallLogPullRequest(mContext, BluetoothPbapClient.MCH_PATH); addPullRequest(mchCallLog); } private void downloadPhoneBook() { // Download the phone book. PhonebookPullRequest pb = new PhonebookPullRequest(mContext, mAccount); addPullRequest(pb); } private void addPullRequest(PullRequest r) { if (DBG) { Log.d(TAG, "pull request mClient=" + mClient + " connected= " + mClientConnected + " mDevice=" + mDevice + " path= " + r.path); } if (mClient == null || mDevice == null) { // It seems we want to pull but the bt connection isn't up, fail it // immediately. Log.w(TAG, "aborting pull request."); return; } mPendingRequests.add(r); } private class ContactHandler extends Handler { public static final int EVENT_ADD_ACCOUNT = 1; public static final int EVENT_ADD_CONTACTS = 2; public static final int EVENT_CLEANUP = 3; public ContactHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { if (DBG) { Log.d(TAG, "Contact Handler Message " + msg.what + " with " + msg.obj); } switch (msg.what) { case EVENT_ADD_ACCOUNT: if (msg.obj instanceof Account) { Account account = (Account) msg.obj; addAccount(account); } else { Log.e(TAG, "invalid Instance in Contact Handler: Add Account"); } break; case EVENT_ADD_CONTACTS: if (msg.obj instanceof PullRequest) { PullRequest req = (PullRequest) msg.obj; req.onPullComplete(); } else { Log.e(TAG, "invalid Instance in Contact Handler: Add Contacts"); } break; case EVENT_CLEANUP: Thread.currentThread().interrupted(); //clear state of interrupt. removeUncleanAccounts(); mContext.getContentResolver().delete(CallLog.Calls.CONTENT_URI, null, null); if (DBG) { Log.d(TAG, "Call logs deleted."); } break; default: Log.e(TAG, "Unknown Request to Contact Handler"); break; } } private void removeUncleanAccounts() { // Find all accounts that match the type "pbap" and delete them. This section is // executed only if the device was shut down in an unclean state and contacts persisted. Account[] accounts = mAccountManager.getAccountsByType(mContext.getString(R.string.pbap_account_type)); Log.w(TAG, "Found " + accounts.length + " unclean accounts"); for (Account acc : accounts) { Log.w(TAG, "Deleting " + acc); // The device ID is the name of the account. removeAccount(acc); } } private boolean addAccount(Account account) { if (mAccountManager.addAccountExplicitly(account, null, null)) { if (DBG) { Log.d(TAG, "Added account " + mAccount); } return true; } return false; } private boolean removeAccount(Account acc) { if (mAccountManager.removeAccountExplicitly(acc)) { if (DBG) { Log.d(TAG, "Removed account " + acc); } return true; } Log.e(TAG, "Failed to remove account " + mAccount); return false; } } }