/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.telemetry;
import static com.android.car.telemetry.CarTelemetryService.DEBUG;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.car.builtin.util.Slogf;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.SparseArray;
import com.android.car.CarLog;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
/**
* Maps app package name to UID using {@link PackageManager}, and app install/remove and user
* add/remove broadcasts. It also stores some uninstalled apps, because some publishers may have
* data for recent uninstalled apps.
*
*
See https://source.android.com/security/app-sandbox to learn more about UIDs. Note that an app
* (package name) has single UID, but a UID can have multiple apps, there is one-to-many
* relationship.
*
*
Use {@code adb shell pm list packages -U -u --show-versioncode} to list the packages.
*/
public class UidPackageMapper {
// Store removed app info just in case some publishers (e.g. ConnectivityPublisher) may send
// data related to them.
private static final int DEFAULT_MAX_REMOVED_APPS_COUNT = 100;
private final Context mContext;
private final Handler mTelemetryHandler;
private final int mMaxRemovedAppsCount;
// Maps uid to the list of AppInfo.
private final SparseArray> mUidAppInfo = new SparseArray<>();
// Caches "mMaxRemovedAppsCount" removed apps, as there will be statistics even for the
// uninstalled apps.
//
// Note that it may contain different AppInfo object for the same uid/packageName, because
// of refetchAllAppInfo() method.
private final ArrayDeque mRemovedApps = new ArrayDeque<>();
private final BroadcastReceiver mAppUpdateReceiver = new AppUpdateReceiver();
private final BroadcastReceiver mUserUpdateReceiver = new UserUpdateReceiver();
/** Constructs an instance. */
public UidPackageMapper(@NonNull Context context, @NonNull Handler telemetryHandler) {
this(context, telemetryHandler, DEFAULT_MAX_REMOVED_APPS_COUNT);
}
@VisibleForTesting
UidPackageMapper(
@NonNull Context context, @NonNull Handler telemetryHandler, int maxRemovedAppsCount) {
mContext = context;
mTelemetryHandler = telemetryHandler;
mMaxRemovedAppsCount = maxRemovedAppsCount;
}
/**
* Subscribes for broadcast events and initializes the mapper by fetching all the apps from
* PackageManager.
*/
public void init() {
// Setup broadcast receiver for app updates.
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REPLACED);
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addDataScheme("package");
mContext.registerReceiverForAllUsers(mAppUpdateReceiver, filter, null, null);
// Setup receiver for user initialize (happens once for a new user) and if a user is
// removed.
filter = new IntentFilter(Intent.ACTION_USER_INITIALIZE);
filter.addAction(Intent.ACTION_USER_REMOVED);
mContext.registerReceiverForAllUsers(mUserUpdateReceiver, filter, null, null);
refetchAllAppInfo(mContext);
}
/** Releases resources. */
public void release() {
mContext.unregisterReceiver(mAppUpdateReceiver);
mContext.unregisterReceiver(mUserUpdateReceiver);
}
/**
* Returns the list of packages for uid, including APEX and some uninstalled apps. May return
* an empty list.
*/
@NonNull
public List getPackagesForUid(int uid) {
List uidApps = mUidAppInfo.get(uid);
if (uidApps == null) {
return List.of();
}
ArrayList result = new ArrayList<>();
for (int i = 0; i < uidApps.size(); i++) {
result.add(uidApps.get(i).mPackageName);
}
return result;
}
/** Gets AppInfo from "mUidAppInfo" map. */
@Nullable
private AppInfo getAppInfo(int uid, @NonNull String packageName) {
ArrayList uidApps = mUidAppInfo.get(uid);
if (uidApps == null) {
uidApps = new ArrayList<>();
mUidAppInfo.put(uid, uidApps);
}
for (int i = 0; i < uidApps.size(); i++) {
AppInfo current = uidApps.get(i);
if (current.mPackageName.equals(packageName)) {
return current;
}
}
return null;
}
private void onAppAddedOrUpdated(int uid, @NonNull String packageName) {
AppInfo appInfo = getAppInfo(uid, packageName);
if (appInfo == null) {
// The uid always exists in mUidAppInfo after getAppInfo() was called.
mUidAppInfo.get(uid).add(new AppInfo(uid, packageName));
} else {
appInfo.mIsRemoved = false;
}
}
/** Marks the AppInfo removed */
private void onAppRemoved(int uid, @NonNull String packageName) {
AppInfo appInfo = getAppInfo(uid, packageName);
if (appInfo == null) {
Slogf.i(
CarLog.TAG_TELEMETRY,
"UidPackageMapper failed to remove the app from its cache, "
+ "the app not found.");
return;
}
if (appInfo.mIsRemoved) {
return; // ignore the already removed apps
}
appInfo.mIsRemoved = true;
mRemovedApps.add(appInfo);
if (mRemovedApps.size() > mMaxRemovedAppsCount) {
AppInfo completelyRemoved = mRemovedApps.removeFirst();
if (completelyRemoved.mIsRemoved && mUidAppInfo.contains(completelyRemoved.mUid)) {
mUidAppInfo
.get(completelyRemoved.mUid)
.removeIf(app -> app.mPackageName.equals(completelyRemoved.mPackageName));
}
}
}
/** Returns installed and uninstalled packages, including Apex packages. */
@NonNull
private static List getAllPackagesIncludingApex(
@NonNull PackageManager pm, @NonNull UserHandle user) {
ArrayList packages =
new ArrayList<>(
pm.getInstalledPackagesAsUser(
PackageManager.MATCH_UNINSTALLED_PACKAGES
| PackageManager.MATCH_ANY_USER,
user.getIdentifier()));
// Get only installed APEX packages, because inactive apexes can conflict with active ones.
for (PackageInfo info : pm.getInstalledPackages(PackageManager.MATCH_APEX)) {
if (info.isApex) {
packages.add(info);
}
}
return packages;
}
private void refetchAllAppInfo(@NonNull Context context) {
UserManager um = context.getSystemService(UserManager.class);
PackageManager pm = context.getPackageManager();
List users = um.getUserHandles(/* excludeDying= */ true);
mUidAppInfo.clear();
if (DEBUG) {
Slogf.d(CarLog.TAG_TELEMETRY, "Fetching packages for %d users", users.size());
}
for (int i = 0; i < users.size(); i++) {
List packages = getAllPackagesIncludingApex(pm, users.get(i));
for (int j = 0; j < packages.size(); j++) {
onAppAddedOrUpdated(
packages.get(j).applicationInfo.uid, packages.get(j).packageName);
}
}
// Add removed apps back to the "mUidAppInfo".
for (AppInfo removedApp : mRemovedApps) {
// This "appInfo" instance is different than the "removedApp" instance.
AppInfo appInfo = getAppInfo(removedApp.mUid, removedApp.mPackageName);
if (appInfo == null) {
onAppAddedOrUpdated(removedApp.mUid, removedApp.mPackageName);
}
}
}
private class AppUpdateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) {
Slogf.d(
CarLog.TAG_TELEMETRY,
"UidPackageMapper received intent %s",
intent);
}
if (intent == null || intent.getAction() == null || intent.getData() == null) {
Slogf.w(
CarLog.TAG_TELEMETRY,
"UidPackageMapper received null intent or null action or null data."
+ " Ignoring.");
return;
}
/*
* App updates (ACTION_PACKAGE_REPLACED) actually consist of REMOVE, ADD, and then
* REPLACE broadcasts. To avoid waste, we ignore the extra REMOVE and ADD broadcasts
* that contain the replacing flag (EXTRA_REPLACING).
*/
if (!intent.getAction().equals(Intent.ACTION_PACKAGE_REPLACED)
&& intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
return; // Keep only replacing or normal add and remove.
}
Bundle extra = intent.getExtras();
if (extra == null) {
Slogf.w(
CarLog.TAG_TELEMETRY,
"UidPackageMapper received an intent with null extras. Ignoring.");
return;
}
int uid = extra.getInt(Intent.EXTRA_UID, -1);
String packageName = intent.getData().getSchemeSpecificPart();
if (uid == -1) {
Slogf.w(
CarLog.TAG_TELEMETRY,
"UidPackageMapper received app update intent with no uid. Ignoring.");
return;
}
if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) {
mTelemetryHandler.post(() -> onAppRemoved(uid, packageName));
} else {
mTelemetryHandler.post(() -> onAppAddedOrUpdated(uid, packageName));
}
}
}
private class UserUpdateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
mTelemetryHandler.post(() -> refetchAllAppInfo(context));
}
}
/** Stores information about an app identified by "uid" and "packageName". */
private static class AppInfo {
int mUid;
@NonNull String mPackageName;
boolean mIsRemoved;
AppInfo(int uid, @NonNull String packageName) {
mUid = uid;
mPackageName = packageName;
mIsRemoved = false;
}
}
}