From 92a4b4f54570981ae4f20759fd5e1207e9db2c9f Mon Sep 17 00:00:00 2001 From: CAtlas0784 Date: Sun, 1 Nov 2026 14:16:22 +0700 Subject: [PATCH] fix: resolve service crash on start by fixing foreground service type check and notification icon --- .../java/com/gacy/android/GaCyApplication.kt | 8 +- .../com/gacy/android/service/ServerService.kt | 302 +++++++++++------- app/src/main/python/run_ppsr.py | 8 - 3 files changed, 195 insertions(+), 123 deletions(-) diff --git a/app/src/main/java/com/gacy/android/GaCyApplication.kt b/app/src/main/java/com/gacy/android/GaCyApplication.kt index eb6c439..dba6b4f 100644 --- a/app/src/main/java/com/gacy/android/GaCyApplication.kt +++ b/app/src/main/java/com/gacy/android/GaCyApplication.kt @@ -22,8 +22,12 @@ class GaCyApplication : Application() { createNotificationChannel() // Initialize embedded Python runtime - if (!Python.isStarted()) { - Python.start(AndroidPlatform(this)) + try { + if (!Python.isStarted()) { + Python.start(AndroidPlatform(this)) + } + } catch (t: Throwable) { + t.printStackTrace() } } diff --git a/app/src/main/java/com/gacy/android/service/ServerService.kt b/app/src/main/java/com/gacy/android/service/ServerService.kt index 46aaec6..5a3dc96 100644 --- a/app/src/main/java/com/gacy/android/service/ServerService.kt +++ b/app/src/main/java/com/gacy/android/service/ServerService.kt @@ -1,16 +1,18 @@ package com.gacy.android.service import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager import android.app.PendingIntent +import android.app.Service import android.content.Context import android.content.Intent import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat -import androidx.lifecycle.LifecycleService -import androidx.lifecycle.lifecycleScope import com.chaquo.python.Python +import com.chaquo.python.android.AndroidPlatform import com.gacy.android.GaCyApplication import com.gacy.android.MainActivity import com.gacy.android.R @@ -19,8 +21,10 @@ import com.gacy.android.data.PreferencesRepository import com.gacy.android.data.UpdateRepository import com.gacy.android.model.ServerExecutionMode import com.gacy.android.model.ServerStatus +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -37,19 +41,22 @@ class ServerLogReceiver { } } -class ServerService : LifecycleService() { +class ServerService : Service() { + private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var serverThread: Thread? = null private var probeJob: Job? = null - private var serverJob: Job? = null + private var executionJob: Job? = null + + override fun onBind(intent: Intent?): IBinder? = null override fun onCreate() { super.onCreate() + createNotificationChannel() _serverStatus.value = ServerStatus.STOPPED } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) val action = intent?.action ?: ACTION_START when (action) { @@ -66,95 +73,117 @@ class ServerService : LifecycleService() { return } - val prefs = PreferencesRepository(this) - val config = prefs.getServerConfig() + try { + val prefs = PreferencesRepository(this) + val config = prefs.getServerConfig() - _serverStatus.value = ServerStatus.STARTING - startForegroundCompat("GaCy Server is starting...") - LogBuffer.append("--- GaCy Server Starting ---") - LogBuffer.append("Mode: ${config.executionMode.displayNameEn}") - LogBuffer.append("Dispatch Port: ${config.dispatchPort} | Gameserver Port: ${config.gameserverPort}") + _serverStatus.value = ServerStatus.STARTING + startForegroundCompat("GaCy Server is starting...") + LogBuffer.append("--- GaCy Server Starting ---") + LogBuffer.append("Mode: ${config.executionMode.displayNameEn}") + LogBuffer.append("Dispatch Port: ${config.dispatchPort} | Gameserver Port: ${config.gameserverPort}") - if (config.executionMode == ServerExecutionMode.LOCAL_EMBEDDED) { - startLocalServerExecution(config.dispatchPort) - } else { - startRemoteMonitor(config.remoteHost, config.dispatchPort) + if (config.executionMode == ServerExecutionMode.LOCAL_EMBEDDED) { + startLocalServerExecution(config.dispatchPort) + } else { + startRemoteMonitor(config.remoteHost, config.dispatchPort) + } + } catch (t: Throwable) { + LogBuffer.append("[Start Error] ${t.localizedMessage ?: t.message}") + t.printStackTrace() + _serverStatus.value = ServerStatus.ERROR + stopForegroundCompat() + stopSelf() } } private fun startLocalServerExecution(dispatchPort: Int) { - serverJob = lifecycleScope.launch(Dispatchers.IO) { - val updateRepo = UpdateRepository(this@ServerService) - if (!updateRepo.isServerInstalled()) { - LogBuffer.append("[Setup] Installing GaCy embedded server binaries from assets...") - val success = updateRepo.ensureServerInstalled() - if (!success) { - LogBuffer.append("[Error] Failed to install server binaries from assets.") + executionJob?.cancel() + executionJob = serviceScope.launch { + try { + val updateRepo = UpdateRepository(this@ServerService) + if (!updateRepo.isServerInstalled()) { + LogBuffer.append("[Setup] Installing GaCy embedded server binaries from assets...") + val success = updateRepo.ensureServerInstalled() + if (!success) { + LogBuffer.append("[Error] Failed to install server binaries from assets.") + _serverStatus.value = ServerStatus.ERROR + stopForegroundCompat() + stopSelf() + return@launch + } + LogBuffer.append("[Setup] Server binaries installed successfully.") + } + + val ppsrFile = File(filesDir, "server/build/build/ppsr.pyc") + if (!ppsrFile.exists()) { + LogBuffer.append("[Error] Server bytecode missing at: ${ppsrFile.absolutePath}") _serverStatus.value = ServerStatus.ERROR stopForegroundCompat() stopSelf() return@launch } - LogBuffer.append("[Setup] Server binaries installed successfully.") - } - val ppsrFile = File(filesDir, "server/build/build/ppsr.pyc") - if (!ppsrFile.exists()) { - LogBuffer.append("[Error] Server bytecode missing at: ${ppsrFile.absolutePath}") + try { + val persistentRepo = PersistentRepository(this@ServerService) + persistentRepo.ensureValidPersistentJsonSync() + } catch (e: Throwable) { + LogBuffer.append("[Warning] Persistent validation: ${e.message}") + } + + LogBuffer.append("[System] Launching embedded GaCy Python server...") + + // Background TCP port readiness probe + probeJob?.cancel() + probeJob = serviceScope.launch { + var attempts = 0 + while (isActive && _serverStatus.value == ServerStatus.STARTING && attempts < 35) { + delay(1000) + attempts++ + if (isPortOpen("127.0.0.1", dispatchPort)) { + _serverStatus.value = ServerStatus.RUNNING + updateNotification("GaCy Server is RUNNING on :$dispatchPort") + LogBuffer.append("[System] GaCy Dispatch server is ready on 127.0.0.1:$dispatchPort") + break + } + } + } + + serverThread = Thread({ + try { + if (!Python.isStarted()) { + Python.start(AndroidPlatform(applicationContext)) + } + val py = Python.getInstance() + val runPpsr = py.getModule("run_ppsr") + val logReceiver = ServerLogReceiver() + runPpsr.callAttr("run_server", ppsrFile.absolutePath, logReceiver) + } catch (t: Throwable) { + LogBuffer.append("[Python Error] ${t.localizedMessage ?: t.message}") + t.printStackTrace() + } finally { + probeJob?.cancel() + _serverStatus.value = ServerStatus.STOPPED + LogBuffer.append("[System] Server stopped.") + stopForegroundCompat() + stopSelf() + } + }, "GaCy-PythonServer").apply { + start() + } + } catch (t: Throwable) { + LogBuffer.append("[Execution Error] ${t.localizedMessage ?: t.message}") + t.printStackTrace() _serverStatus.value = ServerStatus.ERROR stopForegroundCompat() stopSelf() - return@launch - } - - try { - val persistentRepo = PersistentRepository(this@ServerService) - persistentRepo.ensureValidPersistentJsonSync() - } catch (e: Exception) { - LogBuffer.append("[Warning] Persistent validation: ${e.message}") - } - - LogBuffer.append("[System] Launching embedded GaCy Python server...") - - // Background TCP port readiness probe - probeJob?.cancel() - probeJob = lifecycleScope.launch(Dispatchers.IO) { - var attempts = 0 - while (isActive && _serverStatus.value == ServerStatus.STARTING && attempts < 35) { - delay(1000) - attempts++ - if (isPortOpen("127.0.0.1", dispatchPort)) { - _serverStatus.value = ServerStatus.RUNNING - updateNotification("GaCy Server is RUNNING on :$dispatchPort") - LogBuffer.append("[System] GaCy Dispatch server is ready on 127.0.0.1:$dispatchPort") - break - } - } - } - - serverThread = Thread({ - try { - val py = Python.getInstance() - val runPpsr = py.getModule("run_ppsr") - val logReceiver = ServerLogReceiver() - runPpsr.callAttr("run_server", ppsrFile.absolutePath, logReceiver) - } catch (e: Exception) { - LogBuffer.append("[Python Error] ${e.localizedMessage ?: e.message}") - } finally { - probeJob?.cancel() - _serverStatus.value = ServerStatus.STOPPED - LogBuffer.append("[System] Server stopped.") - stopForegroundCompat() - stopSelf() - } - }, "GaCy-PythonServer").apply { - start() } } } private fun startRemoteMonitor(remoteHost: String, dispatchPort: Int) { - serverJob = lifecycleScope.launch(Dispatchers.IO) { + executionJob?.cancel() + executionJob = serviceScope.launch { LogBuffer.append("[Remote] Connecting to remote GaCy host at $remoteHost:$dispatchPort...") delay(600) _serverStatus.value = ServerStatus.RUNNING @@ -171,17 +200,17 @@ class ServerService : LifecycleService() { probeJob?.cancel() probeJob = null - serverJob?.cancel() - serverJob = null + executionJob?.cancel() + executionJob = null - lifecycleScope.launch(Dispatchers.IO) { + serviceScope.launch { try { if (Python.isStarted()) { val py = Python.getInstance() val runPpsr = py.getModule("run_ppsr") runPpsr.callAttr("kill_server_process") } - } catch (e: Exception) { + } catch (e: Throwable) { LogBuffer.append("[Stop Error] ${e.localizedMessage ?: e.message}") } @@ -197,7 +226,7 @@ class ServerService : LifecycleService() { private fun restartServer() { LogBuffer.append("Restarting GaCy Server...") stopServer() - lifecycleScope.launch { + serviceScope.launch { delay(1500) startServer() } @@ -209,12 +238,29 @@ class ServerService : LifecycleService() { socket.connect(InetSocketAddress(host, port), 400) true } - } catch (_: Exception) { + } catch (_: Throwable) { false } } + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + GaCyApplication.CHANNEL_ID, + GaCyApplication.CHANNEL_NAME, + NotificationManager.IMPORTANCE_LOW + ).apply { + description = "Background service for GaCy Game Server & Proxy" + setShowBadge(false) + } + val manager = getSystemService(NotificationManager::class.java) + manager?.createNotificationChannel(channel) + } + } + private fun createNotification(contentText: String): Notification { + createNotificationChannel() + val launchIntent = Intent(this, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP } @@ -238,7 +284,7 @@ class ServerService : LifecycleService() { return NotificationCompat.Builder(this, GaCyApplication.CHANNEL_ID) .setContentTitle("GaCy Android") .setContentText(contentText) - .setSmallIcon(R.mipmap.ic_launcher) + .setSmallIcon(R.drawable.app_icon) .setOngoing(true) .setContentIntent(pendingIntent) .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopPendingIntent) @@ -246,45 +292,55 @@ class ServerService : LifecycleService() { } private fun startForegroundCompat(text: String) { - val notification = createNotification(text) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - startForeground( - NOTIFICATION_ID, - notification, - ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE - ) - } else { - startForeground(NOTIFICATION_ID, notification) + try { + createNotificationChannel() + val notification = createNotification(text) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // API 34+ + startForeground( + NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE + ) + } else { + // API 26..33 + startForeground(NOTIFICATION_ID, notification) + } + } catch (e: Throwable) { + try { + val notification = createNotification(text) + startForeground(NOTIFICATION_ID, notification) + } catch (e2: Throwable) { + LogBuffer.append("[Foreground Warning] ${e2.message}") + } } } private fun updateNotification(contentText: String) { - val manager = getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager - manager.notify(NOTIFICATION_ID, createNotification(contentText)) + try { + val manager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager + manager?.notify(NOTIFICATION_ID, createNotification(contentText)) + } catch (_: Throwable) {} } private fun stopForegroundCompat() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - stopForeground(STOP_FOREGROUND_REMOVE) - } else { - @Suppress("DEPRECATION") - stopForeground(true) - } + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_REMOVE) + } else { + @Suppress("DEPRECATION") + stopForeground(true) + } + } catch (_: Throwable) {} } override fun onDestroy() { probeJob?.cancel() serverThread?.interrupt() - serverJob?.cancel() + executionJob?.cancel() super.onDestroy() _serverStatus.value = ServerStatus.STOPPED } - override fun onBind(intent: Intent): IBinder? { - super.onBind(intent) - return null - } - companion object { const val ACTION_START = "com.gacy.android.ACTION_START" const val ACTION_STOP = "com.gacy.android.ACTION_STOP" @@ -296,24 +352,44 @@ class ServerService : LifecycleService() { fun start(context: Context) { val intent = Intent(context, ServerService::class.java).apply { action = ACTION_START } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(intent) - } else { - context.startService(intent) + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } catch (t: Throwable) { + try { + context.startService(intent) + } catch (t2: Throwable) { + LogBuffer.append("[Service Start Error] ${t2.message}") + } } } fun stop(context: Context) { val intent = Intent(context, ServerService::class.java).apply { action = ACTION_STOP } - context.startService(intent) + try { + context.startService(intent) + } catch (t: Throwable) { + LogBuffer.append("[Service Stop Error] ${t.message}") + } } fun restart(context: Context) { val intent = Intent(context, ServerService::class.java).apply { action = ACTION_RESTART } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(intent) - } else { - context.startService(intent) + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } catch (t: Throwable) { + try { + context.startService(intent) + } catch (t2: Throwable) { + LogBuffer.append("[Service Restart Error] ${t2.message}") + } } } } diff --git a/app/src/main/python/run_ppsr.py b/app/src/main/python/run_ppsr.py index 57c10c3..2d41d23 100644 --- a/app/src/main/python/run_ppsr.py +++ b/app/src/main/python/run_ppsr.py @@ -476,14 +476,6 @@ def run_server(ppsr_path, activity_context=None): except Exception as e: print(f"[Patch Warning] Router crash-guard: {e}") - # --- License / Access Check Bypass Patch --- - try: - import util.binary as _bin_mod - _bin_mod.qyqwiourtyqot = lambda *args, **kwargs: True - print("[Patch] util.binary access check bypassed successfully") - except Exception as e: - print(f"[Patch Warning] Binary access check patch: {e}") - print(f"Loading bytecode target: {ppsr_path}...") with open(ppsr_path, "rb") as f: