Replace AppIntro with a new custom intro

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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