diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 43231803d..2d8dc0223 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -205,6 +205,16 @@
+
+
+
+
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 000000000..6e5887f2b
--- /dev/null
+++ b/briar-android/res/layout/activity_change_password.xml
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index d237e3260..d68a009fd 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -139,6 +139,8 @@
Connect via Tor
When using Wi-Fi or mobile data
Only when using Wi-Fi
+ Security
+ Change password
Panic button setup
Panic button
Configure how Briar will react when you use a panic button app
@@ -165,6 +167,10 @@
Delete your Briar account if a panic button is pressed. Caution: This will permanently delete your identities, contacts and messages
Uninstall Briar
This requires manual confirmation in a panic event
+ Enter your current password:
+ Choose your new password:
+ Confirm your new password:
+ Password has been changed.
Feedback
Send feedback
diff --git a/briar-android/res/xml/settings.xml b/briar-android/res/xml/settings.xml
index 7ee104ad2..80c75046a 100644
--- a/briar-android/res/xml/settings.xml
+++ b/briar-android/res/xml/settings.xml
@@ -25,6 +25,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index 09a9461ea..346c01ef8 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 000000000..4c93924ea
--- /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(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 b49c4c3fa..64e4d4df0 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 resultHandler);
+
+ void changePassword(String password, String newPassword,
+ ResultHandler resultHandler);
}
diff --git a/briar-android/src/org/briarproject/android/controller/PasswordControllerImpl.java b/briar-android/src/org/briarproject/android/controller/PasswordControllerImpl.java
index 22025b0a2..23527c81f 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 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 6500346eb..c194b3137 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 000000000..b7fb34821
--- /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> 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 resultHandler =
+ (ResultHandler) 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 resultHandler2 =
+ (ResultHandler) 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 000000000..a5dc6669d
--- /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;
+ }
+}