Replace AppIntro with a new custom intro

This removes the dependency on AppIntro and replaces it with our own custom
intro implementation, backed by ViewPager2. We're doing this because we want a
more reliable and customizable onboarding for Aegis.

I've kept the design mostly the same as it was before, but tried to achieve a
bit of a cleaner look:

<img src="https://alexbakker.me/u/vsr3ahpjt6.png" width="200"> <img
src="https://alexbakker.me/u/efqid2ixly.png" width="200"> <img
src="https://alexbakker.me/u/oehmjm0rn9.png" width="200">
This commit is contained in:
Alexander Bakker 2020-07-01 19:44:58 +02:00
parent 9d44d6abb2
commit 0e78fd9652
29 changed files with 1231 additions and 521 deletions

View file

@ -36,6 +36,8 @@ android {
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
unitTests.all {
useJUnitPlatform()
@ -115,9 +117,9 @@ dependencies {
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.getbase:floatingactionbutton:1.10.1'
implementation 'com.github.apl-devs:appintro:6.0.0'
implementation 'com.github.avito-tech:krop:0.44'
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
implementation "com.github.bumptech.glide:glide:${glideVersion}"
@ -138,15 +140,15 @@ dependencies {
implementation 'net.lingala.zip4j:zip4j:2.6.0'
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test:core:1.3.0-rc01'
androidTestImplementation 'androidx.test:runner:1.3.0-rc01'
androidTestImplementation 'androidx.test:rules:1.3.0-rc01'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
androidTestImplementation 'junit:junit:4.13'
androidTestUtil 'androidx.test:orchestrator:1.2.0'
androidTestUtil 'androidx.test:orchestrator:1.3.0-rc01'
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"

View file

@ -2,224 +2,25 @@ package com.beemdevelopment.aegis;
import android.view.View;
import androidx.annotation.IdRes;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.AmbiguousViewMatcherException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.platform.app.InstrumentationRegistry;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
import static androidx.test.espresso.action.ViewActions.clearText;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.action.ViewActions.pressBack;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import static org.hamcrest.Matchers.anything;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class AegisTest {
private static final String _password = "test";
private static final String _groupName = "Test";
@Rule
public final ActivityTestRule<MainActivity> activityRule = new ActivityTestRule<>(MainActivity.class);
@Test
public void doOverallTest() {
ViewInteraction next = onView(withId(R.id.next));
next.perform(click());
onView(withId(R.id.rb_password)).perform(click());
next.perform(click());
onView(withId(R.id.text_password)).perform(typeText(_password), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(_password), closeSoftKeyboard());
next.perform(click());
onView(withId(R.id.done)).perform(click());
VaultManager vault = getVault();
assertTrue(vault.isEncryptionEnabled());
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
List<VaultEntry> entries = Arrays.asList(
generateEntry(TotpInfo.class, "Frank", "Google"),
generateEntry(HotpInfo.class, "John", "GitHub"),
generateEntry(TotpInfo.class, "Alice", "Office 365"),
generateEntry(SteamInfo.class, "Gaben", "Steam")
);
for (VaultEntry entry : entries) {
addEntry(entry);
}
List<VaultEntry> realEntries = new ArrayList<>(vault.getEntries());
for (int i = 0; i < realEntries.size(); i++) {
assertTrue(realEntries.get(i).equivalates(entries.get(i)));
}
for (int i = 0; i < 10; i++) {
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, clickChildViewWithId(R.id.buttonRefresh)));
}
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
onView(withId(R.id.action_copy)).perform(click());
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
onView(withId(R.id.action_edit)).perform(click());
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
onView(withId(R.id.spinner_group)).perform(click());
onData(anything()).atPosition(1).perform(click());
onView(withId(R.id.text_input)).perform(typeText(_groupName), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
onView(isRoot()).perform(pressBack());
onView(withId(android.R.id.button1)).perform(click());
changeSort(R.string.sort_alphabetically_name);
changeSort(R.string.sort_alphabetically_name_reverse);
changeSort(R.string.sort_alphabetically);
changeSort(R.string.sort_alphabetically_reverse);
changeSort(R.string.sort_custom);
changeFilter(_groupName);
changeFilter(R.string.filter_ungrouped);
changeFilter(R.string.all);
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
onView(withId(R.id.action_share_qr)).perform(click());
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, longClick()));
onView(withId(R.id.action_delete)).perform(click());
onView(withId(android.R.id.button1)).perform(click());
openContextualActionModeOverflowMenu();
onView(withText(R.string.lock)).perform(click());
onView(withId(R.id.text_password)).perform(typeText(_password), closeSoftKeyboard());
onView(withId(R.id.button_decrypt)).perform(click());
vault = getVault();
openContextualActionModeOverflowMenu();
onView(withText(R.string.action_settings)).perform(click());
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
onView(withId(android.R.id.button1)).perform(click());
assertFalse(vault.isEncryptionEnabled());
assertNull(vault.getCredentials());
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
onView(withId(R.id.text_password)).perform(typeText(_password), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(_password), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
assertTrue(vault.isEncryptionEnabled());
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
public abstract class AegisTest {
protected AegisApplication getApp() {
return (AegisApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
}
private void changeSort(@IdRes int resId) {
onView(withId(R.id.action_sort)).perform(click());
onView(withText(resId)).perform(click());
}
private void changeFilter(String text) {
openContextualActionModeOverflowMenu();
onView(withText(R.string.filter)).perform(click());
onView(withText(text)).perform(click());
}
private void changeFilter(@IdRes int resId) {
changeFilter(ApplicationProvider.getApplicationContext().getString(resId));
}
private void addEntry(VaultEntry entry) {
onView(withId(R.id.fab_expand_menu_button)).perform(click());
onView(withId(R.id.fab_enter)).perform(click());
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
onView(withId(R.id.text_issuer)).perform(typeText(entry.getIssuer()), closeSoftKeyboard());
if (entry.getInfo().getClass() != TotpInfo.class) {
int i = entry.getInfo() instanceof HotpInfo ? 1 : 2;
try {
onView(withId(R.id.spinner_type)).perform(click());
onData(anything()).atPosition(i).perform(click());
} catch (AmbiguousViewMatcherException e) {
// for some reason, clicking twice is sometimes necessary, otherwise the test fails on the next line
onView(withId(R.id.spinner_type)).perform(click());
onData(anything()).atPosition(i).perform(click());
}
if (entry.getInfo() instanceof HotpInfo) {
onView(withId(R.id.text_counter)).perform(typeText("0"), closeSoftKeyboard());
}
if (entry.getInfo() instanceof SteamInfo) {
onView(withId(R.id.text_digits)).perform(clearText(), typeText("5"), closeSoftKeyboard());
}
}
String secret = Base32.encode(entry.getInfo().getSecret());
onView(withId(R.id.text_secret)).perform(typeText(secret), closeSoftKeyboard());
onView(withId(R.id.action_save)).perform(click());
}
private <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
byte[] secret = CryptoUtils.generateRandomBytes(20);
OtpInfo info;
try {
info = type.getConstructor(byte[].class).newInstance(secret);
} catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
return new VaultEntry(info, name, issuer);
}
private AegisApplication getApp() {
return (AegisApplication) activityRule.getActivity().getApplication();
}
private VaultManager getVault() {
protected VaultManager getVault() {
return getApp().getVaultManager();
}
// source: https://stackoverflow.com/a/30338665
private static ViewAction clickChildViewWithId(final int id) {
protected static ViewAction clickChildViewWithId(final int id) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {

View file

@ -0,0 +1,90 @@
package com.beemdevelopment.aegis;
import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import com.beemdevelopment.aegis.ui.IntroActivity;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import static org.hamcrest.Matchers.not;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class IntroTest extends AegisTest {
private static final String _password = "test";
@Rule
public final ActivityScenarioRule<IntroActivity> activityRule = new ActivityScenarioRule<>(IntroActivity.class);
@Test
public void doIntro_None() {
ViewInteraction next = onView(withId(R.id.btnNext));
ViewInteraction prev = onView(withId(R.id.btnPrevious));
prev.check(matches(not(isDisplayed())));
next.perform(click());
onView(withId(R.id.rb_none)).perform(click());
prev.perform(click());
prev.check(matches(not(isDisplayed())));
next.perform(click());
next.perform(click());
prev.check(matches(not(isDisplayed())));
next.perform(click());
next.perform(click());
VaultManager vault = getVault();
assertFalse(vault.isEncryptionEnabled());
assertNull(getVault().getCredentials());
}
@Test
public void doIntro_Password() {
ViewInteraction next = onView(withId(R.id.btnNext));
ViewInteraction prev = onView(withId(R.id.btnPrevious));
prev.check(matches(not(isDisplayed())));
next.perform(click());
onView(withId(R.id.rb_password)).perform(click());
prev.perform(click());
prev.check(matches(not(isDisplayed())));
next.perform(click());
next.perform(click());
onView(withId(R.id.text_password)).perform(typeText(_password), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(_password + "1"), closeSoftKeyboard());
next.perform(click());
onView(withId(R.id.text_password_confirm)).perform(replaceText(_password), closeSoftKeyboard());
prev.perform(click());
prev.perform(click());
prev.check(matches(not(isDisplayed())));
next.perform(click());
next.perform(click());
next.perform(click());
next.perform(click());
VaultManager vault = getVault();
SlotList slots = getVault().getCredentials().getSlots();
assertTrue(vault.isEncryptionEnabled());
assertTrue(slots.has(PasswordSlot.class));
assertFalse(slots.has(BiometricSlot.class));
}
}

View file

@ -0,0 +1,207 @@
package com.beemdevelopment.aegis;
import androidx.annotation.IdRes;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.AmbiguousViewMatcherException;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
import static androidx.test.espresso.action.ViewActions.clearText;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.action.ViewActions.pressBack;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import static org.hamcrest.Matchers.anything;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class OverallTest extends AegisTest {
private static final String _password = "test";
private static final String _groupName = "Test";
@Rule
public final ActivityTestRule<MainActivity> activityRule = new ActivityTestRule<>(MainActivity.class);
@Test
public void doOverallTest() {
ViewInteraction next = onView(withId(R.id.btnNext));
next.perform(click());
onView(withId(R.id.rb_password)).perform(click());
next.perform(click());
onView(withId(R.id.text_password)).perform(typeText(_password), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(_password), closeSoftKeyboard());
next.perform(click());
onView(withId(R.id.btnNext)).perform(click());
VaultManager vault = getVault();
assertTrue(vault.isEncryptionEnabled());
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
List<VaultEntry> entries = Arrays.asList(
generateEntry(TotpInfo.class, "Frank", "Google"),
generateEntry(HotpInfo.class, "John", "GitHub"),
generateEntry(TotpInfo.class, "Alice", "Office 365"),
generateEntry(SteamInfo.class, "Gaben", "Steam")
);
for (VaultEntry entry : entries) {
addEntry(entry);
}
List<VaultEntry> realEntries = new ArrayList<>(vault.getEntries());
for (int i = 0; i < realEntries.size(); i++) {
assertTrue(realEntries.get(i).equivalates(entries.get(i)));
}
for (int i = 0; i < 10; i++) {
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, clickChildViewWithId(R.id.buttonRefresh)));
}
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
onView(withId(R.id.action_copy)).perform(click());
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
onView(withId(R.id.action_edit)).perform(click());
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
onView(withId(R.id.spinner_group)).perform(click());
onData(anything()).atPosition(1).perform(click());
onView(withId(R.id.text_input)).perform(typeText(_groupName), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
onView(isRoot()).perform(pressBack());
onView(withId(android.R.id.button1)).perform(click());
changeSort(R.string.sort_alphabetically_name);
changeSort(R.string.sort_alphabetically_name_reverse);
changeSort(R.string.sort_alphabetically);
changeSort(R.string.sort_alphabetically_reverse);
changeSort(R.string.sort_custom);
changeFilter(_groupName);
changeFilter(R.string.filter_ungrouped);
changeFilter(R.string.all);
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
onView(withId(R.id.action_share_qr)).perform(click());
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, longClick()));
onView(withId(R.id.action_delete)).perform(click());
onView(withId(android.R.id.button1)).perform(click());
openContextualActionModeOverflowMenu();
onView(withText(R.string.lock)).perform(click());
onView(withId(R.id.text_password)).perform(typeText(_password), closeSoftKeyboard());
onView(withId(R.id.button_decrypt)).perform(click());
vault = getVault();
openContextualActionModeOverflowMenu();
onView(withText(R.string.action_settings)).perform(click());
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
onView(withId(android.R.id.button1)).perform(click());
assertFalse(vault.isEncryptionEnabled());
assertNull(vault.getCredentials());
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click()));
onView(withId(R.id.text_password)).perform(typeText(_password), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(_password), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
assertTrue(vault.isEncryptionEnabled());
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
}
private void changeSort(@IdRes int resId) {
onView(withId(R.id.action_sort)).perform(click());
onView(withText(resId)).perform(click());
}
private void changeFilter(String text) {
openContextualActionModeOverflowMenu();
onView(withText(R.string.filter)).perform(click());
onView(withText(text)).perform(click());
}
private void changeFilter(@IdRes int resId) {
changeFilter(ApplicationProvider.getApplicationContext().getString(resId));
}
private void addEntry(VaultEntry entry) {
onView(withId(R.id.fab_expand_menu_button)).perform(click());
onView(withId(R.id.fab_enter)).perform(click());
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
onView(withId(R.id.text_issuer)).perform(typeText(entry.getIssuer()), closeSoftKeyboard());
if (entry.getInfo().getClass() != TotpInfo.class) {
int i = entry.getInfo() instanceof HotpInfo ? 1 : 2;
try {
onView(withId(R.id.spinner_type)).perform(click());
onData(anything()).atPosition(i).perform(click());
} catch (AmbiguousViewMatcherException e) {
// for some reason, clicking twice is sometimes necessary, otherwise the test fails on the next line
onView(withId(R.id.spinner_type)).perform(click());
onData(anything()).atPosition(i).perform(click());
}
if (entry.getInfo() instanceof HotpInfo) {
onView(withId(R.id.text_counter)).perform(typeText("0"), closeSoftKeyboard());
}
if (entry.getInfo() instanceof SteamInfo) {
onView(withId(R.id.text_digits)).perform(clearText(), typeText("5"), closeSoftKeyboard());
}
}
String secret = Base32.encode(entry.getInfo().getSecret());
onView(withId(R.id.text_secret)).perform(typeText(secret), closeSoftKeyboard());
onView(withId(R.id.action_save)).perform(click());
}
private <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
byte[] secret = CryptoUtils.generateRandomBytes(20);
OtpInfo info;
try {
info = type.getConstructor(byte[].class).newInstance(secret);
} catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
return new VaultEntry(info, name, issuer);
}
}

View file

@ -57,7 +57,8 @@
android:label="@string/title_activity_edit_entry" />
<activity
android:name=".ui.IntroActivity"
android:theme="@style/Theme.Intro" />
android:theme="@style/AppTheme.NoActionBar"
android:screenOrientation="portrait" />
<activity
android:name=".ui.AuthActivity"
android:theme="@style/AppTheme.NoActionBar" />

View file

@ -151,6 +151,6 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
* the vault was locked by an external trigger while the Activity was still open.
*/
protected boolean isOrphan() {
return !(this instanceof MainActivity) && !(this instanceof AuthActivity) && _app.isVaultLocked();
return !(this instanceof MainActivity) && !(this instanceof AuthActivity) && !(this instanceof IntroActivity) && _app.isVaultLocked();
}
}

View file

@ -1,107 +1,77 @@
package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
import android.view.WindowManager;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.slides.SecuritySetupSlide;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.ui.intro.IntroBaseActivity;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
import com.beemdevelopment.aegis.ui.slides.DoneSlide;
import com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide;
import com.beemdevelopment.aegis.ui.slides.SecuritySetupSlide;
import com.beemdevelopment.aegis.ui.slides.WelcomeSlide;
import com.beemdevelopment.aegis.vault.Vault;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.github.appintro.AppIntro2;
import com.github.appintro.AppIntroFragment;
import com.github.appintro.model.SliderPage;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import org.json.JSONObject;
public class IntroActivity extends AppIntro2 {
private SecuritySetupSlide securitySetupSlide;
private SecurityPickerSlide _securityPickerSlide;
private Fragment _endSlide;
private AegisApplication _app;
private Preferences _prefs;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_INVALID;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_NONE;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_PASS;
public class IntroActivity extends IntroBaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_app = (AegisApplication) getApplication();
// set FLAG_SECURE on the window of every IntroActivity
_prefs = new Preferences(this);
if (_prefs.isSecureScreenEnabled()) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
setWizardMode(true);
setSkipButtonEnabled(false);
showStatusBar(true);
setSystemBackButtonLocked(true);
setBarColor(getResources().getColor(R.color.colorPrimary));
SliderPage homeSliderPage = new SliderPage();
homeSliderPage.setTitle(getString(R.string.welcome));
homeSliderPage.setImageDrawable(R.drawable.app_icon);
homeSliderPage.setTitleColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setDescription(getString(R.string.app_description));
homeSliderPage.setDescriptionColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setBackgroundColor(getResources().getColor(R.color.colorSecondary));
addSlide(AppIntroFragment.newInstance(homeSliderPage));
_securityPickerSlide = new SecurityPickerSlide();
_securityPickerSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(_securityPickerSlide);
securitySetupSlide = new SecuritySetupSlide();
securitySetupSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(securitySetupSlide);
SliderPage endSliderPage = new SliderPage();
endSliderPage.setTitle(getString(R.string.setup_completed));
endSliderPage.setDescription(getString(R.string.setup_completed_description));
endSliderPage.setImageDrawable(R.drawable.app_icon);
endSliderPage.setBackgroundColor(getResources().getColor(R.color.colorSecondary));
_endSlide = AppIntroFragment.newInstance(endSliderPage);
addSlide(_endSlide);
addSlide(WelcomeSlide.class);
addSlide(SecurityPickerSlide.class);
addSlide(SecuritySetupSlide.class);
addSlide(DoneSlide.class);
}
@Override
public void onSlideChanged(Fragment oldFragment, Fragment newFragment) {
if (oldFragment == _securityPickerSlide && newFragment != _endSlide) {
// skip to the last slide if no encryption will be used
int cryptType = getIntent().getIntExtra("cryptType", SecurityPickerSlide.CRYPT_TYPE_INVALID);
if (cryptType == SecurityPickerSlide.CRYPT_TYPE_NONE) {
// TODO: no magic indices
goToNextSlide(false);
}
}
if (newFragment == _endSlide) {
setWizardMode(false);
}
setSwipeLock(true);
protected void onSetTheme() {
setTheme(ThemeMap.NO_ACTION_BAR);
}
@Override
public void onDonePressed(Fragment currentFragment) {
super.onDonePressed(currentFragment);
protected boolean onBeforeSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
if (oldSlide == SecurityPickerSlide.class
&& newSlide == SecuritySetupSlide.class
&& getState().getInt("cryptType", CRYPT_TYPE_INVALID) == CRYPT_TYPE_NONE) {
skipToSlide(DoneSlide.class);
return true;
}
int cryptType = securitySetupSlide.getCryptType();
VaultFileCredentials creds = securitySetupSlide.getCredentials();
return false;
}
@Override
protected void onDonePressed() {
Bundle state = getState();
int cryptType = state.getInt("cryptType", CRYPT_TYPE_INVALID);
VaultFileCredentials creds = (VaultFileCredentials) state.getSerializable("creds");
if (cryptType == CRYPT_TYPE_INVALID
|| (cryptType == CRYPT_TYPE_NONE && creds != null)
|| (cryptType == CRYPT_TYPE_PASS && (creds == null || !creds.getSlots().has(PasswordSlot.class)))
|| (cryptType == CRYPT_TYPE_BIOMETRIC && (creds == null || !creds.getSlots().has(PasswordSlot.class) || !creds.getSlots().has(BiometricSlot.class)))) {
throw new RuntimeException(String.format("State of SecuritySetupSlide not properly propagated, cryptType: %d, creds: %s", cryptType, creds));
}
Vault vault = new Vault();
VaultFile vaultFile = new VaultFile();
try {
JSONObject obj = vault.toJson();
if (cryptType == SecurityPickerSlide.CRYPT_TYPE_NONE) {
if (cryptType == CRYPT_TYPE_NONE) {
vaultFile.setContent(obj);
} else {
vaultFile.setContent(obj, creds);
@ -114,20 +84,16 @@ public class IntroActivity extends AppIntro2 {
return;
}
if (cryptType == SecurityPickerSlide.CRYPT_TYPE_NONE) {
_app.initVaultManager(vault, null);
if (cryptType == CRYPT_TYPE_NONE) {
getApp().initVaultManager(vault, null);
} else {
_app.initVaultManager(vault, creds);
getApp().initVaultManager(vault, creds);
}
// skip the intro from now on
_prefs.setIntroDone(true);
getPreferences().setIntroDone(true);
setResult(RESULT_OK);
finish();
}
public void goToNextSlide() {
super.goToNextSlide(false);
}
}

View file

@ -0,0 +1,30 @@
package com.beemdevelopment.aegis.ui.intro;
import android.os.Bundle;
import androidx.annotation.NonNull;
public interface IntroActivityInterface {
/**
* Navigate to the next slide.
*/
void goToNextSlide();
/**
* Navigate to the previous slide.
*/
void goToPreviousSlide();
/**
* Navigate to the slide of the given type.
*/
void skipToSlide(Class<? extends SlideFragment> type);
/**
* Retrieves the state of the intro. The state is shared among all slides and is
* properly restored after a configuration change. This method may only be called
* after onAttach has been called.
*/
@NonNull
Bundle getState();
}

View file

@ -0,0 +1,215 @@
package com.beemdevelopment.aegis.ui.intro;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageButton;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ui.AegisActivity;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public abstract class IntroBaseActivity extends AegisActivity implements IntroActivityInterface {
private Bundle _state;
private ViewPager2 _pager;
private ScreenSlidePagerAdapter _adapter;
private List<Class<? extends SlideFragment>> _slides;
private WeakReference<SlideFragment> _currentSlide;
private ImageButton _btnPrevious;
private ImageButton _btnNext;
private SlideIndicator _slideIndicator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_intro);
_slides = new ArrayList<>();
_state = new Bundle();
_btnPrevious = findViewById(R.id.btnPrevious);
_btnPrevious.setOnClickListener(v -> goToPreviousSlide());
_btnNext = findViewById(R.id.btnNext);
_btnNext.setOnClickListener(v -> goToNextSlide());
_slideIndicator = findViewById(R.id.slideIndicator);
_adapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
_pager = findViewById(R.id.pager);
_pager.setAdapter(_adapter);
_pager.setUserInputEnabled(false);
_pager.registerOnPageChangeCallback(new SlideSkipBlocker());
View pagerChild = _pager.getChildAt(0);
if (pagerChild instanceof RecyclerView) {
pagerChild.setOverScrollMode(View.OVER_SCROLL_NEVER);
}
}
@Override
public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
_state = savedInstanceState.getBundle("introState");
updatePagerControls();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBundle("introState", _state);
}
void setCurrentSlide(SlideFragment slide) {
_currentSlide = new WeakReference<>(slide);
}
@Override
public void goToNextSlide() {
int pos = _pager.getCurrentItem();
if (pos != _slides.size() - 1) {
SlideFragment currentSlide = _currentSlide.get();
if (currentSlide.isFinished()) {
currentSlide.onSaveIntroState(_state);
setPagerPosition(pos, 1);
} else {
currentSlide.onNotFinishedError();
}
} else {
onDonePressed();
}
}
@Override
public void goToPreviousSlide() {
int pos = _pager.getCurrentItem();
if (pos != 0 && pos != _slides.size() - 1) {
setPagerPosition(pos, -1);
}
}
@Override
public void skipToSlide(Class<? extends SlideFragment> type) {
int i = _slides.indexOf(type);
if (i == -1) {
throw new IllegalStateException(String.format("Cannot skip to slide of type %s because it is not in the slide list", type.getName()));
}
setPagerPosition(i);
}
/**
* Called before a slide change is made. Overriding gives implementers the
* opportunity to block a slide change. onSaveIntroState is guaranteed to have been
* called on oldSlide before onBeforeSlideChanged is called.
* @param oldSlide the slide that is currently shown.
* @param newSlide the next slide that will be shown.
* @return whether to block the transition.
*/
protected boolean onBeforeSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
return false;
}
/**
* Called after a slide change was made.
* @param oldSlide the slide that was previously shown.
* @param newSlide the slide that is now shown.
*/
protected void onAfterSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
}
private void setPagerPosition(int pos) {
Class<? extends SlideFragment> oldSlide = _currentSlide.get().getClass();
Class<? extends SlideFragment> newSlide = _slides.get(pos);
if (!onBeforeSlideChanged(oldSlide, newSlide)) {
_pager.setCurrentItem(pos);
}
onAfterSlideChanged(oldSlide, newSlide);
updatePagerControls();
}
private void setPagerPosition(int pos, int delta) {
pos += delta;
setPagerPosition(pos);
}
private void updatePagerControls() {
int pos = _pager.getCurrentItem();
_btnPrevious.setVisibility(
pos != 0 && pos != _slides.size() - 1
? View.VISIBLE
: View.INVISIBLE);
if (pos == _slides.size() - 1) {
_btnNext.setImageResource(R.drawable.circular_button_done);
}
_slideIndicator.setSlideCount(_slides.size());
_slideIndicator.setCurrentSlide(pos);
}
@NonNull
public Bundle getState() {
return _state;
}
@Override
public void onBackPressed() {
goToPreviousSlide();
}
protected abstract void onDonePressed();
public void addSlide(Class<? extends SlideFragment> type) {
if (_slides.contains(type)) {
throw new IllegalStateException(String.format("Only one slide of type %s may be added to the intro", type.getName()));
}
_slides.add(type);
_slideIndicator.setSlideCount(_slides.size());
}
private class ScreenSlidePagerAdapter extends FragmentStateAdapter {
public ScreenSlidePagerAdapter(FragmentManager fm) {
super(fm, getLifecycle());
}
@NonNull
@Override
public Fragment createFragment(int position) {
Class<? extends SlideFragment> type = _slides.get(position);
try {
return type.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
throw new RuntimeException(e);
}
}
@Override
public int getItemCount() {
return _slides.size();
}
}
private class SlideSkipBlocker extends ViewPager2.OnPageChangeCallback {
@Override
public void onPageScrollStateChanged(@ViewPager2.ScrollState int state) {
// disable the buttons while scrolling to prevent disallowed skipping of slides
boolean enabled = state == ViewPager2.SCROLL_STATE_IDLE;
_btnNext.setEnabled(enabled);
_btnPrevious.setEnabled(enabled);
}
}
}

View file

@ -0,0 +1,87 @@
package com.beemdevelopment.aegis.ui.intro;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import java.lang.ref.WeakReference;
public abstract class SlideFragment extends Fragment implements IntroActivityInterface {
private WeakReference<IntroBaseActivity> _parent;
@CallSuper
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (!(context instanceof IntroBaseActivity)) {
throw new ClassCastException("Parent context is expected to be of type IntroBaseActivity");
}
_parent = new WeakReference<>((IntroBaseActivity) context);
}
@CallSuper
@Override
public void onResume() {
super.onResume();
getParent().setCurrentSlide(this);
}
/**
* Reports whether or not all required user actions are finished on this slide,
* indicating that we're ready to move to the next slide.
*/
public boolean isFinished() {
return true;
}
/**
* Called if the user tried to move to the next slide, but isFinished returned false.
*/
protected void onNotFinishedError() {
}
/**
* Called when the SlideFragment is expected to write its state to the given shared
* introState. This is only called if the user navigates to the next slide, not
* when a previous slide is next to be shown.
*/
protected void onSaveIntroState(@NonNull Bundle introState) {
}
@Override
public void goToNextSlide() {
getParent().goToNextSlide();
}
@Override
public void goToPreviousSlide() {
getParent().goToPreviousSlide();
}
@Override
public void skipToSlide(Class<? extends SlideFragment> type) {
getParent().skipToSlide(type);
}
@NonNull
@Override
public Bundle getState() {
return getParent().getState();
}
@NonNull
private IntroBaseActivity getParent() {
if (_parent == null || _parent.get() == null) {
throw new IllegalStateException("This method must not be called before onAttach()");
}
return _parent.get();
}
}

