blob: 63a8851ffe65150d6b74c29873e5459232d6b37c [file] [log] [blame]
/*
* Copyright (C) 2015 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.google.android.car.pbapsink;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.client.pbap.BluetoothPbapClient;
import android.content.ContentResolver;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
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.google.android.car.pbapsink.R;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 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 extends Service implements PbapHandler.PbapListener {
private static final String TAG = "PbapPCEClient";
private final Queue<PullRequest> mPendingRequests = new ArrayDeque<PullRequest>();
private final AtomicBoolean mPendingPull = new AtomicBoolean(false);
private BluetoothDevice mDevice;
private BluetoothPbapClient mClient;
private boolean mClientConnected = false;
private PbapHandler mHandler;
private Handler mSelfHandler;
private PullRequest mLastPull;
private Account mAccount = null;
private AccountManager mAccountManager;
private DeleteCallLogTask mDeleteCallLogTask;
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate");
mSelfHandler = new Handler(getMainLooper());
mHandler = new PbapHandler(this);
mAccountManager = AccountManager.get(this);
}
@Override
public void onDestroy() {
Log.d(TAG, "onDestroy");
mHandler = null;
super.onDestroy();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onReceive intent=" + intent);
String action = intent != null ? intent.getAction() : null;
if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) {
BluetoothDevice device =
(BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// Handle a new device connecting.
handleConnect(device);
} else if (BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED.equals(action) ||
BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)) {
BluetoothDevice device =
(BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// Handle a device disconnecting.
handleDisconnect(device);
} else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action) ||
BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
if (state == BluetoothAdapter.STATE_TURNING_OFF ||
state == BluetoothAdapter.STATE_OFF) {
handleDisconnect(null);
}
} else if (BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
if (state == BluetoothProfile.STATE_DISCONNECTED) {
handleDisconnect(null);
}
} else if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
handleBootComplete();
}
// We always start the service as STICKY. But if we want to kill the service (on a
// disconnect or on maintenance tasks such as ACTION_BOOT_COMPLETE we post a new runnable
// which will run *after* START_STICKY is returned.
return START_STICKY;
}
private synchronized boolean maybePull() {
if (!mClientConnected) {
Log.w(TAG, "Client not connected yet -- will execute on next cycle.");
return false;
}
return maybePullLocked();
}
private boolean maybePullLocked() {
// I _think_ this is ok. Maybe.
if (mPendingPull.compareAndSet(false, true)) {
if (mPendingRequests.isEmpty()) {
mPendingPull.set(false);
return false;
}
if (mClient != null) {
mLastPull = mPendingRequests.remove();
Log.d(TAG, "Pulling phone book from: " + mLastPull.path);
return mClient.pullPhoneBook(mLastPull.path);
}
}
return false;
}
private void pullComplete() {
mPendingPull.set(false);
maybePull();
}
@Override
public void onPhoneBookPullDone(List<VCardEntry> entries) {
mLastPull.onPullComplete(true, entries);
pullComplete();
}
@Override
public void onPhoneBookError() {
Log.d(TAG, "Error, mLastPull = " + mLastPull);
mLastPull.onPullComplete(false, null);
pullComplete();
}
@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.
handleDisconnect(null);
}
if (mClientConnected == true) maybePullLocked();
}
private void handleConnect(BluetoothDevice 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;
}
// Cancel any pending delete tasks that might race.
if (mDeleteCallLogTask != null) {
mDeleteCallLogTask.cancel(true);
}
mDeleteCallLogTask = new DeleteCallLogTask();
// Cleanup any existing accounts if we get a connected event but previous account state was
// left hanging (such as unclean shutdown).
removeUncleanAccounts();
// Add the account. This should give us a place to stash the data.
addAccount(device.getAddress());
// Update the device and open the connection over pbap.
mDevice = device;
mClient = new BluetoothPbapClient(mDevice, mAccount, mHandler);
mClient.connect();
// Download the phonebook.
downloadPhoneBook();
// Download the call logs.
downloadCallLogs();
}
private void handleDisconnect(BluetoothDevice 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;
}
if (device != null) {
removeAccount(mAccount);
mAccount = null;
}
resetState();
// We stop the service in separate runnable which can run *after* onStartCommand (which
// calls this function) has finished.
stopSelfInSeparateRunnable();
}
private void handleBootComplete() {
if (mDevice != null) {
// We are already connected - hence boot complete came a bit too late. Ignore.
Log.w(TAG, "Boot complete received when we are connected to device = " + mDevice +
" . Ignoring this broadcast.");
return;
}
// Device is NULL, we go on remove any unclean shutdown accounts.
removeUncleanAccounts();
resetState();
stopSelfInSeparateRunnable();
}
private void resetState() {
if (mClient != null) {
// This should abort any inflight messages.
mClient.disconnect();
}
mClient = null;
if (mDeleteCallLogTask != null &&
mDeleteCallLogTask.getStatus() == AsyncTask.Status.PENDING) {
mDeleteCallLogTask.execute();
}
mDevice = null;
mAccount = null;
}
// Stop the service always after onStartCommand has been finished running.
private void stopSelfInSeparateRunnable() {
mSelfHandler.post(
new Runnable() {
@Override
public void run() {
stopSelf();
}
}
);
}
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(getString(R.string.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 void downloadCallLogs() {
// Download Incoming Call Logs.
CallLogPullRequest ichCallLog = new CallLogPullRequest(this, BluetoothPbapClient.ICH_PATH);
addPullRequest(ichCallLog);
// Downoad Outgoing Call Logs.
CallLogPullRequest ochCallLog = new CallLogPullRequest(this, BluetoothPbapClient.OCH_PATH);
addPullRequest(ochCallLog);
// Downoad Missed Call Logs.
CallLogPullRequest mchCallLog = new CallLogPullRequest(this, BluetoothPbapClient.MCH_PATH);
addPullRequest(mchCallLog);
}
private void downloadPhoneBook() {
// Download the phone book.
PhonebookPullRequest pb = new PhonebookPullRequest(this, mAccount);
addPullRequest(pb);
}
private class DeleteCallLogTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... unused) {
if (isCancelled()) {
return null;
}
getContentResolver().delete(CallLog.Calls.CONTENT_URI, null, null);
Log.d(TAG, "Call logs deleted.");
return null;
}
}
private boolean addAccount(String id) {
mAccount = new Account(id, getString(R.string.account_type));
if (mAccountManager.addAccountExplicitly(mAccount, null, null)) {
Log.d(TAG, "Added account " + mAccount);
return true;
}
throw new IllegalStateException(TAG + ":Failed to add account!");
}
private boolean removeAccount(Account acc) {
if (mAccountManager.removeAccountExplicitly(acc)) {
Log.d(TAG, "Removed account " + acc);
return true;
}
Log.e(TAG, "Failed to remove account " + mAccount);
return false;
}
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind " + intent);
return null;
}
public void addPullRequest(PullRequest r) {
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.");
r.onPullComplete(false, null);
return;
}
mPendingRequests.add(r);
maybePull();
}
}