mirror of https://github.com/beemdevelopment/Aegis
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">pull/536/head
parent
9d44d6abb2
commit
0e78fd9652
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
||||
</LinearLayout>
|
||||
<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"
|
||||
<TextView
|
||||
android:id="@+id/textView2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/choose_authentication_method"
|
||||
android:textAlignment="center"
|
||||
android:textSize="24sp"
|
||||
android:layout_marginTop="30dp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<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>
|
||||
<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" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/text_password_confirm_wrapper"
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp">
|
||||
android:layout_marginTop="24dp">
|
||||
|
||||
<EditText
|
||||
android:hint="@string/set_password_confirm"
|
||||
android:id="@+id/text_password_confirm"
|
||||
android:inputType="textPassword"
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/text_password_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<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" />
|
||||
<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>
|
||||
|
||||
<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" />
|
||||
<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>
|
||||
|
@ -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>
|
Loading…
Reference in New Issue