diff --git a/app/src/main/java/com/guardanis/applock/AppLock.java b/app/src/main/java/com/guardanis/applock/AppLock.java new file mode 100644 index 000000000..ab3dd7acb --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/AppLock.java @@ -0,0 +1,318 @@ +package com.guardanis.applock; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.hardware.fingerprint.FingerprintManagerCompat; +import androidx.core.os.CancellationSignal; + +import com.guardanis.applock.activities.UnlockActivity; +import com.guardanis.applock.services.FingerprintLockService; +import com.guardanis.applock.services.FingerprintLockService.AuthenticationDelegate; +import com.guardanis.applock.services.LockService; +import com.guardanis.applock.services.PINLockService; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +import ru.vtosters.lite.utils.AndroidUtils; + +public class AppLock { + + public static final int REQUEST_CODE_UNLOCK = 9371; + public static final int REQUEST_CODE_LOCK_CREATION = 9372; + public static final int REQUEST_CODE_FINGERPRINT_PERMISSION = 9373; + public static final int ERROR_CODE_FINGERPRINTS_MISSING_HARDWARE = 1; + public static final int ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED = 2; + public static final int ERROR_CODE_FINGERPRINTS_EMPTY = 3; + public static final int ERROR_CODE_FINGERPRINTS_NOT_LOCALLY_ENROLLED = 4; + public static final int ERROR_CODE_SDK_VERSION_MINIMUM = 5; + private static final String PREFS = "pin__preferences"; + private static final String PREF_UNLOCK_FAILURE_TIME = "pin__unlock_failure_time"; + private static final String PREF_UNLOCK_SUCCESS_TIME = "pin__unlock_success_time"; + private static AppLock instance; + protected Context context; + protected HashMap lockServices = new HashMap(); + protected int unlockAttemptsCount = 1; + + protected AppLock(Context context) { + this.context = context.getApplicationContext(); + + this.lockServices.put(PINLockService.class, new PINLockService()); + this.lockServices.put(FingerprintLockService.class, new FingerprintLockService()); + } + + public static AppLock getInstance(Context context) { + if (instance == null) + instance = new AppLock(context); + + return instance; + } + + /** + * @return true if the user has enrolled in either PIN or Fingerprint locking + */ + public static boolean isEnrolled(Context context) { + AppLock helper = getInstance(context); + + for (LockService service : helper.lockServices.values()) { + if (service.isEnrolled(context)) + return true; + } + + return false; + } + + /** + * @return true if the user is enrolled in locking and the last successful unlock happened more than the default lock duration ago + */ + public static boolean isUnlockRequired(Context context) { + int minutes = context.getResources() + .getInteger(AndroidUtils.getIdentifier("applock__activity_lock_reenable_minutes", "integer")); + + return isUnlockRequired(context, TimeUnit.MINUTES.toMillis(minutes)); + } + + /** + * @return true if the user is enrolled in locking and the last successful unlock happened more than lastSuccessValidMs ago + */ + public static boolean isUnlockRequired(Context context, long lastSuccessValidMs) { + return isEnrolled(context) && lastSuccessValidMs < System.currentTimeMillis() - getUnlockSuccessTime(context); + } + + protected static long getUnlockSuccessTime(Context context) { + return AppLock.getInstance(context) + .getPreferences() + .getLong(PREF_UNLOCK_SUCCESS_TIME, 0); + } + + public static void onActivityResumed(Activity activity) { + if (isUnlockRequired(activity)) { + boolean unlockActivityReturnAllowed = activity.getResources() + .getBoolean(AndroidUtils.getIdentifier("applock__unlock_activity_return_allowed", "bool")); + + Intent intent = new Intent(activity, UnlockActivity.class) + .putExtra(UnlockActivity.INTENT_ALLOW_UNLOCKED_EXIT, unlockActivityReturnAllowed); + + activity.startActivityForResult(intent, REQUEST_CODE_UNLOCK); + } + } + + /** + * Check if an action-based unlock is required and navigates to the UnlockActivity if true. + * + * @return true if unlock is required. + */ + public static boolean unlockIfRequired(Activity activity) { + if (isUnlockRequired(activity)) { + Intent intent = new Intent(activity, UnlockActivity.class) + .putExtra(UnlockActivity.INTENT_ALLOW_UNLOCKED_EXIT, true); + + activity.startActivityForResult(intent, REQUEST_CODE_UNLOCK); + + return true; + } else + return false; + } + + public void attemptFingerprintUnlock(final UnlockDelegate eventListener) { + if (handleFailureBlocking(eventListener)) + return; + + AuthenticationDelegate delegate = new AuthenticationDelegate() { + @Override + public void onResolutionRequired(int errorCode) { + // eventListener.onResolutionRequired(errorCode); + } + + @Override + public void onAuthenticationHelp(int code, CharSequence message) { + eventListener.onAuthenticationHelp(code, String.valueOf(message)); + } + + @Override + public void onAuthenticating(CancellationSignal cancellationSignal) { + // Handled internally + } + + @Override + public void onAuthenticationSuccess(@NonNull FingerprintManagerCompat.AuthenticationResult result) { + onUnlockSuccessful(eventListener); + } + + @Override + public void onAuthenticationFailed(String message) { + Log.w(getClass().getSimpleName(), "Fingerprint authentication failed: " + message); + } + }; + + getLockService(FingerprintLockService.class) + .authenticate(context, delegate); + } + + public void attemptPINUnlock(String pin, final UnlockDelegate eventListener) { + if (handleFailureBlocking(eventListener)) + return; + + PINLockService.AuthenticationDelegate delegate = new PINLockService.AuthenticationDelegate() { + @Override + public void onNoPIN() { + onUnlockFailed(context.getString(AndroidUtils.getIdentifier("applock__unlock_error_no_matching_pin_found", "string"))); + } + + @Override + public void onPINDoesNotMatch() { + onUnlockFailed(context.getString(AndroidUtils.getIdentifier("applock__unlock_error_match_failed", "string"))); + } + + @Override + public void onPINMatches() { + onUnlockSuccessful(eventListener); + } + + private void onUnlockFailed(String message) { + handleUnlockFailure(message, eventListener); + } + }; + + getLockService(PINLockService.class) + .authenticate(context, pin, delegate); + } + + /** + * @return true if failure blocking is enabled + */ + private boolean handleFailureBlocking(final UnlockDelegate eventListener) { + if (isUnlockFailureBlockEnabled()) { + unlockAttemptsCount++; + + if (getFailureDelayMs() < System.currentTimeMillis() - getUnlockFailureBlockStart()) + resetUnlockFailure(); + else { + String message = String.format( + context.getString(AndroidUtils.getIdentifier("applock__unlock_error_retry_limit_exceeded", "string")), + formatTimeRemaining()); + + if (eventListener != null) + eventListener.onFailureLimitExceeded(message); + + return true; + } + } + + return false; + } + + protected void handleUnlockFailure(String message, UnlockDelegate eventListener) { + this.unlockAttemptsCount++; + + if (eventListener != null) + eventListener.onFailureLimitExceeded(message); + + if (context.getResources().getInteger(AndroidUtils.getIdentifier("applock__max_retry_count", "integer")) < unlockAttemptsCount) + onFailureExceedsLimit(); + } + + public SharedPreferences getPreferences() { + return context.getSharedPreferences(PREFS, 0); + } + + protected void onFailureExceedsLimit() { + getPreferences() + .edit() + .putLong(PREF_UNLOCK_FAILURE_TIME, System.currentTimeMillis()) + .commit(); + } + + public boolean isUnlockFailureBlockEnabled() { + return context.getResources().getInteger(AndroidUtils.getIdentifier("applock__max_retry_count", "integer")) < unlockAttemptsCount + || System.currentTimeMillis() - getUnlockFailureBlockStart() < getFailureDelayMs(); + } + + protected long getUnlockFailureBlockStart() { + return getPreferences() + .getLong(PREF_UNLOCK_FAILURE_TIME, 0); + } + + protected void onUnlockSuccessful(UnlockDelegate eventListener) { + getPreferences() + .edit() + .putLong(PREF_UNLOCK_SUCCESS_TIME, System.currentTimeMillis()) + .commit(); + + resetUnlockFailure(); + + if (eventListener != null) + eventListener.onUnlockSuccessful(); + } + + /** + * Reset the last login time to require the user to re-authenticate with + * AppLock the next time they are eligible to unlock. + */ + public void setAuthenticationRequired() { + getPreferences() + .edit() + .putLong(PREF_UNLOCK_SUCCESS_TIME, 0) + .commit(); + } + + protected void resetUnlockFailure() { + unlockAttemptsCount = 1; + + getPreferences() + .edit() + .putLong(PREF_UNLOCK_FAILURE_TIME, 0) + .commit(); + } + + protected String formatTimeRemaining() { + long millis = getFailureDelayMs() - (System.currentTimeMillis() - getUnlockFailureBlockStart()); + long seconds = TimeUnit.MILLISECONDS.toSeconds(millis) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis)); + + if (TimeUnit.MILLISECONDS.toMinutes(millis) < 1) + return String.format("%d seconds", seconds); + else + return String.format("%d minutes, %d seconds", TimeUnit.MILLISECONDS.toMinutes(millis), seconds); + } + + protected long getFailureDelayMs() { + return TimeUnit.MINUTES.toMillis(context.getResources() + .getInteger(AndroidUtils.getIdentifier("applock__failure_retry_delay", "integer"))); + } + + /** + * This will remove all PIN and/or Fingerprint enrollment data. + * Users will need to re-enroll in AppLock after this call. + */ + public void invalidateEnrollments() { + resetUnlockFailure(); + setAuthenticationRequired(); + + for (LockService service : lockServices.values()) + service.invalidateEnrollments(context); + } + + public void cancelPendingAuthentications() { + for (LockService service : lockServices.values()) + service.cancelPendingAuthentications(context); + } + + public T getLockService(Class named) { + return (T) lockServices.get(named); + } + + public interface UnlockDelegate { + void onUnlockSuccessful(); + + void onResolutionRequired(int errorCode); + + void onAuthenticationHelp(int code, String message); + + void onFailureLimitExceeded(String message); + } +} diff --git a/app/src/main/java/com/guardanis/applock/activities/LockCreationActivity.java b/app/src/main/java/com/guardanis/applock/activities/LockCreationActivity.java new file mode 100644 index 000000000..d8e6a0c5c --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/activities/LockCreationActivity.java @@ -0,0 +1,38 @@ +package com.guardanis.applock.activities; + +import static ru.vtosters.lite.utils.AndroidUtils.getIdentifier; + +import android.app.Activity; +import android.os.Bundle; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +import com.guardanis.applock.views.LockCreationViewController; + +import ru.vtosters.lite.utils.ThemesUtils; + +public class LockCreationActivity extends AppCompatActivity implements LockCreationViewController.Delegate { + + protected LockCreationViewController viewController; + + @Override + public void onCreate(Bundle savedInstance) { + super.onCreate(savedInstance); + + setContentView(getIdentifier("applock__activity_lock_creation", "layout")); + ((LinearLayout) findViewById(getIdentifier("applock__activity_lock_creation", "id"))).setBackgroundColor(ThemesUtils.getBackgroundContent()); + ((TextView) findViewById(getIdentifier("pin__description", "id"))).setTextColor(ThemesUtils.getTextAttr()); + + this.viewController = new LockCreationViewController(this, findViewById(getIdentifier("pin__container", "id"))); + this.viewController.setDelegate(this); + this.viewController.setupRootFlow(); + } + + @Override + public void onLockCreated() { + setResult(Activity.RESULT_OK); + finish(); + } +} diff --git a/app/src/main/java/com/guardanis/applock/activities/LockableAppCompatActivity.java b/app/src/main/java/com/guardanis/applock/activities/LockableAppCompatActivity.java new file mode 100644 index 000000000..95b144b66 --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/activities/LockableAppCompatActivity.java @@ -0,0 +1,31 @@ +package com.guardanis.applock.activities; + +import android.app.Activity; +import android.content.Intent; + +import androidx.appcompat.app.AppCompatActivity; + +import com.guardanis.applock.AppLock; + +public class LockableAppCompatActivity extends AppCompatActivity { + @Override + protected void onPostResume() { + super.onPostResume(); + + AppLock.onActivityResumed(this); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == AppLock.REQUEST_CODE_UNLOCK && resultCode == Activity.RESULT_CANCELED) + handleUnlockActivityReturnFailure(); + } + + protected void handleUnlockActivityReturnFailure() { + setResult(Activity.RESULT_CANCELED); + finish(); + } +} + diff --git a/app/src/main/java/com/guardanis/applock/activities/UnlockActivity.java b/app/src/main/java/com/guardanis/applock/activities/UnlockActivity.java new file mode 100644 index 000000000..5372b9c0d --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/activities/UnlockActivity.java @@ -0,0 +1,71 @@ +package com.guardanis.applock.activities; + +import android.app.Activity; +import android.os.Bundle; +import android.view.KeyEvent; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.guardanis.applock.views.UnlockViewController; + +import ru.vtosters.lite.utils.AndroidUtils; +import ru.vtosters.lite.utils.ThemesUtils; + +public class UnlockActivity extends AppCompatActivity implements UnlockViewController.Delegate { + + public static final String INTENT_ALLOW_UNLOCKED_EXIT = "pin__allow_activity_exit"; // false by default + + protected UnlockViewController viewController; + + @Override + public void onCreate(Bundle savedInstance) { + super.onCreate(savedInstance); + + setContentView(AndroidUtils.getIdentifier("applock__activity_unlock", "layout")); + ((LinearLayout) findViewById(AndroidUtils.getIdentifier("applock__activity_unlock", "id"))).setBackgroundColor(ThemesUtils.getBackgroundContent()); + ((TextView) findViewById(AndroidUtils.getIdentifier("pin__description", "id"))).setTextColor(ThemesUtils.getTextAttr()); + + + this.viewController = new UnlockViewController(this, findViewById(AndroidUtils.getIdentifier("pin__container", "id"))); + this.viewController.setAutoAuthorizationEnabled(false); // Disable auto authorization so fingerprint doesn't crash onResume + this.viewController.setDelegate(this); + this.viewController.setupRootFlow(); + this.viewController.setAutoAuthorizationEnabled(true); + } + + @Override + public void onUnlockSuccessful() { + setResult(Activity.RESULT_OK); + finish(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (event.getRepeatCount() == 0) handleBackPressed(); + + return true; + } + + return super.onKeyDown(keyCode, event); + } + + @Override + public void onBackPressed() { + handleBackPressed(); + } + + protected void handleBackPressed() { + if (!getIntent().getBooleanExtra(INTENT_ALLOW_UNLOCKED_EXIT, false)) { + Toast.makeText(this, getString(AndroidUtils.getIdentifier("applock__toast_unlock_required", "string")), Toast.LENGTH_LONG).show(); + + return; + } + + setResult(Activity.RESULT_CANCELED); + finish(); + } +} diff --git a/app/src/main/java/com/guardanis/applock/dialogs/AppLockDialogBuilder.java b/app/src/main/java/com/guardanis/applock/dialogs/AppLockDialogBuilder.java new file mode 100644 index 000000000..741614749 --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/dialogs/AppLockDialogBuilder.java @@ -0,0 +1,107 @@ +package com.guardanis.applock.dialogs; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.ContextWrapper; +import android.os.Build; +import android.util.Log; +import android.view.View; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDialog; + +import com.guardanis.applock.AppLock; +import com.guardanis.applock.views.AppLockViewController; + +import java.lang.ref.WeakReference; + +public abstract class AppLockDialogBuilder { + + protected WeakReference activity; + protected ALVC viewController; + + protected WeakReference dialog = new WeakReference(null); + protected int layoutResId; + + public AppLockDialogBuilder(Activity activity, int layoutResId) { + this.activity = new WeakReference<>(activity); + this.layoutResId = layoutResId; + } + + public Dialog show() { + if (viewController != null) + throw new RuntimeException("You can only call show() once per AppLockDialogBuilder instance."); + + final Activity activity = this.activity.get(); + + if (activity == null) + return null; + + View parent = activity.getLayoutInflater() + .inflate(layoutResId, null, false); + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setView(parent); + builder.setOnCancelListener(dialogInterface -> handleCanceled()); + builder.setOnDismissListener(dialogInterface -> handleDismissed()); + + this.viewController = buildViewControllerInstance(parent); + this.viewController.setupRootFlow(); + + AppCompatDialog dialog = builder.show(); + + this.dialog = new WeakReference<>(dialog); + + return dialog; + } + + protected abstract ALVC buildViewControllerInstance(View parent); + + protected void handleCanceled() { + dismissDialog(); + } + + protected void handleDismissed() { + this.dialog = new WeakReference<>(null); + this.viewController.unregisterReceivers(); + + Activity activity = this.activity.get(); + + if (activity == null) + return; + + AppLock.getInstance(activity) + .cancelPendingAuthentications(); + } + + protected void dismissDialog() { + try { + AppCompatDialog dialog = this.dialog.get(); + + if (dialog == null) + return; + + // Attempt to prevent stupid fucking uncatchable system error "View not Attached to Window" when dialog dismissed + Context context = ((ContextWrapper) dialog.getContext()).getBaseContext(); + if (context instanceof Activity) { + if (!((Activity) context).isFinishing()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (!((Activity) context).isDestroyed()) + dialog.dismiss(); + } else + dialog.dismiss(); + } + } else + dialog.dismiss(); + } catch (Throwable e) { + Log.e("AppLockDialogBuilder", "View not Attached to Window???", e); + } + + this.dialog = new WeakReference<>(null); + } + + public ALVC getViewController() { + return viewController; + } +} diff --git a/app/src/main/java/com/guardanis/applock/dialogs/LockCreationDialogBuilder.java b/app/src/main/java/com/guardanis/applock/dialogs/LockCreationDialogBuilder.java new file mode 100644 index 000000000..beb79b412 --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/dialogs/LockCreationDialogBuilder.java @@ -0,0 +1,60 @@ +package com.guardanis.applock.dialogs; + +import android.app.Activity; +import android.view.View; + +import com.guardanis.applock.views.LockCreationViewController; + +import ru.vtosters.lite.utils.AndroidUtils; + +public class LockCreationDialogBuilder extends AppLockDialogBuilder implements LockCreationViewController.Delegate { + + protected Runnable lockCreatedCallback; + protected Runnable canceledCallback; + + public LockCreationDialogBuilder(Activity activity) { + super(activity, AndroidUtils.getIdentifier("applock__lock_creation", "layout")); + } + + /** + * Set a Runnable to be triggered when a user has successfully enrolled in locking. + */ + public LockCreationDialogBuilder onLockCreated(Runnable lockCreatedCallback) { + this.lockCreatedCallback = lockCreatedCallback; + + return this; + } + + /** + * Set a Runnable to be triggered when a user has canceled enrollment. + */ + public LockCreationDialogBuilder onCanceled(Runnable canceledCallback) { + this.canceledCallback = canceledCallback; + + return this; + } + + @Override + protected LockCreationViewController buildViewControllerInstance(View parent) { + LockCreationViewController controller = new LockCreationViewController(activity.get(), parent); + controller.setDelegate(this); + + return controller; + } + + @Override + public void onLockCreated() { + dismissDialog(); + + if (lockCreatedCallback != null) + lockCreatedCallback.run(); + } + + @Override + protected void handleCanceled() { + super.handleCanceled(); + + if (canceledCallback != null) + canceledCallback.run(); + } +} diff --git a/app/src/main/java/com/guardanis/applock/dialogs/UnlockDialogBuilder.java b/app/src/main/java/com/guardanis/applock/dialogs/UnlockDialogBuilder.java new file mode 100644 index 000000000..1b1c800f5 --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/dialogs/UnlockDialogBuilder.java @@ -0,0 +1,110 @@ +package com.guardanis.applock.dialogs; + +import android.app.Activity; +import android.app.Dialog; +import android.view.View; + +import com.guardanis.applock.AppLock; +import com.guardanis.applock.views.UnlockViewController; + +import ru.vtosters.lite.utils.AndroidUtils; + +public class UnlockDialogBuilder extends AppLockDialogBuilder implements UnlockViewController.Delegate { + + protected Runnable unlockCallback; + protected Runnable canceledCallback; + + public UnlockDialogBuilder(Activity activity) { + super(activity, AndroidUtils.getIdentifier("applock__unlock", "layout")); + } + + /** + * Set a Runnable to be triggered when a user has successfully unlocked. + */ + public UnlockDialogBuilder onUnlocked(Runnable unlockCallback) { + this.unlockCallback = unlockCallback; + + return this; + } + + /** + * Set a Runnable to be triggered when a user has canceled unlocking. + */ + public UnlockDialogBuilder onCanceled(Runnable canceledCallback) { + this.canceledCallback = canceledCallback; + + return this; + } + + @Override + protected UnlockViewController buildViewControllerInstance(View parent) { + UnlockViewController controller = new UnlockViewController(activity.get(), parent); + controller.setDelegate(this); + + return controller; + } + + @Override + public void onUnlockSuccessful() { + dismissDialog(); + triggerUnlockCallback(); + } + + @Override + protected void handleCanceled() { + super.handleCanceled(); + + if (canceledCallback != null) + canceledCallback.run(); + } + + protected void triggerUnlockCallback() { + if (unlockCallback != null) + unlockCallback.run(); + } + + /** + * Check if the user is actually enrolled in locking before opening the unlock dialog. + * If the user is enrolled, show() will be called. Otherwise, this function will trigger this + * instance's UnlockDialogBuilder.unlockCallback immediately. + * + * @return Dialog if show() is called + */ + public Dialog showIfEnrolledOrSuccess() { + Activity activity = this.activity.get(); + + if (activity == null) + return null; + + if (!AppLock.isEnrolled(activity)) { + triggerUnlockCallback(); + + return null; + } + + return show(); + } + + /** + * Check if the user is both enrolled in locking and that unlocking is required before + * opening the unlock dialog. If the user is enrolled, show() will be called. Otherwise, + * this function will trigger this instance's UnlockDialogBuilder.unlockCallback immediately. + * + * @param longValidDurationMs the milliseconds a user's last unlock is valid for + * @return Dialog if show() is called + */ + public Dialog showIfRequiredOrSuccess(long longValidDurationMs) { + Activity activity = this.activity.get(); + + if (activity == null) + return null; + + if (!AppLock.isUnlockRequired(activity, longValidDurationMs)) { + triggerUnlockCallback(); + + return null; + } + + return show(); + } +} diff --git a/app/src/main/java/com/guardanis/applock/pin/PINInputController.java b/app/src/main/java/com/guardanis/applock/pin/PINInputController.java new file mode 100644 index 000000000..f6817f81d --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/pin/PINInputController.java @@ -0,0 +1,114 @@ +package com.guardanis.applock.pin; + +import android.view.KeyEvent; +import android.view.inputmethod.EditorInfo; +import android.widget.TextView; + +import java.lang.ref.WeakReference; + +interface InputTextListener { + void onTextEntered(int count); +} + +public class PINInputController implements TextView.OnEditorActionListener, InputTextListener { + + private final WeakReference inputView; + private WeakReference eventListener = new WeakReference<>(null); + + /** + * If you want to actually receive events after this, make sure you call setInputEventListener + */ + public PINInputController(PINInputView inputView) { + this.inputView = new WeakReference<>(inputView); + + inputView.setOnEditorActionListener(this); + inputView.setInputTextListener(this); + } + + public PINInputController ensureKeyboardVisible() { + final PINInputView inputView = this.inputView.get(); + + if (inputView == null) + return this; + + inputView.postDelayed(inputView::ensureKeyboardVisible, 300); + + return this; + } + + public PINInputController setPasswordCharactersEnabled(boolean passwordCharacterEnabled) { + this.inputView.get().setPasswordCharactersEnabled(passwordCharacterEnabled); + return this; + } + + public PINInputController setInputNumbersCount(int inputNumbersCount) { + this.inputView.get().setInputViewsCount(inputNumbersCount); + return this; + } + + public PINInputController setInputEventListener(InputEventListener eventListener) { + this.eventListener = new WeakReference<>(eventListener); + return this; + } + + @Override + public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) { + PINInputView inputView = this.inputView.get(); + + if (inputView == null) + return false; + + if (isSoftKeyboardFinishedAction(textView, i, keyEvent)) { + InputEventListener eventListener = this.eventListener.get(); + + if (eventListener != null) + eventListener.onInputEntered(inputView.getText()); + + inputView.reset(); + + return true; + } + + return false; + } + + private boolean isSoftKeyboardFinishedAction(TextView view, int action, KeyEvent event) { + // Some devices return null event on editor actions for Enter Button (thanks, Samsung) + if (!(event == null || event.getAction() == KeyEvent.ACTION_DOWN)) + return false; + + // Enter clicked on Bluetooth Keyboard (action is zero on hardware keyboards) + if (action == EditorInfo.IME_ACTION_UNSPECIFIED && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) + return true; + + return action == EditorInfo.IME_ACTION_DONE + || action == EditorInfo.IME_ACTION_GO + || action == EditorInfo.IME_ACTION_SEND; + } + + public boolean matchesRequiredPINLength(String input) { + final PINInputView inputView = this.inputView.get(); + + if (inputView == null) + return false; + + return inputView.matchesRequiredPINLength(input); + } + + @Override + public void onTextEntered(int count) { + if (count == 4) { + var inputView = this.inputView.get(); + InputEventListener eventListener = this.eventListener.get(); + + if (eventListener != null) + eventListener.onInputEntered(inputView.getText()); + + inputView.reset(); + } + } + + public interface InputEventListener { + void onInputEntered(String input); + } +} diff --git a/app/src/main/java/com/guardanis/applock/pin/PINInputView.java b/app/src/main/java/com/guardanis/applock/pin/PINInputView.java new file mode 100644 index 000000000..be8330489 --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/pin/PINInputView.java @@ -0,0 +1,231 @@ +package com.guardanis.applock.pin; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.WeakHashMap; + +import ru.vtosters.lite.utils.AndroidUtils; +import ru.vtosters.lite.utils.ThemesUtils; + +public class PINInputView extends LinearLayout implements TextWatcher { + + private final WeakHashMap animators = new WeakHashMap(); + private Paint itemTextPaint; + private Paint itemBackgroundPaint; + private int inputViewsCount = 10; + private PINItemView[] pinItemViews; + private EditText editText; + private String lastText = ""; + private boolean passwordCharactersEnabled = true; + private String passwordCharacter; + private InputTextListener listener; + + + public PINInputView(Context context, AttributeSet attrs) { + super(context, attrs); + + init(attrs); + } + + private void init(AttributeSet attrs) { + setWillNotDraw(false); + setupEditText(); + + + itemTextPaint = new Paint(); + itemTextPaint.setColor(ThemesUtils.getAccentColor()); + + itemBackgroundPaint = new Paint(); + itemBackgroundPaint.setColor(ThemesUtils.getMutedAccentColor()); + + + inputViewsCount = getResources().getInteger(AndroidUtils.getIdentifier("applock__input_pin_item_count", "integer")); + + passwordCharacter = getResources().getString(AndroidUtils.getIdentifier("applock__password_char", "string")); + + } + + protected void setupEditText() { + removeAllViews(); + + this.editText = inflateFakeEditText(); + this.editText.addTextChangedListener(this); + + addView(editText); + } + + protected EditText inflateFakeEditText() { + return (EditText) LayoutInflater.from(getContext()) + .inflate(AndroidUtils.getIdentifier("applock__fake_edit_text", "layout"), this, false); + } + + public PINInputView setInputViewsCount(int inputViewsCount) { + this.inputViewsCount = inputViewsCount; + + return reset(); + } + + public PINInputView reset() { + this.lastText = ""; + this.editText.setText(""); + + if (pinItemViews != null) + animateLastOut(); + + return this; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + ensureKeyboardVisible(); + return true; + } + + return super.onTouchEvent(event); + } + + public void ensureKeyboardVisible() { + editText.requestFocus(); + + ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE)) + .showSoftInput(editText, 0); + } + + @Override + public void onDraw(Canvas canvas) { + if (pinItemViews == null || pinItemViews.length != inputViewsCount) + setupItemViews(canvas); + + String text = editText.getText().toString(); + + for (int i = 0; i < pinItemViews.length; i++) { + String textItem = i < text.length() + ? (passwordCharactersEnabled ? passwordCharacter : text.substring(i, i + 1)) + : ""; + + pinItemViews[i].draw(canvas, textItem); + } + } + + private void setupItemViews(Canvas canvas) { + pinItemViews = new PINItemView[inputViewsCount]; + + int cellWidth = canvas.getWidth() / inputViewsCount; + int largestRadius = Math.min(cellWidth / 2, canvas.getHeight() / 2); + int smallestRadius = (int) (largestRadius * Float.parseFloat(getResources().getString(AndroidUtils.getIdentifier("applock__empty_item_min_size_percent", "string")))); + int[] minMaxRadius = new int[]{smallestRadius, largestRadius}; + + itemTextPaint.setTextSize((int) (largestRadius * .85)); + + for (int i = 0; i < pinItemViews.length; i++) { + float[] positionInCanvas = getPositionInCanvas(canvas, i, cellWidth); + + pinItemViews[i] = new PINItemView(positionInCanvas, minMaxRadius, itemTextPaint, itemBackgroundPaint); + } + } + + private float[] getPositionInCanvas(Canvas canvas, int position, int cellWidth) { + return new float[]{ + (cellWidth * position) + (cellWidth / 2), + canvas.getHeight() / 2 + }; + } + + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + } + + @Override + public void afterTextChanged(Editable editable) { + if (editable == null || lastText == null) + return; + + String text = editable.toString(); + if (inputViewsCount < text.length()) { + editText.setText(lastText); + editText.setSelection(editText.getText().length()); + } else if (text.length() < lastText.length()) { + animateLastOut(); + this.lastText = text; + } else if (lastText.length() < text.length()) { + animateLastIn(); + this.lastText = text; + } + if (listener != null) { + listener.onTextEntered(text.length()); + } + } + + public void setInputTextListener(InputTextListener listener) { + this.listener = listener; + } + + private void animateLastOut() { + int startingIndex = editText.getText().toString().length(); // One after current length, only happens on backspace + + for (int i = pinItemViews.length - 1; startingIndex <= i; i--) + if (!pinItemViews[i].isAnimatedOut()) + animate(pinItemViews[i], PINItemAnimator.ItemAnimationDirection.OUT); + } + + private void animateLastIn() { + PINItemView item = pinItemViews[editText.getText().toString().length() - 1]; + animate(item, PINItemAnimator.ItemAnimationDirection.IN); + } + + private void animate(PINItemView view, PINItemAnimator.ItemAnimationDirection direction) { + cancelPreviousAnimation(view); + view.setAnimationDirection(direction); + + PINItemAnimator animator = new PINItemAnimator(this, view, direction); + animators.put(view, animator); + animator.start(); + } + + private void cancelPreviousAnimation(PINItemView view) { + PINItemAnimator animator = animators.get(view); + + try { + animator.cancel(); + animators.put(view, null); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String getText() { + return editText.getText().toString(); + } + + public void setOnEditorActionListener(TextView.OnEditorActionListener actionListener) { + editText.setOnEditorActionListener(actionListener); + } + + public void setPasswordCharactersEnabled(boolean passwordCharactersEnabled) { + this.passwordCharactersEnabled = passwordCharactersEnabled; + } + + public void setPasswordCharacter(String passwordCharacter) { + this.passwordCharacter = passwordCharacter; + } + + public boolean matchesRequiredPINLength(String input) { + return input.length() == inputViewsCount; + } +} diff --git a/app/src/main/java/com/guardanis/applock/pin/PINItemAnimator.java b/app/src/main/java/com/guardanis/applock/pin/PINItemAnimator.java new file mode 100644 index 000000000..0a97a5836 --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/pin/PINItemAnimator.java @@ -0,0 +1,92 @@ +package com.guardanis.applock.pin; + +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; + +import java.lang.ref.WeakReference; + +import ru.vtosters.lite.utils.AndroidUtils; + +public class PINItemAnimator extends Thread { + + private static final int ANIMATION_DURATION = 150; + private static final int UPDATE_RATE = 25; + private final WeakReference inputView; + private final PINItemView itemView; + private final ItemAnimationDirection animationDirection; + private final Interpolator itemInterpolator = new AccelerateDecelerateInterpolator(); + private final float minSizePercent; + private long startTime; + private boolean canceled = false; + + public PINItemAnimator(PINInputView inputView, PINItemView itemView, ItemAnimationDirection animationDirection) { + this.inputView = new WeakReference<>(inputView); + this.itemView = itemView; + this.animationDirection = animationDirection; + this.minSizePercent = Float.parseFloat(inputView.getResources().getString(AndroidUtils.getIdentifier("applock__empty_item_min_size_percent", "string"))); + } + + @Override + public void run() { + this.startTime = System.currentTimeMillis(); + + try { + if (animationDirection == ItemAnimationDirection.IN) + animateIn(); + else + animateOut(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void animateIn() throws Exception { + float percent = minSizePercent; + + while (percent < 1 && !canceled) { + percent = Math.min(minSizePercent + calculatePercentComplete(), 1); + updateView(percent); + + Thread.sleep(UPDATE_RATE); + } + } + + private void animateOut() throws Exception { + float percent = 1 - calculatePercentComplete(); + + while (minSizePercent < percent && !canceled) { + percent = Math.max(1 - calculatePercentComplete(), minSizePercent); + updateView(percent); + + Thread.sleep(UPDATE_RATE); + } + } + + protected float calculatePercentComplete() { + float completed = ((float) (System.currentTimeMillis() - startTime)) / ANIMATION_DURATION; + + return itemInterpolator.getInterpolation(completed); + } + + private void updateView(final float percent) { + final PINInputView inputView = this.inputView.get(); + + if (inputView == null) + return; + + inputView.post(() -> { + itemView.onAnimationUpdate(percent); + + inputView.invalidate(); + }); + } + + public void cancel() { + this.canceled = true; + } + + public enum ItemAnimationDirection { + IN, OUT + } + +} diff --git a/app/src/main/java/com/guardanis/applock/pin/PINItemView.java b/app/src/main/java/com/guardanis/applock/pin/PINItemView.java new file mode 100644 index 000000000..a193c3716 --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/pin/PINItemView.java @@ -0,0 +1,61 @@ +package com.guardanis.applock.pin; + +import android.graphics.Canvas; +import android.graphics.Paint; + +public class PINItemView { + + private final float[] position; + private final int intendedRadius; + private int currentRadius; + private Paint backgroundPaint; + + private float[] textPosition; + private Paint textPaint; + + private PINItemAnimator.ItemAnimationDirection animationDirection = PINItemAnimator.ItemAnimationDirection.IN; + + public PINItemView(float[] position, int[] minMaxRadius, Paint baseTextPaint, Paint baseBackgroundPaint) { + this.position = position; + this.intendedRadius = minMaxRadius[1]; + this.currentRadius = minMaxRadius[0]; + + setupPaints(baseTextPaint, baseBackgroundPaint); + } + + private void setupPaints(Paint baseTextPaint, Paint baseBackgroundPaint) { + this.textPaint = new Paint(); + this.textPaint.setTextAlign(Paint.Align.CENTER); + this.textPaint.setAntiAlias(true); + this.textPaint.setColor(baseTextPaint.getColor()); + this.textPaint.setTextSize(baseTextPaint.getTextSize()); + + this.textPosition = new float[]{ + position[0], + position[1] - ((textPaint.descent() + textPaint.ascent()) / 2) + }; + + this.backgroundPaint = new Paint(); + this.backgroundPaint.setTextAlign(Paint.Align.CENTER); + this.backgroundPaint.setAntiAlias(true); + this.backgroundPaint.setColor(baseBackgroundPaint.getColor()); + } + + public void draw(Canvas canvas, String textValue) { + canvas.drawCircle(position[0], position[1], currentRadius, backgroundPaint); + canvas.drawText(textValue, textPosition[0], textPosition[1], textPaint); + } + + public void setAnimationDirection(PINItemAnimator.ItemAnimationDirection animationDirection) { + this.animationDirection = animationDirection; + } + + public void onAnimationUpdate(float percentCompleted) { + this.currentRadius = (int) (intendedRadius * percentCompleted); + this.textPaint.setAlpha((int) (255 * percentCompleted)); + } + + public boolean isAnimatedOut() { + return animationDirection == PINItemAnimator.ItemAnimationDirection.OUT; + } +} diff --git a/app/src/main/java/com/guardanis/applock/services/FingerprintLockService.java b/app/src/main/java/com/guardanis/applock/services/FingerprintLockService.java new file mode 100644 index 000000000..c0a3655c4 --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/services/FingerprintLockService.java @@ -0,0 +1,169 @@ +package com.guardanis.applock.services; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; + +import androidx.core.content.ContextCompat; +import androidx.core.hardware.fingerprint.FingerprintManagerCompat; +import androidx.core.os.CancellationSignal; + +import com.guardanis.applock.AppLock; +import com.guardanis.applock.utils.CipherGenerator; + +import javax.crypto.Cipher; + +import ru.vtosters.lite.utils.AndroidUtils; + +public class FingerprintLockService extends LockService { + + private static final String PREF_ENROLLMENT_ALLOWED = "pin__fingerprint_enrollment_allowed"; + protected CancellationSignal fingerprintCancellationSignal; + + @Override + public boolean isEnrollmentEligible(Context context) { + return Build.VERSION_CODES.M <= Build.VERSION.SDK_INT && context.getResources().getBoolean(AndroidUtils.getIdentifier("applock__fingerprint_service_enabled", "bool")) && isHardwarePresent(context); + } + + public void enroll(Context context, AuthenticationDelegate delegate) { + authenticate(context, false, delegate); + } + + public void authenticate(Context context, AuthenticationDelegate delegate) { + authenticate(context, true, delegate); + } + + protected void authenticate(Context context, boolean localEnrollmentRequired, AuthenticationDelegate delegate) { + int errorCode = getRequiredResolutionErrorCode(context, localEnrollmentRequired); + + if (-1 < errorCode) { + delegate.onResolutionRequired(errorCode); + + return; + } + + // Should be handled by getRequiredResolutionErrorCode(Context, boolean) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return; + + attemptFingerprintManagerAuthentication(context, delegate); + } + + /** + * @return the resolvable error code or -1 if there are no issues requiring a resolution + */ + protected int getRequiredResolutionErrorCode(Context context, boolean localEnrollmentRequired) { + FingerprintManagerCompat manager = FingerprintManagerCompat.from(context); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return AppLock.ERROR_CODE_SDK_VERSION_MINIMUM; + + if (localEnrollmentRequired && !isEnrolled(context)) + return AppLock.ERROR_CODE_FINGERPRINTS_NOT_LOCALLY_ENROLLED; + + if (!isHardwarePresent(context)) return AppLock.ERROR_CODE_FINGERPRINTS_MISSING_HARDWARE; + + if (!manager.hasEnrolledFingerprints()) return AppLock.ERROR_CODE_FINGERPRINTS_EMPTY; + + if (ContextCompat.checkSelfPermission(context, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) + return AppLock.ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED; + + return -1; + } + + protected void attemptFingerprintManagerAuthentication(final Context context, final AuthenticationDelegate delegate) { + this.fingerprintCancellationSignal = new CancellationSignal(); + + + var callback = new FingerprintManagerCompat.AuthenticationCallback() { + @Override + public void onAuthenticationError(int errMsgId, CharSequence errString) { + super.onAuthenticationError(errMsgId, errString); + + delegate.onResolutionRequired(AndroidUtils.getIdentifier("applock__fingerprint_error_unknown", "string")); + } + + @Override + public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { + super.onAuthenticationHelp(helpMsgId, helpString); + + delegate.onAuthenticationHelp(helpMsgId, helpString); + } + + @Override + public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + + notifyEnrolled(context); + + delegate.onAuthenticationSuccess(result); + } + + @Override + public void onAuthenticationFailed() { + super.onAuthenticationFailed(); + } + }; + + delegate.onAuthenticating(fingerprintCancellationSignal); + + try { + Cipher cipher = generateAuthCipher(context, false, 0); + FingerprintManagerCompat.CryptoObject cryptoObject = new FingerprintManagerCompat.CryptoObject(cipher); + + FingerprintManagerCompat manager = FingerprintManagerCompat.from(context); + manager.authenticate(cryptoObject, 0, fingerprintCancellationSignal, callback, null); + } catch (Exception e) { + e.printStackTrace(); + + delegate.onResolutionRequired(AppLock.ERROR_CODE_FINGERPRINTS_MISSING_HARDWARE); + } + } + + public boolean isHardwarePresent(Context context) { + return FingerprintManagerCompat.from(context).isHardwareDetected(); + } + + @Override + public boolean isEnrolled(Context context) { + return AppLock.getInstance(context).getPreferences().getBoolean(PREF_ENROLLMENT_ALLOWED, false); + } + + protected void notifyEnrolled(Context context) { + AppLock.getInstance(context).getPreferences().edit().putBoolean(PREF_ENROLLMENT_ALLOWED, true).commit(); + } + + @SuppressLint("NewApi") + protected Cipher generateAuthCipher(Context context, boolean forceRegenerate, int attempts) throws Exception { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + throw new RuntimeException("generateAuthCipher() not supported before Api 23"); + + return new CipherGenerator().generateAuthCipher(context, forceRegenerate, attempts); + } + + @Override + public void invalidateEnrollments(Context context) { + AppLock.getInstance(context).getPreferences().edit().putBoolean(PREF_ENROLLMENT_ALLOWED, false).commit(); + } + + @Override + public void cancelPendingAuthentications(Context context) { + if (fingerprintCancellationSignal != null) { + this.fingerprintCancellationSignal.cancel(); + this.fingerprintCancellationSignal = null; + } + } + + public interface AuthenticationDelegate { + void onResolutionRequired(int errorCode); + + void onAuthenticationHelp(int code, CharSequence message); + + void onAuthenticating(CancellationSignal cancellationSignal); + + void onAuthenticationSuccess(FingerprintManagerCompat.AuthenticationResult result); + + void onAuthenticationFailed(String message); + } +} diff --git a/app/src/main/java/com/guardanis/applock/services/LockService.java b/app/src/main/java/com/guardanis/applock/services/LockService.java new file mode 100644 index 000000000..113045dbd --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/services/LockService.java @@ -0,0 +1,14 @@ +package com.guardanis.applock.services; + +import android.content.Context; + +public abstract class LockService { + + public abstract boolean isEnrolled(Context context); + + public abstract boolean isEnrollmentEligible(Context context); + + public abstract void cancelPendingAuthentications(Context context); + + public abstract void invalidateEnrollments(Context context); +} diff --git a/app/src/main/java/com/guardanis/applock/services/PINLockService.java b/app/src/main/java/com/guardanis/applock/services/PINLockService.java new file mode 100644 index 000000000..afa0ac48b --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/services/PINLockService.java @@ -0,0 +1,74 @@ +package com.guardanis.applock.services; + +import android.content.Context; + +import com.guardanis.applock.AppLock; +import com.guardanis.applock.utils.CryptoUtils; + +public class PINLockService extends LockService { + + private static final String PREF_SAVED_LOCKED_PASSWORD = "pin__saved_locked_password"; + + @Override + public boolean isEnrollmentEligible(Context context) { + // I couldn't find a good reason to allow the disabling of the PIN as an option. + return true; + } + + public void authenticate(Context context, String pin, AuthenticationDelegate eventListener) { + if (!isEnrolled(context)) { + eventListener.onNoPIN(); + return; + } + + if (!getEnrolledPIN(context).equals(CryptoUtils.encryptSha1(pin))) { + eventListener.onPINDoesNotMatch(); + return; + } + + eventListener.onPINMatches(); + } + + @Override + public boolean isEnrolled(Context context) { + return AppLock.getInstance(context) + .getPreferences() + .getString(PREF_SAVED_LOCKED_PASSWORD, null) != null; + } + + private String getEnrolledPIN(Context context) { + return AppLock.getInstance(context) + .getPreferences() + .getString(PREF_SAVED_LOCKED_PASSWORD, null); + } + + public void enroll(Context context, String pin) { + AppLock.getInstance(context) + .getPreferences() + .edit() + .putString(PREF_SAVED_LOCKED_PASSWORD, CryptoUtils.encryptSha1(pin)) + .commit(); + } + + @Override + public void invalidateEnrollments(Context context) { + AppLock.getInstance(context) + .getPreferences() + .edit() + .remove(PREF_SAVED_LOCKED_PASSWORD) + .commit(); + } + + @Override + public void cancelPendingAuthentications(Context context) { + // There are none + } + + public interface AuthenticationDelegate { + void onNoPIN(); + + void onPINDoesNotMatch(); + + void onPINMatches(); + } +} diff --git a/app/src/main/java/com/guardanis/applock/utils/CipherGenerator.java b/app/src/main/java/com/guardanis/applock/utils/CipherGenerator.java new file mode 100644 index 000000000..2cd002d2e --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/utils/CipherGenerator.java @@ -0,0 +1,69 @@ +package com.guardanis.applock.utils; + +import android.content.Context; +import android.os.Build; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.security.keystore.KeyProperties; + +import androidx.annotation.RequiresApi; + +import java.security.KeyStore; +import java.security.UnrecoverableKeyException; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; + +import ru.vtosters.lite.utils.AndroidUtils; + +@RequiresApi(api = Build.VERSION_CODES.M) +public class CipherGenerator { + + private static final String KEYSTORE_NAME = "AndroidKeyStore"; + + public Cipher generateAuthCipher(Context context, boolean forceRegenerate, int attempts) throws Exception { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + throw new RuntimeException("generateAuthCipher() not supported before Api 23"); + + String alias = context.getString(AndroidUtils.getIdentifier("applock__fingerprint_alias", "string")); + + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_NAME); + keyStore.load(null); + + if (forceRegenerate || !keyStore.containsAlias(alias)) { + KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_NAME); + keyGenerator.init(new KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setUserAuthenticationRequired(true) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + .build()); + + keyGenerator.generateKey(); + } + + String cipherFormat = String.format( + "%s/%s/%s", + KeyProperties.KEY_ALGORITHM_AES, + KeyProperties.BLOCK_MODE_CBC, + KeyProperties.ENCRYPTION_PADDING_PKCS7); + + try { + Cipher cipher = Cipher.getInstance(cipherFormat); + cipher.init(Cipher.ENCRYPT_MODE, keyStore.getKey(alias, null)); + + return cipher; + } catch (KeyPermanentlyInvalidatedException e) { + e.printStackTrace(); + + if (1 < attempts) + return generateAuthCipher(context, true, attempts + 1); + } catch (UnrecoverableKeyException e) { + e.printStackTrace(); + + if (1 < attempts) + return generateAuthCipher(context, true, attempts + 1); + } + + return null; + } +} diff --git a/app/src/main/java/com/guardanis/applock/utils/CryptoUtils.java b/app/src/main/java/com/guardanis/applock/utils/CryptoUtils.java new file mode 100644 index 000000000..3e475ca76 --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/utils/CryptoUtils.java @@ -0,0 +1,40 @@ +package com.guardanis.applock.utils; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +public class CryptoUtils { + + public static String encryptSha1(String text) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + md.update(text.getBytes(StandardCharsets.UTF_8), 0, text.length()); + + return convertToHex(md.digest()); + } catch (Exception e) { + e.printStackTrace(); + } + + return ""; + } + + private static String convertToHex(byte[] data) { + StringBuilder buf = new StringBuilder(); + + for (byte b : data) { + int half = (b >>> 4) & 0x0F; + int twoHalves = 0; + + do { + buf.append((0 <= half) && (half <= 9) + ? (char) ('0' + half) + : (char) ('a' + (half - 10))); + + half = b & 0x0F; + } + while (twoHalves++ < 1); + } + + return buf.toString(); + } +} diff --git a/app/src/main/java/com/guardanis/applock/utils/LifeCycleUtils.java b/app/src/main/java/com/guardanis/applock/utils/LifeCycleUtils.java new file mode 100644 index 000000000..356a0bf43 --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/utils/LifeCycleUtils.java @@ -0,0 +1,85 @@ +package com.guardanis.applock.utils; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + +import java.lang.ref.WeakReference; + +public class LifeCycleUtils { + + public static AppLockActivityLifeCycleCallbacks attach(Activity activity, AppLockActivityLifeCycleCallbacks.Delegate delegate) { + AppLockActivityLifeCycleCallbacks callbacks = new AppLockActivityLifeCycleCallbacks(activity, delegate); + + activity.getApplication() + .registerActivityLifecycleCallbacks(callbacks); + + return callbacks; + } + + public static class AppLockActivityLifeCycleCallbacks implements Application.ActivityLifecycleCallbacks { + + protected WeakReference openedActivity; + protected WeakReference delegate; + + public AppLockActivityLifeCycleCallbacks(Activity activity, Delegate delegate) { + this.openedActivity = new WeakReference<>(activity); + this.delegate = new WeakReference<>(delegate); + } + + @Override + public void onActivityCreated(Activity activity, Bundle bundle) { + } + + @Override + public void onActivityStarted(Activity activity) { + } + + @Override + public void onActivityStopped(Activity activity) { + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { + } + + @Override + public void onActivityResumed(Activity activity) { + Activity opened = this.openedActivity.get(); + Delegate delegate = this.delegate.get(); + + if (opened == null || delegate == null || opened != activity) + return; + + delegate.onActivityResumed(); + } + + @Override + public void onActivityPaused(Activity activity) { + Activity opened = this.openedActivity.get(); + Delegate delegate = this.delegate.get(); + + if (opened == null || delegate == null || opened != activity) + return; + + delegate.onActivityPaused(); + } + + @Override + public void onActivityDestroyed(Activity activity) { + Activity opened = this.openedActivity.get(); + + if (opened == null || opened != activity) + return; + + opened.getApplication() + .unregisterActivityLifecycleCallbacks(this); + } + + public interface Delegate { + void onActivityResumed(); + + void onActivityPaused(); + } + } +} diff --git a/app/src/main/java/com/guardanis/applock/views/AppLockViewController.java b/app/src/main/java/com/guardanis/applock/views/AppLockViewController.java new file mode 100644 index 000000000..8a39be9da --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/views/AppLockViewController.java @@ -0,0 +1,189 @@ +package com.guardanis.applock.views; + +import android.Manifest; +import android.app.Activity; +import android.app.Application; +import android.content.Intent; +import android.view.View; +import android.widget.TextView; + +import androidx.core.app.ActivityCompat; + +import com.guardanis.applock.AppLock; +import com.guardanis.applock.pin.PINInputController; +import com.guardanis.applock.pin.PINInputView; +import com.guardanis.applock.utils.LifeCycleUtils; + +import java.lang.ref.WeakReference; + +import ru.vtosters.lite.utils.AndroidUtils; + +public abstract class AppLockViewController implements LifeCycleUtils.AppLockActivityLifeCycleCallbacks.Delegate { + + protected PINInputController pinInputController; + + protected WeakReference activity; + protected WeakReference parent; + + protected WeakReference pinInputView; + + protected WeakReference descriptionView; + protected WeakReference actionSettings; + + protected boolean autoAuthorizationEnabled = true; + + protected Application.ActivityLifecycleCallbacks activityLifecycleCallbacks; + + public AppLockViewController(Activity activity, View parent) { + this.activity = new WeakReference<>(activity); + this.parent = new WeakReference<>(parent); + this.descriptionView = new WeakReference((TextView) parent.findViewById(AndroidUtils.getIdentifier("pin__description", "id"))); + this.actionSettings = new WeakReference<>(parent.findViewById(AndroidUtils.getIdentifier("pin__action_settings", "id"))); + + this.pinInputView = new WeakReference((PINInputView) parent.findViewById(AndroidUtils.getIdentifier("pin__input_view", "id"))); + + int inputViewsCount = parent.getResources() + .getInteger(AndroidUtils.getIdentifier("applock__input_pin_item_count", "integer")); + + boolean passwordCharsEnabled = parent.getResources() + .getBoolean(AndroidUtils.getIdentifier("applock__item_password_chars_enabled", "bool")); + + this.pinInputController = new PINInputController(pinInputView.get()) + .setInputNumbersCount(inputViewsCount) + .setPasswordCharactersEnabled(passwordCharsEnabled); + + this.activityLifecycleCallbacks = LifeCycleUtils.attach(activity, this); + } + + public abstract void setupRootFlow(); + + public void setDescription(int descriptionResId) { + final TextView descriptionView = this.descriptionView.get(); + + if (descriptionView == null) + return; + + descriptionView.setText(descriptionResId); + } + + public void setDescription(String description) { + final TextView descriptionView = this.descriptionView.get(); + + if (descriptionView == null) + return; + + descriptionView.setText(description); + } + + protected void hide(WeakReference weakView) { + final T view = weakView.get(); + + if (view == null) + return; + + view.setVisibility(View.GONE); + } + + protected void show(WeakReference weakView) { + final T view = weakView.get(); + + if (view == null) + return; + + view.setVisibility(View.VISIBLE); + } + + public PINInputController getPINInputController() { + return pinInputController; + } + + public View getParent() { + return parent.get(); + } + + public void setAutoAuthorizationEnabled(boolean autoAuthorizationEnabled) { + this.autoAuthorizationEnabled = autoAuthorizationEnabled; + } + + public void updateActionSettings(final int errorCode) { + View actionSettings = this.actionSettings.get(); + + if (actionSettings == null) + return; + + if (getSettingsIntent(errorCode) == null) { + actionSettings.setVisibility(View.GONE); + return; + } + + actionSettings.setVisibility(View.VISIBLE); + actionSettings.setOnClickListener(new View.OnClickListener() { + public void onClick(View view) { + handleActionSettingsClicked(errorCode); + } + }); + } + + protected abstract void handleActionSettingsClicked(int errorCode); + + protected void handleInitialErrorPrompt(int errorCode) { + Activity activity = this.activity.get(); + + if (activity == null) + return; + + switch (errorCode) { + case AppLock.ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED: + ActivityCompat.requestPermissions( + activity, + new String[]{Manifest.permission.USE_FINGERPRINT}, + AppLock.REQUEST_CODE_FINGERPRINT_PERMISSION); + break; + default: + break; + } + } + + protected int getDescriptionResIdForError(int errorCode) { + return switch (errorCode) { + case AppLock.ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED -> + AndroidUtils.getIdentifier("applock__fingerprint_error_permission", "string"); + case AppLock.ERROR_CODE_FINGERPRINTS_EMPTY -> + AndroidUtils.getIdentifier("applock__fingerprint_error_none", "string"); + case AppLock.ERROR_CODE_FINGERPRINTS_MISSING_HARDWARE -> + AndroidUtils.getIdentifier("applock__fingerprint_error_hardware", "string"); + case AppLock.ERROR_CODE_FINGERPRINTS_NOT_LOCALLY_ENROLLED -> + AndroidUtils.getIdentifier("applock__fingerprint_error_not_enrolled", "string"); + default -> AndroidUtils.getIdentifier("applock__fingerprint_error_unknown", "string"); + }; + } + + /** + * @return an Intent directed towards the correct system setting for the error, or null if there is none. + */ + public Intent getSettingsIntent(int errorCode) { + return switch (errorCode) { + case AppLock.ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED, AppLock.ERROR_CODE_FINGERPRINTS_EMPTY, AppLock.ERROR_CODE_FINGERPRINTS_MISSING_HARDWARE -> + new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS); + default -> null; + }; + } + + public void unregisterReceivers() { + Activity activity = this.activity.get(); + + if (activity == null) + return; + + activity.getApplication() + .unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks); + } + + @Override + public void onActivityResumed() { + } + + @Override + public void onActivityPaused() { + } +} diff --git a/app/src/main/java/com/guardanis/applock/views/LockCreationViewController.java b/app/src/main/java/com/guardanis/applock/views/LockCreationViewController.java new file mode 100644 index 000000000..ef4e1e0fb --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/views/LockCreationViewController.java @@ -0,0 +1,253 @@ +package com.guardanis.applock.views; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.view.View; + +import androidx.core.content.ContextCompat; +import androidx.core.hardware.fingerprint.FingerprintManagerCompat; +import androidx.core.os.CancellationSignal; + +import com.guardanis.applock.AppLock; +import com.guardanis.applock.pin.PINInputController; +import com.guardanis.applock.services.FingerprintLockService; +import com.guardanis.applock.services.PINLockService; + +import java.lang.ref.WeakReference; + +import ru.vtosters.lite.utils.AndroidUtils; + +public class LockCreationViewController extends AppLockViewController + implements PINInputController.InputEventListener, FingerprintLockService.AuthenticationDelegate { + + protected DisplayVariant displayVariant = DisplayVariant.NONE; + protected WeakReference delegate; + protected WeakReference chooserParent; + protected String pinFirst; + + public LockCreationViewController(Activity activity, View parent) { + super(activity, parent); + + this.chooserParent = new WeakReference(parent.findViewById(AndroidUtils.getIdentifier("pin__create_chooser_items", "id"))); + } + + public LockCreationViewController setDelegate(Delegate delegate) { + this.delegate = new WeakReference<>(delegate); + + return this; + } + + @Override + public void setupRootFlow() { + Activity activity = this.activity.get(); + + if (activity == null) + return; + + setupPINCreation(); + } + + protected void setupPINCreation() { + this.displayVariant = DisplayVariant.PIN_CREATION; + + hide(chooserParent); + hide(actionSettings); + + show(pinInputView); + + setDescription(AndroidUtils.getIdentifier("applock__description_create_pin", "string")); + + pinInputController.ensureKeyboardVisible(); + pinInputController.setInputEventListener(this); + } + + protected void setupPINConfirmation() { + this.displayVariant = DisplayVariant.PIN_CONFIRMATION; + + hide(chooserParent); + hide(actionSettings); + + show(pinInputView); + + setDescription(AndroidUtils.getIdentifier("applock__description_confirm", "string")); + + pinInputController.ensureKeyboardVisible(); + pinInputController.setInputEventListener(this); + } + + @Override + public void onInputEntered(String input) { + switch (displayVariant) { + case PIN_CREATION -> { + if (!pinInputController.matchesRequiredPINLength(input)) { + setDescription(AndroidUtils.getIdentifier("applock__unlock_error_insufficient_selection", "string")); + + return; + } + this.pinFirst = input; + setupPINConfirmation(); + } + case PIN_CONFIRMATION -> { + if (!pinInputController.matchesRequiredPINLength(input)) { + setDescription(AndroidUtils.getIdentifier("applock__unlock_error_insufficient_selection", "string")); + + return; + } + if (!input.equals(pinFirst)) { + this.pinFirst = null; + + setupPINCreation(); + setDescription(AndroidUtils.getIdentifier("applock__description_create_pin_reattempt", "string")); + + return; + } + createPINLock(input); + } + default -> { + } + } + } + + protected void createPINLock(String input) { + Activity activity = this.activity.get(); + + if (activity == null) + return; + + AppLock.getInstance(activity) + .getLockService(PINLockService.class) + .enroll(activity, input); + + var fingerprintLockService = AppLock.getInstance(activity).getLockService(FingerprintLockService.class); + if (fingerprintLockService.isEnrollmentEligible(activity)) { + setupFingerprintAuthentication(); + } else { + handleLockCreated(); + } + } + + protected void setupFingerprintAuthentication() { + this.displayVariant = DisplayVariant.FINGERPRINT_AUTHENTICATION; + + hide(pinInputView); + hide(chooserParent); + hide(actionSettings); + + setDescription(AndroidUtils.getIdentifier("applock__description_create_fingerprint", "string")); + + if (autoAuthorizationEnabled) + attemptFingerprintAuthentication(); + } + + protected void attemptFingerprintAuthentication() { + Activity activity = this.activity.get(); + + if (activity == null) + return; + + AppLock.getInstance(activity) + .getLockService(FingerprintLockService.class) + .enroll(activity, this); + } + + @Override + public void onResolutionRequired(int errorCode) { + setDescription(getDescriptionResIdForError(errorCode)); + updateActionSettings(errorCode); + handleInitialErrorPrompt(errorCode); + } + + @Override + public void onAuthenticationHelp(int code, CharSequence message) { + Activity activity = this.activity.get(); + + if (activity == null) + return; + + String unformattedHelpMessage = activity.getString(AndroidUtils.getIdentifier("applock__description_unlock_fingerprint_help", "string")); + String formatted = String.format(unformattedHelpMessage, message); + + setDescription(formatted); + } + + @Override + public void onAuthenticating(CancellationSignal cancellationSignal) { + } + + @Override + public void onAuthenticationSuccess(FingerprintManagerCompat.AuthenticationResult result) { + handleLockCreated(); + } + + @Override + public void onAuthenticationFailed(String message) { + setDescription(message); + } + + protected void handleLockCreated() { + this.displayVariant = DisplayVariant.NONE; + + Delegate delegate = this.delegate.get(); + + if (delegate != null) + delegate.onLockCreated(); + } + + @Override + public void onActivityPaused() { + Activity activity = this.activity.get(); + + if (activity == null) + return; + + AppLock.getInstance(activity) + .cancelPendingAuthentications(); + + if (displayVariant == DisplayVariant.FINGERPRINT_AUTHENTICATION) + setDescription(AndroidUtils.getIdentifier("applock__description_create_fingerprint_paused", "string")); + } + + @Override + public void onActivityResumed() { + Activity activity = this.activity.get(); + + if (activity == null || displayVariant != DisplayVariant.FINGERPRINT_AUTHENTICATION) + return; + + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) { + setDescription(AndroidUtils.getIdentifier("applock__fingerprint_error_permission_multiple", "string")); + updateActionSettings(AppLock.ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED); + + return; + } + + setDescription(AndroidUtils.getIdentifier("applock__description_create_fingerprint", "string")); + hide(actionSettings); + + attemptFingerprintAuthentication(); + } + + @Override + protected void handleActionSettingsClicked(int errorCode) { + Activity activity = this.activity.get(); + Intent intent = getSettingsIntent(errorCode); + + if (activity == null || intent == null) + return; + + activity.startActivity(intent); + } + + public enum DisplayVariant { + NONE, + PIN_CREATION, + PIN_CONFIRMATION, + FINGERPRINT_AUTHENTICATION + } + + public interface Delegate { + void onLockCreated(); + } +} diff --git a/app/src/main/java/com/guardanis/applock/views/UnlockViewController.java b/app/src/main/java/com/guardanis/applock/views/UnlockViewController.java new file mode 100644 index 000000000..099ff91f4 --- /dev/null +++ b/app/src/main/java/com/guardanis/applock/views/UnlockViewController.java @@ -0,0 +1,181 @@ +package com.guardanis.applock.views; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.view.View; + +import androidx.core.content.ContextCompat; + +import com.guardanis.applock.AppLock; +import com.guardanis.applock.pin.PINInputController; +import com.guardanis.applock.services.FingerprintLockService; + +import java.lang.ref.WeakReference; + +import ru.vtosters.lite.utils.AndroidUtils; + +public class UnlockViewController extends AppLockViewController implements AppLock.UnlockDelegate, PINInputController.InputEventListener { + + protected DisplayVariant displayVariant = DisplayVariant.PIN_UNLOCK; + protected WeakReference delegate; + + public UnlockViewController(Activity activity, View parent) { + super(activity, parent); + } + + public UnlockViewController setDelegate(Delegate delegate) { + this.delegate = new WeakReference(delegate); + + return this; + } + + @Override + public void setupRootFlow() { + View parent = this.parent.get(); + + if (parent == null) + return; + + setupPINUnlock(); + } + + protected void setupPINUnlock() { + this.displayVariant = DisplayVariant.PIN_UNLOCK; + + hide(actionSettings); + show(pinInputView); + + setDescription(AndroidUtils.getIdentifier("applock__description_unlock_pin", "string")); + + pinInputController.ensureKeyboardVisible(); + pinInputController.setInputEventListener(this); + + FingerprintLockService fingerprintService = AppLock.getInstance(this.parent.get().getContext()) + .getLockService(FingerprintLockService.class); + + if (fingerprintService.isEnrolled(parent.get().getContext())) + attemptFingerprintAuthentication(); + } + + @Override + public void onInputEntered(String input) { + if (!pinInputController.matchesRequiredPINLength(input)) { + setDescription(AndroidUtils.getIdentifier("applock__unlock_error_insufficient_selection", "string")); + + return; + } + + attemptPINUnlock(input); + } + + protected void attemptPINUnlock(String input) { + Activity activity = this.activity.get(); + + if (activity == null) + return; + + AppLock.getInstance(activity) + .attemptPINUnlock(input, this); + } + + protected void attemptFingerprintAuthentication() { + Activity activity = this.activity.get(); + + if (activity == null) + return; + + AppLock.getInstance(activity) + .attemptFingerprintUnlock(this); + } + + @Override + public void onUnlockSuccessful() { + this.displayVariant = DisplayVariant.NONE; + + Delegate delegate = this.delegate.get(); + + if (delegate != null) + delegate.onUnlockSuccessful(); + } + + @Override + public void onResolutionRequired(int errorCode) { + setDescription(getDescriptionResIdForError(errorCode)); + updateActionSettings(errorCode); + handleInitialErrorPrompt(errorCode); + } + + @Override + public void onAuthenticationHelp(int code, String message) { + Activity activity = this.activity.get(); + + if (activity == null) + return; + + String unformattedHelpMessage = activity.getString(AndroidUtils.getIdentifier("applock__description_unlock_fingerprint_help", "string")); + String formatted = String.format(unformattedHelpMessage, message); + + setDescription(formatted); + } + + @Override + public void onFailureLimitExceeded(String message) { + setDescription(message); + } + + @Override + public void onActivityPaused() { + Activity activity = this.activity.get(); + + if (activity == null) + return; + + AppLock.getInstance(activity) + .cancelPendingAuthentications(); + + if (displayVariant == DisplayVariant.FINGERPRINT_AUTHENTICATION) + setDescription(AndroidUtils.getIdentifier("applock__description_create_fingerprint_paused", "string")); + } + + @Override + public void onActivityResumed() { + Activity activity = this.activity.get(); + + if (activity == null || displayVariant != DisplayVariant.FINGERPRINT_AUTHENTICATION) + return; + + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) { + setDescription(AndroidUtils.getIdentifier("applock__fingerprint_error_permission_multiple", "string")); + updateActionSettings(AppLock.ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED); + return; + } + + setDescription(AndroidUtils.getIdentifier("applock__description_unlock_fingerprint", "string")); + hide(actionSettings); + + attemptFingerprintAuthentication(); + } + + @Override + protected void handleActionSettingsClicked(int errorCode) { + Activity activity = this.activity.get(); + Intent intent = getSettingsIntent(errorCode); + + if (activity == null || intent == null) + return; + + activity.startActivity(intent); + } + + public enum DisplayVariant { + NONE, + PIN_UNLOCK, + FINGERPRINT_AUTHENTICATION + } + + public interface Delegate { + void onUnlockSuccessful(); + } +} diff --git a/app/src/main/java/ru/vtosters/lite/hooks/MainActivityInjector.java b/app/src/main/java/ru/vtosters/lite/hooks/MainActivityInjector.java index d47bc289e..a1b3ac91a 100644 --- a/app/src/main/java/ru/vtosters/lite/hooks/MainActivityInjector.java +++ b/app/src/main/java/ru/vtosters/lite/hooks/MainActivityInjector.java @@ -7,8 +7,12 @@ import static ru.vtosters.lite.utils.Preferences.checkupdates; import static ru.vtosters.lite.utils.ThemesUtils.setNeededTheme; import android.app.Activity; +import android.content.Intent; import android.os.Build; +import com.guardanis.applock.AppLock; +import com.guardanis.applock.activities.UnlockActivity; + import ru.vtosters.lite.concurrent.VTExecutors; import ru.vtosters.lite.downloaders.notifications.NotificationChannels; import ru.vtosters.lite.ssfs.UsersList; @@ -38,6 +42,10 @@ public class MainActivityInjector { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannels.createChannels(); } + if (AppLock.isEnrolled(activity)) { + var intent = new Intent(activity, UnlockActivity.class); + activity.startActivity(intent); + } // VKIDProtection.alert(activity); } diff --git a/app/src/main/java/ru/vtosters/lite/ui/fragments/OtherFragment.java b/app/src/main/java/ru/vtosters/lite/ui/fragments/OtherFragment.java index 5d235bf23..f792c5276 100644 --- a/app/src/main/java/ru/vtosters/lite/ui/fragments/OtherFragment.java +++ b/app/src/main/java/ru/vtosters/lite/ui/fragments/OtherFragment.java @@ -1,16 +1,39 @@ package ru.vtosters.lite.ui.fragments; +import static android.widget.Toast.LENGTH_LONG; +import static android.widget.Toast.LENGTH_SHORT; +import static ru.vtosters.lite.ui.components.BackupManager.backupOnlines; +import static ru.vtosters.lite.ui.components.BackupManager.backupSettings; +import static ru.vtosters.lite.ui.components.BackupManager.deletePrefs; +import static ru.vtosters.lite.ui.components.BackupManager.restoreBackup; +import static ru.vtosters.lite.utils.AccountManagerUtils.getUserToken; +import static ru.vtosters.lite.utils.AndroidUtils.dp2px; +import static ru.vtosters.lite.utils.AndroidUtils.getDefaultPrefs; +import static ru.vtosters.lite.utils.AndroidUtils.sendToast; +import static ru.vtosters.lite.utils.LifecycleUtils.restartApplication; +import static ru.vtosters.lite.utils.LifecycleUtils.restartApplicationWithTimer; +import static ru.vtosters.lite.utils.ThemesUtils.getTextAttr; + import android.annotation.SuppressLint; import android.app.Activity; -import android.content.*; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.webkit.WebView; -import android.widget.*; +import android.widget.ArrayAdapter; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.Toast; + import androidx.appcompat.view.ContextThemeWrapper; -import b.h.g.m.FileUtils; + import com.vk.auth.api.VKAccount; import com.vk.core.dialogs.alert.VkAlertDialog; import com.vk.core.util.ToastUtils; @@ -21,29 +44,25 @@ import com.vk.im.ui.providers.audiomsg.PlayerActionSources; import com.vk.imageloader.VKImageLoader; import com.vk.media.player.cache.AutoPlayCacheHolder; import com.vk.mediastore.MediaStorage; -import com.vk.navigation.Navigator; import com.vk.pushes.PushSubscriber; import com.vk.stickers.Stickers; import com.vtosters.lite.R; import com.vtosters.lite.auth.VKAccountManager; import com.vtosters.lite.general.fragments.MaterialPreferenceToolbarFragment; import com.vtosters.lite.im.ImEngineProvider; + +import java.io.IOException; + +import b.h.g.m.FileUtils; import ru.vtosters.lite.deviceinfo.DeviceInfoCollector; import ru.vtosters.lite.ssfs.UsersList; import ru.vtosters.lite.ui.activities.VKAdminTokenActivity; import ru.vtosters.lite.ui.components.BackupManager; -import ru.vtosters.lite.utils.*; - -import java.io.IOException; - -import static android.widget.Toast.LENGTH_LONG; -import static android.widget.Toast.LENGTH_SHORT; -import static ru.vtosters.lite.ui.components.BackupManager.*; -import static ru.vtosters.lite.utils.AccountManagerUtils.getUserToken; -import static ru.vtosters.lite.utils.AndroidUtils.*; -import static ru.vtosters.lite.utils.LifecycleUtils.restartApplication; -import static ru.vtosters.lite.utils.LifecycleUtils.restartApplicationWithTimer; -import static ru.vtosters.lite.utils.ThemesUtils.getTextAttr; +import ru.vtosters.lite.utils.AccountManagerUtils; +import ru.vtosters.lite.utils.AndroidUtils; +import ru.vtosters.lite.utils.NavigatorUtils; +import ru.vtosters.lite.utils.Preferences; +import ru.vtosters.lite.utils.VTVerifications; public class OtherFragment extends MaterialPreferenceToolbarFragment { private static final int VK_ADMIN_TOKEN_REQUEST_CODE = 1; @@ -69,6 +88,11 @@ public class OtherFragment extends MaterialPreferenceToolbarFragment { } private void prefs() { + findPreference("pin").setOnPreferenceClickListener(preference -> { + NavigatorUtils.switchFragment(requireContext(), PinFragment.class); + return true; + }); + findPreference("firebasefix").setOnPreferenceClickListener(preference -> { VKAccount b = VKAccountManager.d(); PushSubscriber.e.a(); diff --git a/app/src/main/java/ru/vtosters/lite/ui/fragments/PinFragment.java b/app/src/main/java/ru/vtosters/lite/ui/fragments/PinFragment.java new file mode 100644 index 000000000..fefe230de --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/ui/fragments/PinFragment.java @@ -0,0 +1,30 @@ +package ru.vtosters.lite.ui.fragments; + +import android.content.Intent; +import android.os.Bundle; + +import com.guardanis.applock.AppLock; +import com.guardanis.applock.activities.LockCreationActivity; +import com.vtosters.lite.general.fragments.MaterialPreferenceToolbarFragment; + +import ru.vtosters.lite.ui.PreferenceFragmentUtils; + +public class PinFragment extends MaterialPreferenceToolbarFragment { + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + this.addPreferencesFromResource(com.vtosters.lite.R.xml.empty); + PreferenceFragmentUtils.addPreference( + getPreferenceScreen(), + "", + "Установить PIN-код", + "Установить PIN-код для входа в приложение", + null, + preference -> { + var intent = new Intent(this.getActivity(), LockCreationActivity.class); + startActivityForResult(intent, AppLock.REQUEST_CODE_LOCK_CREATION); + return true; + } + ); + } +} diff --git a/smali/res/drawable/applock__chooser_option_bg.xml b/smali/res/drawable/applock__chooser_option_bg.xml new file mode 100644 index 000000000..9ea701e69 --- /dev/null +++ b/smali/res/drawable/applock__chooser_option_bg.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/smali/res/drawable/applock__fingerprint.png b/smali/res/drawable/applock__fingerprint.png new file mode 100644 index 000000000..0b5b4e093 Binary files /dev/null and b/smali/res/drawable/applock__fingerprint.png differ diff --git a/smali/res/drawable/applock__pin.png b/smali/res/drawable/applock__pin.png new file mode 100644 index 000000000..5082e3def Binary files /dev/null and b/smali/res/drawable/applock__pin.png differ diff --git a/smali/res/layout/applock__activity_lock_creation.xml b/smali/res/layout/applock__activity_lock_creation.xml new file mode 100644 index 000000000..a016276ee --- /dev/null +++ b/smali/res/layout/applock__activity_lock_creation.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/smali/res/layout/applock__activity_unlock.xml b/smali/res/layout/applock__activity_unlock.xml new file mode 100644 index 000000000..580bf0ddf --- /dev/null +++ b/smali/res/layout/applock__activity_unlock.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/smali/res/layout/applock__fake_edit_text.xml b/smali/res/layout/applock__fake_edit_text.xml new file mode 100644 index 000000000..6328b3019 --- /dev/null +++ b/smali/res/layout/applock__fake_edit_text.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/smali/res/layout/applock__lock_creation.xml b/smali/res/layout/applock__lock_creation.xml new file mode 100644 index 000000000..d6fe62985 --- /dev/null +++ b/smali/res/layout/applock__lock_creation.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/smali/res/layout/applock__unlock.xml b/smali/res/layout/applock__unlock.xml new file mode 100644 index 000000000..27254bf73 --- /dev/null +++ b/smali/res/layout/applock__unlock.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/smali/res/values/attrs.xml b/smali/res/values/attrs.xml index 856f8c01e..52c8fc070 100644 --- a/smali/res/values/attrs.xml +++ b/smali/res/values/attrs.xml @@ -2511,4 +2511,8 @@ + + + + diff --git a/smali/res/values/bools.xml b/smali/res/values/bools.xml index fa5912a36..aa3c8a018 100644 --- a/smali/res/values/bools.xml +++ b/smali/res/values/bools.xml @@ -17,4 +17,7 @@ false false true + false + true + true diff --git a/smali/res/values/colors.xml b/smali/res/values/colors.xml index 61d0d7cb3..8447fee76 100644 --- a/smali/res/values/colors.xml +++ b/smali/res/values/colors.xml @@ -686,4 +686,15 @@ #ffffd54f #fffece00 #fffcfcfc + + #FAFAFA + #7F8C8D + + #3C9ADF + #FAFAFA + + #00FFFFFF + #11000000 + + #95A5A6 diff --git a/smali/res/values/dimens.xml b/smali/res/values/dimens.xml index 69dde40bc..4f1dea47d 100644 --- a/smali/res/values/dimens.xml +++ b/smali/res/values/dimens.xml @@ -855,4 +855,20 @@ -2 64.0dip 22.0dip + + 24dip + 24dip + 36dip + 20dip + 50dip + 35dip + 65dip + 50dip + 24dip + 12dip + 18dip + 20dip + 14dip + 12dip + 24dip diff --git a/smali/res/values/ids.xml b/smali/res/values/ids.xml index 276bf1ab1..b1c218d12 100644 --- a/smali/res/values/ids.xml +++ b/smali/res/values/ids.xml @@ -3919,4 +3919,9 @@ + + + + + diff --git a/smali/res/values/integers.xml b/smali/res/values/integers.xml index 098e30e64..ea0db4758 100644 --- a/smali/res/values/integers.xml +++ b/smali/res/values/integers.xml @@ -43,4 +43,9 @@ 999 200 4 + + 4 + 5 + 5 + 15 diff --git a/smali/res/values/strings.xml b/smali/res/values/strings.xml index f21d2357f..54e6ef2a1 100644 --- a/smali/res/values/strings.xml +++ b/smali/res/values/strings.xml @@ -6499,4 +6499,32 @@ May look like a fake leaving the online" To submit bug info to GitHub Issues Device info copied You need to allow the app to open links by default for the app to work properly + + Secure this application with a PIN or your fingerprints? + Create a PIN to secure this application. + PINs didn\'t match. Try again. + Scan your fingerprint to secure this application. + Fingerprint authentication canceled. Reload this content to restart. + Re-enter your PIN to confirm. + Enter your PIN to unlock. + Scan your fingerprint to unlock. + We\'re having trouble recognizing your fingerprint: %1$s + PIN + Fingerprint + There is no saved PIN to unlock. + The PINs don\'t match! + Wrong number of digits! Please try again + You failed too many times. You may try again in %1$s. + You can\'t do that + * + Open System Settings + AppLock + You haven\'t setup fingerprint unlocking. + Your fingerprint wasn\'t recognized. + Something weird happened. + No fingerprints found! Enable them in your system settings. + Missing fingerprint hardware? + Fingerprint permission required. + Fingerprint permission denied. Please enable fingerprint support in your system settings. + 0.2 diff --git a/smali/res/values/styles.xml b/smali/res/values/styles.xml index d2faed7ab..52014dd2d 100644 --- a/smali/res/values/styles.xml +++ b/smali/res/values/styles.xml @@ -10262,4 +10262,47 @@ -1.0 0.25 + + + + + + + + + + diff --git a/smali/res/xml/preferences_other.xml b/smali/res/xml/preferences_other.xml index bd06f0702..205800867 100644 --- a/smali/res/xml/preferences_other.xml +++ b/smali/res/xml/preferences_other.xml @@ -1,5 +1,10 @@ + + +