diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 43d73f14..3e2dc6c2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -13,6 +13,13 @@ - On Windows, use `.\gradlew.bat` instead of `./gradlew`. - CI runs JDK 21, writes `app/google-services.json` from secrets, initializes submodules, and runs `./gradlew testDebugUnitTest`; test failures are `continue-on-error`, so inspect uploaded reports. +## Crashlytics investigation + +- For Crashlytics issue URLs, extract the issue id from `/issues/{issue-id}` and the event id from the `sessionEventKey` query parameter. +- Use Firebase Console or authenticated local tooling to inspect issue and event details. Keep Firebase project ids, app ids, OAuth tokens, raw logs, and private Crashlytics URLs out of commits, PR descriptions, and public docs. +- The Firebase CLI only exposes Crashlytics mapping/symbol upload commands, not issue/event reads. If REST API access is needed, use the current local Firebase CLI auth token without printing or copying token values. +- Inspect the issue title, fatal exception, app version, device/OS, stack trace, breadcrumbs, and logs. Verify the inferred app flow against app logs; if logs do not identify the triggering UI action, add a targeted `AppLog` at the app-owned boundary that launches the crashing flow. + ## Release and open testing - "Prepare the branch for release" means preparing a publishable Play/open-testing version, not installing on a device. Before creating an open testing release, bump `versionCode`, regenerate the release baseline profile with `:app:generateReleaseBaselineProfile` on a physical device, and include any changed generated baseline profile files. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dc78a6fd..6c5d6beb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -77,11 +77,9 @@ android:taskAffinity="com.anod.appwatcher.notification" android:theme="@style/InvisibleTheme" /> - + android:name="com.google.android.gms.oss.licenses.v2.OssLicensesMenuActivity" + android:theme="@style/Theme.AppCompat.DayNight" + tools:replace="android:theme" /> emitAction( - SettingsViewAction.StartActivity( - Intent(application, OssLicensesMenuActivity::class.java), - )) + SettingsViewEvent.OssLicenses -> { + AppLog.i("Open source licenses selected", "Settings") + emitAction( + SettingsViewAction.StartActivity( + Intent(application, OssLicensesMenuActivity::class.java), + )) + } is SettingsViewEvent.SetRecreateFlag -> { val result = setRecreateFlag(event.item, event.enabled) event.update(result) diff --git a/app/src/main/java/com/anod/appwatcher/search/ListEndpointPagingSource.kt b/app/src/main/java/com/anod/appwatcher/search/ListEndpointPagingSource.kt index b4796fd7..be7ecfa8 100644 --- a/app/src/main/java/com/anod/appwatcher/search/ListEndpointPagingSource.kt +++ b/app/src/main/java/com/anod/appwatcher/search/ListEndpointPagingSource.kt @@ -30,8 +30,8 @@ abstract class ListEndpointPagingSource(private val listType: DfeListType, priva AppLog.d("ListPagingSource load: [${params.key}] null") return LoadResult.Page(emptyList(), null, null) } - val nextPageUrl = params.key ?: "" - val response = execute(nextPageUrl = nextPageUrl).toListResponse(listType) + val requestedNextPageUrl = params.key ?: "" + val response = execute(nextPageUrl = requestedNextPageUrl).toListResponse(listType) val items = response.items.map { val installedInfo = installedApps.packageInfo(it.docId) @@ -43,11 +43,15 @@ abstract class ListEndpointPagingSource(private val listType: DfeListType, priva } isFirst = false - AppLog.d("ListPagingSource load: [${params.key}] $nextPageUrl") + val nextPageUrl = resolveNextPageUrl( + requestedNextPageUrl = requestedNextPageUrl, + responseNextPageUrl = response.nextPageUrl + ) + AppLog.d("ListPagingSource load: [${params.key}] $requestedNextPageUrl -> $nextPageUrl") return LoadResult.Page( data = items, prevKey = null, // Only paging forward. - nextKey = response.nextPageUrl + nextKey = nextPageUrl ) } catch (e: Exception) { AppLog.e(e) @@ -56,4 +60,11 @@ abstract class ListEndpointPagingSource(private val listType: DfeListType, priva } override fun getRefreshKey(state: PagingState): String? = null + + companion object { + internal fun resolveNextPageUrl(requestedNextPageUrl: String, responseNextPageUrl: String?): String? { + val nextPageUrl = responseNextPageUrl?.takeIf { it.isNotBlank() } + return nextPageUrl?.takeUnless { it == requestedNextPageUrl } + } + } } \ No newline at end of file diff --git a/app/src/test/java/com/anod/appwatcher/search/ListEndpointPagingSourceTest.kt b/app/src/test/java/com/anod/appwatcher/search/ListEndpointPagingSourceTest.kt new file mode 100644 index 00000000..13b4e515 --- /dev/null +++ b/app/src/test/java/com/anod/appwatcher/search/ListEndpointPagingSourceTest.kt @@ -0,0 +1,51 @@ +package com.anod.appwatcher.search + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ListEndpointPagingSourceTest { + + @Test + fun `resolveNextPageUrl returns new continuation url`() { + assertEquals( + "purchaseHistory?o=158", + ListEndpointPagingSource.resolveNextPageUrl( + requestedNextPageUrl = "purchaseHistory?o=79", + responseNextPageUrl = "purchaseHistory?o=158" + ) + ) + } + + @Test + fun `resolveNextPageUrl stops paging when continuation url repeats`() { + assertEquals( + null, + ListEndpointPagingSource.resolveNextPageUrl( + requestedNextPageUrl = "purchaseHistory?o=158", + responseNextPageUrl = "purchaseHistory?o=158" + ) + ) + } + + @Test + fun `resolveNextPageUrl stops paging when continuation url is missing`() { + assertEquals( + null, + ListEndpointPagingSource.resolveNextPageUrl( + requestedNextPageUrl = "purchaseHistory?o=158", + responseNextPageUrl = null + ) + ) + } + + @Test + fun `resolveNextPageUrl stops paging when continuation url is blank`() { + assertEquals( + null, + ListEndpointPagingSource.resolveNextPageUrl( + requestedNextPageUrl = "purchaseHistory?o=158", + responseNextPageUrl = "" + ) + ) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0a4d3bee..a75b2b0b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,7 +39,7 @@ paging = "3.3.6" palette = "1.0.0" play-services-auth = "21.5.0" play-services-identity = "18.1.0" -play-services-oss-licenses = "17.3.0" +play-services-oss-licenses = "17.5.1" process-phoenix = "3.0.0" room = "2.8.4" runtime-tracing = "1.11.2"