View file

@ -0,0 +1,98 @@
package com.beemdevelopment.aegis.ui.intro;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.R;
public class SlideIndicator extends View {
private Paint _paint;
private int _slideCount;
private int _slideIndex;
private float _dotRadius;
private float _dotSeparator;
private int _dotColor;
private int _dotColorSelected;
public SlideIndicator(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
_paint = new Paint();
_paint.setAntiAlias(true);
_paint.setStyle(Paint.Style.FILL);
TypedArray array = null;
try {
array = context.obtainStyledAttributes(attrs, R.styleable.SlideIndicator);
_dotRadius = array.getDimension(R.styleable.SlideIndicator_dot_radius, 5f);
_dotSeparator = array.getDimension(R.styleable.SlideIndicator_dot_separation, 5f);
_dotColor = array.getColor(R.styleable.SlideIndicator_dot_color, Color.GRAY);
_dotColorSelected = array.getColor(R.styleable.SlideIndicator_dot_color_selected, Color.BLACK);
} finally {
if (array != null) {
array.recycle();
}
}
}
public void setSlideCount(int slideCount) {
if (slideCount < 0) {
throw new IllegalArgumentException("Slide count cannot be negative");
}
_slideCount = slideCount;
invalidate();
}
public void setCurrentSlide(int index) {
if (index < 0) {
throw new IllegalArgumentException("Slide index cannot be negative");
}
if (index + 1 > _slideCount) {
throw new IllegalStateException(String.format("Slide index out of range, slides: %d, index: %d", _slideCount, index));
}
_slideIndex = index;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
if (_slideCount <= 0) {
return;
}
float density = getResources().getDisplayMetrics().density;
float dotDp = density * _dotRadius * 2;
float spaceDp = density * _dotSeparator;
float offset;
if (_slideCount % 2 == 0) {
offset = (spaceDp / 2) + (dotDp / 2) + dotDp * (_slideCount / 2f - 1) + spaceDp * (_slideCount / 2f - 1);
} else {
int spaces = _slideCount > 1 ? _slideCount - 2 : 0;
offset = (_slideCount - 1) * (dotDp / 2) + spaces * spaceDp;
}
canvas.translate((getWidth() / 2f) - offset,getHeight() / 2f);
for (int i = 0; i < _slideCount; i++) {
int slideIndex = isRtl() ? (_slideCount - 1) - _slideIndex : _slideIndex;
_paint.setColor(i == slideIndex ? _dotColorSelected : _dotColor);
canvas.drawCircle(0,0, dotDp / 2, _paint);
canvas.translate(dotDp + spaceDp,0);
}
}
private boolean isRtl() {
return getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
}
}

