Skip to content

ADFA-2756: add LeakCanary#978

Open
itsaky-adfa wants to merge 2 commits intostagefrom
fix/ADFA-2756
Open

ADFA-2756: add LeakCanary#978
itsaky-adfa wants to merge 2 commits intostagefrom
fix/ADFA-2756

Conversation

@itsaky-adfa
Copy link
Contributor

@itsaky-adfa itsaky-adfa commented Feb 13, 2026

See ADFA-2756 for more details.

Signed-off-by: Akash Yadav <akashyadav@appdevforall.org>
@itsaky-adfa itsaky-adfa requested review from a team and Daniel-ADFA February 13, 2026 14:12
@itsaky-adfa itsaky-adfa self-assigned this Feb 13, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

📝 Walkthrough
  • Changes

    • Adds LeakCanary dependency:
      • gradle/libs.versions.toml: adds version leakcanary = "2.14" and library alias common-leakcanary -> com.squareup.leakcanary:leakcanary-android.
      • app/build.gradle.kts: adds debugImplementation(libs.common.leakcanary).
    • Significant refactor in app/build.gradle.kts:
      • Rewrote registerD8Task to compute D8 executable path from android.sdkDirectory/build-tools and invoke d8 with explicit --output and input arguments.
      • Major rewrite of createAssetsZip: streaming AAR/jar explosion, per-dependency D8 invocation, streaming repackaging of AARs into assets zip, added checksum/download helpers and CDN/fallback logic.
      • Formatting, spacing, and minor Kotlin safety edits across many build task helpers (registerBundleLlamaAssetsTask, assembleV7/V8/Assets, recompressApk, signing/manifest wiring).
      • Added MessageDigest import and other small import/grouping changes.
    • Asset handling and download improvements:
      • Introduces staged download/checksum steps, streaming downloads, and retry/error handling for assets.
      • Changes to how llama AAR and plugin artifacts are packaged into assets zips.
    • No detected changes to public API signatures (no exported functions/types removed or altered).
  • Risks & violations of best practices

    • Documentation mismatch risk: PR title/description claims "add LeakCanary" — which is correct per changes — ensure commit messages and PR body match intent; previous summary that claimed removal was incorrect and could cause confusion.
    • Large mixed-change PR: combining dependency addition, significant build system refactors, and formatting/style changes in one PR increases review complexity and risk; recommend splitting into separate PRs (dependency, D8/task refactor, assets/download flow).
    • Build reproducibility / environment risk: D8 executable path now relies on android.sdkDirectory and build-tools layout; ensure CI/dev machines have matching SDK/build-tools versions and that d8 exists at the expected path (throws FileNotFoundException otherwise).
    • Windows path/exec behavior: registerD8Task uses d8 or d8.bat selection; validate command-line invocation behaves correctly on CI Windows runners and that quoting/escaping is safe.
    • Backwards-compatibility of asset packaging: new streaming repackaging and per-dependency D8/dexing may change produced asset contents (e.g., classes.dex handling) — validate resulting assets on all targeted flavors/architectures and in release builds.
    • Testing & rollout: large build-script changes can break local builds or CI. Recommend running a full CI build (debug/release, all flavors/arches) and smoke-testing app flows that rely on packaged assets.
    • Missing rationale/comments: several non-trivial refactors (D8 rewrite, streaming packaging, CDN fallback) lack inline justification — add comments or PR description notes to help future maintainers understand motivations and failure modes.

Walkthrough

Refactors Gradle build scripts: rewrites D8 task invocation and asset packaging to use explicit paths, streaming AAR repackaging, and checksum-backed CDN/fallback downloads; reflows Gradle DSL formatting and adds LeakCanary dependency. No public API signature changes.

Changes

Cohort / File(s) Summary
D8 task & asset packaging
app/build.gradle.kts
Rewrote registerD8Task to compute absolute SDK/build-tools paths and validate the D8 executable; replaced in-memory AAR rewriting with streaming repackaging in createAssetsZip; builds runtime classpath by exploding AARs/jars and invokes D8 explicitly for dexing. Added streaming asset download/checksum helpers and CDN/fallback logic.
Asset bundling & plugin artifacts
app/build.gradle.kts
Refactored tasks: registerBundleLlamaAssetsTask, copyPluginApiJarToAssets, createPluginArtifactsZip, assembleV8Assets, assembleV7Assets, assembleAssets — style and flow cleaned, preserved outputs and task signatures.
APK recompression & packaging settings
app/build.gradle.kts
Adjusted recompressApk helpers and noCompress handling with safer Kotlin syntax and multi-line literals; minor packaging/resource DSL formatting changes.
Lint/config wiring & misc formatting
app/build.gradle.kts
Modernized afterEvaluate wiring for lint/task dependencies, various formatting/indentation and non-functional DSL tweaks.
Dependency versions
gradle/libs.versions.toml
Added leakcanary = "2.14" and common-leakcanary library mapping.

