Home | History | Annotate | Download | only in pbapclient
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.bluetooth.pbapclient;
     17 
     18 import android.accounts.Account;
     19 import android.accounts.AccountManager;
     20 import android.bluetooth.BluetoothAdapter;
     21 import android.bluetooth.BluetoothDevice;
     22 import android.bluetooth.BluetoothSocket;
     23 import android.bluetooth.BluetoothUuid;
     24 import android.bluetooth.SdpPseRecord;
     25 import android.content.Context;
     26 import android.os.Handler;
     27 import android.os.Looper;
     28 import android.os.Message;
     29 import android.provider.CallLog;
     30 import android.provider.CallLog.Calls;
     31 import android.util.Log;
     32 
     33 import com.android.bluetooth.BluetoothObexTransport;
     34 import com.android.bluetooth.R;
     35 
     36 import java.io.IOException;
     37 import java.util.HashMap;
     38 
     39 import javax.obex.ClientSession;
     40 import javax.obex.HeaderSet;
     41 import javax.obex.ResponseCodes;
     42 
     43 /* Bluetooth/pbapclient/PbapClientConnectionHandler is responsible
     44  * for connecting, disconnecting and downloading contacts from the
     45  * PBAP PSE when commanded. It receives all direction from the
     46  * controlling state machine.
     47  */
     48 class PbapClientConnectionHandler extends Handler {
     49     static final String TAG = "PBAP PCE handler";
     50     static final boolean DBG = true;
     51     static final int MSG_CONNECT = 1;
     52     static final int MSG_DISCONNECT = 2;
     53     static final int MSG_DOWNLOAD = 3;
     54 
     55     // The following constants are pulled from the Bluetooth Phone Book Access Profile specification
     56     // 1.1
     57     private static final byte[] PBAP_TARGET = new byte[]{
     58             0x79,
     59             0x61,
     60             0x35,
     61             (byte) 0xf0,
     62             (byte) 0xf0,
     63             (byte) 0xc5,
     64             0x11,
     65             (byte) 0xd8,
     66             0x09,
     67             0x66,
     68             0x08,
     69             0x00,
     70             0x20,
     71             0x0c,
     72             (byte) 0x9a,
     73             0x66
     74     };
     75 
     76     private static final int PBAP_FEATURE_DEFAULT_IMAGE_FORMAT = 0x00000200;
     77     private static final int PBAP_FEATURE_BROWSING = 0x00000002;
     78     private static final int PBAP_FEATURE_DOWNLOADING = 0x00000001;
     79 
     80     private static final long PBAP_FILTER_VERSION = 1 << 0;
     81     private static final long PBAP_FILTER_FN = 1 << 1;
     82     private static final long PBAP_FILTER_N = 1 << 2;
     83     private static final long PBAP_FILTER_PHOTO = 1 << 3;
     84     private static final long PBAP_FILTER_ADR = 1 << 5;
     85     private static final long PBAP_FILTER_TEL = 1 << 7;
     86     private static final long PBAP_FILTER_EMAIL = 1 << 8;
     87     private static final long PBAP_FILTER_NICKNAME = 1 << 23;
     88 
     89     private static final int PBAP_SUPPORTED_FEATURE =
     90             PBAP_FEATURE_DEFAULT_IMAGE_FORMAT | PBAP_FEATURE_BROWSING | PBAP_FEATURE_DOWNLOADING;
     91     private static final long PBAP_REQUESTED_FIELDS =
     92             PBAP_FILTER_VERSION | PBAP_FILTER_FN | PBAP_FILTER_N | PBAP_FILTER_PHOTO
     93                     | PBAP_FILTER_ADR | PBAP_FILTER_EMAIL | PBAP_FILTER_TEL | PBAP_FILTER_NICKNAME;
     94     private static final int PBAP_V1_2 = 0x0102;
     95     private static final int L2CAP_INVALID_PSM = -1;
     96 
     97     public static final String PB_PATH = "telecom/pb.vcf";
     98     public static final String MCH_PATH = "telecom/mch.vcf";
     99     public static final String ICH_PATH = "telecom/ich.vcf";
    100     public static final String OCH_PATH = "telecom/och.vcf";
    101 
    102     public static final byte VCARD_TYPE_21 = 0;
    103     public static final byte VCARD_TYPE_30 = 1;
    104 
    105     private Account mAccount;
    106     private AccountManager mAccountManager;
    107     private BluetoothSocket mSocket;
    108     private final BluetoothAdapter mAdapter;
    109     private final BluetoothDevice mDevice;
    110     // PSE SDP Record for current device.
    111     private SdpPseRecord mPseRec = null;
    112     private ClientSession mObexSession;
    113     private Context mContext;
    114     private BluetoothPbapObexAuthenticator mAuth = null;
    115     private final PbapClientStateMachine mPbapClientStateMachine;
    116     private boolean mAccountCreated;
    117 
    118     PbapClientConnectionHandler(Looper looper, Context context, PbapClientStateMachine stateMachine,
    119             BluetoothDevice device) {
    120         super(looper);
    121         mAdapter = BluetoothAdapter.getDefaultAdapter();
    122         mDevice = device;
    123         mContext = context;
    124         mPbapClientStateMachine = stateMachine;
    125         mAuth = new BluetoothPbapObexAuthenticator(this);
    126         mAccountManager = AccountManager.get(mPbapClientStateMachine.getContext());
    127         mAccount =
    128                 new Account(mDevice.getAddress(), mContext.getString(R.string.pbap_account_type));
    129     }
    130 
    131     /**
    132      * Constructs PCEConnectionHandler object
    133      *
    134      * @param Builder To build  BluetoothPbapClientHandler Instance.
    135      */
    136     PbapClientConnectionHandler(Builder pceHandlerbuild) {
    137         super(pceHandlerbuild.mLooper);
    138         mAdapter = BluetoothAdapter.getDefaultAdapter();
    139         mDevice = pceHandlerbuild.mDevice;
    140         mContext = pceHandlerbuild.mContext;
    141         mPbapClientStateMachine = pceHandlerbuild.mClientStateMachine;
    142         mAuth = new BluetoothPbapObexAuthenticator(this);
    143         mAccountManager = AccountManager.get(mPbapClientStateMachine.getContext());
    144         mAccount =
    145                 new Account(mDevice.getAddress(), mContext.getString(R.string.pbap_account_type));
    146     }
    147 
    148     public static class Builder {
    149 
    150         private Looper mLooper;
    151         private Context mContext;
    152         private BluetoothDevice mDevice;
    153         private PbapClientStateMachine mClientStateMachine;
    154 
    155         public Builder setLooper(Looper loop) {
    156             this.mLooper = loop;
    157             return this;
    158         }
    159 
    160         public Builder setClientSM(PbapClientStateMachine clientStateMachine) {
    161             this.mClientStateMachine = clientStateMachine;
    162             return this;
    163         }
    164 
    165         public Builder setRemoteDevice(BluetoothDevice device) {
    166             this.mDevice = device;
    167             return this;
    168         }
    169 
    170         public Builder setContext(Context context) {
    171             this.mContext = context;
    172             return this;
    173         }
    174 
    175         public PbapClientConnectionHandler build() {
    176             PbapClientConnectionHandler pbapClientHandler = new PbapClientConnectionHandler(this);
    177             return pbapClientHandler;
    178         }
    179 
    180     }
    181 
    182     @Override
    183     public void handleMessage(Message msg) {
    184         if (DBG) {
    185             Log.d(TAG, "Handling Message = " + msg.what);
    186         }
    187         switch (msg.what) {
    188             case MSG_CONNECT:
    189                 mPseRec = (SdpPseRecord) msg.obj;
    190                 /* To establish a connection, first open a socket and then create an OBEX session */
    191                 if (connectSocket()) {
    192                     if (DBG) {
    193                         Log.d(TAG, "Socket connected");
    194                     }
    195                 } else {
    196                     Log.w(TAG, "Socket CONNECT Failure ");
    197                     mPbapClientStateMachine.obtainMessage(
    198                             PbapClientStateMachine.MSG_CONNECTION_FAILED).sendToTarget();
    199                     return;
    200                 }
    201 
    202                 if (connectObexSession()) {
    203                     mPbapClientStateMachine.obtainMessage(
    204                             PbapClientStateMachine.MSG_CONNECTION_COMPLETE).sendToTarget();
    205                 } else {
    206                     mPbapClientStateMachine.obtainMessage(
    207                             PbapClientStateMachine.MSG_CONNECTION_FAILED).sendToTarget();
    208                 }
    209                 break;
    210 
    211             case MSG_DISCONNECT:
    212                 if (DBG) {
    213                     Log.d(TAG, "Starting Disconnect");
    214                 }
    215                 try {
    216                     if (mObexSession != null) {
    217                         if (DBG) {
    218                             Log.d(TAG, "obexSessionDisconnect" + mObexSession);
    219                         }
    220                         mObexSession.disconnect(null);
    221                         mObexSession.close();
    222                     }
    223 
    224                     if (DBG) {
    225                         Log.d(TAG, "Closing Socket");
    226                     }
    227                     closeSocket();
    228                 } catch (IOException e) {
    229                     Log.w(TAG, "DISCONNECT Failure ", e);
    230                 }
    231                 if (DBG) {
    232                     Log.d(TAG, "Completing Disconnect");
    233                 }
    234                 removeAccount(mAccount);
    235                 removeCallLog(mAccount);
    236 
    237                 mPbapClientStateMachine.obtainMessage(PbapClientStateMachine.MSG_CONNECTION_CLOSED)
    238                     .sendToTarget();
    239                 break;
    240 
    241             case MSG_DOWNLOAD:
    242                 try {
    243                     mAccountCreated = addAccount(mAccount);
    244                     if (!mAccountCreated) {
    245                         Log.e(TAG, "Account creation failed.");
    246                         return;
    247                     }
    248                     // Start at contact 1 to exclued Owner Card PBAP 1.1 sec 3.1.5.2
    249                     BluetoothPbapRequestPullPhoneBook request =
    250                             new BluetoothPbapRequestPullPhoneBook(PB_PATH, mAccount,
    251                                     PBAP_REQUESTED_FIELDS, VCARD_TYPE_30, 0, 1);
    252                     request.execute(mObexSession);
    253                     PhonebookPullRequest processor =
    254                             new PhonebookPullRequest(mPbapClientStateMachine.getContext(),
    255                                     mAccount);
    256                     processor.setResults(request.getList());
    257                     processor.onPullComplete();
    258                     HashMap<String, Integer> callCounter = new HashMap<>();
    259                     downloadCallLog(MCH_PATH, callCounter);
    260                     downloadCallLog(ICH_PATH, callCounter);
    261                     downloadCallLog(OCH_PATH, callCounter);
    262                 } catch (IOException e) {
    263                     Log.w(TAG, "DOWNLOAD_CONTACTS Failure" + e.toString());
    264                 }
    265                 break;
    266 
    267             default:
    268                 Log.w(TAG, "Received Unexpected Message");
    269         }
    270         return;
    271     }
    272 
    273     /* Utilize SDP, if available, to create a socket connection over L2CAP, RFCOMM specified
    274      * channel, or RFCOMM default channel. */
    275     private boolean connectSocket() {
    276         try {
    277             /* Use BluetoothSocket to connect */
    278             if (mPseRec == null) {
    279                 // BackWardCompatability: Fall back to create RFCOMM through UUID.
    280                 Log.v(TAG, "connectSocket: UUID: " + BluetoothUuid.PBAP_PSE.getUuid());
    281                 mSocket =
    282                         mDevice.createRfcommSocketToServiceRecord(BluetoothUuid.PBAP_PSE.getUuid());
    283             } else if (mPseRec.getL2capPsm() != L2CAP_INVALID_PSM) {
    284                 Log.v(TAG, "connectSocket: PSM: " + mPseRec.getL2capPsm());
    285                 mSocket = mDevice.createL2capSocket(mPseRec.getL2capPsm());
    286             } else {
    287                 Log.v(TAG, "connectSocket: channel: " + mPseRec.getRfcommChannelNumber());
    288                 mSocket = mDevice.createRfcommSocket(mPseRec.getRfcommChannelNumber());
    289             }
    290 
    291             if (mSocket != null) {
    292                 mSocket.connect();
    293                 return true;
    294             } else {
    295                 Log.w(TAG, "Could not create socket");
    296             }
    297         } catch (IOException e) {
    298             Log.e(TAG, "Error while connecting socket", e);
    299         }
    300         return false;
    301     }
    302 
    303     /* Connect an OBEX session over the already connected socket.  First establish an OBEX Transport
    304      * abstraction, then establish a Bluetooth Authenticator, and finally issue the connect call */
    305     private boolean connectObexSession() {
    306         boolean connectionSuccessful = false;
    307 
    308         try {
    309             if (DBG) {
    310                 Log.v(TAG, "Start Obex Client Session");
    311             }
    312             BluetoothObexTransport transport = new BluetoothObexTransport(mSocket);
    313             mObexSession = new ClientSession(transport);
    314             mObexSession.setAuthenticator(mAuth);
    315 
    316             HeaderSet connectionRequest = new HeaderSet();
    317             connectionRequest.setHeader(HeaderSet.TARGET, PBAP_TARGET);
    318 
    319             if (mPseRec != null) {
    320                 if (DBG) {
    321                     Log.d(TAG, "Remote PbapSupportedFeatures " + mPseRec.getSupportedFeatures());
    322                 }
    323 
    324                 ObexAppParameters oap = new ObexAppParameters();
    325 
    326                 if (mPseRec.getProfileVersion() >= PBAP_V1_2) {
    327                     oap.add(BluetoothPbapRequest.OAP_TAGID_PBAP_SUPPORTED_FEATURES,
    328                             PBAP_SUPPORTED_FEATURE);
    329                 }
    330 
    331                 oap.addToHeaderSet(connectionRequest);
    332             }
    333             HeaderSet connectionResponse = mObexSession.connect(connectionRequest);
    334 
    335             connectionSuccessful =
    336                     (connectionResponse.getResponseCode() == ResponseCodes.OBEX_HTTP_OK);
    337             if (DBG) {
    338                 Log.d(TAG, "Success = " + Boolean.toString(connectionSuccessful));
    339             }
    340         } catch (IOException e) {
    341             Log.w(TAG, "CONNECT Failure " + e.toString());
    342             closeSocket();
    343         }
    344         return connectionSuccessful;
    345     }
    346 
    347     public void abort() {
    348         // Perform forced cleanup, it is ok if the handler throws an exception this will free the
    349         // handler to complete what it is doing and finish with cleanup.
    350         closeSocket();
    351         this.getLooper().getThread().interrupt();
    352     }
    353 
    354     private void closeSocket() {
    355         try {
    356             if (mSocket != null) {
    357                 if (DBG) {
    358                     Log.d(TAG, "Closing socket" + mSocket);
    359                 }
    360                 mSocket.close();
    361                 mSocket = null;
    362             }
    363         } catch (IOException e) {
    364             Log.e(TAG, "Error when closing socket", e);
    365             mSocket = null;
    366         }
    367     }
    368 
    369     void downloadCallLog(String path, HashMap<String, Integer> callCounter) {
    370         try {
    371             BluetoothPbapRequestPullPhoneBook request =
    372                     new BluetoothPbapRequestPullPhoneBook(path, mAccount, 0, VCARD_TYPE_30, 0, 0);
    373             request.execute(mObexSession);
    374             CallLogPullRequest processor =
    375                     new CallLogPullRequest(mPbapClientStateMachine.getContext(), path,
    376                         callCounter, mAccount);
    377             processor.setResults(request.getList());
    378             processor.onPullComplete();
    379         } catch (IOException e) {
    380             Log.w(TAG, "Download call log failure");
    381         }
    382     }
    383 
    384     private boolean addAccount(Account account) {
    385         if (mAccountManager.addAccountExplicitly(account, null, null)) {
    386             if (DBG) {
    387                 Log.d(TAG, "Added account " + mAccount);
    388             }
    389             return true;
    390         }
    391         return false;
    392     }
    393 
    394     private void removeAccount(Account account) {
    395         if (mAccountManager.removeAccountExplicitly(account)) {
    396             if (DBG) {
    397                 Log.d(TAG, "Removed account " + account);
    398             }
    399         } else {
    400             Log.e(TAG, "Failed to remove account " + mAccount);
    401         }
    402     }
    403 
    404     private void removeCallLog(Account account) {
    405         try {
    406             // need to check call table is exist ?
    407             if (mContext.getContentResolver() == null) {
    408                 if (DBG) {
    409                     Log.d(TAG, "CallLog ContentResolver is not found");
    410                 }
    411                 return;
    412             }
    413             String where = Calls.PHONE_ACCOUNT_ID + "=" + account.hashCode();
    414             mContext.getContentResolver().delete(CallLog.Calls.CONTENT_URI, where, null);
    415         } catch (IllegalArgumentException e) {
    416             Log.d(TAG, "Call Logs could not be deleted, they may not exist yet.");
    417         }
    418     }
    419 }
    420