View file

@ -0,0 +1,16 @@
package com.beemdevelopment.aegis.ui.slides;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
public class DoneSlide extends SlideFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_done_slide, container, false);
}
}

View file

@ -1,6 +1,5 @@
package com.beemdevelopment.aegis.ui.slides;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@ -8,80 +7,87 @@ import android.view.ViewGroup;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.fragment.app.Fragment;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.github.appintro.SlidePolicy;
import com.google.android.material.snackbar.Snackbar;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
public class SecurityPickerSlide extends Fragment implements SlidePolicy, RadioGroup.OnCheckedChangeListener {
public class SecurityPickerSlide extends SlideFragment {
public static final int CRYPT_TYPE_INVALID = 0;
public static final int CRYPT_TYPE_NONE = 1;
public static final int CRYPT_TYPE_PASS = 2;
public static final int CRYPT_TYPE_BIOMETRIC = 3;
private RadioGroup _buttonGroup;
private int _bgColor;
private RadioButton _bioButton;
private TextView _bioText;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_security_picker_slide, container, false);
View view = inflater.inflate(R.layout.fragment_security_picker_slide, container, false);
_buttonGroup = view.findViewById(R.id.rg_authenticationMethod);
_buttonGroup.setOnCheckedChangeListener(this);
onCheckedChanged(_buttonGroup, _buttonGroup.getCheckedRadioButtonId());
// only enable the fingerprint option if the api version is new enough, permission is granted and a scanner is found
if (BiometricsHelper.isAvailable(getContext())) {
RadioButton button = view.findViewById(R.id.rb_biometrics);
TextView text = view.findViewById(R.id.text_rb_biometrics);
button.setEnabled(true);
text.setEnabled(true);
_buttonGroup.check(R.id.rb_biometrics);
}
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
_bioButton = view.findViewById(R.id.rb_biometrics);
_bioText = view.findViewById(R.id.text_rb_biometrics);
updateBiometricsOption(true);
return view;
}
public void setBgColor(int color) {
_bgColor = color;
@Override
public void onResume() {
super.onResume();
updateBiometricsOption(false);
}
/**
* Updates the status of the biometrics option. Auto-selects the biometrics option
* if the API version is new enough, permission is granted and a scanner is found.
*/
private void updateBiometricsOption(boolean autoSelect) {
boolean canUseBio = BiometricsHelper.isAvailable(getContext());
_bioButton.setEnabled(canUseBio);
_bioText.setEnabled(canUseBio);
if (!canUseBio && _buttonGroup.getCheckedRadioButtonId() == R.id.rb_biometrics) {
_buttonGroup.check(R.id.rb_password);
}
if (canUseBio && autoSelect) {
_buttonGroup.check(R.id.rb_biometrics);
}
}
@Override
public boolean isPolicyRespected() {
public boolean isFinished() {
return _buttonGroup.getCheckedRadioButtonId() != -1;
}
@Override
public void onUserIllegallyRequestedNextPage() {
Snackbar snackbar = Snackbar.make(getView(), getString(R.string.snackbar_authentication_method), Snackbar.LENGTH_LONG);
snackbar.show();
public void onNotFinishedError() {
Toast.makeText(getContext(), R.string.snackbar_authentication_method, Toast.LENGTH_SHORT).show();
}
@Override
public void onCheckedChanged(RadioGroup radioGroup, int i) {
if (i == -1) {
return;
}
public void onSaveIntroState(@NonNull Bundle introState) {
int buttonId = _buttonGroup.getCheckedRadioButtonId();
int id;
switch (i) {
int type;
switch (buttonId) {
case R.id.rb_none:
id = CRYPT_TYPE_NONE;
type = CRYPT_TYPE_NONE;
break;
case R.id.rb_password:
id = CRYPT_TYPE_PASS;
type = CRYPT_TYPE_PASS;
break;
case R.id.rb_biometrics:
id = CRYPT_TYPE_BIOMETRIC;
type = CRYPT_TYPE_BIOMETRIC;
break;
default:
throw new RuntimeException(String.format("Unsupported security setting: %d", i));
throw new RuntimeException(String.format("Unsupported security type: %d", buttonId));
}
Intent intent = getActivity().getIntent();
intent.putExtra("cryptType", id);
introState.putInt("cryptType", type);
}
}

View file

@ -1,6 +1,5 @@
package com.beemdevelopment.aegis.ui.slides;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Bundle;
@ -14,10 +13,10 @@ import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
@ -25,16 +24,13 @@ import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.PasswordStrengthHelper;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.IntroActivity;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.github.appintro.SlidePolicy;
import com.github.appintro.SlideSelectionListener;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputLayout;
import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.Zxcvbn;
@ -42,8 +38,12 @@ import com.nulabinc.zxcvbn.Zxcvbn;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSelectionListener {
private int _bgColor;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_INVALID;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_NONE;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_PASS;
public class SecuritySetupSlide extends SlideFragment {
private EditText _textPassword;
private EditText _textPasswordConfirm;
private CheckBox _checkPasswordVisibility;
@ -56,8 +56,7 @@ public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSe
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Zxcvbn zxcvbn = new Zxcvbn();
final View view = inflater.inflate(R.layout.fragment_security_setup_slide, container, false);
View view = inflater.inflate(R.layout.fragment_security_setup_slide, container, false);
_textPassword = view.findViewById(R.id.text_password);
_textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
_checkPasswordVisibility = view.findViewById(R.id.check_toggle_visibility);
@ -78,9 +77,11 @@ public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSe
});
_textPassword.addTextChangedListener(new TextWatcher() {
private Zxcvbn _zxcvbn = new Zxcvbn();
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
Strength strength = zxcvbn.measure(_textPassword.getText());
Strength strength = _zxcvbn.measure(_textPassword.getText());
_barPasswordStrength.setProgress(strength.getScore());
_barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(PasswordStrengthHelper.getColor(strength.getScore()))));
_textPasswordStrength.setText((_textPassword.getText().length() != 0) ? PasswordStrengthHelper.getString(strength.getScore(), getContext()) : "");
@ -97,20 +98,19 @@ public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSe
}
});
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
return view;
}
public int getCryptType() {
return _cryptType;
}
@Override
public void onResume() {
super.onResume();
public VaultFileCredentials getCredentials() {
return _creds;
}
_cryptType = getState().getInt("cryptType", CRYPT_TYPE_INVALID);
if (_cryptType == CRYPT_TYPE_INVALID || _cryptType == CRYPT_TYPE_NONE) {
throw new RuntimeException(String.format("State of SecuritySetupSlide not properly propagated, cryptType: %d", _cryptType));
}
public void setBgColor(int color) {
_bgColor = color;
_creds = new VaultFileCredentials();
}
private void showBiometricPrompt() {
@ -129,30 +129,16 @@ public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSe
}
@Override
public void onSlideSelected() {
Intent intent = getActivity().getIntent();
_cryptType = intent.getIntExtra("cryptType", SecurityPickerSlide.CRYPT_TYPE_INVALID);
if (_cryptType != SecurityPickerSlide.CRYPT_TYPE_NONE) {
_creds = new VaultFileCredentials();
}
}
@Override
public void onSlideDeselected() {
}
@Override
public boolean isPolicyRespected() {
public boolean isFinished() {
switch (_cryptType) {
case SecurityPickerSlide.CRYPT_TYPE_NONE:
case CRYPT_TYPE_NONE:
return true;
case SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC:
case CRYPT_TYPE_BIOMETRIC:
if (!_creds.getSlots().has(BiometricSlot.class)) {
return false;
}
// intentional fallthrough
case SecurityPickerSlide.CRYPT_TYPE_PASS:
case CRYPT_TYPE_PASS:
if (EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
return _creds.getSlots().has(PasswordSlot.class);
}
@ -164,16 +150,9 @@ public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSe
}
@Override
public void onUserIllegallyRequestedNextPage() {
String message;
public void onNotFinishedError() {
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
message = getString(R.string.password_equality_error);
View view = getView();
if (view != null) {
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
snackbar.show();
}
Toast.makeText(getContext(), R.string.password_equality_error, Toast.LENGTH_SHORT).show();
} else if (_cryptType != SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC) {
deriveKey();
} else if (!_creds.getSlots().has(BiometricSlot.class)) {
@ -181,6 +160,11 @@ public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSe
}
}
@Override
public void onSaveIntroState(@NonNull Bundle introState) {
introState.putSerializable("creds", _creds);
}
private class PasswordDerivationListener implements KeyDerivationTask.Callback {
@Override
public void onTaskFinished(PasswordSlot slot, SecretKey key) {
@ -194,7 +178,7 @@ public class SecuritySetupSlide extends Fragment implements SlidePolicy, SlideSe
return;
}
((IntroActivity) getActivity()).goToNextSlide();
goToNextSlide();
}
}

View file

@ -0,0 +1,16 @@
package com.beemdevelopment.aegis.ui.slides;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
public class WelcomeSlide extends SlideFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_welcome_slide, container, false);
}
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#59000000" />
<size
android:width="50dp"
android:height="50dp" />
</shape>

