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 who's permission is being checked
132      * @param originatingUid the UID from where install is being originated. This could be same as
133      * callingUid or it will be the UID of the package performing a session based install
134      * @param isTrustedSource whether install request is coming from a privileged app or an app that
135      * has [Manifest.permission.INSTALL_PACKAGES] permission granted
136      * @return `true` if the package is granted the said permission
137      */
138     @JvmStatic
isInstallPermissionGrantedOrRequestednull139     fun isInstallPermissionGrantedOrRequested(
140         context: Context,
141         callingUid: Int,
142         originatingUid: Int,
143         isTrustedSource: Boolean,
144     ): Boolean {
145         val isDocumentsManager =
146             isPermissionGranted(context, Manifest.permission.MANAGE_DOCUMENTS, callingUid)
147         val isSystemDownloadsProvider =
148             getSystemDownloadsProviderInfo(context.packageManager, callingUid) != null
149 
150         if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager) {
151             val targetSdkVersion = getMaxTargetSdkVersionForUid(context, originatingUid)
152             if (targetSdkVersion < 0) {
153                 // Invalid originating uid supplied. Abort install.
154                 Log.w(LOG_TAG, "Cannot get target sdk version for uid $originatingUid")
155                 return false
156             } else if (targetSdkVersion >= Build.VERSION_CODES.O
157                 && !isUidRequestingPermission(
158                     context.packageManager, originatingUid,
159                     Manifest.permission.REQUEST_INSTALL_PACKAGES
160                 )
161             ) {
162                 Log.e(
163                     LOG_TAG, "Requesting uid " + originatingUid + " 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, originatingUid: Int, sessionId: Int): Boolean {
208         if (originatingUid == Process.ROOT_UID) {
209             return true
210         }
211         val sessionInfo = pi.getSessionInfo(sessionId) ?: return false
212         val installerUid = sessionInfo.getInstallerUid()
213         return originatingUid == 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, sourceUid: Int, callingPackage: String?): String? {
366         if (sourceUid == 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, sourceUid)
375         if (systemDownloadProviderInfo != null) {
376             return systemDownloadProviderInfo.packageName
377         }
378         val packagesForUid = pm.getPackagesForUid(sourceUid) ?: return null
379         if (packagesForUid.size > 1) {
380             if (callingPackage != null) {
381                 for (packageName in packagesForUid) {
382                     if (packageName == callingPackage) {
383                         return packageName
384                     }
385                 }
386             }
387             Log.i(LOG_TAG, "Multiple packages found for source uid $sourceUid")
388         }
389         return packagesForUid[0]
390     }
391 
392     /**
393      * Utility method to get package information for a given [File]
394      */
395     @JvmStatic
getPackageInfonull396     fun getPackageInfo(context: Context, sourceFile: File, flags: Int): PackageInfo? {
397         var filePath = sourceFile.absolutePath
398         if (filePath.endsWith(SPLIT_BASE_APK_SUFFIX)) {
399             val dir = sourceFile.parentFile
400             if ((dir?.listFiles()?.size ?: 0) > 1) {
401                 // split apks, use file directory to get archive info
402                 filePath = dir.path
403             }
404         }
405         return try {
406             context.packageManager.getPackageArchiveInfo(filePath, flags)
407         } catch (ignored: Exception) {
408             null
409         }
410     }
411 
412     /**
413      * Is a profile part of a user?
414      *
415      * @param userManager The user manager
416      * @param userHandle The handle of the user
417      * @param profileHandle The handle of the profile
418      *
419      * @return If the profile is part of the user or the profile parent of the user
420      */
421     @JvmStatic
isProfileOfOrSamenull422     fun isProfileOfOrSame(
423         userManager: UserManager,
424         userHandle: UserHandle,
425         profileHandle: UserHandle?,
426     ): Boolean {
427         if (profileHandle == null) {
428             return false
429         }
430         return if (userHandle == profileHandle) {
431             true
432         } else userManager.getProfileParent(profileHandle) != null
433             && userManager.getProfileParent(profileHandle) == userHandle
434     }
435 
436     /**
437      * The class to hold an incoming package's icon and label.
438      * See [getAppSnippet]
439      */
440     data class AppSnippet(var label: CharSequence?, var icon: Drawable?) {
toStringnull441         override fun toString(): String {
442             return "AppSnippet[label = ${label}, hasIcon = ${icon != null}]"
443         }
444     }
445 }
446