Skip to content

refactor: convert PackageLister from Java to Kotlin#27

Merged
gmegidish merged 2 commits into
mainfrom
convert-packagelister-kotlin
Jun 16, 2026
Merged

refactor: convert PackageLister from Java to Kotlin#27
gmegidish merged 2 commits into
mainfrom
convert-packagelister-kotlin

Conversation

@gmegidish

@gmegidish gmegidish commented Jun 16, 2026

Copy link
Copy Markdown
Member

Summary

Converts the project's lone Java file, PackageLister.java, to Kotlin so the codebase is fully Kotlin (was 8 Kotlin files + 1 Java).

Key detail: the app_process entry point is preserved

PackageLister is a standalone app_process tool, not an app component:

adb shell CLASSPATH=/path/to/classes.dex app_process / com.mobilenext.devicekit.PackageLister

A top-level Kotlin fun main would normally compile to a class named PackageListerKt, which would change that invocation. To avoid that, the file uses:

@file:JvmName("PackageLister")

so the generated class stays com.mobilenext.devicekit.PackageLister and the command line is unchanged.

Behavior

Identical to the Java version — reflection into ServiceManager / IPackageManager, same JSON output ({packageName, appName, version}).

Verification

  • ./gradlew assembleDebug succeeds.
  • Confirmed the dex still exposes Lcom/mobilenext/devicekit/PackageLister; with main([Ljava/lang/String;)V.

Test plan

  • Run app_process / com.mobilenext.devicekit.PackageLister on a device and confirm JSON output matches the previous build.

Summary by cubic

Converted PackageLister to Kotlin and preserved the app_process entry point. Removed a stray DisplayUtils.kt1 file. No behavior or JSON output changes.

  • Refactors
    • Rewrote PackageLister.java as PackageLister.kt and added @file:JvmName("PackageLister") to keep com.mobilenext.devicekit.PackageLister.
    • Deleted accidental DisplayUtils.kt1.

Written for commit bf19fcf. Summary will update on new commits.

Review in cubic

Makes the codebase fully Kotlin (was 8 Kotlin files + 1 Java). The
@file:JvmName("PackageLister") annotation preserves the generated class
name, so the app_process invocation is unchanged:

  app_process / com.mobilenext.devicekit.PackageLister

Behavior is identical: reflection into ServiceManager/IPackageManager,
same JSON output. Verified the dex still exposes
Lcom/mobilenext/devicekit/PackageLister; with main([Ljava/lang/String;)V.
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Walkthrough

The PR introduces a new DisplayUtils Kotlin object with a DisplayInfo data class and two reflection-based functions: getDisplayInfo() queries Android's hidden IDisplayManager for primary display metrics and returns fallback values on failure, while createVirtualDisplay() invokes IDisplayManager.createVirtualDisplay and returns null on failure. Separately, the existing PackageLister.java is deleted and replaced by PackageLister.kt, a Kotlin entry point with @file:JvmName("PackageLister") that uses reflection against ServiceManager/IPackageManager to enumerate installed packages and emit a JSON array to stdout.

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: converting PackageLister from Java to Kotlin, which is the primary objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description clearly describes converting PackageLister.java to Kotlin while preserving the app_process entry point using @file:JvmName annotation.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch convert-packagelister-kotlin

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/src/main/java/com/mobilenext/devicekit/DisplayUtils.kt1 (1)

40-41: ⚡ Quick win

Use a primitive parameter token for reflective getDisplayInfo(int) lookup.

On Line 40, use Int::class.javaPrimitiveType for deterministic method resolution of getDisplayInfo(int). Using Int::class.java here is ambiguous and can fail lookup depending on JVM mapping assumptions, forcing fallback defaults.

Suggested patch
-            val getDisplayInfoMethod = displayManager.javaClass.getMethod("getDisplayInfo", Int::class.java)
+            val getDisplayInfoMethod = displayManager.javaClass.getMethod(
+                "getDisplayInfo",
+                Int::class.javaPrimitiveType
+            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/mobilenext/devicekit/DisplayUtils.kt1` around lines 40
- 41, The reflective method lookup for getDisplayInfo on line 40 uses
Int::class.java as the parameter type, which is ambiguous and can fail depending
on JVM mapping assumptions. Replace Int::class.java with
Int::class.javaPrimitiveType in the getMethod call to ensure deterministic
resolution of the primitive int parameter type and avoid fallback defaults.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/com/mobilenext/devicekit/DisplayUtils.kt1`:
- Around line 92-110: The issue is that the reflection code is attempting to
invoke createVirtualDisplay on the hidden IDisplayManager interface, which has
an incompatible signature (returns int instead of VirtualDisplay) and will cause
a casting exception at line 109 that propagates to process exit. Replace the
reflection-based approach (lines 92-109) with a direct call to the public
DisplayManager.createVirtualDisplay method, which is the proper public API that
returns a VirtualDisplay object and handles API compatibility correctly. This
should be done by using the displayManager object directly (obtained from
context) rather than reflecting on its hidden interface layer.

In `@app/src/main/java/com/mobilenext/devicekit/PackageLister.kt`:
- Around line 61-70: The AssetManager instance created on line 61 via
getDeclaredConstructor().newInstance() is never closed, causing native resource
leaks within the loop. Add a finally block after the try-catch statement that
closes the assets object, or refactor the code to use Kotlin's use extension
function to ensure the AssetManager is properly closed after the label lookup is
complete. The AssetManager should be closed immediately after the Resources
object finishes retrieving the getString call, whether the operation succeeds or
fails.

---

Nitpick comments:
In `@app/src/main/java/com/mobilenext/devicekit/DisplayUtils.kt1`:
- Around line 40-41: The reflective method lookup for getDisplayInfo on line 40
uses Int::class.java as the parameter type, which is ambiguous and can fail
depending on JVM mapping assumptions. Replace Int::class.java with
Int::class.javaPrimitiveType in the getMethod call to ensure deterministic
resolution of the primitive int parameter type and avoid fallback defaults.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 607d6e6d-3adc-4170-8a4c-5ec2d52e9185

📥 Commits

Reviewing files that changed from the base of the PR and between 1d46386 and 92d867d.

📒 Files selected for processing (3)
  • app/src/main/java/com/mobilenext/devicekit/DisplayUtils.kt1
  • app/src/main/java/com/mobilenext/devicekit/PackageLister.java
  • app/src/main/java/com/mobilenext/devicekit/PackageLister.kt
💤 Files with no reviewable changes (1)
  • app/src/main/java/com/mobilenext/devicekit/PackageLister.java

Comment on lines +92 to +110
val createVirtualDisplayMethod = displayManager.javaClass
.getMethod(
"createVirtualDisplay",
String::class.java,
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
Surface::class.java
)

createVirtualDisplayMethod.invoke(
displayManager,
name,
width,
height,
dpi,
surface
) as VirtualDisplay

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

For Android AOSP (API 29 through latest), what are the exact method signatures and return types for android.hardware.display.IDisplayManager.createVirtualDisplay, and does any overload return android.hardware.display.VirtualDisplay directly?

💡 Result:

The android.hardware.display.IDisplayManager interface, which is defined in AOSP via AIDL, does not return a VirtualDisplay object. Instead, its createVirtualDisplay method returns an int [1][2]. The method signature for createVirtualDisplay within IDisplayManager.aidl has evolved across AOSP versions but consistently returns an integer representing a display ID or status, or is used to facilitate the creation process [1][3][4][2]. In the current AOSP master branch, the signature is defined as: int createVirtualDisplay(in VirtualDisplayConfig virtualDisplayConfig, in IVirtualDisplayCallback callback, in IMediaProjection projectionToken, String packageName); [1][2] The android.hardware.display.VirtualDisplay object itself is managed at a higher abstraction layer by the android.hardware.display.DisplayManager class in the Android framework, not by the low-level IDisplayManager interface [5][6][7]. Developers interact with DisplayManager, which provides public methods that return a VirtualDisplay instance, such as: public VirtualDisplay createVirtualDisplay(String name, int width, int height, int densityDpi, Surface surface, int flags) [6][8] No overload of the IDisplayManager.createVirtualDisplay method returns an android.hardware.display.VirtualDisplay directly [1][3][2]. The IDisplayManager interface serves as the binder interface to the system service, while the creation of the high-level VirtualDisplay wrapper is handled within the framework's DisplayManager implementation [5][7].

Citations:


🏁 Script executed:

cat -n app/src/main/java/com/mobilenext/devicekit/DisplayUtils.kt1 | sed -n '70,120p'

Repository: mobile-next/devicekit-android

Length of output: 2184


createVirtualDisplay is reflecting on the wrong API layer with incompatible signature and return type.

The code at lines 83–89 obtains an IDisplayManager interface proxy, then attempts to invoke createVirtualDisplay(String, int, int, int, Surface) on it (lines 92–109). According to AOSP, IDisplayManager.createVirtualDisplay returns an int (display ID), not VirtualDisplay, and the method signature differs significantly across API levels. The cast to VirtualDisplay (line 109) will fail, the exception is caught (lines 111–114), and null is returned—triggering immediate process exit in both MjpegServer and AvcServer.

To fix this, call DisplayManager.createVirtualDisplay directly via the public framework API instead of reflecting on the hidden IDisplayManager interface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/mobilenext/devicekit/DisplayUtils.kt1` around lines 92
- 110, The issue is that the reflection code is attempting to invoke
createVirtualDisplay on the hidden IDisplayManager interface, which has an
incompatible signature (returns int instead of VirtualDisplay) and will cause a
casting exception at line 109 that propagates to process exit. Replace the
reflection-based approach (lines 92-109) with a direct call to the public
DisplayManager.createVirtualDisplay method, which is the proper public API that
returns a VirtualDisplay object and handles API compatibility correctly. This
should be done by using the displayManager object directly (obtained from
context) rather than reflecting on its hidden interface layer.

Comment on lines +61 to +70
val assets = AssetManager::class.java.getDeclaredConstructor().newInstance()
val addAssetPath = AssetManager::class.java.getMethod("addAssetPath", String::class.java)
addAssetPath.invoke(assets, appInfo.sourceDir)
val res = Resources(assets, DisplayMetrics(), Configuration())
val label = res.getString(appInfo.labelRes)
if (label.isNotEmpty()) {
displayName = label
}
} catch (ignored: Exception) {
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify allocation site and absence/presence of explicit close in this file.
rg -n -C2 'AssetManager::class\.java|getDeclaredConstructor\(\)\.newInstance\(\)|assets\.close\(' app/src/main/java/com/mobilenext/devicekit/PackageLister.kt

Repository: mobile-next/devicekit-android

Length of output: 571


Close AssetManager after each package label lookup.

On line 61, a new AssetManager is created per package but never closed. In this loop, that leaks native resources and can destabilize the process.

Suggested fix
-                        val assets = AssetManager::class.java.getDeclaredConstructor().newInstance()
-                        val addAssetPath = AssetManager::class.java.getMethod("addAssetPath", String::class.java)
-                        addAssetPath.invoke(assets, appInfo.sourceDir)
-                        val res = Resources(assets, DisplayMetrics(), Configuration())
-                        val label = res.getString(appInfo.labelRes)
-                        if (label.isNotEmpty()) {
-                            displayName = label
-                        }
+                        val assets = AssetManager::class.java.getDeclaredConstructor().newInstance()
+                        try {
+                            val addAssetPath = AssetManager::class.java.getMethod("addAssetPath", String::class.java)
+                            addAssetPath.invoke(assets, appInfo.sourceDir)
+                            val res = Resources(assets, DisplayMetrics(), Configuration())
+                            val label = res.getString(appInfo.labelRes)
+                            if (label.isNotEmpty()) {
+                                displayName = label
+                            }
+                        } finally {
+                            assets.close()
+                        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/mobilenext/devicekit/PackageLister.kt` around lines 61
- 70, The AssetManager instance created on line 61 via
getDeclaredConstructor().newInstance() is never closed, causing native resource
leaks within the loop. Add a finally block after the try-catch statement that
closes the assets object, or refactor the code to use Kotlin's use extension
function to ensure the AssetManager is properly closed after the label lookup is
complete. The AssetManager should be closed immediately after the Resources
object finishes retrieving the getString call, whether the operation succeeds or
fails.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/com/mobilenext/devicekit/PackageLister.kt">

<violation number="1" location="app/src/main/java/com/mobilenext/devicekit/PackageLister.kt:61">
P2: The `AssetManager` instance created here is never closed after use. Since this runs in a loop over all installed packages, each iteration leaks native resources. Add a `try/finally` block (or `.use {}`) to close the `AssetManager` after the label lookup.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread app/src/main/java/com/mobilenext/devicekit/DisplayUtils.kt1 Outdated
Comment thread app/src/main/java/com/mobilenext/devicekit/DisplayUtils.kt1 Outdated
displayName = appInfo.nonLocalizedLabel.toString()
} else if (appInfo.labelRes != 0 && appInfo.sourceDir != null) {
try {
val assets = AssetManager::class.java.getDeclaredConstructor().newInstance()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The AssetManager instance created here is never closed after use. Since this runs in a loop over all installed packages, each iteration leaks native resources. Add a try/finally block (or .use {}) to close the AssetManager after the label lookup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/com/mobilenext/devicekit/PackageLister.kt, line 61:

<comment>The `AssetManager` instance created here is never closed after use. Since this runs in a loop over all installed packages, each iteration leaks native resources. Add a `try/finally` block (or `.use {}`) to close the `AssetManager` after the label lookup.</comment>

<file context>
@@ -0,0 +1,87 @@
+                    displayName = appInfo.nonLocalizedLabel.toString()
+                } else if (appInfo.labelRes != 0 && appInfo.sourceDir != null) {
+                    try {
+                        val assets = AssetManager::class.java.getDeclaredConstructor().newInstance()
+                        val addAssetPath = AssetManager::class.java.getMethod("addAssetPath", String::class.java)
+                        addAssetPath.invoke(assets, appInfo.sourceDir)
</file context>

@gmegidish gmegidish merged commit d7025e5 into main Jun 16, 2026
8 checks passed
@gmegidish gmegidish deleted the convert-packagelister-kotlin branch June 16, 2026 09:55
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.

1 participant