Methods marked with @UiThread must be executed on the main thread. Current thread: WallpaperService

This issue has been tracked since 2023-03-17.

I'm trying to run FlutterEngine in Android WallpaperService, every thing works so far except some VIVO phone crashed and throw the java.lang.RuntimeException with message in title.

Steps to Reproduce

  1. When WallpaperService#onCreateEngine getting called, and WallpaperService.Engine#onCreate getting called.
  2. Some VIVO phone seem start the WallpaperService in a thread named "WallpaperService".

Expected results:
Pass the @UiThread checking in Android Service

Actual results:
Crash when calling FlutterJNI.ensureRunningOnMainThread(FlutterJNI.java:36)

LiveWallpaperFltService.kt
package com.LondonX.live_wallpaper_flt.service

import android.content.res.Configuration
import android.service.wallpaper.WallpaperService
import android.text.format.DateFormat
import android.view.SurfaceHolder
import android.view.ViewConfiguration
import com.LondonX.live_wallpaper_flt.entity.Config
import com.google.gson.Gson
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterJNI
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.embedding.engine.renderer.FlutterRenderer
import io.flutter.embedding.engine.systemchannels.SettingsChannel
import kotlinx.coroutines.*
import java.io.File

private const val TAG = "[live_wallpaper_flt]"

class LiveWallpaperFltService : WallpaperService() {

    override fun onCreateEngine(): Engine {
        return object : Engine() {
            private val config by lazy {
                val json = File(filesDir, "live_wallpaper_config.json").readText()
                Gson().fromJson(json, Config::class.java)
                    ?: throw Exception("${TAG}invalid config file, make sure you called LiveWallpaperFlt.instance.applyConfig first.")
            }
            private lateinit var flutterEngine: FlutterEngine

            private val scope = CoroutineScope(Dispatchers.Main)
            private var refreshJob: Job? = null

            private var width: Int? = null
            private var height: Int? = null
            private var isDark = false

            override fun onCreate(surfaceHolder: SurfaceHolder?) {
                super.onCreate(surfaceHolder)
                flutterEngine = FlutterEngine(this@LiveWallpaperFltService)
                flutterEngine.dartExecutor.executeDartEntrypoint(
                    DartExecutor.DartEntrypoint(
                        FlutterInjector.instance().flutterLoader().findAppBundlePath(),
                        config.entryFunction,
                    ),
                    listOf("is_started_from_wallpaper_service"),
                )
                isDark = isInDarkMode()
                applyPlatformDarkMode()
            }

            private fun engineScope(f: suspend FlutterEngine.() -> Unit) {
                scope.launch {
                    f.invoke(flutterEngine)
                }
            }

            private fun FlutterEngine.isAttached(): Boolean {
                surfaceHolder?.surface ?: return false
                val flutterJNI = FlutterRenderer::class.java.getDeclaredField("flutterJNI")
                    .apply { isAccessible = true }.get(renderer) as? FlutterJNI
                return flutterJNI?.isAttached == true
            }

            override fun onVisibilityChanged(visible: Boolean) {
                super.onVisibilityChanged(visible)
                if (visible) {
                    refreshJob = scope.launch {
                        while (true) {
                            delay(24)
                            if (flutterEngine.isAttached()) {
                                applyViewportMetrics()
                                if (isDark != isInDarkMode()) {
                                    isDark = isInDarkMode()
                                    applyPlatformDarkMode()
                                }
                            }
                        }
                    }
                    engineScope {
                        lifecycleChannel.appIsResumed()
                        if (flutterEngine.isAttached()) {
                            renderer.startRenderingToSurface(surfaceHolder.surface!!, true)
                        }
                    }
                } else {
                    refreshJob?.cancel()
                    engineScope {
                        lifecycleChannel.appIsPaused()
                        if (flutterEngine.isAttached()) {
                            renderer.stopRenderingToSurface()
                        }
                        lifecycleChannel.appIsInactive()
                    }
                }
            }

            override fun onDestroy() {
                refreshJob?.cancel()
                engineScope {
                    lifecycleChannel.appIsDetached()
                    destroy()
                }
                super.onDestroy()
            }

            override fun onSurfaceChanged(
                holder: SurfaceHolder?, format: Int, width: Int, height: Int
            ) {
                super.onSurfaceChanged(holder, format, width, height)
                this.width = width
                this.height = height
                if (!flutterEngine.renderer.isDisplayingFlutterUi) return
                applyViewportMetrics()
            }

            private fun applyViewportMetrics() {
                val width = this.width ?: resources.displayMetrics.widthPixels
                val height = this.height ?: resources.displayMetrics.heightPixels
                val viewportMetrics = FlutterRenderer.ViewportMetrics()
                viewportMetrics.devicePixelRatio = resources.displayMetrics.density
                viewportMetrics.height = height
                viewportMetrics.width = width
                viewportMetrics.physicalTouchSlop =
                    ViewConfiguration.get(this@LiveWallpaperFltService).scaledTouchSlop
                engineScope {
                    if (flutterEngine.isAttached()) {
                        renderer.setViewportMetrics(viewportMetrics)
                        renderer.surfaceChanged(width, height)
                    }
                }
            }

            private fun isInDarkMode(): Boolean {
                return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
            }

            private fun applyPlatformDarkMode() {
                engineScope {
                    settingsChannel.startMessage()
                        .setTextScaleFactor(resources.configuration.fontScale)
                        .setUse24HourFormat(DateFormat.is24HourFormat(this@LiveWallpaperFltService))
                        .setPlatformBrightness(
                            if (isInDarkMode()) {
                                SettingsChannel.PlatformBrightness.dark
                            } else {
                                SettingsChannel.PlatformBrightness.light
                            }
                        ).send()
                }
            }
        }
    }
}

