blob: d1571680064b83bba295cb7fee0b740d8437ed43 [file] [log] [blame]
// 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);
}
}