BridgeService.kt (10087B) - raw
1 package me.rhunk.snapenhance.bridge 2 3 import android.app.Service 4 import android.content.Intent 5 import android.os.IBinder 6 import android.os.ParcelFileDescriptor 7 import kotlinx.coroutines.runBlocking 8 import me.rhunk.snapenhance.RemoteSideContext 9 import me.rhunk.snapenhance.SharedContextHolder 10 import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge 11 import me.rhunk.snapenhance.common.data.MessagingFriendInfo 12 import me.rhunk.snapenhance.common.data.MessagingGroupInfo 13 import me.rhunk.snapenhance.common.data.SocialScope 14 import me.rhunk.snapenhance.common.logger.LogLevel 15 import me.rhunk.snapenhance.common.ui.OverlayType 16 import me.rhunk.snapenhance.common.util.toParcelable 17 import me.rhunk.snapenhance.download.DownloadProcessor 18 import me.rhunk.snapenhance.download.FFMpegProcessor 19 import me.rhunk.snapenhance.storage.* 20 import me.rhunk.snapenhance.task.Task 21 import me.rhunk.snapenhance.task.TaskType 22 import java.io.File 23 import java.util.UUID 24 import kotlin.system.measureTimeMillis 25 26 class BridgeService : Service() { 27 private lateinit var remoteSideContext: RemoteSideContext 28 lateinit var syncCallback: SyncCallback 29 var messagingBridge: MessagingBridge? = null 30 31 override fun onDestroy() { 32 if (::remoteSideContext.isInitialized) { 33 remoteSideContext.bridgeService = null 34 } 35 } 36 37 override fun onBind(intent: Intent): IBinder? { 38 remoteSideContext = SharedContextHolder.remote(this).apply { 39 if (checkForRequirements()) return null 40 } 41 remoteSideContext.apply { 42 bridgeService = this@BridgeService 43 } 44 return BridgeBinder() 45 } 46 47 fun triggerScopeSync(scope: SocialScope, id: String, updateOnly: Boolean = false) { 48 runCatching { 49 if (!syncCallback.asBinder().pingBinder()) { 50 remoteSideContext.log.warn("Failed to sync $scope $id: Callback is dead") 51 return 52 } 53 val modDatabase = remoteSideContext.database 54 val syncedObject = when (scope) { 55 SocialScope.FRIEND -> { 56 if (updateOnly && modDatabase.getFriendInfo(id) == null) return 57 syncCallback.syncFriend(id) 58 } 59 SocialScope.GROUP -> { 60 if (updateOnly && modDatabase.getGroupInfo(id) == null) return 61 syncCallback.syncGroup(id) 62 } 63 else -> null 64 } 65 66 if (syncedObject == null) { 67 remoteSideContext.log.warn("Failed to sync $scope $id") 68 return 69 } 70 71 when (scope) { 72 SocialScope.FRIEND -> { 73 toParcelable<MessagingFriendInfo>(syncedObject)?.let { 74 modDatabase.syncFriend(it) 75 } 76 } 77 SocialScope.GROUP -> { 78 toParcelable<MessagingGroupInfo>(syncedObject)?.let { 79 modDatabase.syncGroupInfo(it) 80 } 81 } 82 } 83 }.onFailure { 84 remoteSideContext.log.error("Failed to sync $scope $id", it) 85 } 86 } 87 88 inner class BridgeBinder : BridgeInterface.Stub() { 89 override fun getApplicationApkPath(): String = applicationInfo.publicSourceDir 90 91 override fun broadcastLog(tag: String, level: String, message: String) { 92 remoteSideContext.log.internalLog(tag, LogLevel.fromShortName(level) ?: LogLevel.INFO, message) 93 } 94 override fun enqueueDownload(intent: Intent, callback: DownloadCallback) { 95 DownloadProcessor( 96 remoteSideContext = remoteSideContext, 97 callback = callback 98 ).onReceive(intent) 99 } 100 101 override fun convertMedia( 102 input: ParcelFileDescriptor?, 103 inputExtension: String, 104 outputExtension: String, 105 audioCodec: String?, 106 videoCodec: String? 107 ): ParcelFileDescriptor? { 108 return runBlocking { 109 val taskId = UUID.randomUUID().toString() 110 val inputFile = File.createTempFile(taskId, ".$inputExtension", remoteSideContext.androidContext.cacheDir) 111 112 runCatching { 113 ParcelFileDescriptor.AutoCloseInputStream(input).use { inputStream -> 114 inputFile.outputStream().use { outputStream -> 115 inputStream.copyTo(outputStream) 116 } 117 } 118 }.onFailure { 119 remoteSideContext.log.error("Failed to copy input file", it) 120 inputFile.delete() 121 return@runBlocking null 122 } 123 val cachedFile = File.createTempFile(taskId, ".$outputExtension", remoteSideContext.androidContext.cacheDir) 124 125 val pendingTask = remoteSideContext.taskManager.createPendingTask( 126 Task( 127 type = TaskType.DOWNLOAD, 128 title = "Media conversion", 129 author = null, 130 hash = taskId 131 ) 132 ) 133 runCatching { 134 FFMpegProcessor.newFFMpegProcessor(remoteSideContext, pendingTask).execute( 135 FFMpegProcessor.Request( 136 action = FFMpegProcessor.Action.CONVERSION, 137 inputs = listOf(inputFile.absolutePath), 138 output = cachedFile, 139 videoCodec = videoCodec, 140 audioCodec = audioCodec 141 ) 142 ) 143 pendingTask.success() 144 return@runBlocking ParcelFileDescriptor.open(cachedFile, ParcelFileDescriptor.MODE_READ_ONLY) 145 }.onFailure { 146 pendingTask.fail(it.message ?: "Failed to convert video") 147 remoteSideContext.log.error("Failed to convert video", it) 148 } 149 150 inputFile.delete() 151 cachedFile.delete() 152 null 153 } 154 } 155 156 override fun getRules(uuid: String): List<String> { 157 return remoteSideContext.database.getRules(uuid).map { it.key } 158 } 159 160 override fun getRuleIds(type: String): MutableList<String> { 161 return remoteSideContext.database.getRuleIds(type) 162 } 163 164 override fun setRule(uuid: String, rule: String, state: Boolean) { 165 remoteSideContext.database.setRule(uuid, rule, state) 166 } 167 168 override fun sync(callback: SyncCallback) { 169 syncCallback = callback 170 measureTimeMillis { 171 remoteSideContext.database.getFriends().map { it.userId } .forEach { friendId -> 172 triggerScopeSync(SocialScope.FRIEND, friendId, true) 173 } 174 remoteSideContext.database.getGroups().map { it.conversationId }.forEach { groupId -> 175 triggerScopeSync(SocialScope.GROUP, groupId, true) 176 } 177 }.also { 178 remoteSideContext.log.verbose("Syncing remote took $it ms") 179 } 180 } 181 182 override fun triggerSync(scope: String, id: String) { 183 remoteSideContext.log.verbose("trigger sync for $scope $id") 184 triggerScopeSync(SocialScope.getByName(scope), id, true) 185 } 186 187 override fun passGroupsAndFriends( 188 groups: List<String>, 189 friends: List<String> 190 ) { 191 remoteSideContext.log.verbose("Received ${groups.size} groups and ${friends.size} friends") 192 remoteSideContext.database.receiveMessagingDataCallback( 193 friends.mapNotNull { toParcelable<MessagingFriendInfo>(it) }, 194 groups.mapNotNull { toParcelable<MessagingGroupInfo>(it) } 195 ) 196 } 197 198 override fun getScopeNotes(id: String): String? { 199 return remoteSideContext.database.getScopeNotes(id) 200 } 201 202 override fun setScopeNotes(id: String, content: String?) { 203 remoteSideContext.database.setScopeNotes(id, content) 204 } 205 206 override fun getScriptingInterface() = remoteSideContext.scriptManager 207 208 override fun getE2eeInterface() = remoteSideContext.e2eeImplementation 209 override fun getLogger() = remoteSideContext.messageLogger 210 override fun getTracker() = remoteSideContext.tracker 211 override fun getAccountStorage() = remoteSideContext.accountStorage 212 override fun getFileHandleManager() = remoteSideContext.fileHandleManager 213 override fun getLocationManager() = remoteSideContext.locationManager 214 215 override fun registerMessagingBridge(bridge: MessagingBridge) { 216 messagingBridge = bridge 217 } 218 219 override fun openOverlay(type: String) { 220 runCatching { 221 val overlayType = OverlayType.fromKey(type) ?: throw IllegalArgumentException("Unknown overlay type: $type") 222 remoteSideContext.remoteOverlay.show { routes -> 223 when (overlayType) { 224 OverlayType.SETTINGS -> routes.features 225 OverlayType.BETTER_LOCATION -> routes.betterLocation 226 } 227 } 228 }.onFailure { 229 remoteSideContext.log.error("Failed to open $type overlay", it) 230 } 231 } 232 233 override fun closeOverlay() { 234 runCatching { 235 remoteSideContext.remoteOverlay.close() 236 }.onFailure { 237 remoteSideContext.log.error("Failed to close overlay", it) 238 } 239 } 240 241 override fun registerConfigStateListener(listener: ConfigStateListener) { 242 remoteSideContext.config.configStateListener = listener 243 } 244 245 override fun getDebugProp(key: String, defaultValue: String?): String? { 246 return remoteSideContext.sharedPreferences.all["debug_$key"]?.toString() ?: defaultValue 247 } 248 } 249 }