Update registration flow

This commit is contained in:
Moxie Marlinspike
2017-11-08 12:20:11 -08:00
parent e56e55363d
commit 90ff0e58b0
25 changed files with 1488 additions and 947 deletions

View File

@@ -76,7 +76,7 @@ class Tweener {
interpolator = (TimeInterpolator) value; // TODO: multiple interpolators?
} else if ("onUpdate".equals(key) || "onUpdateListener".equals(key)) {
updateListener = (AnimatorUpdateListener) value;
} else if ("onComplete".equals(key) || "onCompleteListener".equals(key)) {
} else if ("onCodeComplete".equals(key) || "onCompleteListener".equals(key)) {
listener = (AnimatorListener) value;
} else if ("delay".equals(key)) {
delay = ((Number) value).longValue();

View File

@@ -0,0 +1,107 @@
package org.thoughtcrime.securesms.components.registration;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
public class CallMeCountDownView extends RelativeLayout {
private ImageView phone;
private TextView callMeText;
private TextView availableInText;
private TextView countDownText;
private int countDown;
private OnClickListener listener;
public CallMeCountDownView(Context context) {
super(context);
initialize();
}
public CallMeCountDownView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public CallMeCountDownView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CallMeCountDownView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize();
}
private void initialize() {
inflate(getContext(), R.layout.registration_call_me_view, this);
this.phone = findViewById(R.id.phone_icon);
this.callMeText = findViewById(R.id.call_me_text);
this.availableInText = findViewById(R.id.available_in_text);
this.countDownText = findViewById(R.id.countdown);
}
public void setOnClickListener(@Nullable OnClickListener listener) {
this.listener = listener;
}
public void startCountDown(int countDown) {
setVisibility(View.VISIBLE);
this.phone.setColorFilter(null);
this.phone.setOnClickListener(null);
this.callMeText.setTextColor(getResources().getColor(R.color.grey_700));
this.callMeText.setOnClickListener(null);
this.availableInText.setVisibility(View.VISIBLE);
this.countDownText.setVisibility(View.VISIBLE);
this.countDown = countDown;
updateCountDown();
}
public void setCallEnabled() {
setVisibility(View.VISIBLE);
this.phone.setColorFilter(new PorterDuffColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN));
this.callMeText.setTextColor(getResources().getColor(R.color.signal_primary));
this.availableInText.setVisibility(View.GONE);
this.countDownText.setVisibility(View.GONE);
this.phone.setOnClickListener(v -> handlePhoneCallRequest());
this.callMeText.setOnClickListener(v -> handlePhoneCallRequest());
}
private void updateCountDown() {
if (countDown > 0) {
countDown--;
int minutesRemaining = countDown / 60;
int secondsRemaining = countDown - (minutesRemaining * 60);
countDownText.setText(String.format("%02d:%02d", minutesRemaining, secondsRemaining));
countDownText.postDelayed(this::updateCountDown, 1000);
} else if (countDown == 0) {
setCallEnabled();
}
}
private void handlePhoneCallRequest() {
if (listener != null) listener.onClick(this);
}
}

View File

@@ -0,0 +1,162 @@
package org.thoughtcrime.securesms.components.registration;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Build;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.OvershootInterpolator;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.ArrayList;
import java.util.List;
public class VerificationCodeView extends FrameLayout {
private final List<View> spaces = new ArrayList<>(6);
private final List<TextView> codes = new ArrayList<>(6);
private final List<View> containers = new ArrayList<>(7);
private OnCodeEnteredListener listener;
private int index = 0;
public VerificationCodeView(Context context) {
super(context);
initialize(context, null);
}
public VerificationCodeView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
public VerificationCodeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(context, attrs);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public VerificationCodeView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize(context, attrs);
}
private void initialize(@NonNull Context context, @Nullable AttributeSet attrs) {
inflate(context, R.layout.verification_code_view, this);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VerificationCodeView);
try {
TextView separator = findViewById(R.id.separator);
this.spaces.add(findViewById(R.id.space_zero));
this.spaces.add(findViewById(R.id.space_one));
this.spaces.add(findViewById(R.id.space_two));
this.spaces.add(findViewById(R.id.space_three));
this.spaces.add(findViewById(R.id.space_four));
this.spaces.add(findViewById(R.id.space_five));
this.codes.add(findViewById(R.id.code_zero));
this.codes.add(findViewById(R.id.code_one));
this.codes.add(findViewById(R.id.code_two));
this.codes.add(findViewById(R.id.code_three));
this.codes.add(findViewById(R.id.code_four));
this.codes.add(findViewById(R.id.code_five));
this.containers.add(findViewById(R.id.container_zero));
this.containers.add(findViewById(R.id.container_one));
this.containers.add(findViewById(R.id.container_two));
this.containers.add(findViewById(R.id.separator_container));
this.containers.add(findViewById(R.id.container_three));
this.containers.add(findViewById(R.id.container_four));
this.containers.add(findViewById(R.id.container_five));
Stream.of(spaces).forEach(view -> view.setBackgroundColor(typedArray.getColor(R.styleable.VerificationCodeView_vcv_inputColor, Color.BLACK)));
Stream.of(spaces).forEach(view -> view.setLayoutParams(new LinearLayout.LayoutParams(typedArray.getDimensionPixelSize(R.styleable.VerificationCodeView_vcv_inputWidth, ViewUtil.dpToPx(context, 20)),
typedArray.getDimensionPixelSize(R.styleable.VerificationCodeView_vcv_inputHeight, ViewUtil.dpToPx(context, 1)))));
Stream.of(codes).forEach(textView -> textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, typedArray.getDimension(R.styleable.VerificationCodeView_vcv_textSize, 30)));
Stream.of(codes).forEach(textView -> textView.setTextColor(typedArray.getColor(R.styleable.VerificationCodeView_vcv_textColor, Color.GRAY)));
Stream.of(containers).forEach(view -> {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)view.getLayoutParams();
params.setMargins(typedArray.getDimensionPixelSize(R.styleable.VerificationCodeView_vcv_spacing, ViewUtil.dpToPx(context, 5)),
params.topMargin, params.rightMargin, params.bottomMargin);
view.setLayoutParams(params);
});
separator.setTextSize(TypedValue.COMPLEX_UNIT_SP, typedArray.getDimension(R.styleable.VerificationCodeView_vcv_textSize, 30));
} finally {
if (typedArray != null) typedArray.recycle();
}
}
@MainThread
public void setOnCompleteListener(OnCodeEnteredListener listener) {
this.listener = listener;
}
@MainThread
public void append(int value) {
if (index >= codes.size()) return;
TextView codeView = codes.get(index++);
Animation translateIn = new TranslateAnimation(0, 0, codeView.getHeight(), 0);
translateIn.setInterpolator(new OvershootInterpolator());
translateIn.setDuration(500);
Animation fadeIn = new AlphaAnimation(0, 1);
fadeIn.setDuration(200);
AnimationSet animationSet = new AnimationSet(false);
animationSet.addAnimation(fadeIn);
animationSet.addAnimation(translateIn);
animationSet.reset();
animationSet.setStartTime(0);
codeView.setText(String.valueOf(value));
codeView.clearAnimation();
codeView.startAnimation(animationSet);
if (index == codes.size() && listener != null) {
listener.onCodeComplete(Stream.of(codes).map(TextView::getText).collect(Collectors.joining()));
}
}
@MainThread
public void delete() {
if (index <= 0) return;
codes.get(--index).setText("");
}
@MainThread
public void clear() {
if (index != 0) {
Stream.of(codes).forEach(code -> code.setText(""));
index = 0;
}
}
public interface OnCodeEnteredListener {
void onCodeComplete(@NonNull String code);
}
}