Sequence Diagram

sequenceDiagram
    participant GradleTask as Gradle Task\n(createAssetsZip)
    participant FileSystem as File System
    participant CDN as CDN/HTTP
    participant D8 as D8 Tool
    participant ZipOutput as ZIP Output

    GradleTask->>FileSystem: normalize buildDirectory & create temp dirs
    GradleTask->>CDN: download required AAR/JAR assets (with checksum)
    CDN-->>GradleTask: asset streams / fallbacks
    GradleTask->>FileSystem: explode AARs & collect classpath jars
    GradleTask->>D8: invoke with explicit --output and per-dependency inputs
    D8-->>GradleTask: produce classes.dex
    GradleTask->>ZipOutput: stream original AAR contents
    GradleTask->>ZipOutput: insert/replace classes.dex in nested AAR location
    ZipOutput-->>FileSystem: write final assets-*.zip
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • hal-eisen-adfa
  • jomen-adfa

Poem

🐰 I hopped through build scripts late at night,
D8 glowing, streams stitched tight,
AARs unpacked, checksums sing,
LeakCanary brings a debugging spring,
Zip sealed, the rabbit hops in delight. 🎋

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'ADFA-2756: add LeakCanary' accurately describes the main change in the changeset, which adds LeakCanary as a new dependency.
Description check ✅ Passed The description references the associated Jira ticket ADFA-2756 which provides context for the changes, demonstrating relevance to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/ADFA-2756

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/build.gradle.kts`:
- Around line 542-558: The inner ZipOutputStream created as
ZipOutputStream(zipOut).use { aarZipOut -> ... } will call close() which closes
the underlying zipOut and corrupts the outer ZIP; change the inner handling so
it does not close the underlying stream — e.g., stop using .use on the inner
ZipOutputStream and after writing entries call aarZipOut.finish() (not close())
before returning control, ensuring you still close the outer ZipOutputStream
only once; update the block that writes entries from originalLlamaAarFile and
the classes.dex write (references: ZipOutputStream, aarZipOut,
originalLlamaAarFile, dexOutputFile) accordingly.
🧹 Nitpick comments (3)
app/build.gradle.kts (3)

1148-1177: Redundant return on line 1165.

The outer if/else on line 1149 already returns its value. The inner return try { … } on line 1165 is redundant — it works, but adds confusion. Remove return and let the try expression be the else branch value.

Proposed fix
 	} else {
 		val checksumUrl = asset.url + ".md5"
 		val conn = URL(checksumUrl).openConnection() as HttpURLConnection
 		conn.requestMethod = "GET"
 		conn.setRequestProperty("User-Agent", "Mozilla/5.0")
 		conn.instanceFollowRedirects = true
 		conn.connectTimeout = 10_000
 		conn.readTimeout = 10_000
 
-		return try {
+		try {
 			val status = conn.responseCode
 
 			if (status == HttpURLConnection.HTTP_OK) {
 				conn.inputStream.bufferedReader().use { it.readText().trim() }
 			} else {
 				throw GradleException("Failed to fetch checksum from $checksumUrl (HTTP $status: ${conn.responseMessage})")
 			}
 		} finally {
 			conn.disconnect()
 		}
 	}

1075-1082: SCP command has no timeout — could hang indefinitely in CI.

If the SCP server is unreachable or slow, this exec call will block the CI pipeline with no way to time out gracefully. Consider adding a timeout via scp -o ConnectTimeout=30 or wrapping the exec with a Gradle timeout.

Proposed fix
 		project.exec {
 			commandLine(
 				"scp",
+				"-o", "ConnectTimeout=30",
 				"-r",
 				"$scpServer:public_html/dev-assets/$variant/",
 				tmpDir.absolutePath,
 			)
 		}

1179-1230: Download writes directly to target path — partial file risk on failure.

If the HTTP download in assetsFileDownload fails mid-stream (network error, timeout), the target file is left in a partial/corrupt state. While the checksum validation will catch this and throw, the corrupt file persists on disk and could cause confusion.

Consider downloading to a temp file first, then atomically moving it to the target path after checksum validation passes.

Comment on lines +542 to +558
ZipOutputStream(zipOut).use { aarZipOut ->
// Copy all files from the original AAR *except* classes.jar
ZipInputStream(originalLlamaAarFile.inputStream()).use { originalAarStream ->
var entry = originalAarStream.nextEntry
while (entry != null) {
if (entry.name != "classes.jar") {
aarZipOut.putNextEntry(ZipEntry(entry.name))
originalAarStream.copyTo(aarZipOut)
aarZipOut.closeEntry()
}
entry = originalAarStream.nextEntry
}
}
aarZipOut.putNextEntry(ZipEntry("classes.dex"))
dexOutputFile.inputStream().use { dexInput -> dexInput.copyTo(aarZipOut) }
aarZipOut.closeEntry()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: nested ZipOutputStream.use will close the outer zip stream prematurely.

ZipOutputStream(zipOut).use { aarZipOut -> ... } — when use calls aarZipOut.close(), it propagates through DeflaterOutputStream.close()out.close(), which closes zipOut. This means the outer ZipOutputStream can never write its central directory, resulting in a corrupt ZIP file.

Use finish() instead of close() for the inner stream to write the ZIP trailer without closing the underlying stream.

Proposed fix
-		ZipOutputStream(zipOut).use { aarZipOut ->
+		val aarZipOut = ZipOutputStream(zipOut)
+		try {
 			// Copy all files from the original AAR *except* classes.jar
 			ZipInputStream(originalLlamaAarFile.inputStream()).use { originalAarStream ->
 				var entry = originalAarStream.nextEntry
 				while (entry != null) {
 					if (entry.name != "classes.jar") {
 						aarZipOut.putNextEntry(ZipEntry(entry.name))
 						originalAarStream.copyTo(aarZipOut)
 						aarZipOut.closeEntry()
 					}
 					entry = originalAarStream.nextEntry
 				}
 			}
 			aarZipOut.putNextEntry(ZipEntry("classes.dex"))
 			dexOutputFile.inputStream().use { dexInput -> dexInput.copyTo(aarZipOut) }
 			aarZipOut.closeEntry()
+		} finally {
+			aarZipOut.finish() // writes ZIP central directory without closing zipOut
 		}
+		zipOut.closeEntry() // close the "dynamic_libs/llama.aar" entry
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ZipOutputStream(zipOut).use { aarZipOut ->
// Copy all files from the original AAR *except* classes.jar
ZipInputStream(originalLlamaAarFile.inputStream()).use { originalAarStream ->
var entry = originalAarStream.nextEntry
while (entry != null) {
if (entry.name != "classes.jar") {
aarZipOut.putNextEntry(ZipEntry(entry.name))
originalAarStream.copyTo(aarZipOut)
aarZipOut.closeEntry()
}
entry = originalAarStream.nextEntry
}
}
aarZipOut.putNextEntry(ZipEntry("classes.dex"))
dexOutputFile.inputStream().use { dexInput -> dexInput.copyTo(aarZipOut) }
aarZipOut.closeEntry()
}
val aarZipOut = ZipOutputStream(zipOut)
try {
// Copy all files from the original AAR *except* classes.jar
ZipInputStream(originalLlamaAarFile.inputStream()).use { originalAarStream ->
var entry = originalAarStream.nextEntry
while (entry != null) {
if (entry.name != "classes.jar") {
aarZipOut.putNextEntry(ZipEntry(entry.name))
originalAarStream.copyTo(aarZipOut)
aarZipOut.closeEntry()
}
entry = originalAarStream.nextEntry
}
}
aarZipOut.putNextEntry(ZipEntry("classes.dex"))
dexOutputFile.inputStream().use { dexInput -> dexInput.copyTo(aarZipOut) }
aarZipOut.closeEntry()
} finally {
aarZipOut.finish() // writes ZIP central directory without closing zipOut
}
zipOut.closeEntry() // close the "dynamic_libs/llama.aar" entry
🤖 Prompt for AI Agents
In `@app/build.gradle.kts` around lines 542 - 558, The inner ZipOutputStream
created as ZipOutputStream(zipOut).use { aarZipOut -> ... } will call close()
which closes the underlying zipOut and corrupts the outer ZIP; change the inner
handling so it does not close the underlying stream — e.g., stop using .use on
the inner ZipOutputStream and after writing entries call aarZipOut.finish() (not
close()) before returning control, ensuring you still close the outer
ZipOutputStream only once; update the block that writes entries from
originalLlamaAarFile and the classes.dex write (references: ZipOutputStream,
aarZipOut, originalLlamaAarFile, dexOutputFile) accordingly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/build.gradle.kts`:
- Line 483: The current call
d8Command.add(android.defaultConfig.minSdk.toString()) can pass the literal
"null" because android.defaultConfig.minSdk is Int?; update this to validate and
handle null explicitly: either requireNotNull(android.defaultConfig.minSdk) with
a clear error message to fail fast, or use the Elvis operator
(android.defaultConfig.minSdk ?: throw IllegalStateException("minSdk not set"))
and then add the integer's string value to d8Command; ensure you reference the
existing d8Command.add(...) call and replace the .toString() usage with the
null-checked value.
- Around line 1207-1213: The code inconsistently uses rootProject.projectDir
when calling assetsFileDownload but computes the checksum on target (which is
derived from the projectDir parameter), risking mismatched files; update the
assetsFileDownload call to use the same projectDir-derived File (i.e.,
File(projectDir, asset.localPath)) or otherwise ensure the checksum and download
both operate on the same File instance (refer to assetsFileDownload, projectDir,
rootProject.projectDir, and target/MessageDigest usage) so the downloaded file
and subsequent MD5 calculation reference the identical path.

