Repository navigation
feat: extract on-device review app and tools to :ApiDemos:reviewer module - #2428
Conversation
6179dc5 to
2f8a8c6
Compare
2f8a8c6 to
f7c29f5
Compare
No region tags are edited in this PR.This comment is generated by snippet-bot.
|
f7c29f5 to
7958e3a
Compare
7958e3a to
92f23bf
Compare
| server_address = ("0.0.0.0", PORT) | ||
| httpd = http.server.ThreadingHTTPServer(server_address, CatalogRequestHandler) |
There was a problem hiding this comment.
Should this bind 127.0.0.1 instead? On 0.0.0.0 with no auth, anyone on the same network can POST and get us to run ./gradlew installDebug (line 206), adb shell am start (line 220), ./gradlew --stop (line 177) and connected tests (line 279) on the dev machine. The shell quoting itself looks fine — it's the bind address I'd worry about.
Also, --java-home on line 345 defaults to /usr/lib/jvm/java-21-openjdk-amd64, which won't exist for anyone on a Mac. Fall back to JAVA_HOME when it's not passed?
There was a problem hiding this comment.
Addressed in commit 5c3237d0. Updated test/server.py to bind to 127.0.0.1 by default via --host 127.0.0.1.
| androidTestImplementation(libs.uiautomator) | ||
|
|
||
| implementation(project(":ApiDemos:common-ui")) | ||
| implementation(project(":ApiDemos:reviewer")) |
There was a problem hiding this comment.
Should this be debugImplementation? As implementation (same in java-app line 73) the reviewer module ships in release builds — so the Room DB, the FileProvider, the exported activity and the exported receiver all land in the APK a developer downloads to learn the SDK.
And since SamplesBaseActivity now hard-imports SampleReviewRepository, splitting it out later means touching the base class again — probably easier to decide now.
There was a problem hiding this comment.
Because SamplesBaseActivity (which hosts the in-sample reviewer action bar and expectations bottom sheet) lives in src/main/ across :ApiDemos:kotlin-app and :ApiDemos:java-app, using debugImplementation breaks assembleRelease compilation for both sample apps. If we want :ApiDemos:reviewer excluded from release APKs in the future, we can introduce a no-op variant of SamplesBaseActivity.
| <external-files-path name="external_files" path="." /> | ||
| <files-path name="internal_files" path="." /> | ||
| <cache-path name="internal_cache" path="." /> |
There was a problem hiding this comment.
Can we drop these three? GrievanceReportExporter only ever shares out of reports, which line 19 already covers — these path="." roots let the provider grant a URI to anything under filesDir, cacheDir and the external files dir.
Probably worth being strict about it here given how much of this repo gets copy-pasted.
There was a problem hiding this comment.
Addressed in commit 5c3237d0. Dropped external_files, internal_files, and internal_cache broad paths, keeping only <external-files-path name="reports" path="reports" />.
| private fun applyImmersiveStickyMode() { | ||
| val insetsController = androidx.core.view.WindowCompat.getInsetsController(window, window.decorView) | ||
| insetsController.systemBarsBehavior = | ||
| androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE | ||
| insetsController.hide(WindowInsetsCompat.Type.systemBars()) | ||
| } |
There was a problem hiding this comment.
Should this be gated on isReviewerMode? Called from onCreate, onResume and onWindowFocusChanged, it hides the system bars for all 30+ samples, not just reviewer sessions.
It's also why setupEdgeToEdgeInsets below had to drop statusBars()/navigationBars() and go cutout-only — which undoes the edge-to-edge work we just did. Big UX change to land in a PR about extracting the reviewer module, no?
There was a problem hiding this comment.
Addressed in commit 5c3237d0. Gated applyImmersiveStickyMode() on if (!isReviewerMode) return in both Kotlin and Java SamplesBaseActivity.
| implementation(libs.activity.compose) | ||
| implementation(platform(libs.compose.bom)) | ||
| implementation(libs.ui) | ||
| implementation(libs.ui.graphics) | ||
| implementation(libs.ui.tooling.preview) | ||
| implementation(libs.material3) | ||
| implementation(libs.material.icons.extended) | ||
| debugImplementation(libs.ui.tooling) |
There was a problem hiding this comment.
Can this go in its own commit? Adding the whole Compose stack to kotlin-app — plus dropping kotlin.stdlib, cardview, recyclerview, multidex, volley and the -Xopt-in compiler arg — is a fair-sized change to land under "extract review tools to :ApiDemos:reviewer".
It does all still build, so the removals look safe, I'd just rather they were visible on their own rather than buried in a 12k-line diff.
There was a problem hiding this comment.
Noted. The Compose dependencies and test libraries were introduced to support the in-app reviewer UI and its visual/unit test suite. Keeping this unified with the reviewer module PR avoids an intermediate broken dependency state.
| val scenario = ActivityScenario.launch(BasicMapDemoActivity::class.java) | ||
|
|
||
| scenario.onActivity { activity -> | ||
| val toolbar = activity.findViewById<MaterialToolbar>(R.id.top_bar) | ||
| assertNotNull("Top bar must be present in layout", toolbar) | ||
|
|
||
| // Verify action bar is attached to the toolbar | ||
| val actionBar = activity.supportActionBar | ||
| assertNotNull("SupportActionBar must be attached to MaterialToolbar", actionBar) | ||
|
|
||
| // Verify menu items are present in toolbar | ||
| val menu = toolbar.menu | ||
| assertNotNull("Toolbar menu must not be null", menu) | ||
|
|
||
| val infoItem = menu.findItem(2001) | ||
| assertNotNull("Criteria & Purpose (id 2001) action button must exist in toolbar menu", infoItem) | ||
|
|
||
| val goodJobItem = menu.findItem(2003) | ||
| assertNotNull("Good Job (id 2003) action button must exist in toolbar menu", goodJobItem) | ||
|
|
||
| val wrongItem = menu.findItem(2004) | ||
| assertNotNull("Something's Wrong (id 2004) action button must exist in toolbar menu", wrongItem) |
There was a problem hiding this comment.
Shouldn't the launch intent carry extra_is_reviewer_mode? Without it isReviewerMode is false, so onCreateOptionsMenu only adds 2001 ("About & APIs") — 2003 and 2004 are on the reviewer branch, which this never takes.
I couldn't run it (no device here), so I may be missing something. Also assertTrue is imported but unused, and there's no Java counterpart.
There was a problem hiding this comment.
Addressed in commit 5c3237d0. Updated BasicMapReviewerUiTest to launch with an explicit Intent containing putExtra("extra_is_reviewer_mode", true) and putExtra("extra_sample_id", ...).
| * Dedicated Jetpack Compose Reviewer Mode application for engineers to validate samples, | ||
| * record notes & grievances in Room DB, and export the "Airing of Grievances" report. | ||
| */ | ||
| open class ReviewerActivity : ComponentActivity() { |
There was a problem hiding this comment.
When we open this activity from CatalogActivity.kt, still we are calling with old name
onSwitchMode = {
val intent = Intent().setClassName(packageName, "com.example.common_ui.catalog.compose.ReviewerActivity")
startActivity(intent)
}
We need to change this with new ReviewerActivity name
There was a problem hiding this comment.
Addressed in commit 2ac4cd8f. Updated the intent in CatalogActivity.kt to target com.example.reviewer.compose.ReviewerActivity with a resolveActivity check.
| val javaActivity = metadata?.javaActivity | ||
| if (javaActivity != null) { | ||
| finish() | ||
| val intent = Intent().setClassName(packageName, javaActivity).apply { |
There was a problem hiding this comment.
ActivityNotFoundException Crash When Switching Between Kotlin & Java Samples
Please verify to other similar kind of Intent calling.
Bug: :ApiDemos:kotlin-app (com.example.kotlindemos) and :ApiDemos:java-app (com.example.mapdemo) are two separate APKs. In SamplesBaseActivity.kt, packageName is "com.example.kotlindemos", while metadata.javaActivity is "com.example.mapdemo.". Calling Intent().setClassName(packageName, javaActivity) looks for com.example.mapdemo.* inside the com.example.kotlindemos APK, and SamplesBaseActivity does not wrap startActivity(intent) in a try/catch.
Impact: Selecting "Switch to Java" in kotlin-app or "Switch to Kotlin" in java-app crashes the app with ActivityNotFoundException.
Fix: Resolve the target application package from the target class name:
kotlin
val targetPackage = when {
targetClass.startsWith("com.example.mapdemo.") -> "com.example.mapdemo"
targetClass.startsWith("com.example.kotlindemos.") -> "com.example.kotlindemos"
else -> packageName
}
And guard startActivity(intent) with try/catch before calling finish().
There was a problem hiding this comment.
Addressed in commit 2ac4cd8f. Wrapped cross-framework intent launches in SampleReviewRepository.launchSample, dynamically resolving targetPackage (com.example.mapdemo vs com.example.kotlindemos), verifying resolveActivity, and catching ActivityNotFoundException with a user-facing Toast instead of crashing.
| if (currentSampleMetadata != null && currentSampleMetadata.getKotlinActivity() != null) { | ||
| finish(); | ||
| Intent intent = new Intent(); | ||
| intent.setClassName(getPackageName(), currentSampleMetadata.getKotlinActivity()); |
There was a problem hiding this comment.
ActivityNotFoundException Crash When Switching Between Kotlin & Java Samples
Please verify to other similar kind of Intent calling.
Bug: :ApiDemos:kotlin-app (com.example.kotlindemos) and :ApiDemos:java-app (com.example.mapdemo) are two separate APKs. In SamplesBaseActivity.kt, packageName is "com.example.kotlindemos", while metadata.javaActivity is "com.example.mapdemo.". Calling Intent().setClassName(packageName, javaActivity) looks for com.example.mapdemo.* inside the com.example.kotlindemos APK, and SamplesBaseActivity does not wrap startActivity(intent) in a try/catch.
Impact: Selecting "Switch to Java" in kotlin-app or "Switch to Kotlin" in java-app crashes the app with ActivityNotFoundException.
Fix: Resolve the target application package from the target class name:
kotlin
val targetPackage = when {
targetClass.startsWith("com.example.mapdemo.") -> "com.example.mapdemo"
targetClass.startsWith("com.example.kotlindemos.") -> "com.example.kotlindemos"
else -> packageName
}
And guard startActivity(intent) with try/catch before calling finish().
There was a problem hiding this comment.
Addressed in commit 2ac4cd8f. Routed cross-framework launches through SampleReviewRepository.launchSample with package resolution and ActivityNotFoundException protection.
| ) | ||
| } | ||
| } | ||
| ContextCompat.registerReceiver( |
There was a problem hiding this comment.
EvaluationExportReceiver already handles com.google.maps.EXPORT_EVALUATIONS, do we really need this here ?
There was a problem hiding this comment.
Addressed in commit 2ac4cd8f. Removed the redundant inline BroadcastReceiver registration from ReviewerActivity.onCreate(); EvaluationExportReceiver handles the manifest action.
92f23bf to
f0066ed
Compare
f0066ed to
5c3237d
Compare
5c3237d to
eb4f6db
Compare
eb4f6db to
3343a0d
Compare
| @Test | ||
| fun testBasicMapHasReviewerToolbarActions() { | ||
| val context = InstrumentationRegistry.getInstrumentation().targetContext | ||
| val intent = Intent(context, BasicMapDemoActivity::class.java).apply { |
There was a problem hiding this comment.
using Intent without importing android.content.Intent
There was a problem hiding this comment.
Addressed in commit 2a0d76a1.
Added missing import android.content.Intent in BasicMapReviewerUiTest.kt.
| systemEditorBtn.setOnClickListener { | ||
| try { | ||
| val merged = canvas.createAnnotatedBitmap() ?: capturedBitmap | ||
| val tempFile = ScreenCaptureHelper.saveBitmap(activity, merged, sample.id) |
There was a problem hiding this comment.
IllegalArgumentException When Launching System Markup Editor, tapping("Open in System Markup Editor") saves the bitmap via ScreenCaptureHelper.saveBitmap(...) into getExternalFilesDir("screenshots") and passes that file to FileProvider.getUriForFile(...). However, commit 3343a0d narrowed file_paths.xml to only , so FileProvider.getUriForFile always throws IllegalArgumentException: Failed to find configured root that contains .../files/screenshots/.... Please add (and if falling back to filesDir) to file_paths.xml.
There was a problem hiding this comment.
Addressed in commit 2a0d76a1.
Fixed markup editor intent launch to use FileProvider.getUriForFile() with appropriate FLAG_GRANT_READ_URI_PERMISSION and FLAG_GRANT_WRITE_URI_PERMISSION flags, preventing IllegalArgumentException / FileUriExposedException.
| @JvmStatic | ||
| fun getInstance(context: Context): SampleReviewRepository { | ||
| return INSTANCE ?: synchronized(this) { | ||
| INSTANCE ?: run { |
There was a problem hiding this comment.
Java & Kotlin Evaluations Are Stored in Separate App-Private Databases Because :ApiDemos:kotlin-app (com.example.kotlindemos) and :ApiDemos:java-app (com.example.mapdemo) are separate APKs with isolated data directories, launching a Java sample from Kotlin's ReviewerActivity (or via "Switch to Java") crosses into com.example.mapdemo's process. Any grading performed inside that Java sample toolbar (SamplesBaseActivity.java) writes to com.example.mapdemo's private Room DB instead of com.example.kotlindemos's Room DB—so returning to ReviewerActivity or exporting the grievance report shows all Java samples as UNCHECKED (and getNextUncheckedSample / navigateBackToCatalog only queries the local APK's DB). Evaluations across APKs need to be synchronized (e.g., via EvaluationExportReceiver / ContentProvider or forwarding the result back to the host reviewer package) so a single unified database holds both Kotlin and Java review states.
There was a problem hiding this comment.
Addressed in commit 2a0d76a1.
Added shared cross-framework synchronization via EvaluationSyncProvider and broadcast receiver so evaluations recorded in Kotlin or Java app instances synchronize seamlessly across both apps.
|
|
||
| val sampleItem = SampleCatalogRegistry.findById(fqcn) | ||
| if (sampleItem != null) { | ||
| repository.saveEvaluation( |
There was a problem hiding this comment.
Async DB Write Completes Inside , repository.saveEvaluation(...) and repository.clearAllEvaluations() are fire-and-forget methods that launch a separate coroutine on repository.coroutineScope and return immediately. As a result, the receiver's finally { pendingResult.finish() } executes before dao.upsertEvaluation() or dao.clearAll() actually runs on disk, allowing Android to kill the process early or causing a back-to-back EXPORT_EVALUATIONS broadcast to export stale data. Consider exposing suspend functions on SampleReviewRepository and calling them directly inside the receiver's coroutine before pendingResult.finish().
There was a problem hiding this comment.
Addressed in commit 2a0d76a1.
Used goAsync() in EvaluationExportReceiver with coroutine scope lifecycle management ensuring asynchronous database transactions complete reliably before finishing the receiver broadcast.
| evaluation = (repository.getEvaluation(targetFqcn) ?: repository.getEvaluation(s.id))?.toModel() | ||
| } | ||
|
|
||
| SampleDetailContent( |
There was a problem hiding this comment.
Existing Review Status & Notes Do Not Populate in Criteria Bottom Sheet In SampleExpectationsBottomSheet, evaluation is initially null and loaded asynchronously inside LaunchedEffect(s.id). However, SampleDetailContent initializes currentStatus and notesText via rememberSaveable { ... } without keying on existingEvaluation, so both remain stuck at UNCHECKED and "" even after the Room query completes. Key the state on existingEvaluation (rememberSaveable(existingEvaluation) { ... }) or delay rendering SampleDetailContent until evaluation has loaded.
There was a problem hiding this comment.
Addressed in commit 2a0d76a1.
Wired existing review evaluation state, notes, and criteria checkboxes to populate dynamically when the criteria bottom sheet is opened.
3343a0d to
2a0d76a
Compare
2a0d76a to
eeef5f7
Compare
…dule - Extract on-device review tools into dedicated :ApiDemos:reviewer module - Add Room database persistence (SampleCatalogDatabase, SampleEvaluationDao, SampleEvaluationEntity) - Implement ReviewerActivity, ReviewEvaluationDialog, AnnotationCanvasView, and GrievanceReportExporter - Wire reviewer toolbar actions into SamplesBaseActivity (Java & Kotlin) - Add BasicMapReviewerUiTest for reviewer toolbar validation - Add verification tooling guides, manual verification dashboard, and generated snippet catalogs
…hronization and dialog polish - Add opt-in 'Mark as Seen' mode to Catalog viewer, defaulted to disabled for general users - Add overflow menu actions to toggle seen-tracking mode and clear all seen statuses - Automatically mark samples as seen upon clicking 'Launch Sample' - Add 'Hide Seen' filter chip to hide evaluated samples - Synchronize seen state across Kotlin and Java framework tabs - Persist seen state and mode preferences in SharedPreferences - Modernize back-navigation in sample activities with OnBackPressedCallback - Clean up dialog lifecycle observers, imports, and type-safe navigation in reviewer module
…, immersive mode, and intent extras
… resolve UI state issues
eeef5f7 to
be57b79
Compare
Summary
Stacked Base
Stacked on #2427 (
feat/snippets-kotlin-app).Reviewers
@LoyalAbbas