├── .gitignore
├── .idea
└── codeStyleSettings.xml
├── Changelog.md
├── LICENSE
├── README.md
├── build.gradle
├── demo
├── .gitignore
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── venmo
│ │ └── android
│ │ └── pin
│ │ └── pindemo
│ │ └── demo
│ │ ├── PinDemoActivity.java
│ │ ├── PinLauncher.java
│ │ └── PinSupportDemoActivity.java
│ └── res
│ ├── drawable-xhdpi
│ └── ic_launcher.png
│ ├── drawable-xxhdpi
│ └── ic_launcher.png
│ ├── layout
│ ├── activity_pin_demo.xml
│ └── activity_pin_launcher.xml
│ ├── menu
│ └── pin_demo.xml
│ └── values
│ ├── strings.xml
│ └── styles.xml
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── .gitignore
├── build.gradle
└── src
│ ├── androidTest
│ ├── java
│ │ └── com
│ │ │ └── venmo
│ │ │ └── android
│ │ │ └── pin
│ │ │ ├── PinFragmentTests.java
│ │ │ ├── PinHelperTest.java
│ │ │ ├── TestActivity.java
│ │ │ └── TestSupportActivity.java
│ └── res
│ │ └── layout
│ │ └── layout_pin_test.xml
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── venmo
│ │ └── android
│ │ └── pin
│ │ ├── AsyncSaver.java
│ │ ├── AsyncValidator.java
│ │ ├── BaseViewController.java
│ │ ├── ConfirmPinViewController.java
│ │ ├── CreatePinViewController.java
│ │ ├── DefaultSaver.java
│ │ ├── DefaultValidator.java
│ │ ├── PinDisplayType.java
│ │ ├── PinFragment.java
│ │ ├── PinFragmentConfiguration.java
│ │ ├── PinFragmentImplement.java
│ │ ├── PinListener.java
│ │ ├── PinSaver.java
│ │ ├── PinSupportFragment.java
│ │ ├── TryDepletionListener.java
│ │ ├── Validator.java
│ │ ├── VerifyPinViewController.java
│ │ ├── util
│ │ ├── AppLifeCycleListener.java
│ │ ├── PinHelper.java
│ │ └── VibrationHelper.java
│ │ └── view
│ │ ├── PinKeyboardView.java
│ │ └── PinputView.java
│ └── res
│ ├── drawable-xhdpi
│ └── key_back.png
│ ├── drawable
│ └── pin_key_selector.xml
│ ├── layout
│ └── layout_pin_view.xml
│ ├── values-v16
│ └── styles.xml
│ ├── values
│ ├── attrs.xml
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
│ └── xml
│ └── keyboard_number_pad.xml
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 |
15 | # Gradle files
16 | .gradle/
17 | build/
18 |
19 | # Local configuration file (sdk path, etc)
20 | local.properties
21 |
22 | # Proguard folder generated by Eclipse
23 | proguard/
24 |
25 | # Log Files
26 | *.log
27 |
28 | # IntelliJ files
29 | .idea/*
30 | *.iml
31 | !.idea/codeStyleSettings.xml
32 |
--------------------------------------------------------------------------------
/.idea/codeStyleSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 | * Works in accordance to how Activities interact. This assumption should not change in future 17 | * iterations of the Android framework, but documented here just in case [link]. 19 | * For two activities A and B, we expect the following lifecycle methods in order when transitions 20 | * to B: 21 | *
29 | * Thus after this series of executions, that is after {@code A.onStop();}, the number of started 30 | * Activities from our app is equal to the number of stopped activities iff we're moving to another 31 | * process's Activity (B). 32 | *
33 | * Note that if the system can no longer accommodate an Activity's memory usage, the system will 34 | * destroy the entire process and not just an arbitrary Activity. In this situation, our counts 35 | * will be reset and work as normal. 36 | *
37 | * Only for use on API 14+
38 | */
39 | @TargetApi(VERSION_CODES.ICE_CREAM_SANDWICH)
40 | public abstract class AppLifeCycleListener implements ActivityLifecycleCallbacks {
41 | private boolean hasStarted = false;
42 | private int startedCount = 0;
43 |
44 | @Override
45 | public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
46 |
47 | @Override
48 | public void onActivityStarted(Activity activity) {
49 | startedCount++;
50 | if (!hasStarted) {
51 | hasStarted = true;
52 | onAppForegrounded(activity);
53 | }
54 | }
55 |
56 | @Override
57 | public void onActivityResumed(Activity activity) {}
58 |
59 | @Override
60 | public void onActivityPaused(Activity activity) {}
61 |
62 | @Override
63 | public void onActivityStopped(Activity activity) {
64 | startedCount--;
65 | if (startedCount == 0) {
66 | hasStarted = false;
67 | onAppBackgrounded(activity);
68 | }
69 | }
70 |
71 | @Override
72 | public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
73 |
74 | @Override
75 | public void onActivityDestroyed(Activity activity) {}
76 |
77 | protected abstract void onAppForegrounded(Activity openedActivity);
78 | protected abstract void onAppBackgrounded(Activity closedActivity);
79 | }
80 |
--------------------------------------------------------------------------------
/library/src/main/java/com/venmo/android/pin/util/PinHelper.java:
--------------------------------------------------------------------------------
1 | package com.venmo.android.pin.util;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.util.Base64;
6 |
7 | import java.security.NoSuchAlgorithmException;
8 | import java.security.SecureRandom;
9 | import java.security.spec.InvalidKeySpecException;
10 | import java.util.Arrays;
11 |
12 | import javax.crypto.SecretKeyFactory;
13 | import javax.crypto.spec.PBEKeySpec;
14 |
15 | import static android.preference.PreferenceManager.getDefaultSharedPreferences;
16 |
17 | public class PinHelper {
18 | private static final String KEY_PINPUT_PIN_HASH = "com.venmo.pin.pinputview_pin";
19 | private static final String KEY_PR_SALT = "com.venmo.pin.pr_salt";
20 |
21 | // default pin encryption settings
22 | private static final SecureRandom RANDOM = new SecureRandom();
23 | private static final int ROUNDS = 100;
24 | private static final int KEY_LEN = 256;
25 | private static final String KEY_ALGORITHM = "PBKDF2WithHmacSHA1";
26 |
27 | private static byte[] generateSalt() {
28 | byte[] salt = new byte[24];
29 | RANDOM.nextBytes(salt);
30 | return salt;
31 | }
32 |
33 | private static byte[] hash(char[] pin, byte[] salt)
34 | throws NoSuchAlgorithmException, InvalidKeySpecException {
35 | PBEKeySpec spec = new PBEKeySpec(pin, salt, ROUNDS, KEY_LEN);
36 | Arrays.fill(pin, Character.MIN_VALUE);
37 | try {
38 | SecretKeyFactory skf = SecretKeyFactory.getInstance(KEY_ALGORITHM);
39 | return skf.generateSecret(spec).getEncoded();
40 | } finally {
41 | spec.clearPassword();
42 | }
43 | }
44 |
45 | private static boolean validate(char[] actual, byte[] expected, byte[] salt)
46 | throws NoSuchAlgorithmException, InvalidKeySpecException {
47 | byte[] pwdHash = hash(actual, salt);
48 | Arrays.fill(actual, Character.MIN_VALUE);
49 | if (pwdHash.length != expected.length) return false;
50 | for (int i = 0; i < pwdHash.length; i++) {
51 | if (pwdHash[i] != expected[i]) return false;
52 | }
53 | return true;
54 | }
55 |
56 | public static boolean hasDefaultPinSaved(Context c) {
57 | return getDefaultSharedPreferences(c).getString(KEY_PINPUT_PIN_HASH, null) != null;
58 | }
59 |
60 | public static void resetDefaultSavedPin(Context c) {
61 | getDefaultSharedPreferences(c).edit()
62 | .clear()
63 | .commit();
64 | }
65 |
66 | public static boolean doesMatchDefaultPin(Context c, String pin) {
67 | try {
68 | SharedPreferences def = getDefaultSharedPreferences(c);
69 | return validate(pin.toCharArray(),
70 | getPinHashFromPreferences(def),
71 | getSaltFromPreferences(def));
72 | } catch (NoSuchAlgorithmException e) {
73 | throw new RuntimeException("error validating pin", e);
74 | } catch (InvalidKeySpecException e) {
75 | throw new RuntimeException("error validating pin", e);
76 | }
77 | }
78 |
79 | public static void saveDefaultPin(Context context, String pin) {
80 | try {
81 | final byte[] salt = generateSalt();
82 | final byte[] hash = hash(pin.toCharArray(), salt);
83 |
84 | // save salt & pin after successful hashing
85 | saveToPreferences(getDefaultSharedPreferences(context), salt, hash);
86 | } catch (NoSuchAlgorithmException e) {
87 | throw new RuntimeException("error saving pin: ", e);
88 | } catch (InvalidKeySpecException e) {
89 | throw new RuntimeException("error saving pin: ", e);
90 | }
91 | }
92 |
93 | private static void saveToPreferences(SharedPreferences prefs, byte[] salt, byte[] hash) {
94 | prefs.edit()
95 | .putString(KEY_PR_SALT, encode(salt))
96 | .putString(KEY_PINPUT_PIN_HASH, encode(hash))
97 | .commit();
98 | }
99 |
100 | private static byte[] getSaltFromPreferences(SharedPreferences prefs) {
101 | return decode(getStringFromPrefsOrThow(prefs, KEY_PR_SALT));
102 | }
103 |
104 | private static byte[] getPinHashFromPreferences(SharedPreferences prefs) {
105 | return decode(getStringFromPrefsOrThow(prefs, KEY_PINPUT_PIN_HASH));
106 | }
107 |
108 | private static String getStringFromPrefsOrThow(SharedPreferences prefs, String key) {
109 | String val = prefs.getString(key, null);
110 | if (val == null) {
111 | throw new NullPointerException("Trying to retrieve pin value before it's been set");
112 | }
113 | return val;
114 | }
115 |
116 | private static String encode(byte[] src) {
117 | return Base64.encodeToString(src, Base64.DEFAULT);
118 | }
119 |
120 | private static byte[] decode(String src) {
121 | return Base64.decode(src, Base64.DEFAULT);
122 | }
123 |
124 | }
125 |
--------------------------------------------------------------------------------
/library/src/main/java/com/venmo/android/pin/util/VibrationHelper.java:
--------------------------------------------------------------------------------
1 | package com.venmo.android.pin.util;
2 |
3 | import android.Manifest.permission;
4 | import android.content.Context;
5 | import android.content.pm.PackageManager;
6 | import android.os.Vibrator;
7 |
8 | public class VibrationHelper {
9 |
10 | public static void vibrate(Context context, int duration) {
11 | if (hasVibrationPermission(context)) {
12 | ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE))
13 | .vibrate(duration);
14 | }
15 | }
16 |
17 | private static boolean hasVibrationPermission(Context context) {
18 | int result = context.checkCallingOrSelfPermission(permission.VIBRATE);
19 | return (result == PackageManager.PERMISSION_GRANTED);
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/library/src/main/java/com/venmo/android/pin/view/PinKeyboardView.java:
--------------------------------------------------------------------------------
1 | package com.venmo.android.pin.view;
2 |
3 | import android.content.Context;
4 | import android.content.res.Resources;
5 | import android.content.res.TypedArray;
6 | import android.graphics.Canvas;
7 | import android.graphics.Color;
8 | import android.graphics.Paint;
9 | import android.graphics.PorterDuff.Mode;
10 | import android.graphics.Typeface;
11 | import android.graphics.drawable.Drawable;
12 | import android.inputmethodservice.Keyboard;
13 | import android.inputmethodservice.Keyboard.Key;
14 | import android.inputmethodservice.KeyboardView;
15 | import android.os.Build.VERSION;
16 | import android.os.Build.VERSION_CODES;
17 | import android.util.AttributeSet;
18 | import android.util.TypedValue;
19 |
20 | import com.venmo.android.pin.R;
21 |
22 | import java.util.List;
23 |
24 | public class PinKeyboardView extends KeyboardView {
25 |
26 | public static final int KEYCODE_DELETE = -5;
27 | private Drawable mKeyBackgroundDrawable;
28 | private boolean mShowUnderline;
29 | private int mUnderlinePadding;
30 | private Paint mPaint;
31 | private Paint mUnderlinePaint;
32 |
33 | @SuppressWarnings("unused")
34 | public PinKeyboardView(Context context, AttributeSet attrs) {
35 | super(context, attrs);
36 | init(attrs, 0);
37 | }
38 |
39 | @SuppressWarnings("unused")
40 | public PinKeyboardView(Context context, AttributeSet attrs, int defStyle) {
41 | super(context, attrs, defStyle);
42 | init(attrs, defStyle);
43 | }
44 |
45 | private void init(AttributeSet attrs, int defStyle) {
46 | // @formatter:off
47 | final TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.PinKeyboardView, defStyle, 0);
48 | // @formatter:on
49 | Resources res = getResources();
50 | mKeyBackgroundDrawable = a.getDrawable(R.styleable.PinKeyboardView_pinkeyboardview_keyBackground);
51 | mShowUnderline = a.getBoolean(R.styleable.PinKeyboardView_pinkeyboardview_showUnderline, false);
52 | mUnderlinePadding =
53 | a.getDimensionPixelSize(R.styleable.PinKeyboardView_pinkeyboardview_underlinePadding,
54 | res.getDimensionPixelSize(R.dimen.keyboard_underline_padding));
55 | int textSize = a.getDimensionPixelSize(R.styleable.PinKeyboardView_pinkeyboardview_textSize,
56 | res.getDimensionPixelSize(R.dimen.pin_keyboard_default_text_size));
57 | int textColor = a.getColor(R.styleable.PinKeyboardView_pinkeyboardview_textColor, Color.BLACK);
58 | int underlineColor = a.getColor(R.styleable.PinKeyboardView_pinkeyboardview_keyUnderlineColor,
59 | getResources().getColor(R.color.pin_light_gray_50));
60 | a.recycle();
61 |
62 | mPaint = new Paint();
63 | mPaint.setTextAlign(Paint.Align.CENTER);
64 | mPaint.setAntiAlias(true);
65 | if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
66 | mPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
67 | }
68 | mPaint.setTextSize(textSize);
69 | mPaint.setColor(textColor);
70 |
71 | mUnderlinePaint = new Paint();
72 |
73 | mUnderlinePaint.setColor(underlineColor);
74 | float stroke = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
75 | getResources().getDisplayMetrics());
76 | mUnderlinePaint.setStrokeWidth(stroke);
77 | setPreviewEnabled(false);
78 | setKeyboard(new Keyboard(getContext(), R.xml.keyboard_number_pad));
79 | Drawable back = getResources().getDrawable(R.drawable.key_back);
80 | back.setColorFilter(textColor, Mode.SRC_ATOP);
81 | }
82 |
83 | @Override
84 | public void onDraw(Canvas canvas) {
85 | Drawable keyBackground = mKeyBackgroundDrawable;
86 | List