From c5708ee3ce3b0cd62afe582e9c78c9089d413b2a Mon Sep 17 00:00:00 2001
From: str4d <str4d@mail.i2p>
Date: Fri, 10 Jun 2016 12:58:01 +0000
Subject: [PATCH] Add a UI for changing the password

---
 briar-android/AndroidManifest.xml             |  10 +
 .../res/layout/activity_change_password.xml   | 128 ++++++++++
 briar-android/res/values/strings.xml          |   6 +
 briar-android/res/xml/settings.xml            |  14 ++
 .../android/ActivityComponent.java            |   2 +
 .../android/ChangePasswordActivity.java       | 162 ++++++++++++
 .../controller/PasswordController.java        |   3 +
 .../controller/PasswordControllerImpl.java    |  52 +++-
 .../controller/SetupControllerImpl.java       |  31 +--
 .../activity/ChangePasswordActivityTest.java  | 234 ++++++++++++++++++
 .../activity/TestChangePasswordActivity.java  |  29 +++
 11 files changed, 640 insertions(+), 31 deletions(-)
 create mode 100644 briar-android/res/layout/activity_change_password.xml
 create mode 100644 briar-android/src/org/briarproject/android/ChangePasswordActivity.java
 create mode 100644 briar-android/test/java/briarproject/activity/ChangePasswordActivityTest.java
 create mode 100644 briar-android/test/java/briarproject/activity/TestChangePasswordActivity.java

diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 43231803db..2d8dc0223d 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -205,6 +205,16 @@
 			</intent-filter>
 		</activity>
 
+		<activity
+			android:name=".android.ChangePasswordActivity"
+			android:label="@string/change_password"
+			android:parentActivityName=".android.SettingsActivity">
+			<meta-data
+				android:name="android.support.PARENT_ACTIVITY"
+				android:value=".android.SettingsActivity"
+				/>
+		</activity>
+
 		<activity
 			android:name=".android.panic.PanicPreferencesActivity"
 			android:label="@string/panic_setting">
diff --git a/briar-android/res/layout/activity_change_password.xml b/briar-android/res/layout/activity_change_password.xml
new file mode 100644
index 0000000000..6e5887f2bf
--- /dev/null
+++ b/briar-android/res/layout/activity_change_password.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"
+	tools:context=".android.ChangePasswordActivity">
+
+	<RelativeLayout
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:orientation="vertical"
+		android:paddingBottom="@dimen/margin_activity_vertical"
+		android:paddingEnd="@dimen/margin_activity_horizontal"
+		android:paddingLeft="@dimen/margin_activity_horizontal"
+		android:paddingRight="@dimen/margin_activity_horizontal"
+		android:paddingStart="@dimen/margin_activity_horizontal"
+		android:paddingTop="@dimen/margin_activity_vertical">
+
+		<TextView
+			android:id="@+id/current_password_title"
+			style="@style/BriarTextTitle"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_centerHorizontal="true"
+			android:text="@string/current_password"
+			android:textSize="@dimen/text_size_medium"/>
+
+		<android.support.design.widget.TextInputLayout
+			android:id="@+id/current_password_entry_wrapper"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:layout_below="@id/current_password_title"
+			android:layout_centerHorizontal="true"
+			app:errorEnabled="true">
+
+			<EditText
+				android:id="@+id/current_password_entry"
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:inputType="textPassword"
+				android:maxLines="1"/>
+		</android.support.design.widget.TextInputLayout>
+
+		<TextView
+			android:id="@+id/new_password_title"
+			style="@style/BriarTextTitle"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_below="@id/current_password_entry_wrapper"
+			android:layout_centerHorizontal="true"
+			android:text="@string/choose_new_password"
+			android:textSize="@dimen/text_size_medium"/>
+
+		<android.support.design.widget.TextInputLayout
+			android:id="@+id/new_password_entry_wrapper"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:layout_below="@id/new_password_title"
+			android:layout_centerHorizontal="true"
+			app:errorEnabled="true">
+
+			<EditText
+				android:id="@+id/new_password_entry"
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:inputType="textPassword"
+				android:maxLines="1"/>
+		</android.support.design.widget.TextInputLayout>
+
+		<TextView
+			android:id="@+id/new_password_confirm_title"
+			style="@style/BriarTextTitle"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_below="@id/new_password_entry_wrapper"
+			android:layout_centerHorizontal="true"
+			android:text="@string/confirm_new_password"
+			android:textSize="@dimen/text_size_medium"/>
+
+		<android.support.design.widget.TextInputLayout
+			android:id="@+id/new_password_confirm_wrapper"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:layout_below="@id/new_password_confirm_title"
+			android:layout_centerHorizontal="true"
+			app:errorEnabled="true">
+
+			<EditText
+				android:id="@+id/new_password_confirm"
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:imeOptions="actionDone"
+				android:inputType="textPassword"
+				android:maxLines="1"/>
+		</android.support.design.widget.TextInputLayout>
+
+		<org.briarproject.android.util.StrengthMeter
+			android:id="@+id/strength_meter"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:layout_below="@id/new_password_confirm_wrapper"
+			android:layout_centerHorizontal="true"
+			android:visibility="invisible"/>
+
+		<Button
+			android:id="@+id/change_password"
+			style="@style/BriarButton.Default"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:layout_below="@id/strength_meter"
+			android:layout_centerHorizontal="true"
+			android:layout_marginTop="@dimen/margin_medium"
+			android:enabled="false"
+			android:text="@string/change_password"/>
+
+		<ProgressBar
+			android:id="@+id/progress_wheel"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_alignTop="@id/change_password"
+			android:layout_centerHorizontal="true"
+			android:visibility="invisible"/>
+
+	</RelativeLayout>
+
+</ScrollView>
\ No newline at end of file
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index d237e3260c..d68a009fd0 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -139,6 +139,8 @@
 	<string name="tor_mobile_setting">Connect via Tor</string>
 	<string name="tor_mobile_setting_enabled">When using Wi-Fi or mobile data</string>
 	<string name="tor_mobile_setting_disabled">Only when using Wi-Fi</string>
