blob: f56702b31c9272820185fb18e8975f476aa2f9fd [file] [log] [blame]
/*
* Copyright (C) 2018 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
import com.android.SdkConstants.ANDROID_PKG
import com.android.SdkConstants.ANDROID_URI
import com.android.SdkConstants.ATTR_NAME
import com.android.SdkConstants.ATTR_THEME
import com.android.SdkConstants.CLASS_ACTIVITY
import com.android.SdkConstants.TAG_APPLICATION
import com.android.SdkConstants.TAG_ITEM
import com.android.SdkConstants.TAG_STYLE
import com.android.resources.ResourceFolderType
import com.android.resources.ResourceType
import com.android.resources.ResourceUrl
import com.android.sdklib.AndroidVersion
import com.android.tools.lint.client.api.ResourceReference
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.tools.lint.detector.api.resolveManifestName
import com.android.utils.XmlUtils
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.getParentOfType
import org.w3c.dom.Attr
import org.w3c.dom.Element
import java.util.EnumSet
const val ATTR_SCREEN_ORIENTATION = "screenOrientation"
/** Detects potential bugs such as the one described in b/b/33483680 */
class TranslucentViewDetector : Detector(), XmlScanner, SourceCodeScanner {
companion object Issues {
/** Mixing Translucency and Orientation */
@JvmField
val ISSUE = Issue.create(
id = "TranslucentOrientation",
briefDescription = "Mixing screenOrientation and translucency",
explanation = """
Specifying a fixed screen orientation with a translucent theme isn't supported \
on apps with `targetSdkVersion` O or greater since there can be an another activity \
visible behind your activity with a conflicting request.
For example, your activity requests landscape and the visible activity behind \
your translucent activity request portrait. In this case the system can only \
honor one of the requests and currently prefers to honor the request from \
non-translucent activities since there is nothing visible behind them.
Devices running platform version O or greater will throw an exception in your \
app if this state is detected.
""",
category = Category.CORRECTNESS,
priority = 8,
severity = Severity.WARNING,
implementation = Implementation(
TranslucentViewDetector::class.java,
EnumSet.of(Scope.MANIFEST, Scope.ALL_RESOURCE_FILES, Scope.JAVA_FILE)
)
)
}
private var interestingActivities: MutableList<String>? = null
private var interestingThemes: MutableList<String>? = null
private var defaultTheme: String? = null
override fun getApplicableAttributes(): Collection<String>? = listOf(ATTR_SCREEN_ORIENTATION)
override fun getApplicableElements(): Collection<String>? = listOf(TAG_STYLE)
override fun appliesTo(folderType: ResourceFolderType): Boolean {
return folderType == ResourceFolderType.VALUES
}
override fun visitAttribute(context: XmlContext, attribute: Attr) {
if (context.mainProject.targetSdk < AndroidVersion.VersionCodes.O) {
return
}
// If anyone specifies screenOrientation (other than "unspecified"), then
// write down the theme applied on this activity. (If theme is not specified,
// look it up in the activity or worst of all, use default in manifest)
if (SdkConstants.ANDROID_URI != attribute.namespaceURI) {
return
}
val value = attribute.value
if (value == "unspecified") {
return
}
val activity = attribute.ownerElement
if (activity == null || !activity.hasAttributeNS(ANDROID_URI, ATTR_NAME)) {
return
}
val name = resolveManifestName(activity)
val theme = activity.getAttributeNS(ANDROID_URI, ATTR_THEME)
if (theme.isBlank()) {
val application = activity.parentNode as? Element
defaultTheme = getThemeName(application?.getAttributeNS(ANDROID_URI, ATTR_THEME))
addActivity(name)
} else {
addTheme(getThemeName(theme))
}
}
private fun addActivity(name: String) {
val activities = interestingActivities ?: run {
val newList = mutableListOf<String>()
interestingActivities = newList
newList
}
activities.add(name)
}
private fun addTheme(theme: String?) {
theme ?: return
val themes = interestingThemes ?: run {
val newList = mutableListOf<String>()
interestingThemes = newList
newList
}
themes.add(theme)
}
private fun getThemeName(themeUrl: String?): String? {
themeUrl ?: return null
// TODO(namespaces)
val theme = ResourceUrl.parse(themeUrl)
return if (theme?.type == ResourceType.STYLE && !theme.isFramework) {
theme.name
} else {
null
}
}
override fun visitElement(context: XmlContext, element: Element) {
val themes = interestingThemes
if (themes == null && defaultTheme == null) {
return
}
if (context.mainProject.targetSdk < AndroidVersion.VersionCodes.O) {
return
}
// TODO: Need full map to chase parent pointers etc
val themeName = element.getAttribute(ATTR_NAME)
if (themeName == defaultTheme || themes != null && themes.contains(themeName)) {
// Look at children
var curr = XmlUtils.getFirstSubTagByName(element, TAG_ITEM)
while (curr != null) {
val attributeName = curr.getAttribute(ATTR_NAME)
if (attributeName == "android:windowIsFloating" ||
attributeName == "windowIsTranslucent"
) {
val attributeNode = curr.getAttributeNode(ATTR_NAME)
val location = context.getValueLocation(attributeNode)
// TODO: look up the manifest location too and attach it here
// TODO: Consider going about this from the other end: if we find
// a theme occurrence, THEN look up merged manifest to see if it's referenced
// from an interesting manifest location!
val mergedManifest = context.mainProject.mergedManifest
if (mergedManifest != null) {
val application = XmlUtils.getFirstSubTagByName(
mergedManifest.documentElement, TAG_APPLICATION
)
var currentActivity = XmlUtils.getFirstSubTag(application)
while (currentActivity != null) {
val attr = currentActivity.getAttributeNodeNS(
ANDROID_URI,
ATTR_SCREEN_ORIENTATION
)
// TODO - pick the one that doesn't specify unspecified (and ideally
// map back to the same activity that contributed the activity,
// which is why it might make sense to compute forwards instead.)
if (attr != null) {
val secondary = context.getValueLocation(attr)
location.secondary = secondary
break
}
currentActivity = XmlUtils.getNextTag(currentActivity)
}
}
val message = "Should not specify screen orientation with translucent or " +
"floating theme"
context.report(ISSUE, curr, location, message)
break
}
curr = XmlUtils.getNextTagByName(curr, TAG_ITEM)
}
}
}
// In Java/Kotlin files, look for setTheme() calls in activities listed in
// interestingActivities
override fun getApplicableMethodNames(): List<String>? {
return listOf("setTheme")
}
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
val activities = interestingActivities ?: return
val uClass = node.getParentOfType<UClass>(UClass::class.java, true) ?: return
if (!activities.contains(uClass.qualifiedName)) {
return
}
if (!context.evaluator.inheritsFrom(uClass, CLASS_ACTIVITY, false)) {
return
}
val arguments = node.valueArguments
if (arguments.size != 1) {
return
}
val reference = ResourceReference.get(arguments[0])
if (reference?.type == ResourceType.STYLE && reference.`package` != ANDROID_PKG) {
addTheme(reference.name)
// We've already processed resources, so handle it in a second pass
context.driver.requestRepeat(this, Scope.ALL_RESOURCES_SCOPE)
}
}
}