View file

@ -0,0 +1,9 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/circular_button_background" />
<item
android:drawable="@drawable/ic_check_black_24dp"
android:bottom="16dp"
android:top="16dp"
android:left="16dp"
android:right="16dp" />
</layer-list>

View file

@ -0,0 +1,9 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/circular_button_background" />
<item
android:drawable="@drawable/ic_arrow_right_black_24dp"
android:bottom="16dp"
android:top="16dp"
android:left="16dp"
android:right="16dp" />
</layer-list>

View file

@ -0,0 +1,9 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/circular_button_background" />
<item
android:drawable="@drawable/ic_arrow_left_black_24dp"
android:bottom="16dp"
android:top="16dp"
android:left="16dp"
android:right="16dp" />
</layer-list>

View file

@ -0,0 +1,9 @@
<!-- drawable/ic_arrow_left_black_24dp.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:autoMirrored="true">
<path android:fillColor="#000" android:pathData="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" />
</vector>

View file

@ -0,0 +1,9 @@
<!-- drawable/ic_arrow_right_black_24dp.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:autoMirrored="true">
<path android:fillColor="#000" android:pathData="M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z" />
</vector>

View file

@ -1,10 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<androidx.constraintlayout.widget.ConstraintLayout
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"
android:fitsSystemWindows="true"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.ui.IntroActivity">
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/btnPrevious" />
<ImageButton
android:id="@+id/btnPrevious"
android:layout_width="65dp"
android:layout_height="65dp"
android:layout_margin="10dp"
android:src="@drawable/circular_button_prev"
android:tint="?attr/iconColorPrimary"
android:background="?selectableItemBackgroundBorderless"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.beemdevelopment.aegis.ui.intro.SlideIndicator
android:id="@+id/slideIndicator"
android:layout_width="0dp"
android:layout_height="65dp"
android:layout_marginBottom="10dp"
app:layout_constraintStart_toEndOf="@+id/btnPrevious"
app:layout_constraintEnd_toStartOf="@+id/btnNext"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageButton
android:id="@+id/btnNext"
android:layout_width="65dp"
android:layout_height="65dp"
android:layout_margin="10dp"
android:src="@drawable/circular_button_next"
android:tint="?attr/iconColorPrimary"
android:background="?selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="32dp">
<TextView
android:id="@+id/titleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/setup_completed"
android:textAlignment="center"
android:textSize="24sp"
android:layout_marginTop="30dp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:src="@drawable/app_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleText"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/setup_completed_description"
android:textAlignment="center"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,91 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:orientation="vertical"
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="32dp">
<TextView
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/choose_authentication_method"
android:textAlignment="center"
android:textColor="@color/primary_text_inverted"
android:textSize="24sp"
android:layout_marginTop="30dp"
android:textStyle="bold" />
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/main"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="24dp"
android:orientation="vertical">
android:layout_height="wrap_content"
android:padding="32dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/authentication_method_explanation"/>
android:text="@string/choose_authentication_method"
android:textAlignment="center"
android:textSize="24sp"
android:layout_marginTop="30dp"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rg_authenticationMethod"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp">
<RadioButton
android:id="@+id/rb_none"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/authentication_method_none"
android:textSize="16sp" />
android:layout_height="match_parent"
android:layout_marginTop="24dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="-5dp"
android:text="@string/authentication_method_none_description"
android:textColor="@color/secondary_text_inverted" />
android:text="@string/authentication_method_explanation"/>
<RadioButton
android:id="@+id/rb_password"
<RadioGroup
android:id="@+id/rg_authenticationMethod"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/authentication_method_password"
android:textSize="16sp" />
android:layout_marginTop="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="-5dp"
android:text="@string/authentication_method_password_description"
android:textColor="@color/secondary_text_inverted" />
<RadioButton
android:id="@+id/rb_none"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/authentication_method_none"
android:textSize="16sp" />
<RadioButton
android:id="@+id/rb_biometrics"
android:enabled="false"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/authentication_method_biometrics"
android:textSize="16sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="-5dp"
android:text="@string/authentication_method_none_description" />
<TextView
android:id="@+id/text_rb_biometrics"
android:enabled="false"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="-5dp"
<RadioButton
android:id="@+id/rb_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/authentication_method_password"
android:textSize="16sp" />
android:text="@string/authentication_method_biometrics_description"
android:textColor="@color/disabled_textview_colors" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="-5dp"
android:text="@string/authentication_method_password_description" />
</RadioGroup>
<RadioButton
android:id="@+id/rb_biometrics"
android:enabled="false"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/authentication_method_biometrics"
android:textSize="16sp" />
<TextView
android:id="@+id/text_rb_biometrics"
android:enabled="false"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="-5dp"
android:text="@string/authentication_method_biometrics_description" />
</RadioGroup>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -1,82 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:orientation="vertical"
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="32dp">
<TextView
android:text="@string/authentication_method_set_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textColor="@color/primary_text_inverted"
android:id="@+id/textView2" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/authentication_method_password_explanation"
android:textColor="#FFFF00"
android:textStyle="bold"
android:layout_marginTop="24dp" />
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/main"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp">
android:padding="32dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_password_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/text_password"
android:hint="@string/set_password"
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_password_confirm_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<EditText
android:hint="@string/set_password_confirm"
android:id="@+id/text_password_confirm"
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:max="4"
android:paddingStart="4dp"
android:paddingEnd="3.5dp" />
<TextView
android:id="@+id/text_password_strength"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end" />
<CheckBox
android:id="@+id/check_toggle_visibility"
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@string/show_password" />
android:text="@string/choose_authentication_method"
android:textAlignment="center"
android:textSize="24sp"
android:layout_marginTop="30dp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/authentication_method_password_explanation"
android:textColor="@color/warning_color"
android:textStyle="bold"
android:layout_marginTop="24dp" />
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_password_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/text_password"
android:hint="@string/set_password"
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_password_confirm_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<EditText
android:hint="@string/set_password_confirm"
android:id="@+id/text_password_confirm"
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:max="4"
android:paddingStart="4dp"
android:paddingEnd="3.5dp" />
<TextView
android:id="@+id/text_password_strength"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end" />
<CheckBox
android:id="@+id/check_toggle_visibility"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@string/show_password" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="32dp">
<TextView
android:id="@+id/titleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/welcome"
android:textAlignment="center"
android:textSize="24sp"
android:layout_marginTop="30dp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:src="@drawable/app_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleText"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_description"
android:textAlignment="center"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -9,4 +9,11 @@
<attr name="codePrimaryText" format="color" />
<attr name="iconColorPrimary" format="color" />
<attr name="iconColorInverted" format="color" />
<declare-styleable name="SlideIndicator">
<attr name="dot_radius" format="dimension" />
<attr name="dot_separation" format="dimension" />
<attr name="dot_color" format="color" />
<attr name="dot_color_selected" format="color" />
</declare-styleable>
</resources>

