blob: 4e4a240f1bce56ae3a0e2dead680b07ace40b3b6 [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.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.SdpPseRecord;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.util.Log;
import com.android.bluetooth.BluetoothObexTransport;
import com.android.bluetooth.R;
import com.android.vcard.VCardEntry;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import javax.obex.ClientSession;
import javax.obex.HeaderSet;
import javax.obex.ResponseCodes;
/* Bluetooth/pbapclient/PbapClientConnectionHandler is responsible
* for connecting, disconnecting and downloading contacts from the
* PBAP PSE when commanded. It receives all direction from the
* controlling state machine.
*/
class PbapClientConnectionHandler extends Handler {
// Tradeoff: larger BATCH_SIZE leads to faster download rates, while smaller
// BATCH_SIZE is less prone to IO Exceptions if there is a download in
// progress when Bluetooth stack is torn down.
private static final int DEFAULT_BATCH_SIZE = 250;
// Upper limit on the indices of the vcf cards/entries, inclusive,
// i.e., valid indices are [0, 1, ... , UPPER_LIMIT]
private static final int UPPER_LIMIT = 65535;
static final String TAG = "PbapClientConnHandler";
static final boolean DBG = Utils.DBG;
static final boolean VDBG = Utils.VDBG;
static final int MSG_CONNECT = 1;
static final int MSG_DISCONNECT = 2;
static final int MSG_DOWNLOAD = 3;
// The following constants are pulled from the Bluetooth Phone Book Access Profile specification
// 1.1
private static final byte[] PBAP_TARGET = new byte[]{
0x79,
0x61,
0x35,
(byte) 0xf0,
(byte) 0xf0,
(byte) 0xc5,
0x11,
(byte) 0xd8,
0x09,
0x66,
0x08,
0x00,
0x20,
0x0c,
(byte) 0x9a,
0x66
};
private static final int PBAP_FEATURE_DEFAULT_IMAGE_FORMAT = 0x00000200;
private static final int PBAP_FEATURE_BROWSING = 0x00000002;
private static final int PBAP_FEATURE_DOWNLOADING = 0x00000001;
private static final long PBAP_FILTER_VERSION = 1 << 0;
private static final long PBAP_FILTER_FN = 1 << 1;
private static final long PBAP_FILTER_N = 1 << 2;
private static final long PBAP_FILTER_PHOTO = 1 << 3;
private static final long PBAP_FILTER_ADR = 1 << 5;
private static final long PBAP_FILTER_TEL = 1 << 7;
private static final long PBAP_FILTER_EMAIL = 1 << 8;
private static final long PBAP_FILTER_NICKNAME = 1 << 23;
private static final int PBAP_SUPPORTED_FEATURE =
PBAP_FEATURE_DEFAULT_IMAGE_FORMAT | PBAP_FEATURE_DOWNLOADING;
private static final long PBAP_REQUESTED_FIELDS =
PBAP_FILTER_VERSION | PBAP_FILTER_FN | PBAP_FILTER_N | PBAP_FILTER_PHOTO
| PBAP_FILTER_ADR | PBAP_FILTER_EMAIL | PBAP_FILTER_TEL | PBAP_FILTER_NICKNAME;
private static final int L2CAP_INVALID_PSM = -1;
public static final String PB_PATH = "telecom/pb.vcf";
public static final String FAV_PATH = "telecom/fav.vcf";
public static final String MCH_PATH = "telecom/mch.vcf";
public static final String ICH_PATH = "telecom/ich.vcf";
public static final String OCH_PATH = "telecom/och.vcf";
public static final String SIM_PB_PATH = "SIM1/telecom/pb.vcf";
public static final String SIM_MCH_PATH = "SIM1/telecom/mch.vcf";
public static final String SIM_ICH_PATH = "SIM1/telecom/ich.vcf";
public static final String SIM_OCH_PATH = "SIM1/telecom/och.vcf";
// PBAP v1.2.3 Sec. 7.1.2
private static final int SUPPORTED_REPOSITORIES_LOCALPHONEBOOK = 1 << 0;
private static final int SUPPORTED_REPOSITORIES_SIMCARD = 1 << 1;
private static final int SUPPORTED_REPOSITORIES_FAVORITES = 1 << 3;
public static final int PBAP_V1_2 = 0x0102;
public static final byte VCARD_TYPE_21 = 0;
public static final byte VCARD_TYPE_30 = 1;
private Account mAccount;
private AccountManager mAccountManager;
private BluetoothSocket mSocket;
private final BluetoothAdapter mAdapter;
private final BluetoothDevice mDevice;
// PSE SDP Record for current device.
private SdpPseRecord mPseRec = null;
private ClientSession mObexSession;
private Context mContext;
private BluetoothPbapObexAuthenticator mAuth = null;
private final PbapClientStateMachine mPbapClientStateMachine;
private boolean mAccountCreated;
PbapClientConnectionHandler(Looper looper, Context context, PbapClientStateMachine stateMachine,
BluetoothDevice device) {
super(looper);
mAdapter = BluetoothAdapter.getDefaultAdapter();
mDevice = device;
mContext = context;
mPbapClientStateMachine = stateMachine;
mAuth = new BluetoothPbapObexAuthenticator(this);
mAccountManager = AccountManager.get(mPbapClientStateMachine.getContext());
mAccount =
new Account(mDevice.getAddress(), mContext.getString(R.string.pbap_account_type));
}
/**
* Constructs PCEConnectionHandler object
*
* @param Builder To build BluetoothPbapClientHandler Instance.
*/
PbapClientConnectionHandler(Builder pceHandlerbuild) {
super(pceHandlerbuild.mLooper);
mAdapter = BluetoothAdapter.getDefaultAdapter();
mDevice = pceHandlerbuild.mDevice;
mContext = pceHandlerbuild.mContext;
mPbapClientStateMachine = pceHandlerbuild.mClientStateMachine;
mAuth = new BluetoothPbapObexAuthenticator(this);
mAccountManager = AccountManager.get(mPbapClientStateMachine.getContext());
mAccount =
new Account(mDevice.getAddress(), mContext.getString(R.string.pbap_account_type));
}
public static class Builder {
private Looper mLooper;
private Context mContext;
private BluetoothDevice mDevice;
private PbapClientStateMachine mClientStateMachine;
public Builder setLooper(Looper loop) {
this.mLooper = loop;
return this;
}
public Builder setClientSM(PbapClientStateMachine clientStateMachine) {
this.mClientStateMachine = clientStateMachine;
return this;
}
public Builder setRemoteDevice(BluetoothDevice device) {
this.mDevice = device;
return this;
}
public Builder setContext(Context context) {
this.mContext = context;
return this;
}
public PbapClientConnectionHandler build() {
PbapClientConnectionHandler pbapClientHandler = new PbapClientConnectionHandler(this);
return pbapClientHandler;
}
}
@Override
public void handleMessage(Message msg) {
if (DBG) {
Log.d(TAG, "Handling Message = " + msg.what);
}
switch (msg.what) {
case MSG_CONNECT:
mPseRec = (SdpPseRecord) msg.obj;
/* To establish a connection, first open a socket and then create an OBEX session */
if (connectSocket()) {
if (DBG) {
Log.d(TAG, "Socket connected");
}
} else {
Log.w(TAG, "Socket CONNECT Failure ");
mPbapClientStateMachine.sendMessage(
PbapClientStateMachine.MSG_CONNECTION_FAILED);
return;
}
if (connectObexSession()) {
mPbapClientStateMachine.sendMessage(
PbapClientStateMachine.MSG_CONNECTION_COMPLETE);
} else {
mPbapClientStateMachine.sendMessage(
PbapClientStateMachine.MSG_CONNECTION_FAILED);
}
break;
case MSG_DISCONNECT:
if (DBG) {
Log.d(TAG, "Starting Disconnect");
}
try {
if (mObexSession != null) {
if (DBG) {
Log.d(TAG, "obexSessionDisconnect" + mObexSession);
}
mObexSession.disconnect(null);
mObexSession.close();
}
if (DBG) {
Log.d(TAG, "Closing Socket");
}
closeSocket();
} catch (IOException e) {
Log.w(TAG, "DISCONNECT Failure ", e);
}
if (DBG) {
Log.d(TAG, "Completing Disconnect");
}
removeAccount(mAccount);
removeCallLog(mAccount);
mPbapClientStateMachine.sendMessage(PbapClientStateMachine.MSG_CONNECTION_CLOSED);
break;
case MSG_DOWNLOAD:
mAccountCreated = addAccount(mAccount);
if (!mAccountCreated) {
Log.e(TAG, "Account creation failed.");
return;
}
if (isRepositorySupported(SUPPORTED_REPOSITORIES_FAVORITES)) {
downloadContacts(FAV_PATH);
}
if (isRepositorySupported(SUPPORTED_REPOSITORIES_LOCALPHONEBOOK)) {
downloadContacts(PB_PATH);
}
if (isRepositorySupported(SUPPORTED_REPOSITORIES_SIMCARD)) {
downloadContacts(SIM_PB_PATH);
}
HashMap<String, Integer> callCounter = new HashMap<>();
downloadCallLog(MCH_PATH, callCounter);
downloadCallLog(ICH_PATH, callCounter);
downloadCallLog(OCH_PATH, callCounter);
break;
default:
Log.w(TAG, "Received Unexpected Message");
}
return;
}
/* Utilize SDP, if available, to create a socket connection over L2CAP, RFCOMM specified
* channel, or RFCOMM default channel. */
private synchronized boolean connectSocket() {
try {
/* Use BluetoothSocket to connect */
if (mPseRec == null) {
// BackWardCompatability: Fall back to create RFCOMM through UUID.
if (VDBG) Log.v(TAG, "connectSocket: UUID: " + BluetoothUuid.PBAP_PSE.getUuid());
mSocket =
mDevice.createRfcommSocketToServiceRecord(BluetoothUuid.PBAP_PSE.getUuid());
} else if (mPseRec.getL2capPsm() != L2CAP_INVALID_PSM) {
if (VDBG) Log.v(TAG, "connectSocket: PSM: " + mPseRec.getL2capPsm());
mSocket = mDevice.createL2capSocket(mPseRec.getL2capPsm());
} else {
if (VDBG) Log.v(TAG, "connectSocket: channel: " + mPseRec.getRfcommChannelNumber());
mSocket = mDevice.createRfcommSocket(mPseRec.getRfcommChannelNumber());
}
if (mSocket != null) {
mSocket.connect();
return true;
} else {
Log.w(TAG, "Could not create socket");
}
} catch (IOException e) {
Log.e(TAG, "Error while connecting socket", e);
}
return false;
}
/* Connect an OBEX session over the already connected socket. First establish an OBEX Transport
* abstraction, then establish a Bluetooth Authenticator, and finally issue the connect call */
private boolean connectObexSession() {
boolean connectionSuccessful = false;
try {
if (VDBG) {
Log.v(TAG, "Start Obex Client Session");
}
BluetoothObexTransport transport = new BluetoothObexTransport(mSocket);
mObexSession = new ClientSession(transport);
mObexSession.setAuthenticator(mAuth);
HeaderSet connectionRequest = new HeaderSet();
connectionRequest.setHeader(HeaderSet.TARGET, PBAP_TARGET);
if (mPseRec != null) {
if (DBG) {
Log.d(TAG, "Remote PbapSupportedFeatures " + mPseRec.getSupportedFeatures());
}
ObexAppParameters oap = new ObexAppParameters();
if (mPseRec.getProfileVersion() >= PBAP_V1_2) {
oap.add(BluetoothPbapRequest.OAP_TAGID_PBAP_SUPPORTED_FEATURES,
PBAP_SUPPORTED_FEATURE);
}
oap.addToHeaderSet(connectionRequest);
}
HeaderSet connectionResponse = mObexSession.connect(connectionRequest);
connectionSuccessful =
(connectionResponse.getResponseCode() == ResponseCodes.OBEX_HTTP_OK);
if (DBG) {
Log.d(TAG, "Success = " + Boolean.toString(connectionSuccessful));
}
} catch (IOException | NullPointerException e) {
// Will get NPE if a null mSocket is passed to BluetoothObexTransport.
// mSocket can be set to null if an abort() --> closeSocket() was called between
// the calls to connectSocket() and connectObexSession().
Log.w(TAG, "CONNECT Failure " + e.toString());
closeSocket();
}
return connectionSuccessful;
}
public void abort() {
// Perform forced cleanup, it is ok if the handler throws an exception this will free the
// handler to complete what it is doing and finish with cleanup.
closeSocket();
this.getLooper().getThread().interrupt();
}
private synchronized void closeSocket() {
try {
if (mSocket != null) {
if (DBG) {
Log.d(TAG, "Closing socket" + mSocket);
}
mSocket.close();
mSocket = null;
}
} catch (IOException e) {
Log.e(TAG, "Error when closing socket", e);
mSocket = null;
}
}
void downloadContacts(String path) {
try {
PhonebookPullRequest processor =
new PhonebookPullRequest(mPbapClientStateMachine.getContext(),
mAccount);
// Download contacts in batches of size DEFAULT_BATCH_SIZE
BluetoothPbapRequestPullPhoneBookSize requestPbSize =
new BluetoothPbapRequestPullPhoneBookSize(path,
PBAP_REQUESTED_FIELDS);
requestPbSize.execute(mObexSession);
int numberOfContactsRemaining = requestPbSize.getSize();
int startOffset = 0;
if (PB_PATH.equals(path)) {
// PBAP v1.2.3, Sec 3.1.5. The first contact in pb is owner card 0.vcf, which we
// do not want to download. The other phonebook objects (e.g., fav) don't have an
// owner card, so they don't need an offset.
startOffset = 1;
// "-1" because Owner Card 0.vcf is also included in /pb, but not in /fav.
numberOfContactsRemaining -= 1;
}
while ((numberOfContactsRemaining > 0) && (startOffset <= UPPER_LIMIT)) {
int numberOfContactsToDownload =
Math.min(Math.min(DEFAULT_BATCH_SIZE, numberOfContactsRemaining),
UPPER_LIMIT - startOffset + 1);
BluetoothPbapRequestPullPhoneBook request =
new BluetoothPbapRequestPullPhoneBook(path, mAccount,
PBAP_REQUESTED_FIELDS, VCARD_TYPE_30,
numberOfContactsToDownload, startOffset);
request.execute(mObexSession);
ArrayList<VCardEntry> vcards = request.getList();
if (path == FAV_PATH) {
// mark each vcard as a favorite
for (VCardEntry v : vcards) {
v.setStarred(true);
}
}
processor.setResults(vcards);
processor.onPullComplete();
startOffset += numberOfContactsToDownload;
numberOfContactsRemaining -= numberOfContactsToDownload;
}
if ((startOffset > UPPER_LIMIT) && (numberOfContactsRemaining > 0)) {
Log.w(TAG, "Download contacts incomplete, index exceeded upper limit.");
}
} catch (IOException e) {
Log.w(TAG, "Download contacts failure" + e.toString());
}
}
void downloadCallLog(String path, HashMap<String, Integer> callCounter) {
try {
BluetoothPbapRequestPullPhoneBook request =
new BluetoothPbapRequestPullPhoneBook(path, mAccount, 0, VCARD_TYPE_30, 0, 0);
request.execute(mObexSession);
CallLogPullRequest processor =
new CallLogPullRequest(mPbapClientStateMachine.getContext(), path,
callCounter, mAccount);
processor.setResults(request.getList());
processor.onPullComplete();
} catch (IOException e) {
Log.w(TAG, "Download call log failure");
}
}
private boolean addAccount(Account account) {
if (mAccountManager.addAccountExplicitly(account, null, null)) {
if (DBG) {
Log.d(TAG, "Added account " + mAccount);
}
return true;
}
return false;
}
private void removeAccount(Account account) {
if (mAccountManager.removeAccountExplicitly(account)) {
if (DBG) {
Log.d(TAG, "Removed account " + account);
}
} else {
Log.e(TAG, "Failed to remove account " + mAccount);
}
}
private void removeCallLog(Account account) {
try {
// need to check call table is exist ?
if (mContext.getContentResolver() == null) {
if (DBG) {
Log.d(TAG, "CallLog ContentResolver is not found");
}
return;
}
mContext.getContentResolver().delete(CallLog.Calls.CONTENT_URI,
Calls.PHONE_ACCOUNT_ID + "=?", new String[]{mAccount.name});
} catch (IllegalArgumentException e) {
Log.d(TAG, "Call Logs could not be deleted, they may not exist yet.");
}
}
private boolean isRepositorySupported(int mask) {
if (mPseRec == null) {
if (VDBG) Log.v(TAG, "No PBAP Server SDP Record");
return false;
}
return (mask & mPseRec.getSupportedRepositories()) != 0;
}
}