PbapStateMachine.java revision e402f0cb470f1433a50ae427495bb0b4c81238fd
1/*
2 * Copyright 2017 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
17package com.android.bluetooth.pbap;
18
19import android.annotation.NonNull;
20import android.app.Notification;
21import android.app.NotificationChannel;
22import android.app.NotificationManager;
23import android.app.PendingIntent;
24import android.bluetooth.BluetoothDevice;
25import android.bluetooth.BluetoothProfile;
26import android.bluetooth.BluetoothSocket;
27import android.content.Context;
28import android.content.Intent;
29import android.os.Handler;
30import android.os.Looper;
31import android.os.Message;
32import android.util.Log;
33
34import com.android.bluetooth.BluetoothObexTransport;
35import com.android.bluetooth.IObexConnectionHandler;
36import com.android.bluetooth.ObexRejectServer;
37import com.android.bluetooth.R;
38import com.android.internal.util.State;
39import com.android.internal.util.StateMachine;
40
41import java.io.IOException;
42
43import javax.obex.ResponseCodes;
44import javax.obex.ServerSession;
45
46/**
47 * Bluetooth PBAP StateMachine
48 *              (New connection socket)
49 *                 WAITING FOR AUTH
50 *                        |
51 *                        |    (request permission from Settings UI)
52 *                        |
53 *           (Accept)    / \   (Reject)
54 *                      /   \
55 *                     v     v
56 *          CONNECTED   ----->  FINISHED
57 *                (OBEX Server done)
58 */
59class PbapStateMachine extends StateMachine {
60    private static final String TAG = "PbapStateMachine";
61    private static final boolean DEBUG = true;
62    private static final boolean VERBOSE = true;
63
64    private static final String PBAP_OBEX_NOTIFICATION_CHANNEL = "pbap_obex_notification_channel";
65    private static final int NOTIFICATION_ID_AUTH = -1000002;
66    // TODO: set a notification channel for each sm
67
68    static final int AUTHORIZED = 1;
69    static final int REJECTED = 2;
70    static final int DISCONNECT = 3;
71    static final int REQUEST_PERMISSION = 4;
72    static final int CREATE_NOTIFICATION = 5;
73    static final int REMOVE_NOTIFICATION = 6;
74    static final int AUTH_KEY_INPUT = 7;
75    static final int AUTH_CANCELLED = 8;
76
77    BluetoothPbapService mService;
78    IObexConnectionHandler mIObexConnectionHandler;
79
80    private final WaitingForAuth mWaitingForAuth = new WaitingForAuth();
81    private final Finished mFinished = new Finished();
82    private final Connected mConnected = new Connected();
83    private BluetoothDevice mRemoteDevice;
84    private Handler mServiceHandler;
85    private BluetoothSocket mConnSocket;
86    private BluetoothPbapObexServer mPbapServer;
87    private BluetoothPbapAuthenticator mObexAuth;
88    private ServerSession mServerSession;
89
90    private PbapStateMachine(@NonNull BluetoothPbapService service, Looper looper,
91            @NonNull BluetoothDevice device, @NonNull BluetoothSocket connSocket,
92            IObexConnectionHandler obexConnectionHandler, Handler pbapHandler) {
93        super(TAG, looper);
94        mService = service;
95        mIObexConnectionHandler = obexConnectionHandler;
96        mRemoteDevice = device;
97        mServiceHandler = pbapHandler;
98        mConnSocket = connSocket;
99
100        addState(mFinished);
101        addState(mWaitingForAuth);
102        addState(mConnected);
103        setInitialState(mWaitingForAuth);
104    }
105
106    static PbapStateMachine make(BluetoothPbapService service, Looper looper,
107            BluetoothDevice device, BluetoothSocket connSocket,
108            IObexConnectionHandler obexConnectionHandler, Handler pbapHandler) {
109        PbapStateMachine stateMachine = new PbapStateMachine(service, looper, device, connSocket,
110                obexConnectionHandler, pbapHandler);
111        stateMachine.start();
112        return stateMachine;
113    }
114
115    BluetoothDevice getRemoteDevice() {
116        return mRemoteDevice;
117    }
118
119    private abstract class PbapStateBase extends State {
120        /**
121         * Get a state value from {@link BluetoothProfile} that represents the connection state of
122         * this headset state
123         *
124         * @return a value in {@link BluetoothProfile#STATE_DISCONNECTED},
125         * {@link BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_CONNECTED}, or
126         * {@link BluetoothProfile#STATE_DISCONNECTING}
127         */
128        abstract int getConnectionStateInt();
129    }
130
131    class WaitingForAuth extends PbapStateBase {
132        @Override
133        int getConnectionStateInt() {
134            return BluetoothProfile.STATE_CONNECTING;
135        }
136
137        @Override
138        public void enter() {
139            mService.checkOrGetPhonebookPermission(PbapStateMachine.this);
140        }
141
142        @Override
143        public boolean processMessage(Message message) {
144            switch (message.what) {
145                case AUTHORIZED:
146                    transitionTo(mConnected);
147                    break;
148                case REJECTED:
149                    rejectConnection();
150                    transitionTo(mFinished);
151                    break;
152                case DISCONNECT:
153                    mServiceHandler.removeMessages(BluetoothPbapService.USER_TIMEOUT);
154                    Message msg = mServiceHandler.obtainMessage(
155                            BluetoothPbapService.USER_TIMEOUT);
156                    msg.obj = PbapStateMachine.this;
157                    msg.sendToTarget();
158                    transitionTo(mFinished);
159                    break;
160            }
161            return HANDLED;
162        }
163
164        private void rejectConnection() {
165            mPbapServer = new BluetoothPbapObexServer(mServiceHandler, mService,
166                    PbapStateMachine.this);
167            BluetoothObexTransport transport = new BluetoothObexTransport(mConnSocket);
168            ObexRejectServer server = new ObexRejectServer(ResponseCodes.OBEX_HTTP_UNAVAILABLE,
169                    mConnSocket);
170            try {
171                mServerSession = new ServerSession(transport, server, null);
172            } catch (IOException ex) {
173                Log.e(TAG, "Caught exception starting OBEX reject server session"
174                        + ex.toString());
175            }
176        }
177    }
178
179    class Finished extends PbapStateBase {
180        @Override
181        int getConnectionStateInt() {
182            return BluetoothProfile.STATE_DISCONNECTED;
183        }
184
185        @Override
186        public void enter() {
187            // Close OBEX server session
188            if (mServerSession != null) {
189                mServerSession.close();
190                mServerSession = null;
191            }
192
193            // Close connection socket
194            try {
195                mConnSocket.close();
196                mConnSocket = null;
197            } catch (IOException e) {
198                Log.e(TAG, "Close Connection Socket error: " + e.toString());
199            }
200
201            mServiceHandler.obtainMessage(BluetoothPbapService.MSG_STATE_MACHINE_DONE)
202                    .sendToTarget();
203        }
204
205    }
206
207    class Connected extends PbapStateBase {
208        @Override
209        int getConnectionStateInt() {
210            return BluetoothProfile.STATE_CONNECTED;
211        }
212
213        @Override
214        public void enter() {
215            try {
216                startObexServerSession();
217            } catch (IOException ex) {
218                Log.e(TAG, "Caught exception starting OBEX server session" + ex.toString());
219            }
220        }
221
222        @Override
223        public void exit() {
224        }
225
226        @Override
227        public boolean processMessage(Message message) {
228            switch (message.what) {
229                case DISCONNECT:
230                    stopObexServerSession();
231                    break;
232                case CREATE_NOTIFICATION:
233                    createPbapNotification();
234                    break;
235                case REMOVE_NOTIFICATION:
236                    Intent i = new Intent(BluetoothPbapService.USER_CONFIRM_TIMEOUT_ACTION);
237                    mService.sendBroadcast(i);
238                    notifyAuthCancelled();
239                    removePbapNotification(NOTIFICATION_ID_AUTH);
240                    break;
241                case AUTH_KEY_INPUT:
242                    String key = (String) message.obj;
243                    notifyAuthKeyInput(key);
244                    break;
245                case AUTH_CANCELLED:
246                    notifyAuthCancelled();
247                    break;
248            }
249            return HANDLED;
250        }
251
252        private void startObexServerSession() throws IOException {
253            if (VERBOSE) {
254                Log.v(TAG, "Pbap Service startObexServerSession");
255            }
256
257            // acquire the wakeLock before start Obex transaction thread
258            mServiceHandler.sendMessage(
259                    mServiceHandler.obtainMessage(BluetoothPbapService.MSG_ACQUIRE_WAKE_LOCK));
260
261            mPbapServer = new BluetoothPbapObexServer(mServiceHandler, mService,
262                    PbapStateMachine.this);
263            synchronized (this) {
264                mObexAuth = new BluetoothPbapAuthenticator(PbapStateMachine.this);
265                mObexAuth.setChallenged(false);
266                mObexAuth.setCancelled(false);
267            }
268            BluetoothObexTransport transport = new BluetoothObexTransport(mConnSocket);
269            mServerSession = new ServerSession(transport, mPbapServer, mObexAuth);
270            // It's ok to just use one wake lock
271            // Message MSG_ACQUIRE_WAKE_LOCK is always surrounded by RELEASE. safe.
272        }
273
274        private void stopObexServerSession() {
275            if (VERBOSE) {
276                Log.v(TAG, "Pbap Service stopObexServerSession");
277            }
278            transitionTo(mFinished);
279        }
280
281        private void createPbapNotification() {
282            NotificationManager nm =
283                    (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE);
284            NotificationChannel notificationChannel = new NotificationChannel(
285                    PBAP_OBEX_NOTIFICATION_CHANNEL,
286                    mService.getString(R.string.pbap_notification_group),
287                    NotificationManager.IMPORTANCE_HIGH);
288            nm.createNotificationChannel(notificationChannel);
289
290            // Create an intent triggered by clicking on the status icon.
291            Intent clickIntent = new Intent();
292            clickIntent.setClass(mService, BluetoothPbapActivity.class);
293            clickIntent.putExtra(BluetoothPbapService.EXTRA_DEVICE, mRemoteDevice);
294            clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
295            clickIntent.setAction(BluetoothPbapService.AUTH_CHALL_ACTION);
296
297            // Create an intent triggered by clicking on the
298            // "Clear All Notifications" button
299            Intent deleteIntent = new Intent();
300            deleteIntent.setClass(mService, BluetoothPbapService.class);
301            deleteIntent.setAction(BluetoothPbapService.AUTH_CANCELLED_ACTION);
302
303            String name = mRemoteDevice.getName();
304
305            Notification notification =
306                    new Notification.Builder(mService, PBAP_OBEX_NOTIFICATION_CHANNEL)
307                            .setWhen(System.currentTimeMillis())
308                            .setContentTitle(mService.getString(R.string.auth_notif_title))
309                            .setContentText(mService.getString(R.string.auth_notif_message, name))
310                            .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
311                            .setTicker(mService.getString(R.string.auth_notif_ticker))
312                            .setColor(mService.getResources().getColor(
313                                    com.android.internal.R.color.system_notification_accent_color,
314                                    mService.getTheme()))
315                            .setFlag(Notification.FLAG_AUTO_CANCEL, true)
316                            .setFlag(Notification.FLAG_ONLY_ALERT_ONCE, true)
317                            .setContentIntent(PendingIntent.getActivity(mService, 0, clickIntent,
318                                    0))
319                            .setDeleteIntent(PendingIntent.getBroadcast(mService, 0, deleteIntent,
320                                    0))
321                            .setLocalOnly(true)
322                            .build();
323            nm.notify(NOTIFICATION_ID_AUTH, notification);
324        }
325
326        private void removePbapNotification(int id) {
327            NotificationManager nm =
328                    (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE);
329            nm.cancel(id);
330        }
331
332        private void notifyAuthCancelled() {
333            synchronized (this) {
334                mObexAuth.setCancelled(true);
335                mObexAuth.notify();
336            }
337        }
338
339        private void notifyAuthKeyInput(final String key) {
340            synchronized (this) {
341                if (key != null) {
342                    mObexAuth.setSessionKey(key);
343                }
344                mObexAuth.setChallenged(true);
345                mObexAuth.notify();
346            }
347        }
348    }
349
350    /**
351     * Get the current connection state of this state machine
352     *
353     * @return current connection state, one of {@link BluetoothProfile#STATE_DISCONNECTED},
354     * {@link BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_CONNECTED}, or
355     * {@link BluetoothProfile#STATE_DISCONNECTING}
356     */
357    synchronized int getConnectionState() {
358        PbapStateBase state = (PbapStateBase) getCurrentState();
359        if (state == null) {
360            return BluetoothProfile.STATE_DISCONNECTED;
361        }
362        return state.getConnectionStateInt();
363    }
364}
365