diff --git a/lint-checks-android/src/main/java/com/uber/lintchecks/android/LintRegistry.kt b/lint-checks-android/src/main/java/com/uber/lintchecks/android/LintRegistry.kt index ae492ae..6e6d87f 100644 --- a/lint-checks-android/src/main/java/com/uber/lintchecks/android/LintRegistry.kt +++ b/lint-checks-android/src/main/java/com/uber/lintchecks/android/LintRegistry.kt @@ -28,7 +28,8 @@ class LintRegistry : IssueRegistry() { XmlHardcodedColorOrDimensionDetector.ISSUE, ColorResourceUsageDetector.ISSUE, GetDrawableDetector.ISSUE, - FrameworkPairDetector.ISSUE + FrameworkPairDetector.ISSUE, + XmlWebViewInsideScrollViewDetector.ISSUE ) override val api: Int = CURRENT_API diff --git a/lint-checks-android/src/main/java/com/uber/lintchecks/android/XmlWebViewInsideScrollViewDetector.kt b/lint-checks-android/src/main/java/com/uber/lintchecks/android/XmlWebViewInsideScrollViewDetector.kt new file mode 100644 index 0000000..6272232 --- /dev/null +++ b/lint-checks-android/src/main/java/com/uber/lintchecks/android/XmlWebViewInsideScrollViewDetector.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2019. Uber Technologies + * + * 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.uber.lintchecks.android + +import com.android.SdkConstants.ANDROID_NS_NAME_PREFIX +import com.android.SdkConstants.ATTR_FILL_VIEWPORT +import com.android.resources.ResourceFolderType +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.LintFix +import com.android.tools.lint.detector.api.ResourceXmlDetector +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.XmlContext +import org.w3c.dom.Element + +/** Custom lint check to make sure that Scrollview which has WebView as its child sets fillViewport to true*/ +class XmlWebViewInsideScrollViewDetector : ResourceXmlDetector() { + companion object { + private const val ISSUE_ID = "WebViewInsideScrollview" + private const val BRIEF_DESCRIPTION = "Add android:fillViewport=true in the ScrollView to avoid unexpected behaviors in WebView" + val LINT_ERROR_MESSAGE = """ + Add android:fillViewport=true in the ScrollView to avoid unexpected behaviors in WebView. + When WebView is wrapped inside ScrollView, the WebView sometimes doesn't provide the necessary + viewport height to the webpage and a results in a zero height page.""".trimIndent().replace('\n', ' ') + val ISSUE = Issue.create( + id = ISSUE_ID, + briefDescription = BRIEF_DESCRIPTION, + explanation = LINT_ERROR_MESSAGE, + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.ERROR, + implementation = createImplementation()) + + private const val SCROLLVIEW_VIEW_CLASS_NAME = "ScrollView" + private const val SUPPORT_WIDGET_NESTED_SCROLLVIEW_CLASS_NAME = "android.support.v4.widget.NestedScrollView" + private const val ANDROIDX_WIDGET_NESTED_SCROLLVIEW_CLASS_NAME = "androidx.core.widget.NestedScrollView" + private const val WEB_VIEW_CLASS_NAME = "WebView" + private const val ATTR_ANDROID_VIEWPORT = ANDROID_NS_NAME_PREFIX + ATTR_FILL_VIEWPORT + } + + override fun appliesTo(folderType: ResourceFolderType) = folderType == ResourceFolderType.LAYOUT + + override fun getApplicableElements() = setOf(WEB_VIEW_CLASS_NAME) + + override fun visitElement(context: XmlContext, element: Element) { + val parentScrollView = findParentScrollView(element) + parentScrollView?.attributes?.let { attrs -> + if (attrs.getNamedItem(ATTR_ANDROID_VIEWPORT) == null || + attrs.getNamedItem(ATTR_ANDROID_VIEWPORT).nodeValue != "true") { + val replaceFix = LintFix.create() + .set() + .attribute(ATTR_FILL_VIEWPORT) + .value("true") + .build() + context.report(ISSUE, + context.getElementLocation(parentScrollView), + LINT_ERROR_MESSAGE, + replaceFix) + } + } + } + + private fun findParentScrollView(element: Element): Element? { + return when { + SCROLLVIEW_VIEW_CLASS_NAME == element.tagName -> element + SUPPORT_WIDGET_NESTED_SCROLLVIEW_CLASS_NAME == element.tagName -> element + ANDROIDX_WIDGET_NESTED_SCROLLVIEW_CLASS_NAME == element.tagName -> element + element.parentNode is Element -> findParentScrollView(element.parentNode as Element) + else -> null + } + } +} diff --git a/lint-checks-android/src/test/java/com/uber/lintchecks/android/XmlWebViewInsideScrollViewDetectorTest.kt b/lint-checks-android/src/test/java/com/uber/lintchecks/android/XmlWebViewInsideScrollViewDetectorTest.kt new file mode 100644 index 0000000..9d878d4 --- /dev/null +++ b/lint-checks-android/src/test/java/com/uber/lintchecks/android/XmlWebViewInsideScrollViewDetectorTest.kt @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2019. Uber Technologies + * + * 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.uber.lintchecks.android + +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.uber.lintchecks.base.test.LintTestBase +import org.junit.Test + +class XmlWebViewInsideScrollViewDetectorTest : LintTestBase() { + @Test + fun testDetector_scrollViewWithoutFillViewPort_shouldFail() { + TestLintTask.lint() + .files(xmlSource("res/layout/webview_inside_scrollview_wo_fill_viewport.xml", + """ + + + + """)) + .detector(XmlWebViewInsideScrollViewDetector()) + .issues(XmlWebViewInsideScrollViewDetector.ISSUE) + .run() + .expectErrorCount(1) + .expectMatches(XmlWebViewInsideScrollViewDetector.LINT_ERROR_MESSAGE) + } + + @Test + fun testDetector_supportWidgetNestedScrollViewWithoutFillViewPort_shouldFail() { + TestLintTask.lint() + .files(xmlSource("res/layout/webview_inside_support_widget_scrollview_wo_fill_viewport.xml", + """ + + + + """)) + .detector(XmlWebViewInsideScrollViewDetector()) + .issues(XmlWebViewInsideScrollViewDetector.ISSUE) + .run() + .expectErrorCount(1) + .expectMatches(XmlWebViewInsideScrollViewDetector.LINT_ERROR_MESSAGE) + } + + @Test + fun testDetector_androidxWidgetNestedScrollViewWithoutFillViewPort_shouldFail() { + TestLintTask.lint() + .files(xmlSource("res/layout/webview_inside_androidx_widget_scrollview_wo_fill_viewport.xml", + """ + + + + """)) + .detector(XmlWebViewInsideScrollViewDetector()) + .issues(XmlWebViewInsideScrollViewDetector.ISSUE) + .run() + .expectErrorCount(1) + .expectMatches(XmlWebViewInsideScrollViewDetector.LINT_ERROR_MESSAGE) + } + + @Test + fun testDetector_scrollViewWithFillViewPort_shouldPass() { + TestLintTask.lint() + .files(xmlSource("res/layout/webview_inside_scrollview_with_fill_viewport.xml", + """ + + + + """)) + .detector(XmlWebViewInsideScrollViewDetector()) + .issues(XmlWebViewInsideScrollViewDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testDetector_supportWidgetNestedScrollViewWithFillViewPort_shouldPass() { + TestLintTask.lint() + .files(xmlSource("res/layout/webview_inside_support_widget_scrollview_with_fill_viewport.xml", + """ + + + + """)) + .detector(XmlWebViewInsideScrollViewDetector()) + .issues(XmlWebViewInsideScrollViewDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testDetector_androidxWidgetNestedScrollViewWithFillViewPort_shouldPass() { + TestLintTask.lint() + .files(xmlSource("res/layout/webview_inside_androidx_widget_scrollview_with_fill_viewport.xml", + """ + + + + """)) + .detector(XmlWebViewInsideScrollViewDetector()) + .issues(XmlWebViewInsideScrollViewDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testDetector_scrollView_withMultipleChildren_WithoutFillViewPort_shouldFail() { + TestLintTask.lint() + .files(xmlSource("res/layout/scrollview_with_multiple_children_without_fill_viewport.xml", + """ + + + + + + + """)) + .detector(XmlWebViewInsideScrollViewDetector()) + .issues(XmlWebViewInsideScrollViewDetector.ISSUE) + .run() + .expectErrorCount(1) + .expectMatches(XmlWebViewInsideScrollViewDetector.LINT_ERROR_MESSAGE) + } + + @Test + fun testDetector_scrollView_withMultipleChildren_WithFillViewPort_shouldPass() { + TestLintTask.lint() + .files(xmlSource("res/layout/scrollview_with_multiple_children_with_fill_viewport.xml", + """ + + + + + + + """)) + .detector(XmlWebViewInsideScrollViewDetector()) + .issues(XmlWebViewInsideScrollViewDetector.ISSUE) + .run() + .expectClean() + } +}