| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.chrome.browser.webauth.authenticator; |
| |
| import android.Manifest.permission; |
| import android.app.Activity; |
| import android.app.KeyguardManager; |
| import android.bluetooth.BluetoothAdapter; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.graphics.drawable.Drawable; |
| import android.hardware.usb.UsbAccessory; |
| import android.hardware.usb.UsbManager; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.ViewGroup; |
| import android.widget.CheckBox; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| |
| import androidx.annotation.RequiresApi; |
| import androidx.core.app.NotificationManagerCompat; |
| import androidx.fragment.app.Fragment; |
| import androidx.vectordrawable.graphics.drawable.Animatable2Compat; |
| import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; |
| |
| import org.chromium.base.BuildInfo; |
| import org.chromium.base.Log; |
| import org.chromium.base.ThreadUtils; |
| import org.chromium.base.task.PostTask; |
| import org.chromium.base.task.TaskTraits; |
| import org.chromium.device.DeviceFeatureList; |
| import org.chromium.ui.permissions.ActivityAndroidPermissionDelegate; |
| import org.chromium.ui.permissions.AndroidPermissionDelegate; |
| import org.chromium.ui.widget.Toast; |
| |
| import java.lang.ref.WeakReference; |
| |
| /** |
| * A fragment that provides a UI for various caBLE v2 actions. |
| */ |
| public class CableAuthenticatorUI extends Fragment implements OnClickListener { |
| private static final String TAG = "CableAuthenticatorUI"; |
| |
| // ENABLE_BLUETOOTH_REQUEST_CODE is a random int used to identify responses |
| // to a request to enable Bluetooth. (Request codes can only be 16-bit.) |
| private static final int ENABLE_BLUETOOTH_REQUEST_CODE = 64907; |
| |
| // BLE_SCREEN_DELAY_SECS is the number of seconds that the screen for BLE |
| // enabling will show before the request to actually enable BLE (which |
| // causes Android to draw on top of it) is made. |
| private static final int BLE_SCREEN_DELAY_SECS = 2; |
| |
| // USB_PROMPT_TIMEOUT_SECS is the number of seconds the spinner will show |
| // for before being replaced with a prompt to connect via USB cable. |
| private static final int USB_PROMPT_TIMEOUT_SECS = 20; |
| |
| private static final String FCM_EXTRA = "org.chromium.chrome.modules.cablev2_authenticator.FCM"; |
| private static final String EVENT_EXTRA = |
| "org.chromium.chrome.modules.cablev2_authenticator.EVENT"; |
| private static final String NETWORK_CONTEXT_EXTRA = |
| "org.chromium.chrome.modules.cablev2_authenticator.NetworkContext"; |
| private static final String REGISTRATION_EXTRA = |
| "org.chromium.chrome.modules.cablev2_authenticator.Registration"; |
| private static final String SECRET_EXTRA = |
| "org.chromium.chrome.modules.cablev2_authenticator.Secret"; |
| private static final String SERVER_LINK_EXTRA = |
| "org.chromium.chrome.browser.webauth.authenticator.ServerLink"; |
| private static final String QR_EXTRA = "org.chromium.chrome.browser.webauth.authenticator.QR"; |
| |
| // These entries duplicate some of the enum values from |
| // device::cablev2::authenticator::Platform::Error. They must be kept in |
| // sync with the C++ side because C++ communicates these values to this |
| // code. |
| private static final int ERROR_NONE = 0; |
| private static final int ERROR_UNEXPECTED_EOF = 100; |
| private static final int ERROR_NO_SCREENLOCK = 110; |
| private static final int ERROR_NO_BLUETOOTH_PERMISSION = 111; |
| private static final int ERROR_AUTHENTICATOR_SELECTION_RECEIVED = 114; |
| private static final int ERROR_DISCOVERABLE_CREDENTIALS_REQUEST = 115; |
| |
| // These entries duplicate some of the enum values from |
| // `CableV2MobileEvent`. The C++ enum is the source of truth for these |
| // values. |
| private static final int EVENT_BLUETOOTH_ADVERTISE_PERMISSION_REQUESTED = 23; |
| private static final int EVENT_BLUETOOTH_ADVERTISE_PERMISSION_GRANTED = 24; |
| private static final int EVENT_BLUETOOTH_ADVERTISE_PERMISSION_REJECTED = 25; |
| |
| private enum Mode { |
| QR, // QR code scanned by external app. |
| FCM, // Triggered by user selecting notification; handshake already running. |
| USB, // Triggered by connecting via USB. |
| SERVER_LINK, // Triggered by GMSCore forwarding from GAIA. |
| } |
| private Mode mMode; |
| |
| // State enumerates the different states of the UI. Apart from transitions to `ERROR`, changes |
| // to the state are handled by `onEvent`. |
| private enum State { |
| START, |
| QR_CONFIRM, |
| CHECK_SCREENLOCK, |
| NO_SCREENLOCK, |
| START_MODE, |
| ENABLE_BLUETOOTH, |
| ENABLE_BLUETOOTH_WAITING, |
| ENABLE_BLUETOOTH_PENDING, |
| ENABLE_BLUETOOTH_PERMISSION_REQUESTED, |
| REQUEST_BLUETOOTH_ENABLE, |
| BLUETOOTH_ENABLED, |
| BLUETOOTH_ADVERTISE_PERMISSION_REQUESTED, |
| BLUETOOTH_READY, |
| RUNNING_USB, |
| RUNNING_BLE, |
| ERROR, |
| } |
| private State mState; |
| |
| // mErrorCode contains a value of the authenticator::Platform::Error |
| // enumeration when |mState| is |ERROR|. |
| private int mErrorCode; |
| |
| // Event enumerates outside events that can cause a state transition in `onEvent`. |
| private enum Event { |
| NONE, |
| RESUMED, |
| BLE_ENABLED, |
| PERMISSIONS_GRANTED, |
| QR_ALLOW_BUTTON_CLICKED, |
| QR_DENY_BUTTON_CLICKED, |
| TIMEOUT_COMPLETE; |
| } |
| |
| private AndroidPermissionDelegate mPermissionDelegate; |
| private CableAuthenticator mAuthenticator; |
| // mViewsCreated is set to true after `onCreateView` has been called, which sets values for all |
| // the `View`-typed members of this object. Prior to this UI updates are suppressed. |
| private boolean mViewsCreated; |
| // mActivityStarted is set to true by `onResume`. Some event transitions are suppressed until |
| // this flag has been set. |
| private boolean mActivityStarted; |
| |
| // These are top-level views that can fill this activity. |
| private View mErrorView; |
| private View mSpinnerView; |
| private View mBLEEnableView; |
| private View mUSBView; |
| private View mQRConfirmView; |
| |
| // mCurrentView is one of the above views depending on which is currently showing. |
| private View mCurrentView; |
| |
| // These are views within the top-level activity. |
| private View mErrorCloseButton; |
| private View mErrorSettingsButton; |
| private View mQRAllowButton; |
| private View mQRRejectButton; |
| private TextView mStatusText; |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| final Context context = getContext(); |
| |
| Bundle arguments = getArguments(); |
| final UsbAccessory accessory = |
| (UsbAccessory) arguments.getParcelable(UsbManager.EXTRA_ACCESSORY); |
| final byte[] serverLink = arguments.getByteArray(SERVER_LINK_EXTRA); |
| final byte[] fcmEvent = arguments.getByteArray(EVENT_EXTRA); |
| final Uri qrFromArguments = (Uri) arguments.getParcelable(QR_EXTRA); |
| final String qrURI = qrFromArguments == null ? null : qrFromArguments.toString(); |
| if (accessory != null) { |
| mMode = Mode.USB; |
| } else if (arguments.getBoolean(FCM_EXTRA)) { |
| mMode = Mode.FCM; |
| } else if (serverLink != null) { |
| mErrorCode = CableAuthenticator.validateServerLinkData(serverLink); |
| if (mErrorCode != ERROR_NONE) { |
| mState = State.ERROR; |
| return; |
| } |
| |
| mMode = Mode.SERVER_LINK; |
| } else if (qrURI != null) { |
| mErrorCode = CableAuthenticator.validateQRURI(qrURI); |
| if (mErrorCode != ERROR_NONE) { |
| mState = State.ERROR; |
| return; |
| } |
| |
| mMode = Mode.QR; |
| } else { |
| assert false; |
| getActivity().finish(); |
| } |
| |
| Log.i(TAG, "Starting in mode " + mMode.toString()); |
| |
| final long networkContext = arguments.getLong(NETWORK_CONTEXT_EXTRA); |
| final long registration = arguments.getLong(REGISTRATION_EXTRA); |
| final byte[] secret = arguments.getByteArray(SECRET_EXTRA); |
| |
| mPermissionDelegate = new ActivityAndroidPermissionDelegate( |
| new WeakReference<Activity>((Activity) context)); |
| mAuthenticator = new CableAuthenticator(getContext(), this, networkContext, registration, |
| secret, mMode == Mode.FCM, accessory, serverLink, fcmEvent, qrURI); |
| |
| mState = State.START; |
| onEvent(Event.NONE); |
| } |
| |
| private void onEvent(Event event) { |
| if (mAuthenticator == null) { |
| // Activity was stopped before this event happened. Ignore. |
| return; |
| } |
| |
| while (true) { |
| final State stateBefore = mState; |
| updateUiForState(); |
| |
| switch (mState) { |
| case START: |
| assert event == Event.NONE; |
| |
| // QR mode requires a confirmation screen first. All other modes move |
| // on to the screen-lock check. |
| if (mMode == Mode.QR) { |
| mState = State.QR_CONFIRM; |
| } else { |
| mState = State.CHECK_SCREENLOCK; |
| } |
| break; |
| |
| case QR_CONFIRM: |
| if (event == Event.QR_ALLOW_BUTTON_CLICKED) { |
| ViewGroup top = (ViewGroup) getView(); |
| boolean link = ((CheckBox) top.findViewById(R.id.qr_link)).isChecked(); |
| if (link |
| && !DeviceFeatureList.isEnabled( |
| DeviceFeatureList |
| .WEBAUTHN_HYBRID_LINK_WITHOUT_NOTIFICATIONS)) { |
| link = NotificationManagerCompat.from(getContext()) |
| .areNotificationsEnabled(); |
| } |
| mAuthenticator.setQRLinking(link); |
| mState = State.CHECK_SCREENLOCK; |
| break; |
| } else if (event == Event.QR_DENY_BUTTON_CLICKED) { |
| getActivity().finish(); |
| return; |
| } |
| return; |
| |
| case CHECK_SCREENLOCK: |
| // GMSCore will immediately fail all requests if a screenlock isn't |
| // configured, except for server-link because PaaSK is special. |
| // Outside of server-link, the device shouldn't have advertised itself |
| // via Sync, but it's possible for a request to come in soon after a |
| // screen lock was removed. When using a QR code it's always possible |
| // for the screen-lock to be missing. |
| if (mMode == Mode.SERVER_LINK || hasScreenLockConfigured(getContext())) { |
| mState = State.START_MODE; |
| } else { |
| mState = State.NO_SCREENLOCK; |
| } |
| break; |
| |
| case NO_SCREENLOCK: |
| if (event == Event.RESUMED && hasScreenLockConfigured(getContext())) { |
| // The user tapped the "Open settings" button on the error page |
| // explaining that no screen lock was set, and set a screen lock. |
| // The flow now continues based on `mMode`. |
| mState = State.START_MODE; |
| break; |
| } |
| return; |
| |
| case START_MODE: |
| switch (mMode) { |
| case USB: |
| // USB mode doesn't require Bluetooth. |
| mState = State.RUNNING_USB; |
| mAuthenticator.onTransportReady(); |
| break; |
| |
| case SERVER_LINK: |
| case FCM: |
| case QR: |
| mState = State.ENABLE_BLUETOOTH; |
| break; |
| } |
| break; |
| |
| case ENABLE_BLUETOOTH: |
| if (BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser() != null) { |
| mState = State.BLUETOOTH_ENABLED; |
| break; |
| } |
| |
| if (!mActivityStarted) { |
| return; |
| } |
| |
| mState = State.ENABLE_BLUETOOTH_WAITING; |
| PostTask.postDelayedTask(TaskTraits.UI_DEFAULT, () -> { |
| onEvent(Event.TIMEOUT_COMPLETE); |
| }, BLE_SCREEN_DELAY_SECS * 1000); |
| break; |
| |
| case ENABLE_BLUETOOTH_WAITING: |
| if (event != Event.TIMEOUT_COMPLETE) { |
| return; |
| } |
| |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S |
| && requestBluetoothPermissions()) { |
| mState = State.ENABLE_BLUETOOTH_PERMISSION_REQUESTED; |
| return; |
| } |
| |
| mState = State.REQUEST_BLUETOOTH_ENABLE; |
| break; |
| |
| case ENABLE_BLUETOOTH_PERMISSION_REQUESTED: |
| if (event != Event.PERMISSIONS_GRANTED) { |
| return; |
| } |
| mState = State.REQUEST_BLUETOOTH_ENABLE; |
| break; |
| |
| case REQUEST_BLUETOOTH_ENABLE: |
| mState = State.ENABLE_BLUETOOTH_PENDING; |
| startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), |
| ENABLE_BLUETOOTH_REQUEST_CODE); |
| break; |
| |
| case ENABLE_BLUETOOTH_PENDING: |
| if (event != Event.BLE_ENABLED) { |
| return; |
| } |
| mState = State.BLUETOOTH_ENABLED; |
| break; |
| |
| case BLUETOOTH_ENABLED: |
| if (!mActivityStarted) { |
| return; |
| } |
| |
| // In Android 12 and above there is a new BLUETOOTH_ADVERTISE runtime |
| // permission. |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S |
| && requestBluetoothPermissions()) { |
| mState = State.BLUETOOTH_ADVERTISE_PERMISSION_REQUESTED; |
| return; |
| } |
| |
| mState = State.BLUETOOTH_READY; |
| break; |
| |
| case BLUETOOTH_ADVERTISE_PERMISSION_REQUESTED: |
| if (event != Event.PERMISSIONS_GRANTED) { |
| return; |
| } |
| mState = State.BLUETOOTH_READY; |
| break; |
| |
| case BLUETOOTH_READY: |
| mAuthenticator.onTransportReady(); |
| mState = State.RUNNING_BLE; |
| break; |
| |
| case RUNNING_BLE: |
| case RUNNING_USB: |
| return; |
| |
| case ERROR: |
| if (event == Event.RESUMED && mErrorCode == ERROR_NO_BLUETOOTH_PERMISSION |
| && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S |
| && haveBluetoothPermissions()) { |
| // The user navigated away and came back, but now we have the needed |
| // permission. |
| mState = State.ENABLE_BLUETOOTH; |
| break; |
| } |
| |
| return; |
| } |
| |
| Log.e(TAG, stateBefore.toString() + " -> " + mState.toString()); |
| |
| if (stateBefore == mState) { |
| // A state should either `return` to block for an event or else have updated |
| // `mState`. |
| assert false; |
| break; |
| } |
| |
| // An event shouldn't appear to have happened again for the next state. |
| event = Event.NONE; |
| } |
| } |
| |
| /** |
| * Returns the {@link View} that should be showing, given {@link mState}. |
| */ |
| private View getUiForState() { |
| switch (mState) { |
| case QR_CONFIRM: |
| return mQRConfirmView; |
| |
| case NO_SCREENLOCK: |
| fillOutErrorUI(ERROR_NO_SCREENLOCK); |
| return mErrorView; |
| |
| case ENABLE_BLUETOOTH: |
| case ENABLE_BLUETOOTH_WAITING: |
| case ENABLE_BLUETOOTH_PENDING: |
| case ENABLE_BLUETOOTH_PERMISSION_REQUESTED: |
| case REQUEST_BLUETOOTH_ENABLE: |
| return mBLEEnableView; |
| |
| case RUNNING_USB: |
| return mUSBView; |
| |
| case ERROR: |
| fillOutErrorUI(mErrorCode); |
| return mErrorView; |
| |
| default: |
| return mSpinnerView; |
| } |
| } |
| |
| /** |
| * Updates the UI based on the value of {@link mState}. |
| */ |
| private void updateUiForState() { |
| if (!mViewsCreated) { |
| // If {@link onCreateView} hasn't been called yet then there is no |
| // UI to update. Instead {@link onCreateView} will set the initial |
| // {@link View} based on {@link mState}. |
| return; |
| } |
| |
| View newView = getUiForState(); |
| if (newView == mCurrentView) { |
| return; |
| } |
| |
| ViewGroup top = (ViewGroup) getView(); |
| top.removeAllViews(); |
| mCurrentView = newView; |
| top.addView(mCurrentView); |
| } |
| |
| private View createSpinnerScreen(LayoutInflater inflater, ViewGroup container) { |
| View v = inflater.inflate(R.layout.cablev2_spinner, container, false); |
| mStatusText = v.findViewById(R.id.status_text); |
| |
| final AnimatedVectorDrawableCompat anim = AnimatedVectorDrawableCompat.create( |
| getContext(), R.drawable.circle_loader_animation); |
| // There is no way to make an animation loop. Instead it must be |
| // manually started each time it completes. |
| anim.registerAnimationCallback(new Animatable2Compat.AnimationCallback() { |
| @Override |
| public void onAnimationEnd(Drawable drawable) { |
| if (drawable != null) { |
| anim.start(); |
| } |
| } |
| }); |
| ((ImageView) v.findViewById(R.id.spinner)).setImageDrawable(anim); |
| anim.start(); |
| |
| return v; |
| } |
| |
| @Override |
| public View onCreateView( |
| LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
| mErrorView = inflater.inflate(R.layout.cablev2_error, container, false); |
| mErrorCloseButton = mErrorView.findViewById(R.id.error_close); |
| mErrorCloseButton.setOnClickListener(this); |
| mErrorSettingsButton = mErrorView.findViewById(R.id.error_settings_button); |
| mErrorSettingsButton.setOnClickListener(this); |
| |
| mSpinnerView = createSpinnerScreen(inflater, container); |
| mBLEEnableView = inflater.inflate(R.layout.cablev2_ble_enable, container, false); |
| mUSBView = inflater.inflate(R.layout.cablev2_usb_attached, container, false); |
| |
| mQRConfirmView = inflater.inflate(R.layout.cablev2_qr, container, false); |
| mQRAllowButton = mQRConfirmView.findViewById(R.id.qr_connect); |
| mQRAllowButton.setOnClickListener(this); |
| mQRRejectButton = mQRConfirmView.findViewById(R.id.qr_reject); |
| mQRRejectButton.setOnClickListener(this); |
| |
| mViewsCreated = true; |
| |
| getActivity().setTitle(R.string.cablev2_activity_title); |
| ViewGroup top = new LinearLayout(getContext()); |
| mCurrentView = getUiForState(); |
| top.addView(mCurrentView); |
| return top; |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| mActivityStarted = true; |
| onEvent(Event.RESUMED); |
| } |
| |
| // This class should not be reachable on Android versions < N (API level 24). |
| @RequiresApi(24) |
| private static boolean hasScreenLockConfigured(Context context) { |
| KeyguardManager km = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); |
| return km.isDeviceSecure(); |
| } |
| |
| @RequiresApi(31) |
| private boolean haveBluetoothPermissions() { |
| return getContext().checkSelfPermission(permission.BLUETOOTH_CONNECT) |
| == PackageManager.PERMISSION_GRANTED |
| && getContext().checkSelfPermission(permission.BLUETOOTH_ADVERTISE) |
| == PackageManager.PERMISSION_GRANTED; |
| } |
| |
| /** |
| * Request Bluetooth permissions, if needed. |
| * |
| * @return true if permissions were requested. |
| */ |
| @RequiresApi(31) |
| private boolean requestBluetoothPermissions() { |
| if (!haveBluetoothPermissions()) { |
| if (shouldShowRequestPermissionRationale(permission.BLUETOOTH_CONNECT) |
| || shouldShowRequestPermissionRationale(permission.BLUETOOTH_ADVERTISE)) { |
| // Since the user took explicit action to make a connection to their |
| // computer, and there's a big "Enabling Bluetooth" or "Connecting to your computer" |
| // in the background, the rationale should be clear. However, these functions must |
| // always be called otherwise the permission will be automatically refused. |
| } |
| |
| requestPermissions( |
| new String[] {permission.BLUETOOTH_CONNECT, permission.BLUETOOTH_ADVERTISE}, |
| /* requestCode= */ 1); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| @Override |
| public void onClick(View v) { |
| if (v == mErrorCloseButton) { |
| getActivity().finish(); |
| } else if (v == mErrorSettingsButton) { |
| // Open the Settings screen for Chromium. |
| Intent intent; |
| if (mState == State.NO_SCREENLOCK) { |
| intent = new Intent(android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD); |
| } else if (mState == State.ERROR && mErrorCode == ERROR_NO_BLUETOOTH_PERMISSION) { |
| intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); |
| intent.setData(android.net.Uri.fromParts( |
| "package", BuildInfo.getInstance().packageName, null)); |
| } else { |
| // Should never be reached. Button should not be shown unless |
| // the error is known. |
| intent = new Intent(android.provider.Settings.ACTION_SETTINGS); |
| } |
| startActivity(intent); |
| } else if (v == mQRAllowButton) { |
| onEvent(Event.QR_ALLOW_BUTTON_CLICKED); |
| } else if (v == mQRRejectButton) { |
| onEvent(Event.QR_DENY_BUTTON_CLICKED); |
| } |
| } |
| |
| void onStatus(int code) { |
| switch (mMode) { |
| case QR: |
| case SERVER_LINK: |
| case FCM: |
| // These values must match up with the Status enum in v2_authenticator.h |
| int id = -1; |
| if (code == 1) { |
| if (mMode == Mode.SERVER_LINK) { |
| id = R.string.cablev2_serverlink_status_connecting; |
| } else { |
| id = R.string.cablev2_fcm_status_connecting; |
| } |
| } else if (code == 2) { |
| id = R.string.cablev2_serverlink_status_connected; |
| } else if (code == 3) { |
| id = R.string.cablev2_serverlink_status_processing; |
| } else { |
| break; |
| } |
| |
| mStatusText.setText(getResources().getString(id)); |
| break; |
| |
| case USB: |
| // In USB mode everything should happen immediately. |
| break; |
| } |
| } |
| |
| /** |
| * Called when camera permission has been requested and the user has resolved the permission |
| * request. |
| */ |
| @Override |
| public void onRequestPermissionsResult( |
| int requestCode, String[] permissions, int[] grantResults) { |
| final boolean granted = |
| grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; |
| |
| if (granted) { |
| onEvent(Event.PERMISSIONS_GRANTED); |
| return; |
| } |
| |
| mState = State.ERROR; |
| mErrorCode = ERROR_NO_BLUETOOTH_PERMISSION; |
| updateUiForState(); |
| } |
| |
| @Override |
| public void onStop() { |
| super.onStop(); |
| |
| if (mAuthenticator != null) { |
| mAuthenticator.onActivityStop(); |
| } |
| } |
| |
| @Override |
| public void onDestroy() { |
| super.onDestroy(); |
| |
| // Closing mAuthenticator is not done in |onStop| because Android can |
| // generate spurious |onStop| calls when the activity is started from |
| // a lock-screen notification. |
| if (mAuthenticator != null) { |
| mAuthenticator.close(); |
| mAuthenticator = null; |
| } |
| } |
| |
| @Override |
| public void onActivityResult(int requestCode, int resultCode, Intent data) { |
| super.onActivityResult(requestCode, resultCode, data); |
| |
| if (mAuthenticator == null) { |
| return; |
| } |
| |
| if (requestCode != ENABLE_BLUETOOTH_REQUEST_CODE) { |
| mAuthenticator.onActivityResult(requestCode, resultCode, data); |
| return; |
| } |
| |
| if (resultCode != Activity.RESULT_OK) { |
| getActivity().finish(); |
| return; |
| } |
| |
| onEvent(Event.BLE_ENABLED); |
| } |
| |
| void onAuthenticatorConnected() {} |
| |
| void onAuthenticatorResult(CableAuthenticator.Result result) { |
| getActivity().runOnUiThread(() -> { |
| int id = -1; |
| switch (result) { |
| case REGISTER_OK: |
| id = R.string.cablev2_registration_succeeded; |
| break; |
| case REGISTER_ERROR: |
| id = R.string.cablev2_registration_failed; |
| break; |
| case SIGN_OK: |
| id = R.string.cablev2_sign_in_succeeded; |
| break; |
| case SIGN_ERROR: |
| case OTHER: |
| id = R.string.cablev2_sign_in_failed; |
| break; |
| } |
| Toast.makeText(getActivity(), getResources().getString(id), Toast.LENGTH_SHORT).show(); |
| |
| // Finish the Activity unless we're connected via USB. In that case |
| // we continue to show a message advising the user to disconnect |
| // the cable because the USB connection is in AOA mode. |
| if (mMode != Mode.USB) { |
| getActivity().finish(); |
| } |
| }); |
| } |
| |
| /** |
| * Called when a transaction has completed. |
| * |
| * @param ok true if the transaction completed successfully. Otherwise it |
| * indicates some form of error that could include tunnel server |
| * errors, handshake failures, etc. |
| * @param errorCode a value from cablev2::authenticator::Platform::Error. |
| */ |
| void onComplete(boolean ok, int errorCode) { |
| ThreadUtils.assertOnUiThread(); |
| |
| if (ok) { |
| getActivity().finish(); |
| return; |
| } |
| |
| mState = State.ERROR; |
| mErrorCode = errorCode; |
| updateUiForState(); |
| } |
| |
| /** |
| * Fills out the elements of |mErrorView| for the given error code. |
| * |
| * @param errorCode a value from cablev2::authenticator::Platform::Error. |
| */ |
| void fillOutErrorUI(int errorCode) { |
| mErrorCloseButton = mErrorView.findViewById(R.id.error_close); |
| mErrorCloseButton.setOnClickListener(this); |
| mErrorSettingsButton = mErrorView.findViewById(R.id.error_settings_button); |
| mErrorSettingsButton.setOnClickListener(this); |
| |
| final String packageLabel = BuildInfo.getInstance().hostPackageLabel; |
| String desc; |
| boolean settingsButtonVisible = false; |
| switch (errorCode) { |
| case ERROR_UNEXPECTED_EOF: |
| desc = getResources().getString(R.string.cablev2_error_timeout); |
| break; |
| |
| case ERROR_NO_BLUETOOTH_PERMISSION: |
| desc = getResources().getString( |
| R.string.cablev2_error_ble_permission, packageLabel); |
| settingsButtonVisible = true; |
| break; |
| |
| case ERROR_NO_SCREENLOCK: |
| desc = getResources().getString(R.string.cablev2_error_no_screenlock, packageLabel); |
| settingsButtonVisible = true; |
| break; |
| |
| case ERROR_AUTHENTICATOR_SELECTION_RECEIVED: |
| case ERROR_DISCOVERABLE_CREDENTIALS_REQUEST: |
| desc = getResources().getString(R.string.cablev2_error_disco_cred, packageLabel); |
| break; |
| |
| default: |
| TextView errorCodeTextView = (TextView) mErrorView.findViewById(R.id.error_code); |
| errorCodeTextView.setText( |
| getResources().getString(R.string.cablev2_error_code, errorCode)); |
| |
| desc = getResources().getString(R.string.cablev2_error_generic); |
| break; |
| } |
| |
| ((View) mErrorView.findViewById(R.id.error_settings_button)) |
| .setVisibility(settingsButtonVisible ? View.VISIBLE : View.INVISIBLE); |
| |
| TextView descriptionTextView = (TextView) mErrorView.findViewById(R.id.error_description); |
| descriptionTextView.setText(desc); |
| } |
| } |