init PIN Lock
This commit is contained in:
@@ -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<Class, LockService> lockServices = new HashMap<Class, LockService>();
|
||||
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 extends LockService> T getLockService(Class<T> 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<ALVC extends AppLockViewController> {
|
||||
|
||||
protected WeakReference<Activity> activity;
|
||||
protected ALVC viewController;
|
||||
|
||||
protected WeakReference<AppCompatDialog> dialog = new WeakReference<AppCompatDialog>(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;
|
||||
}
|
||||
}
|
||||
@@ -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<LockCreationViewController> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<UnlockViewController> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<PINInputView> inputView;
|
||||
private WeakReference<InputEventListener> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<PINItemView, PINItemAnimator> animators = new WeakHashMap<PINItemView, PINItemAnimator>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<PINInputView> 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<Activity> openedActivity;
|
||||
protected WeakReference<Delegate> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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> activity;
|
||||
protected WeakReference<View> parent;
|
||||
|
||||
protected WeakReference<PINInputView> pinInputView;
|
||||
|
||||
protected WeakReference<TextView> descriptionView;
|
||||
protected WeakReference<View> 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 <T extends View> void hide(WeakReference<T> weakView) {
|
||||
final T view = weakView.get();
|
||||
|
||||
if (view == null)
|
||||
return;
|
||||
|
||||
view.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
protected <T extends View> void show(WeakReference<T> 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() {
|
||||
}
|
||||
}
|
||||
@@ -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> delegate;
|
||||
protected WeakReference<View> 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();
|
||||
}
|
||||
}
|
||||
@@ -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> delegate;
|
||||
|
||||
public UnlockViewController(Activity activity, View parent) {
|
||||
super(activity, parent);
|
||||
}
|
||||
|
||||
public UnlockViewController setDelegate(Delegate delegate) {
|
||||
this.delegate = new WeakReference<Delegate>(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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:drawable="@color/applock__create_choose_option_bg_pressed" android:state_selected="true"/>
|
||||
<item android:drawable="@color/applock__create_choose_option_bg_pressed" android:state_pressed="true"/>
|
||||
<item android:drawable="@color/applock__create_choose_option_bg" />
|
||||
|
||||
</selector>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/applock__activity_lock_creation"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/applock__activity_padding">
|
||||
|
||||
<include
|
||||
android:id="@+id/pin__container"
|
||||
layout="@layout/applock__lock_creation" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/applock__activity_unlock"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/applock__activity_padding">
|
||||
|
||||
<include
|
||||
android:id="@+id/pin__container"
|
||||
layout="@layout/applock__unlock" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<EditText
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="1dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:cursorVisible="false"
|
||||
android:imeOptions="flagNoExtractUi|actionDone"
|
||||
android:inputType="number"
|
||||
android:textColor="@android:color/transparent"/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:applock="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/applock__content_parent"
|
||||
android:descendantFocusability="beforeDescendants"
|
||||
android:focusableInTouchMode="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pin__description"
|
||||
style="@style/applock__description" />
|
||||
|
||||
<com.guardanis.applock.pin.PINInputView
|
||||
android:id="@+id/pin__input_view"
|
||||
style="@style/applock__input_pin_view"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/pin__action_settings"
|
||||
style="@style/applock__action_settings"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,20 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:applock="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/applock__content_parent"
|
||||
android:descendantFocusability="beforeDescendants"
|
||||
android:focusableInTouchMode="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pin__description"
|
||||
style="@style/applock__description"/>
|
||||
|
||||
<com.guardanis.applock.pin.PINInputView
|
||||
android:id="@+id/pin__input_view"
|
||||
style="@style/applock__input_pin_view"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/pin__action_settings"
|
||||
style="@style/applock__action_settings"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -2511,4 +2511,8 @@
|
||||
<attr name="im_ic_msgreload" format="reference" />
|
||||
<attr name="im_ic_msgdl" format="reference" />
|
||||
<attr name="im_ic_stats" format="reference" />
|
||||
<declare-styleable name="PINInputView">
|
||||
<attr name="pinTextColor" format="reference|color"/>
|
||||
<attr name="pinBackgroundColor" format="reference|color"/>
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
|
||||
@@ -17,4 +17,7 @@
|
||||
<bool name="spb_default_reversed">false</bool>
|
||||
<bool name="vk_is_tablet">false</bool>
|
||||
<bool name="com_samsung_android_icon_container_has_icon_container">true</bool>
|
||||
<bool name="applock__unlock_activity_return_allowed">false</bool>
|
||||
<bool name="applock__item_password_chars_enabled">true</bool>
|
||||
<bool name="applock__fingerprint_service_enabled">true</bool>
|
||||
</resources>
|
||||
|
||||
@@ -686,4 +686,15 @@
|
||||
<color name="yellow_light">#ffffd54f</color>
|
||||
<color name="yellow_sunflower">#fffece00</color>
|
||||
<color name="iclauncher">#fffcfcfc</color>
|
||||
|
||||
<color name="applock__activity_background">#FAFAFA</color>
|
||||
<color name="applock__description_text">#7F8C8D</color>
|
||||
|
||||
<color name="applock__item_background">#3C9ADF</color>
|
||||
<color name="applock__item_text">#FAFAFA</color>
|
||||
|
||||
<color name="applock__create_choose_option_bg">#00FFFFFF</color>
|
||||
<color name="applock__create_choose_option_bg_pressed">#11000000</color>
|
||||
|
||||
<color name="applock__action_settings">#95A5A6</color>
|
||||
</resources>
|
||||
|
||||
@@ -855,4 +855,20 @@
|
||||
<item name="wrap_content" type="dimen">-2</item>
|
||||
<dimen name="year_label_height">64.0dip</dimen>
|
||||
<dimen name="year_label_text_size">22.0dip</dimen>
|
||||
|
||||
<dimen name="applock__activity_padding">24dip</dimen>
|
||||
<dimen name="applock__content_padding_horizontal">24dip</dimen>
|
||||
<dimen name="applock__content_padding_vertical">36dip</dimen>
|
||||
<dimen name="applock__item_margin_top">20dip</dimen>
|
||||
<dimen name="applock__item_size">50dip</dimen>
|
||||
<dimen name="applock__fingerprint_scan_image_margin_top">35dip</dimen>
|
||||
<dimen name="applock__fingerprint_scan_image_size">65dip</dimen>
|
||||
<dimen name="applock__chooser_image_size">50dip</dimen>
|
||||
<dimen name="applock__chooser_parent_padding_vertical">24dip</dimen>
|
||||
<dimen name="applock__chooser_text_padding_top">12dip</dimen>
|
||||
<dimen name="applock__description_text">18dip</dimen>
|
||||
<dimen name="applock__description_padding_bottom">20dip</dimen>
|
||||
<dimen name="applock__action_settings">14dip</dimen>
|
||||
<dimen name="applock__action_settings_padding">12dip</dimen>
|
||||
<dimen name="applock__action_settings_margin_top">24dip</dimen>
|
||||
</resources>
|
||||
|
||||
@@ -3919,4 +3919,9 @@
|
||||
<item name="download_mp3" type="id" />
|
||||
<item name="remove_from_cache_vkx" type="id" />
|
||||
<item name="add_to_cache_vkx" type="id" />
|
||||
|
||||
<item name="pin__description" type="id" />
|
||||
<item name="pin__action_settings" type="id" />
|
||||
<item name="pin__input_view" type="id" />
|
||||
<item name="pin__container" type="id"/>
|
||||
</resources>
|
||||
|
||||
@@ -43,4 +43,9 @@
|
||||
<integer name="status_bar_notification_info_maxnum">999</integer>
|
||||
<integer name="vkim_activity_animation_duration">200</integer>
|
||||
<integer name="vkim_playing_drawable_rect_count">4</integer>
|
||||
|
||||
<integer name="applock__input_pin_item_count">4</integer>
|
||||
<integer name="applock__max_retry_count">5</integer>
|
||||
<integer name="applock__failure_retry_delay">5</integer>
|
||||
<integer name="applock__activity_lock_reenable_minutes">15</integer>
|
||||
</resources>
|
||||
|
||||
@@ -6499,4 +6499,32 @@ May look like a fake leaving the online"</string>
|
||||
<string name="copydebuginfo_summ">To submit bug info to GitHub Issues</string>
|
||||
<string name="device_info_copied">Device info copied</string>
|
||||
<string name="app_open_by_default_settings">You need to allow the app to open links by default for the app to work properly</string>
|
||||
|
||||
<string name="applock__description_chooser">Secure this application with a PIN or your fingerprints?</string>
|
||||
<string name="applock__description_create_pin">Create a PIN to secure this application.</string>
|
||||
<string name="applock__description_create_pin_reattempt">PINs didn\'t match. Try again.</string>
|
||||
<string name="applock__description_create_fingerprint">Scan your fingerprint to secure this application.</string>
|
||||
<string name="applock__description_create_fingerprint_paused">Fingerprint authentication canceled. Reload this content to restart.</string>
|
||||
<string name="applock__description_confirm">Re-enter your PIN to confirm.</string>
|
||||
<string name="applock__description_unlock_pin">Enter your PIN to unlock.</string>
|
||||
<string name="applock__description_unlock_fingerprint">Scan your fingerprint to unlock.</string>
|
||||
<string name="applock__description_unlock_fingerprint_help">We\'re having trouble recognizing your fingerprint: %1$s</string>
|
||||
<string name="applock__create_chooser_option_pin">PIN</string>
|
||||
<string name="applock__create_chooser_option_fingerprint">Fingerprint</string>
|
||||
<string name="applock__unlock_error_no_matching_pin_found">There is no saved PIN to unlock.</string>
|
||||
<string name="applock__unlock_error_match_failed">The PINs don\'t match!</string>
|
||||
<string name="applock__unlock_error_insufficient_selection">Wrong number of digits! Please try again</string>
|
||||
<string name="applock__unlock_error_retry_limit_exceeded">You failed too many times. You may try again in %1$s.</string>
|
||||
<string name="applock__toast_unlock_required">You can\'t do that</string>
|
||||
<string name="applock__password_char">*</string>
|
||||
<string name="applock__action_settings">Open System Settings</string>
|
||||
<string name="applock__fingerprint_alias">AppLock</string>
|
||||
<string name="applock__fingerprint_error_not_enrolled">You haven\'t setup fingerprint unlocking.</string>
|
||||
<string name="applock__fingerprint_error_unrecognized">Your fingerprint wasn\'t recognized.</string>
|
||||
<string name="applock__fingerprint_error_unknown">Something weird happened.</string>
|
||||
<string name="applock__fingerprint_error_none">No fingerprints found! Enable them in your system settings.</string>
|
||||
<string name="applock__fingerprint_error_hardware">Missing fingerprint hardware?</string>
|
||||
<string name="applock__fingerprint_error_permission">Fingerprint permission required.</string>
|
||||
<string name="applock__fingerprint_error_permission_multiple">Fingerprint permission denied. Please enable fingerprint support in your system settings.</string>
|
||||
<item name="applock__empty_item_min_size_percent" format="float" type="string">0.2</item>
|
||||
</resources>
|
||||
|
||||
@@ -10262,4 +10262,47 @@
|
||||
<item name="android:shadowDy">-1.0</item>
|
||||
<item name="android:shadowRadius">0.25</item>
|
||||
</style>
|
||||
|
||||
<style name="applock__content_parent">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:gravity">center_horizontal</item>
|
||||
<item name="android:orientation">vertical</item>
|
||||
<item name="android:paddingLeft">@dimen/applock__content_padding_horizontal</item>
|
||||
<item name="android:paddingTop">@dimen/applock__content_padding_vertical</item>
|
||||
<item name="android:paddingRight">@dimen/applock__content_padding_horizontal</item>
|
||||
<item name="android:paddingBottom">@dimen/applock__content_padding_vertical</item>
|
||||
</style>
|
||||
|
||||
<style name="applock__description">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:gravity">center</item>
|
||||
<item name="android:text">These is some kind of instructions you should follow.</item>
|
||||
<item name="android:textColor">?text_primary</item>
|
||||
<item name="android:textSize">@dimen/applock__description_text</item>
|
||||
</style>
|
||||
|
||||
<style name="applock__input_pin_view">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">@dimen/applock__item_size</item>
|
||||
<item name="android:layout_marginTop">@dimen/applock__item_margin_top</item>
|
||||
</style>
|
||||
|
||||
<style name="applock__input_fingerprint_view">
|
||||
<item name="android:layout_width">@dimen/applock__fingerprint_scan_image_size</item>
|
||||
<item name="android:layout_height">@dimen/applock__fingerprint_scan_image_size</item>
|
||||
<item name="android:layout_marginTop">@dimen/applock__fingerprint_scan_image_margin_top</item>
|
||||
</style>
|
||||
|
||||
<style name="applock__action_settings">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_marginTop">@dimen/applock__action_settings_margin_top</item>
|
||||
<item name="android:gravity">center</item>
|
||||
<item name="android:padding">@dimen/applock__action_settings_padding</item>
|
||||
<item name="android:text">@string/applock__action_settings</item>
|
||||
<item name="android:textColor">?text_primary</item>
|
||||
<item name="android:textSize">@dimen/applock__action_settings</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory android:title="Настройки PIN">
|
||||
<Preference
|
||||
android:key="pin"
|
||||
android:title="Установить PIN"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/other_fix_app">
|
||||
<Preference
|
||||
android:key="firebasefix"
|
||||
|
||||
Reference in New Issue
Block a user