| /* |
| * Copyright (C) 2019 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 android.car.trust; |
| |
| import static android.car.Car.PERMISSION_CAR_ENROLL_TRUST; |
| |
| import android.annotation.Nullable; |
| import android.annotation.RequiresPermission; |
| import android.annotation.SystemApi; |
| import android.bluetooth.BluetoothDevice; |
| import android.car.CarManagerBase; |
| import android.car.CarNotConnectedException; |
| import android.content.Context; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.RemoteException; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.GuardedBy; |
| |
| import java.lang.ref.WeakReference; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.stream.Collectors; |
| |
| |
| /** |
| * APIs to help enroll a remote device as a trusted device that can be used to authenticate a user |
| * in the head unit. |
| * <p> |
| * The call sequence to add a new trusted device from the client should be as follows: |
| * <ol> |
| * <li> setEnrollmentCallback() |
| * <li> setBleCallback(bleCallback) |
| * <li> startEnrollmentAdvertising() |
| * <li> wait for onEnrollmentAdvertisingStarted() or |
| * <li> wait for onBleEnrollmentDeviceConnected() and check if the device connected is the right |
| * one. |
| * <li> initiateEnrollmentHandshake() |
| * <li> wait for onAuthStringAvailable() to get the pairing code to display to the user |
| * <li> enrollmentHandshakeAccepted() after user confirms the pairing code |
| * <li> wait for onEscrowTokenAdded() |
| * <li> Authenticate user's credentials by showing the lock screen |
| * <li> activateToken() |
| * <li> wait for onEscrowTokenActiveStateChanged() to add the device as a trusted device and show |
| * in the list |
| * </ol> |
| * |
| * @hide |
| */ |
| @SystemApi |
| public final class CarTrustAgentEnrollmentManager implements CarManagerBase { |
| private static final String TAG = "CarTrustEnrollMgr"; |
| private static final String KEY_HANDLE = "handle"; |
| private static final String KEY_ACTIVE = "active"; |
| private static final String KEY_SUCCESS = "success"; |
| private static final int MSG_ENROLL_ADVERTISING_STARTED = 0; |
| private static final int MSG_ENROLL_ADVERTISING_FAILED = 1; |
| private static final int MSG_ENROLL_DEVICE_CONNECTED = 2; |
| private static final int MSG_ENROLL_DEVICE_DISCONNECTED = 3; |
| private static final int MSG_ENROLL_HANDSHAKE_FAILURE = 4; |
| private static final int MSG_ENROLL_AUTH_STRING_AVAILABLE = 5; |
| private static final int MSG_ENROLL_TOKEN_ADDED = 6; |
| private static final int MSG_ENROLL_TOKEN_REVOKED = 7; |
| private static final int MSG_ENROLL_TOKEN_STATE_CHANGED = 8; |
| |
| private final Context mContext; |
| private final ICarTrustAgentEnrollment mEnrollmentService; |
| private Object mListenerLock = new Object(); |
| @GuardedBy("mListenerLock") |
| private CarTrustAgentEnrollmentCallback mEnrollmentCallback; |
| @GuardedBy("mListenerLock") |
| private CarTrustAgentBleCallback mBleCallback; |
| @GuardedBy("mListenerLock") |
| private final ListenerToEnrollmentService mListenerToEnrollmentService = |
| new ListenerToEnrollmentService(this); |
| private final ListenerToBleService mListenerToBleService = new ListenerToBleService(this); |
| private final EventCallbackHandler mEventCallbackHandler; |
| |
| |
| /** @hide */ |
| public CarTrustAgentEnrollmentManager(IBinder service, Context context, Handler handler) { |
| mContext = context; |
| mEnrollmentService = ICarTrustAgentEnrollment.Stub.asInterface(service); |
| mEventCallbackHandler = new EventCallbackHandler(this, handler.getLooper()); |
| } |
| |
| /** @hide */ |
| @Override |
| public synchronized void onCarDisconnected() { |
| } |
| |
| /** |
| * Starts broadcasting enrollment UUID on BLE. |
| * Phones can scan and connect for the enrollment process to begin. |
| */ |
| @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST) |
| public void startEnrollmentAdvertising() throws CarNotConnectedException { |
| try { |
| mEnrollmentService.startEnrollmentAdvertising(); |
| } catch (RemoteException e) { |
| throw new CarNotConnectedException(e); |
| } |
| } |
| |
| /** |
| * Stops Enrollment advertising. |
| */ |
| @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST) |
| public void stopEnrollmentAdvertising() throws CarNotConnectedException { |
| try { |
| mEnrollmentService.stopEnrollmentAdvertising(); |
| } catch (RemoteException e) { |
| throw new CarNotConnectedException(e); |
| } |
| } |
| |
| /** |
| * Initiates the handshake with the phone for enrollment. This should be called after the |
| * user has confirmed the phone that is requesting enrollment. |
| * |
| * @param device the remote Bluetooth device that is trying to enroll. |
| */ |
| @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST) |
| public void initiateEnrollmentHandshake(BluetoothDevice device) |
| throws CarNotConnectedException { |
| try { |
| mEnrollmentService.initiateEnrollmentHandshake(device); |
| } catch (RemoteException e) { |
| throw new CarNotConnectedException(e); |
| } |
| } |
| |
| /** |
| * Confirms that the enrollment handshake has been accepted by the user. This should be called |
| * after the user has confirmed the verification code displayed on the UI. |
| */ |
| @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST) |
| public void enrollmentHandshakeAccepted() throws CarNotConnectedException { |
| try { |
| mEnrollmentService.enrollmentHandshakeAccepted(); |
| } catch (RemoteException e) { |
| throw new CarNotConnectedException(e); |
| } |
| } |
| |
| /** |
| * Provides an option to quit enrollment if the pairing code doesn't match for example. |
| */ |
| @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST) |
| public void terminateEnrollmentHandshake() throws CarNotConnectedException { |
| try { |
| mEnrollmentService.terminateEnrollmentHandshake(); |
| } catch (RemoteException e) { |
| throw new CarNotConnectedException(e); |
| } |
| } |
| |
| /** |
| * Activate the newly added escrow token. |
| * |
| * @param handle the handle corresponding to the escrow token |
| */ |
| @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST) |
| public void activateToken(long handle) throws CarNotConnectedException { |
| try { |
| mEnrollmentService.activateToken(handle); |
| } catch (RemoteException e) { |
| throw new CarNotConnectedException(e); |
| } |
| } |
| |
| /** |
| * Revoke trust for the remote device denoted by the handle. |
| * |
| * @param handle the handle associated with the escrow token |
| */ |
| @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST) |
| public void revokeTrust(long handle) throws CarNotConnectedException { |
| try { |
| mEnrollmentService.revokeTrust(handle); |
| } catch (RemoteException e) { |
| throw new CarNotConnectedException(e); |
| } |
| } |
| |
| /** |
| * Register for enrollment event callbacks. |
| * |
| * @param callback The callback methods to call, null to unregister |
| */ |
| @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST) |
| public void setEnrollmentCallback(@Nullable CarTrustAgentEnrollmentCallback callback) |
| throws CarNotConnectedException { |
| if (callback == null) { |
| unregisterEnrollmentCallback(); |
| } else { |
| registerEnrollmentCallback(callback); |
| } |
| } |
| |
| private void registerEnrollmentCallback(CarTrustAgentEnrollmentCallback callback) |
| throws CarNotConnectedException { |
| synchronized (mListenerLock) { |
| if (callback != null && mEnrollmentCallback == null) { |
| try { |
| mEnrollmentService.registerEnrollmentCallback(mListenerToEnrollmentService); |
| mEnrollmentCallback = callback; |
| } catch (RemoteException e) { |
| throw new CarNotConnectedException(e); |
| } |
| } |
| } |
| } |
| |
| private void unregisterEnrollmentCallback() throws CarNotConnectedException { |
| synchronized (mListenerLock) { |
| if (mEnrollmentCallback != null) { |
| try { |
| mEnrollmentService.unregisterEnrollmentCallback(mListenerToEnrollmentService); |
| } catch (RemoteException e) { |
| throw new CarNotConnectedException(e); |
| } |
| mEnrollmentCallback = null; |
| } |
| } |
| } |
| |
| /** |
| * Register for general BLE callbacks |
| * |
| * @param callback The callback methods to call, null to unregister |
| */ |
| @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST) |
| public void setBleCallback(@Nullable CarTrustAgentBleCallback callback) |
| throws CarNotConnectedException { |
| if (callback == null) { |
| unregisterBleCallback(); |
| } else { |
| registerBleCallback(callback); |
| } |
| } |
| |
| private void registerBleCallback(CarTrustAgentBleCallback callback) |
| throws CarNotConnectedException { |
| synchronized (mListenerLock) { |
| if (callback != null && mBleCallback == null) { |
| try { |
| mEnrollmentService.registerBleCallback(mListenerToBleService); |
| mBleCallback = callback; |
| } catch (RemoteException e) { |
| throw new CarNotConnectedException(e); |
| } |
| } |
| } |
| } |
| |
| private void unregisterBleCallback() throws CarNotConnectedException { |
| synchronized (mListenerLock) { |
| if (mBleCallback != null) { |
| try { |
| mEnrollmentService.unregisterBleCallback(mListenerToBleService); |
| } catch (RemoteException e) { |
| throw new CarNotConnectedException(e); |
| } |
| mBleCallback = null; |
| } |
| } |
| } |
| |
| /** |
| * Provides a list of enrollment handles for the given user id. |
| * Each enrollment handle corresponds to a trusted device for the given user. |
| * |
| * @param uid user id. |
| * @return list of the Enrollment handles for the user id. |
| */ |
| @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST) |
| public List<Integer> getEnrollmentHandlesForUser(int uid) throws CarNotConnectedException { |
| try { |
| return Arrays.stream( |
| mEnrollmentService.getEnrollmentHandlesForUser(uid)).boxed().collect( |
| Collectors.toList()); |
| } catch (RemoteException e) { |
| throw new CarNotConnectedException(e); |
| } |
| } |
| |
| private Handler getEventCallbackHandler() { |
| return mEventCallbackHandler; |
| } |
| |
| /** |
| * Callback interface for Trusted device enrollment applications to implement. The applications |
| * get notified on various enrollment state change events. |
| */ |
| public interface CarTrustAgentEnrollmentCallback { |
| /** |
| * Communicate about failure/timeouts in the handshake process. |
| * |
| * @param device the remote device trying to enroll |
| * @param errorCode information on what failed. |
| */ |
| void onEnrollmentHandshakeFailure(BluetoothDevice device, int errorCode); |
| |
| /** |
| * Present the pairing/authentication string to the user. |
| * |
| * @param device the remote device trying to enroll |
| * @param authString the authentication string to show to the user to confirm across |
| * both devices |
| */ |
| void onAuthStringAvailable(BluetoothDevice device, String authString); |
| |
| /** |
| * Escrow token was received and the Trust Agent framework has generated a corresponding |
| * handle. |
| * |
| * @param handle the handle associated with the escrow token. |
| */ |
| void onEscrowTokenAdded(long handle); |
| |
| /** |
| * Escrow token corresponding to the given handle has been removed. |
| * |
| * @param handle the handle associated with the escrow token. |
| * @param success status of the revoke operation. |
| */ |
| void onTrustRevoked(long handle, boolean success); |
| |
| /** |
| * Escrow token's active state changed. |
| * |
| * @param handle the handle associated with the escrow token |
| * @param active True if token has been activated, false if not. |
| */ |
| void onEscrowTokenActiveStateChanged(long handle, boolean active); |
| |
| } |
| |
| /** |
| * Callback interface for Trusted device enrollment applications to implement. The applications |
| * get notified on various BLE state change events that happen during trusted device enrollment. |
| */ |
| public interface CarTrustAgentBleCallback { |
| /** |
| * Indicates a remote device connected on BLE. |
| */ |
| void onBleEnrollmentDeviceConnected(BluetoothDevice device); |
| |
| /** |
| * Indicates a remote device disconnected on BLE. |
| */ |
| void onBleEnrollmentDeviceDisconnected(BluetoothDevice device); |
| |
| /** |
| * Indicates that the device is broadcasting for trusted device enrollment on BLE. |
| */ |
| void onEnrollmentAdvertisingStarted(); |
| |
| /** |
| * Indicates a failure in BLE broadcasting for enrollment. |
| */ |
| void onEnrollmentAdvertisingFailed(int errorCode); |
| } |
| |
| private static final class ListenerToEnrollmentService extends |
| ICarTrustAgentEnrollmentCallback.Stub { |
| private final WeakReference<CarTrustAgentEnrollmentManager> mMgr; |
| |
| ListenerToEnrollmentService(CarTrustAgentEnrollmentManager mgr) { |
| mMgr = new WeakReference<>(mgr); |
| } |
| |
| /** |
| * Communicate about failure/timeouts in the handshake process. |
| */ |
| @Override |
| public void onEnrollmentHandshakeFailure(BluetoothDevice device, int errorCode) { |
| CarTrustAgentEnrollmentManager enrollmentManager = mMgr.get(); |
| if (enrollmentManager == null) { |
| return; |
| } |
| enrollmentManager.getEventCallbackHandler().sendMessage( |
| enrollmentManager.getEventCallbackHandler().obtainMessage( |
| MSG_ENROLL_HANDSHAKE_FAILURE, new AuthInfo(device, null, errorCode))); |
| } |
| |
| /** |
| * Present the pairing/authentication string to the user. |
| */ |
| @Override |
| public void onAuthStringAvailable(BluetoothDevice device, String authString) { |
| CarTrustAgentEnrollmentManager enrollmentManager = mMgr.get(); |
| if (enrollmentManager == null) { |
| return; |
| } |
| enrollmentManager.getEventCallbackHandler().sendMessage( |
| enrollmentManager.getEventCallbackHandler().obtainMessage( |
| MSG_ENROLL_AUTH_STRING_AVAILABLE, new AuthInfo(device, authString, 0))); |
| } |
| |
| /** |
| * Escrow token was received and the Trust Agent framework has generated a corresponding |
| * handle. |
| */ |
| @Override |
| public void onEscrowTokenAdded(long handle) { |
| CarTrustAgentEnrollmentManager enrollmentManager = mMgr.get(); |
| if (enrollmentManager == null) { |
| return; |
| } |
| Message message = enrollmentManager.getEventCallbackHandler().obtainMessage( |
| MSG_ENROLL_TOKEN_ADDED); |
| Bundle data = new Bundle(); |
| data.putLong(KEY_HANDLE, handle); |
| message.setData(data); |
| enrollmentManager.getEventCallbackHandler().sendMessage(message); |
| } |
| |
| /** |
| * Escrow token corresponding to the given handle has been removed. |
| */ |
| @Override |
| public void onTrustRevoked(long handle, boolean success) { |
| CarTrustAgentEnrollmentManager enrollmentManager = mMgr.get(); |
| if (enrollmentManager == null) { |
| return; |
| } |
| Message message = enrollmentManager.getEventCallbackHandler().obtainMessage( |
| MSG_ENROLL_TOKEN_REVOKED); |
| Bundle data = new Bundle(); |
| data.putLong(KEY_HANDLE, handle); |
| data.putBoolean(KEY_SUCCESS, success); |
| message.setData(data); |
| enrollmentManager.getEventCallbackHandler().sendMessage(message); |
| } |
| |
| /** |
| * Escrow token's active state changed. |
| */ |
| @Override |
| public void onEscrowTokenActiveStateChanged(long handle, boolean active) { |
| CarTrustAgentEnrollmentManager enrollmentManager = mMgr.get(); |
| if (enrollmentManager == null) { |
| return; |
| } |
| Message message = enrollmentManager.getEventCallbackHandler().obtainMessage( |
| MSG_ENROLL_TOKEN_STATE_CHANGED); |
| Bundle data = new Bundle(); |
| data.putLong(KEY_HANDLE, handle); |
| data.putBoolean(KEY_ACTIVE, active); |
| message.setData(data); |
| enrollmentManager.getEventCallbackHandler().sendMessage(message); |
| } |
| } |
| |
| private static final class ListenerToBleService extends ICarTrustAgentBleCallback.Stub { |
| private final WeakReference<CarTrustAgentEnrollmentManager> mMgr; |
| |
| ListenerToBleService(CarTrustAgentEnrollmentManager mgr) { |
| mMgr = new WeakReference<>(mgr); |
| } |
| |
| /** |
| * Called when the GATT server is started and BLE is successfully advertising for |
| * enrollment. |
| */ |
| public void onEnrollmentAdvertisingStarted() { |
| CarTrustAgentEnrollmentManager enrollmentManager = mMgr.get(); |
| if (enrollmentManager == null) { |
| return; |
| } |
| enrollmentManager.getEventCallbackHandler().sendMessage( |
| enrollmentManager.getEventCallbackHandler().obtainMessage( |
| MSG_ENROLL_ADVERTISING_STARTED)); |
| } |
| |
| /** |
| * Called when the BLE enrollment advertisement fails to start. |
| * see AdvertiseCallback#ADVERTISE_FAILED_* for possible error codes. |
| */ |
| public void onEnrollmentAdvertisingFailed(int errorCode) { |
| CarTrustAgentEnrollmentManager enrollmentManager = mMgr.get(); |
| if (enrollmentManager == null) { |
| return; |
| } |
| enrollmentManager.getEventCallbackHandler().sendMessage( |
| enrollmentManager.getEventCallbackHandler().obtainMessage( |
| MSG_ENROLL_ADVERTISING_FAILED, errorCode)); |
| } |
| |
| /** |
| * Called when a remote device is connected on BLE. |
| */ |
| public void onBleEnrollmentDeviceConnected(BluetoothDevice device) { |
| CarTrustAgentEnrollmentManager enrollmentManager = mMgr.get(); |
| if (enrollmentManager == null) { |
| return; |
| } |
| enrollmentManager.getEventCallbackHandler().sendMessage( |
| enrollmentManager.getEventCallbackHandler().obtainMessage( |
| MSG_ENROLL_DEVICE_CONNECTED, device)); |
| } |
| |
| /** |
| * Called when a remote device is disconnected on BLE. |
| */ |
| public void onBleEnrollmentDeviceDisconnected(BluetoothDevice device) { |
| CarTrustAgentEnrollmentManager enrollmentManager = mMgr.get(); |
| if (enrollmentManager == null) { |
| return; |
| } |
| enrollmentManager.getEventCallbackHandler().sendMessage( |
| enrollmentManager.getEventCallbackHandler().obtainMessage( |
| MSG_ENROLL_DEVICE_DISCONNECTED, device)); |
| } |
| } |
| |
| /** |
| * Callback Handler to handle dispatching the enrollment state changes to the corresponding |
| * listeners |
| */ |
| private static final class EventCallbackHandler extends Handler { |
| private final WeakReference<CarTrustAgentEnrollmentManager> mEnrollmentManager; |
| |
| EventCallbackHandler(CarTrustAgentEnrollmentManager manager, Looper looper) { |
| super(looper); |
| mEnrollmentManager = new WeakReference<>(manager); |
| } |
| |
| @Override |
| public void handleMessage(Message message) { |
| CarTrustAgentEnrollmentManager enrollmentManager = mEnrollmentManager.get(); |
| if (enrollmentManager == null) { |
| return; |
| } |
| switch (message.what) { |
| case MSG_ENROLL_ADVERTISING_STARTED: |
| case MSG_ENROLL_ADVERTISING_FAILED: |
| case MSG_ENROLL_DEVICE_CONNECTED: |
| case MSG_ENROLL_DEVICE_DISCONNECTED: |
| enrollmentManager.dispatchBleCallback(message); |
| break; |
| case MSG_ENROLL_HANDSHAKE_FAILURE: |
| case MSG_ENROLL_AUTH_STRING_AVAILABLE: |
| case MSG_ENROLL_TOKEN_ADDED: |
| case MSG_ENROLL_TOKEN_REVOKED: |
| case MSG_ENROLL_TOKEN_STATE_CHANGED: |
| enrollmentManager.dispatchEnrollmentCallback(message); |
| break; |
| default: |
| Log.e(TAG, "Unknown message:" + message.what); |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Dispatch BLE related state change callbacks |
| * |
| * @param message Message to handle and dispatch |
| */ |
| private void dispatchBleCallback(Message message) { |
| CarTrustAgentBleCallback bleCallback; |
| synchronized (mListenerLock) { |
| bleCallback = mBleCallback; |
| } |
| if (bleCallback == null) { |
| return; |
| } |
| switch (message.what) { |
| case MSG_ENROLL_ADVERTISING_STARTED: |
| bleCallback.onEnrollmentAdvertisingStarted(); |
| break; |
| case MSG_ENROLL_ADVERTISING_FAILED: |
| bleCallback.onEnrollmentAdvertisingFailed((int) message.obj); |
| break; |
| case MSG_ENROLL_DEVICE_CONNECTED: |
| bleCallback.onBleEnrollmentDeviceConnected((BluetoothDevice) message.obj); |
| break; |
| case MSG_ENROLL_DEVICE_DISCONNECTED: |
| bleCallback.onBleEnrollmentDeviceDisconnected((BluetoothDevice) message.obj); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| /** |
| * Dispatch Enrollment related state changes to the listener. |
| * |
| * @param message Message to handle and dispatch |
| */ |
| private void dispatchEnrollmentCallback(Message message) { |
| CarTrustAgentEnrollmentCallback enrollmentCallback; |
| synchronized (mListenerLock) { |
| enrollmentCallback = mEnrollmentCallback; |
| } |
| if (enrollmentCallback == null) { |
| return; |
| } |
| AuthInfo auth; |
| Bundle data; |
| switch (message.what) { |
| case MSG_ENROLL_HANDSHAKE_FAILURE: |
| auth = (AuthInfo) message.obj; |
| enrollmentCallback.onEnrollmentHandshakeFailure(auth.mDevice, auth.mErrorCode); |
| break; |
| case MSG_ENROLL_AUTH_STRING_AVAILABLE: |
| auth = (AuthInfo) message.obj; |
| if (auth.mDevice != null && auth.mAuthString != null) { |
| enrollmentCallback.onAuthStringAvailable(auth.mDevice, auth.mAuthString); |
| } |
| break; |
| case MSG_ENROLL_TOKEN_ADDED: |
| data = message.getData(); |
| if (data == null) { |
| break; |
| } |
| enrollmentCallback.onEscrowTokenAdded(data.getLong(KEY_HANDLE)); |
| break; |
| case MSG_ENROLL_TOKEN_REVOKED: |
| data = message.getData(); |
| if (data == null) { |
| break; |
| } |
| enrollmentCallback.onTrustRevoked(data.getLong(KEY_HANDLE), |
| data.getBoolean(KEY_SUCCESS)); |
| break; |
| case MSG_ENROLL_TOKEN_STATE_CHANGED: |
| data = message.getData(); |
| if (data == null) { |
| break; |
| } |
| enrollmentCallback.onEscrowTokenActiveStateChanged(data.getLong(KEY_HANDLE), |
| data.getBoolean(KEY_ACTIVE)); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| /** |
| * Container class to pass information through a Message to the handler. |
| */ |
| private static class AuthInfo { |
| final BluetoothDevice mDevice; |
| @Nullable |
| final String mAuthString; |
| final int mErrorCode; |
| |
| AuthInfo(BluetoothDevice device, @Nullable String authString, int errorCode) { |
| mDevice = device; |
| mAuthString = authString; |
| mErrorCode = errorCode; |
| } |
| } |
| } |