/* * 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 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 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 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; } }