+	<string name="security_settings_title">Security</string>
+	<string name="change_password">Change password</string>
 	<string name="panic_setting">Panic button setup</string>
 	<string name="panic_setting_title">Panic button</string>
 	<string name="panic_setting_hint">Configure how Briar will react when you use a panic button app</string>
@@ -165,6 +167,10 @@
 	<string name="purge_setting_summary">Delete your Briar account if a panic button is pressed. Caution: This will permanently delete your identities, contacts and messages</string>
 	<string name="uninstall_setting_title">Uninstall Briar</string>
 	<string name="uninstall_setting_summary">This requires manual confirmation in a panic event</string>
+	<string name="current_password">Enter your current password:</string>
+	<string name="choose_new_password">Choose your new password:</string>
+	<string name="confirm_new_password">Confirm your new password:</string>
+	<string name="password_changed">Password has been changed.</string>
 	<string name="feedback_settings_title">Feedback</string>
 	<string name="send_feedback">Send feedback</string>
 
diff --git a/briar-android/res/xml/settings.xml b/briar-android/res/xml/settings.xml
index 7ee104ad2b..80c75046aa 100644
--- a/briar-android/res/xml/settings.xml
+++ b/briar-android/res/xml/settings.xml
@@ -25,6 +25,20 @@
 
 	</PreferenceCategory>
 
+	<PreferenceCategory
+		android:title="@string/security_settings_title">
+
+		<Preference
+			android:key="pref_key_change_password"
+			android:title="@string/change_password">
+
+			<intent
+				android:targetClass="org.briarproject.android.ChangePasswordActivity"
+				android:targetPackage="org.briarproject"/>
+		</Preference>
+
+	</PreferenceCategory>
+
 	<PreferenceCategory
 		android:title="@string/panic_setting_title">
 
diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index 09a9461eae..346c01ef8b 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -62,6 +62,8 @@ public interface ActivityComponent {
 
 	void inject(SettingsActivity activity);
 
+	void inject(ChangePasswordActivity activity);
+
 	void inject(IntroductionActivity activity);
 
 	@Named("ContactListFragment")
diff --git a/briar-android/src/org/briarproject/android/ChangePasswordActivity.java b/briar-android/src/org/briarproject/android/ChangePasswordActivity.java
new file mode 100644
index 0000000000..4c93924ea8
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/ChangePasswordActivity.java
@@ -0,0 +1,162 @@
+package org.briarproject.android;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.TextInputLayout;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.Toast;
+
+import org.briarproject.R;
+import org.briarproject.android.controller.PasswordController;
+import org.briarproject.android.controller.SetupController;
+import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.android.util.AndroidUtils;
+import org.briarproject.android.util.StrengthMeter;
+
+import javax.inject.Inject;
+
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+import static org.briarproject.api.crypto.PasswordStrengthEstimator.WEAK;
+
+public class ChangePasswordActivity extends BaseActivity
+		implements OnClickListener,
+		OnEditorActionListener {
+
+	@Inject
+	protected PasswordController passwordController;
+	@Inject
+	protected SetupController setupController;
+
+	private TextInputLayout currentPasswordEntryWrapper;
+	private TextInputLayout newPasswordEntryWrapper;
+	private TextInputLayout newPasswordConfirmationWrapper;
+	private EditText currentPassword;
+	private EditText newPassword;
+	private EditText newPasswordConfirmation;
+	private StrengthMeter strengthMeter;
+	private Button changePasswordButton;
+	private ProgressBar progress;
+
+	@Override
+	public void onCreate(Bundle state) {
+		super.onCreate(state);
+		setContentView(R.layout.activity_change_password);
+
+		currentPasswordEntryWrapper =
+				(TextInputLayout) findViewById(
+						R.id.current_password_entry_wrapper);
+		newPasswordEntryWrapper =
+				(TextInputLayout) findViewById(R.id.new_password_entry_wrapper);
+		newPasswordConfirmationWrapper =
+				(TextInputLayout) findViewById(
+						R.id.new_password_confirm_wrapper);
+		currentPassword = (EditText) findViewById(R.id.current_password_entry);
+		newPassword = (EditText) findViewById(R.id.new_password_entry);
+		newPasswordConfirmation =
+				(EditText) findViewById(R.id.new_password_confirm);
+		strengthMeter = (StrengthMeter) findViewById(R.id.strength_meter);
+		changePasswordButton = (Button) findViewById(R.id.change_password);
+		progress = (ProgressBar) findViewById(R.id.progress_wheel);
+
+		TextWatcher tw = new TextWatcher() {
+
+			@Override
+			public void beforeTextChanged(CharSequence s, int start, int count,
+					int after) {
+			}
+
+			@Override
+			public void onTextChanged(CharSequence s, int start, int before,
+					int count) {
+				enableOrDisableContinueButton();
+			}
+
+			@Override
+			public void afterTextChanged(Editable s) {
+			}
+		};
+
+		currentPassword.addTextChangedListener(tw);
+		newPassword.addTextChangedListener(tw);
+		newPasswordConfirmation.addTextChangedListener(tw);
+		newPasswordConfirmation.setOnEditorActionListener(this);
+		changePasswordButton.setOnClickListener(this);
+	}
+
+	@Override
+	public void injectActivity(ActivityComponent component) {
+		component.inject(this);
+	}
+
+	private void enableOrDisableContinueButton() {
+		if (progress == null) return; // Not created yet
+		if (newPassword.getText().length() > 0 && newPassword.hasFocus())
+			strengthMeter.setVisibility(VISIBLE);
+		else strengthMeter.setVisibility(INVISIBLE);
+		String firstPassword = newPassword.getText().toString();
+		String secondPassword = newPasswordConfirmation.getText().toString();
+		boolean passwordsMatch = firstPassword.equals(secondPassword);
+		float strength =
+				setupController.estimatePasswordStrength(firstPassword);
+		strengthMeter.setStrength(strength);
+		AndroidUtils.setError(newPasswordEntryWrapper,
+				getString(R.string.password_too_weak),
+				firstPassword.length() > 0 && strength < WEAK);
+		AndroidUtils.setError(newPasswordConfirmationWrapper,
+				getString(R.string.passwords_do_not_match),
+				secondPassword.length() > 0 && !passwordsMatch);
+		changePasswordButton.setEnabled(
+				!currentPassword.getText().toString().isEmpty() &&
+						passwordsMatch && strength >= WEAK);
+	}
+
+	@Override
+	public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+		hideSoftKeyboard(v);
+		return true;
+	}
+
+	@Override
+	public void onClick(View view) {
+		// Replace the button with a progress bar
+		changePasswordButton.setVisibility(INVISIBLE);
+		progress.setVisibility(VISIBLE);
+		passwordController.changePassword(currentPassword.getText().toString(),
+				newPassword.getText().toString(),
+				new UiResultHandler<Boolean>(this) {
+					@Override
+					public void onResultUi(@NonNull Boolean result) {
+						if (result) {
+							Toast.makeText(ChangePasswordActivity.this,
+									R.string.password_changed,
+									Toast.LENGTH_LONG).show();
+							setResult(RESULT_OK);
+							finish();
+						} else {
+							tryAgain();
+						}
+					}
+				});
+	}
+
+	private void tryAgain() {
+		AndroidUtils.setError(currentPasswordEntryWrapper,
+				getString(R.string.try_again), true);
+		changePasswordButton.setVisibility(VISIBLE);
+		progress.setVisibility(INVISIBLE);
+		currentPassword.setText("");
+
+		// show the keyboard again
+		showSoftKeyboard(currentPassword);
+	}
+}
diff --git a/briar-android/src/org/briarproject/android/controller/PasswordController.java b/briar-android/src/org/briarproject/android/controller/PasswordController.java
index b49c4c3fa6..64e4d4df00 100644
--- a/briar-android/src/org/briarproject/android/controller/PasswordController.java
+++ b/briar-android/src/org/briarproject/android/controller/PasswordController.java
@@ -6,4 +6,7 @@ public interface PasswordController extends ConfigController {
 
 	void validatePassword(String password,
 			ResultHandler<Boolean> resultHandler);
+
+	void changePassword(String password, String newPassword,
+			ResultHandler<Boolean> resultHandler);
 }
