From bf5bd8c4174ddf482a2e7212edeaa1ec9258d5de Mon Sep 17 00:00:00 2001
From: callebtc <93376500+callebtc@users.noreply.github.com>
Date: Wed, 29 Jul 2026 18:15:50 +0200
Subject: [PATCH 1/2] fix: restore Android Studio sync with strict Gradle
security
---
app/gradle.lockfile | 6 +++---
build.gradle.kts | 26 ++++++++++++++++++++++++++
docs/reproducible-builds.md | 12 +++++++++++-
gradle/verification-metadata.xml | 5 +++++
wear/gradle.lockfile | 6 +++---
5 files changed, 48 insertions(+), 7 deletions(-)
diff --git a/app/gradle.lockfile b/app/gradle.lockfile
index 85d0b7c3..bd31d677 100644
--- a/app/gradle.lockfile
+++ b/app/gradle.lockfile
@@ -385,7 +385,7 @@ org.jetbrains.kotlin:kotlin-daemon-client:2.4.10=kotlinBuildToolsApiClasspath,ko
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease
org.jetbrains.kotlin:kotlin-parcelize-compiler:2.4.10=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease
-org.jetbrains.kotlin:kotlin-parcelize-runtime:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
+org.jetbrains.kotlin:kotlin-parcelize-runtime:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugRuntimeClasspathCopy,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseRuntimeClasspathCopy
org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease
org.jetbrains.kotlin:kotlin-reflect:1.8.21=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher
org.jetbrains.kotlin:kotlin-reflect:2.1.20=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
@@ -407,7 +407,7 @@ org.jetbrains.kotlin:kotlin-stdlib:1.9.0=unified-test-platform-android-driver-in
org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action
org.jetbrains.kotlin:kotlin-stdlib:2.2.21=composeMappingProducerClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath
-org.jetbrains.kotlin:kotlin-stdlib:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
+org.jetbrains.kotlin:kotlin-stdlib:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugRuntimeClasspathCopy,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseRuntimeClasspathCopy
org.jetbrains.kotlin:kotlin-tooling-core:2.4.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlinx:atomicfu-jvm:0.22.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher
org.jetbrains.kotlinx:atomicfu-jvm:0.28.0=debugAndroidTestLintChecksClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
@@ -429,7 +429,7 @@ org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0=debugAndroidTestCompileClas
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
-org.jetbrains:annotations:13.0=composeMappingProducerClasspath,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease
+org.jetbrains:annotations:13.0=composeMappingProducerClasspath,debugRuntimeClasspathCopy,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,releaseRuntimeClasspathCopy
org.jetbrains:annotations:23.0.0=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher
org.jspecify:jspecify:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jvnet.staxex:stax-ex:1.8.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle
diff --git a/build.gradle.kts b/build.gradle.kts
index 21951d74..16bf6428 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -7,11 +7,37 @@ plugins {
alias(libs.plugins.kotlin.compose) apply false
}
+val resolveIdeRuntimeClasspathCopyLocks = tasks.register("resolveIdeRuntimeClasspathCopyLocks") {
+ group = "build setup"
+ description = "Resolves Android Studio runtime classpath copies when refreshing lock state."
+}
+
subprojects {
dependencyLocking {
lockAllConfigurations()
lockMode.set(LockMode.STRICT)
}
+
+ pluginManager.withPlugin("com.android.application") {
+ val resolveIdeRuntimeClasspathCopyLock = tasks.register("resolveIdeRuntimeClasspathCopyLock") {
+ group = "build setup"
+ description = "Resolves this module's Android Studio runtime classpath copies."
+ notCompatibleWithConfigurationCache("Resolves copied configurations at execution time")
+ doFirst {
+ check(gradle.startParameter.isWriteDependencyLocks) {
+ "$path must be run with --write-locks"
+ }
+ }
+ doLast {
+ listOf("debugRuntimeClasspath", "releaseRuntimeClasspath").forEach { configurationName ->
+ configurations.getByName(configurationName).copy().resolve()
+ }
+ }
+ }
+ resolveIdeRuntimeClasspathCopyLocks.configure {
+ dependsOn(resolveIdeRuntimeClasspathCopyLock)
+ }
+ }
}
tasks.whenTaskAdded {
diff --git a/docs/reproducible-builds.md b/docs/reproducible-builds.md
index d0e921a8..3d94e8f1 100644
--- a/docs/reproducible-builds.md
+++ b/docs/reproducible-builds.md
@@ -224,11 +224,16 @@ Dependency changes must update and review both the lock state and verification
metadata:
```bash
-./gradlew testDebugUnitTest lintDebug \
+./gradlew testDebugUnitTest lintDebug resolveIdeRuntimeClasspathCopyLocks \
--write-locks \
--write-verification-metadata sha256
```
+`resolveIdeRuntimeClasspathCopyLocks` records the transient runtime classpath
+copies that Android Studio resolves during model import. Their selected versions
+are persisted under the generated copy configuration names while the canonical
+debug and release runtime classpaths remain strictly locked.
+
Generate release lock entries in separate invocations because split APK and AAB
intermediates cannot coexist:
@@ -245,6 +250,11 @@ Review every new repository, component, artifact name, version, and checksum.
Do not accept verification metadata generated after an unexplained checksum
failure.
+The verification metadata deliberately trusts only IDE documentation and source
+attachments (`*-javadoc.jar`, `*-sources.jar`, and Gradle's `*-src.zip`). Android
+Studio resolves these outside the build dependency graph, and they are not build
+inputs. Compiled artifacts and dependency metadata remain checksum-verified.
+
When changing Gradle, update the wrapper and independently verify the new
distribution SHA-256. When changing JDK or Android tools, update the exact
version, archive checksum, and base-image digest together. Native updates follow
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 709b9b56..a62cbff6 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -3,6 +3,11 @@
true
false
+
+
+
+
+
diff --git a/wear/gradle.lockfile b/wear/gradle.lockfile
index 82bd5091..2f0a7566 100644
--- a/wear/gradle.lockfile
+++ b/wear/gradle.lockfile
@@ -339,7 +339,7 @@ org.jetbrains.kotlin:kotlin-daemon-client:2.4.10=kotlinBuildToolsApiClasspath,ko
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease
org.jetbrains.kotlin:kotlin-parcelize-compiler:2.4.10=kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease
-org.jetbrains.kotlin:kotlin-parcelize-runtime:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
+org.jetbrains.kotlin:kotlin-parcelize-runtime:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugRuntimeClasspathCopy,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseRuntimeClasspathCopy
org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease
org.jetbrains.kotlin:kotlin-reflect:1.8.21=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher
org.jetbrains.kotlin:kotlin-reflect:2.1.20=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
@@ -359,7 +359,7 @@ org.jetbrains.kotlin:kotlin-stdlib:1.9.0=unified-test-platform-android-driver-in
org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-gradle-work-action
org.jetbrains.kotlin:kotlin-stdlib:2.2.21=composeMappingProducerClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath
-org.jetbrains.kotlin:kotlin-stdlib:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
+org.jetbrains.kotlin:kotlin-stdlib:2.4.10=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugRuntimeClasspathCopy,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseRuntimeClasspathCopy
org.jetbrains.kotlin:kotlin-tooling-core:2.4.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlinx:atomicfu-jvm:0.22.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher
org.jetbrains.kotlinx:atomicfu:0.22.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher
@@ -379,7 +379,7 @@ org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0=debugUnitTestCompileClasspa
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
-org.jetbrains:annotations:13.0=composeMappingProducerClasspath,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease
+org.jetbrains:annotations:13.0=composeMappingProducerClasspath,debugRuntimeClasspathCopy,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,releaseRuntimeClasspathCopy
org.jetbrains:annotations:23.0.0=androidLintTool,debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher
org.jspecify:jspecify:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jvnet.staxex:stax-ex:1.8.1=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle
From 4fdc9babbe01098b5ab01f0e2b8539bff02c6a36 Mon Sep 17 00:00:00 2001
From: callebtc <93376500+callebtc@users.noreply.github.com>
Date: Wed, 29 Jul 2026 19:11:06 +0200
Subject: [PATCH 2/2] feat: unify people lists with conversation-row avatar
design
- Extract shared PeerAvatar (initial circle + lower-right transport badge)
from the conversation row and use it in the mesh peer list and the
geohash/Nostr people list
- Remove the favorite toggle button from the peer list; favorite state is
now a small star badge on the avatar (filled = we favorited, outline =
they favorited us), so favoriting only happens from the private chat
- Show the unread-count badge on peer rows, matching conversation rows
---
.../bitchat/android/ui/GeohashPeopleList.kt | 41 ++--
.../bitchat/android/ui/MeshPeerListSheet.kt | 222 +++++++-----------
.../java/com/bitchat/android/ui/PeerAvatar.kt | 105 +++++++++
app/src/main/res/values/strings.xml | 2 +
4 files changed, 213 insertions(+), 157 deletions(-)
create mode 100644 app/src/main/java/com/bitchat/android/ui/PeerAvatar.kt
diff --git a/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt b/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt
index d784610a..0c0e583f 100644
--- a/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt
+++ b/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt
@@ -281,6 +281,7 @@ private fun GeohashPersonItem(
onTap: () -> Unit
) {
val palette = LocalBitchatPalette.current
+ val colorScheme = MaterialTheme.colorScheme
val statusIconRes =
if (isTeleported) R.drawable.ic_spec_teleport
@@ -298,25 +299,14 @@ private fun GeohashPersonItem(
Row(
modifier = Modifier
.fillMaxWidth()
- // Exact height, not padding: a row that sizes to its content makes the card change
- // height whenever the list reorders.
- .height(SheetRowHeight)
.clickable(onClick = onTap)
- .padding(horizontal = SheetRowHorizontal),
+ .padding(horizontal = SheetRowHorizontal, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
- Box(
- modifier = Modifier.size(SheetRowLeadingSlot),
- contentAlignment = Alignment.Center
- ) {
- if (hasUnreadDM) {
- Icon(
- imageVector = Icons.Filled.Email,
- contentDescription = stringResource(R.string.cd_unread_message),
- modifier = Modifier.size(22.dp),
- tint = palette.accentOrange
- )
- } else {
+ PeerAvatar(
+ name = baseNameRaw,
+ color = baseColor,
+ badge = {
Icon(
painter = painterResource(statusIconRes),
contentDescription = if (isTeleported) {
@@ -324,13 +314,13 @@ private fun GeohashPersonItem(
} else {
stringResource(R.string.section_on_location)
},
- modifier = Modifier.size(22.dp),
- tint = baseColor
+ modifier = Modifier.size(13.dp),
+ tint = if (isTeleported) palette.accentPurple else colorScheme.primary
)
}
- }
+ )
- Spacer(modifier = Modifier.width(SheetRowLeadingGutter))
+ Spacer(modifier = Modifier.width(12.dp))
Row(
modifier = Modifier.weight(1f),
@@ -365,5 +355,16 @@ private fun GeohashPersonItem(
)
}
}
+
+ if (hasUnreadDM) {
+ Icon(
+ imageVector = Icons.Filled.Email,
+ contentDescription = stringResource(R.string.cd_unread_message),
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .size(18.dp),
+ tint = palette.accentOrange
+ )
+ }
}
}
diff --git a/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt b/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt
index 6f7b8969..89746a56 100644
--- a/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt
+++ b/app/src/main/java/com/bitchat/android/ui/MeshPeerListSheet.kt
@@ -831,18 +831,14 @@ fun PeopleSection(
displayName = displayName,
isDirect = isDirectLive,
isWifiAware = peerID in wifiAwarePeerIDs,
+ isConnected = true,
isSelected = conversationID == selectedPrivatePeer || peerID == selectedPrivatePeer,
isFavorite = isFavorite,
theyFavoritedUs = theyFavoritedUs,
isVerified = isVerified,
- hasUnreadDM = combinedHasUnread,
colorScheme = colorScheme,
viewModel = viewModel,
onItemClick = { onPrivateChatStart(peerID) },
- onToggleFavorite = {
- Log.d("SidebarComponents", "Sidebar toggle favorite: peerID=$peerID, currentFavorite=$isFavorite")
- viewModel.toggleFavorite(peerID)
- },
unreadCount = if (combinedUnreadCount > 0) combinedUnreadCount else if (combinedHasUnread) 1 else 0,
showNostrGlobe = false,
showHashSuffix = showHash
@@ -881,18 +877,14 @@ fun PeopleSection(
peerID = favPeerID,
displayName = dn,
isDirect = false,
+ isConnected = false,
isSelected = conversationID == selectedPrivatePeer || (mappedConnectedPeerID ?: favPeerID) == selectedPrivatePeer,
isFavorite = true,
theyFavoritedUs = fav.theyFavoritedUs,
isVerified = isVerified,
- hasUnreadDM = hasUnread,
colorScheme = colorScheme,
viewModel = viewModel,
onItemClick = { onPrivateChatStart(mappedConnectedPeerID ?: favPeerID) },
- onToggleFavorite = {
- Log.d("SidebarComponents", "Sidebar toggle favorite (offline): peerID=$favPeerID")
- viewModel.toggleFavorite(favPeerID)
- },
unreadCount = if (unreadCount > 0) unreadCount else if (hasUnread) 1 else 0,
showNostrGlobe = (fav.isMutual && fav.peerNostrPublicKey != null),
showHashSuffix = showHash
@@ -1263,72 +1255,42 @@ private fun ConversationRow(
),
verticalAlignment = Alignment.CenterVertically
) {
- Box(
- modifier = Modifier.size(42.dp),
- contentAlignment = Alignment.Center,
- ) {
- Box(
- modifier = Modifier
- .size(38.dp)
- .background(assignedColor.copy(alpha = 0.16f), CircleShape),
- contentAlignment = Alignment.Center
- ) {
- Text(
- text = baseNameRaw
- .trim()
- .firstOrNull()
- ?.uppercase()
- ?: "#",
- style = MaterialTheme.typography.titleMedium.copy(
- fontFamily = BitchatFontFamily,
- fontWeight = FontWeight.SemiBold
- ),
- color = assignedColor
- )
- }
+ PeerAvatar(
+ name = baseNameRaw,
+ color = assignedColor,
+ badge = {
+ when {
+ conversation.isConnected -> Icon(
+ painter = painterResource(
+ conversationTransportIcon(
+ isReachedOverInternet = false,
+ isWifiAware = isWifiAware,
+ isDirect = isDirect
+ )
+ ),
+ contentDescription = connectionDescription,
+ modifier = Modifier.size(13.dp),
+ tint = colorScheme.primary
+ )
- Surface(
- modifier = Modifier
- .size(18.dp)
- .align(Alignment.BottomEnd),
- shape = CircleShape,
- color = colorScheme.surface,
- tonalElevation = 1.dp
- ) {
- Box(contentAlignment = Alignment.Center) {
- when {
- conversation.isConnected -> Icon(
- painter = painterResource(
- conversationTransportIcon(
- isReachedOverInternet = false,
- isWifiAware = isWifiAware,
- isDirect = isDirect
- )
- ),
- contentDescription = connectionDescription,
- modifier = Modifier.size(13.dp),
- tint = colorScheme.primary
- )
+ conversation.transport == DirectMessageTransport.NOSTR -> Icon(
+ painter = painterResource(R.drawable.ic_spec_globe),
+ contentDescription = stringResource(
+ R.string.offline_reachable_via_nostr
+ ),
+ modifier = Modifier.size(13.dp),
+ tint = palette.accentPurple
+ )
- conversation.transport == DirectMessageTransport.NOSTR -> Icon(
- painter = painterResource(R.drawable.ic_spec_globe),
- contentDescription = stringResource(
- R.string.offline_reachable_via_nostr
- ),
- modifier = Modifier.size(13.dp),
- tint = palette.accentPurple
- )
-
- else -> Icon(
- imageVector = Icons.Outlined.Circle,
- contentDescription = stringResource(R.string.offline_not_in_mesh),
- modifier = Modifier.size(11.dp),
- tint = palette.textTertiary
- )
- }
+ else -> Icon(
+ imageVector = Icons.Outlined.Circle,
+ contentDescription = stringResource(R.string.offline_not_in_mesh),
+ modifier = Modifier.size(11.dp),
+ tint = palette.textTertiary
+ )
}
}
- }
+ )
Spacer(modifier = Modifier.width(12.dp))
@@ -1559,15 +1521,14 @@ private fun PeerItem(
displayName: String,
isDirect: Boolean,
isWifiAware: Boolean = false,
+ isConnected: Boolean = true,
isSelected: Boolean,
isFavorite: Boolean,
theyFavoritedUs: Boolean = false,
isVerified: Boolean,
- hasUnreadDM: Boolean,
colorScheme: ColorScheme,
viewModel: ChatViewModel,
onItemClick: () -> Unit,
- onToggleFavorite: () -> Unit,
unreadCount: Int = 0,
showNostrGlobe: Boolean = false,
showHashSuffix: Boolean = true
@@ -1595,57 +1556,47 @@ private fun PeerItem(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onItemClick)
- .padding(horizontal = SheetRowHorizontal, vertical = SheetRowVertical),
+ .padding(horizontal = SheetRowHorizontal, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
- Box(
- modifier = Modifier.size(SheetRowLeadingSlot),
- contentAlignment = Alignment.Center
- ) {
- if (isSelected) {
- Box(
- modifier = Modifier
- .size(SheetRowSelectedDot)
- .background(colorScheme.primary, CircleShape)
- )
- } else if (hasUnreadDM) {
- Icon(
- painter = painterResource(R.drawable.ic_spec_envelope),
- contentDescription = stringResource(R.string.cd_unread_message),
- modifier = Modifier.size(PeerRowIconSize),
- tint = palette.accentOrange
- )
- } else if (showNostrGlobe) {
- Icon(
- painter = painterResource(R.drawable.ic_spec_globe),
- contentDescription = stringResource(R.string.cd_reachable_via_nostr),
- modifier = Modifier.size(PeerRowIconSize),
- tint = palette.accentPurple
- )
- } else if (!isDirect && isFavorite) {
- Icon(
- imageVector = Icons.Outlined.Circle,
- contentDescription = stringResource(R.string.cd_offline_favorite),
- modifier = Modifier.size(PeerRowIconSize),
- tint = palette.textTertiary
- )
- } else {
- Icon(
- painter = painterResource(
- conversationTransportIcon(
- isReachedOverInternet = false,
- isWifiAware = isWifiAware,
- isDirect = isDirect
- )
- ),
- contentDescription = connectionDescription,
- modifier = Modifier.size(PeerRowIconSize),
- tint = colorScheme.onSurfaceVariant
- )
- }
- }
+ PeerAvatar(
+ name = baseNameRaw,
+ color = baseColor,
+ isFavorite = isFavorite,
+ theyFavoritedUs = theyFavoritedUs,
+ badge = {
+ when {
+ isConnected -> Icon(
+ painter = painterResource(
+ conversationTransportIcon(
+ isReachedOverInternet = false,
+ isWifiAware = isWifiAware,
+ isDirect = isDirect
+ )
+ ),
+ contentDescription = connectionDescription,
+ modifier = Modifier.size(13.dp),
+ tint = colorScheme.primary
+ )
- Spacer(modifier = Modifier.width(SheetRowLeadingGutter))
+ showNostrGlobe -> Icon(
+ painter = painterResource(R.drawable.ic_spec_globe),
+ contentDescription = stringResource(R.string.cd_reachable_via_nostr),
+ modifier = Modifier.size(13.dp),
+ tint = palette.accentPurple
+ )
+
+ else -> Icon(
+ imageVector = Icons.Outlined.Circle,
+ contentDescription = stringResource(R.string.cd_offline_favorite),
+ modifier = Modifier.size(11.dp),
+ tint = palette.textTertiary
+ )
+ }
+ }
+ )
+
+ Spacer(modifier = Modifier.width(12.dp))
Row(
modifier = Modifier.weight(1f),
@@ -1682,21 +1633,18 @@ private fun PeerItem(
}
}
- Box(
- modifier = Modifier
- .size(36.dp)
- .clickable(onClick = onToggleFavorite),
- contentAlignment = Alignment.Center
- ) {
- // Three-state star (matches private-chat header): grey outline (no relation),
- // orange outline (they favorited us), filled orange (we favorited them).
- Icon(
- painter = painterResource(
- if (isFavorite) R.drawable.ic_spec_star_filled else R.drawable.ic_spec_star
- ),
- contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
- modifier = Modifier.size(PeerRowIconSize),
- tint = if (isFavorite || theyFavoritedUs) palette.accentOrange else palette.textTertiary
+ UnreadBadge(
+ count = unreadCount,
+ colorScheme = colorScheme,
+ modifier = Modifier.padding(start = 4.dp)
+ )
+
+ if (isSelected) {
+ Box(
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .size(SheetRowSelectedDot)
+ .background(colorScheme.primary, CircleShape)
)
}
}
diff --git a/app/src/main/java/com/bitchat/android/ui/PeerAvatar.kt b/app/src/main/java/com/bitchat/android/ui/PeerAvatar.kt
new file mode 100644
index 00000000..fcdf4146
--- /dev/null
+++ b/app/src/main/java/com/bitchat/android/ui/PeerAvatar.kt
@@ -0,0 +1,105 @@
+package com.bitchat.android.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.bitchat.android.R
+import com.bitchat.android.ui.theme.BitchatFontFamily
+import com.bitchat.android.ui.theme.LocalBitchatPalette
+
+internal val PeerAvatarBadgeSize = 18.dp
+private val PeerAvatarStarSize = 16.dp
+
+@Composable
+internal fun PeerAvatar(
+ name: String,
+ color: Color,
+ modifier: Modifier = Modifier,
+ isFavorite: Boolean = false,
+ theyFavoritedUs: Boolean = false,
+ badge: (@Composable () -> Unit)? = null
+) {
+ val palette = LocalBitchatPalette.current
+ val colorScheme = MaterialTheme.colorScheme
+
+ Box(
+ modifier = modifier.size(42.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(
+ modifier = Modifier
+ .size(38.dp)
+ .background(color.copy(alpha = 0.16f), CircleShape),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = name.trim().firstOrNull()?.uppercase() ?: "#",
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontFamily = BitchatFontFamily,
+ fontWeight = FontWeight.SemiBold
+ ),
+ color = color
+ )
+ }
+
+ if (badge != null) {
+ Surface(
+ modifier = Modifier
+ .size(PeerAvatarBadgeSize)
+ .align(Alignment.BottomEnd),
+ shape = CircleShape,
+ color = colorScheme.surface,
+ tonalElevation = 1.dp
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ badge()
+ }
+ }
+ }
+
+ if (isFavorite || theyFavoritedUs) {
+ Surface(
+ modifier = Modifier
+ .size(PeerAvatarStarSize)
+ .align(Alignment.TopEnd),
+ shape = CircleShape,
+ color = colorScheme.surface,
+ tonalElevation = 1.dp
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ Icon(
+ painter = painterResource(
+ if (isFavorite) {
+ R.drawable.ic_spec_star_filled
+ } else {
+ R.drawable.ic_spec_star
+ }
+ ),
+ contentDescription = stringResource(
+ if (isFavorite) {
+ R.string.cd_favorite
+ } else {
+ R.string.cd_favorited_you
+ }
+ ),
+ modifier = Modifier.size(10.dp),
+ tint = palette.accentOrange
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index eeb01c75..236ba20c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -69,6 +69,8 @@
Add to favorites
Remove from favorites
+ Favorite
+ Favorited you
Add bookmark