| /* |
| * 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) |
| } |
| } |
| } |