commit a49447ef64a729073cdb42137847e0a9a70b793f
parent cd04fd047724f433d8273aa2c29e152d4032cc6a
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Sat, 1 Jun 2024 16:23:54 +0200
refactor: bridge
Diffstat:
9 files changed, 208 insertions(+), 119 deletions(-)
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt
@@ -186,7 +186,7 @@ class RemoteSideContext(
log.debug(message.toString())
}
- fun hasMessagingBridge() = bridgeService != null && bridgeService?.messagingBridge != null
+ fun hasMessagingBridge() = bridgeService != null && bridgeService?.messagingBridge != null && bridgeService?.messagingBridge?.asBinder()?.pingBinder() == true
fun checkForRequirements(overrideRequirements: Int? = null): Boolean {
var requirements = overrideRequirements ?: 0
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt
@@ -45,6 +45,10 @@ class BridgeService : Service() {
fun triggerScopeSync(scope: SocialScope, id: String, updateOnly: Boolean = false) {
runCatching {
+ if (!syncCallback.asBinder().pingBinder()) {
+ remoteSideContext.log.warn("Failed to sync $scope $id: Callback is dead")
+ return
+ }
val modDatabase = remoteSideContext.database
val syncedObject = when (scope) {
SocialScope.FRIEND -> {
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/social/MessagingPreview.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/social/MessagingPreview.kt
@@ -44,10 +44,10 @@ import me.rhunk.snapenhance.ui.util.Dialog
class MessagingPreview: Routes.Route() {
private lateinit var coroutineScope: CoroutineScope
- private lateinit var messagingBridge: MessagingBridge
private lateinit var previewScrollState: LazyListState
private val contentTypeTranslation by lazy { context.translation.getCategory("content_type") }
+ private val messagingBridge: MessagingBridge? get() = context.bridgeService?.messagingBridge
private var messages = mutableStateListOf<Message>()
private var conversationId by mutableStateOf<String?>(null)
@@ -194,10 +194,14 @@ class MessagingPreview: Routes.Route() {
}
fun launchMessagingTask(taskType: MessagingTaskType, constraints: List<MessagingTaskConstraint> = listOf(), onSuccess: (Message) -> Unit = {}) {
+ if (messagingBridge == null) {
+ context.longToast(translation["bridge_connection_failed"])
+ return
+ }
taskSelectionDropdown = false
processMessageCount.intValue = 0
activeTask = MessagingTask(
- messagingBridge, conversationId!!, taskType, constraints,
+ messagingBridge!!, conversationId!!, taskType, constraints,
overrideClientMessageIds = selectedMessages.takeIf { it.isNotEmpty() }?.toList(),
processedMessageCount = processMessageCount,
onSuccess = onSuccess,
@@ -299,15 +303,23 @@ class MessagingPreview: Routes.Route() {
else selectConstraintsDialog = true
}
ActionButton(text = translation[if (hasSelection) "mark_selection_as_seen_option" else "mark_all_as_seen_option"], icon = Icons.Rounded.RemoveRedEye) {
+ if (messagingBridge == null) {
+ context.longToast(translation["bridge_connection_failed"])
+ return@ActionButton
+ }
launchMessagingTask(
MessagingTaskType.READ, listOf(
- MessagingConstraints.NO_USER_ID(messagingBridge.myUserId),
+ MessagingConstraints.NO_USER_ID(messagingBridge!!.myUserId),
MessagingConstraints.CONTENT_TYPE(arrayOf(ContentType.SNAP))
))
runCurrentTask()
}
ActionButton(text = translation[if (hasSelection) "delete_selection_option" else "delete_all_option"], icon = Icons.Rounded.DeleteForever) {
- launchMessagingTask(MessagingTaskType.DELETE, listOf(MessagingConstraints.USER_ID(messagingBridge.myUserId), {
+ if (messagingBridge == null) {
+ context.longToast(translation["bridge_connection_failed"])
+ return@ActionButton
+ }
+ launchMessagingTask(MessagingTaskType.DELETE, listOf(MessagingConstraints.USER_ID(messagingBridge!!.myUserId), {
contentType != ContentType.STATUS.id
})) { message ->
coroutineScope.launch {
@@ -426,7 +438,7 @@ class MessagingPreview: Routes.Route() {
fun fetchNewMessages() {
coroutineScope.launch(Dispatchers.IO) cs@{
runCatching {
- val queriedMessages = messagingBridge.fetchConversationWithMessagesPaginated(
+ val queriedMessages = messagingBridge!!.fetchConversationWithMessagesPaginated(
conversationId!!,
20,
lastMessageId
@@ -447,18 +459,18 @@ class MessagingPreview: Routes.Route() {
context.log.verbose("onMessagingBridgeReady: $scope $scopeId")
runCatching {
- messagingBridge = context.bridgeService!!.messagingBridge!!
- conversationId = (if (scope == SocialScope.FRIEND) messagingBridge.getOneToOneConversationId(scopeId) else scopeId) ?: throw IllegalStateException("Failed to get conversation id")
- if (runCatching { !messagingBridge.isSessionStarted }.getOrDefault(true)) {
+ conversationId = (if (scope == SocialScope.FRIEND) messagingBridge!!.getOneToOneConversationId(scopeId) else scopeId) ?: throw IllegalStateException("Failed to get conversation id")
+ if (runCatching { !messagingBridge!!.isSessionStarted }.getOrDefault(true)) {
context.androidContext.packageManager.getLaunchIntentForPackage(
Constants.SNAPCHAT_PACKAGE_NAME
)?.let {
- val mainIntent = Intent.makeRestartActivityTask(it.component).apply {
+ val mainIntent = Intent.makeMainActivity(it.component).apply {
putExtra(ReceiversConfig.MESSAGING_PREVIEW_EXTRA, true)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.androidContext.startActivity(mainIntent)
}
- messagingBridge.registerSessionStartListener(object: SessionStartListener.Stub() {
+ messagingBridge!!.registerSessionStartListener(object: SessionStartListener.Stub() {
override fun onConnected() {
fetchNewMessages()
}
diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json
@@ -109,8 +109,8 @@
"save_from_cache_button": "Save from Cache"
},
"messaging_preview": {
- "bridge_connection_failed": "Failed to connect to Snapchat through bridge service",
- "bridge_init_failed": "Failed to initialize messaging bridge",
+ "bridge_connection_failed": "Failed to connect to bridge. Make sure Snapchat is running in the background",
+ "bridge_init_failed": "Failed to initialize messaging bridge. Make sure Snapchat is running in the background",
"message_fetch_failed": "Failed to fetch messages",
"no_message_hint": "No message",
"save_selection_option": "Save Selection",
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt
@@ -163,4 +163,6 @@ class ModContext(
fun getConfigLocale(): String {
return _config.locale
}
+
+ fun isLoggedIn() = androidContext.getSharedPreferences("user_session_shared_pref", 0).getString("key_user_id", null) != null
}
\ No newline at end of file
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt
@@ -27,6 +27,7 @@ import me.rhunk.snapenhance.core.util.LSPatchUpdater
import me.rhunk.snapenhance.core.util.hook.HookAdapter
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
+import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis
@@ -56,44 +57,55 @@ class SnapEnhance {
)
appContext.apply {
bridgeClient = BridgeClient(this)
- bridgeClient.apply {
- connect(
- onFailure = {
- InAppOverlay.showCrashOverlay(
- "Snapchat can't connect to the SnapEnhance app. Make sure you have the latest version installed on your device. You can download the latest stable version on github.com/rhunk/SnapEnhance",
- throwable = it
- )
- }
- ) { bridgeResult ->
- if (!bridgeResult) {
- InAppOverlay.showCrashOverlay(
- "Snapchat timed out while trying to connect to the SnapEnhance app. Make sure you have disabled any battery optimizations for SnapEnhance."
- )
- logCritical("Cannot connect to the SnapEnhance app")
- return@connect
- }
+ initConfigListener()
+ bridgeClient.addOnConnectedCallback {
+ bridgeClient.registerMessagingBridge(messagingBridge)
+ coroutineScope.launch {
runCatching {
- LSPatchUpdater.onBridgeConnected(appContext, bridgeClient)
+ syncRemote()
}.onFailure {
- log.error("Failed to init LSPatchUpdater", it)
- }
- runCatching {
- measureTimeMillis {
- runBlocking {
- init(this)
- }
- }.also {
- appContext.log.verbose("init took ${it}ms")
- }
- }.onSuccess {
- isBridgeInitialized = true
- }.onFailure {
- logCritical("Failed to initialize bridge", it)
- InAppOverlay.showCrashOverlay("SnapEnhance failed to initialize. Please check logs for more details.", it)
+ log.error("Failed to sync remote", it)
}
}
}
}
+
+ runBlocking {
+ var throwable: Throwable? = null
+ val canLoad = appContext.bridgeClient.connect { throwable = it }
+ if (canLoad == null) {
+ InAppOverlay.showCrashOverlay(
+ buildString {
+ append("Snapchat timed out while trying to connect to SnapEnhance\n\n")
+ append("Make sure you:\n")
+ append(" - Have installed the latest SnapEnhance version (https://github.com/rhunk/SnapEnhance)\n")
+ append(" - Disabled battery optimizations\n")
+ append(" - Excluded SnapEnhance and Snapchat in HideMyApplist")
+ },
+ throwable
+ )
+ appContext.logCritical("Cannot connect to the SnapEnhance app")
+ return@runBlocking
+ }
+ if (!canLoad) exitProcess(1)
+ runCatching {
+ LSPatchUpdater.onBridgeConnected(appContext)
+ }.onFailure {
+ appContext.log.error("Failed to init LSPatchUpdater", it)
+ }
+ runCatching {
+ measureTimeMillis {
+ init(this)
+ }.also {
+ appContext.log.verbose("init took ${it}ms")
+ }
+ }.onSuccess {
+ isBridgeInitialized = true
+ }.onFailure {
+ appContext.logCritical("Failed to initialize bridge", it)
+ InAppOverlay.showCrashOverlay("SnapEnhance failed to initialize. Please check logs for more details.", it)
+ }
+ }
}
hookMainActivity("onCreate") {
@@ -137,15 +149,9 @@ class SnapEnhance {
}
reloadConfig()
- initConfigListener()
initWidgetListener()
initNative()
scope.launch(Dispatchers.IO) {
- runCatching {
- syncRemote()
- }.onFailure {
- log.error("Failed to sync remote", it)
- }
translation.userLocale = getConfigLocale()
translation.load()
}
@@ -155,7 +161,6 @@ class SnapEnhance {
eventDispatcher.init()
//if mappings aren't loaded, we can't initialize features
if (!mappings.isMappingsLoaded) return
- bridgeClient.registerMessagingBridge(messagingBridge)
features.init()
scriptRuntime.connect(bridgeClient.getScriptingInterface())
scriptRuntime.eachModule { callFunction("module.onSnapApplicationLoad", androidContext) }
@@ -178,7 +183,7 @@ class SnapEnhance {
private fun initNative() {
// don't initialize native when not logged in
if (
- appContext.androidContext.getSharedPreferences("user_session_shared_pref", 0).getString("key_user_id", null) == null &&
+ !appContext.isLoggedIn() &&
appContext.bridgeClient.getDebugProp("force_native_load", null) != "true"
) return
if (appContext.config.experimental.nativeHooks.globalState != true) return
@@ -219,27 +224,27 @@ class SnapEnhance {
}
}
- appContext.executeAsync {
- bridgeClient.registerConfigStateListener(object: ConfigStateListener.Stub() {
+ appContext.bridgeClient.addOnConnectedCallback {
+ appContext.bridgeClient.registerConfigStateListener(object: ConfigStateListener.Stub() {
override fun onConfigChanged() {
- log.verbose("onConfigChanged")
- reloadConfig()
+ appContext.log.verbose("onConfigChanged")
+ appContext.reloadConfig()
}
override fun onRestartRequired() {
- log.verbose("onRestartRequired")
+ appContext.log.verbose("onRestartRequired")
runLater {
- log.verbose("softRestart")
- softRestartApp(saveSettings = false)
+ appContext.log.verbose("softRestart")
+ appContext.softRestartApp(saveSettings = false)
}
}
override fun onCleanCacheRequired() {
- log.verbose("onCleanCacheRequired")
+ appContext.log.verbose("onCleanCacheRequired")
tasks.clear()
runLater {
- log.verbose("cleanCache")
- actionManager.execute(EnumAction.CLEAN_CACHE)
+ appContext.log.verbose("cleanCache")
+ appContext.actionManager.execute(EnumAction.CLEAN_CACHE)
}
}
})
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt
@@ -5,9 +5,18 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
-import android.os.*
+import android.os.Build
+import android.os.DeadObjectException
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.IBinder
+import android.os.ParcelFileDescriptor
+import android.os.Process
import android.util.Log
import de.robv.android.xposed.XposedHelpers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeoutOrNull
import me.rhunk.snapenhance.bridge.AccountStorage
import me.rhunk.snapenhance.bridge.BridgeInterface
import me.rhunk.snapenhance.bridge.ConfigStateListener
@@ -26,83 +35,135 @@ import me.rhunk.snapenhance.common.data.MessagingRuleType
import me.rhunk.snapenhance.common.data.SocialScope
import me.rhunk.snapenhance.common.util.toSerialized
import me.rhunk.snapenhance.core.ModContext
-import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
-import java.util.concurrent.TimeUnit
-import kotlin.system.exitProcess
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
class BridgeClient(
private val context: ModContext
): ServiceConnection {
- private lateinit var future: CompletableFuture<Boolean>
+ private var continuation: Continuation<Boolean>? = null
private lateinit var service: BridgeInterface
- fun connect(onFailure: (Throwable) -> Unit, onResult: (Boolean) -> Unit) {
- this.future = CompletableFuture()
+ private val onConnectedCallbacks = mutableListOf<suspend () -> Unit>()
- with(context.androidContext) {
- runCatching {
- startActivity(Intent()
- .setClassName(Constants.SE_PACKAGE_NAME, "me.rhunk.snapenhance.bridge.ForceStartActivity")
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
- )
- }
+ fun addOnConnectedCallback(callback: suspend () -> Unit) {
+ synchronized(onConnectedCallbacks) {
+ onConnectedCallbacks.add(callback)
+ }
+ }
+
+ suspend fun connect(onFailure: (Throwable) -> Unit): Boolean? {
+ if (this::service.isInitialized && service.asBinder().pingBinder()) {
+ return true
+ }
- //ensure the remote process is running
- runCatching {
- val intent = Intent()
- .setClassName(Constants.SE_PACKAGE_NAME,"me.rhunk.snapenhance.bridge.BridgeService")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- bindService(
- intent,
- Context.BIND_AUTO_CREATE,
- Executors.newSingleThreadExecutor(),
- this@BridgeClient
- )
- } else {
- XposedHelpers.callMethod(
- this,
- "bindServiceAsUser",
- intent,
- this@BridgeClient,
- Context.BIND_AUTO_CREATE,
- Handler(HandlerThread("BridgeClient").apply {
- start()
- }.looper),
- android.os.Process.myUserHandle()
- )
+ return withTimeoutOrNull(5000L) {
+ suspendCancellableCoroutine { cancellableContinuation ->
+ continuation = cancellableContinuation
+ with(context.androidContext) {
+ //ensure the remote process is running
+ runCatching {
+ startActivity(Intent()
+ .setClassName(Constants.SE_PACKAGE_NAME, "me.rhunk.snapenhance.bridge.ForceStartActivity")
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
+ )
+ }
+
+ runCatching {
+ val intent = Intent()
+ .setClassName(Constants.SE_PACKAGE_NAME, "me.rhunk.snapenhance.bridge.BridgeService")
+ runCatching {
+ if (this@BridgeClient::service.isInitialized) {
+ unbindService(this@BridgeClient)
+ }
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ bindService(
+ intent,
+ Context.BIND_AUTO_CREATE,
+ Executors.newSingleThreadExecutor(),
+ this@BridgeClient
+ )
+ } else {
+ XposedHelpers.callMethod(
+ this,
+ "bindServiceAsUser",
+ intent,
+ this@BridgeClient,
+ Context.BIND_AUTO_CREATE,
+ Handler(HandlerThread("BridgeClient").apply {
+ start()
+ }.looper),
+ Process.myUserHandle()
+ )
+ }
+ }.onFailure {
+ onFailure(it)
+ continuation = null
+ cancellableContinuation.resume(false)
+ }
}
- onResult(future.get(15, TimeUnit.SECONDS))
- }.onFailure {
- onFailure(it)
}
}
}
-
override fun onServiceConnected(name: ComponentName, service: IBinder) {
this.service = BridgeInterface.Stub.asInterface(service)
- future.complete(true)
+ runBlocking {
+ onConnectedCallbacks.forEach {
+ runCatching {
+ it()
+ }.onFailure {
+ context.log.error("Failed to run onConnectedCallback", it)
+ }
+ }
+ }
+ continuation?.resume(true)
+ continuation = null
}
override fun onNullBinding(name: ComponentName) {
- context.log.error("BridgeClient", "failed to connect to bridge service")
- exitProcess(1)
+ Log.d("BridgeClient", "bridge null binding")
+ continuation?.resume(false)
+ continuation = null
}
override fun onServiceDisconnected(name: ComponentName) {
- exitProcess(0)
+ continuation = null
+ }
+
+ private fun tryReconnect() {
+ runBlocking {
+ Log.d("BridgeClient", "service is dead, restarting")
+ val canLoad = connect {
+ Log.e("BridgeClient", "connection failed", it)
+ context.softRestartApp()
+ }
+ if (canLoad != true) {
+ Log.e("BridgeClient", "failed to reconnect to service", Throwable())
+ context.softRestartApp()
+ }
+ }
}
private fun <T> safeServiceCall(block: () -> T): T {
return runCatching {
block()
- }.getOrElse {
- Log.e("SnapEnhance", "service call failed", it)
- if (it is DeadObjectException) {
- context.softRestartApp()
+ }.getOrElse { throwable ->
+ if (throwable is DeadObjectException) {
+ tryReconnect()
+ return@getOrElse runCatching {
+ block()
+ }.getOrElse {
+ Log.e("BridgeClient", "service call failed", it)
+ if (it is DeadObjectException) {
+ context.softRestartApp()
+ }
+ throw it
+ }
}
- throw it
+ throw throwable
}
}
@@ -180,5 +241,5 @@ class BridgeClient(
fun registerConfigStateListener(listener: ConfigStateListener) = safeServiceCall { service.registerConfigStateListener(listener) }
- fun getDebugProp(name: String, defaultValue: String? = null) = safeServiceCall { service.getDebugProp(name, defaultValue) }
+ fun getDebugProp(name: String, defaultValue: String? = null): String? = safeServiceCall { service.getDebugProp(name, defaultValue) }
}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt
@@ -1,5 +1,8 @@
package me.rhunk.snapenhance.core.features.impl.messaging
+import android.content.ComponentName
+import android.content.Intent
+import me.rhunk.snapenhance.common.Constants
import me.rhunk.snapenhance.common.ReceiversConfig
import me.rhunk.snapenhance.core.event.events.impl.ConversationUpdateEvent
import me.rhunk.snapenhance.core.event.events.impl.OnSnapInteractionEvent
@@ -48,8 +51,11 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
context.classCache.conversationManager.hookConstructor(HookStage.BEFORE) { param ->
conversationManager = ConversationManager(context, param.thisObject())
context.messagingBridge.triggerSessionStart()
- context.mainActivity?.takeIf { it.intent.getBooleanExtra(ReceiversConfig.MESSAGING_PREVIEW_EXTRA,false) }?.run {
- finishAndRemoveTask()
+ context.mainActivity?.takeIf { it.intent.getBooleanExtra(ReceiversConfig.MESSAGING_PREVIEW_EXTRA, false) }?.run {
+ startActivity(Intent().apply {
+ setComponent(ComponentName(Constants.SE_PACKAGE_NAME, "me.rhunk.snapenhance.ui.manager.MainActivity"))
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ })
}
}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/LSPatchUpdater.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/LSPatchUpdater.kt
@@ -2,7 +2,6 @@ package me.rhunk.snapenhance.core.util
import me.rhunk.snapenhance.common.Constants
import me.rhunk.snapenhance.core.ModContext
-import me.rhunk.snapenhance.core.bridge.BridgeClient
import java.io.File
import java.util.zip.ZipFile
@@ -20,7 +19,7 @@ object LSPatchUpdater {
.toString(16)
}
- fun onBridgeConnected(context: ModContext, bridgeClient: BridgeClient) {
+ fun onBridgeConnected(context: ModContext) {
val obfuscatedModulePath by lazy {
(runCatching {
context::class.java.classLoader?.loadClass("org.lsposed.lspatch.share.Constants")
@@ -44,7 +43,7 @@ object LSPatchUpdater {
HAS_LSPATCH = true
context.log.verbose("Found embedded SE at ${embeddedModule.absolutePath}", TAG)
- val seAppApk = File(bridgeClient.getApplicationApkPath()).also {
+ val seAppApk = File(context.bridgeClient.getApplicationApkPath()).also {
if (!it.canRead()) {
throw IllegalStateException("Cannot read SnapEnhance apk")
}