feat: app lock
This commit is contained in:
@@ -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" />
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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.",
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -84,7 +84,7 @@ class FeatureManager(
|
||||
BypassVideoLengthRestriction(),
|
||||
MediaQualityLevelOverride(),
|
||||
MeoPasscodeBypass(),
|
||||
AppPasscode(),
|
||||
AppLock(),
|
||||
CameraTweaks(),
|
||||
InfiniteStoryBoost(),
|
||||
AmoledDarkMode(),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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?) {}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user