From 1f57f469398938e528be57231df32333b7880da2 Mon Sep 17 00:00:00 2001 From: Tornaco Date: Wed, 4 Sep 2024 15:40:05 +0800 Subject: [PATCH] App lock pattern --- .../app/activity/ActivityStackSupervisor.java | 26 +++ .../activity/IActivityStackSupervisor.aidl | 5 + .../activity/IActivityStackSupervisor.java | 134 +++++++++++++ .../main/res/values-zh-rCN/app_strings.xml | 11 ++ .../main/res/values-zh-rTW/app_strings.xml | 11 ++ .../res/src/main/res/values/app_strings.xml | 22 +++ android/internal/Thanox-Internal | 2 +- .../src/main/AndroidManifest.xml | 3 + .../locker/ui/setup/SettingsFragment.java | 31 ++- .../locker/ui/verify/LockPatternContent.kt | 62 ++++++ .../ui/verify/PatternSettingsActivity.kt | 141 ++++++++++++++ .../locker/ui/verify/PatternSettingsVM.kt | 93 +++++++++ .../locker/ui/verify/VerifyActivity.kt | 94 +++++++--- .../ui/verify/composelock/ComposeLock.kt | 177 ++++++++++++++++++ .../verify/composelock/ComposeLockCallback.kt | 7 + .../locker/ui/verify/composelock/Dot.kt | 11 ++ .../locker/ui/verify/composelock/Line.kt | 8 + .../src/main/res/values/preference_keys.xml | 2 + .../xml/module_locker_settings_fragment.xml | 14 +- 19 files changed, 821 insertions(+), 33 deletions(-) create mode 100644 android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/LockPatternContent.kt create mode 100644 android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/PatternSettingsActivity.kt create mode 100644 android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/PatternSettingsVM.kt create mode 100644 android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/ComposeLock.kt create mode 100644 android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/ComposeLockCallback.kt create mode 100644 android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/Dot.kt create mode 100644 android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/Line.kt diff --git a/android/android_framework/base/src/main/java/github/tornaco/android/thanos/core/app/activity/ActivityStackSupervisor.java b/android/android_framework/base/src/main/java/github/tornaco/android/thanos/core/app/activity/ActivityStackSupervisor.java index cc0cae3cc..fff52218f 100644 --- a/android/android_framework/base/src/main/java/github/tornaco/android/thanos/core/app/activity/ActivityStackSupervisor.java +++ b/android/android_framework/base/src/main/java/github/tornaco/android/thanos/core/app/activity/ActivityStackSupervisor.java @@ -17,6 +17,12 @@ public static final class LaunchOtherAppPkgSetting { public static final int IGNORE = -1; } + public static final class LockMethod { + public static final int SYSTEM = 0; + public static final int PATTERN = 1; + public static final int PIN = 2; + } + private final IActivityStackSupervisor supervisor; public ActivityStackSupervisor(IActivityStackSupervisor supervisor) { @@ -207,4 +213,24 @@ public void addPkgToLaunchOtherAppAllowList(Pkg pkg, Pkg pkgToAdd) { public List getLaunchOtherAppAllowListOrNull(Pkg callerPkg) { return supervisor.getLaunchOtherAppAllowListOrNull(callerPkg); } + + @SneakyThrows + public void setLockMethod(int method) { + supervisor.setLockMethod(method); + } + + @SneakyThrows + public void setLockPattern(String pattern) { + supervisor.setLockPattern(pattern); + } + + @SneakyThrows + public String getLockPattern() { + return supervisor.getLockPattern(); + } + + @SneakyThrows + public int getLockMethod() { + return supervisor.getLockMethod(); + } } diff --git a/android/android_framework/base/src/main/java/github/tornaco/android/thanos/core/app/activity/IActivityStackSupervisor.aidl b/android/android_framework/base/src/main/java/github/tornaco/android/thanos/core/app/activity/IActivityStackSupervisor.aidl index 972234891..eff585b5c 100644 --- a/android/android_framework/base/src/main/java/github/tornaco/android/thanos/core/app/activity/IActivityStackSupervisor.aidl +++ b/android/android_framework/base/src/main/java/github/tornaco/android/thanos/core/app/activity/IActivityStackSupervisor.aidl @@ -71,4 +71,9 @@ interface IActivityStackSupervisor { void removePkgFromLaunchOtherAppAllowList(in Pkg pkg, in Pkg pkgToRemove); void addPkgToLaunchOtherAppAllowList(in Pkg pkg, in Pkg pkgToAdd); List getLaunchOtherAppAllowListOrNull(in Pkg callerPkg); + + int getLockMethod(); + void setLockMethod(int method); + void setLockPattern(String pattern); + String getLockPattern(); } \ No newline at end of file diff --git a/android/android_framework/base/src/main/java/github/tornaco/android/thanos/core/app/activity/IActivityStackSupervisor.java b/android/android_framework/base/src/main/java/github/tornaco/android/thanos/core/app/activity/IActivityStackSupervisor.java index 1f9681d6a..3577ac82e 100644 --- a/android/android_framework/base/src/main/java/github/tornaco/android/thanos/core/app/activity/IActivityStackSupervisor.java +++ b/android/android_framework/base/src/main/java/github/tornaco/android/thanos/core/app/activity/IActivityStackSupervisor.java @@ -157,6 +157,20 @@ public static class Default implements github.tornaco.android.thanos.core.app.ac { return null; } + @Override public int getLockMethod() throws android.os.RemoteException + { + return 0; + } + @Override public void setLockMethod(int method) throws android.os.RemoteException + { + } + @Override public void setLockPattern(java.lang.String pattern) throws android.os.RemoteException + { + } + @Override public java.lang.String getLockPattern() throws android.os.RemoteException + { + return null; + } @Override public android.os.IBinder asBinder() { return null; @@ -688,6 +702,40 @@ public static github.tornaco.android.thanos.core.app.activity.IActivityStackSupe reply.writeTypedList(_result); return true; } + case TRANSACTION_getLockMethod: + { + data.enforceInterface(descriptor); + int _result = this.getLockMethod(); + reply.writeNoException(); + reply.writeInt(_result); + return true; + } + case TRANSACTION_setLockMethod: + { + data.enforceInterface(descriptor); + int _arg0; + _arg0 = data.readInt(); + this.setLockMethod(_arg0); + reply.writeNoException(); + return true; + } + case TRANSACTION_setLockPattern: + { + data.enforceInterface(descriptor); + java.lang.String _arg0; + _arg0 = data.readString(); + this.setLockPattern(_arg0); + reply.writeNoException(); + return true; + } + case TRANSACTION_getLockPattern: + { + data.enforceInterface(descriptor); + java.lang.String _result = this.getLockPattern(); + reply.writeNoException(); + reply.writeString(_result); + return true; + } default: { return super.onTransact(code, data, reply, flags); @@ -1656,6 +1704,84 @@ public java.lang.String getInterfaceDescriptor() } return _result; } + @Override public int getLockMethod() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getLockMethod, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return getDefaultImpl().getLockMethod(); + } + _reply.readException(); + _result = _reply.readInt(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + @Override public void setLockMethod(int method) throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(method); + boolean _status = mRemote.transact(Stub.TRANSACTION_setLockMethod, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + getDefaultImpl().setLockMethod(method); + return; + } + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public void setLockPattern(java.lang.String pattern) throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(pattern); + boolean _status = mRemote.transact(Stub.TRANSACTION_setLockPattern, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + getDefaultImpl().setLockPattern(pattern); + return; + } + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + @Override public java.lang.String getLockPattern() throws android.os.RemoteException + { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + java.lang.String _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getLockPattern, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return getDefaultImpl().getLockPattern(); + } + _reply.readException(); + _result = _reply.readString(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } public static github.tornaco.android.thanos.core.app.activity.IActivityStackSupervisor sDefaultImpl; } static final int TRANSACTION_checkActivity = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); @@ -1701,6 +1827,10 @@ public java.lang.String getInterfaceDescriptor() static final int TRANSACTION_removePkgFromLaunchOtherAppAllowList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 40); static final int TRANSACTION_addPkgToLaunchOtherAppAllowList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 41); static final int TRANSACTION_getLaunchOtherAppAllowListOrNull = (android.os.IBinder.FIRST_CALL_TRANSACTION + 42); + static final int TRANSACTION_getLockMethod = (android.os.IBinder.FIRST_CALL_TRANSACTION + 43); + static final int TRANSACTION_setLockMethod = (android.os.IBinder.FIRST_CALL_TRANSACTION + 44); + static final int TRANSACTION_setLockPattern = (android.os.IBinder.FIRST_CALL_TRANSACTION + 45); + static final int TRANSACTION_getLockPattern = (android.os.IBinder.FIRST_CALL_TRANSACTION + 46); public static boolean setDefaultImpl(github.tornaco.android.thanos.core.app.activity.IActivityStackSupervisor impl) { // Only one user of this interface can use this function // at a time. This is a heuristic to detect if two different @@ -1764,4 +1894,8 @@ public static github.tornaco.android.thanos.core.app.activity.IActivityStackSupe public void removePkgFromLaunchOtherAppAllowList(github.tornaco.android.thanos.core.pm.Pkg pkg, github.tornaco.android.thanos.core.pm.Pkg pkgToRemove) throws android.os.RemoteException; public void addPkgToLaunchOtherAppAllowList(github.tornaco.android.thanos.core.pm.Pkg pkg, github.tornaco.android.thanos.core.pm.Pkg pkgToAdd) throws android.os.RemoteException; public java.util.List getLaunchOtherAppAllowListOrNull(github.tornaco.android.thanos.core.pm.Pkg callerPkg) throws android.os.RemoteException; + public int getLockMethod() throws android.os.RemoteException; + public void setLockMethod(int method) throws android.os.RemoteException; + public void setLockPattern(java.lang.String pattern) throws android.os.RemoteException; + public java.lang.String getLockPattern() throws android.os.RemoteException; } diff --git a/android/android_framework/res/src/main/res/values-zh-rCN/app_strings.xml b/android/android_framework/res/src/main/res/values-zh-rCN/app_strings.xml index d92ef3e48..d2aeb7c26 100644 --- a/android/android_framework/res/src/main/res/values-zh-rCN/app_strings.xml +++ b/android/android_framework/res/src/main/res/values-zh-rCN/app_strings.xml @@ -18,6 +18,17 @@ 白名单中的组件不会被锁定 组件白名单 + 验证方式 + 跟随系统 + 自定义图案 + 自定义PIN + 设置图案 + 请牢记密码,密码设置后不可找回 + 请绘制图案 + 请再次绘制图案 + 图案不一致 + 图案已设置 + 设置密码 启用应用锁之前,请前往系统安全设置,完成人脸、指纹、PIN或者图案密码设置 diff --git a/android/android_framework/res/src/main/res/values-zh-rTW/app_strings.xml b/android/android_framework/res/src/main/res/values-zh-rTW/app_strings.xml index a0dc2560b..3015e5916 100644 --- a/android/android_framework/res/src/main/res/values-zh-rTW/app_strings.xml +++ b/android/android_framework/res/src/main/res/values-zh-rTW/app_strings.xml @@ -16,6 +16,17 @@ 白名单中的组件不会被锁定 组件白名单 + 验证方式 + 跟随系统 + 自定义图案 + 自定义PIN + 设置图案 + 请牢记密码,密码设置后不可找回 + 请绘制图案 + 请再次绘制图案 + 图案不一致 + 图案已设置 + 设置密码 启用应用锁之前,请前往系统安全设置,完成人脸、指纹、PIN或者图案密码设置 diff --git a/android/android_framework/res/src/main/res/values/app_strings.xml b/android/android_framework/res/src/main/res/values/app_strings.xml index 66380a7d0..a4057bd1d 100644 --- a/android/android_framework/res/src/main/res/values/app_strings.xml +++ b/android/android_framework/res/src/main/res/values/app_strings.xml @@ -18,10 +18,32 @@ When an already unlocked app goes to the background, the app will be re-locked. Activity in Allow list will not be locked Allowed components + Verify method + Follow System + Custom Pattern + Custom PIN + Pattern settings + Please remember your password. Once set, the password cannot be recovered. + Draw pattern + Draw pattern again + Two patterns do not match. + Patterns has been set Set a password Before enabling the app lock, please go to the system security settings and complete the setup of face recognition, fingerprint, PIN, or pattern password. + + @string/module_locker_title_verify_system + @string/module_locker_title_verify_custom_pattern + @string/module_locker_title_verify_custom_pin + + + + 0 + 1 + 2 + + "" "" diff --git a/android/internal/Thanox-Internal b/android/internal/Thanox-Internal index 99f09d5cb..5299a6ab2 160000 --- a/android/internal/Thanox-Internal +++ b/android/internal/Thanox-Internal @@ -1 +1 @@ -Subproject commit 99f09d5cbb50a082cf2e97a0db74f2eb7ade2ec1 +Subproject commit 5299a6ab21ddf64915baaf99ba56c826cf551a76 diff --git a/android/modules/module_locker/src/main/AndroidManifest.xml b/android/modules/module_locker/src/main/AndroidManifest.xml index 626a676d0..1d3767363 100755 --- a/android/modules/module_locker/src/main/AndroidManifest.xml +++ b/android/modules/module_locker/src/main/AndroidManifest.xml @@ -27,6 +27,9 @@ + \ No newline at end of file diff --git a/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/setup/SettingsFragment.java b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/setup/SettingsFragment.java index ffc38b21e..570ccd724 100644 --- a/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/setup/SettingsFragment.java +++ b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/setup/SettingsFragment.java @@ -1,7 +1,9 @@ package github.tornaco.practice.honeycomb.locker.ui.setup; import android.os.Bundle; +import android.text.TextUtils; +import androidx.preference.DropDownPreference; import androidx.preference.Preference; import androidx.preference.SwitchPreferenceCompat; @@ -11,6 +13,7 @@ import github.tornaco.android.thanos.core.app.ThanosManager; import github.tornaco.android.thanos.core.app.activity.ActivityStackSupervisor; import github.tornaco.practice.honeycomb.locker.R; +import github.tornaco.practice.honeycomb.locker.ui.verify.PatternSettingsActivity; public class SettingsFragment extends BasePreferenceFragmentCompat { @@ -30,10 +33,11 @@ public void onResume() { } private void bindPreferences() { + ThanosManager thanos = ThanosManager.from(getContext()); - SwitchPreferenceCompat reVerifyScreenOff = (SwitchPreferenceCompat) findPreference(getString(R.string.module_locker_key_re_verify_on_screen_off)); - SwitchPreferenceCompat reVerifyAppSwitch = (SwitchPreferenceCompat) findPreference(getString(R.string.module_locker_key_re_verify_on_app_switch)); - SwitchPreferenceCompat reVerifyTaskRemoved = (SwitchPreferenceCompat) findPreference(getString(R.string.module_locker_key_re_verify_on_task_removed)); + SwitchPreferenceCompat reVerifyScreenOff = findPreference(getString(R.string.module_locker_key_re_verify_on_screen_off)); + SwitchPreferenceCompat reVerifyAppSwitch = findPreference(getString(R.string.module_locker_key_re_verify_on_app_switch)); + SwitchPreferenceCompat reVerifyTaskRemoved = findPreference(getString(R.string.module_locker_key_re_verify_on_task_removed)); ActivityStackSupervisor supervisor = ThanosManager.from(getContext()).getActivityStackSupervisor(); @@ -59,5 +63,26 @@ private void bindPreferences() { WhiteListComponentViewerActivity.Starter.INSTANCE.start(requireActivity()); return true; }); + + Preference patternSettingsPref = findPreference(getString(R.string.module_locker_key_verify_method_custom_pattern)); + if (TextUtils.isEmpty(thanos.getActivityStackSupervisor().getLockPattern())) { + patternSettingsPref.setSummary(github.tornaco.android.thanos.module.common.R.string.common_text_value_not_set); + } else { + patternSettingsPref.setSummary("******"); + } + patternSettingsPref.setOnPreferenceClickListener(preference -> { + PatternSettingsActivity.start(requireContext()); + return true; + }); + + DropDownPreference methodPref = findPreference(getString(R.string.module_locker_key_verify_method)); + int currentMethod = thanos.getActivityStackSupervisor().getLockMethod(); + methodPref.setValue(String.valueOf(currentMethod)); + methodPref.setOnPreferenceChangeListener((preference, newValue) -> { + int method = Integer.parseInt(String.valueOf(newValue)); + thanos.getActivityStackSupervisor().setLockMethod(method); + return true; + }); + } } diff --git a/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/LockPatternContent.kt b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/LockPatternContent.kt new file mode 100644 index 000000000..5a6076e7a --- /dev/null +++ b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/LockPatternContent.kt @@ -0,0 +1,62 @@ +package github.tornaco.practice.honeycomb.locker.ui.verify + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import github.tornaco.android.thanos.core.pm.AppInfo +import github.tornaco.android.thanos.module.compose.common.widget.AppIcon +import github.tornaco.android.thanos.module.compose.common.widget.LargeTitle +import github.tornaco.android.thanos.module.compose.common.widget.MediumSpacer +import github.tornaco.android.thanos.res.R +import github.tornaco.practice.honeycomb.locker.ui.verify.composelock.ComposeLock +import github.tornaco.practice.honeycomb.locker.ui.verify.composelock.ComposeLockCallback +import github.tornaco.practice.honeycomb.locker.ui.verify.composelock.Dot + +@Composable +fun LockPatternContent( + appInfo: AppInfo, + title: String = stringResource(id = R.string.module_locker_app_name), + onResult: (String) -> Unit +) { + Surface(Modifier.fillMaxSize()) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + AppIcon(modifier = Modifier.size(80.dp), appInfo = appInfo) + MediumSpacer() + LargeTitle(text = title) + BoxWithConstraints(Modifier.padding(top = 64.dp)) { + val width = maxWidth + ComposeLock( + modifier = Modifier + .size(width), + callback = object : ComposeLockCallback { + override fun onStart(dot: Dot) { + } + + override fun onDotConnected(dot: Dot) { + } + + override fun onResult(result: List) { + val patternString = result.joinToString("") { it.id.toString() } + onResult(patternString) + } + }, + dimension = 3 + ) + } + } + + } +} \ No newline at end of file diff --git a/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/PatternSettingsActivity.kt b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/PatternSettingsActivity.kt new file mode 100644 index 000000000..c427045e7 --- /dev/null +++ b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/PatternSettingsActivity.kt @@ -0,0 +1,141 @@ +package github.tornaco.practice.honeycomb.locker.ui.verify + +import android.content.Context +import android.content.Intent +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.elvishew.xlog.XLog +import dagger.hilt.android.AndroidEntryPoint +import github.tornaco.android.thanos.module.compose.common.ComposeThemeActivity +import github.tornaco.android.thanos.module.compose.common.theme.TypographyDefaults +import github.tornaco.android.thanos.module.compose.common.widget.AnimatedTextContainer +import github.tornaco.android.thanos.module.compose.common.widget.LargeTitle +import github.tornaco.android.thanos.module.compose.common.widget.MediumSpacer +import github.tornaco.android.thanos.module.compose.common.widget.ThanoxSmallAppBarScaffold +import github.tornaco.android.thanos.module.compose.common.widget.TipBody +import github.tornaco.android.thanos.res.R +import github.tornaco.practice.honeycomb.locker.ui.verify.composelock.ComposeLock +import github.tornaco.practice.honeycomb.locker.ui.verify.composelock.ComposeLockCallback +import github.tornaco.practice.honeycomb.locker.ui.verify.composelock.Dot + +@AndroidEntryPoint +class PatternSettingsActivity : ComposeThemeActivity() { + + companion object { + @JvmStatic + fun start(context: Context) { + val starter = Intent(context, PatternSettingsActivity::class.java) + context.startActivity(starter) + } + } + + @Composable + override fun Content() { + val context = LocalContext.current + val viewModel = hiltViewModel() + val state by viewModel.state.collectAsState() + + ThanoxSmallAppBarScaffold( + title = { + Text( + text = stringResource(id = R.string.module_locker_title_verify_custom_pattern_settings), + style = TypographyDefaults.appBarTitleTextStyle() + ) + }, + onBackPressed = { + thisActivity().finish() + }, + actions = { + } + ) { paddings -> + LaunchedEffect(viewModel) { + viewModel.events.collect { event -> + when (event) { + PatternSettingsEvent.Done -> { + Toast.makeText( + context, + R.string.module_locker_title_verify_custom_pattern_settings_draw_set_complete, + Toast.LENGTH_SHORT + ).show() + finish() + } + + PatternSettingsEvent.PatternMisMatch -> { + Toast.makeText( + context, + R.string.module_locker_title_verify_custom_pattern_settings_draw_mismatch, + Toast.LENGTH_SHORT + ).show() + } + } + } + } + + Column( + Modifier + .fillMaxSize() + .padding(paddings), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + val tip = when (state.step) { + Step.Done -> { + "" + } + + Step.First -> { + stringResource(id = R.string.module_locker_title_verify_custom_pattern_settings_draw_1) + } + + Step.Second -> { + stringResource(id = R.string.module_locker_title_verify_custom_pattern_settings_draw_2) + } + } + AnimatedTextContainer(text = tip) { + LargeTitle(text = it) + } + MediumSpacer() + TipBody(text = stringResource(id = R.string.module_locker_title_verify_custom_pattern_settings_warn)) + + BoxWithConstraints(Modifier.padding(top = 64.dp)) { + // Get the maximum width of the screen + val width = maxWidth + ComposeLock( + modifier = Modifier + .size(width), + callback = object : ComposeLockCallback { + override fun onStart(dot: Dot) { + } + + override fun onDotConnected(dot: Dot) { + } + + override fun onResult(result: List) { + val patternString = result.joinToString("") { it.id.toString() } + XLog.d("patternString: $patternString") + viewModel.drawPattern(patternString) + } + }, + dimension = 3 + ) + } + } + } + } +} \ No newline at end of file diff --git a/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/PatternSettingsVM.kt b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/PatternSettingsVM.kt new file mode 100644 index 000000000..47f01093b --- /dev/null +++ b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/PatternSettingsVM.kt @@ -0,0 +1,93 @@ +/* + * (C) Copyright 2022 Thanox + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package github.tornaco.practice.honeycomb.locker.ui.verify + +import android.annotation.SuppressLint +import android.content.Context +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import github.tornaco.android.thanos.common.LifeCycleAwareViewModel +import github.tornaco.android.thanos.core.app.ThanosManager +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class PatternState( + val step: Step = Step.First, + val pattern: String = "" +) + +sealed interface Step { + data object First : Step + data object Second : Step + data object Done : Step +} + +sealed interface PatternSettingsEvent { + data object PatternMisMatch : PatternSettingsEvent + data object Done : PatternSettingsEvent +} + +@SuppressLint("StaticFieldLeak") +@HiltViewModel +class PatternSettingsVM @Inject constructor(@ApplicationContext private val context: Context) : + LifeCycleAwareViewModel() { + + private val _state = MutableStateFlow( + PatternState() + ) + val state = _state.asStateFlow() + + val events = MutableSharedFlow() + + private val thanox by lazy { ThanosManager.from(context) } + + fun drawPattern(pattern: String) { + when (state.value.step) { + Step.First -> { + _state.update { + it.copy(pattern = pattern, step = Step.Second) + } + } + + Step.Second -> { + if (pattern != state.value.pattern) { + _state.update { + it.copy(pattern = "", step = Step.First) + } + viewModelScope.launch { events.emit(PatternSettingsEvent.PatternMisMatch) } + } else { + _state.update { + it.copy(step = Step.Done) + } + thanox.activityStackSupervisor.lockPattern = pattern + viewModelScope.launch { events.emit(PatternSettingsEvent.Done) } + } + } + + else -> { + + } + } + + } +} \ No newline at end of file diff --git a/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/VerifyActivity.kt b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/VerifyActivity.kt index 3ef22b3ce..83302c032 100755 --- a/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/VerifyActivity.kt +++ b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/VerifyActivity.kt @@ -1,26 +1,59 @@ package github.tornaco.practice.honeycomb.locker.ui.verify -import android.os.Bundle import androidx.biometric.BiometricPrompt +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import com.elvishew.xlog.XLog import github.tornaco.android.thanos.core.T import github.tornaco.android.thanos.core.app.ThanosManager +import github.tornaco.android.thanos.core.app.activity.ActivityStackSupervisor import github.tornaco.android.thanos.core.app.activity.VerifyResult import github.tornaco.android.thanos.core.pm.AppInfo import github.tornaco.android.thanos.core.util.ConfirmDeviceCredentialUtils -import github.tornaco.android.thanos.theme.ThemeActivity +import github.tornaco.android.thanos.module.compose.common.ComposeThemeActivity -class VerifyActivity : ThemeActivity() { +class VerifyActivity : ComposeThemeActivity() { private var requestCode = 0 private var appInfo: AppInfo? = null private var biometricPrompt: BiometricPrompt? = null - private val thanox get() = ThanosManager.from(thisActivity()) + private val thanox by lazy { ThanosManager.from(thisActivity()) } + private val lockMethod + get() = thanox.activityStackSupervisor.lockMethod.also { + XLog.d("lockMethod: $it") + } + private val lockPattern + get() = thanox.activityStackSupervisor.lockPattern.also { + XLog.d("lockPattern: $it") + } + + private var misMatchTimes = 0 - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (resolveIntent()) { - startVerifyWithBiometrics(appInfo!!) + private fun canUseLockPattern(): Boolean { + return lockMethod == ActivityStackSupervisor.LockMethod.PATTERN && !lockPattern.isNullOrBlank() + } + + @Composable + override fun Content() { + val isValidIntent = remember { resolveIntent() } + if (isValidIntent) { + if (canUseLockPattern()) { + LockPatternContent(appInfo = appInfo!!, onResult = { + if (it == lockPattern) { + verifySuccess() + } else { + if (misMatchTimes > 3) { + verifyFail() + } + misMatchTimes += 1 + } + }) + } else { + LaunchedEffect(Unit) { + startVerifyWithBiometrics(appInfo!!) + } + } } else { XLog.e("VerifyActivity, bad intent.") finish() @@ -33,10 +66,7 @@ class VerifyActivity : ThemeActivity() { requestCode = intent.getIntExtra(T.Actions.ACTION_LOCKER_VERIFY_EXTRA_REQUEST_CODE, Int.MIN_VALUE) appInfo = ThanosManager.from(this).pkgManager.getAppInfo(pkg) - if (appInfo == null) { - return false - } - return true + return appInfo != null } private fun startVerifyWithBiometrics(appInfo: AppInfo) { @@ -56,27 +86,35 @@ class VerifyActivity : ThemeActivity() { biometricPrompt = authenticateWithBiometric(appInfo) { success: Boolean, message: String -> XLog.i("authenticateWithBiometric result: $success $message") if (success) { - XLog.d("setVerifyResult ALLOW") - thanox.activityStackSupervisor.setVerifyResult( - requestCode, - VerifyResult.ALLOW, - VerifyResult.REASON_USER_INPUT_CORRECT - ) - ConfirmDeviceCredentialUtils.checkForPendingIntent(this@VerifyActivity) - setResult(RESULT_OK) - finish() + verifySuccess() } else { - XLog.d("setVerifyResult DENY") - thanox.activityStackSupervisor.setVerifyResult( - requestCode, - VerifyResult.DENY, - VerifyResult.REASON_USER_INPUT_INCORRECT - ) - cancelVerifyAndFinish() + verifyFail() } } } + private fun verifyFail() { + XLog.d("setVerifyResult DENY") + thanox.activityStackSupervisor.setVerifyResult( + requestCode, + VerifyResult.DENY, + VerifyResult.REASON_USER_INPUT_INCORRECT + ) + cancelVerifyAndFinish() + } + + private fun verifySuccess() { + XLog.d("setVerifyResult ALLOW") + thanox.activityStackSupervisor.setVerifyResult( + requestCode, + VerifyResult.ALLOW, + VerifyResult.REASON_USER_INPUT_CORRECT + ) + ConfirmDeviceCredentialUtils.checkForPendingIntent(this@VerifyActivity) + setResult(RESULT_OK) + finish() + } + @Deprecated("Deprecated in Java") override fun onBackPressed() { thanox.activityStackSupervisor.setVerifyResult( diff --git a/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/ComposeLock.kt b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/ComposeLock.kt new file mode 100644 index 000000000..583ceebff --- /dev/null +++ b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/ComposeLock.kt @@ -0,0 +1,177 @@ +package github.tornaco.practice.honeycomb.locker.ui.verify.composelock + +import android.util.Range +import android.view.MotionEvent +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.material.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.input.pointer.pointerInteropFilter +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun ComposeLock( + modifier: Modifier, + callback: ComposeLockCallback, + sensitivity: Float = 100f, + dotsColor: Color = LocalContentColor.current, + dotsSize: Float = 20f, + linesColor: Color = LocalContentColor.current.copy(alpha = 0.8f), + linesStroke: Float = 30f, + dimension: Int = 4, + animationDuration: Int = 200, + animationDelay: Long = 100, +) { + val scope = rememberCoroutineScope() + val dotsList = remember { + mutableListOf() + } + val previewLine = remember { + mutableStateOf(Line(Offset(0f, 0f), Offset(0f, 0f))) + } + val connectedLines = remember { + mutableListOf() + } + val connectedDots = remember { + mutableListOf() + } + + Canvas( + modifier.pointerInteropFilter { + when (it.action) { + MotionEvent.ACTION_DOWN -> { + for (dots in dotsList) { + if ( + it.x in Range( + dots.offset.x - sensitivity, + dots.offset.x + sensitivity + ) && + it.y in Range(dots.offset.y - sensitivity, dots.offset.y + sensitivity) + ) { + connectedDots.add(dots) + callback.onStart(dots) + scope.launch { + dots.size.animateTo( + (dotsSize * 1.8).toFloat(), + tween(animationDuration) + ) + delay(animationDelay) + dots.size.animateTo(dotsSize, tween(animationDuration)) + } + previewLine.value = + previewLine.value.copy(start = Offset(dots.offset.x, dots.offset.y)) + } + } + } + + MotionEvent.ACTION_MOVE -> { + previewLine.value = previewLine.value.copy(end = Offset(it.x, it.y)) + for (dots in dotsList) { + if (!connectedDots.contains(dots)) { + if ( + it.x in Range( + dots.offset.x - sensitivity, + dots.offset.x + sensitivity + ) && + it.y in Range( + dots.offset.y - sensitivity, + dots.offset.y + sensitivity + ) + ) { + connectedLines.add( + Line( + start = previewLine.value.start, + end = dots.offset + ) + ) + connectedDots.add(dots) + callback.onDotConnected(dots) + scope.launch { + dots.size.animateTo( + (dotsSize * 1.8).toFloat(), + tween(animationDuration) + ) + delay(animationDelay) + dots.size.animateTo(dotsSize, tween(animationDuration)) + } + previewLine.value = previewLine.value.copy(start = dots.offset) + } + } + } + } + + MotionEvent.ACTION_UP -> { + previewLine.value = + previewLine.value.copy(start = Offset(0f, 0f), end = Offset(0f, 0f)) + callback.onResult(connectedDots) + connectedLines.clear() + connectedDots.clear() + } + } + true + }) { + val realDimension = dimension + 1 + val spaceBetweenWidthDots = size.width / realDimension + val spaceBetweenHeightDots = size.height / realDimension + val dotsOnWidth = arrayOfNulls(realDimension * realDimension) + val dotsOnHeight = arrayOfNulls(realDimension * realDimension) + dotsOnWidth.forEachIndexed { WidthIndex, _ -> + val readWidthIndex = WidthIndex + 1 + dotsOnHeight.forEachIndexed { HeightIndex, _ -> + val readHeightIndex = HeightIndex + 1 + if (readWidthIndex < realDimension && readHeightIndex < realDimension) { + if (dotsList.count() < dimension * dimension) { + val dotOffset = Offset( + (spaceBetweenWidthDots * readWidthIndex), + (spaceBetweenHeightDots * readHeightIndex) + ) + dotsList.add( + Dot( + dotsList.size + 1, + dotOffset, + Animatable(dotsSize) + ) + ) + } + } + } + } + if (previewLine.value.start != Offset(0f, 0f) && previewLine.value.end != Offset(0f, 0f)) { + drawLine( + color = linesColor, + start = previewLine.value.start, + end = previewLine.value.end, + strokeWidth = linesStroke, + cap = StrokeCap.Round + ) + } + for (dots in dotsList) { + drawCircle( + color = dotsColor, + radius = dots.size.value, + center = dots.offset + ) + } + for (line in connectedLines) { + drawLine( + color = linesColor, + start = line.start, + end = line.end, + strokeWidth = linesStroke, + cap = StrokeCap.Round + ) + } + + } +} \ No newline at end of file diff --git a/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/ComposeLockCallback.kt b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/ComposeLockCallback.kt new file mode 100644 index 000000000..4d7ed93cb --- /dev/null +++ b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/ComposeLockCallback.kt @@ -0,0 +1,7 @@ +package github.tornaco.practice.honeycomb.locker.ui.verify.composelock + +interface ComposeLockCallback { + fun onStart(dot: Dot) + fun onDotConnected(dot: Dot) + fun onResult(result: List) +} \ No newline at end of file diff --git a/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/Dot.kt b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/Dot.kt new file mode 100644 index 000000000..7875eac9f --- /dev/null +++ b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/Dot.kt @@ -0,0 +1,11 @@ +package github.tornaco.practice.honeycomb.locker.ui.verify.composelock + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.ui.geometry.Offset + +data class Dot ( + val id: Int, + val offset: Offset, + val size:Animatable +) \ No newline at end of file diff --git a/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/Line.kt b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/Line.kt new file mode 100644 index 000000000..867e12ffa --- /dev/null +++ b/android/modules/module_locker/src/main/java/github/tornaco/practice/honeycomb/locker/ui/verify/composelock/Line.kt @@ -0,0 +1,8 @@ +package github.tornaco.practice.honeycomb.locker.ui.verify.composelock + +import androidx.compose.ui.geometry.Offset + +data class Line( + var start: Offset, + var end: Offset +) \ No newline at end of file diff --git a/android/modules/module_locker/src/main/res/values/preference_keys.xml b/android/modules/module_locker/src/main/res/values/preference_keys.xml index 8c5de2d68..9d12622dc 100644 --- a/android/modules/module_locker/src/main/res/values/preference_keys.xml +++ b/android/modules/module_locker/src/main/res/values/preference_keys.xml @@ -4,4 +4,6 @@ key_re_verify_on_screen_off key_re_verify_on_task_removed module_locker_key_white_list_components + module_locker_key_verify_method + module_locker_key_verify_method_custom_pattern \ No newline at end of file diff --git a/android/modules/module_locker/src/main/res/xml/module_locker_settings_fragment.xml b/android/modules/module_locker/src/main/res/xml/module_locker_settings_fragment.xml index 514a126cf..57427245b 100644 --- a/android/modules/module_locker/src/main/res/xml/module_locker_settings_fragment.xml +++ b/android/modules/module_locker/src/main/res/xml/module_locker_settings_fragment.xml @@ -1,7 +1,19 @@ - + + +