I've found a solution. I went into the AOSP source and found some hidden methods in AssetManager that do the trick when invoked via reflection. System permissions aren't needed.
public static String extractPackageName(Context ctx, String apkPath) {
try {
AssetManager assmgr = ctx.getAssets();
Method addAssetPathMethod = assmgr.getClass().getMethod("addAssetPath", String.class);
Method setConfigurationMethod = assmgr.getClass().getMethod("setConfiguration",
int.class, int.class, String.class, int.class, int.class,
int.class,int.class, int.class, int.class, int.class, int.class,
int.class, int.class, int.class, int.class, int.class, int.class);
int cookie = ((Integer) addAssetPathMethod.invoke(assmgr, apkPath)).intValue();
if (cookie != 0) {
final DisplayMetrics metrics = new DisplayMetrics();
metrics.setToDefaults();
setConfigurationMethod.invoke(assmgr, 0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, Build.VERSION.SDK_INT);
XmlResourceParser parser = assmgr.openXmlResourceParser(cookie, "AndroidManifest.xml");
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) {}
String packageName = parser.getAttributeValue(null, "package");
return packageName;
}
} catch (Exception e) {
}
return null;
}