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