1 /*
2  * Copyright (C) 2023 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.packageinstaller.v2.model
18 
19 import android.Manifest
20 import android.content.Context
21 import android.content.pm.ApplicationInfo
22 import android.content.pm.PackageInfo
23 import android.content.pm.PackageInstaller
24 import android.content.pm.PackageManager
25 import android.content.res.Resources
26 import android.graphics.drawable.BitmapDrawable
27 import android.graphics.drawable.Drawable
28 import android.net.Uri
29 import android.os.Build
30 import android.os.Process
31 import android.os.UserHandle
32 import android.os.UserManager
33 import android.util.Log
34 import java.io.File
35 
36 object PackageUtil {
37     private val LOG_TAG = InstallRepository::class.java.simpleName
38     private const val DOWNLOADS_AUTHORITY = "downloads"
39     private const val SPLIT_BASE_APK_SUFFIX = "base.apk"
40     const val localLogv = false
41 
42     /**
43      * Determines if the UID belongs to the system downloads provider and returns the
44      * [ApplicationInfo] of the provider
45      *
46      * @param uid UID of the caller
47      * @return [ApplicationInfo] of the provider if a downloads provider exists, it is a
48      * system app, and its UID matches with the passed UID, null otherwise.
49      */
getSystemDownloadsProviderInfonull50     private fun getSystemDownloadsProviderInfo(pm: PackageManager, uid: Int): ApplicationInfo? {
51         // Check if there are currently enabled downloads provider on the system.
52         val providerInfo = pm.resolveContentProvider(DOWNLOADS_AUTHORITY, 0)
53             ?: return null
54         val appInfo = providerInfo.applicationInfo
55         return if ((appInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) && uid == appInfo.uid) {
56             appInfo
57         } else null
58     }
59 
60     /**
61      * Get the maximum target sdk for a UID.
62      *
63      * @param context The context to use
64      * @param uid The UID requesting the install/uninstall
65      * @return The maximum target SDK or -1 if the uid does not match any packages.
66      */
67     @JvmStatic
getMaxTargetSdkVersionForUidnull68     fun getMaxTargetSdkVersionForUid(context: Context, uid: Int): Int {
69         val pm = context.packageManager
70         val packages = pm.getPackagesForUid(uid)
71         var targetSdkVersion = -1
72         if (packages != null) {
73             for (packageName in packages) {
74                 try {
75                     val info = pm.getApplicationInfo(packageName!!, 0)
76                     targetSdkVersion = maxOf(targetSdkVersion, info.targetSdkVersion)
77                 } catch (e: PackageManager.NameNotFoundException) {
78                     // Ignore and try the next package
79                 }
80             }
81         }
82         return targetSdkVersion
83     }
84 
85     @JvmStatic
canPackageQuerynull86     fun canPackageQuery(context: Context, callingUid: Int, packageUri: Uri): Boolean {
87         val pm = context.packageManager
88         val info = pm.resolveContentProvider(
89             packageUri.authority!!,
90             PackageManager.ComponentInfoFlags.of(0)
91         ) ?: return false
92         val targetPackage = info.packageName
93         val callingPackages = pm.getPackagesForUid(callingUid) ?: return false
94         for (callingPackage in callingPackages) {
95             try {
96                 if (pm.canPackageQuery(callingPackage!!, targetPackage)) {
97                     return true
98                 }
99             } catch (e: PackageManager.NameNotFoundException) {
100                 // no-op
101             }
102         }
103         return false
104     }
105 
106     /**
107      * @param context the [Context] object
108      * @param permission the permission name to check
109      * @param callingUid the UID of the caller who's permission is being checked
110      * @return `true` if the callingUid is granted the said permission
111      */
112     @JvmStatic
isPermissionGrantednull113     fun isPermissionGranted(context: Context, permission: String, callingUid: Int): Boolean {
114         return (context.checkPermission(permission, -1, callingUid)
115             == PackageManager.PERMISSION_GRANTED)
116     }
117 
118     /**
119      * @param pm the [PackageManager] object
120      * @param permission the permission name to check
121      * @param packageName the name of the package who's permission is being checked
122      * @return `true` if the package is granted the said permission
123      */
124     @JvmStatic
isPermissionGrantednull125     fun isPermissionGranted(pm: PackageManager, permission: String, packageName: String): Boolean {
126         return pm.checkPermission(permission, packageName) == PackageManager.PERMISSION_GRANTED
127     }
128 
129     /**
130      * @param context the [Context] object
131      * @param callingUid the UID of the caller of Pia
132      * @param isTrustedSource indicates whether install request is coming from a privileged app
133      * that has passed EXTRA_NOT_UNKNOWN_SOURCE as `true` in the installation intent, or an app that
134      * has the [INSTALL_PACKAGES][Manifest.permission.INSTALL_PACKAGES] permission granted.
135      *
136      * @return `true` if the package is either a system downloads provider, a document manager,
137      * a trusted source, or has declared the
138      * [REQUEST_INSTALL_PACKAGES][Manifest.permission.REQUEST_INSTALL_PACKAGES] in its manifest.
139      */
140     @JvmStatic
isInstallPermissionGrantedOrRequestednull141     fun isInstallPermissionGrantedOrRequested(
142         context: Context,
143         callingUid: Int,
144         isTrustedSource: Boolean,
145     ): Boolean {
146         val isDocumentsManager =
147             isPermissionGranted(context, Manifest.permission.MANAGE_DOCUMENTS, callingUid)
148         val isSystemDownloadsProvider =
149             getSystemDownloadsProviderInfo(context.packageManager, callingUid) != null
150 
151         if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager) {
152             val targetSdkVersion = getMaxTargetSdkVersionForUid(context, callingUid)
153             if (targetSdkVersion < 0) {
154                 // Invalid calling uid supplied. Abort install.
155                 Log.e(LOG_TAG, "Cannot get target SDK version for uid $callingUid")
156                 return false
157             } else if (targetSdkVersion >= Build.VERSION_CODES.O
158                 && !isUidRequestingPermission(
159                     context.packageManager, callingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES
160                 )
161             ) {
162                 Log.e(
163                     LOG_TAG, "Requesting uid " + callingUid + " needs to declare permission "
164                         + Manifest.permission.REQUEST_INSTALL_PACKAGES
165                 )
166                 return false
167             }
168         }
169         return true
170     }
171 
172     /**
173      * @param pm the [PackageManager] object
174      * @param uid the UID of the caller who's permission is being checked
175      * @param permission the permission name to check
176      * @return `true` if the caller is requesting the said permission in its Manifest
177      */
isUidRequestingPermissionnull178     private fun isUidRequestingPermission(
179         pm: PackageManager,
180         uid: Int,
181         permission: String,
182     ): Boolean {
183         val packageNames = pm.getPackagesForUid(uid) ?: return false
184         for (packageName in packageNames) {
185             val packageInfo: PackageInfo = try {
186                 pm.getPackageInfo(packageName!!, PackageManager.GET_PERMISSIONS)
187             } catch (e: PackageManager.NameNotFoundException) {
188                 // Ignore and try the next package
189                 continue
190             }
191             if (packageInfo.requestedPermissions != null
192                 && listOf(*packageInfo.requestedPermissions!!).contains(permission)
193             ) {
194                 return true
195             }
196         }
197         return false
198     }
199 
200     /**
201      * @param pi the [PackageInstaller] object to use
202      * @param originatingUid the UID of the package performing a session based install
203      * @param sessionId ID of the install session
204      * @return `true` if the caller is the session owner
205      */
206     @JvmStatic
isCallerSessionOwnernull207     fun isCallerSessionOwner(pi: PackageInstaller, callingUid: Int, sessionId: Int): Boolean {
208         if (callingUid == Process.ROOT_UID) {
209             return true
210         }
211         val sessionInfo = pi.getSessionInfo(sessionId) ?: return false
212         val installerUid = sessionInfo.getInstallerUid()
213         return callingUid == installerUid
214     }
215 
216     /**
217      * Generates a stub [PackageInfo] object for the given packageName
218      */
219     @JvmStatic
generateStubPackageInfonull220     fun generateStubPackageInfo(packageName: String?): PackageInfo {
221         val info = PackageInfo()
222         val aInfo = ApplicationInfo()
223         info.applicationInfo = aInfo
224         info.applicationInfo!!.packageName = packageName
225         info.packageName = info.applicationInfo!!.packageName
226         return info
227     }
228 
229     /**
230      * Generates an [AppSnippet] containing an appIcon and appLabel from the
231      * [PackageInstaller.SessionInfo] object
232      */
233     @JvmStatic
getAppSnippetnull234     fun getAppSnippet(context: Context, info: PackageInstaller.SessionInfo): AppSnippet {
235         val pm = context.packageManager
236         val label = info.getAppLabel()
237         val icon = if (info.getAppIcon() != null) BitmapDrawable(
238             context.resources,
239             info.getAppIcon()
240         ) else pm.defaultActivityIcon
241         return AppSnippet(label, icon)
242     }
243 
244     /**
245      * Generates an [AppSnippet] containing an appIcon and appLabel from the
246      * [PackageInfo] object
247      */
248     @JvmStatic
getAppSnippetnull249     fun getAppSnippet(context: Context, pkgInfo: PackageInfo): AppSnippet {
250         return pkgInfo.applicationInfo?.let { getAppSnippet(context, it) } ?: run {
251             AppSnippet(pkgInfo.packageName, context.packageManager.defaultActivityIcon)
252         }
253     }
254 
255     /**
256      * Generates an [AppSnippet] containing an appIcon and appLabel from the
257      * [ApplicationInfo] object
258      */
259     @JvmStatic
getAppSnippetnull260     fun getAppSnippet(context: Context, appInfo: ApplicationInfo): AppSnippet {
261         val pm = context.packageManager
262         val label = pm.getApplicationLabel(appInfo)
263         val icon = pm.getApplicationIcon(appInfo)
264         return AppSnippet(label, icon)
265     }
266 
267     /**
268      * Generates an [AppSnippet] containing an appIcon and appLabel from the
269      * supplied APK file
270      */
271     @JvmStatic
getAppSnippetnull272     fun getAppSnippet(context: Context, pkgInfo: PackageInfo, sourceFile: File): AppSnippet {
273         pkgInfo.applicationInfo?.let {
274             val appInfoFromFile = processAppInfoForFile(it, sourceFile)
275             val label = getAppLabelFromFile(context, appInfoFromFile)
276             val icon = getAppIconFromFile(context, appInfoFromFile)
277             return AppSnippet(label, icon)
278         } ?: run {
279             return AppSnippet(pkgInfo.packageName, context.packageManager.defaultActivityIcon)
280         }
281     }
282 
283     /**
284      * Utility method to load application label
285      *
286      * @param context context of package that can load the resources
287      * @param appInfo ApplicationInfo object of package whose resources are to be loaded
288      */
getAppLabelFromFilenull289     private fun getAppLabelFromFile(context: Context, appInfo: ApplicationInfo): CharSequence? {
290         val pm = context.packageManager
291         var label: CharSequence? = null
292         // Try to load the label from the package's resources. If an app has not explicitly
293         // specified any label, just use the package name.
294         if (appInfo.labelRes != 0) {
295             try {
296                 label = appInfo.loadLabel(pm)
297             } catch (e: Resources.NotFoundException) {
298             }
299         }
300         if (label == null) {
301             label = if (appInfo.nonLocalizedLabel != null) appInfo.nonLocalizedLabel
302             else appInfo.packageName
303         }
304         return label
305     }
306 
307     /**
308      * Utility method to load application icon
309      *
310      * @param context context of package that can load the resources
311      * @param appInfo ApplicationInfo object of package whose resources are to be loaded
312      */
getAppIconFromFilenull313     private fun getAppIconFromFile(context: Context, appInfo: ApplicationInfo): Drawable? {
314         val pm = context.packageManager
315         var icon: Drawable? = null
316         // Try to load the icon from the package's resources. If an app has not explicitly
317         // specified any resource, just use the default icon for now.
318         try {
319             if (appInfo.icon != 0) {
320                 try {
321                     icon = appInfo.loadIcon(pm)
322                 } catch (e: Resources.NotFoundException) {
323                 }
324             }
325             if (icon == null) {
326                 icon = context.packageManager.defaultActivityIcon
327             }
328         } catch (e: OutOfMemoryError) {
329             Log.i(LOG_TAG, "Could not load app icon", e)
330         }
331         return icon
332     }
333 
processAppInfoForFilenull334     private fun processAppInfoForFile(appInfo: ApplicationInfo, sourceFile: File): ApplicationInfo {
335         val archiveFilePath = sourceFile.absolutePath
336         appInfo.publicSourceDir = archiveFilePath
337         if (appInfo.splitNames != null && appInfo.splitSourceDirs == null) {
338             val files = sourceFile.parentFile?.listFiles()
339             val splits = appInfo.splitNames!!
340                 .mapNotNull { findFilePath(files, "$it.apk") }
341                 .toTypedArray()
342 
343             appInfo.splitSourceDirs = splits
344             appInfo.splitPublicSourceDirs = splits
345         }
346         return appInfo
347     }
348 
findFilePathnull349     private fun findFilePath(files: Array<File>?, postfix: String): String? {
350         files?.let {
351             for (file in it) {
352                 val path = file.absolutePath
353                 if (path.endsWith(postfix)) {
354                     return path
355                 }
356             }
357         }
358         return null
359     }
360 
361     /**
362      * @return the packageName corresponding to a UID.
363      */
364     @JvmStatic
getPackageNameForUidnull365     fun getPackageNameForUid(context: Context, uid: Int, preferredPkgName: String?): String? {
366         if (uid == Process.INVALID_UID) {
367             return null
368         }
369         // If the sourceUid belongs to the system downloads provider, we explicitly return the
370         // name of the Download Manager package. This is because its UID is shared with multiple
371         // packages, resulting in uncertainty about which package will end up first in the list
372         // of packages associated with this UID
373         val pm = context.packageManager
374         val systemDownloadProviderInfo = getSystemDownloadsProviderInfo(pm, uid)
375         if (systemDownloadProviderInfo != null) {
376             return systemDownloadProviderInfo.packageName
377         }
378 
379         val packagesForUid = pm.getPackagesForUid(uid) ?: return null
380         if (packagesForUid.size > 1) {
381             Log.i(LOG_TAG, "Multiple packages found for source uid $uid")
382             if (preferredPkgName != null) {
383                 for (packageName in packagesForUid) {
384                     if (packageName == preferredPkgName) {
385                         return packageName
386                     }
387                 }
388             }
389         }
390         return packagesForUid[0]
391     }
392 
393     /**
394      * Utility method to get package information for a given [File]
395      */
396     @JvmStatic
getPackageInfonull397     fun getPackageInfo(context: Context, sourceFile: File, flags: Int): PackageInfo? {
398         var filePath = sourceFile.absolutePath
399         if (filePath.endsWith(SPLIT_BASE_APK_SUFFIX)) {
400             val dir = sourceFile.parentFile
401             if ((dir?.listFiles()?.size ?: 0) > 1) {
402                 // split apks, use file directory to get archive info
403                 filePath = dir.path
404             }
405         }
406         return try {
407             context.packageManager.getPackageArchiveInfo(filePath, flags)
408         } catch (ignored: Exception) {
409             null
410         }
411     }
412 
413     /**
414      * Is a profile part of a user?
415      *
416      * @param userManager The user manager
417      * @param userHandle The handle of the user
418      * @param profileHandle The handle of the profile
419      *
420      * @return If the profile is part of the user or the profile parent of the user
421      */
422     @JvmStatic
isProfileOfOrSamenull423     fun isProfileOfOrSame(
424         userManager: UserManager,
425         userHandle: UserHandle,
426         profileHandle: UserHandle?,
427     ): Boolean {
428         if (profileHandle == null) {
429             return false
430         }
431         return if (userHandle == profileHandle) {
432             true
433         } else userManager.getProfileParent(profileHandle) != null
434             && userManager.getProfileParent(profileHandle) == userHandle
435     }
436 
437     /**
438      * The class to hold an incoming package's icon and label.
439      * See [getAppSnippet]
440      */
441     data class AppSnippet(var label: CharSequence?, var icon: Drawable?) {
toStringnull442         override fun toString(): String {
443             return "AppSnippet[label = $label, hasIcon = ${icon != null}]"
444         }
445     }
446 }
447