View File

@@ -0,0 +1,174 @@
package org.thoughtcrime.securesms.components.registration;
import android.content.Context;
import android.graphics.PorterDuff;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.KeyboardView;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.OvershootInterpolator;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
public class VerificationPinKeyboard extends FrameLayout {
private KeyboardView keyboardView;
private ProgressBar progressBar;
private ImageView successView;
private ImageView failureView;
private OnKeyPressListener listener;
public VerificationPinKeyboard(@NonNull Context context) {
super(context);
initialize();
}
public VerificationPinKeyboard(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize();
}
public VerificationPinKeyboard(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public VerificationPinKeyboard(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize();
}
private void initialize() {
inflate(getContext(), R.layout.verification_pin_keyboard_view, this);
this.keyboardView = findViewById(R.id.keyboard_view);
this.progressBar = findViewById(R.id.progress);
this.successView = findViewById(R.id.success);
this.failureView = findViewById(R.id.failure); ;
keyboardView.setPreviewEnabled(false);
keyboardView.setKeyboard(new Keyboard(getContext(), R.xml.pin_keyboard));
keyboardView.setOnKeyboardActionListener(new KeyboardView.OnKeyboardActionListener() {
@Override
public void onPress(int primaryCode) {
if (listener != null) listener.onKeyPress(primaryCode);
}
@Override
public void onRelease(int primaryCode) {}
@Override
public void onKey(int primaryCode, int[] keyCodes) {}
@Override
public void onText(CharSequence text) {}
@Override
public void swipeLeft() {}
@Override
public void swipeRight() {}
@Override
public void swipeDown() {}
@Override
public void swipeUp() {}
});
displayKeyboard();
}
public void setOnKeyPressListener(@Nullable OnKeyPressListener listener) {
this.listener = listener;
}
public void displayKeyboard() {
this.keyboardView.setVisibility(View.VISIBLE);
this.progressBar.setVisibility(View.GONE);
this.successView.setVisibility(View.GONE);
this.failureView.setVisibility(View.GONE);
}
public void displayProgress() {
this.keyboardView.setVisibility(View.INVISIBLE);
this.progressBar.setVisibility(View.VISIBLE);
this.successView.setVisibility(View.GONE);
this.failureView.setVisibility(View.GONE);
}
public ListenableFuture<Boolean> displaySuccess() {
SettableFuture<Boolean> result = new SettableFuture<>();
this.keyboardView.setVisibility(View.INVISIBLE);
this.progressBar.setVisibility(View.GONE);
this.failureView.setVisibility(View.GONE);
this.successView.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN);
ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setInterpolator(new OvershootInterpolator());
scaleAnimation.setDuration(800);
scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
result.set(true);
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
ViewUtil.animateIn(this.successView, scaleAnimation);
return result;
}
public ListenableFuture<Boolean> displayFailure() {
SettableFuture<Boolean> result = new SettableFuture<>();
this.keyboardView.setVisibility(View.INVISIBLE);
this.progressBar.setVisibility(View.GONE);
this.failureView.setVisibility(View.GONE);
this.failureView.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.SRC_IN);
this.failureView.setVisibility(View.VISIBLE);
TranslateAnimation shake = new TranslateAnimation(0, 30, 0, 0);
shake.setDuration(50);
shake.setRepeatCount(7);
shake.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
result.set(true);
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
this.failureView.startAnimation(shake);
return result;
}
public interface OnKeyPressListener {
void onKeyPress(int keyCode);
}
}