Skip to content

Commit

Permalink
Merge branch 'master' into sufy/1995/conditional-branches
Browse files Browse the repository at this point in the history
  • Loading branch information
sufyanAbbasi committed Feb 27, 2024
2 parents d9b9490 + ec64292 commit 45e2979
Show file tree
Hide file tree
Showing 43 changed files with 668 additions and 191 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ tasks.dependencyUpdates {
ext {
androidCompileSdk = 34
androidMinSdk = 24
androidTargetSdk = 30
androidTargetSdk = 33

gmsMapsVersion = '18.1.0'
jvmToolchainVersion = 17
Expand Down
2 changes: 1 addition & 1 deletion config/detekt/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ naming:
FunctionNaming:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
functionPattern: '[a-z][a-zA-Z0-9]*'
functionPattern: '[a-zA-Z][a-zA-Z0-9]*'
excludeClassPattern: '$^'
FunctionParameterNaming:
active: true
Expand Down
12 changes: 9 additions & 3 deletions ground/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,13 @@ android {

buildFeatures {
buildConfig true
compose true
dataBinding true
viewBinding true
}
composeOptions {
kotlinCompilerExtensionVersion "1.5.5"
}
testOptions {
unitTests {
includeAndroidResources = true
Expand Down Expand Up @@ -182,9 +186,11 @@ dependencies {

// UI widgets.
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.9.0'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.compose.material3:material3:1.1.0"
implementation 'androidx.compose.ui:ui:1.6.1'
implementation 'androidx.compose.compiler:compiler:1.5.9'
implementation 'androidx.compose.material3:material3-android:1.2.0'

// Google Play Services.
implementation 'com.google.android.gms:play-services-auth:20.6.0'
Expand Down Expand Up @@ -273,7 +279,7 @@ dependencies {
testImplementation 'com.google.truth:truth:1.1.3'
androidTestImplementation 'com.google.truth:truth:1.1.3'
testImplementation 'androidx.test:core:1.5.0'
testImplementation 'org.robolectric:robolectric:4.9.2'
testImplementation 'org.robolectric:robolectric:4.11.1'
testImplementation 'android.arch.core:core-testing:1.1.1'
androidTestImplementation 'android.arch.core:core-testing:1.1.1'
testImplementation 'com.jraska.livedata:testing:1.2.0'
Expand Down
15 changes: 13 additions & 2 deletions ground/src/main/java/com/google/android/ground/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ object Config {
const val MAX_MEDIA_UPLOAD_RETRY_COUNT = 5

// TODO(#1730): Make sub-paths configurable and stop hardcoding here.
fun getMogSources(url: String) =
listOf(MogSource("${url}/world-masked.tif", 0..7), MogSource("${url}/{x}/{y}.tif", 8..14))
const val DEFAULT_MOG_MIN_ZOOM = 8
const val DEFAULT_MOG_MAX_ZOOM = 14
fun getMogSources(path: String = "/offline-imagery/default/") =
listOf(
MogSource(
0 ..< DEFAULT_MOG_MIN_ZOOM,
"$path/$DEFAULT_MOG_MIN_ZOOM/overview.tif",
),
MogSource(
DEFAULT_MOG_MIN_ZOOM..DEFAULT_MOG_MAX_ZOOM,
"$path/$DEFAULT_MOG_MIN_ZOOM/{x}/{y}.tif",
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ class LocalValueStore @Inject constructor(private val preferences: SharedPrefere
_offlineImageryEnabled.value = value
}

/** Whether to display instructions when loading draw area task. */
var drawAreaInstructionsShown: Boolean
get() = allowThreadDiskReads { preferences.getBoolean(DRAW_AREA_INSTRUCTIONS_SHOWN, false) }
set(value) = allowThreadDiskReads {
preferences.edit().putBoolean(DRAW_AREA_INSTRUCTIONS_SHOWN, value).apply()
}

/** Removes all values stored in the local store. */
fun clear() = allowThreadDiskWrites { preferences.edit().clear().apply() }

Expand Down Expand Up @@ -120,5 +127,6 @@ class LocalValueStore @Inject constructor(private val preferences: SharedPrefere
const val TOS_ACCEPTED = "tos_accepted"
const val LOCATION_LOCK_ENABLED = "location_lock_enabled"
const val OFFLINE_MAP_IMAGERY = "offline_map_imagery"
const val DRAW_AREA_INSTRUCTIONS_SHOWN = "draw_area_instructions_shown"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,9 @@ import javax.inject.Inject
/** Enqueues media upload work to be performed in the background. */
class MediaUploadWorkManager
@Inject
constructor(
private val workManager: WorkManager,
private val localValueStore: LocalValueStore,
) : SyncService() {
override val workerClass: Class<MediaUploadWorker>
get() = MediaUploadWorker::class.java
constructor(private val workManager: WorkManager, private val localValueStore: LocalValueStore) {

override fun preferredNetworkType(): NetworkType =
private fun preferredNetworkType(): NetworkType =
if (localValueStore.shouldUploadMediaOverUnmeteredConnectionOnly()) NetworkType.UNMETERED
else NetworkType.CONNECTED

Expand All @@ -45,10 +40,15 @@ constructor(
*/
fun enqueueSyncWorker(locationOfInterestId: String) {
val workInputData = createInputData(locationOfInterestId)
val request =
WorkRequestBuilder()
.setWorkerClass(MediaUploadWorker::class.java)
.setNetworkType(preferredNetworkType())
.buildWorkerRequest(workInputData)
workManager.enqueueUniqueWork(
MediaUploadWorker::class.java.name,
ExistingWorkPolicy.APPEND_OR_REPLACE,
buildWorkerRequest(workInputData)
request,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ import com.google.android.ground.persistence.sync.LocalMutationSyncWorker.Compan
import javax.inject.Inject

/** Enqueues data sync work to be done in the background. */
class MutationSyncWorkManager @Inject constructor(private val workManager: WorkManager) :
SyncService() {
override val workerClass: Class<LocalMutationSyncWorker>
get() = LocalMutationSyncWorker::class.java
class MutationSyncWorkManager @Inject constructor(private val workManager: WorkManager) {

/**
* Enqueues a worker that sends changes made locally to the remote data store once a network
Expand All @@ -38,10 +35,14 @@ class MutationSyncWorkManager @Inject constructor(private val workManager: WorkM
// implementation and avoids race conditions in the rare event the worker finishes just when new
// mutations are added to the db.
val inputData = createInputData(locationOfInterestId)
val request =
WorkRequestBuilder()
.setWorkerClass(LocalMutationSyncWorker::class.java)
.buildWorkerRequest(inputData)
workManager.enqueueUniqueWork(
LocalMutationSyncWorker::class.java.name,
ExistingWorkPolicy.APPEND,
buildWorkerRequest(inputData)
request,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,12 @@ package com.google.android.ground.persistence.sync

import androidx.work.ExistingWorkPolicy
import androidx.work.WorkManager
import java.util.*
import java.util.UUID
import javax.inject.Inject
import timber.log.Timber

/** Service responsible for enqueuing survey and LOI updates from remote server. */
class SurveySyncService @Inject constructor(private val workManager: WorkManager) : SyncService() {

override val workerClass: Class<SurveySyncWorker>
get() = SurveySyncWorker::class.java
class SurveySyncService @Inject constructor(private val workManager: WorkManager) {

/**
* Enqueues a worker that fetches the latest survey and LOIs from the remote survey and updates
Expand All @@ -35,12 +32,15 @@ class SurveySyncService @Inject constructor(private val workManager: WorkManager
*/
fun enqueueSync(surveyId: String): UUID {
val inputData = SurveySyncWorker.createInputData(surveyId)
val request = buildWorkerRequest(inputData)
val request =
WorkRequestBuilder()
.setWorkerClass(SurveySyncWorker::class.java)
.buildWorkerRequest(inputData)
workManager
.enqueueUniqueWork(
"${SurveySyncWorker::class.java}#${surveyId}",
ExistingWorkPolicy.APPEND,
request
request,
)
.result
.get()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import com.google.android.ground.domain.usecases.survey.SyncSurveyUseCase
import com.google.android.ground.persistence.sync.SyncService.Companion.DEFAULT_MAX_RETRY_ATTEMPTS
import com.google.android.ground.persistence.sync.WorkRequestBuilder.Companion.DEFAULT_MAX_RETRY_ATTEMPTS
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 Google LLC
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -26,37 +26,45 @@ import androidx.work.Worker
import java.util.concurrent.TimeUnit

/**
* Base class for creating a work manager for scheduling background tasks.
* Builder for creating a work manager for scheduling background tasks.
*
* By default, the only constraint is availability of any type of internet connection, as it is
* assumed that all background tasks need at least some sort of connectivity.
*
* In case the required criteria are not met, the next attempt uses LINEAR backoff policy with a
* backoff delay of 10 seconds.
*/
abstract class SyncService {
/** A set of constraints that must be satisfied in order to start the scheduled job. */
private val workerConstraints: Constraints
get() = Constraints.Builder().setRequiredNetworkType(preferredNetworkType()).build()
class WorkRequestBuilder {

/** A class extending [Worker] which gets scheduled for a request. */
protected abstract val workerClass: Class<out ListenableWorker?>
private lateinit var workerClass: Class<out ListenableWorker?>

/**
* Override this method if the worker requires a stable internet connection for large file
* upload/download. By default, the worker just needs access to internet connection.
* Defines whether the worker requires a stable internet connection for large file upload /
* download. By default, the worker just needs access to internet connection.
*/
protected open fun preferredNetworkType(): NetworkType = DEFAULT_NETWORK_TYPE
private var networkType: NetworkType = DEFAULT_NETWORK_TYPE

fun setNetworkType(networkType: NetworkType): WorkRequestBuilder {
this.networkType = networkType
return this
}

fun setWorkerClass(workerClass: Class<out ListenableWorker?>): WorkRequestBuilder {
this.workerClass = workerClass
return this
}

/**
* Create a work request for non-repeating work along with input data that would be passed along
* to the worker class.
*/
@JvmOverloads
protected fun buildWorkerRequest(inputData: Data? = null): OneTimeWorkRequest {
fun buildWorkerRequest(inputData: Data? = null): OneTimeWorkRequest {
val constraints = Constraints.Builder().setRequiredNetworkType(networkType).build()

val builder =
OneTimeWorkRequest.Builder(workerClass)
.setConstraints(workerConstraints)
.setConstraints(constraints)
.setBackoffCriteria(BACKOFF_POLICY, BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)

if (inputData != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.google.android.ground.Config
import com.google.android.ground.model.imagery.OfflineArea
import com.google.android.ground.model.imagery.TileSource
import com.google.android.ground.persistence.local.stores.LocalOfflineAreaStore
import com.google.android.ground.persistence.remote.RemoteStorageManager
import com.google.android.ground.persistence.uuid.OfflineUuidGenerator
import com.google.android.ground.system.GeocodingManager
import com.google.android.ground.ui.map.Bounds
Expand Down Expand Up @@ -56,7 +57,8 @@ constructor(
private val surveyRepository: SurveyRepository,
private val fileUtil: FileUtil,
private val geocodingManager: GeocodingManager,
private val offlineUuidGenerator: OfflineUuidGenerator
private val offlineUuidGenerator: OfflineUuidGenerator,
private val remoteStorageManager: RemoteStorageManager,
) {

private suspend fun addOfflineArea(bounds: Bounds, zoomRange: IntRange) {
Expand Down Expand Up @@ -138,7 +140,7 @@ constructor(
private fun getMogClient(): MogClient {
val mogCollection = MogCollection(getMogSources())
// TODO(#1754): Create a factory and inject rather than instantiating here. Add tests.
return MogClient(mogCollection)
return MogClient(mogCollection, remoteStorageManager)
}

private fun getMogSources(): List<MogSource> = Config.getMogSources(getFirstTileSourceUrl())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,25 @@ package com.google.android.ground.ui.datacollection.tasks.polygon

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.lifecycleScope
import com.google.android.ground.R
import com.google.android.ground.model.geometry.LineString
Expand All @@ -30,6 +48,7 @@ import com.google.android.ground.ui.datacollection.components.TaskViewFactory
import com.google.android.ground.ui.datacollection.tasks.AbstractTaskFragment
import com.google.android.ground.ui.map.Feature
import com.google.android.ground.ui.map.MapFragment
import com.google.android.material.color.MaterialColors
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
Expand Down Expand Up @@ -77,6 +96,10 @@ class DrawAreaTaskFragment : AbstractTaskFragment<DrawAreaTaskViewModel>() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.draftArea.collectLatest { onFeatureUpdated(it) }
}

if (!viewModel.instructionsDialogShown) {
showInstructionsDialog()
}
}

private fun onFeatureUpdated(feature: Feature?) {
Expand All @@ -87,4 +110,59 @@ class DrawAreaTaskFragment : AbstractTaskFragment<DrawAreaTaskViewModel>() {
completeButton.showIfTrue(geometry.isClosed() && !viewModel.isMarkedComplete())
nextButton.showIfTrue(viewModel.isMarkedComplete())
}

private fun showInstructionsDialog() {
(view as ViewGroup).addView(
ComposeView(requireContext()).apply {
setContent {
val openAlertDialog = remember { mutableStateOf(true) }
when {
openAlertDialog.value -> {
CreateInstructionsDialog {
openAlertDialog.value = false
viewModel.instructionsDialogShown = true
}
}
}
}
}
)
}

@Composable
private fun CreateInstructionsDialog(onDismissRequest: () -> Unit) {
AlertDialog(
icon = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.touch_app_24),
contentDescription = "",
modifier = Modifier.width(48.dp).height(48.dp),
)
},
title = { StyledText(getText(R.string.draw_area_task_instruction)) },
onDismissRequest = {}, // Prevent dismissing the dialog by clicking outside
confirmButton = {}, // Hide confirm button
dismissButton = {
OutlinedButton(onClick = { onDismissRequest() }) {
Text(
text = getString(R.string.close),
color = Color(MaterialColors.getColor(context, R.attr.colorPrimary, "")),
)
}
},
)
}

/** Supports annotated texts e.g. <b>Hello world</b> */
@Composable
private fun StyledText(text: CharSequence, modifier: Modifier = Modifier) {
AndroidView(
modifier = modifier,
factory = { context -> TextView(context) },
update = {
it.text = text
it.textSize = 18.toFloat()
},
)
}
}
Loading

0 comments on commit 45e2979

Please sign in to comment.