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