diff --git a/app/build.gradle b/app/build.gradle index 289a8f3f90..b6a9d6f938 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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}" diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/AegisTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/AegisTest.java index db3a49e8c2..520b1cf469 100644 --- a/app/src/androidTest/java/com/beemdevelopment/aegis/AegisTest.java +++ b/app/src/androidTest/java/com/beemdevelopment/aegis/AegisTest.java @@ -2,224 +2,25 @@ 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 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 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 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 VaultEntry generateEntry(Class 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(); +public abstract class AegisTest { + protected AegisApplication getApp() { + return (AegisApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); } - 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 getConstraints() { diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/IntroTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/IntroTest.java new file mode 100644 index 0000000000..6e74b99963 --- /dev/null +++ b/app/src/androidTest/java/com/beemdevelopment/aegis/IntroTest.java @@ -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 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)); + } +} diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java new file mode 100644 index 0000000000..88a17e38d4 --- /dev/null +++ b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java @@ -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 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 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 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 VaultEntry generateEntry(Class 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); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 583a3535b6..a93309fac6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,7 +57,8 @@ android:label="@string/title_activity_edit_entry" /> + android:theme="@style/AppTheme.NoActionBar" + android:screenOrientation="portrait" /> diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/AegisActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/AegisActivity.java index 0a3e036402..898ece1ddf 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/AegisActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/AegisActivity.java @@ -151,6 +151,6 @@ protected boolean isOpen() { * 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(); } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/IntroActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/IntroActivity.java index 73c2af1549..212b188cbf 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/IntroActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/IntroActivity.java @@ -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); - } - } + protected void onSetTheme() { + setTheme(ThemeMap.NO_ACTION_BAR); + } - if (newFragment == _endSlide) { - setWizardMode(false); + @Override + protected boolean onBeforeSlideChanged(Class oldSlide, Class newSlide) { + if (oldSlide == SecurityPickerSlide.class + && newSlide == SecuritySetupSlide.class + && getState().getInt("cryptType", CRYPT_TYPE_INVALID) == CRYPT_TYPE_NONE) { + skipToSlide(DoneSlide.class); + return true; } - setSwipeLock(true); + return false; } @Override - public void onDonePressed(Fragment currentFragment) { - super.onDonePressed(currentFragment); - - int cryptType = securitySetupSlide.getCryptType(); - VaultFileCredentials creds = securitySetupSlide.getCredentials(); + 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 void onDonePressed(Fragment currentFragment) { 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); - } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/intro/IntroActivityInterface.java b/app/src/main/java/com/beemdevelopment/aegis/ui/intro/IntroActivityInterface.java new file mode 100644 index 0000000000..32c34daa40 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/intro/IntroActivityInterface.java @@ -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 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(); +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/intro/IntroBaseActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/intro/IntroBaseActivity.java new file mode 100644 index 0000000000..aecf53a59b --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/intro/IntroBaseActivity.java @@ -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> _slides; + private WeakReference _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 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 oldSlide, Class 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 oldSlide, Class newSlide) { + + } + + private void setPagerPosition(int pos) { + Class oldSlide = _currentSlide.get().getClass(); + Class 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 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 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); + } + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/intro/SlideFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/intro/SlideFragment.java new file mode 100644 index 0000000000..f3f0a0e057 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/intro/SlideFragment.java @@ -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 _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 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(); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/intro/SlideIndicator.java b/app/src/main/java/com/beemdevelopment/aegis/ui/intro/SlideIndicator.java new file mode 100644 index 0000000000..f27ea56417 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/intro/SlideIndicator.java @@ -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; + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/slides/DoneSlide.java b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/DoneSlide.java new file mode 100644 index 0000000000..d2c8946581 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/DoneSlide.java @@ -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); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/slides/SecurityPickerSlide.java b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/SecurityPickerSlide.java index 436dd62c2d..b89dcb5659 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/slides/SecurityPickerSlide.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/SecurityPickerSlide.java @@ -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.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); } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/slides/SecuritySetupSlide.java b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/SecuritySetupSlide.java index 02dad8a0e0..f2c1b47344 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/slides/SecuritySetupSlide.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/SecuritySetupSlide.java @@ -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.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.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 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 View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa }); _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 void afterTextChanged(Editable s) { } }); - 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 @@ private void deriveKey() { } @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 boolean isPolicyRespected() { } @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 void onUserIllegallyRequestedNextPage() { } } + @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 void onTaskFinished(PasswordSlot slot, SecretKey key) { return; } - ((IntroActivity) getActivity()).goToNextSlide(); + goToNextSlide(); } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/slides/WelcomeSlide.java b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/WelcomeSlide.java new file mode 100644 index 0000000000..ada16d856b --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/slides/WelcomeSlide.java @@ -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); + } +} diff --git a/app/src/main/res/drawable/circular_button_background.xml b/app/src/main/res/drawable/circular_button_background.xml new file mode 100644 index 0000000000..dd746464d0 --- /dev/null +++ b/app/src/main/res/drawable/circular_button_background.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/drawable/circular_button_done.xml b/app/src/main/res/drawable/circular_button_done.xml new file mode 100644 index 0000000000..9b2563eb5c --- /dev/null +++ b/app/src/main/res/drawable/circular_button_done.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/circular_button_next.xml b/app/src/main/res/drawable/circular_button_next.xml new file mode 100644 index 0000000000..36065dc76f --- /dev/null +++ b/app/src/main/res/drawable/circular_button_next.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/circular_button_prev.xml b/app/src/main/res/drawable/circular_button_prev.xml new file mode 100644 index 0000000000..4fc7029319 --- /dev/null +++ b/app/src/main/res/drawable/circular_button_prev.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/ic_arrow_left_black_24dp.xml b/app/src/main/res/drawable/ic_arrow_left_black_24dp.xml new file mode 100644 index 0000000000..aff49cb588 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_left_black_24dp.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_right_black_24dp.xml b/app/src/main/res/drawable/ic_arrow_right_black_24dp.xml new file mode 100644 index 0000000000..461a6018cd --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right_black_24dp.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_intro.xml b/app/src/main/res/layout/activity_intro.xml index 6839c163f4..e7e75a1b4d 100644 --- a/app/src/main/res/layout/activity_intro.xml +++ b/app/src/main/res/layout/activity_intro.xml @@ -1,10 +1,46 @@ - - - + + + + + diff --git a/app/src/main/res/layout/fragment_done_slide.xml b/app/src/main/res/layout/fragment_done_slide.xml new file mode 100644 index 0000000000..e31e808f87 --- /dev/null +++ b/app/src/main/res/layout/fragment_done_slide.xml @@ -0,0 +1,39 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_security_picker_slide.xml b/app/src/main/res/layout/fragment_security_picker_slide.xml index 8c64922bfd..e002c59880 100644 --- a/app/src/main/res/layout/fragment_security_picker_slide.xml +++ b/app/src/main/res/layout/fragment_security_picker_slide.xml @@ -1,91 +1,92 @@ - - - + android:layout_height="match_parent"> + android:layout_height="wrap_content" + android:padding="32dp"> + android:text="@string/choose_authentication_method" + android:textAlignment="center" + android:textSize="24sp" + android:layout_marginTop="30dp" + android:textStyle="bold" /> - - - + android:layout_height="match_parent" + android:layout_marginTop="24dp" + android:orientation="vertical"> + android:text="@string/authentication_method_explanation"/> - + android:layout_marginTop="24dp"> - + - + - - android:text="@string/authentication_method_biometrics_description" - android:textColor="@color/disabled_textview_colors" /> + - + - + + + - \ No newline at end of file + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_security_setup_slide.xml b/app/src/main/res/layout/fragment_security_setup_slide.xml index f229d06365..a9ea224c6c 100644 --- a/app/src/main/res/layout/fragment_security_setup_slide.xml +++ b/app/src/main/res/layout/fragment_security_setup_slide.xml @@ -1,82 +1,91 @@ - - - - - + android:layout_height="match_parent"> + android:padding="32dp"> - + android:layout_height="wrap_content" + android:text="@string/choose_authentication_method" + android:textAlignment="center" + android:textSize="24sp" + android:layout_marginTop="30dp" + android:textStyle="bold" /> - - + - + android:layout_marginTop="24dp"> - - + android:layout_height="wrap_content"> - - + + - + + + + + + + + + + + - + diff --git a/app/src/main/res/layout/fragment_welcome_slide.xml b/app/src/main/res/layout/fragment_welcome_slide.xml new file mode 100644 index 0000000000..43c99dabf7 --- /dev/null +++ b/app/src/main/res/layout/fragment_welcome_slide.xml @@ -0,0 +1,39 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 77341174f1..aa96dea106 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -9,4 +9,11 @@ + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 0ac0043fdc..bc0e618c33 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -26,6 +26,11 @@ #ffffff #212121 + #aaaaaa + #4c4c4c + #656565 + #ffffff + #1058C9 #ffffff diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index effba3cd2e..7a58d00938 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -10,11 +10,6 @@ @color/colorPrimary - -