blob: d04fabc9a01805bdbf0cd63062b81372e3d952f5 [file] [log] [blame]
/*
* 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<PullRequest> mPendingRequests = new ArrayDeque<PullRequest>();
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<VCardEntry> 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;
}
}
}