diff --git a/app-sandbox/build.gradle.kts b/app-sandbox/build.gradle.kts index 4b357f9c9..0e7ca7de5 100644 --- a/app-sandbox/build.gradle.kts +++ b/app-sandbox/build.gradle.kts @@ -39,16 +39,15 @@ dependencies { implementation(projects.feature.caseeditor) implementation(projects.core.appnav) - implementation(projects.core.common) implementation(projects.core.commoncase) implementation(projects.core.data) implementation(projects.core.designsystem) + implementation(projects.core.mapmarker) implementation(projects.core.network) implementation(projects.core.ui) implementation(libs.kotlinx.serialization.json) - implementation(libs.accompanist.systemuicontroller) implementation(libs.androidx.activity.compose) implementation(libs.androidx.appcompat) implementation(libs.androidx.core.ktx) diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxActivity.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxActivity.kt index ae56d3c7e..1c3e94f2f 100644 --- a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxActivity.kt +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxActivity.kt @@ -15,7 +15,6 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class SandboxActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { - enableEdgeToEdge() super.onCreate(savedInstanceState) MapsInitializer.initialize(this, MapsInitializer.Renderer.LATEST) {} @@ -24,6 +23,8 @@ class SandboxActivity : ComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) setContent { + enableEdgeToEdge() + CrisisCleanupTheme { SandboxApp( windowSizeClass = calculateWindowSizeClass(this), diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 75b78d515..5ef8ab951 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,7 +14,7 @@ plugins { android { defaultConfig { - val buildVersion = 254 + val buildVersion = 256 applicationId = "com.crisiscleanup" versionCode = buildVersion versionName = "0.9.${buildVersion - 168}" @@ -144,19 +144,8 @@ dependencies { implementation(projects.sync.work) - androidTestImplementation(projects.core.testing) - androidTestImplementation(projects.core.dataTest) - androidTestImplementation(projects.core.datastoreTest) - androidTestImplementation(projects.core.network) - androidTestImplementation(libs.androidx.navigation.testing) - androidTestImplementation(libs.accompanist.testharness) - androidTestImplementation(kotlin("test")) - debugImplementation(libs.androidx.compose.ui.testManifest) - debugImplementation(projects.uiTestHiltManifest) - implementation(libs.kotlinx.serialization.json) - implementation(libs.accompanist.systemuicontroller) implementation(libs.androidx.activity.compose) implementation(libs.androidx.appcompat) implementation(libs.androidx.core.ktx) @@ -184,6 +173,30 @@ dependencies { implementation(libs.kotlinx.coroutines.playservices) implementation(libs.playservices.maps) + + ksp(libs.hilt.compiler) + + debugImplementation(libs.androidx.compose.ui.testManifest) + debugImplementation(projects.uiTestHiltManifest) + + kspTest(libs.hilt.compiler) + + testImplementation(projects.core.dataTest) + testImplementation(projects.core.datastoreTest) + testImplementation(libs.hilt.android.testing) + testImplementation(projects.sync.syncTest) + testImplementation(libs.kotlin.test) + + testDemoImplementation(libs.androidx.navigation.testing) + testDemoImplementation(projects.core.testing) + + androidTestImplementation(projects.core.testing) + androidTestImplementation(projects.core.dataTest) + androidTestImplementation(projects.core.datastoreTest) + androidTestImplementation(libs.androidx.test.espresso.core) + androidTestImplementation(libs.androidx.compose.ui.test) + androidTestImplementation(libs.hilt.android.testing) + androidTestImplementation(libs.kotlin.test) } dependencyGuard { diff --git a/app/src/main/java/com/crisiscleanup/MainActivity.kt b/app/src/main/java/com/crisiscleanup/MainActivity.kt index b813f138d..958c6ffcb 100644 --- a/app/src/main/java/com/crisiscleanup/MainActivity.kt +++ b/app/src/main/java/com/crisiscleanup/MainActivity.kt @@ -3,6 +3,7 @@ package com.crisiscleanup import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels @@ -10,12 +11,12 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.toArgb import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.core.view.WindowCompat import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver @@ -28,7 +29,6 @@ import com.crisiscleanup.core.common.NetworkMonitor import com.crisiscleanup.core.common.PermissionManager import com.crisiscleanup.core.common.PhoneNumberPicker import com.crisiscleanup.core.common.VisualAlertManager -import com.crisiscleanup.core.common.event.AccountEventBus import com.crisiscleanup.core.common.event.TrimMemoryEventManager import com.crisiscleanup.core.common.log.AppLogger import com.crisiscleanup.core.common.log.CrisisCleanupLoggers @@ -42,7 +42,7 @@ import com.crisiscleanup.core.designsystem.theme.navigationContainerColor import com.crisiscleanup.core.model.data.DarkThemeConfig import com.crisiscleanup.sync.initializers.scheduleSyncWorksites import com.crisiscleanup.ui.CrisisCleanupApp -import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.crisiscleanup.ui.rememberCrisisCleanupAppState import com.google.android.gms.maps.MapsInitializer import com.google.android.gms.maps.MapsInitializer.Renderer import dagger.hilt.android.AndroidEntryPoint @@ -67,9 +67,6 @@ class MainActivity : ComponentActivity() { private val viewModel: MainActivityViewModel by viewModels() - @Inject - internal lateinit var accountEventBus: AccountEventBus - @Inject internal lateinit var syncPuller: SyncPuller @@ -101,7 +98,6 @@ class MainActivity : ComponentActivity() { private val lifecycleObservers = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { - enableEdgeToEdge() val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) MapsInitializer.initialize(this, Renderer.LATEST) {} @@ -122,28 +118,27 @@ class MainActivity : ComponentActivity() { viewState is Loading || authState is AuthState.Loading } - // Turn off the decor fitting system windows, which allows us to handle insets, - // including IME animations - WindowCompat.setDecorFitsSystemWindows(window, false) - setContent { - val systemUiController = rememberSystemUiController() val darkTheme = shouldUseDarkTheme(viewState) - // Update the dark content of the system bars to match the theme - DisposableEffect(systemUiController, darkTheme) { - systemUiController.systemBarsDarkContentEnabled = !darkTheme - systemUiController.setSystemBarsColor(navigationContainerColor) - onDispose {} - } - - CrisisCleanupTheme( - darkTheme = darkTheme, - ) { - CrisisCleanupApp( - windowSizeClass = calculateWindowSizeClass(this), - networkMonitor = networkMonitor, - ) + val windowSizeClass = calculateWindowSizeClass(this) + val appState = rememberCrisisCleanupAppState( + networkMonitor = networkMonitor, + windowSizeClass = windowSizeClass, + ) + + val barColor = navigationContainerColor.toArgb() + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.dark(barColor), + navigationBarStyle = SystemBarStyle.dark(barColor), + ) + + CompositionLocalProvider { + CrisisCleanupTheme( + darkTheme = darkTheme, + ) { + CrisisCleanupApp(appState) + } } } diff --git a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt index bbc0e926c..62940560a 100644 --- a/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt +++ b/app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt @@ -27,7 +27,6 @@ import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult -import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -37,7 +36,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned @@ -51,7 +49,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.crisiscleanup.AuthState import com.crisiscleanup.MainActivityViewModel import com.crisiscleanup.MainActivityViewState -import com.crisiscleanup.core.common.NetworkMonitor import com.crisiscleanup.core.common.TutorialStep import com.crisiscleanup.core.designsystem.LayoutProvider import com.crisiscleanup.core.designsystem.LocalAppTranslator @@ -77,12 +74,7 @@ import com.crisiscleanup.feature.authentication.R as authenticationR @Composable fun CrisisCleanupApp( - windowSizeClass: WindowSizeClass, - networkMonitor: NetworkMonitor, - appState: CrisisCleanupAppState = rememberCrisisCleanupAppState( - networkMonitor = networkMonitor, - windowSizeClass = windowSizeClass, - ), + appState: CrisisCleanupAppState, viewModel: MainActivityViewModel = hiltViewModel(), ) { CrisisCleanupBackground { @@ -248,9 +240,6 @@ private fun BoxScope.LoadedContent( } } -@OptIn( - ExperimentalComposeUiApi::class, -) @Composable private fun AuthenticateContent( snackbarHostState: SnackbarHostState, @@ -283,7 +272,6 @@ private fun AuthenticateContent( } } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun AcceptTermsContent( snackbarHostState: SnackbarHostState, @@ -325,9 +313,6 @@ private fun AcceptTermsContent( } } -@OptIn( - ExperimentalComposeUiApi::class, -) @Composable private fun NavigableContent( snackbarHostState: SnackbarHostState, diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index c735fa4a0..6d0237010 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -15,10 +15,10 @@ */ import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `kotlin-dsl` + alias(libs.plugins.android.lint) } group = "com.google.samples.apps.nowinandroid.buildlogic" @@ -46,6 +46,7 @@ dependencies { compileOnly(libs.ksp.gradlePlugin) compileOnly(libs.room.gradlePlugin) implementation(libs.truth) + lintChecks(libs.androidx.lint.gradle) } tasks { @@ -58,59 +59,59 @@ tasks { gradlePlugin { plugins { register("androidApplicationCompose") { - id = "nowinandroid.android.application.compose" + id = libs.plugins.nowinandroid.android.application.compose.get().pluginId implementationClass = "AndroidApplicationComposeConventionPlugin" } register("androidApplication") { - id = "nowinandroid.android.application" + id = libs.plugins.nowinandroid.android.application.asProvider().get().pluginId implementationClass = "AndroidApplicationConventionPlugin" } register("androidApplicationJacoco") { - id = "nowinandroid.android.application.jacoco" + id = libs.plugins.nowinandroid.android.application.jacoco.get().pluginId implementationClass = "AndroidApplicationJacocoConventionPlugin" } register("androidLibraryCompose") { - id = "nowinandroid.android.library.compose" + id = libs.plugins.nowinandroid.android.library.compose.get().pluginId implementationClass = "AndroidLibraryComposeConventionPlugin" } register("androidLibrary") { - id = "nowinandroid.android.library" + id = libs.plugins.nowinandroid.android.library.asProvider().get().pluginId implementationClass = "AndroidLibraryConventionPlugin" } register("androidFeature") { - id = "nowinandroid.android.feature" + id = libs.plugins.nowinandroid.android.feature.get().pluginId implementationClass = "AndroidFeatureConventionPlugin" } register("androidLibraryJacoco") { - id = "nowinandroid.android.library.jacoco" + id = libs.plugins.nowinandroid.android.library.jacoco.get().pluginId implementationClass = "AndroidLibraryJacocoConventionPlugin" } register("androidTest") { - id = "nowinandroid.android.test" + id = libs.plugins.nowinandroid.android.test.get().pluginId implementationClass = "AndroidTestConventionPlugin" } register("hilt") { - id = "nowinandroid.hilt" + id = libs.plugins.nowinandroid.hilt.get().pluginId implementationClass = "HiltConventionPlugin" } register("androidRoom") { - id = "nowinandroid.android.room" + id = libs.plugins.nowinandroid.android.room.get().pluginId implementationClass = "AndroidRoomConventionPlugin" } register("androidFirebase") { - id = "nowinandroid.android.application.firebase" + id = libs.plugins.nowinandroid.android.application.firebase.get().pluginId implementationClass = "AndroidApplicationFirebaseConventionPlugin" } register("androidFlavors") { - id = "nowinandroid.android.application.flavors" + id = libs.plugins.nowinandroid.android.application.flavors.get().pluginId implementationClass = "AndroidApplicationFlavorsConventionPlugin" } register("androidLint") { - id = "nowinandroid.android.lint" + id = libs.plugins.nowinandroid.android.lint.get().pluginId implementationClass = "AndroidLintConventionPlugin" } register("jvmLibrary") { - id = "nowinandroid.jvm.library" + id = libs.plugins.nowinandroid.jvm.library.get().pluginId implementationClass = "JvmLibraryConventionPlugin" } } diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 57777d886..f6b08c62d 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -24,18 +24,17 @@ import com.google.samples.apps.nowinandroid.configureKotlinAndroid import com.google.samples.apps.nowinandroid.configurePrintApksTask import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.getByType class AndroidApplicationConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - with(pluginManager) { - apply("com.android.application") - apply("org.jetbrains.kotlin.android") - apply("nowinandroid.android.lint") - apply("com.dropbox.dependency-guard") - } + apply(plugin = "com.android.application") + apply(plugin = "org.jetbrains.kotlin.android") + apply(plugin = "nowinandroid.android.lint") + apply(plugin = "com.dropbox.dependency-guard") extensions.configure { configureKotlinAndroid(this) @@ -50,5 +49,4 @@ class AndroidApplicationConventionPlugin : Plugin { } } } - } diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt index 422592b8a..be3ec0365 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt @@ -19,23 +19,32 @@ import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension import com.google.samples.apps.nowinandroid.libs import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.exclude class AndroidApplicationFirebaseConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - with(pluginManager) { - apply("com.google.gms.google-services") - apply("com.google.firebase.firebase-perf") - apply("com.google.firebase.crashlytics") - } + apply(plugin = "com.google.gms.google-services") + apply(plugin = "com.google.firebase.firebase-perf") + apply(plugin = "com.google.firebase.crashlytics") dependencies { val bom = libs.findLibrary("firebase-bom").get() - add("implementation", platform(bom)) + "implementation"(platform(bom)) "implementation"(libs.findLibrary("firebase.analytics").get()) - "implementation"(libs.findLibrary("firebase.performance").get()) + "implementation"(libs.findLibrary("firebase.performance").get()) { + /* + Exclusion of protobuf / protolite dependencies is necessary as the + datastore-proto brings in protobuf dependencies. These are the source of truth + for Now in Android. + That's why the duplicate classes from below dependencies are excluded. + */ + exclude(group = "com.google.protobuf", module = "protobuf-javalite") + exclude(group = "com.google.firebase", module = "protolite-well-known-types") + } "implementation"(libs.findLibrary("firebase.crashlytics").get()) } diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt index ac385b0d9..b0eece41d 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationJacocoConventionPlugin.kt @@ -14,18 +14,20 @@ * limitations under the License. */ +import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.variant.ApplicationAndroidComponentsExtension -import com.android.build.gradle.internal.dsl.BaseAppModuleExtension import com.google.samples.apps.nowinandroid.configureJacoco import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.getByType class AndroidApplicationJacocoConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply("jacoco") - val androidExtension = extensions.getByType() + apply(plugin = "jacoco") + + val androidExtension = extensions.getByType() androidExtension.buildTypes.configureEach { enableAndroidTestCoverage = true diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt index 41bb98ee0..6899d90c2 100644 --- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -19,39 +19,42 @@ import com.google.samples.apps.nowinandroid.configureGradleManagedDevices import com.google.samples.apps.nowinandroid.libs import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies class AndroidFeatureConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply { - apply("nowinandroid.android.library") - apply("nowinandroid.hilt") - } + apply(plugin = "nowinandroid.android.library") + apply(plugin = "nowinandroid.hilt") + apply(plugin = "org.jetbrains.kotlin.plugin.serialization") + extensions.configure { testOptions.animationsDisabled = true configureGradleManagedDevices(this) } dependencies { - add("implementation", project(":core:appnav")) - add("implementation", project(":core:common")) - add("implementation", project(":core:data")) - add("implementation", project(":core:designsystem")) - add("implementation", project(":core:model")) - add("implementation", project(":core:ui")) + "implementation"(project(":core:appnav")) + "implementation"(project(":core:common")) + "implementation"(project(":core:data")) + "implementation"(project(":core:designsystem")) + "implementation"(project(":core:model")) + "implementation"(project(":core:ui")) - add("implementation", libs.findLibrary("coil.kt").get()) - add("implementation", libs.findLibrary("coil.kt.compose").get()) + "implementation"(libs.findLibrary("coil.kt").get()) + "implementation"(libs.findLibrary("coil.kt.compose").get()) - add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get()) - add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) - add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) - add("implementation", libs.findLibrary("androidx.tracing.ktx").get()) + "implementation"(libs.findLibrary("androidx.hilt.navigation.compose").get()) + "implementation"(libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) + "implementation"(libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) + "implementation"(libs.findLibrary("androidx.navigation.compose").get()) + "implementation"(libs.findLibrary("androidx.tracing.ktx").get()) + "implementation"(libs.findLibrary("kotlinx.serialization.json").get()) - add( - "androidTestImplementation", + "testImplementation"(libs.findLibrary("androidx.navigation.testing").get()) + "androidTestImplementation"( libs.findLibrary("androidx.lifecycle.runtimeTesting").get(), ) } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 62af43f5c..0042258f3 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -25,22 +25,21 @@ import com.google.samples.apps.nowinandroid.disableUnnecessaryAndroidTests import com.google.samples.apps.nowinandroid.libs import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies -import org.gradle.kotlin.dsl.kotlin class AndroidLibraryConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - with(pluginManager) { - apply("com.android.library") - apply("org.jetbrains.kotlin.android") - apply("nowinandroid.android.lint") - } + apply(plugin = "com.android.library") + apply(plugin = "org.jetbrains.kotlin.android") + apply(plugin = "nowinandroid.android.lint") extensions.configure { configureKotlinAndroid(this) defaultConfig.targetSdk = DefaultConfigTargetSdk + defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testOptions.animationsDisabled = true configureFlavors(this) configureGradleManagedDevices(this) @@ -53,10 +52,10 @@ class AndroidLibraryConventionPlugin : Plugin { disableUnnecessaryAndroidTests(target) } dependencies { - add("androidTestImplementation", kotlin("test")) - add("testImplementation", kotlin("test")) + "androidTestImplementation"(libs.findLibrary("kotlin.test").get()) + "testImplementation"(libs.findLibrary("kotlin.test").get()) - add("implementation", libs.findLibrary("androidx.tracing.ktx").get()) + "implementation"(libs.findLibrary("androidx.tracing.ktx").get()) } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt index 6f2ff60c5..d249e4cbf 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryJacocoConventionPlugin.kt @@ -15,17 +15,18 @@ */ import com.android.build.api.dsl.LibraryExtension -import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.LibraryAndroidComponentsExtension import com.google.samples.apps.nowinandroid.configureJacoco import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.getByType class AndroidLibraryJacocoConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply("jacoco") + apply(plugin = "jacoco") + val androidExtension = extensions.getByType() androidExtension.buildTypes.configureEach { diff --git a/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt index 1734df930..884d6f076 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt @@ -19,6 +19,7 @@ import com.android.build.api.dsl.LibraryExtension import com.android.build.api.dsl.Lint import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure class AndroidLintConventionPlugin : Plugin { @@ -32,7 +33,7 @@ class AndroidLintConventionPlugin : Plugin { configure { lint(Lint::configure) } else -> { - pluginManager.apply("com.android.lint") + apply(plugin = "com.android.lint") configure(Lint::configure) } } @@ -42,5 +43,7 @@ class AndroidLintConventionPlugin : Plugin { private fun Lint.configure() { xmlReport = true + sarifReport = true checkDependencies = true + disable += "GradleDependency" } diff --git a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt index dbca79a5e..6919b5e5d 100644 --- a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt @@ -19,6 +19,7 @@ import com.google.devtools.ksp.gradle.KspExtension import com.google.samples.apps.nowinandroid.libs import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies @@ -26,8 +27,8 @@ class AndroidRoomConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply("androidx.room") - pluginManager.apply("com.google.devtools.ksp") + apply(plugin = "androidx.room") + apply(plugin = "com.google.devtools.ksp") extensions.configure { arg("room.generateKotlin", "true") @@ -41,10 +42,10 @@ class AndroidRoomConventionPlugin : Plugin { } dependencies { - add("implementation", libs.findLibrary("room.runtime").get()) - add("implementation", libs.findLibrary("room.ktx").get()) - add("ksp", libs.findLibrary("room.compiler").get()) + "implementation"(libs.findLibrary("room.runtime").get()) + "implementation"(libs.findLibrary("room.ktx").get()) + "ksp"(libs.findLibrary("room.compiler").get()) } } } -} \ No newline at end of file +} diff --git a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt index 2c953227e..f7094a5d9 100644 --- a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt @@ -20,15 +20,14 @@ import com.google.samples.apps.nowinandroid.configureGradleManagedDevices import com.google.samples.apps.nowinandroid.configureKotlinAndroid import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure class AndroidTestConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - with(pluginManager) { - apply("com.android.test") - apply("org.jetbrains.kotlin.android") - } + apply(plugin = "com.android.test") + apply(plugin = "org.jetbrains.kotlin.android") extensions.configure { configureKotlinAndroid(this) @@ -37,5 +36,4 @@ class AndroidTestConventionPlugin : Plugin { } } } - } diff --git a/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt b/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt index a8228e5af..5a90ff98f 100644 --- a/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt @@ -18,22 +18,30 @@ import com.android.build.gradle.api.AndroidBasePlugin import com.google.samples.apps.nowinandroid.libs import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.dependencies class HiltConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - pluginManager.apply("com.google.devtools.ksp") + apply(plugin = "com.google.devtools.ksp") + dependencies { - add("ksp", libs.findLibrary("hilt.compiler").get()) - add("implementation", libs.findLibrary("hilt.core").get()) + "ksp"(libs.findLibrary("hilt.compiler").get()) + } + + // Add support for Jvm Module, base on org.jetbrains.kotlin.jvm + pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { + dependencies { + "implementation"(libs.findLibrary("hilt.core").get()) + } } /** Add support for Android modules, based on [AndroidBasePlugin] */ pluginManager.withPlugin("com.android.base") { - pluginManager.apply("dagger.hilt.android.plugin") + apply(plugin = "dagger.hilt.android.plugin") dependencies { - add("implementation", libs.findLibrary("hilt.android").get()) + "implementation"(libs.findLibrary("hilt.android").get()) } } } diff --git a/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt index 35932c835..a1477891d 100644 --- a/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt @@ -15,17 +15,22 @@ */ import com.google.samples.apps.nowinandroid.configureKotlinJvm +import com.google.samples.apps.nowinandroid.libs import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.dependencies class JvmLibraryConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { - with(pluginManager) { - apply("org.jetbrains.kotlin.jvm") - apply("nowinandroid.android.lint") - } + apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "nowinandroid.android.lint") + configureKotlinJvm() + dependencies { + "testImplementation"(libs.findLibrary("kotlin.test").get()) + } } } } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt index 74e355691..ed2a5289b 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidCompose.kt @@ -19,6 +19,7 @@ package com.google.samples.apps.nowinandroid import com.android.build.api.dsl.CommonExtension import org.gradle.api.Project import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension @@ -36,10 +37,10 @@ internal fun Project.configureAndroidCompose( dependencies { val bom = libs.findLibrary("androidx-compose-bom").get() - add("implementation", platform(bom)) - add("androidTestImplementation", platform(bom)) - add("implementation", libs.findLibrary("androidx-compose-ui-tooling-preview").get()) - add("debugImplementation", libs.findLibrary("androidx-compose-ui-tooling").get()) + "implementation"(platform(bom)) + "androidTestImplementation"(platform(bom)) + "implementation"(libs.findLibrary("androidx-compose-ui-tooling-preview").get()) + "debugImplementation"(libs.findLibrary("androidx-compose-ui-tooling").get()) } testOptions { @@ -52,8 +53,10 @@ internal fun Project.configureAndroidCompose( extensions.configure { fun Provider.onlyIfTrue() = flatMap { provider { it.takeIf(String::toBoolean) } } - fun Provider<*>.relativeToRootProject(dir: String) = flatMap { - rootProject.layout.buildDirectory.dir(projectDir.toRelativeString(rootDir)) + fun Provider<*>.relativeToRootProject(dir: String) = map { + isolated.rootProject.projectDirectory + .dir("build") + .dir(projectDir.toRelativeString(rootDir)) }.map { it.dir(dir) } project.providers.gradleProperty("enableComposeCompilerMetrics").onlyIfTrue() @@ -64,10 +67,7 @@ internal fun Project.configureAndroidCompose( .relativeToRootProject("compose-reports") .let(reportsDestination::set) - // TODO How does this work when building NIA repo -// stabilityConfigurationFile = -// rootProject.layout.projectDirectory.file("compose_compiler_config.conf") -// -// enableStrongSkippingMode = true + stabilityConfigurationFiles + .add(isolated.rootProject.projectDirectory.file("compose_compiler_config.conf")) } } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidInstrumentedTests.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidInstrumentedTests.kt index d0c26e4e6..c51dac5c9 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidInstrumentedTests.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/AndroidInstrumentedTests.kt @@ -30,6 +30,6 @@ import org.gradle.api.Project internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests( project: Project, ) = beforeVariants { - it.enableAndroidTest = it.enableAndroidTest + it.androidTest.enable = it.androidTest.enable && project.projectDir.resolve("src/androidTest").exists() } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt index 4447b8602..886c70625 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt @@ -35,12 +35,10 @@ import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction -import org.gradle.configurationcache.extensions.capitalized import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.register import org.gradle.language.base.plugins.LifecycleBasePlugin import org.gradle.process.ExecOperations -import java.io.File import javax.inject.Inject @CacheableTask @@ -107,6 +105,10 @@ abstract class CheckBadgingTask : DefaultTask() { } } +private fun String.capitalized() = replaceFirstChar { + if (it.isLowerCase()) it.titlecase() else it.toString() +} + fun Project.configureBadgingTasks( baseExtension: BaseExtension, componentsExtension: ApplicationAndroidComponentsExtension, @@ -119,15 +121,17 @@ fun Project.configureBadgingTasks( val generateBadging = tasks.register(generateBadgingTaskName) { apk = variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE) - - aapt2Executable = File( - baseExtension.sdkDirectory, - "${SdkConstants.FD_BUILD_TOOLS}/" + - "${baseExtension.buildToolsVersion}/" + - SdkConstants.FN_AAPT2, + aapt2Executable.set( + // TODO: Replace with `sdkComponents.aapt2` when it's available in AGP + // https://issuetracker.google.com/issues/376815836 + componentsExtension.sdkComponents.sdkDirectory.map { directory -> + directory.file( + "${SdkConstants.FD_BUILD_TOOLS}/" + + "${baseExtension.buildToolsVersion}/" + + SdkConstants.FN_AAPT2, + ) + } ) - - badging = project.layout.buildDirectory.file( "outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt", ) @@ -136,7 +140,7 @@ fun Project.configureBadgingTasks( val updateBadgingTaskName = "update${capitalizedVariantName}Badging" tasks.register(updateBadgingTaskName) { - from(generateBadging.get().badging) + from(generateBadging.map(GenerateBadgingTask::badging)) into(project.layout.projectDirectory) } @@ -144,7 +148,7 @@ fun Project.configureBadgingTasks( tasks.register(checkBadgingTaskName) { goldenBadging = project.layout.projectDirectory.file("${variant.name}-badging.txt") - generatedBadging = generateBadging.get().badging + generatedBadging.set(generateBadging.flatMap(GenerateBadgingTask::badging)) this.updateBadgingTaskName = updateBadgingTaskName diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt index 972d539c6..ed1ea4254 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Jacoco.kt @@ -19,10 +19,12 @@ package com.google.samples.apps.nowinandroid import com.android.build.api.artifact.ScopedArtifact import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.api.variant.ScopedArtifacts +import com.android.build.api.variant.SourceDirectories import org.gradle.api.Project import org.gradle.api.file.Directory import org.gradle.api.file.RegularFile import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Provider import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.configure @@ -88,11 +90,14 @@ internal fun Project.configureJacoco( html.required = true } - // TODO: This is missing files in src/debug/, src/prod, src/demo, src/demoDebug... + fun SourceDirectories.Flat?.toFilePaths(): Provider> = this + ?.all + ?.map { directories -> directories.map { it.asFile.path } } + ?: provider { emptyList() } sourceDirectories.setFrom( files( - "$projectDir/src/main/java", - "$projectDir/src/main/kotlin", + variant.sources.java.toFilePaths(), + variant.sources.kotlin.toFilePaths() ), ) diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt index 3fa169300..e57a60873 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt @@ -23,11 +23,10 @@ import org.gradle.api.plugins.JavaPluginExtension import org.gradle.kotlin.dsl.assign import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies -import org.gradle.kotlin.dsl.provideDelegate import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinTopLevelExtension internal const val DefaultConfigTargetSdk = 35 @@ -56,7 +55,7 @@ internal fun Project.configureKotlinAndroid( configureKotlin() dependencies { - add("coreLibraryDesugaring", libs.findLibrary("android.desugarJdkLibs").get()) + "coreLibraryDesugaring"(libs.findLibrary("android.desugarJdkLibs").get()) } } @@ -77,20 +76,36 @@ internal fun Project.configureKotlinJvm() { /** * Configure base Kotlin options */ -private inline fun Project.configureKotlin() = configure { +private inline fun Project.configureKotlin() = configure { // Treat all Kotlin warnings as errors (disabled by default) // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties - val warningsAsErrors: String? by project + val warningsAsErrors = providers.gradleProperty("warningsAsErrors").map { + it.toBoolean() + }.orElse(false) when (this) { is KotlinAndroidProjectExtension -> compilerOptions is KotlinJvmProjectExtension -> compilerOptions else -> TODO("Unsupported project extension $this ${T::class}") }.apply { jvmTarget = JvmTarget.JVM_17 - allWarningsAsErrors = warningsAsErrors.toBoolean() + allWarningsAsErrors = warningsAsErrors freeCompilerArgs.add( // Enable experimental coroutines APIs, including Flow "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", ) + freeCompilerArgs.add( + /** + * Remove this args after Phase 3. + * https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-consistent-copy-visibility/#deprecation-timeline + * + * Deprecation timeline + * Phase 3. (Supposedly Kotlin 2.2 or Kotlin 2.3). + * The default changes. + * Unless ExposedCopyVisibility is used, the generated 'copy' method has the same visibility as the primary constructor. + * The binary signature changes. The error on the declaration is no longer reported. + * '-Xconsistent-data-class-copy-visibility' compiler flag and ConsistentCopyVisibility annotation are now unnecessary. + */ + "-Xconsistent-data-class-copy-visibility" + ) } } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt index cba50289c..09e0cd5c8 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/NiaFlavor.kt @@ -23,18 +23,21 @@ enum class NiaFlavor(val dimension: FlavorDimension, val applicationIdSuffix: St fun configureFlavors( commonExtension: CommonExtension<*, *, *, *, *, *>, - flavorConfigurationBlock: ProductFlavor.(flavor: NiaFlavor) -> Unit = {} + flavorConfigurationBlock: ProductFlavor.(flavor: NiaFlavor) -> Unit = {}, ) { commonExtension.apply { - flavorDimensions += FlavorDimension.contentType.name + FlavorDimension.values().forEach { flavorDimension -> + flavorDimensions += flavorDimension.name + } + productFlavors { - NiaFlavor.values().forEach { - create(it.name) { - dimension = it.dimension.name - flavorConfigurationBlock(this, it) + NiaFlavor.values().forEach { niaFlavor -> + register(niaFlavor.name) { + dimension = niaFlavor.dimension.name + flavorConfigurationBlock(this, niaFlavor) if (this@apply is ApplicationExtension && this is ApplicationProductFlavor) { - if (it.applicationIdSuffix != null) { - applicationIdSuffix = it.applicationIdSuffix + if (niaFlavor.applicationIdSuffix != null) { + applicationIdSuffix = niaFlavor.applicationIdSuffix } } } diff --git a/build-logic/gradle.properties b/build-logic/gradle.properties index 1c9073eb9..5e07c65d0 100644 --- a/build-logic/gradle.properties +++ b/build-logic/gradle.properties @@ -2,3 +2,5 @@ org.gradle.parallel=true org.gradle.caching=true org.gradle.configureondemand=true +org.gradle.configuration-cache=true +org.gradle.configuration-cache.parallel=true diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index de9224e22..ff96cc84a 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -14,9 +14,22 @@ * limitations under the License. */ -dependencyResolutionManagement { +pluginManagement { repositories { + gradlePluginPortal() google() + } +} + +dependencyResolutionManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } mavenCentral() } versionCatalogs { diff --git a/core/addresssearch/src/main/java/com/crisiscleanup/core/addresssearch/AndroidGeocoderAddressSearchRepository.kt b/core/addresssearch/src/main/java/com/crisiscleanup/core/addresssearch/AndroidGeocoderAddressSearchRepository.kt deleted file mode 100644 index e5165b381..000000000 --- a/core/addresssearch/src/main/java/com/crisiscleanup/core/addresssearch/AndroidGeocoderAddressSearchRepository.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.crisiscleanup.core.addresssearch - -import android.location.Geocoder -import com.crisiscleanup.core.addresssearch.model.asLocationAddress -import com.crisiscleanup.core.model.data.LocationAddress -import com.google.android.gms.maps.model.LatLng - -internal fun Geocoder.getAddress(coordinates: LatLng): LocationAddress? { - val results = getFromLocation(coordinates.latitude, coordinates.longitude, 1) - return results?.firstOrNull()?.asLocationAddress()?.copy( - latitude = coordinates.latitude, - longitude = coordinates.longitude, - ) -} diff --git a/core/addresssearch/src/main/java/com/crisiscleanup/core/addresssearch/GooglePlaceAddressSearchRepository.kt b/core/addresssearch/src/main/java/com/crisiscleanup/core/addresssearch/GooglePlaceAddressSearchRepository.kt index d72e369fa..79325b3c4 100644 --- a/core/addresssearch/src/main/java/com/crisiscleanup/core/addresssearch/GooglePlaceAddressSearchRepository.kt +++ b/core/addresssearch/src/main/java/com/crisiscleanup/core/addresssearch/GooglePlaceAddressSearchRepository.kt @@ -2,8 +2,10 @@ package com.crisiscleanup.core.addresssearch import android.content.Context import android.location.Geocoder +import android.os.Build import android.util.LruCache import com.crisiscleanup.core.addresssearch.model.KeySearchAddress +import com.crisiscleanup.core.addresssearch.model.asLocationAddress import com.crisiscleanup.core.common.AppSettingsProvider import com.crisiscleanup.core.common.combineTrimText import com.crisiscleanup.core.common.log.AppLogger @@ -25,6 +27,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CancellationException import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.tasks.await @@ -32,6 +35,8 @@ import kotlinx.datetime.Clock import kotlinx.datetime.Instant import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException import kotlin.time.Duration.Companion.hours class GooglePlaceAddressSearchRepository @Inject constructor( @@ -75,7 +80,35 @@ class GooglePlaceAddressSearchRepository @Inject constructor( ) } - override suspend fun getAddress(coordinates: LatLng) = geocoder.getAddress(coordinates) + override suspend fun getAddress(coordinates: LatLng): LocationAddress? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + val results = geocoder.getFromLocation(coordinates.latitude, coordinates.longitude, 1) + return results?.firstOrNull()?.asLocationAddress()?.copy( + latitude = coordinates.latitude, + longitude = coordinates.longitude, + ) + } else { + return suspendCancellableCoroutine { continuation -> + val listener = Geocoder.GeocodeListener { results -> + val firstAddress = results.firstOrNull()?.asLocationAddress()?.copy( + latitude = coordinates.latitude, + longitude = coordinates.longitude, + ) + continuation.resume(firstAddress) + } + try { + geocoder.getFromLocation( + coordinates.latitude, + coordinates.longitude, + 1, + listener, + ) + } catch (e: Exception) { + continuation.resumeWithException(e) + } + } + } + } override fun startSearchSession() { val token = AutocompleteSessionToken.newInstance() @@ -142,7 +175,7 @@ class GooglePlaceAddressSearchRepository @Inject constructor( override suspend fun getPlaceAddress(placeId: String): LocationAddress? { val sessionToken = getSessionToken() val placeFields = listOf( - Place.Field.LAT_LNG, + Place.Field.LOCATION, Place.Field.ADDRESS_COMPONENTS, ) @@ -162,7 +195,7 @@ class GooglePlaceAddressSearchRepository @Inject constructor( "postal_code", ) with(response.place) { - latLng?.let { coordinates -> + location?.let { coordinates -> addressComponents?.asList()?.let { components -> val addressComponentLookup = mutableMapOf() components.forEach { diff --git a/core/common/src/main/java/com/crisiscleanup/core/common/PhoneNumberUtil.kt b/core/common/src/main/java/com/crisiscleanup/core/common/PhoneNumberUtil.kt index 2bacd614b..d6de39b54 100644 --- a/core/common/src/main/java/com/crisiscleanup/core/common/PhoneNumberUtil.kt +++ b/core/common/src/main/java/com/crisiscleanup/core/common/PhoneNumberUtil.kt @@ -120,8 +120,9 @@ object PhoneNumberUtil { return it } - if (noShortWords.trim().length in 10..15) { - if (noShortWords.length <= 12) { + val trimmedLength = noShortWords.trim().length + if (trimmedLength in 10..15) { + if (trimmedLength <= 12) { separated37Pattern.find(noShortWords)?.let { return singleParsedNumber("${it.groupValues[1]}${it.groupValues[2]}") } diff --git a/core/common/src/test/java/com/crisiscleanup/core/common/PhoneNumberUtilTest.kt b/core/common/src/test/java/com/crisiscleanup/core/common/PhoneNumberUtilTest.kt index c34aaf2e4..11186cf35 100644 --- a/core/common/src/test/java/com/crisiscleanup/core/common/PhoneNumberUtilTest.kt +++ b/core/common/src/test/java/com/crisiscleanup/core/common/PhoneNumberUtilTest.kt @@ -2,6 +2,7 @@ package com.crisiscleanup.core.common import org.junit.Test import kotlin.test.assertEquals +import kotlin.test.assertNull class PhoneNumberUtilTest { @Test @@ -14,7 +15,7 @@ class PhoneNumberUtilTest { ) for (input in inputs) { val actual = PhoneNumberUtil.parsePhoneNumbers(input)?.parsedNumbers - assertEquals(null, actual) + assertNull(actual) } } @@ -119,10 +120,6 @@ class PhoneNumberUtilTest { "2345678901", "3456789012", ), - "2345678901 or 3456789012" to listOf( - "2345678901", - "3456789012", - ), "1.7068339198" to listOf("7068339198"), "(23456789012" to listOf("23456789012"), "18002345678901" to listOf("8002345678901"), diff --git a/core/commoncase/build.gradle.kts b/core/commoncase/build.gradle.kts index ea324eefd..02ffa0e2f 100644 --- a/core/commoncase/build.gradle.kts +++ b/core/commoncase/build.gradle.kts @@ -8,11 +8,18 @@ android { } dependencies { - implementation(projects.core.common) + api(projects.core.common) implementation(projects.core.commonassets) + implementation(projects.core.data) implementation(projects.core.designsystem) + implementation(projects.core.mapmarker) implementation(projects.core.model) implementation(projects.core.ui) + implementation(libs.androidx.constraintlayout) + + implementation(libs.google.maps.compose) + implementation(libs.playservices.maps) + implementation(libs.kotlinx.datetime) } \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/Button.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/Button.kt index 391ea2225..5b45556d1 100644 --- a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/Button.kt +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/Button.kt @@ -2,26 +2,28 @@ package com.crisiscleanup.core.designsystem.component import androidx.annotation.StringRes import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ripple.LocalRippleTheme import androidx.compose.material.ripple.RippleAlpha -import androidx.compose.material.ripple.RippleTheme import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonElevation import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.FloatingActionButtonElevation import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalRippleConfiguration import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.RippleConfiguration import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.contentColorFor @@ -85,6 +87,7 @@ private val fabPlusSpaceHeight = 48.dp.plus(actionEdgeSpace.times(2)) fun Modifier.actionHeight() = this.heightIn(min = 48.dp) fun Modifier.actionSize() = this.size(48.dp) fun Modifier.actionSmallSize() = this.size(44.dp) +fun Modifier.actionSmallWidth() = this.size(width = 44.dp, height = 0.dp) fun Modifier.fabPlusSpaceHeight() = this.size(fabPlusSpaceHeight) @Composable @@ -213,6 +216,7 @@ fun CrisisCleanupOutlinedButton( borderColor: Color = LocalContentColor.current, fontWeight: FontWeight? = null, style: TextStyle = LocalFontStyles.current.header4, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, trailingContent: (@Composable () -> Unit)? = null, ) { val border = BorderStroke( @@ -226,6 +230,7 @@ fun CrisisCleanupOutlinedButton( enabled = enabled, shape = roundedRectangleButtonShape(), border = border, + contentPadding = contentPadding, ) { if (indicateBusy) { CircularProgressIndicator(Modifier.size(LocalDimensions.current.buttonSpinnerSize)) @@ -240,14 +245,6 @@ fun CrisisCleanupOutlinedButton( } } -private val NoRippleTheme = object : RippleTheme { - @Composable - override fun defaultColor() = Color.Transparent - - @Composable - override fun rippleAlpha() = RippleAlpha(0f, 0f, 0f, 0f) -} - private val fabElevationZero: FloatingActionButtonElevation @Composable get() = FloatingActionButtonDefaults.elevation( @@ -257,6 +254,15 @@ private val fabElevationZero: FloatingActionButtonElevation hoveredElevation = 0.dp, ) +@OptIn(ExperimentalMaterial3Api::class) +private val disabledRippleConfiguration: RippleConfiguration + @Composable + get() = RippleConfiguration( + color = Color.Transparent, + rippleAlpha = RippleAlpha(0f, 0f, 0f, 0f), + ) + +@OptIn(ExperimentalMaterial3Api::class) @Composable fun CrisisCleanupFab( onClick: () -> Unit, @@ -268,27 +274,32 @@ fun CrisisCleanupFab( elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), iconContent: @Composable () -> Unit = {}, ) { - CompositionLocalProvider( - LocalRippleTheme provides if (enabled) LocalRippleTheme.current else NoRippleTheme, - ) { - // TODO Complex conditions below are due to some bug when shape is not a circle. - // Container color would not need the condition if elevation changed as expected. - // Elevation does not change as expected when shape is not a circle. - // File a bug or wait for fixes in the future. + if (enabled) { FloatingActionButton( modifier = modifier, - containerColor = if (enabled || shape != CircleShape) containerColor else disabledButtonContainerColor, - contentColor = if (enabled) contentColor else disabledButtonContentColor, - elevation = if (enabled) elevation else fabElevationZero, + containerColor = containerColor, + contentColor = contentColor, + elevation = elevation, shape = shape, - onClick = { - if (enabled) { - onClick() - } - }, + onClick = onClick, ) { iconContent() } + } else { + CompositionLocalProvider( + LocalRippleConfiguration provides disabledRippleConfiguration, + ) { + FloatingActionButton( + modifier = modifier, + containerColor = disabledButtonContainerColor, + contentColor = disabledButtonContentColor, + elevation = fabElevationZero, + shape = shape, + onClick = {}, + ) { + iconContent() + } + } } } diff --git a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/CrisisCleanupIncidentLocationBounder.kt b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/CrisisCleanupIncidentLocationBounder.kt index 387aedd55..f4bfe892a 100644 --- a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/CrisisCleanupIncidentLocationBounder.kt +++ b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/CrisisCleanupIncidentLocationBounder.kt @@ -1,6 +1,5 @@ package com.crisiscleanup.core.mapmarker -import androidx.lifecycle.AtomicReference import com.crisiscleanup.core.data.repository.IncidentsRepository import com.crisiscleanup.core.data.repository.LocationsRepository import com.crisiscleanup.core.mapmarker.model.IncidentBounds @@ -10,6 +9,7 @@ import com.crisiscleanup.core.model.data.EmptyIncident import com.crisiscleanup.core.model.data.IncidentLocation import com.crisiscleanup.core.model.data.IncidentLocationBounder import com.google.android.gms.maps.model.LatLng +import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject class CrisisCleanupIncidentLocationBounder @Inject constructor( diff --git a/core/selectincident/src/main/java/com/crisiscleanup/core/selectincident/SelectIncidentDialog.kt b/core/selectincident/src/main/java/com/crisiscleanup/core/selectincident/SelectIncidentDialog.kt index f7b153bc7..5b2b03aca 100644 --- a/core/selectincident/src/main/java/com/crisiscleanup/core/selectincident/SelectIncidentDialog.kt +++ b/core/selectincident/src/main/java/com/crisiscleanup/core/selectincident/SelectIncidentDialog.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.lazy.LazyColumn @@ -17,8 +16,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.pulltorefresh.PullToRefreshContainer -import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -29,7 +27,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp @@ -148,21 +145,27 @@ private fun ColumnScope.IncidentSelectContent( } val listState = rememberLazyListState() - val pullRefreshState = rememberPullToRefreshState() val coroutineScope = rememberCoroutineScope() - if (pullRefreshState.isRefreshing) { - LaunchedEffect(true) { - onRefreshIncidents() - pullRefreshState.endRefresh() + var isRefreshingIncidents by remember { mutableStateOf(false) } + val refreshIncidents = remember(onRefreshIncidents, listState) { + { coroutineScope.launch { - listState.animateScrollToItem(0) + isRefreshingIncidents = true + try { + onRefreshIncidents() + listState.animateScrollToItem(0) + } finally { + isRefreshingIncidents = false + } } + Unit } } - Box( - Modifier - .weight(weight = 1f, fill = false) - .nestedScroll(pullRefreshState.nestedScrollConnection), + PullToRefreshBox( + modifier = Modifier + .weight(weight = 1f, fill = false), + isRefreshing = isRefreshingIncidents, + onRefresh = refreshIncidents, ) { LazyColumn( state = listState, @@ -190,12 +193,6 @@ private fun ColumnScope.IncidentSelectContent( ) } } - PullToRefreshContainer( - modifier = Modifier - .align(Alignment.TopCenter) - .offset(y = (-64).dp), - state = pullRefreshState, - ) LaunchedEffect(Unit) { val selectedIndex = incidents.indexOfFirst { it.id == selectedIncidentId } if (selectedIndex > 0) { diff --git a/feature/incidentcache/src/main/kotlin/com/crisiscleanup/feature/incidentcache/IncidentWorksitesCacheViewModel.kt b/feature/incidentcache/src/main/kotlin/com/crisiscleanup/feature/incidentcache/IncidentWorksitesCacheViewModel.kt index 2fbc49936..5acc05b5b 100644 --- a/feature/incidentcache/src/main/kotlin/com/crisiscleanup/feature/incidentcache/IncidentWorksitesCacheViewModel.kt +++ b/feature/incidentcache/src/main/kotlin/com/crisiscleanup/feature/incidentcache/IncidentWorksitesCacheViewModel.kt @@ -1,6 +1,5 @@ package com.crisiscleanup.feature.incidentcache -import androidx.lifecycle.AtomicReference import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.crisiscleanup.core.common.AppEnv @@ -38,6 +37,7 @@ import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.Instant import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject import kotlin.time.Duration.Companion.seconds diff --git a/feature/lists/src/main/kotlin/com/crisiscleanup/feature/crisiscleanuplists/ui/ListsScreen.kt b/feature/lists/src/main/kotlin/com/crisiscleanup/feature/crisiscleanuplists/ui/ListsScreen.kt index 2d09da522..e8fa9cbe4 100644 --- a/feature/lists/src/main/kotlin/com/crisiscleanup/feature/crisiscleanuplists/ui/ListsScreen.kt +++ b/feature/lists/src/main/kotlin/com/crisiscleanup/feature/crisiscleanuplists/ui/ListsScreen.kt @@ -1,8 +1,6 @@ package com.crisiscleanup.feature.crisiscleanuplists.ui -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -18,8 +16,7 @@ import androidx.compose.material3.TabRow import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text -import androidx.compose.material3.pulltorefresh.PullToRefreshContainer -import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -27,10 +24,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp @@ -45,9 +39,8 @@ import com.crisiscleanup.core.commonassets.getDisasterIcon import com.crisiscleanup.core.commoncase.ui.IncidentHeaderView import com.crisiscleanup.core.designsystem.LocalAppTranslator import com.crisiscleanup.core.designsystem.component.BusyIndicatorFloatingTopCenter -import com.crisiscleanup.core.designsystem.component.CrisisCleanupAlertDialog import com.crisiscleanup.core.designsystem.component.CrisisCleanupIconButton -import com.crisiscleanup.core.designsystem.component.CrisisCleanupTextButton +import com.crisiscleanup.core.designsystem.component.HelpDialog import com.crisiscleanup.core.designsystem.component.TopAppBarBackCaretAction import com.crisiscleanup.core.designsystem.icon.CrisisCleanupIcons import com.crisiscleanup.core.designsystem.icon.Icon @@ -66,11 +59,9 @@ import com.crisiscleanup.core.model.data.ListModel import com.crisiscleanup.feature.crisiscleanuplists.ListsViewModel import com.crisiscleanup.feature.crisiscleanuplists.model.ListIcon import kotlinx.coroutines.launch -import kotlin.math.min @OptIn( ExperimentalMaterial3Api::class, - ExperimentalFoundationApi::class, ) @Composable internal fun ListsRoute( @@ -114,11 +105,19 @@ internal fun ListsRoute( }, ) - val pullRefreshState = rememberPullToRefreshState() - if (pullRefreshState.isRefreshing) { - LaunchedEffect(true) { - viewModel.refreshLists(true) - pullRefreshState.endRefresh() + val coroutineScope = rememberCoroutineScope() + var isRefreshingLists by remember { mutableStateOf(false) } + val refreshLists = remember(viewModel) { + { + coroutineScope.launch { + isRefreshingLists = true + try { + viewModel.refreshLists(true) + } finally { + isRefreshingLists = false + } + } + Unit } } @@ -180,10 +179,11 @@ internal fun ListsRoute( } } - Box( - Modifier - .weight(1f) - .nestedScroll(pullRefreshState.nestedScrollConnection), + PullToRefreshBox( + modifier = Modifier + .weight(1f), + isRefreshing = isRefreshingLists, + onRefresh = refreshLists, ) { HorizontalPager(state = pagerState) { pagerIndex -> when (pagerIndex) { @@ -201,30 +201,16 @@ internal fun ListsRoute( } BusyIndicatorFloatingTopCenter(isLoading) - - val pullProgress = min(pullRefreshState.progress * 1.5f, 1.0f) - PullToRefreshContainer( - modifier = Modifier - .align(Alignment.TopCenter) - .alpha(pullProgress), - state = pullRefreshState, - ) } if (explainSupportList != EmptyList) { val dismissExplanation = { explainSupportList = EmptyList } // TODO Different title and message for list type none - CrisisCleanupAlertDialog( + HelpDialog( title = t("list.unsupported_list_title"), text = t("list.unsupported_list_explanation") .replace("{list_name}", explainSupportList.name), - onDismissRequest = dismissExplanation, - confirmButton = { - CrisisCleanupTextButton( - text = t("actions.ok"), - onClick = dismissExplanation, - ) - }, + onClose = dismissExplanation, ) } } @@ -233,17 +219,10 @@ internal fun ListsRoute( val readOnlyTitle = t("list.list_read_only") val readOnlyDescription = t("list.read_only_in_app_manage_on_web") - CrisisCleanupAlertDialog( - onDismissRequest = { showReadOnlyDescription = false }, + HelpDialog( title = readOnlyTitle, text = readOnlyDescription, - confirmButton = { - CrisisCleanupTextButton( - text = t("actions.ok"), - ) { - showReadOnlyDescription = false - } - }, + onClose = { showReadOnlyDescription = false }, ) } } @@ -396,9 +375,9 @@ internal fun ListItemSummaryView( Text( "${list.name} (${list.objectIds.size})", + Modifier.weight(1f), style = LocalFontStyles.current.header3, ) - Spacer(Modifier.weight(1f)) Text(list.updatedAt.relativeTime) } diff --git a/feature/team/src/main/java/com/crisiscleanup/feature/team/ui/TeamsScreen.kt b/feature/team/src/main/java/com/crisiscleanup/feature/team/ui/TeamsScreen.kt index 0f7aafa16..83ebd0b0c 100644 --- a/feature/team/src/main/java/com/crisiscleanup/feature/team/ui/TeamsScreen.kt +++ b/feature/team/src/main/java/com/crisiscleanup/feature/team/ui/TeamsScreen.kt @@ -14,18 +14,16 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text -import androidx.compose.material3.pulltorefresh.PullToRefreshContainer -import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.crisiscleanup.core.appcomponent.ui.AppTopBar @@ -50,6 +48,7 @@ import com.crisiscleanup.core.model.data.Incident import com.crisiscleanup.core.selectincident.SelectIncidentDialog import com.crisiscleanup.feature.team.TeamsViewModel import com.crisiscleanup.feature.team.TeamsViewState +import kotlinx.coroutines.launch @Composable internal fun TeamsRoute( @@ -90,17 +89,27 @@ private fun TeamsScreen( val profilePictureLookup by viewModel.profilePictureLookup.collectAsStateWithLifecycle() - val pullRefreshState = rememberPullToRefreshState() - if (pullRefreshState.isRefreshing) { - LaunchedEffect(true) { - viewModel.refreshTeams() - pullRefreshState.endRefresh() + val coroutineScope = rememberCoroutineScope() + var isRefreshingTeams by remember { mutableStateOf(false) } + val refreshTeams = remember(viewModel) { + { + coroutineScope.launch { + isRefreshingTeams = true + try { + viewModel.refreshTeams() + } finally { + isRefreshingTeams = false + } + } + Unit } } - Box { - Column { - // TODO Modifiers and test tag + PullToRefreshBox( + isRefreshing = isRefreshingTeams, + onRefresh = refreshTeams, + ) { + Column(Modifier.fillMaxHeight()) { AppTopBar( dataProvider = viewModel.appTopBarDataProvider, openAuthentication = openAuthentication, @@ -113,7 +122,6 @@ private fun TeamsScreen( val listState = rememberLazyListState() LazyColumn( modifier = Modifier - .nestedScroll(pullRefreshState.nestedScrollConnection) .fillMaxHeight(), state = listState, verticalArrangement = listItemSpacedBy, @@ -214,12 +222,6 @@ private fun TeamsScreen( } BusyIndicatorFloatingTopCenter(isLoading) - - PullToRefreshContainer( - modifier = Modifier - .align(Alignment.TopCenter), - state = pullRefreshState, - ) } if (showIncidentPicker) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 770d147ae..6e5dabeb5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,80 +1,81 @@ [versions] -accompanist = "0.32.0" -androidDesugarJdkLibs = "2.0.4" +accompanist = "0.37.3" +androidDesugarJdkLibs = "2.1.5" # AGP and tools should be updated together -androidGradlePlugin = "8.4.2" -androidTools = "31.5.1" -androidMapsUtil = "2.3.0" -androidMapsUtilKtx = "3.4.0" +androidGradlePlugin = "8.11.1" +androidTools = "31.11.1" +androidMapsUtil = "3.14.0" +androidMapsUtilKtx = "5.2.0" androidMaterial = "1.12.0" -androidxActivity = "1.9.1" -androidxAppCompat = "1.7.0" +androidxActivity = "1.10.1" +androidxAppCompat = "1.7.1" androidxBrowser = "1.8.0" -androidxCamera = "1.3.4" -androidxComposeBom = "2024.06.00" -androidxComposeMaterial3 = "1.2.1" -androidxComposeRuntimeTracing = "1.0.0-beta01" -androidxConstraintLayout = "1.1.0-alpha13" -androidxCore = "1.13.1" +androidxCamera = "1.4.2" +androidxComposeBom = "2025.06.01" +androidxComposeMaterial3 = "1.3.2" +androidxComposeRuntimeTracing = "1.8.3" +androidxConstraintLayout = "1.1.1" +androidxCore = "1.16.0" androidxCoreSplashscreen = "1.0.1" -androidxDataStore = "1.1.1" +androidxDataStore = "1.1.7" androidxEspresso = "3.6.1" androidxHiltNavigationCompose = "1.2.0" -androidxLifecycle = "2.8.4" -androidxMacroBenchmark = "1.2.4" -androidxMetrics = "1.0.0-beta01" -androidxNavigation = "2.7.7" -androidxPaging = "3.3.2" -androidxProfileinstaller = "1.3.1" -androidxStartup = "1.1.1" -androidxSecurityCrypto = "1.1.0-alpha06" +androidxLifecycle = "2.9.1" +androidxLintGradle = "1.0.0-alpha05" +androidxMacroBenchmark = "1.3.4" +androidxMetrics = "1.0.0-beta02" +androidxNavigation = "2.9.1" +androidxPaging = "3.3.6" +androidxProfileinstaller = "1.4.1" +androidxStartup = "1.2.0" +androidxSecurityCrypto = "1.1.0-beta01" androidxTestCore = "1.6.1" androidxTestExt = "1.2.1" androidxTestRules = "1.6.1" -androidxTestRunner = "1.6.1" -androidxTracing = "1.2.0" +androidxTestRunner = "1.6.2" +androidxTracing = "1.3.0" androidxUiAutomator = "2.3.0" -androidxWindowManager = "1.3.0" -androidxWork = "2.9.1" -apacheCommonsText = "1.10.0" +androidxWindowManager = "1.4.0" +androidxWork = "2.10.2" +apacheCommonsText = "1.13.1" coil = "2.7.0" dependencyGuard = "0.5.0" -firebaseBom = "33.1.2" -firebaseCrashlyticsPlugin = "3.0.2" +firebaseBom = "33.16.0" +firebaseCrashlyticsPlugin = "3.0.4" firebasePerfPlugin = "1.4.2" -gmsPlugin = "4.4.2" -googleMapsCompose = "2.9.1" -googlePlaces = "3.5.0" -hilt = "2.51.1" +gmsPlugin = "4.4.3" +googleMapsCompose = "6.6.0" +googlePlaces = "4.3.1" +hilt = "2.56.2" hiltExt = "1.2.0" jacoco = "0.8.12" junit4 = "4.13.2" -jwtDecode = "2.0.1" -kotlin = "2.0.0" -kotlinxCoroutines = "1.8.0" -kotlinxCoroutinesPlayServices = "1.8.0" -kotlinxDatetime = "0.5.0" -kotlinxSerializationJson = "1.6.3" -ksp = "2.0.0-1.0.21" +jwtDecode = "2.0.2" +kotlin = "2.1.10" +kotlinxCoroutines = "1.10.2" +kotlinxCoroutinesPlayServices = "1.10.2" +kotlinxDatetime = "0.6.2" +kotlinxSerializationJson = "1.9.0" +ksp = "2.1.10-1.0.31" mlkitBarcodeScanning = "17.3.0" -mockk = "1.13.5" -moduleGraph = "2.5.0" -okhttp = "4.12.0" +mockk = "1.14.4" +moduleGraph = "2.9.0" +okhttp = "5.1.0" philJayRrule = "1.0.3" -playServicesAuth = "21.2.0" -playServicesAuthPhone = "18.1.0" +playServicesAuth = "21.3.0" +playServicesAuthPhone = "18.2.0" playServicesLocation = "21.3.0" -playServicesMaps = "19.0.0" -protobuf = "4.26.1" -protobufPlugin = "0.9.4" -retrofit = "2.9.0" +playServicesMaps = "19.2.0" +protobuf = "4.31.1" +protobufPlugin = "0.9.5" +retrofit = "3.0.0" retrofitKotlinxSerializationJson = "1.0.0" -room = "2.6.1" +room = "2.7.2" secrets = "2.0.1" -timeAgo = "4.0.3" -truth = "1.4.2" -turbine = "1.1.0" -zxing = "3.5.2" +timeAgo = "4.1.0" +truth = "1.4.4" +turbine = "1.2.1" +zxing = "3.5.3" [bundles] androidx-compose-ui-test = ["androidx-compose-ui-test", "androidx-compose-ui-testManifest"] @@ -120,6 +121,7 @@ androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecy androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-runtimeTesting = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidxLifecycle" } androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } +androidx-lint-gradle = { group = "androidx.lint", name = "lint-gradle", version.ref = "androidxLintGradle" } androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" } @@ -158,7 +160,10 @@ hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.r hilt-ext-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltExt" } junit4 = { group = "junit", name = "junit", version.ref = "junit4" } jwt-decode = { group = "com.auth0.android", name = "jwtdecode", version.ref = "jwtDecode" } +javax-inject = { module = "javax.inject:javax.inject", version = "1" } kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-playservices = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutinesPlayServices" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } @@ -204,6 +209,7 @@ room-gradlePlugin = { group = "androidx.room", name = "room-gradle-plugin", vers [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } +android-lint = { id = "com.android.lint", version.ref = "androidGradlePlugin" } android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } baselineprofile = { id = "androidx.baselineprofile", version.ref = "androidxMacroBenchmark" } compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } @@ -221,17 +227,17 @@ room = { id = "androidx.room", version.ref = "room" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } # Plugins defined by this project -nowinandroid-android-application = { id = "nowinandroid.android.application", version = "unspecified" } -nowinandroid-android-application-compose = { id = "nowinandroid.android.application.compose", version = "unspecified" } -nowinandroid-android-application-firebase = { id = "nowinandroid.android.application.firebase", version = "unspecified" } -nowinandroid-android-application-flavors = { id = "nowinandroid.android.application.flavors", version = "unspecified" } -nowinandroid-android-application-jacoco = { id = "nowinandroid.android.application.jacoco", version = "unspecified" } -nowinandroid-android-feature = { id = "nowinandroid.android.feature", version = "unspecified" } -nowinandroid-android-library = { id = "nowinandroid.android.library", version = "unspecified" } -nowinandroid-android-library-compose = { id = "nowinandroid.android.library.compose", version = "unspecified" } -nowinandroid-android-library-jacoco = { id = "nowinandroid.android.library.jacoco", version = "unspecified" } -nowinandroid-android-lint = { id = "nowinandroid.android.lint", version = "unspecified" } -nowinandroid-android-room = { id = "nowinandroid.android.room", version = "unspecified" } -nowinandroid-android-test = { id = "nowinandroid.android.test", version = "unspecified" } -nowinandroid-hilt = { id = "nowinandroid.hilt", version = "unspecified" } -nowinandroid-jvm-library = { id = "nowinandroid.jvm.library", version = "unspecified" } +nowinandroid-android-application = { id = "nowinandroid.android.application" } +nowinandroid-android-application-compose = { id = "nowinandroid.android.application.compose" } +nowinandroid-android-application-firebase = { id = "nowinandroid.android.application.firebase" } +nowinandroid-android-application-flavors = { id = "nowinandroid.android.application.flavors" } +nowinandroid-android-application-jacoco = { id = "nowinandroid.android.application.jacoco" } +nowinandroid-android-feature = { id = "nowinandroid.android.feature" } +nowinandroid-android-library = { id = "nowinandroid.android.library" } +nowinandroid-android-library-compose = { id = "nowinandroid.android.library.compose" } +nowinandroid-android-library-jacoco = { id = "nowinandroid.android.library.jacoco" } +nowinandroid-android-lint = { id = "nowinandroid.android.lint" } +nowinandroid-android-room = { id = "nowinandroid.android.room" } +nowinandroid-android-test = { id = "nowinandroid.android.test" } +nowinandroid-hilt = { id = "nowinandroid.hilt" } +nowinandroid-jvm-library = { id = "nowinandroid.jvm.library" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0d9fe8cfa..62c1cb4a4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ -#Wed Mar 20 11:17:41 EDT 2024 +#Mon Jan 06 13:11:12 EST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle.kts b/settings.gradle.kts index 60a5821a9..0a7f0bebb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,16 +1,28 @@ pluginManagement { includeBuild("build-logic") repositories { - google() + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } mavenCentral() gradlePluginPortal() } } dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS repositories { - google() + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } mavenCentral() // For RRule library (and possibly others) maven { url = uri("https://jitpack.io") } diff --git a/sync/work/src/main/java/com/crisiscleanup/sync/AppSyncer.kt b/sync/work/src/main/java/com/crisiscleanup/sync/AppSyncer.kt index 3c201581f..7a98b2d21 100644 --- a/sync/work/src/main/java/com/crisiscleanup/sync/AppSyncer.kt +++ b/sync/work/src/main/java/com/crisiscleanup/sync/AppSyncer.kt @@ -63,24 +63,33 @@ class AppSyncer @Inject constructor( applicationScope, ) - private suspend fun hasValidAccountTokens() = - accountDataRepository.accountData.first().areTokensValid - - private suspend fun isNotOnline() = networkMonitor.isNotOnline.first() - - private suspend fun onSyncPreconditions(): SyncResult? { - if (isNotOnline()) { + private suspend fun onlinePrecondition(): SyncResult? { + val isOnline = networkMonitor.isOnline.first() + if (!isOnline) { return SyncResult.NotAttempted("Not online") } + return null + } + + private suspend fun accountTokenPrecondition(): SyncResult? { accountDataRepository.updateAccountTokens() - if (!hasValidAccountTokens()) { + val hasValidAccountTokens = accountDataRepository.accountData.first().areTokensValid + if (!hasValidAccountTokens) { return SyncResult.InvalidAccountTokens } - // Other constraints are not important. - // When app is running assume sync is necessary. - // When app is in background assume Work constraints have been defined. + return null + } + + private suspend fun pushPrecondition(): SyncResult? { + onlinePrecondition()?.let { + return it + } + + accountTokenPrecondition()?.let { + return it + } return null } @@ -113,7 +122,7 @@ class AppSyncer @Inject constructor( cacheFullWorksites: Boolean, restartCacheCheckpoint: Boolean, ): SyncResult { - onSyncPreconditions()?.let { + accountTokenPrecondition()?.let { return it } @@ -173,8 +182,8 @@ class AppSyncer @Inject constructor( } override suspend fun syncPullLanguage(): SyncResult { - if (isNotOnline()) { - return SyncResult.NotAttempted("not-online") + onlinePrecondition()?.let { + return it } return try { @@ -192,8 +201,8 @@ class AppSyncer @Inject constructor( } override suspend fun syncPullStatuses(): SyncResult { - if (isNotOnline()) { - return SyncResult.NotAttempted("not-online") + onlinePrecondition()?.let { + return it } return try { @@ -206,7 +215,7 @@ class AppSyncer @Inject constructor( override fun appPushWorksite(worksiteId: Long, scheduleMediaSync: Boolean) { applicationScope.launch(ioDispatcher) { - onSyncPreconditions()?.let { + pushPrecondition()?.let { return@launch } @@ -225,7 +234,7 @@ class AppSyncer @Inject constructor( } override suspend fun syncPushWorksites(): SyncResult { - onSyncPreconditions()?.let { + pushPrecondition()?.let { return it } @@ -238,7 +247,7 @@ class AppSyncer @Inject constructor( } override suspend fun syncPushMedia(): SyncResult { - onSyncPreconditions()?.let { + pushPrecondition()?.let { return it }