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