package me.eternal.purrfect.bridge import android.app.Service import android.content.Intent import android.os.IBinder import android.os.ParcelFileDescriptor import android.os.RemoteException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import me.eternal.purrfect.RemoteSideContext import me.eternal.purrfect.SharedContextHolder import me.eternal.purrfect.common.TargetApp import me.eternal.purrfect.bridge.call.CallDownloadSession import me.eternal.purrfect.bridge.snapclient.MessagingBridge import me.eternal.purrfect.common.data.MessagingFriendInfo import me.eternal.purrfect.common.data.MessagingGroupInfo import me.eternal.purrfect.common.data.SocialScope import me.eternal.purrfect.common.logger.LogLevel import me.eternal.purrfect.common.ui.OverlayType import me.eternal.purrfect.common.util.toParcelable import me.eternal.purrfect.download.DownloadProcessor import me.eternal.purrfect.download.FFMpegProcessor import me.eternal.purrfect.download.call.CallDownloadSessionImpl import me.eternal.purrfect.storage.* import me.eternal.purrfect.task.Task import me.eternal.purrfect.task.TaskType import java.io.File import java.util.UUID import kotlin.system.measureTimeMillis class BridgeService : Service() { private lateinit var remoteSideContext: RemoteSideContext private var syncCallback: SyncCallback? = null var messagingBridge: MessagingBridge? = null @Volatile private var pendingSocialSnapshotCallback: ((List, List) -> Unit)? = null private val isBridgeWarmed = java.util.concurrent.atomic.AtomicBoolean(false) private fun clearSyncCallback() { syncCallback = null } fun requestEphemeralSocialSnapshot(callback: (List, List) -> Unit) { pendingSocialSnapshotCallback = callback } fun clearEphemeralSocialSnapshotRequest() { pendingSocialSnapshotCallback = null } override fun onDestroy() { clearSyncCallback() if (::remoteSideContext.isInitialized) { remoteSideContext.bridgeService = null } } override fun onBind(intent: Intent): IBinder? { remoteSideContext = SharedContextHolder.remote(this).apply { if (checkForRequirements()) return null } remoteSideContext.apply { bridgeService = this@BridgeService } return BridgeBinder() } fun triggerScopeSync(scope: SocialScope, id: String, updateOnly: Boolean = false) { val callback = syncCallback ?: return runCatching { val database = remoteSideContext.database val syncedObject = when (scope) { SocialScope.FRIEND -> { if (updateOnly && database.getFriendInfo(id) == null) return callback.syncFriend(id) } SocialScope.GROUP -> { if (updateOnly && database.getGroupInfo(id) == null) return callback.syncGroup(id) } } ?: run { if (updateOnly) { if (isBridgeWarmed.get()) { when (scope) { SocialScope.FRIEND -> database.deleteFriend(id) SocialScope.GROUP -> database.deleteGroup(id) } } return } remoteSideContext.log.warn("Failed to sync $scope $id") return } when (scope) { SocialScope.FRIEND -> { toParcelable(syncedObject)?.let { database.syncFriend(it) } ?: run { if (updateOnly) { if (isBridgeWarmed.get()) { database.deleteFriend(id) } return } remoteSideContext.log.warn("Failed to sync $scope $id") return } } SocialScope.GROUP -> { toParcelable(syncedObject)?.let { database.syncGroupInfo(it) } ?: run { if (updateOnly) { if (isBridgeWarmed.get()) { database.deleteGroup(id) } return } remoteSideContext.log.warn("Failed to sync $scope $id") return } } } }.onFailure { if (it is RemoteException) { clearSyncCallback() remoteSideContext.log.warn("Failed to sync $scope $id: Callback is dead") return@onFailure } remoteSideContext.log.error("Failed to sync $scope $id", it) } } inner class BridgeBinder : BridgeInterface.Stub() { override fun getApplicationApkPath(): String = applicationInfo.publicSourceDir override fun broadcastLog(tag: String, level: String, message: String) { remoteSideContext.log.internalLog(tag, LogLevel.fromShortName(level) ?: LogLevel.INFO, message) } override fun enqueueDownload(intent: Intent, callback: DownloadCallback) { DownloadProcessor( remoteSideContext = remoteSideContext, callback = callback ).onReceive(intent) } override fun convertMedia( input: ParcelFileDescriptor?, inputExtension: String, outputExtension: String, audioCodec: String?, videoCodec: String? ): ParcelFileDescriptor? { return runBlocking { val taskId = UUID.randomUUID().toString() val inputFile = File.createTempFile(taskId, ".$inputExtension", remoteSideContext.androidContext.cacheDir) runCatching { ParcelFileDescriptor.AutoCloseInputStream(input).use { inputStream -> inputFile.outputStream().use { outputStream -> inputStream.copyTo(outputStream) } } }.onFailure { remoteSideContext.log.error("Failed to copy input file", it) inputFile.delete() return@runBlocking null } val cachedFile = File.createTempFile(taskId, ".$outputExtension", remoteSideContext.androidContext.cacheDir) val pendingTask = remoteSideContext.taskManager.createPendingTask( Task( type = TaskType.DOWNLOAD, title = remoteSideContext.translation["task_media_conversion_title"], author = null, hash = taskId ) ) runCatching { FFMpegProcessor.newFFMpegProcessor(remoteSideContext, pendingTask).execute( FFMpegProcessor.Request( action = FFMpegProcessor.Action.CONVERSION, inputs = listOf(inputFile.absolutePath), output = cachedFile, videoCodec = videoCodec, audioCodec = audioCodec ) ) pendingTask.success() return@runBlocking ParcelFileDescriptor.open(cachedFile, ParcelFileDescriptor.MODE_READ_ONLY) }.onFailure { pendingTask.fail(it.message ?: "Failed to convert video") remoteSideContext.log.error("Failed to convert video", it) } inputFile.delete() cachedFile.delete() null } } override fun getRules(uuid: String): List { return remoteSideContext.database.getRules(uuid).map { it.key } } override fun getRuleIds(type: String): MutableList { return remoteSideContext.database.getRuleIds(type) } override fun setRule(uuid: String, rule: String, state: Boolean) { remoteSideContext.database.setRule(uuid, rule, state) } override fun sync(callback: SyncCallback) { clearSyncCallback() syncCallback = callback remoteSideContext.coroutineScope.launch(Dispatchers.IO) { delay(300) // 300ms Stabilizer: Ensures Binder connection is solid on cold starts isBridgeWarmed.set(true) // Immediate Unblock: Allow UI features to start loading data from cache val time = measureTimeMillis { // Safety Net: Only sync the Top 50 most recently added friends and all groups val activeFriendIds: List = remoteSideContext.database.getFriends(descOrder = true).take(50).map { it.userId } val groupIds: List = remoteSideContext.database.getGroups().map { it.conversationId } // Throttled Group Sync for (groupId in groupIds) { triggerScopeSync(SocialScope.GROUP, groupId, true) delay(50) // High-quality breather to keep IPC pipe clear } // Throttled Friend Sync for (friendId in activeFriendIds) { triggerScopeSync(SocialScope.FRIEND, friendId, true) delay(50) } } remoteSideContext.log.verbose("Background 'Active 50' sync completed in ${time}ms") } } override fun triggerSync(scope: String, id: String) { remoteSideContext.log.verbose("trigger sync for $scope $id") triggerScopeSync(SocialScope.getByName(scope), id, true) } private val friendAccumulator = mutableListOf() private val groupAccumulator = mutableListOf() override fun passGroupsAndFriends( groups: List, friends: List, chunkIndex: Int, totalChunks: Int ) { synchronized(friendAccumulator) { if (chunkIndex == 0) { friendAccumulator.clear() groupAccumulator.clear() } remoteSideContext.log.verbose("Received chunk $chunkIndex/$totalChunks: ${groups.size} groups, ${friends.size} friends") friendAccumulator.addAll(friends.mapNotNull { toParcelable(it) }) groupAccumulator.addAll(groups.mapNotNull { toParcelable(it) }) if (chunkIndex == totalChunks - 1) { val finalFriends = friendAccumulator.toList() val finalGroups = groupAccumulator.toList() friendAccumulator.clear() groupAccumulator.clear() remoteSideContext.coroutineScope.launch(Dispatchers.IO) { pendingSocialSnapshotCallback?.let { callback -> pendingSocialSnapshotCallback = null callback(finalFriends, finalGroups) } remoteSideContext.database.replaceMessagingData(finalFriends, finalGroups) remoteSideContext.database.messagingDataFlow.tryEmit(finalFriends to finalGroups) } } } } override fun getScopeNotes(id: String): String? { return remoteSideContext.database.getScopeNotes(id) } override fun setScopeNotes(id: String, content: String?) { remoteSideContext.database.setScopeNotes(id, content) } override fun getAllScopeNotes(): Map { return remoteSideContext.database.getAllScopeNotes() } override fun setAllScopeNotes(notes: Map) { remoteSideContext.database.setAllScopeNotes(notes) } override fun getScriptingInterface() = remoteSideContext.scriptManager override fun getE2eeInterface() = remoteSideContext.e2eeImplementation override fun getLogger() = remoteSideContext.messageLogger override fun getTracker() = remoteSideContext.tracker override fun getAccountStorage() = remoteSideContext.accountStorage override fun getFileHandleManager() = remoteSideContext.fileHandleManager override fun getLocationManager() = remoteSideContext.locationManager override fun getTaskInterface() = remoteSideContext.taskInterface override fun registerMessagingBridge(bridge: MessagingBridge) { messagingBridge = bridge } override fun openOverlay(type: String) { runCatching { val overlayType = OverlayType.fromKey(type) ?: throw IllegalArgumentException("Unknown overlay type: $type") remoteSideContext.remoteOverlay.show(TargetApp.SNAPCHAT) { routes -> when (overlayType) { OverlayType.SETTINGS -> routes.features OverlayType.BETTER_LOCATION -> routes.betterLocation } } }.onFailure { remoteSideContext.log.error("Failed to open $type overlay", it) } } override fun closeOverlay() { runCatching { remoteSideContext.remoteOverlay.close() }.onFailure { remoteSideContext.log.error("Failed to close overlay", it) } } override fun registerConfigStateListener(listener: ConfigStateListener) { remoteSideContext.config.configStateListener = listener } override fun getDebugProp(key: String, defaultValue: String?): String? { return remoteSideContext.sharedPreferences.all["debug_$key"]?.toString() ?: defaultValue } override fun getRedditFeaturesJson(): String { remoteSideContext.mirrorRedditFeaturePrefs() return remoteSideContext.getRedditFeaturesJson() } override fun getWhatsAppFeaturesJson(): String { remoteSideContext.mirrorWhatsAppFeaturePrefs() return remoteSideContext.getWhatsAppFeaturesJson() } override fun getInstagramFeaturesJson(): String { remoteSideContext.mirrorInstagramFeaturePrefs() return remoteSideContext.getInstagramFeaturesJson() } override fun startCallDownload( startTimestamp: Long, author: String ): CallDownloadSession { return CallDownloadSessionImpl( context = remoteSideContext, callStartTimestamp = startTimestamp, author = author ) } } }