View file

@ -26,6 +26,11 @@
<color name="icon_primary_dark">#ffffff</color>
<color name="icon_primary_dark_inverted">#212121</color>
<color name="indicator_dot">#aaaaaa</color>
<color name="indicator_dot_selected">#4c4c4c</color>
<color name="indicator_dot_dark">#656565</color>
<color name="indicator_dot_selected_dark">#ffffff</color>
<color name="code_primary_text">#1058C9</color>
<color name="code_primary_text_dark">#ffffff</color>

View file

@ -10,11 +10,6 @@
<item name="android:actionModeBackground">@color/colorPrimary</item>
</style>
<style name="Theme.Intro" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
@ -36,6 +31,9 @@
<item name="actionModeStyle">@style/ActionModeStyle</item>
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
<item name="alertDialogTheme">@style/DialogStyle</item>
<item name="dot_color">@color/indicator_dot</item>
<item name="dot_color_selected">@color/indicator_dot_selected</item>
</style>
<style name="AppTheme" parent="AppThemeBase">
@ -59,6 +57,7 @@
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="android:windowContentOverlay">@null</item>
>>>>>>> Replace AppIntro with a new custom intro
<item name="primaryText">@color/primary_text_dark</item>
<item name="background">@color/background_dark</item>
<item name="authText">@color/primary_text_inverted</item>
@ -147,6 +146,9 @@
<item name="android:navigationBarColor" tools:targetApi="lollipop">@color/background_dark</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
<item name="dot_color">@color/indicator_dot_dark</item>
<item name="dot_color_selected">@color/indicator_dot_selected_dark</item>
</style>
<style name="AppTheme.TrueBlack" parent="AppTheme.Dark">