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 android.content.pm;
18 
19 import android.annotation.NonNull;
20 import android.annotation.UserIdInt;
21 import android.util.SparseArrayMap;
22 
23 import com.android.internal.annotations.GuardedBy;
24 import com.android.internal.annotations.VisibleForTesting;
25 import com.android.internal.util.ArrayUtils;
26 
27 import libcore.util.EmptyArray;
28 
29 import java.util.Objects;
30 import java.util.Random;
31 
32 /**
33  * POJO to represent a package for a specific user ID.
34  *
35  * @hide
36  */
37 public final class UserPackage {
38     private static final boolean ENABLE_CACHING = true;
39     /**
40      * The maximum number of entries to keep in the cache per user ID.
41      * The value should ideally be high enough to cover all packages on an end-user device,
42      * but low enough that stale or invalid packages would eventually (probably) get removed.
43      * This should benefit components that loop through all packages on a device and use this class,
44      * since being able to cache the objects for all packages on the device
45      * means we don't have to keep recreating the objects.
46      */
47     @VisibleForTesting
48     static final int MAX_NUM_CACHED_ENTRIES_PER_USER = 1000;
49 
50     @UserIdInt
51     public final int userId;
52     public final String packageName;
53 
54     private static final Object sCacheLock = new Object();
55     @GuardedBy("sCacheLock")
56     private static final SparseArrayMap<String, UserPackage> sCache = new SparseArrayMap<>();
57 
58     /**
59      * Set of userIDs to cache objects for. We start off with an empty set, so there's no caching
60      * by default. The system will override with a valid set of userIDs in its process so that
61      * caching becomes active in the system process.
62      */
63     @GuardedBy("sCacheLock")
64     private static int[] sUserIds = EmptyArray.INT;
65 
UserPackage(int userId, String packageName)66     private UserPackage(int userId, String packageName) {
67         this.userId = userId;
68         this.packageName = packageName;
69     }
70 
71     @Override
toString()72     public String toString() {
73         return "<" + userId + ">" + packageName;
74     }
75 
76     @Override
equals(Object obj)77     public boolean equals(Object obj) {
78         if (this == obj) {
79             return true;
80         }
81         if (obj instanceof UserPackage) {
82             UserPackage other = (UserPackage) obj;
83             return userId == other.userId && Objects.equals(packageName, other.packageName);
84         }
85         return false;
86     }
87 
88     @Override
hashCode()89     public int hashCode() {
90         int result = 0;
91         result = 31 * result + userId;
92         result = 31 * result + packageName.hashCode();
93         return result;
94     }
95 
96     /** Return an instance of this class representing the given userId + packageName combination. */
97     @NonNull
of(@serIdInt int userId, @NonNull String packageName)98     public static UserPackage of(@UserIdInt int userId, @NonNull String packageName) {
99         if (!ENABLE_CACHING) {
100             return new UserPackage(userId, packageName);
101         }
102 
103         synchronized (sCacheLock) {
104             if (!ArrayUtils.contains(sUserIds, userId)) {
105                 // Don't cache objects for invalid userIds.
106                 return new UserPackage(userId, packageName);
107             }
108 
109             UserPackage up = sCache.get(userId, packageName);
110             if (up == null) {
111                 maybePurgeRandomEntriesLocked(userId);
112                 packageName = packageName.intern();
113                 up = new UserPackage(userId, packageName);
114                 sCache.add(userId, packageName, up);
115             }
116             return up;
117         }
118     }
119 
120     /** Remove the specified app from the cache. */
removeFromCache(@serIdInt int userId, @NonNull String packageName)121     public static void removeFromCache(@UserIdInt int userId, @NonNull String packageName) {
122         if (!ENABLE_CACHING) {
123             return;
124         }
125 
126         synchronized (sCacheLock) {
127             sCache.delete(userId, packageName);
128         }
129     }
130 
131     /** Indicate the list of valid user IDs on the device. */
setValidUserIds(@onNull int[] userIds)132     public static void setValidUserIds(@NonNull int[] userIds) {
133         if (!ENABLE_CACHING) {
134             return;
135         }
136 
137         userIds = userIds.clone();
138         synchronized (sCacheLock) {
139             sUserIds = userIds;
140 
141             for (int u = sCache.numMaps() - 1; u >= 0; --u) {
142                 final int userId = sCache.keyAt(u);
143                 if (!ArrayUtils.contains(userIds, userId)) {
144                     sCache.deleteAt(u);
145                 }
146             }
147         }
148     }
149 
150     @VisibleForTesting
numEntriesForUser(int userId)151     public static int numEntriesForUser(int userId) {
152         synchronized (sCacheLock) {
153             return sCache.numElementsForKey(userId);
154         }
155     }
156 
157     /** Purge a random set of entries if the cache size is too large. */
158     @GuardedBy("sCacheLock")
maybePurgeRandomEntriesLocked(int userId)159     private static void maybePurgeRandomEntriesLocked(int userId) {
160         final int uIdx = sCache.indexOfKey(userId);
161         if (uIdx < 0) {
162             return;
163         }
164         int numCached = sCache.numElementsForKeyAt(uIdx);
165         if (numCached < MAX_NUM_CACHED_ENTRIES_PER_USER) {
166             return;
167         }
168         // Purge a random set of 1% of cached elements for the userId. We don't want to use a
169         // deterministic system of purging because that may cause us to repeatedly remove elements
170         // that are frequently added and queried more than others. Choosing a random set
171         // means we will probably eventually remove less useful elements.
172         // An LRU cache is too expensive for this commonly used utility class.
173         final Random rand = new Random();
174         final int numToPurge = Math.max(1, MAX_NUM_CACHED_ENTRIES_PER_USER / 100);
175         for (int i = 0; i < numToPurge && numCached > 0; ++i) {
176             final int removeIdx = rand.nextInt(numCached--);
177             sCache.deleteAt(uIdx, removeIdx);
178         }
179     }
180 }
181