| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tools.lint.checks |
| |
| import com.android.SdkConstants.ANDROID_URI |
| import com.android.SdkConstants.ATTR_NAME |
| import com.android.SdkConstants.TAG_USES_PERMISSION |
| import com.android.sdklib.AndroidVersion |
| import com.android.tools.lint.detector.api.Category |
| import com.android.tools.lint.detector.api.Detector |
| import com.android.tools.lint.detector.api.Implementation |
| import com.android.tools.lint.detector.api.Issue |
| import com.android.tools.lint.detector.api.JavaContext |
| import com.android.tools.lint.detector.api.Scope |
| import com.android.tools.lint.detector.api.Severity |
| import com.android.tools.lint.detector.api.SourceCodeScanner |
| import com.android.tools.lint.detector.api.XmlContext |
| import com.android.tools.lint.detector.api.XmlScanner |
| import com.android.utils.XmlUtils |
| import com.intellij.psi.PsiMethod |
| import org.jetbrains.uast.UCallExpression |
| import org.w3c.dom.Element |
| import java.util.EnumSet |
| |
| /** |
| * Android 11 introduces new app visibility restrictions: apps must declare extra permissions |
| * when they want to inspect other apps on the device. This detector helps increase visibility |
| * into the new restrictions. |
| */ |
| class PackageVisibilityDetector : Detector(), XmlScanner, SourceCodeScanner { |
| private var cachedQueryPermissions: QueryPermissions? = null |
| |
| private data class QueryPermissions( |
| val canQuerySomePackages: Boolean, |
| val canQueryAllPackages: Boolean |
| ) |
| |
| // ---- Implements XmlScanner ---- |
| // Checks for usage of the QUERY_ALL_PACKAGES permission (discouraged for most apps). |
| |
| override fun getApplicableElements(): Collection<String> = listOf(TAG_USES_PERMISSION) |
| |
| override fun visitElement(context: XmlContext, element: Element) { |
| if (context.mainProject.targetSdk < INITIAL_API) return |
| if (element.tagName != TAG_USES_PERMISSION) return |
| val permission = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME) ?: return |
| if (permission.value == "android.permission.QUERY_ALL_PACKAGES") { |
| context.report( |
| QUERY_ALL_PACKAGES_PERMISSION, |
| context.getLocation(permission), |
| """ |
| A `<queries>` declaration should generally be used instead of QUERY_ALL_PACKAGES; \ |
| see https://g.co/dev/packagevisibility for details |
| """.trimIndent() |
| ) |
| } |
| } |
| |
| // ---- Implements SourceCodeScanner ---- |
| // Checks for usage of APIs requiring app visibility permissions. |
| |
| override fun getApplicableMethodNames(): List<String> { |
| return listOf( |
| // PackageManager. |
| "getInstalledPackages", |
| "getInstalledApplications", |
| "queryBroadcastReceivers", |
| "queryContentProviders", |
| "queryIntentServices", |
| "queryIntentActivities", |
| // Intent. |
| "resolveActivity", |
| "resolveActivityInfo" |
| ) |
| } |
| |
| override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { |
| if (context.mainProject.targetSdk < INITIAL_API) return |
| if (!context.isEnabled(QUERY_PERMISSIONS_NEEDED)) return |
| |
| val methodName = node.methodName |
| val intendedOwner = when (methodName) { |
| // PackageManager. |
| "getInstalledPackages", |
| "getInstalledApplications", |
| "queryBroadcastReceivers", |
| "queryContentProviders", |
| "queryIntentServices", |
| "queryIntentActivities" -> "android.content.pm.PackageManager" |
| // Intent. |
| "resolveActivity", |
| "resolveActivityInfo" -> "android.content.Intent" |
| else -> error("Unexpected method name: $methodName") |
| } |
| if (!context.evaluator.isMemberInSubClassOf(method, intendedOwner)) return |
| |
| val permissions = getQueryPermissions(context) ?: return |
| |
| if (methodName == "getInstalledPackages" || methodName == "getInstalledApplications") { |
| // Special case: these methods generally imply the ability to query *all* packages. |
| if (!permissions.canQueryAllPackages) { |
| context.report( |
| QUERY_PERMISSIONS_NEEDED, |
| context.getLocation(node.methodIdentifier ?: node), |
| """ |
| As of Android 11, this method no longer returns information about all apps; \ |
| see https://g.co/dev/packagevisibility for details |
| """.trimIndent() |
| ) |
| } |
| } else if (!permissions.canQuerySomePackages) { |
| context.report( |
| QUERY_PERMISSIONS_NEEDED, |
| context.getLocation(node.methodIdentifier ?: node), |
| """ |
| Consider adding a `<queries>` declaration to your manifest when calling this \ |
| method; see https://g.co/dev/packagevisibility for details |
| """.trimIndent() |
| ) |
| } |
| } |
| |
| private fun getQueryPermissions(context: JavaContext): QueryPermissions? { |
| cachedQueryPermissions?.let { return it } |
| |
| val manifest = context.mainProject.mergedManifest ?: return null |
| var canQuerySomePackages = false |
| var canQueryAllPackages = false |
| |
| for (tag in XmlUtils.getSubTags(manifest.documentElement)) { |
| when (tag.nodeName) { |
| "queries" -> { |
| canQuerySomePackages = true |
| } |
| TAG_USES_PERMISSION -> { |
| val permission = tag.getAttributeNodeNS(ANDROID_URI, ATTR_NAME) |
| if (permission?.value == "android.permission.QUERY_ALL_PACKAGES") { |
| canQuerySomePackages = true |
| canQueryAllPackages = true |
| } |
| } |
| } |
| } |
| |
| return QueryPermissions(canQuerySomePackages, canQueryAllPackages) |
| .also { cachedQueryPermissions = it } |
| } |
| |
| companion object { |
| /** The API version in which package visibility restrictions were introduced. */ |
| private const val INITIAL_API = AndroidVersion.VersionCodes.R |
| |
| @JvmField |
| val QUERY_ALL_PACKAGES_PERMISSION = Issue.create( |
| id = "QueryAllPackagesPermission", |
| briefDescription = "Using the QUERY_ALL_PACKAGES permission", |
| explanation = """ |
| If you need to query or interact with other installed apps, you should be using a \ |
| `<queries>` declaration in your manifest. Using the QUERY_ALL_PACKAGES permission in \ |
| order to see all installed apps is rarely necessary, and most apps on Google Play are \ |
| not allowed to have this permission. |
| """, |
| category = Category.COMPLIANCE, |
| priority = 8, |
| severity = Severity.ERROR, |
| implementation = Implementation( |
| PackageVisibilityDetector::class.java, |
| Scope.MANIFEST_SCOPE |
| ), |
| androidSpecific = true, |
| moreInfo = "https://g.co/dev/packagevisibility" |
| ) |
| |
| @JvmField |
| val QUERY_PERMISSIONS_NEEDED = Issue.create( |
| id = "QueryPermissionsNeeded", |
| briefDescription = "Using APIs affected by query permissions", |
| explanation = """ |
| Apps that target Android 11 cannot query or interact with other installed apps \ |
| by default. If you need to query or interact with other installed apps, you may need \ |
| to add a `<queries>` declaration in your manifest. |
| |
| As a corollary, the methods `PackageManager#getInstalledPackages` and \ |
| `PackageManager#getInstalledApplications` will no longer return information about all \ |
| installed apps. To query specific apps or types of apps, you can use methods like \ |
| `PackageManager#getPackageInfo` or `PackageManager#queryIntentActivities`. |
| """, |
| category = Category.CORRECTNESS, |
| priority = 5, |
| severity = Severity.WARNING, |
| implementation = Implementation( |
| PackageVisibilityDetector::class.java, |
| EnumSet.of(Scope.JAVA_FILE, Scope.MANIFEST), |
| Scope.JAVA_FILE_SCOPE |
| ), |
| androidSpecific = true, |
| moreInfo = "https://g.co/dev/packagevisibility" |
| ) |
| } |
| } |