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