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