Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get list of apps of all users

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.

like image 936
Forivin Avatar asked Jul 30 '19 07:07

Forivin


People also ask

How can I get a list of all apps on my computer?

Press the Windows key , type All Apps, and then press Enter . The window that opens has a full list of programs installed on the computer.

What does get-AppxPackage do?

The Get-AppxPackage cmdlet gets a list of the app packages that are installed in a user profile. An app package has an . msix or . appx file name extension.

How do I list all installed apps in Windows 10?

Press Windows key + I to open Settings and click Apps > Apps & features. Doing so will list all programs installed on your computer, along with the Windows Store apps that came pre-installed.


2 Answers

I found the code for the ParceledListSlice here, and the code for the Parcelable interface it implements here. It looks like it uses some form of array internally, so you should be able to iterate over that. I can't actually say if this will be helpful to you or not, because I'm not all that familiar with these Libraries, but I hope it's a starting point. Good Luck!

like image 101
NightDice Avatar answered Oct 21 '22 14:10

NightDice


You can apply the approach you mentioned in your first EDIT and EDIT2 and use Android's shell, since you mentioned your device has root permissions.

By the way, I'm not familiar with the library from "stericson" you are using, but I agree with leveraging existing libraries to make your life easier; I am using libsuperuser. Otherwise, you can write code to run shell commands using Process etc. yourself (but there are lots of things to consider with handling error output, closing objects, and so forth).

private List<String> listAllInstalledApps(){
    String user = "0"; //can modify to iterate over all users 
    if (Shell.SU.available()) {
        //grab user installed package names
        List<String> output =
            Shell.SU.run("pm list packages --user " + user + " -3");
        StringBuilder csvBuilder = new StringBuilder();

        for(String item : output){
            csvBuilder.append(item);
            csvBuilder.append(", ");
        }
        Log.info(TAG, "Obtained installed apps: " + csvBuilder.toString());
        return output;
    }
    return new ArrayList<>();
}

Of course, you can use other arguments to pm as necessary such as -e, -d see documentation.

Once you extract the package names, get all required info (as is contained in ApplicationInfo) using dumpsys

//package names extracted from List<String> you returned earlier
for( String name : packageNames ){
    List<String> appInfo =
            Shell.SU.run("dumpsys package " + name);
    //search appInfo for info you need e.g. grantedPermissions, targetSdk...
}

Create ApplicationInfo objects as required, if you have downstream code that requires this object type specifically

ApplicationInfo info = new ApplicationInfo();
info.uid = uid;
info.processName = processName;
info.className = classname;
info.packageName = packageName;
info.minSdkVersion = minSdkVersion;
//etc...

(fields of the ApplicationInfo class are public). This is also true of PackageInfo which is an object that contains an ApplicationInfo field, plus it contains details of the install time etc. which are also details you can extract from the dumpsys output. So you could create an array of these as you wish and can be populated with your new ApplicationInfo objects.

like image 3
dr_g Avatar answered Oct 21 '22 12:10

dr_g