1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.telemetry;
18 
19 import static com.android.car.telemetry.CarTelemetryService.DEBUG;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.car.builtin.util.Slogf;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.content.pm.PackageInfo;
29 import android.content.pm.PackageManager;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.UserHandle;
33 import android.os.UserManager;
34 import android.util.SparseArray;
35 
36 import com.android.car.CarLog;
37 import com.android.internal.annotations.VisibleForTesting;
38 
39 import java.util.ArrayDeque;
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 /**
44  * Maps app package name to UID using {@link PackageManager}, and app install/remove and user
45  * add/remove broadcasts. It also stores some uninstalled apps, because some publishers may have
46  * data for recent uninstalled apps.
47  *
48  * <p>See https://source.android.com/security/app-sandbox to learn more about UIDs. Note that an app
49  * (package name) has single UID, but a UID can have multiple apps, there is one-to-many
50  * relationship.
51  *
52  * <p>Use {@code adb shell pm list packages -U -u --show-versioncode} to list the packages.
53  */
54 public class UidPackageMapper {
55     // Store removed app info just in case some publishers (e.g. ConnectivityPublisher) may send
56     // data related to them.
57     private static final int DEFAULT_MAX_REMOVED_APPS_COUNT = 100;
58 
59     private final Context mContext;
60     private final Handler mTelemetryHandler;
61     private final int mMaxRemovedAppsCount;
62 
63     // Maps uid to the list of AppInfo.
64     private final SparseArray<ArrayList<AppInfo>> mUidAppInfo = new SparseArray<>();
65 
66     // Caches "mMaxRemovedAppsCount" removed apps, as there will be statistics even for the
67     // uninstalled apps.
68     //
69     // Note that it may contain different AppInfo object for the same uid/packageName, because
70     // of refetchAllAppInfo() method.
71     private final ArrayDeque<AppInfo> mRemovedApps = new ArrayDeque<>();
72 
73     private final BroadcastReceiver mAppUpdateReceiver = new AppUpdateReceiver();
74     private final BroadcastReceiver mUserUpdateReceiver = new UserUpdateReceiver();
75 
76     /** Constructs an instance. */
UidPackageMapper(@onNull Context context, @NonNull Handler telemetryHandler)77     public UidPackageMapper(@NonNull Context context, @NonNull Handler telemetryHandler) {
78         this(context, telemetryHandler, DEFAULT_MAX_REMOVED_APPS_COUNT);
79     }
80 
81     @VisibleForTesting
UidPackageMapper( @onNull Context context, @NonNull Handler telemetryHandler, int maxRemovedAppsCount)82     UidPackageMapper(
83             @NonNull Context context, @NonNull Handler telemetryHandler, int maxRemovedAppsCount) {
84         mContext = context;
85         mTelemetryHandler = telemetryHandler;
86         mMaxRemovedAppsCount = maxRemovedAppsCount;
87     }
88 
89     /**
90      * Subscribes for broadcast events and initializes the mapper by fetching all the apps from
91      * PackageManager.
92      */
init()93     public void init() {
94         // Setup broadcast receiver for app updates.
95         IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REPLACED);
96         filter.addAction(Intent.ACTION_PACKAGE_ADDED);
97         filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
98         filter.addDataScheme("package");
99         mContext.registerReceiverForAllUsers(mAppUpdateReceiver, filter, null, null);
100 
101         // Setup receiver for user initialize (happens once for a new user) and if a user is
102         // removed.
103         filter = new IntentFilter(Intent.ACTION_USER_INITIALIZE);
104         filter.addAction(Intent.ACTION_USER_REMOVED);
105         mContext.registerReceiverForAllUsers(mUserUpdateReceiver, filter, null, null);
106 
107         refetchAllAppInfo(mContext);
108     }
109 
110     /** Releases resources. */
release()111     public void release() {
112         mContext.unregisterReceiver(mAppUpdateReceiver);
113         mContext.unregisterReceiver(mUserUpdateReceiver);
114     }
115 
116     /**
117      * Returns the list of packages for uid, including APEX and some uninstalled apps. May return
118      * an empty list.
119      */
120     @NonNull
getPackagesForUid(int uid)121     public List<String> getPackagesForUid(int uid) {
122         List<AppInfo> uidApps = mUidAppInfo.get(uid);
123         if (uidApps == null) {
124             return List.of();
125         }
126         ArrayList<String> result = new ArrayList<>();
127         for (int i = 0; i < uidApps.size(); i++) {
128             result.add(uidApps.get(i).mPackageName);
129         }
130         return result;
131     }
132 
133     /** Gets AppInfo from "mUidAppInfo" map. */
134     @Nullable
getAppInfo(int uid, @NonNull String packageName)135     private AppInfo getAppInfo(int uid, @NonNull String packageName) {
136         ArrayList<AppInfo> uidApps = mUidAppInfo.get(uid);
137         if (uidApps == null) {
138             uidApps = new ArrayList<>();
139             mUidAppInfo.put(uid, uidApps);
140         }
141         for (int i = 0; i < uidApps.size(); i++) {
142             AppInfo current = uidApps.get(i);
143             if (current.mPackageName.equals(packageName)) {
144                 return current;
145             }
146         }
147         return null;
148     }
149 
onAppAddedOrUpdated(int uid, @NonNull String packageName)150     private void onAppAddedOrUpdated(int uid, @NonNull String packageName) {
151         AppInfo appInfo = getAppInfo(uid, packageName);
152         if (appInfo == null) {
153             // The uid always exists in mUidAppInfo after getAppInfo() was called.
154             mUidAppInfo.get(uid).add(new AppInfo(uid, packageName));
155         } else {
156             appInfo.mIsRemoved = false;
157         }
158     }
159 
160     /** Marks the AppInfo removed */
onAppRemoved(int uid, @NonNull String packageName)161     private void onAppRemoved(int uid, @NonNull String packageName) {
162         AppInfo appInfo = getAppInfo(uid, packageName);
163         if (appInfo == null) {
164             Slogf.i(
165                     CarLog.TAG_TELEMETRY,
166                     "UidPackageMapper failed to remove the app from its cache, "
167                             + "the app not found.");
168             return;
169         }
170         if (appInfo.mIsRemoved) {
171             return; // ignore the already removed apps
172         }
173         appInfo.mIsRemoved = true;
174 
175         mRemovedApps.add(appInfo);
176         if (mRemovedApps.size() > mMaxRemovedAppsCount) {
177             AppInfo completelyRemoved = mRemovedApps.removeFirst();
178             if (completelyRemoved.mIsRemoved && mUidAppInfo.contains(completelyRemoved.mUid)) {
179                 mUidAppInfo
180                         .get(completelyRemoved.mUid)
181                         .removeIf(app -> app.mPackageName.equals(completelyRemoved.mPackageName));
182             }
183         }
184     }
185 
186     /** Returns installed and uninstalled packages, including Apex packages. */
187     @NonNull
getAllPackagesIncludingApex( @onNull PackageManager pm, @NonNull UserHandle user)188     private static List<PackageInfo> getAllPackagesIncludingApex(
189             @NonNull PackageManager pm, @NonNull UserHandle user) {
190         ArrayList<PackageInfo> packages =
191                 new ArrayList<>(
192                         pm.getInstalledPackagesAsUser(
193                                 PackageManager.MATCH_UNINSTALLED_PACKAGES
194                                         | PackageManager.MATCH_ANY_USER,
195                                 user.getIdentifier()));
196         // Get only installed APEX packages, because inactive apexes can conflict with active ones.
197         for (PackageInfo info : pm.getInstalledPackages(PackageManager.MATCH_APEX)) {
198             if (info.isApex) {
199                 packages.add(info);
200             }
201         }
202         return packages;
203     }
204 
refetchAllAppInfo(@onNull Context context)205     private void refetchAllAppInfo(@NonNull Context context) {
206         UserManager um = context.getSystemService(UserManager.class);
207         PackageManager pm = context.getPackageManager();
208         List<UserHandle> users = um.getUserHandles(/* excludeDying= */ true);
209         mUidAppInfo.clear();
210         if (DEBUG) {
211             Slogf.d(CarLog.TAG_TELEMETRY, "Fetching packages for %d users", users.size());
212         }
213         for (int i = 0; i < users.size(); i++) {
214             List<PackageInfo> packages = getAllPackagesIncludingApex(pm, users.get(i));
215             for (int j = 0; j < packages.size(); j++) {
216                 onAppAddedOrUpdated(
217                         packages.get(j).applicationInfo.uid, packages.get(j).packageName);
218             }
219         }
220         // Add removed apps back to the "mUidAppInfo".
221         for (AppInfo removedApp : mRemovedApps) {
222             // This "appInfo" instance is different than the "removedApp" instance.
223             AppInfo appInfo = getAppInfo(removedApp.mUid, removedApp.mPackageName);
224             if (appInfo == null) {
225                 onAppAddedOrUpdated(removedApp.mUid, removedApp.mPackageName);
226             }
227         }
228     }
229 
230     private class AppUpdateReceiver extends BroadcastReceiver {
231         @Override
onReceive(Context context, Intent intent)232         public void onReceive(Context context, Intent intent) {
233             if (DEBUG) {
234                 Slogf.d(
235                         CarLog.TAG_TELEMETRY,
236                         "UidPackageMapper received intent %s",
237                         intent);
238             }
239             if (intent == null || intent.getAction() == null || intent.getData() == null) {
240                 Slogf.w(
241                         CarLog.TAG_TELEMETRY,
242                         "UidPackageMapper received null intent or null action or null data."
243                                 + " Ignoring.");
244                 return;
245             }
246             /*
247              * App updates (ACTION_PACKAGE_REPLACED) actually consist of REMOVE, ADD, and then
248              * REPLACE broadcasts. To avoid waste, we ignore the extra REMOVE and ADD broadcasts
249              * that contain the replacing flag (EXTRA_REPLACING).
250              */
251             if (!intent.getAction().equals(Intent.ACTION_PACKAGE_REPLACED)
252                     && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
253                 return; // Keep only replacing or normal add and remove.
254             }
255 
256             Bundle extra = intent.getExtras();
257             if (extra == null) {
258                 Slogf.w(
259                         CarLog.TAG_TELEMETRY,
260                         "UidPackageMapper received an intent with null extras. Ignoring.");
261                 return;
262             }
263             int uid = extra.getInt(Intent.EXTRA_UID, -1);
264             String packageName = intent.getData().getSchemeSpecificPart();
265 
266             if (uid == -1) {
267                 Slogf.w(
268                         CarLog.TAG_TELEMETRY,
269                         "UidPackageMapper received app update intent with no uid. Ignoring.");
270                 return;
271             }
272 
273             if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) {
274                 mTelemetryHandler.post(() -> onAppRemoved(uid, packageName));
275             } else {
276                 mTelemetryHandler.post(() -> onAppAddedOrUpdated(uid, packageName));
277             }
278         }
279     }
280 
281     private class UserUpdateReceiver extends BroadcastReceiver {
282         @Override
onReceive(Context context, Intent intent)283         public void onReceive(Context context, Intent intent) {
284             mTelemetryHandler.post(() -> refetchAllAppInfo(context));
285         }
286     }
287 
288     /** Stores information about an app identified by "uid" and "packageName". */
289     private static class AppInfo {
290         int mUid;
291         @NonNull String mPackageName;
292         boolean mIsRemoved;
293 
AppInfo(int uid, @NonNull String packageName)294         AppInfo(int uid, @NonNull String packageName) {
295             mUid = uid;
296             mPackageName = packageName;
297             mIsRemoved = false;
298         }
299     }
300 }
301