---

Duplicate comments:
In `@app/build.gradle.kts`:
- Around line 542-559: The nested ZipOutputStream(zipOut).use { aarZipOut -> ...
} wrapper closes the underlying zipOut and corrupts the outer ZIP; replace the
.use block for the inner ZipOutputStream with manual resource management: create
aarZipOut = ZipOutputStream(zipOut) (do not use .use), run the existing copy
loop and entries, then call aarZipOut.finish() and aarZipOut.flush() in a
finally block without calling close() so zipOut remains open; reference the
ZipOutputStream instance named aarZipOut and the outer stream zipOut when making
this change.

d8Command.add(d8Executable.absolutePath)
d8Command.add("--release")
d8Command.add("--min-api")
d8Command.add(android.defaultConfig.minSdk.toString()) // Add minSdk for better desugaring
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

minSdk could be null, producing "null" as the --min-api value.

android.defaultConfig.minSdk returns Int?. If unset, .toString() yields the string "null", causing a confusing D8 failure. Consider adding a null check or using minSdk!! to fail fast with a clear message.

Proposed fix
-			d8Command.add(android.defaultConfig.minSdk.toString())
+			d8Command.add((android.defaultConfig.minSdk ?: error("minSdk is not set")).toString())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
d8Command.add(android.defaultConfig.minSdk.toString()) // Add minSdk for better desugaring
d8Command.add((android.defaultConfig.minSdk ?: error("minSdk is not set")).toString()) // Add minSdk for better desugaring
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/build.gradle.kts` at line 483, The current call
d8Command.add(android.defaultConfig.minSdk.toString()) can pass the literal
"null" because android.defaultConfig.minSdk is Int?; update this to validate and
handle null explicitly: either requireNotNull(android.defaultConfig.minSdk) with
a clear error message to fail fast, or use the Elvis operator
(android.defaultConfig.minSdk ?: throw IllegalStateException("minSdk not set"))
and then add the integer's string value to d8Command; ensure you reference the
existing d8Command.add(...) call and replace the .toString() usage with the
null-checked value.

Comment on lines +1207 to +1213
if (needsDownload) {
project.logger.lifecycle("Downloading ${asset.url} → ${asset.localPath}")

assetsFileDownload(asset, File(rootProject.projectDir, asset.localPath))
// Recompute checksum after download
val digest = MessageDigest.getInstance("MD5")
target.inputStream().use { input ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent use of projectDir parameter vs hardcoded rootProject.projectDir.

Line 1210 passes File(rootProject.projectDir, asset.localPath) as the download target, but the checksum at line 1213 is computed on target which is File(projectDir, asset.localPath). If projectDir ever differs from rootProject.projectDir, the checksum will be computed on a stale or missing file.

Use the projectDir parameter consistently:

Proposed fix
-			assetsFileDownload(asset, File(rootProject.projectDir, asset.localPath))
+			assetsFileDownload(asset, target)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/build.gradle.kts` around lines 1207 - 1213, The code inconsistently uses
rootProject.projectDir when calling assetsFileDownload but computes the checksum
on target (which is derived from the projectDir parameter), risking mismatched
files; update the assetsFileDownload call to use the same projectDir-derived
File (i.e., File(projectDir, asset.localPath)) or otherwise ensure the checksum
and download both operate on the same File instance (refer to
assetsFileDownload, projectDir, rootProject.projectDir, and target/MessageDigest
usage) so the downloaded file and subsequent MD5 calculation reference the
identical path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants