commit a49447ef64a729073cdb42137847e0a9a70b793f
parent cd04fd047724f433d8273aa2c29e152d4032cc6a
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sat,  1 Jun 2024 16:23:54 +0200

refactor: bridge

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt | 2+-
Mapp/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt | 4++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/social/MessagingPreview.kt | 32++++++++++++++++++++++----------
Mcommon/src/main/assets/lang/en_US.json | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/ModContext.kt | 2++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt | 105+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt | 163++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/messaging/Messaging.kt | 10++++++++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/util/LSPatchUpdater.kt | 5++---
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") }