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