mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-20 05:49:13 +00:00
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:
parent
9d44d6abb2
commit
0e78fd9652
29 changed files with 1231 additions and 521 deletions
|
@ -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}"
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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" />
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
8
app/src/main/res/drawable/circular_button_background.xml
Normal file
8
app/src/main/res/drawable/circular_button_background.xml
Normal 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>
|
9
app/src/main/res/drawable/circular_button_done.xml
Normal file
9
app/src/main/res/drawable/circular_button_done.xml
Normal 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>
|
9
app/src/main/res/drawable/circular_button_next.xml
Normal file
9
app/src/main/res/drawable/circular_button_next.xml
Normal 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>
|
9
app/src/main/res/drawable/circular_button_prev.xml
Normal file
9
app/src/main/res/drawable/circular_button_prev.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_arrow_left_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_arrow_left_black_24dp.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_arrow_right_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_arrow_right_black_24dp.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
39
app/src/main/res/layout/fragment_done_slide.xml
Normal file
39
app/src/main/res/layout/fragment_done_slide.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
39
app/src/main/res/layout/fragment_welcome_slide.xml
Normal file
39
app/src/main/res/layout/fragment_welcome_slide.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Add table
Reference in a new issue