/* * Copyright 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth.pbap; import static android.Manifest.permission.BLUETOOTH_CONNECT; import android.annotation.NonNull; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothPbap; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProtoEnums; import android.bluetooth.BluetoothSocket; import android.content.Intent; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.UserHandle; import android.util.Log; import com.android.bluetooth.BluetoothMetricsProto; import com.android.bluetooth.BluetoothObexTransport; import com.android.bluetooth.BluetoothStatsLog; import com.android.bluetooth.ObexRejectServer; import com.android.bluetooth.R; import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting.Visibility; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.obex.ResponseCodes; import com.android.obex.ServerSession; import java.io.IOException; /** * Bluetooth PBAP StateMachine (New connection socket) WAITING FOR AUTH | | (request permission from * Settings UI) | (Accept) / \ (Reject) / \ v v CONNECTED -----> FINISHED (OBEX Server done) */ // Next tag value for ContentProfileErrorReportUtils.report(): 3 @VisibleForTesting(visibility = Visibility.PACKAGE) public class PbapStateMachine extends StateMachine { private static final String TAG = "PbapStateMachine"; private static final String PBAP_OBEX_NOTIFICATION_CHANNEL = "pbap_obex_notification_channel"; static final int AUTHORIZED = 1; static final int REJECTED = 2; static final int DISCONNECT = 3; static final int REQUEST_PERMISSION = 4; static final int CREATE_NOTIFICATION = 5; static final int REMOVE_NOTIFICATION = 6; static final int AUTH_KEY_INPUT = 7; static final int AUTH_CANCELLED = 8; /** Used to limit PBAP OBEX maximum packet size in order to reduce transaction time. */ private static final int PBAP_OBEX_MAXIMUM_PACKET_SIZE = 8192; private BluetoothPbapService mService; private final WaitingForAuth mWaitingForAuth = new WaitingForAuth(); private final Finished mFinished = new Finished(); private final Connected mConnected = new Connected(); private PbapStateBase mPrevState; private BluetoothDevice mRemoteDevice; private Handler mServiceHandler; private BluetoothSocket mConnSocket; private BluetoothPbapObexServer mPbapServer; private BluetoothPbapAuthenticator mObexAuth; private ServerSession mServerSession; private int mNotificationId; private PbapStateMachine( @NonNull BluetoothPbapService service, Looper looper, @NonNull BluetoothDevice device, @NonNull BluetoothSocket connSocket, Handler pbapHandler, int notificationId) { super(TAG, looper); // Let the logging framework enforce the log level. TAG is set above in the parent // constructor. setDbg(true); mService = service; mRemoteDevice = device; mServiceHandler = pbapHandler; mConnSocket = connSocket; mNotificationId = notificationId; addState(mFinished); addState(mWaitingForAuth); addState(mConnected); setInitialState(mWaitingForAuth); } static PbapStateMachine make( BluetoothPbapService service, Looper looper, BluetoothDevice device, BluetoothSocket connSocket, Handler pbapHandler, int notificationId) { PbapStateMachine stateMachine = new PbapStateMachine( service, looper, device, connSocket, pbapHandler, notificationId); stateMachine.start(); return stateMachine; } BluetoothDevice getRemoteDevice() { return mRemoteDevice; } private abstract class PbapStateBase extends State { /** * Get a state value from {@link BluetoothProfile} that represents the connection state of * this headset state * * @return a value in {@link BluetoothProfile#STATE_DISCONNECTED}, {@link * BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_CONNECTED}, or * {@link BluetoothProfile#STATE_DISCONNECTING} */ abstract int getConnectionStateInt(); @Override public void enter() { // Crash if mPrevState is null and state is not Disconnected if (!(this instanceof WaitingForAuth) && mPrevState == null) { throw new IllegalStateException("mPrevState is null on entering initial state"); } enforceValidConnectionStateTransition(); } @Override public void exit() { mPrevState = this; } // Should not be called from enter() method private void broadcastConnectionState(BluetoothDevice device, int fromState, int toState) { stateLogD("broadcastConnectionState " + device + ": " + fromState + "->" + toState); AdapterService adapterService = AdapterService.getAdapterService(); if (adapterService != null) { adapterService.updateProfileConnectionAdapterProperties( device, BluetoothProfile.PBAP, toState, fromState); } Intent intent = new Intent(BluetoothPbap.ACTION_CONNECTION_STATE_CHANGED); intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, fromState); intent.putExtra(BluetoothProfile.EXTRA_STATE, toState); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); mService.sendBroadcastAsUser( intent, UserHandle.ALL, BLUETOOTH_CONNECT, Utils.getTempBroadcastOptions().toBundle()); } /** Broadcast connection state change for this state machine */ void broadcastStateTransitions() { int prevStateInt = BluetoothProfile.STATE_DISCONNECTED; if (mPrevState != null) { prevStateInt = mPrevState.getConnectionStateInt(); } if (getConnectionStateInt() != prevStateInt) { stateLogD( "connection state changed: " + mRemoteDevice + ": " + mPrevState + " -> " + this); broadcastConnectionState(mRemoteDevice, prevStateInt, getConnectionStateInt()); } } /** * Verify if the current state transition is legal by design. This is called from enter() * method and crash if the state transition is not expected by the state machine design. * *

