If I want to retrieve an ApplicationInfo list for all apps of the current user, I can just run:
PackageManager pkgmanager = ctx.getPackageManager();
List<ApplicationInfo> installedApps = pkgmanager.getInstalledApplications(PackageManager.GET_META_DATA);
But I am looking for a way to get these lists for every user and not just the current one. The app that I'm working on has root permissions btw!
From what I understand, the user ids can be retrieved like this:
List<Integer> userIds = new ArrayList<>();
PackageManager pkgmanager = ctx.getPackageManager();
final UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE);
List<UserHandle> list = um.getUserProfiles();
for (UserHandle user : list) {
Matcher m = p.matcher(user.toString());
if (m.find()) {
int id = Integer.parseInt(m.group(1));
userIds.add(id);
}
}
Now I'm looking for something like this:
for (int i=0; i<userIds.size(); i++) {
Integer userId = userIds.get(i)
List<ApplicationInfo> installedApps = pkgmanager.getInstalledApplicationsByUserId(userId, PackageManager.GET_META_DATA);
}
Obviously getInstalledApplicationsByUserId
doesn't exist.
At first I thought getPackagesForUid could solve the problem, but as it turns out there is a difference between "users" in the Linux-sense and user profiles. Generally every single Android app runs under it's own user for isolation-purposes. But it is possible to run two apps under the same user so that they can access each others data easily. getPackagesForUid
merely returns the names of all apps that run under the given user id, which is usually exactly one. In addition to "users" there are also "User profiles" which is what I was hoping the method was referring to. Maybe I should have also written userProfileId
instead of userId
in my code.
Edit: Using adb shell, I can retrieve the app IDs like this:
# Get user profile IDs
USER_PROFILE_IDS="$(pm list users | grep UserInfo | cut -d '{' -f2 | cut -d ':' -f1)"
# Iterate over user profile IDs
while read -r USER_PROFILE_ID ; do
# List the packages for each user profile by ID
PACKAGE_IDS_FOR_THIS_USER="$(pm list packages --user "$USER_PROFILE_ID" | cut -d ':' -f2)"
echo "#######################################################################"
echo "The user with id $USER_PROFILE_ID has the following packages installed:"
echo "$PACKAGE_IDS_FOR_THIS_USER"
done <<< "$USER_PROFILE_IDS"
But that's Bash and not Java...
Edit2: Correct me if I'm wrong (this is the first time I've written code in Java) but it doesn't appear like there is a Java API to do this. So the only way to do this would by using a shell. So this is what I came up with:
import com.stericson.rootshell.RootShell;
import com.stericson.rootshell.execution.Command;
import com.stericson.rootshell.execution.Shell;
import com.stericson.roottools.RootTools;
...
public final class Api {
...
/**
* @param ctx application context (mandatory)
* @return a list of user profile ids
*/
private static List<Integer> getUserIds(Context ctx) {
List<Integer> userIds = new ArrayList<>();
PackageManager pkgmanager = ctx.getPackageManager();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//this code will be executed on devices running ICS or later
final UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE);
List<UserHandle> list = um.getUserProfiles();
for (UserHandle user : list) {
Matcher m = p.matcher(user.toString());
if (m.find()) {
int id = Integer.parseInt(m.group(1));
//if (id > 0) {
userIds.add(id);
//}
}
}
} else {
userIds.add(0);
}
return userIds;
}
/**
* @param ctx application context (mandatory)
* @return a list of user profile ids
*/
private static List<String> getPackageIdsByUserProfileId(Integer userId) {
List<String> packageIds = new ArrayList<>();
Command command = new Command(0, "pm list packages --user " + userId + " | cut -d ':' -f2 ")
{
@Override
public void commandOutput(int id, String line) {
packageIds.add(line);
super.commandOutput(id, line);
}
};
Shell shell = RootTools.getShell(true);
shell.add(command);
while (!command.isFinished()) {
Thread.sleep(100);
}
return packageIds;
}
...
/**
* @param ctx application context (mandatory)
* @return a list of applications
*/
public static List<PackageInfoData> getApps(Context ctx, GetAppList appList) {
List<Integer> userIds = getUserIds();
for (int i=0; i<userIds.size(); i++) {
Integer userId = userIds.get(i)
List<String> packageIds = getPackageIdsByUserProfileId(userId)
}
...
}
}
But I have no clue if this is even close to something that would actually work.
And besides that, I only get the package ids ("com.whatsapp" etc) for each user profile, but I would like to get a list of ApplicationInfo just like getInstalledApplications
returns it. I just can't think of a good way to do this. Maybe it would be possible to load the package manifests and then create ApplicationInfo instances based on them somehow?
Edit3:
I think I found the source code for the pm
executable.
Oddly I couldn't find a single mention of the --user
flag in it.
The most relevant parts of the code are:
import android.os.ServiceManager;
...
mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
...
int getFlags = 0;
...
final List<PackageInfo> packages = getInstalledPackages(mPm, getFlags);
The getInstalledPackages
method simply calls mPm.getInstalledPackages
then:
@SuppressWarnings("unchecked")
private List<PackageInfo> getInstalledPackages(IPackageManager pm, int flags)
throws RemoteException {
final List<PackageInfo> packageInfos = new ArrayList<PackageInfo>();
PackageInfo lastItem = null;
ParceledListSlice<PackageInfo> slice;
do {
final String lastKey = lastItem != null ? lastItem.packageName : null;
slice = pm.getInstalledPackages(flags, lastKey);
lastItem = slice.populateList(packageInfos, PackageInfo.CREATOR);
} while (!slice.isLastSlice());
return packageInfos;
}
This leaves me with more questions than I had before. First of all I wonder if I couldn't just import the com.android.commands.pm
class. Secondly I wonder how I could tell it to return the packages of a specific user profile or if this even is the right piece of source code in the first place.
And finally I wonder if I even need root permissions to use that. After all, the if (Process.myUid() != ROOT_UID)
checks are only executed for runRemoveProfile
, runCreateProfile
and runListProfiles
.
Edit4:
I was not able to find the source code of the package
service. I was only able to find this file: /data/system/packages.xml
. It contains some basic package information on all packages (for all user profiles), but it neither contains the actuall app names, nor does it contain information on which user profiles they belong to.
Edit5: I think I found the package service source code. I think this method is the important one:
public ParceledListSlice<PackageInfo> getInstalledPackages(int flags, int userId)
Unfortunately, I just don't understand the code. To me it looks like the packages are somehow coming from mSettings.mPackages
. The variable is explained as follows in a code comment:
{@link #mPackages} is used to guard all in-memory parsed package details and other related state. It is a fine-grained lock that should only be held momentarily, as it's one of the most contended locks in the system.
Edit6: I just found another even more interesting method in that file:
public ParceledListSlice<ApplicationInfo> getInstalledApplications(int flags, int userId)
I don't know what ParceledListSlice is, but sicne it says <ApplicationInfo>
I'm assuming that this is really close to my wanted List<ApplicationInfo>
format. But still, I'm completely clueless how I could access that functionality.
ServiceManager
isn't public, but you should be able to call it by reflection, see https://mcmap.net/q/535278/-the-import-android-os-servicemanager-cannot-be-resolved – Flawed