From fddf518d1d13892a1de34f8f5e42376b6e5a2192 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 16 Feb 2026 14:12:21 -0500 Subject: [PATCH 01/22] chore: cleanup and tooling updates --- .agent/skills/fetch_pr_comments/SKILL.md | 2 +- .../scripts/fetch_comments.py | 105 ++++++++++++++++-- CONTRIBUTING.md | 17 +-- app/build.gradle | 4 +- .../android/CustomTestRunner.kt | 29 +++++ config/lint/lint.xml | 1 + gradle.properties | 1 + 7 files changed, 137 insertions(+), 22 deletions(-) mode change 100644 => 100755 .agent/skills/fetch_pr_comments/scripts/fetch_comments.py create mode 100644 app/src/androidTest/java/org/groundplatform/android/CustomTestRunner.kt diff --git a/.agent/skills/fetch_pr_comments/SKILL.md b/.agent/skills/fetch_pr_comments/SKILL.md index 72d0966049..137d193c54 100644 --- a/.agent/skills/fetch_pr_comments/SKILL.md +++ b/.agent/skills/fetch_pr_comments/SKILL.md @@ -27,7 +27,7 @@ The script outputs Markdown text to stdout, containing: - PR Title and URL - Reviews (Approved/Changes Requested) - General Comments (Pull Request level) -- Code Comments (Grouped by file and line number) +- Code Comments (Grouped by file and line number, unresolved threads only) ## Dependencies diff --git a/.agent/skills/fetch_pr_comments/scripts/fetch_comments.py b/.agent/skills/fetch_pr_comments/scripts/fetch_comments.py old mode 100644 new mode 100755 index 508caa4e9c..173f815aa4 --- a/.agent/skills/fetch_pr_comments/scripts/fetch_comments.py +++ b/.agent/skills/fetch_pr_comments/scripts/fetch_comments.py @@ -54,22 +54,103 @@ def get_repo_owner_name(pr_url): return match.group(1), match.group(2) return None, None -def get_code_comments(owner, repo, pr_number): - cmd = f"gh api repos/{owner}/{repo}/pulls/{pr_number}/comments --paginate" +GRAPHQL_QUERY = """ +query($owner: String!, $repo: String!, $pr: Int!, $cursor: String) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $pr) { + reviewThreads(first: 50, after: $cursor) { + pageInfo { + hasNextPage + endCursor + } + nodes { + isResolved + path + comments(first: 50) { + nodes { + author { login } + body + path + line + originalLine + createdAt + url + } + } + } + } + } + } +} +""" + +def run_graphql_query(query, variables): try: - return json.loads(run_command(cmd)) - except Exception as e: - print(f"Warning: Failed to fetch code comments: {e}", file=sys.stderr) - return [] + result = subprocess.run( + ["gh", "api", "graphql", "--input", "-"], + input=json.dumps({"query": query, "variables": variables}), + capture_output=True, + text=True, + check=True + ) + return json.loads(result.stdout) + except subprocess.CalledProcessError as e: + print(f"Error running GraphQL query: {e}", file=sys.stderr) + print(f"Stderr: {e.stderr}", file=sys.stderr) + return None + +def get_code_comments(owner, repo, pr_number): + comments = [] + cursor = None + has_next = True + + while has_next: + variables = { + "owner": owner, + "repo": repo, + "pr": int(pr_number), + "cursor": cursor + } + + data = run_graphql_query(GRAPHQL_QUERY, variables) + if not data: + break + + pr_data = data.get("data", {}).get("repository", {}).get("pullRequest", {}) + if not pr_data: + break + + threads = pr_data.get("reviewThreads", {}) + page_info = threads.get("pageInfo", {}) + has_next = page_info.get("hasNextPage", False) + cursor = page_info.get("endCursor") + + for thread in threads.get("nodes", []): + if thread.get("isResolved"): + continue + + for comment in thread.get("comments", {}).get("nodes", []): + mapped = { + "path": comment.get("path"), + "line": comment.get("line"), + "original_line": comment.get("originalLine"), + "body": comment.get("body"), + "user": {"login": comment.get("author", {}).get("login") if comment.get("author") else "Unknown"}, + "created_at": comment.get("createdAt"), + "html_url": comment.get("url") + } + comments.append(mapped) + + return comments def main(): # Allow optional PR argument (number or URL) pr_arg = "" if len(sys.argv) > 1: pr_arg = f" {sys.argv[1]}" - + cmd = f"gh pr view{pr_arg} --json number,title,url,state,comments,reviews,latestReviews" - + try: json_output = run_command(cmd) pr_data = json.loads(json_output) @@ -80,7 +161,7 @@ def main(): title = pr_data.get('title') url = pr_data.get('url') state = pr_data.get('state') - + owner, repo = get_repo_owner_name(url) code_comments = [] if owner and repo: @@ -134,19 +215,19 @@ def main(): if path not in comments_by_file: comments_by_file[path] = [] comments_by_file[path].append(cc) - + for path, comments in comments_by_file.items(): print(f"### File: `{path}`\n") # Sort by line number (or position if line is None) comments.sort(key=lambda x: (x.get('line') or x.get('original_line') or 0)) - + for cc in comments: author = cc.get('user', {}).get('login', 'Unknown') body = cc.get('body', '').strip() date = format_date(cc.get('created_at', '')) line = cc.get('line') or cc.get('original_line') or "Outdated" html_url = cc.get('html_url', '') - + print(f"#### Line {line} - {author} ({date})") print(f"[Link]({html_url})\n") print(body) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e740d722ee..2b673fefff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -137,7 +137,6 @@ to the base repository using a pull request. ## Initial build configuration -### Add Google Maps API Key(s) ### Set up Firebase @@ -157,15 +156,17 @@ to the base repository using a pull request. 4. Download the config file for the Android app to `app/src/debug/google-services.json` -5. Create a file named `secrets.properties` in the root of the project with the following contents: +### Add Google Maps API Key(s) - ``` - MAPS_API_KEY= - ``` +Create a file named `secrets.properties` in the root of the project with the following contents: + +``` +MAPS_API_KEY= +``` - You can find the Maps SDK key for your Firebase project at - http://console.cloud.google.com/google/maps-apis/credentials under - "Android key (auto created by Firebase)". +You can find the Maps SDK key for your Firebase project at +http://console.cloud.google.com/google/maps-apis/credentials under +"Android key (auto created by Firebase)". ### Troubleshooting diff --git a/app/build.gradle b/app/build.gradle index e06ae733fd..4aac105e3d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,7 +90,7 @@ android { buildConfigField "String", "SIGNUP_FORM_LINK", "\"\"" manifestPlaceholders.usesCleartextTraffic = true - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "org.groundplatform.android.CustomTestRunner" } // Use flag -PtestBuildType with desired variant to change default behavior. @@ -213,6 +213,7 @@ dependencies { implementation libs.androidx.ui.tooling.preview.android stagingImplementation libs.androidx.ui.test.manifest testImplementation libs.androidx.ui.test.junit4 + androidTestImplementation libs.androidx.ui.test.junit4 implementation libs.androidx.navigation.compose implementation libs.androidx.hilt.navigation.compose @@ -383,3 +384,4 @@ secrets { // checked in version control. defaultPropertiesFileName = "local.defaults.properties" } + diff --git a/app/src/androidTest/java/org/groundplatform/android/CustomTestRunner.kt b/app/src/androidTest/java/org/groundplatform/android/CustomTestRunner.kt new file mode 100644 index 0000000000..9e829916d7 --- /dev/null +++ b/app/src/androidTest/java/org/groundplatform/android/CustomTestRunner.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.android + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import dagger.hilt.android.testing.HiltTestApplication + +class CustomTestRunner : AndroidJUnitRunner() { + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context?, + ): Application = super.newApplication(cl, HiltTestApplication::class.java.name, context) +} diff --git a/config/lint/lint.xml b/config/lint/lint.xml index 7402d7ee2d..2de1d5a865 100644 --- a/config/lint/lint.xml +++ b/config/lint/lint.xml @@ -53,6 +53,7 @@ + diff --git a/gradle.properties b/gradle.properties index 32d84a544b..e191aec6d4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,7 @@ # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true + #Thu Dec 26 12:25:19 EST 2019 android.useAndroidX=true From 655775aa396cb9befb532c36ff7b03b8d02f4df5 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 16 Feb 2026 14:18:49 -0500 Subject: [PATCH 02/22] feat: add HomeDrawer component and ViewModel logic --- .../android/ui/home/HomeDrawer.kt | 253 ++++++++++++++++++ .../android/ui/home/HomeScreenViewModel.kt | 7 + build_output.txt | 64 +++++ 3 files changed, 324 insertions(+) create mode 100644 app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt create mode 100644 build_output.txt diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt new file mode 100644 index 0000000000..98270c797e --- /dev/null +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt @@ -0,0 +1,253 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.android.ui.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ExitToApp +import androidx.compose.material.icons.filled.Build +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.groundplatform.android.R +import org.groundplatform.android.model.Survey +import org.groundplatform.android.model.User + +@Composable +fun HomeDrawer( + user: User?, + survey: Survey?, + onSwitchSurvey: () -> Unit, + onNavigateToOfflineAreas: () -> Unit, + onNavigateToSyncStatus: () -> Unit, + onNavigateToSettings: () -> Unit, + onNavigateToAbout: () -> Unit, + onNavigateToTerms: () -> Unit, + onSignOut: () -> Unit, + versionText: String, +) { + Column( + modifier = + Modifier.fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .verticalScroll(rememberScrollState()) + ) { + AppInfoHeader(user = user) + SurveySelector(survey = survey, onSwitchSurvey = onSwitchSurvey) + HorizontalDivider() + DrawerItems( + onNavigateToOfflineAreas, + onNavigateToSyncStatus, + onNavigateToSettings, + onNavigateToAbout, + onNavigateToTerms, + onSignOut, + versionText, + ) + } +} + +@Composable +private fun DrawerItems( + onNavigateToOfflineAreas: () -> Unit, + onNavigateToSyncStatus: () -> Unit, + onNavigateToSettings: () -> Unit, + onNavigateToAbout: () -> Unit, + onNavigateToTerms: () -> Unit, + onSignOut: () -> Unit, + versionText: String, +) { + val navItems = + listOf( + DrawerItem( + label = stringResource(R.string.offline_map_imagery), + icon = IconSource.Drawable(R.drawable.ic_offline_pin), + onClick = onNavigateToOfflineAreas, + ), + DrawerItem( + label = stringResource(R.string.sync_status), + icon = IconSource.Drawable(R.drawable.ic_sync), + onClick = onNavigateToSyncStatus, + ), + DrawerItem( + label = stringResource(R.string.settings), + icon = IconSource.Vector(Icons.Default.Settings), + onClick = onNavigateToSettings, + ), + DrawerItem( + label = stringResource(R.string.about), + icon = IconSource.Drawable(R.drawable.info_outline), + onClick = onNavigateToAbout, + ), + DrawerItem( + label = stringResource(R.string.terms_of_service), + icon = IconSource.Drawable(R.drawable.feed), + onClick = onNavigateToTerms, + ), + DrawerItem( + label = stringResource(R.string.sign_out), + icon = IconSource.Vector(Icons.AutoMirrored.Filled.ExitToApp), + onClick = onSignOut, + ), + DrawerItem(label = versionText, icon = IconSource.Vector(Icons.Default.Build), onClick = {}), + ) + + navItems.forEach { item -> + NavigationDrawerItem( + label = { Text(item.label) }, + selected = false, + onClick = item.onClick, + icon = { + val description = item.label + when (item.icon) { + is IconSource.Vector -> Icon(item.icon.imageVector, contentDescription = description) + is IconSource.Drawable -> + Icon(painterResource(item.icon.id), contentDescription = description) + } + }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + ) + } +} + +private data class DrawerItem(val label: String, val icon: IconSource, val onClick: () -> Unit) + +@Composable +private fun AppInfoHeader(user: User?) { + Column( + modifier = + Modifier.fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(vertical = 24.dp, horizontal = 16.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Image( + painter = painterResource(R.drawable.ground_logo), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + Spacer(Modifier.width(8.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.app_name), + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + ) + } + if (user?.photoUrl != null) { + androidx.compose.ui.viewinterop.AndroidView( + factory = { context -> + android.widget.ImageView(context).apply { + scaleType = android.widget.ImageView.ScaleType.CENTER_CROP + } + }, + update = { imageView -> + com.bumptech.glide.Glide.with(imageView) + .load(user.photoUrl) + .circleCrop() + .into(imageView) + }, + modifier = Modifier.size(32.dp).clip(CircleShape), + ) + } + } + } +} + +@Composable +private fun SurveySelector(survey: Survey?, onSwitchSurvey: () -> Unit) { + Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(R.drawable.ic_content_paste), + contentDescription = stringResource(R.string.current_survey), + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.width(4.dp)) + Text( + text = stringResource(R.string.current_survey), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(Modifier.height(8.dp)) + + if (survey == null) { + Text(stringResource(R.string.no_survey_selected)) + } else { + Text( + text = survey.title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + if (survey.description.isNotEmpty()) { + Text( + text = survey.description, + style = MaterialTheme.typography.bodyMedium, + maxLines = 4, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 8.dp), + ) + } + } + + Spacer(Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.switch_survey), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + modifier = Modifier.clickable(onClick = onSwitchSurvey).padding(vertical = 8.dp), + ) + } +} + +private sealed interface IconSource { + data class Vector(val imageVector: androidx.compose.ui.graphics.vector.ImageVector) : IconSource + + data class Drawable(@androidx.annotation.DrawableRes val id: Int) : IconSource +} diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt index 72d82335b3..aa45cf1718 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt @@ -60,6 +60,9 @@ internal constructor( // Issue URL: https://github.com/google/ground-android/issues/1730 val showOfflineAreaMenuItem: LiveData = MutableLiveData(true) + private val _showSignOutDialog = MutableSharedFlow() + val showSignOutDialog: SharedFlow = _showSignOutDialog.asSharedFlow() + /* Indicates the application is being restored after a photo capture. * * We need to persist this state here to control [HomeScreenFragement] UI treatments when returning @@ -119,6 +122,10 @@ internal constructor( suspend fun getOfflineAreas() = offlineAreaRepository.offlineAreas().first() + fun showSignOutDialog() { + viewModelScope.launch { _showSignOutDialog.emit(true) } + } + fun signOut() { viewModelScope.launch { userRepository.signOut() } } diff --git a/build_output.txt b/build_output.txt new file mode 100644 index 0000000000..fd4d42f574 --- /dev/null +++ b/build_output.txt @@ -0,0 +1,64 @@ +Configuration on demand is an incubating feature. + +> Configure project :app +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. +To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties +WARNING: The multidex library is included as a dependency, but it is not needed for apps +with minSdk >= 21. Please remove dependency 'androidx.multidex:multidex:2.0.1' from ':app'. +See https://developer.android.com/build/multidex for more information. +w: ⚠️ Deprecated 'org.jetbrains.kotlin.android' plugin usage +The 'org.jetbrains.kotlin.android' plugin in project ':app' is no longer required for Kotlin support since AGP 9.0. +Solution: Remove both `android.builtInKotlin=true` and `android.newDsl=false` from `gradle.properties`, then migrate to built-in Kotlin. +See https://kotl.in/gradle/agp-built-in-kotlin for more details. + + +[Incubating] Problems report is available at: file:///Users/gmiceli/Git/google/ground-android/build/reports/problems/problems-report.html + +FAILURE: Build failed with an exception. + +* What went wrong: +Cannot locate tasks that match ':app:testDebugUnitTest' as task 'testDebugUnitTest' is ambiguous in project ':app'. Candidates are: 'testDevDebugUnitTest', 'testLocalDebugUnitTest', 'testProdDebugUnitTest'. + +* Try: +> Run gradlew tasks to get a list of available tasks. +> For more on name expansion, please refer to https://docs.gradle.org/9.3.1/userguide/command_line_interface.html#sec:name_abbreviation in the Gradle documentation. +> Run with --stacktrace option to get the stack trace. +> Run with --info or --debug option to get more log output. +> Run with --scan to get full insights from a Build Scan (powered by Develocity). +> Get more help at https://help.gradle.org. + +Deprecated Gradle features were used in this build, making it incompatible with Gradle 10. + +You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins. + +For more on this, please refer to https://docs.gradle.org/9.3.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation. + +BUILD FAILED in 999ms From d88694298b316275312a212c3c60b41270e04c6d Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 16 Feb 2026 14:19:01 -0500 Subject: [PATCH 03/22] chore: remove build output file --- build_output.txt | 64 ------------------------------------------------ 1 file changed, 64 deletions(-) delete mode 100644 build_output.txt diff --git a/build_output.txt b/build_output.txt deleted file mode 100644 index fd4d42f574..0000000000 --- a/build_output.txt +++ /dev/null @@ -1,64 +0,0 @@ -Configuration on demand is an incubating feature. - -> Configure project :app -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The property android.dependency.excludeLibraryComponentsFromConstraints improves project import performance for very large projects. It should be enabled to improve performance. -To suppress this warning, add android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false to gradle.properties -WARNING: The multidex library is included as a dependency, but it is not needed for apps -with minSdk >= 21. Please remove dependency 'androidx.multidex:multidex:2.0.1' from ':app'. -See https://developer.android.com/build/multidex for more information. -w: ⚠️ Deprecated 'org.jetbrains.kotlin.android' plugin usage -The 'org.jetbrains.kotlin.android' plugin in project ':app' is no longer required for Kotlin support since AGP 9.0. -Solution: Remove both `android.builtInKotlin=true` and `android.newDsl=false` from `gradle.properties`, then migrate to built-in Kotlin. -See https://kotl.in/gradle/agp-built-in-kotlin for more details. - - -[Incubating] Problems report is available at: file:///Users/gmiceli/Git/google/ground-android/build/reports/problems/problems-report.html - -FAILURE: Build failed with an exception. - -* What went wrong: -Cannot locate tasks that match ':app:testDebugUnitTest' as task 'testDebugUnitTest' is ambiguous in project ':app'. Candidates are: 'testDevDebugUnitTest', 'testLocalDebugUnitTest', 'testProdDebugUnitTest'. - -* Try: -> Run gradlew tasks to get a list of available tasks. -> For more on name expansion, please refer to https://docs.gradle.org/9.3.1/userguide/command_line_interface.html#sec:name_abbreviation in the Gradle documentation. -> Run with --stacktrace option to get the stack trace. -> Run with --info or --debug option to get more log output. -> Run with --scan to get full insights from a Build Scan (powered by Develocity). -> Get more help at https://help.gradle.org. - -Deprecated Gradle features were used in this build, making it incompatible with Gradle 10. - -You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins. - -For more on this, please refer to https://docs.gradle.org/9.3.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation. - -BUILD FAILED in 999ms From da3f1aecfee991eae4aeaf0847b58d63e2b85b17 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 16 Feb 2026 15:06:37 -0500 Subject: [PATCH 04/22] Clean up PR --- .agent/skills/fetch_pr_comments/SKILL.md | 35 --- .../scripts/fetch_comments.py | 238 ------------------ .../android/CustomTestRunner.kt | 5 + .../android/ui/home/HomeScreenViewModel.kt | 7 - config/lint/lint.xml | 1 - gradle.properties | 1 - 6 files changed, 5 insertions(+), 282 deletions(-) delete mode 100644 .agent/skills/fetch_pr_comments/SKILL.md delete mode 100755 .agent/skills/fetch_pr_comments/scripts/fetch_comments.py diff --git a/.agent/skills/fetch_pr_comments/SKILL.md b/.agent/skills/fetch_pr_comments/SKILL.md deleted file mode 100644 index 137d193c54..0000000000 --- a/.agent/skills/fetch_pr_comments/SKILL.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: Fetch PR Comments -description: Fetches comments and reviews from the current GitHub Pull Request and formats them as Markdown. ---- - -# Fetch PR Comments - -This skill allows you to retrieve comments and reviews from the current GitHub Pull Request (PR) associated with the active branch, or a specific PR by number/URL. It converts the data into a readable Markdown format, including code review comments with file and line context. - -## Usage - -To use this skill, run the provided Python script. It requires the GitHub CLI (`gh`) to be installed and authenticated. - -### Command - -```bash -# Fetch comments for the current branch's PR -python3 .agent/skills/fetch_pr_comments/scripts/fetch_comments.py - -# Fetch comments for a specific PR -python3 .agent/skills/fetch_pr_comments/scripts/fetch_comments.py -``` - -### Output - -The script outputs Markdown text to stdout, containing: -- PR Title and URL -- Reviews (Approved/Changes Requested) -- General Comments (Pull Request level) -- Code Comments (Grouped by file and line number, unresolved threads only) - -## Dependencies - -- `gh` (GitHub CLI) must be installed and in your PATH. -- `python3` must be available. diff --git a/.agent/skills/fetch_pr_comments/scripts/fetch_comments.py b/.agent/skills/fetch_pr_comments/scripts/fetch_comments.py deleted file mode 100755 index 173f815aa4..0000000000 --- a/.agent/skills/fetch_pr_comments/scripts/fetch_comments.py +++ /dev/null @@ -1,238 +0,0 @@ -# -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -import json -import subprocess -import sys -import re -from datetime import datetime - -def run_command(command): - try: - result = subprocess.run( - command, - check=True, - capture_output=True, - text=True, - shell=True - ) - return result.stdout.strip() - except subprocess.CalledProcessError as e: - if "no pull requests found" in e.stderr.lower(): - print("No pull request found for the current branch.", file=sys.stderr) - sys.exit(1) - - print(f"Error running command: {command}", file=sys.stderr) - print(f"Stderr: {e.stderr}", file=sys.stderr) - sys.exit(1) - -def format_date(iso_date): - try: - dt = datetime.fromisoformat(iso_date.replace("Z", "+00:00")) - return dt.strftime("%Y-%m-%d %H:%M") - except Exception: - return iso_date - - -def get_repo_owner_name(pr_url): - # Expected format: https://github.com/OWNER/REPO/pull/NUMBER - match = re.search(r"github\.com/([^/]+)/([^/]+)/pull/\d+", pr_url) - if match: - return match.group(1), match.group(2) - return None, None - -GRAPHQL_QUERY = """ -query($owner: String!, $repo: String!, $pr: Int!, $cursor: String) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $pr) { - reviewThreads(first: 50, after: $cursor) { - pageInfo { - hasNextPage - endCursor - } - nodes { - isResolved - path - comments(first: 50) { - nodes { - author { login } - body - path - line - originalLine - createdAt - url - } - } - } - } - } - } -} -""" - -def run_graphql_query(query, variables): - try: - result = subprocess.run( - ["gh", "api", "graphql", "--input", "-"], - input=json.dumps({"query": query, "variables": variables}), - capture_output=True, - text=True, - check=True - ) - return json.loads(result.stdout) - except subprocess.CalledProcessError as e: - print(f"Error running GraphQL query: {e}", file=sys.stderr) - print(f"Stderr: {e.stderr}", file=sys.stderr) - return None - -def get_code_comments(owner, repo, pr_number): - comments = [] - cursor = None - has_next = True - - while has_next: - variables = { - "owner": owner, - "repo": repo, - "pr": int(pr_number), - "cursor": cursor - } - - data = run_graphql_query(GRAPHQL_QUERY, variables) - if not data: - break - - pr_data = data.get("data", {}).get("repository", {}).get("pullRequest", {}) - if not pr_data: - break - - threads = pr_data.get("reviewThreads", {}) - page_info = threads.get("pageInfo", {}) - has_next = page_info.get("hasNextPage", False) - cursor = page_info.get("endCursor") - - for thread in threads.get("nodes", []): - if thread.get("isResolved"): - continue - - for comment in thread.get("comments", {}).get("nodes", []): - mapped = { - "path": comment.get("path"), - "line": comment.get("line"), - "original_line": comment.get("originalLine"), - "body": comment.get("body"), - "user": {"login": comment.get("author", {}).get("login") if comment.get("author") else "Unknown"}, - "created_at": comment.get("createdAt"), - "html_url": comment.get("url") - } - comments.append(mapped) - - return comments - -def main(): - # Allow optional PR argument (number or URL) - pr_arg = "" - if len(sys.argv) > 1: - pr_arg = f" {sys.argv[1]}" - - cmd = f"gh pr view{pr_arg} --json number,title,url,state,comments,reviews,latestReviews" - - try: - json_output = run_command(cmd) - pr_data = json.loads(json_output) - except Exception: - # If run_command fails or json decode fails, we exit (mostly handled in run_command) - sys.exit(1) - number = pr_data.get('number') - title = pr_data.get('title') - url = pr_data.get('url') - state = pr_data.get('state') - - owner, repo = get_repo_owner_name(url) - code_comments = [] - if owner and repo: - code_comments = get_code_comments(owner, repo, number) - - # --- Header --- - print(f"# PR #{number}: {title}") - print(f"**URL:** {url}") - print(f"**State:** {state}\n") - - # --- Reviews --- - print("## Reviews") - reviews = pr_data.get('latestReviews', []) - if not reviews: - print("No official reviews found.") - else: - for review in reviews: - review_state = review.get('state') - author = review.get('author', {}).get('login', 'Unknown') - date = format_date(review.get('submittedAt', '')) - print(f"- **{author}**: {review_state} ({date})") - print("\n" + "-"*40 + "\n") - - # --- General Comments --- - print("## General Comments") - general_comments = pr_data.get('comments', []) - if not general_comments: - print("No general comments.") - else: - for comment in general_comments: - author = comment.get('author', {}).get('login', 'Unknown') - body = comment.get('body', '').strip() - date = format_date(comment.get('createdAt', '')) - comment_url = comment.get('url', '') - - print(f"### {author} at {date}") - print(f"[Link]({comment_url})\n") - print(body) - print("\n") - print("-" * 40 + "\n") - - # --- Code Comments --- - print("## Code Comments") - if not code_comments: - print("No code comments.") - else: - # Group by file path - comments_by_file = {} - for cc in code_comments: - path = cc.get('path', 'Unknown File') - if path not in comments_by_file: - comments_by_file[path] = [] - comments_by_file[path].append(cc) - - for path, comments in comments_by_file.items(): - print(f"### File: `{path}`\n") - # Sort by line number (or position if line is None) - comments.sort(key=lambda x: (x.get('line') or x.get('original_line') or 0)) - - for cc in comments: - author = cc.get('user', {}).get('login', 'Unknown') - body = cc.get('body', '').strip() - date = format_date(cc.get('created_at', '')) - line = cc.get('line') or cc.get('original_line') or "Outdated" - html_url = cc.get('html_url', '') - - print(f"#### Line {line} - {author} ({date})") - print(f"[Link]({html_url})\n") - print(body) - print("\n") - print("-" * 20 + "\n") - -if __name__ == "__main__": - main() diff --git a/app/src/androidTest/java/org/groundplatform/android/CustomTestRunner.kt b/app/src/androidTest/java/org/groundplatform/android/CustomTestRunner.kt index 9e829916d7..4681c58116 100644 --- a/app/src/androidTest/java/org/groundplatform/android/CustomTestRunner.kt +++ b/app/src/androidTest/java/org/groundplatform/android/CustomTestRunner.kt @@ -20,6 +20,11 @@ import android.content.Context import androidx.test.runner.AndroidJUnitRunner import dagger.hilt.android.testing.HiltTestApplication +/** + * A custom [AndroidJUnitRunner] used to replace the application class with [HiltTestApplication]. + * + * This is required for Hilt dependency injection to work correctly in instrumented tests. + */ class CustomTestRunner : AndroidJUnitRunner() { override fun newApplication( cl: ClassLoader?, diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt index aa45cf1718..72d82335b3 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt @@ -60,9 +60,6 @@ internal constructor( // Issue URL: https://github.com/google/ground-android/issues/1730 val showOfflineAreaMenuItem: LiveData = MutableLiveData(true) - private val _showSignOutDialog = MutableSharedFlow() - val showSignOutDialog: SharedFlow = _showSignOutDialog.asSharedFlow() - /* Indicates the application is being restored after a photo capture. * * We need to persist this state here to control [HomeScreenFragement] UI treatments when returning @@ -122,10 +119,6 @@ internal constructor( suspend fun getOfflineAreas() = offlineAreaRepository.offlineAreas().first() - fun showSignOutDialog() { - viewModelScope.launch { _showSignOutDialog.emit(true) } - } - fun signOut() { viewModelScope.launch { userRepository.signOut() } } diff --git a/config/lint/lint.xml b/config/lint/lint.xml index 2de1d5a865..7402d7ee2d 100644 --- a/config/lint/lint.xml +++ b/config/lint/lint.xml @@ -53,7 +53,6 @@ - diff --git a/gradle.properties b/gradle.properties index e191aec6d4..32d84a544b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,6 @@ # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true - #Thu Dec 26 12:25:19 EST 2019 android.useAndroidX=true From 4ea3ae30e19fbe13eaf9d90803c9023765f74f51 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 16 Feb 2026 15:18:06 -0500 Subject: [PATCH 05/22] refactor(home): migrate drawer to Compose and remove XML --- .../android/ui/home/HomeScreenFragment.kt | 177 +++++++----------- .../android/ui/home/HomeScreenViewModel.kt | 6 + app/src/main/res/color/nav_drawer_item.xml | 22 --- app/src/main/res/layout/home_screen_frag.xml | 11 +- app/src/main/res/layout/nav_drawer_header.xml | 153 --------------- app/src/main/res/menu/nav_drawer_menu.xml | 47 ----- 6 files changed, 75 insertions(+), 341 deletions(-) delete mode 100644 app/src/main/res/color/nav_drawer_item.xml delete mode 100644 app/src/main/res/layout/nav_drawer_header.xml delete mode 100644 app/src/main/res/menu/nav_drawer_menu.xml diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt index 4875defa89..8461b7ae80 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt @@ -28,7 +28,6 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.google.android.material.imageview.ShapeableImageView -import com.google.android.material.navigation.NavigationView import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.launch @@ -36,12 +35,15 @@ import org.groundplatform.android.BuildConfig import org.groundplatform.android.R import org.groundplatform.android.data.local.room.converter.SubmissionDeltasConverter import org.groundplatform.android.databinding.HomeScreenFragBinding -import org.groundplatform.android.databinding.NavDrawerHeaderBinding import org.groundplatform.android.model.User import org.groundplatform.android.repository.UserRepository import org.groundplatform.android.ui.common.AbstractFragment import org.groundplatform.android.ui.common.BackPressListener import org.groundplatform.android.ui.common.EphemeralPopups +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.groundplatform.android.ui.components.ConfirmationDialog import org.groundplatform.android.ui.main.MainViewModel import org.groundplatform.android.util.setComposableContent @@ -53,14 +55,12 @@ import org.groundplatform.android.util.systemInsets * fragments (e.g., view submission and edit submission) at runtime. */ @AndroidEntryPoint -class HomeScreenFragment : - AbstractFragment(), BackPressListener, NavigationView.OnNavigationItemSelectedListener { +class HomeScreenFragment : AbstractFragment(), BackPressListener { @Inject lateinit var ephemeralPopups: EphemeralPopups @Inject lateinit var userRepository: UserRepository private lateinit var binding: HomeScreenFragBinding private lateinit var homeScreenViewModel: HomeScreenViewModel - private lateinit var user: User override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -76,31 +76,77 @@ class HomeScreenFragment : super.onCreateView(inflater, container, savedInstanceState) binding = HomeScreenFragBinding.inflate(inflater, container, false) binding.lifecycleOwner = this - lifecycleScope.launch { homeScreenViewModel.openDrawerRequestsFlow.collect { openDrawer() } } return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val binding = binding // Ensure nav drawer cannot be swiped out, which would conflict with map pan gestures. binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - homeScreenViewModel.showOfflineAreaMenuItem.observe(viewLifecycleOwner) { - binding.navView.menu.findItem(R.id.nav_offline_areas).isEnabled = it - } - binding.navView.setNavigationItemSelectedListener(this) - val navHeader = binding.navView.getHeaderView(0) - navHeader.findViewById(R.id.switch_survey_button).setOnClickListener { - findNavController() - .navigate( - HomeScreenFragmentDirections.actionHomeScreenFragmentToSurveySelectorFragment(false) + binding.composeView.setComposableContent { + val showSignOutDialog = homeScreenViewModel.showSignOutDialog.collectAsStateWithLifecycle(false) + + LaunchedEffect(Unit) { + homeScreenViewModel.openDrawerRequestsFlow.collect { openDrawer() } + } + + if (showSignOutDialog.value) { + ConfirmationDialog( + title = R.string.sign_out_dialog_title, + description = R.string.sign_out_dialog_body, + confirmButtonText = R.string.sign_out, + onConfirmClicked = { homeScreenViewModel.signOut() }, ) + } } - viewLifecycleOwner.lifecycleScope.launch { user = userRepository.getAuthenticatedUser() } - navHeader.findViewById(R.id.user_image).setOnClickListener { - showSignOutConfirmationDialogs() + + binding.drawerView.setComposableContent { + val user by produceState(initialValue = null) { + value = userRepository.getAuthenticatedUser() + } + val survey by homeScreenViewModel.surveyRepository.activeSurveyFlow.collectAsStateWithLifecycle() + + HomeDrawer( + user = user, + survey = survey, + onSwitchSurvey = { + findNavController() + .navigate( + HomeScreenFragmentDirections.actionHomeScreenFragmentToSurveySelectorFragment(false) + ) + }, + onNavigateToOfflineAreas = { + lifecycleScope.launch { + if (homeScreenViewModel.getOfflineAreas().isEmpty()) + findNavController().navigate(HomeScreenFragmentDirections.showOfflineAreaSelector()) + else findNavController().navigate(HomeScreenFragmentDirections.showOfflineAreas()) + } + closeDrawer() + }, + onNavigateToSyncStatus = { + findNavController().navigate(HomeScreenFragmentDirections.showSyncStatus()) + closeDrawer() + }, + onNavigateToSettings = { + findNavController() + .navigate(HomeScreenFragmentDirections.actionHomeScreenFragmentToSettingsActivity()) + closeDrawer() + }, + onNavigateToAbout = { + findNavController().navigate(HomeScreenFragmentDirections.showAbout()) + closeDrawer() + }, + onNavigateToTerms = { + findNavController().navigate(HomeScreenFragmentDirections.showTermsOfService(true)) + closeDrawer() + }, + onSignOut = { homeScreenViewModel.showSignOutDialog() }, + versionText = String.format(getString(R.string.build), BuildConfig.VERSION_NAME), + ) } - updateNavHeader() + // Re-open data collection screen if draft submission is present. viewLifecycleOwner.lifecycleScope.launch { homeScreenViewModel.getDraftSubmission()?.let { draft -> @@ -127,29 +173,8 @@ class HomeScreenFragment : } } } - - val navigationView = view.findViewById(R.id.nav_view) - val menuItem = navigationView.menu.findItem(R.id.nav_log_version) - menuItem.title = String.format(getString(R.string.build), BuildConfig.VERSION_NAME) } - private fun updateNavHeader() = - lifecycleScope.launch { - val navHeader = binding.navView.getHeaderView(0) - val headerBinding = NavDrawerHeaderBinding.bind(navHeader) - headerBinding.user = userRepository.getAuthenticatedUser() - homeScreenViewModel.surveyRepository.activeSurveyFlow.collect { - if (it == null) { - headerBinding.surveyInfo.visibility = View.GONE - headerBinding.noSurveysInfo.visibility = View.VISIBLE - } else { - headerBinding.noSurveysInfo.visibility = View.GONE - headerBinding.surveyInfo.visibility = View.VISIBLE - headerBinding.survey = it - } - } - } - private fun openDrawer() { binding.drawerLayout.openDrawer(GravityCompat.START) } @@ -158,77 +183,7 @@ class HomeScreenFragment : binding.drawerLayout.closeDrawer(GravityCompat.START) } - private fun onApplyWindowInsets(insets: WindowInsetsCompat) { - val headerView = binding.navView.getHeaderView(0) - headerView.setPadding(0, insets.systemInsets().top, 0, 0) - } + private fun onApplyWindowInsets(insets: WindowInsetsCompat) {} override fun onBack(): Boolean = false - - override fun onNavigationItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.sync_status -> { - findNavController().navigate(HomeScreenFragmentDirections.showSyncStatus()) - } - R.id.nav_offline_areas -> { - lifecycleScope.launch { - if (homeScreenViewModel.getOfflineAreas().isEmpty()) - findNavController().navigate(HomeScreenFragmentDirections.showOfflineAreaSelector()) - else findNavController().navigate(HomeScreenFragmentDirections.showOfflineAreas()) - } - } - R.id.nav_settings -> { - findNavController() - .navigate(HomeScreenFragmentDirections.actionHomeScreenFragmentToSettingsActivity()) - } - R.id.about -> { - findNavController().navigate(HomeScreenFragmentDirections.showAbout()) - } - R.id.terms_of_service -> { - findNavController().navigate(HomeScreenFragmentDirections.showTermsOfService(true)) - } - } - closeDrawer() - return true - } - - private fun showSignOutConfirmationDialogs() { - val showUserDetailsDialog = mutableStateOf(false) - val showSignOutDialog = mutableStateOf(false) - - fun showUserDetailsDialog() { - showUserDetailsDialog.value = true - showSignOutDialog.value = false - } - - fun showSignOutDialog() { - showUserDetailsDialog.value = false - showSignOutDialog.value = true - } - - fun hideAllDialogs() { - showUserDetailsDialog.value = false - showSignOutDialog.value = false - } - - // Init state for composition - showUserDetailsDialog() - - // Note: Adding a compose view to the fragment's view dynamically causes the navigation click to - // stop working after 1st time. Revisit this once the navigation drawer is also generated using - // compose. - binding.composeView.setComposableContent { - if (showUserDetailsDialog.value) { - UserDetailsDialog(user, { showSignOutDialog() }, { hideAllDialogs() }) - } - if (showSignOutDialog.value) { - ConfirmationDialog( - title = R.string.sign_out_dialog_title, - description = R.string.sign_out_dialog_body, - confirmButtonText = R.string.sign_out, - onConfirmClicked = { homeScreenViewModel.signOut() }, - ) - } - } - } } diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt index 72d82335b3..479d772220 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt @@ -55,6 +55,8 @@ internal constructor( private val savedStateHandle: SavedStateHandle = SavedStateHandle() private val _openDrawerRequests: MutableSharedFlow = MutableSharedFlow() val openDrawerRequestsFlow: SharedFlow = _openDrawerRequests.asSharedFlow() + private val _showSignOutDialog: MutableSharedFlow = MutableSharedFlow() + val showSignOutDialog: SharedFlow = _showSignOutDialog.asSharedFlow() // TODO: Allow tile source configuration from a non-survey accessible source. // Issue URL: https://github.com/google/ground-android/issues/1730 @@ -122,4 +124,8 @@ internal constructor( fun signOut() { viewModelScope.launch { userRepository.signOut() } } + + fun showSignOutDialog() { + viewModelScope.launch { _showSignOutDialog.emit(true) } + } } diff --git a/app/src/main/res/color/nav_drawer_item.xml b/app/src/main/res/color/nav_drawer_item.xml deleted file mode 100644 index 9e0a118926..0000000000 --- a/app/src/main/res/color/nav_drawer_item.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/home_screen_frag.xml b/app/src/main/res/layout/home_screen_frag.xml index d6696c54d8..fb27f2b6ac 100644 --- a/app/src/main/res/layout/home_screen_frag.xml +++ b/app/src/main/res/layout/home_screen_frag.xml @@ -53,17 +53,12 @@ - + android:fitsSystemWindows="true" /> \ No newline at end of file diff --git a/app/src/main/res/layout/nav_drawer_header.xml b/app/src/main/res/layout/nav_drawer_header.xml deleted file mode 100644 index e88c07a706..0000000000 --- a/app/src/main/res/layout/nav_drawer_header.xml +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/nav_drawer_menu.xml b/app/src/main/res/menu/nav_drawer_menu.xml deleted file mode 100644 index 31df413277..0000000000 --- a/app/src/main/res/menu/nav_drawer_menu.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - From 8748d6d081e411fcedfa3646b029f0fbb846476e Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 16 Feb 2026 15:22:42 -0500 Subject: [PATCH 06/22] Restore deleted files --- .agent/skills/fetch_pr_comments/SKILL.md | 35 ++++ .../scripts/fetch_comments.py | 157 ++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 .agent/skills/fetch_pr_comments/SKILL.md create mode 100644 .agent/skills/fetch_pr_comments/scripts/fetch_comments.py diff --git a/.agent/skills/fetch_pr_comments/SKILL.md b/.agent/skills/fetch_pr_comments/SKILL.md new file mode 100644 index 0000000000..72d0966049 --- /dev/null +++ b/.agent/skills/fetch_pr_comments/SKILL.md @@ -0,0 +1,35 @@ +--- +name: Fetch PR Comments +description: Fetches comments and reviews from the current GitHub Pull Request and formats them as Markdown. +--- + +# Fetch PR Comments + +This skill allows you to retrieve comments and reviews from the current GitHub Pull Request (PR) associated with the active branch, or a specific PR by number/URL. It converts the data into a readable Markdown format, including code review comments with file and line context. + +## Usage + +To use this skill, run the provided Python script. It requires the GitHub CLI (`gh`) to be installed and authenticated. + +### Command + +```bash +# Fetch comments for the current branch's PR +python3 .agent/skills/fetch_pr_comments/scripts/fetch_comments.py + +# Fetch comments for a specific PR +python3 .agent/skills/fetch_pr_comments/scripts/fetch_comments.py +``` + +### Output + +The script outputs Markdown text to stdout, containing: +- PR Title and URL +- Reviews (Approved/Changes Requested) +- General Comments (Pull Request level) +- Code Comments (Grouped by file and line number) + +## Dependencies + +- `gh` (GitHub CLI) must be installed and in your PATH. +- `python3` must be available. diff --git a/.agent/skills/fetch_pr_comments/scripts/fetch_comments.py b/.agent/skills/fetch_pr_comments/scripts/fetch_comments.py new file mode 100644 index 0000000000..508caa4e9c --- /dev/null +++ b/.agent/skills/fetch_pr_comments/scripts/fetch_comments.py @@ -0,0 +1,157 @@ +# +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import json +import subprocess +import sys +import re +from datetime import datetime + +def run_command(command): + try: + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + shell=True + ) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + if "no pull requests found" in e.stderr.lower(): + print("No pull request found for the current branch.", file=sys.stderr) + sys.exit(1) + + print(f"Error running command: {command}", file=sys.stderr) + print(f"Stderr: {e.stderr}", file=sys.stderr) + sys.exit(1) + +def format_date(iso_date): + try: + dt = datetime.fromisoformat(iso_date.replace("Z", "+00:00")) + return dt.strftime("%Y-%m-%d %H:%M") + except Exception: + return iso_date + + +def get_repo_owner_name(pr_url): + # Expected format: https://github.com/OWNER/REPO/pull/NUMBER + match = re.search(r"github\.com/([^/]+)/([^/]+)/pull/\d+", pr_url) + if match: + return match.group(1), match.group(2) + return None, None + +def get_code_comments(owner, repo, pr_number): + cmd = f"gh api repos/{owner}/{repo}/pulls/{pr_number}/comments --paginate" + try: + return json.loads(run_command(cmd)) + except Exception as e: + print(f"Warning: Failed to fetch code comments: {e}", file=sys.stderr) + return [] + +def main(): + # Allow optional PR argument (number or URL) + pr_arg = "" + if len(sys.argv) > 1: + pr_arg = f" {sys.argv[1]}" + + cmd = f"gh pr view{pr_arg} --json number,title,url,state,comments,reviews,latestReviews" + + try: + json_output = run_command(cmd) + pr_data = json.loads(json_output) + except Exception: + # If run_command fails or json decode fails, we exit (mostly handled in run_command) + sys.exit(1) + number = pr_data.get('number') + title = pr_data.get('title') + url = pr_data.get('url') + state = pr_data.get('state') + + owner, repo = get_repo_owner_name(url) + code_comments = [] + if owner and repo: + code_comments = get_code_comments(owner, repo, number) + + # --- Header --- + print(f"# PR #{number}: {title}") + print(f"**URL:** {url}") + print(f"**State:** {state}\n") + + # --- Reviews --- + print("## Reviews") + reviews = pr_data.get('latestReviews', []) + if not reviews: + print("No official reviews found.") + else: + for review in reviews: + review_state = review.get('state') + author = review.get('author', {}).get('login', 'Unknown') + date = format_date(review.get('submittedAt', '')) + print(f"- **{author}**: {review_state} ({date})") + print("\n" + "-"*40 + "\n") + + # --- General Comments --- + print("## General Comments") + general_comments = pr_data.get('comments', []) + if not general_comments: + print("No general comments.") + else: + for comment in general_comments: + author = comment.get('author', {}).get('login', 'Unknown') + body = comment.get('body', '').strip() + date = format_date(comment.get('createdAt', '')) + comment_url = comment.get('url', '') + + print(f"### {author} at {date}") + print(f"[Link]({comment_url})\n") + print(body) + print("\n") + print("-" * 40 + "\n") + + # --- Code Comments --- + print("## Code Comments") + if not code_comments: + print("No code comments.") + else: + # Group by file path + comments_by_file = {} + for cc in code_comments: + path = cc.get('path', 'Unknown File') + if path not in comments_by_file: + comments_by_file[path] = [] + comments_by_file[path].append(cc) + + for path, comments in comments_by_file.items(): + print(f"### File: `{path}`\n") + # Sort by line number (or position if line is None) + comments.sort(key=lambda x: (x.get('line') or x.get('original_line') or 0)) + + for cc in comments: + author = cc.get('user', {}).get('login', 'Unknown') + body = cc.get('body', '').strip() + date = format_date(cc.get('created_at', '')) + line = cc.get('line') or cc.get('original_line') or "Outdated" + html_url = cc.get('html_url', '') + + print(f"#### Line {line} - {author} ({date})") + print(f"[Link]({html_url})\n") + print(body) + print("\n") + print("-" * 20 + "\n") + +if __name__ == "__main__": + main() From 6f4f819703aaa344c43cec0f2a94ca35fb1d1ce7 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 16 Feb 2026 15:32:21 -0500 Subject: [PATCH 07/22] fix(home): add system bars padding to drawer --- .../main/java/org/groundplatform/android/ui/home/HomeDrawer.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt index 98270c797e..fbb5774a41 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape @@ -70,6 +71,7 @@ fun HomeDrawer( modifier = Modifier.fillMaxWidth() .background(MaterialTheme.colorScheme.surface) + .systemBarsPadding() .verticalScroll(rememberScrollState()) ) { AppInfoHeader(user = user) From c1513808df24dbcc63b69258b4153cc70638fa32 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 16 Feb 2026 15:44:18 -0500 Subject: [PATCH 08/22] refactor: clean up unused imports and reformat Compose state observation calls. --- .../android/ui/home/HomeScreenFragment.kt | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt index 8461b7ae80..0290403c60 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt @@ -17,17 +17,17 @@ package org.groundplatform.android.ui.home import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.TextView -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.core.view.GravityCompat import androidx.core.view.WindowInsetsCompat import androidx.drawerlayout.widget.DrawerLayout +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController -import com.google.android.material.imageview.ShapeableImageView import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.launch @@ -40,14 +40,9 @@ import org.groundplatform.android.repository.UserRepository import org.groundplatform.android.ui.common.AbstractFragment import org.groundplatform.android.ui.common.BackPressListener import org.groundplatform.android.ui.common.EphemeralPopups -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState -import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.groundplatform.android.ui.components.ConfirmationDialog import org.groundplatform.android.ui.main.MainViewModel import org.groundplatform.android.util.setComposableContent -import org.groundplatform.android.util.systemInsets /** * Fragment containing the map container and location of interest sheet fragments and NavigationView @@ -86,11 +81,10 @@ class HomeScreenFragment : AbstractFragment(), BackPressListener { binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) binding.composeView.setComposableContent { - val showSignOutDialog = homeScreenViewModel.showSignOutDialog.collectAsStateWithLifecycle(false) + val showSignOutDialog = + homeScreenViewModel.showSignOutDialog.collectAsStateWithLifecycle(false) - LaunchedEffect(Unit) { - homeScreenViewModel.openDrawerRequestsFlow.collect { openDrawer() } - } + LaunchedEffect(Unit) { homeScreenViewModel.openDrawerRequestsFlow.collect { openDrawer() } } if (showSignOutDialog.value) { ConfirmationDialog( @@ -103,10 +97,10 @@ class HomeScreenFragment : AbstractFragment(), BackPressListener { } binding.drawerView.setComposableContent { - val user by produceState(initialValue = null) { - value = userRepository.getAuthenticatedUser() - } - val survey by homeScreenViewModel.surveyRepository.activeSurveyFlow.collectAsStateWithLifecycle() + val user by + produceState(initialValue = null) { value = userRepository.getAuthenticatedUser() } + val survey by + homeScreenViewModel.surveyRepository.activeSurveyFlow.collectAsStateWithLifecycle() HomeDrawer( user = user, From 6cf23b4c51ed70fe0dcc032ae80c8e0839e198e6 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 16 Feb 2026 15:46:35 -0500 Subject: [PATCH 09/22] feat: move version text display from a clickable drawer item to a static row at the bottom of the drawer --- .../android/ui/home/HomeDrawer.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt index fbb5774a41..af4b8d61b1 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt @@ -131,7 +131,6 @@ private fun DrawerItems( icon = IconSource.Vector(Icons.AutoMirrored.Filled.ExitToApp), onClick = onSignOut, ), - DrawerItem(label = versionText, icon = IconSource.Vector(Icons.Default.Build), onClick = {}), ) navItems.forEach { item -> @@ -150,6 +149,26 @@ private fun DrawerItems( modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), ) } + + Row( + modifier = + Modifier.fillMaxWidth() + .padding(NavigationDrawerItemDefaults.ItemPadding) + .padding(start = 16.dp, end = 24.dp, top = 12.dp, bottom = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.Build, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.width(12.dp)) + Text( + text = versionText, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } private data class DrawerItem(val label: String, val icon: IconSource, val onClick: () -> Unit) From 8cd3af24ab7be5d2400f7ca8e9d1697f6addb31d Mon Sep 17 00:00:00 2001 From: Gino Miceli <228050+gino-m@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:46:51 -0500 Subject: [PATCH 10/22] Update app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../main/java/org/groundplatform/android/ui/home/HomeDrawer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt index af4b8d61b1..32314d56ba 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt @@ -139,7 +139,7 @@ private fun DrawerItems( selected = false, onClick = item.onClick, icon = { - val description = item.label + val description = null when (item.icon) { is IconSource.Vector -> Icon(item.icon.imageVector, contentDescription = description) is IconSource.Drawable -> From fa98dbf715d77a514df06ab31cc2b4d11cdea805 Mon Sep 17 00:00:00 2001 From: Gino Miceli <228050+gino-m@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:47:06 -0500 Subject: [PATCH 11/22] Update app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../main/java/org/groundplatform/android/ui/home/HomeDrawer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt index 32314d56ba..b75847c075 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt @@ -262,7 +262,7 @@ private fun SurveySelector(survey: Survey?, onSwitchSurvey: () -> Unit) { style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, - modifier = Modifier.clickable(onClick = onSwitchSurvey).padding(vertical = 8.dp), + modifier = Modifier.padding(vertical = 8.dp).clickable(onClick = onSwitchSurvey), ) } } From 67c1ff65fe24c25bd31a2ab6d9557fcff14c6867 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 16 Feb 2026 16:02:03 -0500 Subject: [PATCH 12/22] Fix checkCode issues: Refactor HomeDrawer/HomeScreenFragment, fix formatting and unused resources --- .../android/ui/home/HomeDrawer.kt | 40 +++++++++++-------- .../android/ui/home/HomeScreenFragment.kt | 15 ++++--- .../color/task_transparent_btn_selector.xml | 22 ---------- .../main/res/drawable-anydpi/ic_history.xml | 29 -------------- app/src/main/res/drawable/cloud_off.xml | 28 ------------- app/src/main/res/drawable/ic_settings.xml | 28 ------------- app/src/main/res/values/dimens.xml | 3 -- app/src/main/res/values/styles.xml | 21 ++++++++++ 8 files changed, 55 insertions(+), 131 deletions(-) delete mode 100644 app/src/main/res/color/task_transparent_btn_selector.xml delete mode 100644 app/src/main/res/drawable-anydpi/ic_history.xml delete mode 100644 app/src/main/res/drawable/cloud_off.xml delete mode 100644 app/src/main/res/drawable/ic_settings.xml diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt index b75847c075..88f19990e8 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt @@ -133,23 +133,31 @@ private fun DrawerItems( ), ) - navItems.forEach { item -> - NavigationDrawerItem( - label = { Text(item.label) }, - selected = false, - onClick = item.onClick, - icon = { - val description = null - when (item.icon) { - is IconSource.Vector -> Icon(item.icon.imageVector, contentDescription = description) - is IconSource.Drawable -> - Icon(painterResource(item.icon.id), contentDescription = description) - } - }, - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), - ) - } + navItems.forEach { item -> DrawerNavigationItem(item) } + DrawerVersionFooter(versionText) +} + +@Composable +private fun DrawerNavigationItem(item: DrawerItem) { + NavigationDrawerItem( + label = { Text(item.label) }, + selected = false, + onClick = item.onClick, + icon = { + val description = null + when (item.icon) { + is IconSource.Vector -> Icon(item.icon.imageVector, contentDescription = description) + is IconSource.Drawable -> + Icon(painterResource(item.icon.id), contentDescription = description) + } + }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + ) +} + +@Composable +private fun DrawerVersionFooter(versionText: String) { Row( modifier = Modifier.fillMaxWidth() diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt index 0290403c60..aebd1c349d 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt @@ -23,7 +23,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.core.view.GravityCompat -import androidx.core.view.WindowInsetsCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope @@ -41,7 +40,6 @@ import org.groundplatform.android.ui.common.AbstractFragment import org.groundplatform.android.ui.common.BackPressListener import org.groundplatform.android.ui.common.EphemeralPopups import org.groundplatform.android.ui.components.ConfirmationDialog -import org.groundplatform.android.ui.main.MainViewModel import org.groundplatform.android.util.setComposableContent /** @@ -59,7 +57,6 @@ class HomeScreenFragment : AbstractFragment(), BackPressListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - getViewModel(MainViewModel::class.java).windowInsets.observe(this) { onApplyWindowInsets(it) } homeScreenViewModel = getViewModel(HomeScreenViewModel::class.java) } @@ -80,6 +77,12 @@ class HomeScreenFragment : AbstractFragment(), BackPressListener { // Ensure nav drawer cannot be swiped out, which would conflict with map pan gestures. binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + setupComposeView(binding) + setupDrawerContent(binding) + restoreDraftSubmission(binding) + } + + private fun setupComposeView(binding: HomeScreenFragBinding) { binding.composeView.setComposableContent { val showSignOutDialog = homeScreenViewModel.showSignOutDialog.collectAsStateWithLifecycle(false) @@ -95,7 +98,9 @@ class HomeScreenFragment : AbstractFragment(), BackPressListener { ) } } + } + private fun setupDrawerContent(binding: HomeScreenFragBinding) { binding.drawerView.setComposableContent { val user by produceState(initialValue = null) { value = userRepository.getAuthenticatedUser() } @@ -140,7 +145,9 @@ class HomeScreenFragment : AbstractFragment(), BackPressListener { versionText = String.format(getString(R.string.build), BuildConfig.VERSION_NAME), ) } + } + private fun restoreDraftSubmission(binding: HomeScreenFragBinding) { // Re-open data collection screen if draft submission is present. viewLifecycleOwner.lifecycleScope.launch { homeScreenViewModel.getDraftSubmission()?.let { draft -> @@ -177,7 +184,5 @@ class HomeScreenFragment : AbstractFragment(), BackPressListener { binding.drawerLayout.closeDrawer(GravityCompat.START) } - private fun onApplyWindowInsets(insets: WindowInsetsCompat) {} - override fun onBack(): Boolean = false } diff --git a/app/src/main/res/color/task_transparent_btn_selector.xml b/app/src/main/res/color/task_transparent_btn_selector.xml deleted file mode 100644 index b30f3494b1..0000000000 --- a/app/src/main/res/color/task_transparent_btn_selector.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_history.xml b/app/src/main/res/drawable-anydpi/ic_history.xml deleted file mode 100644 index 885eb0601c..0000000000 --- a/app/src/main/res/drawable-anydpi/ic_history.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/cloud_off.xml b/app/src/main/res/drawable/cloud_off.xml deleted file mode 100644 index eb8b3d88f5..0000000000 --- a/app/src/main/res/drawable/cloud_off.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml deleted file mode 100644 index ee9f81c935..0000000000 --- a/app/src/main/res/drawable/ic_settings.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e8cfac489a..6d93e93bea 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -26,9 +26,6 @@ 48dp 4dp - - 36dp - 50dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 8ff2754e40..c74b0ddafe 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -88,6 +88,27 @@ ?attr/colorOnSurfaceVariant + + + + + + + - - - - - - - - - - - -