Note: This method uses state objects to verify transition because these objects should * be final and any other instances are invalid */ private void enforceValidConnectionStateTransition() { boolean isValidTransition = false; if (this == mWaitingForAuth) { isValidTransition = mPrevState == null; } else if (this == mFinished) { isValidTransition = mPrevState == mConnected || mPrevState == mWaitingForAuth; } else if (this == mConnected) { isValidTransition = mPrevState == mFinished || mPrevState == mWaitingForAuth; } if (!isValidTransition) { throw new IllegalStateException( "Invalid state transition from " + mPrevState + " to " + this + " for device " + mRemoteDevice); } } void stateLogD(String msg) { Log.d(TAG, getName() + ": currentDevice=" + mRemoteDevice + ", msg=" + msg); } } class WaitingForAuth extends PbapStateBase { @Override int getConnectionStateInt() { return BluetoothProfile.STATE_CONNECTING; } @Override public void enter() { super.enter(); broadcastStateTransitions(); } @Override public boolean processMessage(Message message) { switch (message.what) { case REQUEST_PERMISSION: mService.checkOrGetPhonebookPermission(PbapStateMachine.this); break; case AUTHORIZED: transitionTo(mConnected); break; case REJECTED: rejectConnection(); transitionTo(mFinished); break; case DISCONNECT: mServiceHandler.removeMessages( BluetoothPbapService.USER_TIMEOUT, PbapStateMachine.this); mServiceHandler .obtainMessage(BluetoothPbapService.USER_TIMEOUT, PbapStateMachine.this) .sendToTarget(); transitionTo(mFinished); break; } return HANDLED; } private void rejectConnection() { mPbapServer = new BluetoothPbapObexServer(mServiceHandler, mService, PbapStateMachine.this); BluetoothObexTransport transport = new BluetoothObexTransport( mConnSocket, PBAP_OBEX_MAXIMUM_PACKET_SIZE, BluetoothObexTransport.PACKET_SIZE_UNSPECIFIED); ObexRejectServer server = new ObexRejectServer(ResponseCodes.OBEX_HTTP_UNAVAILABLE, mConnSocket); try { mServerSession = new ServerSession(transport, server, null); } catch (IOException ex) { ContentProfileErrorReportUtils.report( BluetoothProfile.PBAP, BluetoothProtoEnums.BLUETOOTH_PBAP_STATE_MACHINE, BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 0); Log.e(TAG, "Caught exception starting OBEX reject server session" + ex.toString()); } } } class Finished extends PbapStateBase { @Override int getConnectionStateInt() { return BluetoothProfile.STATE_DISCONNECTED; } @Override public void enter() { super.enter(); // Close OBEX server session if (mServerSession != null) { mServerSession.close(); mServerSession = null; } // Close connection socket try { mConnSocket.close(); mConnSocket = null; } catch (IOException e) { ContentProfileErrorReportUtils.report( BluetoothProfile.PBAP, BluetoothProtoEnums.BLUETOOTH_PBAP_STATE_MACHINE, BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 1); Log.e(TAG, "Close Connection Socket error: " + e.toString()); } mServiceHandler .obtainMessage( BluetoothPbapService.MSG_STATE_MACHINE_DONE, PbapStateMachine.this) .sendToTarget(); broadcastStateTransitions(); } } class Connected extends PbapStateBase { @Override int getConnectionStateInt() { return BluetoothProfile.STATE_CONNECTED; } @Override public void enter() { try { startObexServerSession(); } catch (IOException ex) { ContentProfileErrorReportUtils.report( BluetoothProfile.PBAP, BluetoothProtoEnums.BLUETOOTH_PBAP_STATE_MACHINE, BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 2); Log.e(TAG, "Caught exception starting OBEX server session" + ex.toString()); } broadcastStateTransitions(); MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.PBAP); mService.setConnectionPolicy(mRemoteDevice, BluetoothProfile.CONNECTION_POLICY_ALLOWED); } @Override public boolean processMessage(Message message) { switch (message.what) { case DISCONNECT: stopObexServerSession(); break; case CREATE_NOTIFICATION: createPbapNotification(); break; case REMOVE_NOTIFICATION: Intent i = new Intent(BluetoothPbapService.USER_CONFIRM_TIMEOUT_ACTION); mService.sendBroadcast(i); notifyAuthCancelled(); removePbapNotification(mNotificationId); break; case AUTH_KEY_INPUT: String key = (String) message.obj; notifyAuthKeyInput(key); break; case AUTH_CANCELLED: notifyAuthCancelled(); break; } return HANDLED; } private void startObexServerSession() throws IOException { Log.v(TAG, "Pbap Service startObexServerSession"); // acquire the wakeLock before start Obex transaction thread mServiceHandler.sendMessage( mServiceHandler.obtainMessage(BluetoothPbapService.MSG_ACQUIRE_WAKE_LOCK)); mPbapServer = new BluetoothPbapObexServer(mServiceHandler, mService, PbapStateMachine.this); synchronized (this) { mObexAuth = new BluetoothPbapAuthenticator(PbapStateMachine.this); mObexAuth.setChallenged(false); mObexAuth.setCancelled(false); } BluetoothObexTransport transport = new BluetoothObexTransport( mConnSocket, PBAP_OBEX_MAXIMUM_PACKET_SIZE, BluetoothObexTransport.PACKET_SIZE_UNSPECIFIED); mServerSession = new ServerSession(transport, mPbapServer, mObexAuth); // It's ok to just use one wake lock // Message MSG_ACQUIRE_WAKE_LOCK is always surrounded by RELEASE. safe. } private void stopObexServerSession() { Log.v(TAG, "Pbap Service stopObexServerSession"); transitionTo(mFinished); } private void createPbapNotification() { NotificationManager nm = mService.getSystemService(NotificationManager.class); NotificationChannel notificationChannel = new NotificationChannel( PBAP_OBEX_NOTIFICATION_CHANNEL, mService.getString(R.string.pbap_notification_group), NotificationManager.IMPORTANCE_HIGH); nm.createNotificationChannel(notificationChannel); // Create an intent triggered by clicking on the status icon. Intent clickIntent = new Intent(); clickIntent.setClass(mService, BluetoothPbapActivity.class); clickIntent.putExtra(BluetoothPbapService.EXTRA_DEVICE, mRemoteDevice); clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); clickIntent.setAction(BluetoothPbapService.AUTH_CHALL_ACTION); // Create an intent triggered by clicking on the // "Clear All Notifications" button Intent deleteIntent = new Intent(); deleteIntent.setClass(mService, BluetoothPbapService.class); deleteIntent.setAction(BluetoothPbapService.AUTH_CANCELLED_ACTION); String name = Utils.getName(mRemoteDevice); Notification notification = new Notification.Builder(mService, PBAP_OBEX_NOTIFICATION_CHANNEL) .setWhen(System.currentTimeMillis()) .setContentTitle(mService.getString(R.string.auth_notif_title)) .setContentText(mService.getString(R.string.auth_notif_message, name)) .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) .setTicker(mService.getString(R.string.auth_notif_ticker)) .setColor( mService.getResources() .getColor( android.R.color .system_notification_accent_color, mService.getTheme())) .setFlag(Notification.FLAG_AUTO_CANCEL, true) .setFlag(Notification.FLAG_ONLY_ALERT_ONCE, true) .setContentIntent( PendingIntent.getActivity( mService, 0, clickIntent, PendingIntent.FLAG_IMMUTABLE)) .setDeleteIntent( PendingIntent.getBroadcast( mService, 0, deleteIntent, PendingIntent.FLAG_IMMUTABLE)) .setLocalOnly(true) .build(); nm.notify(mNotificationId, notification); } private void removePbapNotification(int id) { NotificationManager nm = mService.getSystemService(NotificationManager.class); nm.cancel(id); } private synchronized void notifyAuthCancelled() { mObexAuth.setCancelled(true); } private synchronized void notifyAuthKeyInput(final String key) { if (key != null) { mObexAuth.setSessionKey(key); } mObexAuth.setChallenged(true); } } /** * Get the current connection state of this state machine * * @return current connection state, one of {@link BluetoothProfile#STATE_DISCONNECTED}, {@link * BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_CONNECTED}, or {@link * BluetoothProfile#STATE_DISCONNECTING} */ synchronized int getConnectionState() { PbapStateBase state = (PbapStateBase) getCurrentState(); if (state == null) { return BluetoothProfile.STATE_DISCONNECTED; } return state.getConnectionStateInt(); } }