• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.annotation.SuppressLint
21 import android.app.ActivityManager
22 import android.content.Context
23 import android.content.pm.ApplicationInfo
24 import android.content.pm.PackageInfo
25 import android.content.pm.PackageInstaller
26 import android.content.pm.PackageManager
27 import android.content.res.Resources
28 import android.graphics.Bitmap
29 import android.graphics.BitmapFactory
30 import android.graphics.Canvas
31 import android.graphics.drawable.BitmapDrawable
32 import android.graphics.drawable.Drawable
33 import android.net.Uri
34 import android.os.Build
35 import android.os.Parcel
36 import android.os.Parcelable
37 import android.os.Process
38 import android.os.UserHandle
39 import android.os.UserManager
40 import android.util.Log
41 import com.android.packageinstaller.v2.model.PackageUtil.getAppSnippet
42 import java.io.ByteArrayOutputStream
43 import java.io.File
44 import java.nio.file.Files
45 import java.nio.file.Path
46 import kotlinx.parcelize.Parceler
47 import kotlinx.parcelize.Parcelize
48 
49 object PackageUtil {
50     private val LOG_TAG = InstallRepository::class.java.simpleName
51     private const val DOWNLOADS_AUTHORITY = "downloads"
52     private const val SPLIT_BASE_APK_SUFFIX = "base.apk"
53     private const val SPLIT_APK_SUFFIX = ".apk"
54     const val localLogv = false
55 
56     const val ARGS_ABORT_REASON: String = "abort_reason"
57     const val ARGS_ACTION_REASON: String = "action_reason"
58     const val ARGS_ACTIVITY_RESULT_CODE: String = "activity_result_code"
59     const val ARGS_APP_DATA_SIZE: String = "app_data_size"
60     const val ARGS_APP_LABEL: String = "app_label"
61     const val ARGS_APP_SNIPPET: String = "app_snippet"
62     const val ARGS_ERROR_DIALOG_TYPE: String = "error_dialog_type"
63     const val ARGS_IS_ARCHIVE: String = "is_archive"
64     const val ARGS_IS_CLONE_USER: String = "clone_user"
65     const val ARGS_IS_UPDATING: String = "is_updating"
66     const val ARGS_LEGACY_CODE: String = "legacy_code"
67     const val ARGS_MESSAGE: String = "message"
68     const val ARGS_RESULT_INTENT: String = "result_intent"
69     const val ARGS_SHOULD_RETURN_RESULT: String = "should_return_result"
70     const val ARGS_SOURCE_APP: String = "source_app"
71     const val ARGS_STATUS_CODE: String = "status_code"
72     const val ARGS_TITLE: String = "title"
73 
74     /**
75      * Determines if the UID belongs to the system downloads provider and returns the
76      * [ApplicationInfo] of the provider
77      *
78      * @param uid UID of the caller
79      * @return [ApplicationInfo] of the provider if a downloads provider exists, it is a
80      * system app, and its UID matches with the passed UID, null otherwise.
81      */
82     private fun getSystemDownloadsProviderInfo(pm: PackageManager, uid: Int): ApplicationInfo? {
83         // Check if there are currently enabled downloads provider on the system.
84         val providerInfo = pm.resolveContentProvider(DOWNLOADS_AUTHORITY, 0)
85             ?: return null
86         val appInfo = providerInfo.applicationInfo
87         return if ((appInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) && uid == appInfo.uid) {
88             appInfo
89         } else null
90     }
91 
92     /**
93      * Get the maximum target sdk for a UID.
94      *
95      * @param context The context to use
96      * @param uid The UID requesting the install/uninstall
97      * @return The maximum target SDK or -1 if the uid does not match any packages.
98      */
99     @JvmStatic
100     fun getMaxTargetSdkVersionForUid(context: Context, uid: Int): Int {
101         val pm = context.packageManager
102         val packages = pm.getPackagesForUid(uid)
103         var targetSdkVersion = -1
104         if (packages != null) {
105             for (packageName in packages) {
106                 try {
107                     val info = pm.getApplicationInfo(packageName!!, 0)
108                     targetSdkVersion = maxOf(targetSdkVersion, info.targetSdkVersion)
109                 } catch (e: PackageManager.NameNotFoundException) {
110                     // Ignore and try the next package
111                 }
112             }
113         }
114         return targetSdkVersion
115     }
116 
117     @JvmStatic
118     fun canPackageQuery(context: Context, callingUid: Int, packageUri: Uri): Boolean {
119         val pm = context.packageManager
120         val info = pm.resolveContentProvider(
121             packageUri.authority!!,
122             PackageManager.ComponentInfoFlags.of(0)
123         ) ?: return false
124         val targetPackage = info.packageName
125         val callingPackages = pm.getPackagesForUid(callingUid) ?: return false
126         for (callingPackage in callingPackages) {
127             try {
128                 if (pm.canPackageQuery(callingPackage!!, targetPackage)) {
129                     return true
130                 }
131             } catch (e: PackageManager.NameNotFoundException) {
132                 // no-op
133             }
134         }
135         return false
136     }
137 
138     /**
139      * @param context the [Context] object
140      * @param permission the permission name to check
141      * @param callingUid the UID of the caller who's permission is being checked
142      * @return `true` if the callingUid is granted the said permission
143      */
144     @JvmStatic
145     fun isPermissionGranted(context: Context, permission: String, callingUid: Int): Boolean {
146         return (context.checkPermission(permission, -1, callingUid)
147             == PackageManager.PERMISSION_GRANTED)
148     }
149 
150     /**
151      * @param pm the [PackageManager] object
152      * @param permission the permission name to check
153      * @param packageName the name of the package who's permission is being checked
154      * @return `true` if the package is granted the said permission
155      */
156     @JvmStatic
157     fun isPermissionGranted(pm: PackageManager, permission: String, packageName: String): Boolean {
158         return pm.checkPermission(permission, packageName) == PackageManager.PERMISSION_GRANTED
159     }
160 
161     /**
162      * @param context the [Context] object
163      * @param callingUid the UID of the caller of Pia
164      * @param isTrustedSource indicates whether install request is coming from a privileged app
165      * that has passed EXTRA_NOT_UNKNOWN_SOURCE as `true` in the installation intent, or an app that
166      * has the [INSTALL_PACKAGES][Manifest.permission.INSTALL_PACKAGES] permission granted.
167      *
168      * @return `true` if the package is either a system downloads provider, a document manager,
169      * a trusted source, or has declared the
170      * [REQUEST_INSTALL_PACKAGES][Manifest.permission.REQUEST_INSTALL_PACKAGES] in its manifest.
171      */
172     @JvmStatic
173     fun isInstallPermissionGrantedOrRequested(
174         context: Context,
175         callingUid: Int,
176         isTrustedSource: Boolean,
177     ): Boolean {
178         val isDocumentsManager =
179             isPermissionGranted(context, Manifest.permission.MANAGE_DOCUMENTS, callingUid)
180         val isSystemDownloadsProvider =
181             getSystemDownloadsProviderInfo(context.packageManager, callingUid) != null
182 
183         if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager) {
184             val targetSdkVersion = getMaxTargetSdkVersionForUid(context, callingUid)
185             if (targetSdkVersion < 0) {
186                 // Invalid calling uid supplied. Abort install.
187                 Log.e(LOG_TAG, "Cannot get target SDK version for uid $callingUid")
188                 return false
189             } else if (targetSdkVersion >= Build.VERSION_CODES.O
190                 && !isUidRequestingPermission(
191                     context.packageManager, callingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES
192                 )
193             ) {
194                 Log.e(
195                     LOG_TAG, "Requesting uid " + callingUid + " needs to declare permission "
196                         + Manifest.permission.REQUEST_INSTALL_PACKAGES
197                 )
198                 return false
199             }
200         }
201         return true
202     }
203 
204     /**
205      * @param pm the [PackageManager] object
206      * @param uid the UID of the caller who's permission is being checked
207      * @param permission the permission name to check
208      * @return `true` if the caller is requesting the said permission in its Manifest
209      */
210     private fun isUidRequestingPermission(
211         pm: PackageManager,
212         uid: Int,
213         permission: String,
214     ): Boolean {
215         val packageNames = pm.getPackagesForUid(uid) ?: return false
216         for (packageName in packageNames) {
217             val packageInfo: PackageInfo = try {
218                 pm.getPackageInfo(packageName!!, PackageManager.GET_PERMISSIONS)
219             } catch (e: PackageManager.NameNotFoundException) {
220                 // Ignore and try the next package
221                 continue
222             }
223             if (packageInfo.requestedPermissions != null
224                 && listOf(*packageInfo.requestedPermissions!!).contains(permission)
225             ) {
226                 return true
227             }
228         }
229         return false
230     }
231 
232     /**
233      * @param pi the [PackageInstaller] object to use
234      * @param originatingUid the UID of the package performing a session based install
235      * @param sessionId ID of the install session
236      * @return `true` if the caller is the session owner
237      */
238     @JvmStatic
239     fun isCallerSessionOwner(pi: PackageInstaller, callingUid: Int, sessionId: Int): Boolean {
240         if (callingUid == Process.ROOT_UID) {
241             return true
242         }
243         val sessionInfo = pi.getSessionInfo(sessionId) ?: return false
244         val installerUid = sessionInfo.getInstallerUid()
245         return callingUid == installerUid
246     }
247 
248     /**
249      * Generates a stub [PackageInfo] object for the given packageName
250      */
251     @JvmStatic
252     fun generateStubPackageInfo(packageName: String?): PackageInfo {
253         val info = PackageInfo()
254         val aInfo = ApplicationInfo()
255         info.applicationInfo = aInfo
256         info.applicationInfo!!.packageName = packageName
257         info.packageName = info.applicationInfo!!.packageName
258         return info
259     }
260 
261     /**
262      * Generates an [AppSnippet] containing an appIcon and appLabel from the
263      * [PackageInstaller.SessionInfo] object
264      */
265     @JvmStatic
266     fun getAppSnippet(context: Context, info: PackageInstaller.SessionInfo): AppSnippet {
267         val pm = context.packageManager
268         val label = info.getAppLabel()
269         val icon = if (info.getAppIcon() != null) BitmapDrawable(
270             context.resources,
271             info.getAppIcon()
272         ) else pm.defaultActivityIcon
273         val largeIconSize = getLargeIconSize(context)
274         return AppSnippet(label, icon, largeIconSize)
275     }
276 
277     /**
278      * Generates an [AppSnippet] containing an appIcon and appLabel from the
279      * [PackageInfo] object
280      */
281     @JvmStatic
282     fun getAppSnippet(context: Context, pkgInfo: PackageInfo): AppSnippet {
283         val largeIconSize = getLargeIconSize(context)
284         return pkgInfo.applicationInfo?.let { getAppSnippet(context, it) } ?: run {
285             AppSnippet(
286                 pkgInfo.packageName, context.packageManager.defaultActivityIcon, largeIconSize
287             )
288         }
289     }
290 
291     /**
292      * Generates an [AppSnippet] containing an appIcon and appLabel from the
293      * [ApplicationInfo] object
294      */
295     @JvmStatic
296     fun getAppSnippet(context: Context, appInfo: ApplicationInfo): AppSnippet {
297         val pm = context.packageManager
298         val label = pm.getApplicationLabel(appInfo)
299         val icon = pm.getApplicationIcon(appInfo)
300         val largeIconSize = getLargeIconSize(context)
301         return AppSnippet(label, icon, largeIconSize)
302     }
303 
304     /**
305      * Generates an [AppSnippet] containing an appIcon and appLabel from the
306      * supplied APK file
307      */
308     @JvmStatic
309     fun getAppSnippet(context: Context, pkgInfo: PackageInfo, sourceFile: File): AppSnippet {
310         val largeIconSize = getLargeIconSize(context)
311         pkgInfo.applicationInfo?.let {
312             val appInfoFromFile = processAppInfoForFile(it, sourceFile)
313             val label = getAppLabelFromFile(context, appInfoFromFile)
314             val icon = getAppIconFromFile(context, appInfoFromFile)
315             return AppSnippet(label, icon, largeIconSize)
316         } ?: run {
317             return AppSnippet(
318                 pkgInfo.packageName, context.packageManager.defaultActivityIcon, largeIconSize
319             )
320         }
321     }
322 
323     private fun getLargeIconSize(context: Context): Int {
324         val am = context.getSystemService<ActivityManager>(ActivityManager::class.java)
325         return am.launcherLargeIconSize
326     }
327 
328     /**
329      * Utility method to load application label
330      *
331      * @param context context of package that can load the resources
332      * @param appInfo ApplicationInfo object of package whose resources are to be loaded
333      */
334     private fun getAppLabelFromFile(context: Context, appInfo: ApplicationInfo): CharSequence? {
335         val pm = context.packageManager
336         var label: CharSequence? = null
337         // Try to load the label from the package's resources. If an app has not explicitly
338         // specified any label, just use the package name.
339         if (appInfo.labelRes != 0) {
340             try {
341                 label = appInfo.loadLabel(pm)
342             } catch (e: Resources.NotFoundException) {
343             }
344         }
345         if (label == null) {
346             label = if (appInfo.nonLocalizedLabel != null) appInfo.nonLocalizedLabel
347             else appInfo.packageName
348         }
349         return label
350     }
351 
352     /**
353      * Utility method to load application icon
354      *
355      * @param context context of package that can load the resources
356      * @param appInfo ApplicationInfo object of package whose resources are to be loaded
357      */
358     private fun getAppIconFromFile(context: Context, appInfo: ApplicationInfo): Drawable? {
359         val pm = context.packageManager
360         var icon: Drawable? = null
361         // Try to load the icon from the package's resources. If an app has not explicitly
362         // specified any resource, just use the default icon for now.
363         try {
364             if (appInfo.icon != 0) {
365                 try {
366                     icon = appInfo.loadIcon(pm)
367                 } catch (e: Resources.NotFoundException) {
368                 }
369             }
370             if (icon == null) {
371                 icon = context.packageManager.defaultActivityIcon
372             }
373         } catch (e: OutOfMemoryError) {
374             Log.i(LOG_TAG, "Could not load app icon", e)
375         }
376         return icon
377     }
378 
379     private fun processAppInfoForFile(appInfo: ApplicationInfo, sourceFile: File): ApplicationInfo {
380         val archiveFilePath = sourceFile.absolutePath
381         appInfo.publicSourceDir = archiveFilePath
382         if (appInfo.splitNames != null && appInfo.splitSourceDirs == null) {
383             val files = sourceFile.parentFile?.listFiles()
384             val splits = appInfo.splitNames!!
385                 .mapNotNull { findFilePath(files, "$it.apk") }
386                 .toTypedArray()
387 
388             appInfo.splitSourceDirs = splits
389             appInfo.splitPublicSourceDirs = splits
390         }
391         return appInfo
392     }
393 
394     private fun findFilePath(files: Array<File>?, postfix: String): String? {
395         files?.let {
396             for (file in it) {
397                 val path = file.absolutePath
398                 if (path.endsWith(postfix)) {
399                     return path
400                 }
401             }
402         }
403         return null
404     }
405 
406     /**
407      * @return the packageName corresponding to a UID.
408      */
409     @JvmStatic
410     fun getPackageNameForUid(context: Context, uid: Int, preferredPkgName: String?): String? {
411         if (uid == Process.INVALID_UID) {
412             return null
413         }
414         // If the sourceUid belongs to the system downloads provider, we explicitly return the
415         // name of the Download Manager package. This is because its UID is shared with multiple
416         // packages, resulting in uncertainty about which package will end up first in the list
417         // of packages associated with this UID
418         val pm = context.packageManager
419         val systemDownloadProviderInfo = getSystemDownloadsProviderInfo(pm, uid)
420         if (systemDownloadProviderInfo != null) {
421             return systemDownloadProviderInfo.packageName
422         }
423 
424         val packagesForUid = pm.getPackagesForUid(uid) ?: return null
425         if (packagesForUid.size > 1) {
426             Log.i(LOG_TAG, "Multiple packages found for source uid $uid")
427             if (preferredPkgName != null) {
428                 for (packageName in packagesForUid) {
429                     if (packageName == preferredPkgName) {
430                         return packageName
431                     }
432                 }
433             }
434         }
435         return packagesForUid[0]
436     }
437 
438     /**
439      * Utility method to get package information for a given [File]
440      */
441     @JvmStatic
442     fun getPackageInfo(context: Context, sourceFile: File, flags: Int): PackageInfo? {
443         var filePath = sourceFile.absolutePath
444         if (filePath.endsWith(SPLIT_BASE_APK_SUFFIX)) {
445             val dir = sourceFile.parentFile
446             try {
447                 Files.list(dir.toPath()).use { list ->
448                     val count: Long = list
449                         .filter { name: Path -> name.endsWith(SPLIT_APK_SUFFIX) }
450                         .limit(2)
451                         .count()
452                     if (count > 1) {
453                         // split apks, use file directory to get archive info
454                         filePath = dir.path
455                     }
456                 }
457             } catch (ignored: Exception) {
458                 // No access to the parent directory, proceed to read app snippet
459                 // from the base apk only
460             }
461         }
462         return try {
463             context.packageManager.getPackageArchiveInfo(filePath, flags)
464         } catch (ignored: Exception) {
465             null
466         }
467     }
468 
469     /**
470      * Is a profile part of a user?
471      *
472      * @param userManager The user manager
473      * @param userHandle The handle of the user
474      * @param profileHandle The handle of the profile
475      *
476      * @return If the profile is part of the user or the profile parent of the user
477      */
478     @JvmStatic
479     fun isProfileOfOrSame(
480         userManager: UserManager,
481         userHandle: UserHandle,
482         profileHandle: UserHandle?,
483     ): Boolean {
484         if (profileHandle == null) {
485             return false
486         }
487         return if (userHandle == profileHandle) {
488             true
489         } else userManager.getProfileParent(profileHandle) != null
490             && userManager.getProfileParent(profileHandle) == userHandle
491     }
492 
493     /**
494      * The class to hold an incoming package's icon and label.
495      * See [getAppSnippet]
496      */
497     @Parcelize
498     data class AppSnippet(
499         var label: CharSequence?,
500         var icon: Drawable?,
501         var iconSize: Int,
502     ) : Parcelable {
503         private companion object : Parceler<AppSnippet> {
504             override fun AppSnippet.write(dest: Parcel, flags: Int) {
505                 dest.writeString(label.toString())
506 
507                 val bmp = getBitmapFromDrawable(icon!!)
508                 dest.writeBlob(getBytesFromBitmap(bmp))
509                 bmp.recycle()
510 
511                 dest.writeInt(iconSize)
512             }
513 
514             @SuppressLint("UseKtx")
515             override fun create(parcel: Parcel): AppSnippet {
516                 val label = parcel.readString()
517 
518                 val b: ByteArray = parcel.readBlob()!!
519                 val bmp: Bitmap? = BitmapFactory.decodeByteArray(b, 0, b.size)
520                 val icon = BitmapDrawable(Resources.getSystem(), bmp)
521 
522                 val iconSize = parcel.readInt()
523 
524                 return AppSnippet(label.toString(), icon, iconSize)
525             }
526         }
527 
528         @SuppressLint("UseKtx")
529         private fun getBitmapFromDrawable(drawable: Drawable): Bitmap {
530             // Create an empty bitmap with the dimensions of our drawable
531             val bmp = Bitmap.createBitmap(
532                 drawable.intrinsicWidth,
533                 drawable.intrinsicHeight, Bitmap.Config.ARGB_8888
534             )
535             // Associate it with a canvas. This canvas will draw the icon on the bitmap
536             val canvas = Canvas(bmp)
537             // Draw the drawable in the canvas. The canvas will ultimately paint the drawable in the
538             // bitmap held within
539             drawable.draw(canvas)
540 
541             // Scale it down if the icon is too large
542             if ((bmp.getWidth() > iconSize * 2) || (bmp.getHeight() > iconSize * 2)) {
543                 val scaledBitmap = Bitmap.createScaledBitmap(bmp, iconSize, iconSize, true)
544                 if (scaledBitmap != bmp) {
545                     bmp.recycle()
546                 }
547                 return scaledBitmap
548             }
549             return bmp
550         }
551 
552         private fun getBytesFromBitmap(bmp: Bitmap): ByteArray? {
553             var baos = ByteArrayOutputStream()
554             baos.use {
555                 bmp.compress(Bitmap.CompressFormat.PNG, 100, it)
556             }
557             return baos.toByteArray()
558         }
559 
560         override fun toString(): String {
561             return "AppSnippet[label = $label, hasIcon = ${icon != null}]"
562         }
563     }
564 }
565