• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         }
300 
301         @Override
processMessage(Message message)302         public boolean processMessage(Message message) {
303             switch (message.what) {
304                 case DISCONNECT:
305                     stopObexServerSession();
306                     break;
307                 case CREATE_NOTIFICATION:
308                     createPbapNotification();
309                     break;
310                 case REMOVE_NOTIFICATION:
311                     Intent i = new Intent(BluetoothPbapService.USER_CONFIRM_TIMEOUT_ACTION);
312                     mService.sendBroadcast(i);
313                     notifyAuthCancelled();
314                     removePbapNotification(mNotificationId);
315                     break;
316                 case AUTH_KEY_INPUT:
317                     String key = (String) message.obj;
318                     notifyAuthKeyInput(key);
319                     break;
320                 case AUTH_CANCELLED:
321                     notifyAuthCancelled();
322                     break;
323             }
324             return HANDLED;
325         }
326 
startObexServerSession()327         private void startObexServerSession() throws IOException {
328             if (VERBOSE) {
329                 Log.v(TAG, "Pbap Service startObexServerSession");
330             }
331 
332             // acquire the wakeLock before start Obex transaction thread
333             mServiceHandler.sendMessage(
334                     mServiceHandler.obtainMessage(BluetoothPbapService.MSG_ACQUIRE_WAKE_LOCK));
335 
336             mPbapServer =
337                     new BluetoothPbapObexServer(mServiceHandler, mService, PbapStateMachine.this);
338             synchronized (this) {
339                 mObexAuth = new BluetoothPbapAuthenticator(PbapStateMachine.this);
340                 mObexAuth.setChallenged(false);
341                 mObexAuth.setCancelled(false);
342             }
343             BluetoothObexTransport transport = new BluetoothObexTransport(mConnSocket);
344             mServerSession = new ServerSession(transport, mPbapServer, mObexAuth);
345             // It's ok to just use one wake lock
346             // Message MSG_ACQUIRE_WAKE_LOCK is always surrounded by RELEASE. safe.
347         }
348 
stopObexServerSession()349         private void stopObexServerSession() {
350             if (VERBOSE) {
351                 Log.v(TAG, "Pbap Service stopObexServerSession");
352             }
353             transitionTo(mFinished);
354         }
355 
createPbapNotification()356         private void createPbapNotification() {
357             NotificationManager nm =
358                     (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE);
359             NotificationChannel notificationChannel =
360                     new NotificationChannel(PBAP_OBEX_NOTIFICATION_CHANNEL,
361                             mService.getString(R.string.pbap_notification_group),
362                             NotificationManager.IMPORTANCE_HIGH);
363             nm.createNotificationChannel(notificationChannel);
364 
365             // Create an intent triggered by clicking on the status icon.
366             Intent clickIntent = new Intent();
367             clickIntent.setClass(mService, BluetoothPbapActivity.class);
368             clickIntent.putExtra(BluetoothPbapService.EXTRA_DEVICE, mRemoteDevice);
369             clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
370             clickIntent.setAction(BluetoothPbapService.AUTH_CHALL_ACTION);
371 
372             // Create an intent triggered by clicking on the
373             // "Clear All Notifications" button
374             Intent deleteIntent = new Intent();
375             deleteIntent.setClass(mService, BluetoothPbapService.class);
376             deleteIntent.setAction(BluetoothPbapService.AUTH_CANCELLED_ACTION);
377 
378             String name = mRemoteDevice.getName();
379 
380             Notification notification =
381                     new Notification.Builder(mService, PBAP_OBEX_NOTIFICATION_CHANNEL).setWhen(
382                             System.currentTimeMillis())
383                             .setContentTitle(mService.getString(R.string.auth_notif_title))
384                             .setContentText(mService.getString(R.string.auth_notif_message, name))
385                             .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
386                             .setTicker(mService.getString(R.string.auth_notif_ticker))
387                             .setColor(mService.getResources()
388                                     .getColor(
389                                             com.android.internal.R.color
390                                                     .system_notification_accent_color,
391                                             mService.getTheme()))
392                             .setFlag(Notification.FLAG_AUTO_CANCEL, true)
393                             .setFlag(Notification.FLAG_ONLY_ALERT_ONCE, true)
394                             .setContentIntent(
395                                     PendingIntent.getActivity(mService, 0, clickIntent, 0))
396                             .setDeleteIntent(
397                                     PendingIntent.getBroadcast(mService, 0, deleteIntent, 0))
398                             .setLocalOnly(true)
399                             .build();
400             nm.notify(mNotificationId, notification);
401         }
402 
removePbapNotification(int id)403         private void removePbapNotification(int id) {
404             NotificationManager nm =
405                     (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE);
406             nm.cancel(id);
407         }
408 
notifyAuthCancelled()409         private synchronized void notifyAuthCancelled() {
410             mObexAuth.setCancelled(true);
411         }
412 
notifyAuthKeyInput(final String key)413         private synchronized void notifyAuthKeyInput(final String key) {
414             if (key != null) {
415                 mObexAuth.setSessionKey(key);
416             }
417             mObexAuth.setChallenged(true);
418         }
419     }
420 
421     /**
422      * Get the current connection state of this state machine
423      *
424      * @return current connection state, one of {@link BluetoothProfile#STATE_DISCONNECTED},
425      * {@link BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_CONNECTED}, or
426      * {@link BluetoothProfile#STATE_DISCONNECTING}
427      */
getConnectionState()428     synchronized int getConnectionState() {
429         PbapStateBase state = (PbapStateBase) getCurrentState();
430         if (state == null) {
431             return BluetoothProfile.STATE_DISCONNECTED;
432         }
433         return state.getConnectionStateInt();
434     }
435 
436     @Override
log(String msg)437     protected void log(String msg) {
438         if (DEBUG) {
439             super.log(msg);
440         }
441     }
442 }
443