5/n: Restore biometric dialog state across configuration changes

Bug: 123378871

Test: manual test, rotating device during various stages of
      authentication
Test: atest com.android.systemui.biometrics

Change-Id: I4130f79975f58e5141c9d69e2689680ceaa419ed
This commit is contained in:
Kevin Chyn
2019-08-30 13:33:58 -07:00
parent f8688a0a1e
commit 9cf899162e
10 changed files with 218 additions and 89 deletions

View File

@@ -56,7 +56,7 @@
android:scaleType="fitXY" />
<TextView
android:id="@+id/error"
android:id="@+id/indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"

View File

@@ -154,18 +154,18 @@ public class AuthBiometricFaceView extends AuthBiometricView {
@Override
protected void handleResetAfterError() {
resetErrorView(mContext, mErrorView);
resetErrorView(mContext, mIndicatorView);
}
@Override
protected void handleResetAfterHelp() {
resetErrorView(mContext, mErrorView);
resetErrorView(mContext, mIndicatorView);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mIconController = new IconController(mContext, mIconView, mErrorView);
mIconController = new IconController(mContext, mIconView, mIndicatorView);
}
@Override
@@ -174,7 +174,7 @@ public class AuthBiometricFaceView extends AuthBiometricView {
if (newState == STATE_AUTHENTICATING_ANIMATING_IN ||
(newState == STATE_AUTHENTICATING && mSize == AuthDialog.SIZE_MEDIUM)) {
resetErrorView(mContext, mErrorView);
resetErrorView(mContext, mIndicatorView);
}
// Do this last since the state variable gets updated.

View File

@@ -21,6 +21,8 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.biometrics.BiometricPrompt;
import android.os.Bundle;
@@ -127,8 +129,8 @@ public abstract class AuthBiometricView extends LinearLayout {
return mBiometricView.findViewById(R.id.description);
}
public TextView getErrorView() {
return mBiometricView.findViewById(R.id.error);
public TextView getIndicatorView() {
return mBiometricView.findViewById(R.id.indicator);
}
public ImageView getIconView() {
@@ -150,7 +152,7 @@ public abstract class AuthBiometricView extends LinearLayout {
private TextView mSubtitleView;
private TextView mDescriptionView;
protected ImageView mIconView;
@VisibleForTesting protected TextView mErrorView;
@VisibleForTesting protected TextView mIndicatorView;
@VisibleForTesting Button mNegativeButton;
@VisibleForTesting Button mPositiveButton;
@VisibleForTesting Button mTryAgainButton;
@@ -165,6 +167,7 @@ public abstract class AuthBiometricView extends LinearLayout {
private float mIconOriginalY;
protected boolean mDialogSizeAnimating;
protected Bundle mSavedState;
/**
* Delay after authentication is confirmed, before the dialog should be animated away.
@@ -252,7 +255,7 @@ public abstract class AuthBiometricView extends LinearLayout {
mTitleView.setVisibility(View.GONE);
mSubtitleView.setVisibility(View.GONE);
mDescriptionView.setVisibility(View.GONE);
mErrorView.setVisibility(View.GONE);
mIndicatorView.setVisibility(View.GONE);
mNegativeButton.setVisibility(View.GONE);
final float iconPadding = getResources()
@@ -284,7 +287,7 @@ public abstract class AuthBiometricView extends LinearLayout {
final float opacity = (float) animation.getAnimatedValue();
mTitleView.setAlpha(opacity);
mErrorView.setAlpha(opacity);
mIndicatorView.setAlpha(opacity);
mNegativeButton.setAlpha(opacity);
mTryAgainButton.setAlpha(opacity);
@@ -304,7 +307,7 @@ public abstract class AuthBiometricView extends LinearLayout {
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mTitleView.setVisibility(View.VISIBLE);
mErrorView.setVisibility(View.VISIBLE);
mIndicatorView.setVisibility(View.VISIBLE);
mNegativeButton.setVisibility(View.VISIBLE);
mTryAgainButton.setVisibility(View.VISIBLE);
@@ -339,6 +342,7 @@ public abstract class AuthBiometricView extends LinearLayout {
public void updateState(@BiometricState int newState) {
Log.v(TAG, "newState: " + newState);
switch (newState) {
case STATE_AUTHENTICATING_ANIMATING_IN:
case STATE_AUTHENTICATING:
@@ -353,7 +357,7 @@ public abstract class AuthBiometricView extends LinearLayout {
if (mSize != AuthDialog.SIZE_SMALL) {
mPositiveButton.setVisibility(View.GONE);
mNegativeButton.setVisibility(View.GONE);
mErrorView.setVisibility(View.INVISIBLE);
mIndicatorView.setVisibility(View.INVISIBLE);
}
mHandler.postDelayed(() -> mCallback.onAction(Callback.ACTION_AUTHENTICATED),
getDelayAfterAuthenticatedDurationMs());
@@ -364,9 +368,10 @@ public abstract class AuthBiometricView extends LinearLayout {
mNegativeButton.setText(R.string.cancel);
mNegativeButton.setContentDescription(getResources().getString(R.string.cancel));
mPositiveButton.setEnabled(true);
mErrorView.setTextColor(mTextColorHint);
mErrorView.setText(R.string.biometric_dialog_tap_confirm);
mErrorView.setVisibility(View.VISIBLE);
mPositiveButton.setVisibility(View.VISIBLE);
mIndicatorView.setTextColor(mTextColorHint);
mIndicatorView.setText(R.string.biometric_dialog_tap_confirm);
mIndicatorView.setVisibility(View.VISIBLE);
break;
case STATE_ERROR:
@@ -409,6 +414,27 @@ public abstract class AuthBiometricView extends LinearLayout {
updateState(STATE_HELP);
}
public void onSaveState(@NonNull Bundle outState) {
outState.putInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY,
mTryAgainButton.getVisibility());
outState.putInt(AuthDialog.KEY_BIOMETRIC_STATE, mState);
outState.putString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING,
mIndicatorView.getText().toString());
outState.putBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING,
mHandler.hasCallbacks(mResetErrorRunnable));
outState.putBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_HELP_SHOWING,
mHandler.hasCallbacks(mResetHelpRunnable));
outState.putInt(AuthDialog.KEY_BIOMETRIC_DIALOG_SIZE, mSize);
}
/**
* Invoked after inflation but before being attached to window.
* @param savedState
*/
public void restoreState(@Nullable Bundle savedState) {
mSavedState = savedState;
}
private void setTextOrHide(TextView view, String string) {
if (TextUtils.isEmpty(string)) {
view.setVisibility(View.GONE);
@@ -429,25 +455,28 @@ public abstract class AuthBiometricView extends LinearLayout {
private void showTemporaryMessage(String message, Runnable resetMessageRunnable) {
removePendingAnimations();
mErrorView.setText(message);
mErrorView.setTextColor(mTextColorError);
mErrorView.setVisibility(View.VISIBLE);
mIndicatorView.setText(message);
mIndicatorView.setTextColor(mTextColorError);
mIndicatorView.setVisibility(View.VISIBLE);
mHandler.postDelayed(resetMessageRunnable, BiometricPrompt.HIDE_DIALOG_DELAY);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
initializeViews();
onFinishInflateInternal();
}
/**
* After inflation, but before things like restoreState, onAttachedToWindow, etc.
*/
@VisibleForTesting
void initializeViews() {
void onFinishInflateInternal() {
mTitleView = mInjector.getTitleView();
mSubtitleView = mInjector.getSubtitleView();
mDescriptionView = mInjector.getDescriptionView();
mIconView = mInjector.getIconView();
mErrorView = mInjector.getErrorView();
mIndicatorView = mInjector.getIndicatorView();
mNegativeButton = mInjector.getNegativeButton();
mPositiveButton = mInjector.getPositiveButton();
mTryAgainButton = mInjector.getTryAgainButton();
@@ -474,13 +503,49 @@ public abstract class AuthBiometricView extends LinearLayout {
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
onAttachedToWindowInternal();
}
/**
* Contains all the testable logic that should be invoked when {@link #onAttachedToWindow()} is
* invoked.
*/
@VisibleForTesting
void onAttachedToWindowInternal() {
setText(mTitleView, mBundle.getString(BiometricPrompt.KEY_TITLE));
setText(mNegativeButton, mBundle.getString(BiometricPrompt.KEY_NEGATIVE_TEXT));
setTextOrHide(mSubtitleView, mBundle.getString(BiometricPrompt.KEY_SUBTITLE));
setTextOrHide(mDescriptionView, mBundle.getString(BiometricPrompt.KEY_DESCRIPTION));
updateState(STATE_AUTHENTICATING_ANIMATING_IN);
if (mSavedState == null) {
updateState(STATE_AUTHENTICATING_ANIMATING_IN);
} else {
// Restore as much state as possible first
updateState(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_STATE));
// Restore positive button state
mTryAgainButton.setVisibility(
mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY));
// Restore indicator text state
final String indicatorText =
mSavedState.getString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING);
if (mSavedState.getBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_HELP_SHOWING)) {
onHelp(indicatorText);
} else if (mSavedState.getBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING)) {
onAuthenticationFailed(indicatorText);
}
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// Empty the handler, otherwise things like ACTION_AUTHENTICATED may be duplicated once
// the new dialog is restored.
mHandler.removeCallbacksAndMessages(null /* all */);
}
@Override
@@ -521,13 +586,24 @@ public abstract class AuthBiometricView extends LinearLayout {
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLayoutInternal();
}
/**
* Contains all the testable logic that should be invoked when
* {@link #onLayout(boolean, int, int, int, int)}, is invoked.
*/
@VisibleForTesting
void onLayoutInternal() {
// Start with initial size only once. Subsequent layout changes don't matter since we
// only care about the initial icon position.
if (mIconOriginalY == 0) {
mIconOriginalY = mIconView.getY();
updateSize(mRequireConfirmation ? AuthDialog.SIZE_MEDIUM
: AuthDialog.SIZE_SMALL);
if (mSavedState == null) {
updateSize(mRequireConfirmation ? AuthDialog.SIZE_MEDIUM : AuthDialog.SIZE_SMALL);
} else {
updateSize(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_DIALOG_SIZE));
}
}
}
}

View File

@@ -17,6 +17,8 @@
package com.android.systemui.biometrics;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Binder;
@@ -269,7 +271,8 @@ public class AuthContainerView extends LinearLayout
}
@Override
public void show(WindowManager wm) {
public void show(WindowManager wm, @Nullable Bundle savedState) {
mBiometricView.restoreState(savedState);
wm.addView(this, getLayoutParams(mWindowToken));
}
@@ -308,13 +311,8 @@ public class AuthContainerView extends LinearLayout
}
@Override
public void onSaveState(Bundle outState) {
}
@Override
public void restoreState(Bundle savedState) {
public void onSaveState(@NonNull Bundle outState) {
mBiometricView.onSaveState(outState);
}
@Override

View File

@@ -272,11 +272,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
+ " type: " + type);
}
if (savedState != null) {
// SavedState is only non-null if it's from onConfigurationChanged. Restore the state
// even though it may be removed / re-created again
newDialog.restoreState(savedState);
} else if (mCurrentDialog != null) {
if (mCurrentDialog != null) {
// If somehow we're asked to show a dialog, the old one doesn't need to be animated
// away. This can happen if the app cancels and re-starts auth during configuration
// change. This is ugly because we also have to do things on onConfigurationChanged
@@ -286,7 +282,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
mReceiver = (IBiometricServiceReceiverInternal) args.arg2;
mCurrentDialog = newDialog;
mCurrentDialog.show(mWindowManager);
mCurrentDialog.show(mWindowManager, savedState);
}
private void onDialogDismissed(@DismissedReason int reason) {

View File

@@ -17,7 +17,8 @@
package com.android.systemui.biometrics;
import android.annotation.IntDef;
import android.hardware.biometrics.BiometricPrompt;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
import android.view.WindowManager;
@@ -28,28 +29,12 @@ import java.lang.annotation.RetentionPolicy;
* Interface for the biometric dialog UI.
*/
public interface AuthDialog {
// TODO: Clean up save/restore state
String[] KEYS_TO_BACKUP = {
BiometricPrompt.KEY_TITLE,
BiometricPrompt.KEY_USE_DEFAULT_TITLE,
BiometricPrompt.KEY_SUBTITLE,
BiometricPrompt.KEY_DESCRIPTION,
BiometricPrompt.KEY_POSITIVE_TEXT,
BiometricPrompt.KEY_NEGATIVE_TEXT,
BiometricPrompt.KEY_REQUIRE_CONFIRMATION,
BiometricPrompt.KEY_ALLOW_DEVICE_CREDENTIAL,
BiometricPrompt.KEY_FROM_CONFIRM_DEVICE_CREDENTIAL,
BiometricDialogView.KEY_TRY_AGAIN_VISIBILITY,
BiometricDialogView.KEY_CONFIRM_VISIBILITY,
BiometricDialogView.KEY_CONFIRM_ENABLED,
BiometricDialogView.KEY_STATE,
BiometricDialogView.KEY_ERROR_TEXT_VISIBILITY,
BiometricDialogView.KEY_ERROR_TEXT_STRING,
BiometricDialogView.KEY_ERROR_TEXT_IS_TEMPORARY,
BiometricDialogView.KEY_ERROR_TEXT_COLOR,
};
String KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY = "try_agian_visibility";
String KEY_BIOMETRIC_STATE = "state";
String KEY_BIOMETRIC_INDICATOR_STRING = "indicator_string"; // error / help / hint
String KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING = "error_is_temporary";
String KEY_BIOMETRIC_INDICATOR_HELP_SHOWING = "hint_is_temporary";
String KEY_BIOMETRIC_DIALOG_SIZE = "size";
int SIZE_UNKNOWN = 0;
int SIZE_SMALL = 1;
@@ -68,7 +53,7 @@ public interface AuthDialog {
* Show the dialog.
* @param wm
*/
void show(WindowManager wm);
void show(WindowManager wm, @Nullable Bundle savedState);
/**
* Dismiss the dialog without sending a callback.
@@ -107,13 +92,7 @@ public interface AuthDialog {
* Save the current state.
* @param outState
*/
void onSaveState(Bundle outState);
/**
* Restore a previous state.
* @param savedState
*/
void restoreState(Bundle savedState);
void onSaveState(@NonNull Bundle outState);
/**
* Get the client's package name

View File

@@ -23,6 +23,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.graphics.Outline;
@@ -738,7 +739,10 @@ public abstract class BiometricDialogView extends LinearLayout implements AuthDi
}
@Override
public void show(WindowManager wm) {
public void show(WindowManager wm, @Nullable Bundle savedState) {
if (savedState != null) {
restoreState(savedState);
}
wm.addView(this, getLayoutParams(mWindowToken));
}
@@ -832,7 +836,6 @@ public abstract class BiometricDialogView extends LinearLayout implements AuthDi
bundle.putInt(KEY_DIALOG_SIZE, mSize);
}
@Override
public void restoreState(Bundle bundle) {
mRestoredState = bundle;

View File

@@ -63,7 +63,7 @@ public class AuthBiometricFaceViewTest extends SysuiTestCase {
mFaceView.mNegativeButton = mNegativeButton;
mFaceView.mPositiveButton = mPositiveButton;
mFaceView.mTryAgainButton = mTryAgainButton;
mFaceView.mErrorView = mErrorView;
mFaceView.mIndicatorView = mErrorView;
}
@Test

View File

@@ -17,12 +17,15 @@
package com.android.systemui.biometrics;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.hardware.biometrics.BiometricPrompt;
import android.os.Bundle;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
@@ -55,10 +58,10 @@ public class AuthBiometricViewTest extends SysuiTestCase {
@Mock private TextView mTitleView;
@Mock private TextView mSubtitleView;
@Mock private TextView mDescriptionView;
@Mock private TextView mErrorView;
@Mock private TextView mIndicatorView;
@Mock private ImageView mIconView;
TestableBiometricView mBiometricView;
private TestableBiometricView mBiometricView;
@Before
public void setup() {
@@ -87,8 +90,8 @@ public class AuthBiometricViewTest extends SysuiTestCase {
verify(mCallback, never()).onAction(anyInt());
verify(mBiometricView.mNegativeButton).setText(eq(R.string.cancel));
verify(mBiometricView.mPositiveButton).setEnabled(eq(true));
verify(mErrorView).setText(eq(R.string.biometric_dialog_tap_confirm));
verify(mErrorView).setVisibility(eq(View.VISIBLE));
verify(mIndicatorView).setText(eq(R.string.biometric_dialog_tap_confirm));
verify(mIndicatorView).setVisibility(eq(View.VISIBLE));
}
@Test
@@ -193,11 +196,88 @@ public class AuthBiometricViewTest extends SysuiTestCase {
verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED));
}
@Test
public void testRestoresState() {
final boolean requireConfirmation = true; // set/init from AuthController
Button tryAgainButton = new Button(mContext);
TextView indicatorView = new TextView(mContext);
initDialog(mContext, mCallback, new MockInjector() {
@Override
public Button getTryAgainButton() {
return tryAgainButton;
}
@Override
public TextView getIndicatorView() {
return indicatorView;
}
});
final String failureMessage = "testFailureMessage";
mBiometricView.setRequireConfirmation(requireConfirmation);
mBiometricView.onAuthenticationFailed(failureMessage);
waitForIdleSync();
Bundle state = new Bundle();
mBiometricView.onSaveState(state);
assertEquals(View.VISIBLE, tryAgainButton.getVisibility());
assertEquals(View.VISIBLE, state.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY));
assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState);
assertEquals(AuthBiometricView.STATE_ERROR, state.getInt(AuthDialog.KEY_BIOMETRIC_STATE));
assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility());
assertTrue(state.getBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING));
assertEquals(failureMessage, mBiometricView.mIndicatorView.getText());
assertEquals(failureMessage, state.getString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING));
// TODO: Test dialog size. Should move requireConfirmation to buildBiometricPromptBundle
// Create new dialog and restore the previous state into it
Button tryAgainButton2 = new Button(mContext);
TextView indicatorView2 = new TextView(mContext);
initDialog(mContext, mCallback, state, new MockInjector() {
@Override
public Button getTryAgainButton() {
return tryAgainButton2;
}
@Override
public TextView getIndicatorView() {
return indicatorView2;
}
});
mBiometricView.setRequireConfirmation(requireConfirmation);
waitForIdleSync();
// Test restored state
assertEquals(View.VISIBLE, tryAgainButton.getVisibility());
assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState);
assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility());
assertEquals(failureMessage, mBiometricView.mIndicatorView.getText());
}
private Bundle buildBiometricPromptBundle() {
Bundle bundle = new Bundle();
bundle.putCharSequence(BiometricPrompt.KEY_TITLE, "Title");
bundle.putCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT, "Negative");
return bundle;
}
private void initDialog(Context context, AuthBiometricView.Callback callback,
Bundle savedState, MockInjector injector) {
mBiometricView = new TestableBiometricView(context, null, injector);
mBiometricView.setBiometricPromptBundle(buildBiometricPromptBundle());
mBiometricView.setCallback(callback);
mBiometricView.restoreState(savedState);
mBiometricView.onFinishInflateInternal();
mBiometricView.onAttachedToWindowInternal();
}
private void initDialog(Context context, AuthBiometricView.Callback callback,
MockInjector injector) {
mBiometricView = new TestableBiometricView(context, null, injector);
mBiometricView.setCallback(callback);
mBiometricView.initializeViews();
initDialog(context, callback, null /* savedState */, injector);
}
private class MockInjector extends AuthBiometricView.Injector {
@@ -232,8 +312,8 @@ public class AuthBiometricViewTest extends SysuiTestCase {
}
@Override
public TextView getErrorView() {
return mErrorView;
public TextView getIndicatorView() {
return mIndicatorView;
}
@Override

View File

@@ -147,7 +147,7 @@ public class AuthControllerTest extends SysuiTestCase {
public void testShowInvoked_whenSystemRequested()
throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
verify(mDialog1).show(any());
verify(mDialog1).show(any(), any());
}
@Test
@@ -215,7 +215,7 @@ public class AuthControllerTest extends SysuiTestCase {
@Test
public void testShowNewDialog_beforeOldDialogDismissed_SkipsAnimations() throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
verify(mDialog1).show(any());
verify(mDialog1).show(any(), any());
showDialog(BiometricPrompt.TYPE_FACE);
@@ -223,13 +223,13 @@ public class AuthControllerTest extends SysuiTestCase {
verify(mDialog1).dismissWithoutCallback(eq(false) /* animate */);
// Second dialog should be shown without animation
verify(mDialog2).show(any());
verify(mDialog2).show(any(), any());
}
@Test
public void testConfigurationPersists_whenOnConfigurationChanged() throws Exception {
showDialog(BiometricPrompt.TYPE_FACE);
verify(mDialog1).show(any());
verify(mDialog1).show(any(), any());
mBiometricDialogImpl.onConfigurationChanged(new Configuration());
@@ -241,10 +241,7 @@ public class AuthControllerTest extends SysuiTestCase {
// Saved state is restored into new dialog
ArgumentCaptor<Bundle> captor2 = ArgumentCaptor.forClass(Bundle.class);
verify(mDialog2).restoreState(captor2.capture());
// Dialog for new configuration skips intro
verify(mDialog2).show(any());
verify(mDialog2).show(any(), captor2.capture());
// TODO: This should check all values we want to save/restore
assertEquals(captor.getValue(), captor2.getValue());