diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a1b2fd0f..d2b693f67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. - Viewer: display more items in tag/copy/move quick action choosers +### Fixed + +- multiple widget setup after device reboot + ## [v1.11.8] - 2024-07-19 ### Added diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt index 079ad5d30..f42bb2e8e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt @@ -11,12 +11,18 @@ import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.util.Log import android.util.SizeF import android.widget.RemoteViews import app.loup.streams_channel.StreamsChannel import deckers.thibault.aves.channel.AvesByteSendingMethodCodec -import deckers.thibault.aves.channel.calls.* +import deckers.thibault.aves.channel.calls.DeviceHandler +import deckers.thibault.aves.channel.calls.MediaFetchBytesHandler +import deckers.thibault.aves.channel.calls.MediaFetchObjectHandler +import deckers.thibault.aves.channel.calls.MediaStoreHandler +import deckers.thibault.aves.channel.calls.StorageHandler import deckers.thibault.aves.channel.streams.ImageByteStreamHandler import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler import deckers.thibault.aves.model.FieldMap @@ -26,8 +32,14 @@ import io.flutter.FlutterInjector import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.plugin.common.MethodChannel -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import java.nio.ByteBuffer +import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -108,37 +120,25 @@ class HomeWidgetProvider : AppWidgetProvider() { val isNightModeOn = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + val params = hashMapOf( + "widgetId" to widgetId, + "sizesDip" to sizesDip, + "devicePixelRatio" to getDevicePixelRatio(), + "drawEntryImage" to drawEntryImage, + "reuseEntry" to reuseEntry, + "isSystemThemeDark" to isNightModeOn, + ).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + put("cornerRadiusPx", context.resources.getDimension(android.R.dimen.system_app_widget_background_radius)) + } + } + initFlutterEngine(context) - val messenger = flutterEngine!!.dartExecutor - val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL) try { - val props = suspendCoroutine { cont -> + val props = suspendCoroutine { cont -> defaultScope.launch { FlutterUtils.runOnUiThread { - channel.invokeMethod("drawWidget", hashMapOf( - "widgetId" to widgetId, - "sizesDip" to sizesDip, - "devicePixelRatio" to getDevicePixelRatio(), - "drawEntryImage" to drawEntryImage, - "reuseEntry" to reuseEntry, - "isSystemThemeDark" to isNightModeOn, - ).apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - put("cornerRadiusPx", context.resources.getDimension(android.R.dimen.system_app_widget_background_radius)) - } - }, object : MethodChannel.Result { - override fun success(result: Any?) { - cont.resume(result) - } - - override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { - cont.resumeWithException(Exception("$errorCode: $errorMessage\n$errorDetails")) - } - - override fun notImplemented() { - cont.resumeWithException(Exception("not implemented")) - } - }) + tryDrawWidget(params, cont, 0) } } } @@ -150,6 +150,30 @@ class HomeWidgetProvider : AppWidgetProvider() { return null } + private fun tryDrawWidget(params: HashMap, cont: Continuation, drawRetry: Int) { + val messenger = flutterEngine!!.dartExecutor + val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL) + channel.invokeMethod("drawWidget", params, object : MethodChannel.Result { + override fun success(result: Any?) { + cont.resume(result) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + cont.resumeWithException(Exception("$errorCode: $errorMessage\n$errorDetails")) + } + + override fun notImplemented() { + if (drawRetry > DRAW_RETRY_MAX) { + cont.resumeWithException(Exception("not implemented")) + } else { + Handler(Looper.getMainLooper()).postDelayed({ + tryDrawWidget(params, cont, drawRetry + 1) + }, 2000L) + } + } + }) + } + private fun updateWidgetImage( context: Context, appWidgetManager: AppWidgetManager, @@ -271,6 +295,7 @@ class HomeWidgetProvider : AppWidgetProvider() { private val LOG_TAG = LogUtils.createTag() private const val WIDGET_DART_ENTRYPOINT = "widgetMain" private const val WIDGET_DRAW_CHANNEL = "deckers.thibault/aves/widget_draw" + private const val DRAW_RETRY_MAX = 5 private var flutterEngine: FlutterEngine? = null private var imageByteFetchJob: Job? = null diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index 5c4cbe6db..327485eeb 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -7,6 +7,8 @@ import 'package:flutter/foundation.dart'; final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); +enum _State { uninitialized, initializing, initialized } + class AndroidFileUtils { // cf https://developer.android.com/reference/android/content/ContentResolver#SCHEME_CONTENT static const contentScheme = 'content'; @@ -27,13 +29,19 @@ class AndroidFileUtils { late final String dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath; late final Set videoCapturesPaths; Set storageVolumes = {}; - bool _initialized = false; + _State _initialized = _State.uninitialized; AndroidFileUtils._private(); Future init() async { - if (_initialized) return; + if (_initialized == _State.uninitialized) { + _initialized = _State.initializing; + await _doInit(); + _initialized = _State.initialized; + } + } + Future _doInit() async { separator = pContext.separator; await _initStorageVolumes(); vaultRoot = await storageService.getVaultRoot(); @@ -50,8 +58,6 @@ class AndroidFileUtils { // from Aves avesVideoCapturesPath, }; - - _initialized = true; } Future _initStorageVolumes() async { diff --git a/lib/widget_common.dart b/lib/widget_common.dart index 34a8067cb..7432c4e7e 100644 --- a/lib/widget_common.dart +++ b/lib/widget_common.dart @@ -18,11 +18,13 @@ import 'package:flutter/services.dart'; const _widgetDrawChannel = MethodChannel('deckers.thibault/aves/widget_draw'); void widgetMainCommon(AppFlavor flavor) async { + debugPrint('Widget main start'); WidgetsFlutterBinding.ensureInitialized(); initPlatformServices(); await settings.init(monitorPlatformSettings: false); await reportService.init(); + debugPrint('Widget channel method handling setup'); _widgetDrawChannel.setMethodCallHandler((call) async { // widget settings may be modified in a different process after channel setup await settings.reload(); diff --git a/lib/widgets/home_widget.dart b/lib/widgets/home_widget.dart index 6f6d4a509..5344627ea 100644 --- a/lib/widgets/home_widget.dart +++ b/lib/widgets/home_widget.dart @@ -36,7 +36,7 @@ class HomeWidgetPainter { final widthPx = sizeDip.width * devicePixelRatio; final heightPx = sizeDip.height * devicePixelRatio; final widgetSizePx = Size(widthPx, heightPx); - debugPrint('draw widget for $sizeDip dp ($widgetSizePx px), entry=$entry'); + debugPrint('Draw widget for ${sizeDip.width}x${sizeDip.height} dp (${widgetSizePx.width}x${widgetSizePx.height} px), entry=$entry'); final ui.Image? entryImage; if (entry != null) { final extent = shape.extentPx(widgetSizePx, entry!) / devicePixelRatio;