Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions app/src/main/java/com/mobilenext/devicekit/PackageLister.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.mobilenext.devicekit;

import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.IBinder;
import android.util.DisplayMetrics;

import org.json.JSONArray;
import org.json.JSONObject;

import java.lang.reflect.Method;
import java.util.List;

/**
* Standalone entry point for listing installed packages via app_process.
*
* Usage:
* adb shell CLASSPATH=/path/to/classes.dex app_process / com.mobilenext.devicekit.PackageLister
*
* Outputs a JSON array of {packageName, appName, version} to stdout.
*/
public class PackageLister {

public static void main(String[] args) {
try {
// Get the IPackageManager binder via ServiceManager (hidden API)
Class<?> serviceManager = Class.forName("android.os.ServiceManager");
Method getService = serviceManager.getMethod("getService", String.class);
IBinder binder = (IBinder) getService.invoke(null, "package");

// Get IPackageManager.Stub.asInterface(binder)
Class<?> ipmStub = Class.forName("android.content.pm.IPackageManager$Stub");
Method asInterface = ipmStub.getMethod("asInterface", IBinder.class);
Object ipm = asInterface.invoke(null, binder);

// Call getInstalledPackages(flags, userId)
Method getInstalledPackages = ipm.getClass().getMethod("getInstalledPackages", long.class, int.class);
Object parceledList = getInstalledPackages.invoke(ipm, 0L, 0);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 27, 2026

Choose a reason for hiding this comment

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

P2: Avoid hard-coding userId=0 when querying installed packages; it can return the wrong package set on multi-user devices.

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.java, line 41:

<comment>Avoid hard-coding userId=0 when querying installed packages; it can return the wrong package set on multi-user devices.</comment>

<file context>
@@ -0,0 +1,88 @@
+
+            // Call getInstalledPackages(flags, userId)
+            Method getInstalledPackages = ipm.getClass().getMethod("getInstalledPackages", long.class, int.class);
+            Object parceledList = getInstalledPackages.invoke(ipm, 0L, 0);
+
+            // ParceledListSlice.getList()
</file context>
Fix with Cubic

Comment on lines +39 to +41
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

🌐 Web query:

In AOSP, compare the declared signatures of android.content.pm.IPackageManager#getInstalledPackages on current master and android10-release, and confirm whether the userId parameter scopes the returned package list.

💡 Result:

The declared signatures of android.content.pm.IPackageManager#getInstalledPackages differ between AOSP current master and android10-release. In current AOSP master (refs/heads/main), the relevant method is: ParceledListSlice<PackageInfo> getInstalledPackages(@PackageManager.PackageInfoFlagsBits long flags, int userId) In android10-release (Android 10 / Q), the relevant method is: ParceledListSlice<PackageInfo> getInstalledPackages(int flags, in int userId) Both branches have the userId parameter as the second parameter. The userId parameter scopes the returned package list to packages installed for that specific user. In Android's multi-user system, packages can be installed per-user, and this method returns PackageInfo objects filtered to the specified userId, respecting per-user installation states. This is evident from the implementation pattern where ApplicationPackageManager passes userId to IPackageManager.getInstalledPackages, and PackageManagerService uses userId to filter results based on PackageUserState.isInstalled for that user.

Citations:


🏁 Script executed:

# Get the file content around the specified lines to understand context
head -60 "app/src/main/java/com/mobilenext/devicekit/PackageLister.java" | tail -40

Repository: mobile-next/devicekit-android

Length of output: 2090


Add fallback for different getInstalledPackages signatures across Android versions.

Lines 40–41 assume the method signature getInstalledPackages(long, int) (AOSP master), but older releases like Android 10 expose getInstalledPackages(int, int). This will raise NoSuchMethodException at runtime on those versions. Additionally, hardcoding userId=0 ignores packages installed for other users on multi-user devices.

Implement a try-fallback chain to attempt the master signature first, then fall back to the older (int, int) signature. Consider accepting the target user as a parameter instead of pinning it to 0.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/mobilenext/devicekit/PackageLister.java` around lines
39 - 41, The reflection call in PackageLister that retrieves
getInstalledPackages on ipm currently assumes the signature
getInstalledPackages(long,int) and hardcodes userId 0, which will fail on older
Android (e.g., Android 10) that exposes getInstalledPackages(int,int) and
ignores multi-user installs; update the logic in PackageLister to try a fallback
chain: first attempt ipm.getClass().getMethod("getInstalledPackages",
long.class, int.class) and invoke it if present, then catch
NoSuchMethodException and try ipm.getClass().getMethod("getInstalledPackages",
int.class, int.class) and invoke that instead, mapping the long argument to an
int when using the older signature; also add a parameter to the PackageLister
API (or method where this reflection occurs) to accept a targetUserId instead of
hardcoding 0 and pass that value when invoking either signature so packages for
the requested user are returned.


// ParceledListSlice.getList()
Method getList = parceledList.getClass().getMethod("getList");
@SuppressWarnings("unchecked")
List<PackageInfo> packages = (List<PackageInfo>) getList.invoke(parceledList);

JSONArray result = new JSONArray();

for (PackageInfo pkg : packages) {
String packageName = pkg.packageName;
String versionName = pkg.versionName != null ? pkg.versionName : "";
String displayName = packageName;

ApplicationInfo appInfo = pkg.applicationInfo;
if (appInfo != null) {
if (appInfo.nonLocalizedLabel != null) {
displayName = appInfo.nonLocalizedLabel.toString();
} else if (appInfo.labelRes != 0 && appInfo.sourceDir != null) {
try {
AssetManager assets = AssetManager.class.getDeclaredConstructor().newInstance();
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 27, 2026

Choose a reason for hiding this comment

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

P1: Close AssetManager after use; creating one per package without closing can leak native resources.

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.java, line 61:

<comment>Close `AssetManager` after use; creating one per package without closing can leak native resources.</comment>

<file context>
@@ -0,0 +1,88 @@
+                        displayName = appInfo.nonLocalizedLabel.toString();
+                    } else if (appInfo.labelRes != 0 && appInfo.sourceDir != null) {
+                        try {
+                            AssetManager assets = AssetManager.class.getDeclaredConstructor().newInstance();
+                            Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
+                            addAssetPath.invoke(assets, appInfo.sourceDir);
</file context>
Fix with Cubic

Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assets, appInfo.sourceDir);
Resources res = new Resources(assets, new DisplayMetrics(), new Configuration());
String label = res.getString(appInfo.labelRes);
if (label != null && !label.isEmpty()) {
displayName = label;
}
} catch (Exception ignored) {
}
}
}

JSONObject entry = new JSONObject();
entry.put("packageName", packageName);
entry.put("appName", displayName);
entry.put("version", versionName);
result.put(entry);
}

System.out.println(result.toString());
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace(System.err);
System.exit(1);
}
}
}
Loading