commit d630e5c420b96dc0c5b3aea3f039d2860a8784e2
parent 692bc9309b27b9a2d5b3ac081db2e3296ccb7e2b
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Sat, 13 Apr 2024 22:10:59 +0200
feat: app lock
Diffstat:
8 files changed, 245 insertions(+), 117 deletions(-)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+ <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<queries>
<package android:name="com.snapchat.android" />
@@ -59,6 +60,10 @@
android:theme="@android:style/Theme.NoDisplay"
android:excludeFromRecents="true"
android:exported="true" />
+ <activity android:name=".bridge.BiometricPromptActivity"
+ android:theme="@style/BiometricPromptTheme"
+ android:excludeFromRecents="true"
+ android:exported="true" />
<receiver android:name=".messaging.StreaksReminder" />
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BiometricPromptActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BiometricPromptActivity.kt
@@ -0,0 +1,57 @@
+package me.rhunk.snapenhance.bridge
+
+import android.content.Intent
+import android.hardware.biometrics.BiometricManager
+import android.hardware.biometrics.BiometricPrompt
+import android.os.Build
+import android.os.Bundle
+import android.os.CancellationSignal
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import me.rhunk.snapenhance.SharedContextHolder
+import java.util.concurrent.Executors
+
+class BiometricPromptActivity: ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ fun cancel() {
+ setResult(RESULT_CANCELED, Intent())
+ finish()
+ }
+
+ val remoteSideContext = SharedContextHolder.remote(this)
+
+ BiometricPrompt.Builder(this@BiometricPromptActivity)
+ .setTitle(remoteSideContext.translation["biometric_auth.title"])
+ .setSubtitle(remoteSideContext.translation["biometric_auth.subtitle"])
+ .apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.DEVICE_CREDENTIAL)
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ @Suppress("DEPRECATION")
+ setDeviceCredentialAllowed(true)
+ }
+ }
+ .build().authenticate(
+ CancellationSignal().apply {
+ setOnCancelListener {
+ cancel()
+ }
+ },
+ Executors.newSingleThreadExecutor(),
+ object: BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
+ cancel()
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
+ setResult(RESULT_OK, Intent())
+ finish()
+ }
+ }
+ )
+
+ setContent {}
+ }
+}+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
@@ -9,4 +9,11 @@
<item name="android:editTextColor">@color/primaryText</item>
<item name="android:alertDialogTheme">@android:style/Theme.DeviceDefault.Dialog.Alert</item>
</style>
+ <style name="BiometricPromptTheme" parent="AppTheme">
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowTranslucentStatus">true</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:backgroundDimEnabled">false</item>
+ </style>
</resources>
\ No newline at end of file
diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json
@@ -846,13 +846,15 @@
"name": "Edit Messages",
"description": "Allows you to edit messages in conversations"
},
- "app_passcode": {
- "name": "App Passcode",
- "description": "Sets a passcode to lock the app"
- },
- "app_lock_on_resume": {
- "name": "App Lock On Resume",
- "description": "Locks the app when it's reopened"
+ "app_lock": {
+ "name": "App Lock",
+ "description": "Prevents access to Snapchat without a passcode",
+ "properties": {
+ "lock_on_resume": {
+ "name": "Lock On Resume",
+ "description": "Locks the app when it's reopened"
+ }
+ }
},
"infinite_story_boost": {
"name": "Infinite Story Boost",
@@ -1318,6 +1320,12 @@
"notification_text": "You will lose your Streak with {friend} in {hoursLeft} hours"
},
+ "biometric_auth": {
+ "unlock_button": "Unlock",
+ "title": "Unlock Snapchat",
+ "subtitle": "Please authenticate to unlock Snapchat"
+ },
+
"end_to_end_encryption": {
"toolbox": {
"no_shared_key": "You don't have a shared secret with this friend yet. Click below to initiate a new one.",
diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt
@@ -22,6 +22,10 @@ class Experimental : ConfigContainer() {
val autoBackupCurrentAccount = boolean("auto_backup_current_account", defaultValue = true)
}
+ class AppLockConfig: ConfigContainer(hasGlobalState = true) {
+ val lockOnResume = boolean("lock_on_resume", defaultValue = true)
+ }
+
val nativeHooks = container("native_hooks", NativeHooks()) { icon = "Memory"; requireRestart() }
val sessionEvents = container("session_events", SessionEventsConfig()) { requireRestart(); nativeHooks() }
val spoof = container("spoof", Spoof()) { icon = "Fingerprint" ; addNotices(FeatureNotice.BAN_RISK); requireRestart() }
@@ -32,8 +36,7 @@ class Experimental : ConfigContainer() {
val callRecorder = boolean("call_recorder") { requireRestart(); addNotices(FeatureNotice.UNSTABLE); }
val accountSwitcher = container("account_switcher", AccountSwitcherConfig()) { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
val editMessage = boolean("edit_message") { requireRestart(); addNotices(FeatureNotice.BAN_RISK) }
- val appPasscode = string("app_passcode")
- val appLockOnResume = boolean("app_lock_on_resume")
+ val appLock = container("app_lock", AppLockConfig()) { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
val infiniteStoryBoost = boolean("infinite_story_boost")
val meoPasscodeBypass = boolean("meo_passcode_bypass")
val noFriendScoreDelay = boolean("no_friend_score_delay") { requireRestart()}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt
@@ -84,7 +84,7 @@ class FeatureManager(
BypassVideoLengthRestriction(),
MediaQualityLevelOverride(),
MeoPasscodeBypass(),
- AppPasscode(),
+ AppLock(),
CameraTweaks(),
InfiniteStoryBoost(),
AmoledDarkMode(),
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AppLock.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AppLock.kt
@@ -0,0 +1,153 @@
+package me.rhunk.snapenhance.core.features.impl.experiments
+
+import android.app.Activity
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.Shape
+import android.view.View
+import android.widget.FrameLayout
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Lock
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.unit.dp
+import me.rhunk.snapenhance.common.Constants
+import me.rhunk.snapenhance.common.ui.AppMaterialTheme
+import me.rhunk.snapenhance.common.ui.createComposeView
+import me.rhunk.snapenhance.core.event.events.impl.ActivityResultEvent
+import me.rhunk.snapenhance.core.features.Feature
+import me.rhunk.snapenhance.core.features.FeatureLoadParams
+import me.rhunk.snapenhance.core.ui.addForegroundDrawable
+import me.rhunk.snapenhance.core.ui.children
+import me.rhunk.snapenhance.core.ui.removeForegroundDrawable
+import me.rhunk.snapenhance.core.util.hook.HookStage
+import me.rhunk.snapenhance.core.util.hook.hook
+import kotlin.random.Random
+
+class AppLock : Feature("AppLock", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
+ private var isUnlockRequested = false
+
+ private val rootContentView get() = context.mainActivity!!.findViewById<FrameLayout>(android.R.id.content)
+ private val requestCode = Random.nextInt(100, 65535)
+
+ private fun hideRootView() {
+ rootContentView.addForegroundDrawable("locked_overlay", ShapeDrawable(object: Shape() {
+ override fun draw(canvas: Canvas, paint: Paint) {
+ paint.color = 0xFF000000.toInt()
+ canvas.drawRect(0F, 0F, canvas.width.toFloat(), canvas.height.toFloat(), paint)
+ }
+ }))
+ }
+
+ private fun requestUnlock() {
+ isUnlockRequested = true
+ context.mainActivity!!.startActivityForResult(Intent().apply {
+ component = ComponentName(Constants.SE_PACKAGE_NAME, "me.rhunk.snapenhance.bridge.BiometricPromptActivity")
+ }, requestCode)
+ }
+
+ private fun lock(prompt: Boolean = true) {
+ isUnlockRequested = true
+ hideRootView()
+
+ val lockedView = rootContentView.findViewWithTag<View>("locked_view") ?: createComposeView(rootContentView.context) {
+ AppMaterialTheme(isDarkTheme = true) {
+ Surface(
+ color = MaterialTheme.colorScheme.surface,
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Image(
+ imageVector = Icons.Default.Lock,
+ contentDescription = "Lock",
+ modifier = Modifier.size(100.dp),
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface)
+ )
+ Button(onClick = {
+ requestUnlock()
+ }) {
+ Text(remember { context.translation["biometric_auth.unlock_button"] })
+ }
+ }
+ }
+ }
+ }
+ }.apply {
+ tag = "locked_view"
+ layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
+ rootContentView.addView(this)
+ }
+
+ rootContentView.postDelayed({
+ rootContentView.children().forEach { it.visibility = View.GONE }
+ lockedView.visibility = View.VISIBLE
+ rootContentView.removeForegroundDrawable("locked_overlay")
+ }, 500)
+
+ if (prompt) {
+ requestUnlock()
+ }
+ }
+
+ private fun unlock() {
+ rootContentView.apply {
+ removeForegroundDrawable("locked_overlay")
+ children().forEach { it.visibility = View.VISIBLE }
+ visibility = View.VISIBLE
+ findViewWithTag<View>("locked_view")?.visibility = View.GONE
+ postDelayed({
+ isUnlockRequested = false
+ }, 1000)
+ }
+ }
+
+ override fun onActivityCreate() {
+ if (context.config.experimental.appLock.globalState != true) return
+
+ Activity::class.java.apply {
+ if (context.config.experimental.appLock.lockOnResume.get()) {
+ hook("onResume", HookStage.BEFORE) { param ->
+ if (param.thisObject<Activity>().packageName != Constants.SNAPCHAT_PACKAGE_NAME) return@hook
+ if (isUnlockRequested) return@hook
+ lock(prompt = true)
+ }
+ hook("onPause", HookStage.BEFORE) { param ->
+ if (param.thisObject<Activity>().packageName != Constants.SNAPCHAT_PACKAGE_NAME) return@hook
+ if (isUnlockRequested) return@hook
+ hideRootView()
+ }
+ }
+ }
+
+ context.event.subscribe(ActivityResultEvent::class) { event ->
+ if (event.requestCode != requestCode) return@subscribe
+ if (event.resultCode == Activity.RESULT_OK) {
+ unlock()
+ return@subscribe
+ }
+ lock(prompt = false)
+ }
+ lock()
+ }
+}+
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AppPasscode.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/AppPasscode.kt
@@ -1,106 +0,0 @@
-package me.rhunk.snapenhance.core.features.impl.experiments
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.os.Build
-import android.text.Editable
-import android.text.InputType
-import android.text.TextWatcher
-import android.view.inputmethod.InputMethodManager
-import android.widget.EditText
-import me.rhunk.snapenhance.core.features.Feature
-import me.rhunk.snapenhance.core.features.FeatureLoadParams
-import me.rhunk.snapenhance.core.ui.ViewAppearanceHelper
-
-//TODO: fingerprint unlock
-class AppPasscode : Feature("App Passcode", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
- private var isLocked = false
-
- private fun setActivityVisibility(isVisible: Boolean) {
- context.mainActivity?.let {
- it.window.attributes = it.window.attributes.apply { alpha = if (isVisible) 1.0F else 0.0F }
- }
- }
-
- fun lock() {
- if (isLocked) return
- isLocked = true
- val passcode by context.config.experimental.appPasscode.also {
- if (it.getNullable()?.isEmpty() != false) return
- }
- val isDigitPasscode = passcode.all { it.isDigit() }
-
- val mainActivity = context.mainActivity!!
- setActivityVisibility(false)
-
- val prompt = ViewAppearanceHelper.newAlertDialogBuilder(mainActivity)
- val createPrompt = {
- val alertDialog = prompt.create()
- val textView = EditText(mainActivity)
- textView.setSingleLine()
- textView.inputType = if (isDigitPasscode) {
- (InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD)
- } else {
- (InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD)
- }
- textView.hint = "Code :"
- textView.setPadding(100, 100, 100, 100)
-
- textView.addTextChangedListener(object: TextWatcher {
- override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
- if (s.contentEquals(passcode)) {
- alertDialog.dismiss()
- isLocked = false
- setActivityVisibility(true)
- }
- }
- override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
- override fun afterTextChanged(s: Editable?) {}
- })
-
- alertDialog.setView(textView)
-
- textView.viewTreeObserver.addOnWindowFocusChangeListener { hasFocus ->
- if (!hasFocus) return@addOnWindowFocusChangeListener
- val imm = mainActivity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
- imm.showSoftInput(textView, InputMethodManager.SHOW_IMPLICIT)
- }
-
- alertDialog.window?.let {
- it.attributes.verticalMargin = -0.18F
- }
-
- alertDialog.show()
- textView.requestFocus()
- }
-
- prompt.setOnCancelListener {
- createPrompt()
- }
-
- createPrompt()
- }
-
- @SuppressLint("MissingPermission")
- override fun onActivityCreate() {
- if (!context.database.hasArroyo()) return
-
- context.runOnUiThread {
- lock()
- }
-
- if (!context.config.experimental.appLockOnResume.get()) return
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- context.mainActivity?.registerActivityLifecycleCallbacks(object: android.app.Application.ActivityLifecycleCallbacks {
- override fun onActivityPaused(activity: android.app.Activity) { lock() }
- override fun onActivityResumed(activity: android.app.Activity) {}
- override fun onActivityStarted(activity: android.app.Activity) {}
- override fun onActivityDestroyed(activity: android.app.Activity) {}
- override fun onActivitySaveInstanceState(activity: android.app.Activity, outState: android.os.Bundle) {}
- override fun onActivityStopped(activity: android.app.Activity) {}
- override fun onActivityCreated(activity: android.app.Activity, savedInstanceState: android.os.Bundle?) {}
- })
- }
- }
-}-
\ No newline at end of file