diff --git a/briar-android/src/org/briarproject/android/controller/PasswordControllerImpl.java b/briar-android/src/org/briarproject/android/controller/PasswordControllerImpl.java
index 22025b0a21..23527c81fc 100644
--- a/briar-android/src/org/briarproject/android/controller/PasswordControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/controller/PasswordControllerImpl.java
@@ -1,28 +1,40 @@
 package org.briarproject.android.controller;
 
 import android.app.Activity;
+import android.content.SharedPreferences;
 
 import org.briarproject.android.controller.handler.ResultHandler;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.CryptoExecutor;
 import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.util.StringUtils;
 
 import java.util.concurrent.Executor;
+import java.util.logging.Logger;
 
 import javax.inject.Inject;
 
+import static java.util.logging.Level.INFO;
+
 public class PasswordControllerImpl extends ConfigControllerImpl
 		implements PasswordController {
 
+	private static final Logger LOG =
+			Logger.getLogger(PasswordControllerImpl.class.getName());
+
+	private final static String PREF_DB_KEY = "key";
+
 	@Inject
 	@CryptoExecutor
 	protected Executor cryptoExecutor;
 	@Inject
-	protected CryptoComponent crypto;
-	@Inject
 	protected Activity activity;
 
+	// Fields that are accessed from background threads must be volatile
+	@Inject
+	protected CryptoComponent crypto;
+
 	@Inject
 	public PasswordControllerImpl() {
 
@@ -46,10 +58,46 @@ public class PasswordControllerImpl extends ConfigControllerImpl
 		});
 	}
 
+	@Override
+	public void changePassword(final String password, final String newPassword,
+			final ResultHandler<Boolean> resultHandler) {
+		final byte[] encrypted = getEncryptedKey();
+		cryptoExecutor.execute(new Runnable() {
+			@Override
+			public void run() {
+				byte[] key = crypto.decryptWithPassword(encrypted, password);
+				if (key == null) {
+					resultHandler.onResult(false);
+				} else {
+					String hex =
+							encryptDatabaseKey(new SecretKey(key), newPassword);
+					storeEncryptedDatabaseKey(hex);
+					resultHandler.onResult(true);
+				}
+			}
+		});
+	}
+
 	private byte[] getEncryptedKey() {
 		String hex = getEncryptedDatabaseKey();
 		if (hex == null)
 			throw new IllegalStateException("Encrypted database key is null");
 		return StringUtils.fromHexString(hex);
 	}
+
+	// Call inside cryptoExecutor
+	String encryptDatabaseKey(SecretKey key, String password) {
+		long now = System.currentTimeMillis();
+		byte[] encrypted = crypto.encryptWithPassword(key.getBytes(), password);
+		long duration = System.currentTimeMillis() - now;
+		if (LOG.isLoggable(INFO))
+			LOG.info("Key derivation took " + duration + " ms");
+		return StringUtils.toHexString(encrypted);
+	}
+
+	void storeEncryptedDatabaseKey(String hex) {
+		SharedPreferences.Editor editor = briarPrefs.edit();
+		editor.putString(PREF_DB_KEY, hex);
+		editor.apply();
+	}
 }
