From f19f212fb46a11fd8e604cbb985b85c27f65d9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Wed, 2 Sep 2026 20:17:17 +0200 Subject: [PATCH] platform upgrades --- android/.gitignore | 1 + android/app/build.gradle | 12 ++++----- .../app/FlutterMultiDexApplication.java | 25 ------------------- android/build.gradle | 21 +++++++++++++--- android/gradle.properties | 8 ++---- .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 6 ++--- lib/push/push_registration.dart | 5 ++-- lib/push/push_status.dart | 1 + lib/state/app/modules/app_modules.dart | 2 +- pubspec.yaml | 8 +++--- test/push/push_permission_test.dart | 16 ++++++++---- 12 files changed, 51 insertions(+), 56 deletions(-) delete mode 100644 android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java diff --git a/android/.gitignore b/android/.gitignore index 6f56801..16ac071 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -11,3 +11,4 @@ GeneratedPluginRegistrant.java key.properties **/*.keystore **/*.jks +.kotlin/ diff --git a/android/app/build.gradle b/android/app/build.gradle index 3131c2e..86b7205 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,10 +33,6 @@ android { coreLibraryDesugaringEnabled true } - kotlinOptions { - jvmTarget = '17' - } - sourceSets { main.java.srcDirs += 'src/main/kotlin' } @@ -47,7 +43,6 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName - multiDexEnabled true } buildTypes { @@ -59,12 +54,17 @@ android { } } +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + flutter { source '../..' } dependencies { - implementation 'com.android.support:multidex:2.0.1' // Same version as workmanager_android pins — needed to enqueue its // BackgroundWorker from native widget code (the plugin uses // `implementation`, so androidx.work is not exposed transitively). diff --git a/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java b/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java deleted file mode 100644 index 752fc18..0000000 --- a/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java +++ /dev/null @@ -1,25 +0,0 @@ -// Generated file. -// -// If you wish to remove Flutter's multidex support, delete this entire file. -// -// Modifications to this file should be done in a copy under a different name -// as this file may be regenerated. - -package io.flutter.app; - -import android.app.Application; -import android.content.Context; -import androidx.annotation.CallSuper; -import androidx.multidex.MultiDex; - -/** - * Extension of {@link android.app.Application}, adding multidex support. - */ -public class FlutterMultiDexApplication extends Application { - @Override - @CallSuper - protected void attachBaseContext(Context base) { - super.attachBaseContext(base); - MultiDex.install(this); - } -} diff --git a/android/build.gradle b/android/build.gradle index 1447097..141810a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -5,10 +5,25 @@ allprojects { } } -rootProject.buildDir = '../build' +rootProject.layout.buildDirectory = file('../build') subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" + project.layout.buildDirectory = rootProject.layout.buildDirectory.dir(project.name) } +// AGP 9 enables built-in Kotlin, but Flutter's migrator pins +// android.builtInKotlin=false while the plugin ecosystem catches up. Most +// plugins honour that flag and fall back to KGP themselves; file_picker and +// background_downloader only check the AGP major version and silently skip +// applying KGP, which leaves their Kotlin sources uncompiled (missing +// FilePickerPlugin/BackgroundDownloaderPlugin at link time). Apply KGP for +// them as soon as AGP is on the project, i.e. before their android {} block +// runs. Drop this once both plugins ship a builtInKotlin-aware build script. +gradle.beforeProject { sub -> + if (!(sub.name in ['file_picker', 'background_downloader'])) return + sub.plugins.withId('com.android.library') { + sub.pluginManager.apply('org.jetbrains.kotlin.android') + } +} + // Pin every Android subproject to JVM 17 so plugins that ship Kotlin sources // compiled with a higher target (e.g. receive_sharing_intent at 21) or stale // Java compatibility (e.g. home_widget at 1.8) don't break the build under @@ -42,5 +57,5 @@ subprojects { } tasks.register("clean", Delete) { - delete rootProject.buildDir + delete rootProject.layout.buildDirectory } diff --git a/android/gradle.properties b/android/gradle.properties index 5cfd626..38585f0 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,10 +1,6 @@ org.gradle.jvmargs=-Xmx4G android.useAndroidX=true -android.enableJetifier=true -android.defaults.buildfeatures.buildconfig=true -android.nonTransitiveRClass=false -android.nonFinalResIds=false -# This builtInKotlin flag was added automatically by Flutter migrator -android.builtInKotlin=false # This newDsl flag was added automatically by Flutter migrator android.newDsl=false +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 6514f91..5f38436 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/android/settings.gradle b/android/settings.gradle index f442317..69eb89f 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,9 +19,9 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.13.2' apply false - id "com.android.library" version '8.13.2' apply false - id "org.jetbrains.kotlin.android" version "2.2.20" apply false + id "com.android.application" version '9.1.0' apply false + id "com.android.library" version '9.1.0' apply false + id "org.jetbrains.kotlin.android" version "2.4.0" apply false id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0' } diff --git a/lib/push/push_registration.dart b/lib/push/push_registration.dart index 287de6c..1f0d1b7 100644 --- a/lib/push/push_registration.dart +++ b/lib/push/push_registration.dart @@ -343,7 +343,8 @@ class PushRegistration { /// transient plugin/platform hiccup never silently disables push (the OS /// simply won't show notifications until the user decides). static bool isPermissionUsable(AuthorizationStatus status) => - status != AuthorizationStatus.denied; + status != AuthorizationStatus.denied && + status != AuthorizationStatus.deniedPermanently; /// Requests the OS notification permission (covers iOS + Android 13) and /// returns whether registration should proceed. Errors from the plugin are @@ -365,7 +366,7 @@ class PushRegistration { try { final settings = await FirebaseMessaging.instance .getNotificationSettings(); - return settings.authorizationStatus == AuthorizationStatus.denied; + return !isPermissionUsable(settings.authorizationStatus); } on Object { return false; } diff --git a/lib/push/push_status.dart b/lib/push/push_status.dart index 011eaab..a0664cb 100644 --- a/lib/push/push_status.dart +++ b/lib/push/push_status.dart @@ -148,6 +148,7 @@ Future _osPermission() async { case AuthorizationStatus.provisional: return PushCheck.ok; case AuthorizationStatus.denied: + case AuthorizationStatus.deniedPermanently: return PushCheck.fail; case AuthorizationStatus.notDetermined: return PushCheck.unknown; diff --git a/lib/state/app/modules/app_modules.dart b/lib/state/app/modules/app_modules.dart index 833b75c..59c8c67 100644 --- a/lib/state/app/modules/app_modules.dart +++ b/lib/state/app/modules/app_modules.dart @@ -203,7 +203,7 @@ class AppModule { var moveIndex = 0; return [ for (final m in effective) - displayedSet.contains(m) ? moved[moveIndex++] : m, + if (displayedSet.contains(m)) moved[moveIndex++] else m, ]; } diff --git a/pubspec.yaml b/pubspec.yaml index ef2b2f1..3366f99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: async: ^2.11.0 badges: ^3.1.2 - cached_network_image: ^3.4.1 + cached_network_image: ^4.0.0 # Grapheme-cluster iteration for emoji-only message detection # (lib/utils/emoji_detection.dart). Already present transitively via flutter. characters: ^1.4.0 @@ -44,7 +44,7 @@ dependencies: flutter_bloc: ^9.0.0 flutter_secure_storage: ^10.0.0 home_widget: ^0.9.3 - workmanager: ^0.9.0+3 + workmanager: ^0.10.9 intl: ^0.20.2 flutter_linkify: ^6.0.0 linkify: ^5.0.0 @@ -96,7 +96,7 @@ dependencies: background_downloader: ^9.5.5 # Opens the OS notification settings for this app (iOS + Android) when the # user declined the permission and wants to enable it later. - app_settings: ^7.0.0 + app_settings: ^9.0.0 flutter_layout_grid: ^2.0.8 flutter_markdown_plus: ^1.0.12 # Underlying parser for flutter_markdown_plus; imported directly to build the @@ -114,7 +114,7 @@ dev_dependencies: flutter_launcher_icons: ^0.14.3 build_runner: ^2.10.5 - freezed: ^3.2.4 + freezed: ^4.0.0 json_serializable: ^6.11.4 flutter_lints: ^6.0.0 diff --git a/test/push/push_permission_test.dart b/test/push/push_permission_test.dart index 81755c6..8ec6fc7 100644 --- a/test/push/push_permission_test.dart +++ b/test/push/push_permission_test.dart @@ -5,10 +5,16 @@ import 'package:marianum_mobile/push/push_registration.dart'; void main() { group('PushRegistration.isPermissionUsable', () { test('explicit denial blocks registration', () { - expect( - PushRegistration.isPermissionUsable(AuthorizationStatus.denied), - isFalse, - ); + for (final status in [ + AuthorizationStatus.denied, + AuthorizationStatus.deniedPermanently, + ]) { + expect( + PushRegistration.isPermissionUsable(status), + isFalse, + reason: '$status should block registration', + ); + } }); test('all other statuses allow registration', () { @@ -28,7 +34,7 @@ void main() { test('covers every AuthorizationStatus value', () { // Guard: if firebase_messaging ever adds a status, revisit the gate. - expect(AuthorizationStatus.values, hasLength(4)); + expect(AuthorizationStatus.values, hasLength(5)); }); }); }