Still crash even I tried using engineScope to wrap codes running on CoroutineScope(Dispatchers.Main).

Logs
Fatal Exception: java.lang.RuntimeException: Methods marked with @UiThread must be executed on the main thread. Current thread: WallpaperService
       at io.flutter.embedding.engine.FlutterJNI.ensureRunningOnMainThread(FlutterJNI.java:36)
       at io.flutter.embedding.engine.FlutterJNI.setPlatformMessageHandler(FlutterJNI.java)
       at io.flutter.embedding.engine.dart.DartExecutor.onAttachedToJNI(DartExecutor.java:11)
       at io.flutter.embedding.engine.FlutterEngine.<init>(FlutterEngine.java:58)
       at io.flutter.embedding.engine.FlutterEngine.<init>(FlutterEngine.java:8)
       at io.flutter.embedding.engine.FlutterEngine.<init>(FlutterEngine.java:11)
       at io.flutter.embedding.engine.FlutterEngine.<init>(FlutterEngine.java:6)
       at io.flutter.embedding.engine.FlutterEngine.<init>(FlutterEngine.java:1)
       at com.LondonX.live_wallpaper_flt.service.LiveWallpaperFltService$onCreateEngine$1.<init>(LiveWallpaperFltService.java:18)
       at com.LondonX.live_wallpaper_flt.service.LiveWallpaperFltService.onCreateEngine(LiveWallpaperFltService.java:2)
       at android.service.wallpaper.WallpaperService$IWallpaperEngineWrapper.executeMessage(WallpaperService.java:2192)
       at com.android.internal.os.HandlerCaller$MyHandler.handleMessage(HandlerCaller.java:44)
       at android.os.Handler.dispatchMessage(Handler.java:106)
       at android.os.Looper.loopOnce(Looper.java:233)
       at android.os.Looper.loop(Looper.java:334)
       at android.os.HandlerThread.run(HandlerThread.java:67)
flutter analyze
flutter analyze
Analyzing wallpaper_gpt...                                              
No issues found! (ran in 7.0s)
flutter doctor -v
flutter doctor -v
[✓] Flutter (Channel stable, 3.7.7, on macOS 13.2.1 22D68 darwin-x64, locale zh-Hans-CN)
    • Flutter version 3.7.7 on channel stable at /usr/local/Caskroom/flutter/1.22.6/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 2ad6cd72c0 (9 days ago), 2023-03-08 09:41:59 -0800
    • Engine revision 1837b5be5f
    • Dart version 2.19.4
    • DevTools version 2.20.1

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0-rc2)
    • Android SDK at /Users/london/Library/Android/sdk/
    • Platform android-33, build-tools 33.0.0-rc2
    • ANDROID_HOME = /Users/london/Library/Android/sdk/
    • ANDROID_SDK_ROOT = /Users/london/Library/Android/sdk/
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 14.2)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 14C18
    • CocoaPods version 1.11.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2022.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)

[✓] IntelliJ IDEA Community Edition (version 2022.1)
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart

[✓] VS Code (version 1.76.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.60.0

[✓] Proxy Configuration
    • HTTP_PROXY is set
    • NO_PROXY is localhost,127.0.0.1,::1
    • NO_PROXY contains localhost
    • NO_PROXY contains 127.0.0.1
    • NO_PROXY contains ::1

[✓] Connected device (2 available)
    • London’s iPhone (mobile) • 00008101-001E39DE3A63001E • ios            • iOS 16.3.1 20D67
    • Chrome (web)             • chrome                    • web-javascript • Google Chrome 111.0.5563.64

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!
More Details About Repo
Owner Name flutter
Repo Name flutter
Full Name flutter/flutter
Language Dart
Created Date 2015-03-06
Updated Date 2023-03-30
Star Count 151602
Watcher Count 3555
Fork Count 25000
Issue Count 11498

YOU MAY BE INTERESTED

Issue Title Created Date Updated Date