diff --git a/briar-android/src/org/briarproject/android/controller/SetupControllerImpl.java b/briar-android/src/org/briarproject/android/controller/SetupControllerImpl.java
index 6500346ebe..c194b31370 100644
--- a/briar-android/src/org/briarproject/android/controller/SetupControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/controller/SetupControllerImpl.java
@@ -22,29 +22,17 @@ import javax.inject.Inject;
 
 import static java.util.logging.Level.INFO;
 
-public class SetupControllerImpl implements SetupController {
+public class SetupControllerImpl extends PasswordControllerImpl
+		implements SetupController {
 
 	private static final Logger LOG =
 			Logger.getLogger(SetupControllerImpl.class.getName());
 
-	private final static String PREF_DB_KEY = "key";
-
-	@Inject
-	@CryptoExecutor
-	protected Executor cryptoExecutor;
 	@Inject
 	protected PasswordStrengthEstimator strengthEstimator;
-	@Inject
-	protected Activity activity;
-	@Inject
-	protected SharedPreferences briarPrefs;
 
 	// Fields that are accessed from background threads must be volatile
 	@Inject
-	protected volatile CryptoComponent crypto;
-	@Inject
-	protected volatile DatabaseConfig databaseConfig;
-	@Inject
 	protected volatile AuthorFactory authorFactory;
 	@Inject
 	protected volatile ReferenceManager referenceManager;
@@ -54,15 +42,6 @@ public class SetupControllerImpl implements SetupController {
 
 	}
 
-	private String encryptDatabaseKey(SecretKey key, String password) {
-		long now = System.currentTimeMillis();
-		byte[] encrypted = crypto.encryptWithPassword(key.getBytes(), password);
-		long duration = System.currentTimeMillis() - now;
-		if (LOG.isLoggable(INFO))
-			LOG.info("Key derivation took " + duration + " ms");
-		return StringUtils.toHexString(encrypted);
-	}
-
 	private LocalAuthor createLocalAuthor(String nickname) {
 		long now = System.currentTimeMillis();
 		KeyPair keyPair = crypto.generateSignatureKeyPair();
@@ -98,10 +77,4 @@ public class SetupControllerImpl implements SetupController {
 			}
 		});
 	}
-
-	private void storeEncryptedDatabaseKey(String hex) {
-		SharedPreferences.Editor editor = briarPrefs.edit();
-		editor.putString(PREF_DB_KEY, hex);
-		editor.apply();
-	}
 }
diff --git a/briar-android/test/java/briarproject/activity/ChangePasswordActivityTest.java b/briar-android/test/java/briarproject/activity/ChangePasswordActivityTest.java
new file mode 100644
index 0000000000..b7fb34821f
--- /dev/null
+++ b/briar-android/test/java/briarproject/activity/ChangePasswordActivityTest.java
@@ -0,0 +1,234 @@
+package briarproject.activity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.support.design.widget.TextInputLayout;
+import android.widget.Button;
+import android.widget.EditText;
+
+import org.briarproject.BuildConfig;
+import org.briarproject.R;
+import org.briarproject.android.SettingsActivity;
+import org.briarproject.android.controller.PasswordController;
+import org.briarproject.android.controller.SetupController;
+import org.briarproject.android.controller.handler.ResultHandler;
+import org.briarproject.android.util.StrengthMeter;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowActivity;
+
+import static junit.framework.Assert.assertEquals;
+import static org.briarproject.api.crypto.PasswordStrengthEstimator.NONE;
+import static org.briarproject.api.crypto.PasswordStrengthEstimator.QUITE_STRONG;
+import static org.briarproject.api.crypto.PasswordStrengthEstimator.QUITE_WEAK;
+import static org.briarproject.api.crypto.PasswordStrengthEstimator.STRONG;
+import static org.briarproject.api.crypto.PasswordStrengthEstimator.WEAK;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = BuildConfig.class, sdk = 21,
+		application = TestBriarApplication.class)
+public class ChangePasswordActivityTest {
+
+	private TestChangePasswordActivity changePasswordActivity;
+	private TextInputLayout passwordConfirmationWrapper;
+	private EditText currentPassword;
+	private EditText newPassword;
+	private EditText newPasswordConfirmation;
+	private StrengthMeter strengthMeter;
+	private Button changePasswordButton;
+
+	@Mock
+	private PasswordController passwordController;
+	@Mock
+	private SetupController setupController;
+	@Captor
+	private ArgumentCaptor<ResultHandler<Boolean>> resultCaptor;
+
+	@Before
+	public void setUp() {
+		MockitoAnnotations.initMocks(this);
+		changePasswordActivity =
+				Robolectric.setupActivity(TestChangePasswordActivity.class);
+		passwordConfirmationWrapper = (TextInputLayout) changePasswordActivity
+				.findViewById(R.id.new_password_confirm_wrapper);
+		currentPassword =
+				(EditText) changePasswordActivity
+						.findViewById(R.id.current_password_entry);
+		newPassword =
+				(EditText) changePasswordActivity
+						.findViewById(R.id.new_password_entry);
+		newPasswordConfirmation =
+				(EditText) changePasswordActivity
+						.findViewById(R.id.new_password_confirm);
+		strengthMeter =
+				(StrengthMeter) changePasswordActivity
+						.findViewById(R.id.strength_meter);
+		changePasswordButton =
+				(Button) changePasswordActivity
+						.findViewById(R.id.change_password);
+	}
+
+	private void testStrengthMeter(String pass, float strength, int color) {
+		newPassword.setText(pass);
+		assertEquals(strengthMeter.getProgress(),
+				(int) (strengthMeter.getMax() * strength));
+		assertEquals(color, strengthMeter.getColor());
+	}
+
+	@Test
+	public void testPasswordMatchUI() {
+		// Password mismatch
+		newPassword.setText("really.safe.password");
+		newPasswordConfirmation.setText("really.safe.pass");
+		assertEquals(changePasswordButton.isEnabled(), false);
+		assertEquals(passwordConfirmationWrapper.getError(),
+				changePasswordActivity
+						.getString(R.string.passwords_do_not_match));
+		// Button enabled
+		newPassword.setText("really.safe.pass");
+		newPasswordConfirmation.setText("really.safe.pass");
+		// Confirm that the password mismatch error message is not visible
+		Assert.assertNotEquals(passwordConfirmationWrapper.getError(),
+				changePasswordActivity
+						.getString(R.string.passwords_do_not_match));
+		// Nick has not been set, expect the button to be disabled
+		assertEquals(changePasswordButton.isEnabled(), false);
+	}
+
+	@Test
+	public void testChangePasswordUI() {
+
+		PasswordController mockedPasswordController = this.passwordController;
+		SetupController mockedSetupController = this.setupController;
+		changePasswordActivity.setPasswordController(mockedPasswordController);
+		changePasswordActivity.setSetupController(mockedSetupController);
+		// Mock strong password strength answer
+		when(mockedSetupController.estimatePasswordStrength(anyString()))
+				.thenReturn(STRONG);
+		String curPass = "old.password";
+		String safePass = "really.safe.password";
+		currentPassword.setText(curPass);
+		newPassword.setText(safePass);
+		newPasswordConfirmation.setText(safePass);
+		// Confirm that the create account button is clickable
+		assertEquals(changePasswordButton.isEnabled(), true);
+		changePasswordButton.performClick();
+		// Verify that the controller's method was called with the correct
+		// params and get the callback
+		verify(mockedPasswordController, times(1))
+				.changePassword(eq(curPass), eq(safePass),
+						resultCaptor.capture());
+		// execute the callback
+		resultCaptor.getValue().onResult(true);
+		assertEquals(changePasswordActivity.isFinishing(), true);
+	}
+
+	@Test
+	public void testPasswordChange() {
+		PasswordController passwordController =
+				changePasswordActivity.getPasswordController();
+		SetupController setupController =
+				changePasswordActivity.getSetupController();
+		// mock a resulthandler
+		ResultHandler<Long> resultHandler =
+				(ResultHandler<Long>) mock(ResultHandler.class);
+		setupController.createIdentity("nick", "some.old.pass", resultHandler);
+		// blocking verification call with timeout that waits until the mocked
+		// result gets called with handle 0L, the expected value
+		verify(resultHandler, timeout(2000).times(1)).onResult(0L);
+		SharedPreferences prefs =
+				changePasswordActivity
+						.getSharedPreferences("db", Context.MODE_PRIVATE);
+		// Confirm database key
+		assertTrue(prefs.contains("key"));
+		String oldKey = prefs.getString("key", null);
+		// mock a resulthandler
+		ResultHandler<Boolean> resultHandler2 =
+				(ResultHandler<Boolean>) mock(ResultHandler.class);
+		passwordController
+				.changePassword("some.old.pass", "some.strong.pass",
+						resultHandler2);
+		// blocking verification call with timeout that waits until the mocked
+		// result gets called with handle 0L, the expected value
+		verify(resultHandler2, timeout(2000).times(1)).onResult(true);
+		// Confirm database key
+		assertTrue(prefs.contains("key"));
+		assertNotEquals(oldKey, prefs.getString("key", null));
+		// Note that Robolectric uses its own persistant storage that it
+		// wipes clean after each test run, no need to clean up manually.
+	}
+
+	@Test
+	public void testStrengthMeter() {
+		SetupController controller =
+				changePasswordActivity.getSetupController();
+
+		String strongPass = "very.strong.password.123";
+		String weakPass = "we";
+		String quiteStrongPass = "quite.strong";
+
+		float val = controller.estimatePasswordStrength(strongPass);
+		assertTrue(val == STRONG);
+		val = controller.estimatePasswordStrength(weakPass);
+		assertTrue(val < WEAK && val > NONE);
+		val = controller.estimatePasswordStrength(quiteStrongPass);
+		assertTrue(val < STRONG && val > QUITE_WEAK);
+	}
+
+	@Test
+	public void testStrengthMeterUI() {
+		Assert.assertNotNull(changePasswordActivity);
+		// replace the setup controller with our mocked copy
+		SetupController mockedController = this.setupController;
+		changePasswordActivity.setSetupController(mockedController);
+		// Mock answers for UI testing only
+		when(mockedController.estimatePasswordStrength("strong")).thenReturn(
+				STRONG);
+		when(mockedController.estimatePasswordStrength("qstring")).thenReturn(
+				QUITE_STRONG);
+		when(mockedController.estimatePasswordStrength("qweak")).thenReturn(
+				QUITE_WEAK);
+		when(mockedController.estimatePasswordStrength("weak")).thenReturn(
+				WEAK);
+		when(mockedController.estimatePasswordStrength("empty")).thenReturn(
+				NONE);
+		// Test the meters progress and color for several values
+		testStrengthMeter("strong", STRONG, StrengthMeter.GREEN);
+		Mockito.verify(mockedController, Mockito.times(1))
+				.estimatePasswordStrength(eq("strong"));
+		testStrengthMeter("qstring", QUITE_STRONG, StrengthMeter.LIME);
+		Mockito.verify(mockedController, Mockito.times(1))
+				.estimatePasswordStrength(eq("qstring"));
+		testStrengthMeter("qweak", QUITE_WEAK, StrengthMeter.YELLOW);
+		Mockito.verify(mockedController, Mockito.times(1))
+				.estimatePasswordStrength(eq("qweak"));
+		testStrengthMeter("weak", WEAK, StrengthMeter.ORANGE);
+		Mockito.verify(mockedController, Mockito.times(1))
+				.estimatePasswordStrength(eq("weak"));
+		// Not sure this should be the correct behaviour on an empty input ?
+		testStrengthMeter("empty", NONE, StrengthMeter.RED);
+		Mockito.verify(mockedController, Mockito.times(1))
+				.estimatePasswordStrength(eq("empty"));
+	}
+}
diff --git a/briar-android/test/java/briarproject/activity/TestChangePasswordActivity.java b/briar-android/test/java/briarproject/activity/TestChangePasswordActivity.java
new file mode 100644
index 0000000000..a5dc6669d9
--- /dev/null
+++ b/briar-android/test/java/briarproject/activity/TestChangePasswordActivity.java
@@ -0,0 +1,29 @@
+package briarproject.activity;
+
+import org.briarproject.android.ChangePasswordActivity;
+import org.briarproject.android.SetupActivity;
+import org.briarproject.android.controller.PasswordController;
+import org.briarproject.android.controller.SetupController;
+
+/**
+ * This class exposes the PasswordController and SetupController and offers the
+ * possibility to override them.
+ */
+public class TestChangePasswordActivity extends ChangePasswordActivity {
+
+	public PasswordController getPasswordController() {
+		return passwordController;
+	}
+
+	public SetupController getSetupController() {
+		return setupController;
+	}
+
+	public void setPasswordController(PasswordController passwordController) {
+		this.passwordController = passwordController;
+	}
+
+	public void setSetupController(SetupController setupController) {
+		this.setupController = setupController;
+	}
+}
-- 
GitLab