feat: app lock

This commit is contained in:
rhunk
2024-04-13 22:10:59 +02:00
parent 692bc9309b
commit d630e5c420
8 changed files with 243 additions and 116 deletions

View File

@@ -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" />

View File

@@ -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 {}
}
}

View File

@@ -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>

View File

@@ -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.",

View File

@@ -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()}

View File

@@ -84,7 +84,7 @@ class FeatureManager(
BypassVideoLengthRestriction(),
MediaQualityLevelOverride(),
MeoPasscodeBypass(),
AppPasscode(),
AppLock(),
CameraTweaks(),
InfiniteStoryBoost(),
AmoledDarkMode(),

View File

@@ -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()
}
}

View File

@@ -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?) {}
})
}
}
}