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