add lint rule for OnPreDrawListener

commit_hash:d17e85be9d08761e6e79c11101cc527bfb837859
This commit is contained in:
gulevsky
2025-08-06 11:20:06 +03:00
parent b68630a2a7
commit 551cb599f8
28 changed files with 655 additions and 125 deletions
+11 -4
View File
@@ -624,6 +624,9 @@
"client/android/div-core/src/main/java/com/yandex/div/core/images/DivImagePriority.kt":"divkit/public/client/android/div-core/src/main/java/com/yandex/div/core/images/DivImagePriority.kt",
"client/android/div-core/src/main/java/com/yandex/div/core/images/LoadReference.java":"divkit/public/client/android/div-core/src/main/java/com/yandex/div/core/images/LoadReference.java",
"client/android/div-core/src/main/java/com/yandex/div/core/uri/UriHandler.java":"divkit/public/client/android/div-core/src/main/java/com/yandex/div/core/uri/UriHandler.java",
"client/android/div-core/src/main/java/com/yandex/div/core/view/DrawingPassOverrideStrategy.kt":"divkit/public/client/android/div-core/src/main/java/com/yandex/div/core/view/DrawingPassOverrideStrategy.kt",
"client/android/div-core/src/main/java/com/yandex/div/core/view/OnPreDrawListeners.kt":"divkit/public/client/android/div-core/src/main/java/com/yandex/div/core/view/OnPreDrawListeners.kt",
"client/android/div-core/src/main/java/com/yandex/div/core/view/SafeDrawingPassOverrideStrategy.kt":"divkit/public/client/android/div-core/src/main/java/com/yandex/div/core/view/SafeDrawingPassOverrideStrategy.kt",
"client/android/div-data/build.gradle":"divkit/public/client/android/div-data/build.gradle",
"client/android/div-data/div2-generator-config.json":"divkit/public/client/android/div-data/div2-generator-config.json",
"client/android/div-data/div2-shared-data-generator-config.json":"divkit/public/client/android/div-data/div2-shared-data-generator-config.json",
@@ -1174,9 +1177,9 @@
"client/android/div/src/main/java/com/yandex/div/core/util/ImageRepresentation.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/ImageRepresentation.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/ImageUtils.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/ImageUtils.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/Releasables.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/Releasables.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategy.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategy.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/SafeAlertDialog.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/SafeAlertDialog.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/SafeAlertDialogBuilder.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/SafeAlertDialogBuilder.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategy.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategy.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/SafePopupWindow.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/SafePopupWindow.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/SearchUtil.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/SearchUtil.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/SparseArrays.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/SparseArrays.kt",
@@ -1401,13 +1404,11 @@
"client/android/div/src/main/java/com/yandex/div/core/widget/DivExtendableView.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/DivExtendableView.kt",
"client/android/div/src/main/java/com/yandex/div/core/widget/DivViewDelegate.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/DivViewDelegate.kt",
"client/android/div/src/main/java/com/yandex/div/core/widget/DivViewWrapper.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/DivViewWrapper.kt",
"client/android/div/src/main/java/com/yandex/div/core/widget/DrawingPassOverrideStrategy.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/DrawingPassOverrideStrategy.kt",
"client/android/div/src/main/java/com/yandex/div/core/widget/FixedLineHeightHelper.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/FixedLineHeightHelper.kt",
"client/android/div/src/main/java/com/yandex/div/core/widget/FixedLineHeightView.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/FixedLineHeightView.kt",
"client/android/div/src/main/java/com/yandex/div/core/widget/GridContainer.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/GridContainer.kt",
"client/android/div/src/main/java/com/yandex/div/core/widget/LinearContainerLayout.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/LinearContainerLayout.kt",
"client/android/div/src/main/java/com/yandex/div/core/widget/LoadableImageView.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/LoadableImageView.kt",
"client/android/div/src/main/java/com/yandex/div/core/widget/OverridableOnPreDrawListener.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/OverridableOnPreDrawListener.kt",
"client/android/div/src/main/java/com/yandex/div/core/widget/ShowSeparatorsMode.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/ShowSeparatorsMode.kt",
"client/android/div/src/main/java/com/yandex/div/core/widget/ViewPager2Wrapper.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/ViewPager2Wrapper.kt",
"client/android/div/src/main/java/com/yandex/div/core/widget/Views.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/widget/Views.kt",
@@ -1557,7 +1558,7 @@
"client/android/div/src/test/java/com/yandex/div/core/util/DivPatchApplyItemsTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/DivPatchApplyItemsTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/util/DivWalkTreeTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/DivWalkTreeTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/util/EnableAssertsRule.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/EnableAssertsRule.kt",
"client/android/div/src/test/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategyTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/SafeDrawingPassOverrideStrategyTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategyTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/ReportingSafeDrawingPassOverrideStrategyTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/util/TypeConverterTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/TypeConverterTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/util/mask/CurrencyMaskTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/mask/CurrencyMaskTest.kt",
"client/android/div/src/test/java/com/yandex/div/core/util/mask/FixedLengthMaskTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/util/mask/FixedLengthMaskTest.kt",
@@ -12155,6 +12156,12 @@
"client/android/gradle/wrapper/gradle-wrapper.properties":"divkit/public/client/android/gradle/wrapper/gradle-wrapper.properties",
"client/android/gradlew":"divkit/public/client/android/gradlew",
"client/android/gradlew.bat":"divkit/public/client/android/gradlew.bat",
"client/android/lint-rules/build.gradle":"divkit/public/client/android/lint-rules/build.gradle",
"client/android/lint-rules/src/main/java/com/yandex/div/lint/DivKitIssueRegistry.kt":"divkit/public/client/android/lint-rules/src/main/java/com/yandex/div/lint/DivKitIssueRegistry.kt",
"client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerDetector.kt":"divkit/public/client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerDetector.kt",
"client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerIssue.kt":"divkit/public/client/android/lint-rules/src/main/java/com/yandex/div/lint/OnPreDrawListenerIssue.kt",
"client/android/lint-rules/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry":"divkit/public/client/android/lint-rules/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry",
"client/android/lint-rules/src/test/java/com/yandex/div/lint/OnPreDrawListenerDetectorTest.kt":"divkit/public/client/android/lint-rules/src/test/java/com/yandex/div/lint/OnPreDrawListenerDetectorTest.kt",
"client/android/logging/build.gradle":"divkit/public/client/android/logging/build.gradle",
"client/android/logging/jacoco.excludes":"divkit/public/client/android/logging/jacoco.excludes",
"client/android/logging/proguard-rules.pro":"divkit/public/client/android/logging/proguard-rules.pro",
+1
View File
@@ -126,6 +126,7 @@ apiValidation {
"divkit-demo-app",
"divkit-perftests",
"divkit-regression-testing",
"lint-rules",
"sample",
"screenshot-test-runtime",
"ui-test-common",
@@ -0,0 +1,18 @@
package com.yandex.div.core.view
import android.view.ViewTreeObserver.OnPreDrawListener
import com.yandex.div.core.annotations.InternalApi
@InternalApi
public fun interface DrawingPassOverrideStrategy {
public fun overrideDrawingPass(listener: OnPreDrawListener, proceed: Boolean): Boolean
@InternalApi
public object NoOp : DrawingPassOverrideStrategy {
override fun overrideDrawingPass(listener: OnPreDrawListener, proceed: Boolean): Boolean = proceed
}
@InternalApi
public object Safe : SafeDrawingPassOverrideStrategy()
}
@@ -0,0 +1,42 @@
@file:JvmName("OnPreDrawListeners")
package com.yandex.div.core.view
import android.annotation.SuppressLint
import android.view.ViewTreeObserver
import com.yandex.div.core.annotations.InternalApi
@InternalApi
public fun onPreDrawListener(
overrideStrategy: DrawingPassOverrideStrategy = DrawingPassOverrideStrategy.Safe,
action: () -> Boolean
): ViewTreeObserver.OnPreDrawListener {
return OverridableOnPreDrawListener(
delegate = action,
overrideStrategy
)
}
@InternalApi
public fun onPreDrawListener(
overrideStrategy: DrawingPassOverrideStrategy = DrawingPassOverrideStrategy.Safe,
delegate: ViewTreeObserver.OnPreDrawListener
): ViewTreeObserver.OnPreDrawListener {
return OverridableOnPreDrawListener(
delegate = delegate,
overrideStrategy
)
}
@InternalApi
@SuppressLint("OnPreDrawListenerIssue")
public class OverridableOnPreDrawListener @JvmOverloads constructor(
private val delegate: ViewTreeObserver.OnPreDrawListener,
private val overrideStrategy: DrawingPassOverrideStrategy = DrawingPassOverrideStrategy.Safe
) : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
val proceed = delegate.onPreDraw()
return overrideStrategy.overrideDrawingPass(delegate, proceed)
}
}
@@ -0,0 +1,43 @@
package com.yandex.div.core.view
import android.view.ViewTreeObserver.OnPreDrawListener
import com.yandex.div.core.annotations.InternalApi
@InternalApi
public open class SafeDrawingPassOverrideStrategy : DrawingPassOverrideStrategy {
public var frameCancelLimit: Int = DEFAULT_FRAME_CANCEL_LIMIT
set(value) {
if (field != value) {
field = value
frameCancelCount = 0
}
}
private var frameCancelCount = 0
override fun overrideDrawingPass(listener: OnPreDrawListener, proceed: Boolean): Boolean {
if (proceed) {
frameCancelCount = 0
return true
} else if (frameCancelCount < frameCancelLimit) {
frameCancelCount++
onFrameCancelled(listener, frameCancelCount)
return false
} else if (frameCancelCount == frameCancelLimit) {
frameCancelCount++
onFrameCancelLimitExceeded(listener, frameCancelCount)
return true
} else {
return true
}
}
protected open fun onFrameCancelled(listener: OnPreDrawListener, frameCancelCount: Int): Unit = Unit
protected open fun onFrameCancelLimitExceeded(listener: OnPreDrawListener, frameCancelCount: Int): Unit = Unit
private companion object {
const val DEFAULT_FRAME_CANCEL_LIMIT: Int = 3
}
}
+6 -3
View File
@@ -1,6 +1,9 @@
apply plugin: 'java-library'
apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: "kotlin-allopen"
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.allopen'
}
apply from: "${project.projectDir}/../publish-java.gradle"
apply from: "${project.projectDir}/../div-tests-coverage.gradle"
+4
View File
@@ -1,3 +1,7 @@
apply plugin: 'com.android.library'
apply from: "${buildscript.sourceFile.parent}/div-common.gradle"
dependencies {
lintChecks project(path: ':lint-rules')
}
@@ -2,8 +2,8 @@ package com.yandex.div.sizeprovider
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewTreeObserver
import com.yandex.div.core.extension.DivExtensionHandler
import com.yandex.div.core.view.onPreDrawListener
import com.yandex.div.core.view2.Div2View
import com.yandex.div.core.view2.divs.pxToDp
import com.yandex.div.json.expressions.ExpressionResolver
@@ -59,7 +59,7 @@ class DivSizeProviderExtensionHandler(
if (divView.getTag(R.id.div_size_provider_clear_variables_listener) != null) return
val clearVariablesListener = ViewTreeObserver.OnPreDrawListener {
val clearVariablesListener = onPreDrawListener {
variablesHolder.clear()
sizes.forEach { divView.setVariable(it.key, it.value.toString()) }
sizes.clear()
@@ -2,7 +2,6 @@ package com.yandex.div.core.dagger
import com.yandex.div.core.expression.local.DivRuntimeVisitor
import com.yandex.div.core.tooltip.DivTooltipController
import com.yandex.div.core.util.SafeDrawingPassOverrideStrategy
import com.yandex.div.core.view2.Div2View
import com.yandex.div.core.view2.DivTransitionBuilder
import com.yandex.div.core.view2.DivViewIdProvider
@@ -15,6 +14,7 @@ import com.yandex.div.core.view2.errors.ErrorVisualMonitor
import com.yandex.div.core.view2.reuse.InputFocusTracker
import com.yandex.div.core.view2.state.DivStateSwitcher
import com.yandex.div.core.view2.state.DivStateTransitionHolder
import com.yandex.div.core.view.DrawingPassOverrideStrategy
import com.yandex.yatagan.BindsInstance
import com.yandex.yatagan.Component
@@ -40,7 +40,7 @@ internal interface Div2ViewComponent {
val animatorController: DivAnimatorController
val divTooltipController: DivTooltipController
val runtimeVisitor: DivRuntimeVisitor
val drawingPassOverrideStrategy: SafeDrawingPassOverrideStrategy
val drawingPassOverrideStrategy: DrawingPassOverrideStrategy
@Component.Builder
interface Builder {
@@ -1,24 +1,33 @@
package com.yandex.div.core.dagger
import com.yandex.div.core.experiments.Experiment.MULTIPLE_STATE_CHANGE_ENABLED
import com.yandex.div.core.util.ReportingSafeDrawingPassOverrideStrategy
import com.yandex.div.core.view2.state.DivJoinedStateSwitcher
import com.yandex.div.core.view2.state.DivMultipleStateSwitcher
import com.yandex.div.core.view2.state.DivStateSwitcher
import com.yandex.div.core.view.DrawingPassOverrideStrategy
import com.yandex.yatagan.Binds
import com.yandex.yatagan.Module
import com.yandex.yatagan.Provides
import javax.inject.Provider
@Module
internal object Div2ViewModule {
internal interface Div2ViewModule {
@JvmStatic
@Provides
@DivViewScope
fun provideStateSwitcher(
@ExperimentFlag(MULTIPLE_STATE_CHANGE_ENABLED) multipleStateChangeEnabled: Boolean,
joinedStateSwitcher: Provider<DivJoinedStateSwitcher>,
multipleStateSwitcher: Provider<DivMultipleStateSwitcher>
): DivStateSwitcher {
return if (multipleStateChangeEnabled) multipleStateSwitcher.get() else joinedStateSwitcher.get()
@Binds
fun bindsDrawingPassOverrideStrategy(i: ReportingSafeDrawingPassOverrideStrategy): DrawingPassOverrideStrategy
companion object {
@JvmStatic
@Provides
@DivViewScope
fun provideStateSwitcher(
@ExperimentFlag(MULTIPLE_STATE_CHANGE_ENABLED) multipleStateChangeEnabled: Boolean,
joinedStateSwitcher: Provider<DivJoinedStateSwitcher>,
multipleStateSwitcher: Provider<DivMultipleStateSwitcher>
): DivStateSwitcher {
return if (multipleStateChangeEnabled) multipleStateSwitcher.get() else joinedStateSwitcher.get()
}
}
}
@@ -0,0 +1,26 @@
package com.yandex.div.core.util
import android.view.ViewTreeObserver.OnPreDrawListener
import com.yandex.div.core.Div2Logger
import com.yandex.div.core.dagger.DivViewScope
import com.yandex.div.core.view.SafeDrawingPassOverrideStrategy
import com.yandex.div.core.view2.Div2View
import javax.inject.Inject
@DivViewScope
internal class ReportingSafeDrawingPassOverrideStrategy @Inject constructor(
private val divView: Div2View,
private val logger: Div2Logger,
) : SafeDrawingPassOverrideStrategy() {
override fun onFrameCancelled(listener: OnPreDrawListener, frameCancelCount: Int) {
logger.logFrameCancelled(divView, "Frame cancelled by $listener")
}
override fun onFrameCancelLimitExceeded(listener: OnPreDrawListener, frameCancelCount: Int) {
logger.logFrameCancelLimitExceeded(
divView,
"Frame cancellation limit exceeded by $listener. Forcing frame drawing."
)
}
}
@@ -1,49 +0,0 @@
package com.yandex.div.core.util
import android.view.ViewTreeObserver.OnPreDrawListener
import com.yandex.div.core.Div2Logger
import com.yandex.div.core.dagger.DivViewScope
import com.yandex.div.core.view2.Div2View
import com.yandex.div.core.widget.DrawingPassOverrideStrategy
import javax.inject.Inject
@DivViewScope
internal class SafeDrawingPassOverrideStrategy @Inject constructor(
private val divView: Div2View,
private val logger: Div2Logger,
) : DrawingPassOverrideStrategy {
var frameCancelLimit: Int = DEFAULT_FRAME_CANCEL_LIMIT
set(value) {
if (field != value) {
field = value
frameCancelCount = 0
}
}
private var frameCancelCount = 0
override fun overrideDrawingPass(listener: OnPreDrawListener, proceed: Boolean): Boolean {
if (proceed) {
frameCancelCount = 0
return true
} else if (frameCancelCount < frameCancelLimit) {
frameCancelCount++
logger.logFrameCancelled(divView, "Frame cancelled by $listener")
return false
} else if (frameCancelCount == frameCancelLimit) {
frameCancelCount++
logger.logFrameCancelLimitExceeded(
divView,
"Frame cancellation limit exceeded by $listener. Forcing frame drawing."
)
return true
} else {
return true
}
}
companion object {
const val DEFAULT_FRAME_CANCEL_LIMIT = 3
}
}
@@ -6,7 +6,6 @@ import android.util.DisplayMetrics
import android.view.View
import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.MarginLayoutParams
import android.view.ViewTreeObserver
import androidx.transition.Transition
import androidx.transition.TransitionManager
import androidx.transition.Visibility
@@ -20,6 +19,7 @@ import com.yandex.div.core.util.isConstant
import com.yandex.div.core.util.observeEdgeInsets
import com.yandex.div.core.util.observeSize
import com.yandex.div.core.util.observeTransform
import com.yandex.div.core.view.onPreDrawListener
import com.yandex.div.core.view2.BindingContext
import com.yandex.div.core.view2.Div2View
import com.yandex.div.core.view2.DivAccessibilityBinder
@@ -301,7 +301,7 @@ internal class DivBaseBinder @Inject constructor(
setTag(R.id.div_layout_provider_listener_id, listener)
if (divView.clearVariablesListener != null) return
val clearVariablesListener = ViewTreeObserver.OnPreDrawListener {
val clearVariablesListener = onPreDrawListener {
variablesHolder.clear()
divView.layoutSizes.forEach { (resolver, variableMap) ->
variableMap.forEach {
@@ -5,6 +5,8 @@ import android.view.View
import android.view.ViewTreeObserver
import android.widget.TextView
import androidx.core.view.ViewCompat
import com.yandex.div.core.view.DrawingPassOverrideStrategy
import com.yandex.div.core.view.onPreDrawListener
import com.yandex.div.core.widget.AdaptiveMaxLines.Params
/**
@@ -1,12 +0,0 @@
package com.yandex.div.core.widget
import android.view.ViewTreeObserver.OnPreDrawListener
internal fun interface DrawingPassOverrideStrategy {
fun overrideDrawingPass(listener: OnPreDrawListener, proceed: Boolean): Boolean
object Default : DrawingPassOverrideStrategy {
override fun overrideDrawingPass(listener: OnPreDrawListener, proceed: Boolean) = proceed
}
}
@@ -1,34 +0,0 @@
package com.yandex.div.core.widget
import android.view.ViewTreeObserver
internal fun onPreDrawListener(
overrideStrategy: DrawingPassOverrideStrategy,
action: () -> Boolean
): ViewTreeObserver.OnPreDrawListener {
return OverridableOnPreDrawListener(
delegate = { action() },
overrideStrategy
)
}
internal fun onPreDrawListener(
overrideStrategy: DrawingPassOverrideStrategy,
delegate: ViewTreeObserver.OnPreDrawListener
): ViewTreeObserver.OnPreDrawListener {
return OverridableOnPreDrawListener(
delegate = delegate,
overrideStrategy
)
}
internal class OverridableOnPreDrawListener(
private val delegate: ViewTreeObserver.OnPreDrawListener,
private val overrideStrategy: DrawingPassOverrideStrategy
): ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
val proceed = delegate.onPreDraw()
return overrideStrategy.overrideDrawingPass(delegate, proceed)
}
}
@@ -1,8 +1,8 @@
package com.yandex.div.internal.widget
import android.view.ViewTreeObserver
import com.yandex.div.core.widget.DrawingPassOverrideStrategy
import com.yandex.div.core.widget.onPreDrawListener
import com.yandex.div.core.view.DrawingPassOverrideStrategy
import com.yandex.div.core.view.onPreDrawListener
import com.yandex.div.internal.KLog
/**
@@ -15,7 +15,7 @@ internal class AutoEllipsizeHelper(private val textView: EllipsizedTextView) {
*/
var isEnabled = false
var drawingPassOverrideStrategy: DrawingPassOverrideStrategy = DrawingPassOverrideStrategy.Default
var drawingPassOverrideStrategy: DrawingPassOverrideStrategy = DrawingPassOverrideStrategy.Safe
private var preDrawListener: ViewTreeObserver.OnPreDrawListener? = null
@@ -10,7 +10,7 @@ import android.util.AttributeSet
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import com.yandex.div.R
import com.yandex.div.core.widget.DrawingPassOverrideStrategy
import com.yandex.div.core.view.DrawingPassOverrideStrategy
open class EllipsizedTextView @JvmOverloads constructor(
context: Context,
@@ -14,12 +14,12 @@ import org.mockito.kotlin.verify
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class SafeDrawingPassOverrideStrategyTest {
class ReportingSafeDrawingPassOverrideStrategyTest {
private val divView = mock<Div2View>()
private val logger = mock<Div2Logger>()
private val overrideStrategy = SafeDrawingPassOverrideStrategy(divView, logger)
private val overrideStrategy = ReportingSafeDrawingPassOverrideStrategy(divView, logger)
@Test
fun `no frame cancellation`() {
@@ -3,6 +3,7 @@ package com.yandex.div.core.widget
import android.view.View
import android.view.ViewTreeObserver
import android.widget.TextView
import com.yandex.div.core.view.DrawingPassOverrideStrategy
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@@ -23,7 +24,7 @@ class AdaptiveMaxLinesTest {
on { viewTreeObserver } doReturn viewTreeObserver
}
private val underTest = AdaptiveMaxLines(textView, DrawingPassOverrideStrategy.Default)
private val underTest = AdaptiveMaxLines(textView, DrawingPassOverrideStrategy.NoOp)
@Test
fun `add pre draw listener when view already attached to window`() {
+5
View File
@@ -1,5 +1,6 @@
[versions]
agp = "8.8.2"
android-lint = "31.8.2" # agp + "23.0.0"
androidsvg = "1.4"
buildTimeTracker = "5.0.1"
coil = "3.0.4"
@@ -66,6 +67,10 @@ androidx-work = "2.9.1"
agp-gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" }
agp-gradleApi = { module = "com.android.tools.build:gradle-api", version.ref = "agp" }
android-lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "android-lint" }
android-lint = { module = "com.android.tools.lint:lint", version.ref = "android-lint" }
android-lint-tests = { module = "com.android.tools.lint:lint-tests", version.ref = "android-lint" }
androidsvg-aar = { module = "com.caverock:androidsvg-aar", version.ref = "androidsvg" }
androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
+26
View File
@@ -0,0 +1,26 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
dependencies {
compileOnly(libs.android.lint.api)
testImplementation(libs.junit)
testImplementation(libs.android.lint)
testImplementation(libs.android.lint.tests)
}
tasks.withType(KotlinCompile).configureEach {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
@@ -0,0 +1,20 @@
package com.yandex.div.lint
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API
class DivKitIssueRegistry : IssueRegistry() {
override val api = CURRENT_API
override val minApi = 8
override val issues = listOf(OnPreDrawListenerIssue.get())
override val vendor = Vendor(
feedbackUrl = "https://divkit.tech/",
identifier = "com.yandex.div",
vendorName = "DivKit Open Source Project"
)
}
@@ -0,0 +1,110 @@
package com.yandex.div.lint
import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Location
import com.android.tools.lint.detector.api.SourceCodeScanner
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UCallableReferenceExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UElement
import org.jetbrains.uast.ULambdaExpression
import org.jetbrains.uast.UParameter
import org.jetbrains.uast.getParameterForArgument
import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.getQualifiedName
import org.jetbrains.uast.toUElementOfType
import org.jetbrains.uast.util.isConstructorCall
class OnPreDrawListenerDetector : Detector(), SourceCodeScanner {
override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(
UClass::class.java,
ULambdaExpression::class.java,
UCallableReferenceExpression::class.java
)
override fun createUastHandler(context: JavaContext): UElementHandler = OnPreDrawListenerVisitor(context)
}
class OnPreDrawListenerVisitor(
private val context: JavaContext
) : UElementHandler() {
override fun visitClass(node: UClass) {
val usage = node.uastSuperTypes.find { typeReference ->
typeReference.getQualifiedName() == TARGET_INTERFACE_FQN
}
if (usage != null) {
reportIssue(scopeClass = node, usage = usage)
}
}
override fun visitLambdaExpression(node: ULambdaExpression) {
val callExpression = node.uastParent as? UCallExpression ?: return
val constructorExpression = if (callExpression.isConstructorCall()) {
callExpression.classReference
} else {
null
}
val lambdaType = node.functionalInterfaceType?.canonicalText
if (lambdaType == TARGET_INTERFACE_FQN) {
val nodeClass = node.getParentOfType<UClass>()
val usageNode = if (constructorExpression.getQualifiedName() == TARGET_INTERFACE_FQN) {
constructorExpression!!
} else {
node
}
val location = if (constructorExpression.getQualifiedName() == TARGET_INTERFACE_FQN) {
context.getCallLocation(
callExpression,
includeReceiver = true,
includeArguments = false
)
} else {
context.getLocation(node)
}
reportIssue(
scopeClass = nodeClass,
usage = usageNode,
location = location
)
}
}
override fun visitCallableReferenceExpression(node: UCallableReferenceExpression) {
val callExpression = node.uastParent as? UCallExpression ?: return
val referenceParameter = callExpression.getParameterForArgument(node).toUElementOfType<UParameter>() ?: return
val referenceType = referenceParameter.typeReference?.getQualifiedName()
if (referenceType == TARGET_INTERFACE_FQN) {
val nodeClass = node.getParentOfType<UClass>()
reportIssue(
scopeClass = nodeClass,
usage = node
)
}
}
private fun reportIssue(
scopeClass: UClass?,
usage: UElement,
location: Location = context.getLocation(usage)
) {
context.report(
issue = OnPreDrawListenerIssue.get(),
scopeClass = scopeClass,
location = location,
message = OnPreDrawListenerIssue.fullDescription()
)
}
private companion object {
const val TARGET_INTERFACE_FQN = "android.view.ViewTreeObserver.OnPreDrawListener"
}
}
@@ -0,0 +1,33 @@
package com.yandex.div.lint
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
object OnPreDrawListenerIssue {
const val ID = "OnPreDrawListenerIssue"
private const val DESCRIPTION = "ViewTreeObserver.OnPreDrawListener is potentially unsafe"
private const val EXPLANATION = """Return of `false` from `ViewTreeObserver.OnPreDrawListener.onPreDraw()` may break the drawing of the entire screen.
Replace it with `com.yandex.div.core.view.OverridableOnPreDrawListener` or `com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...)` function."""
private val issue = Issue.create(
id = ID,
briefDescription = DESCRIPTION,
explanation = EXPLANATION,
category = Category.CORRECTNESS,
priority = 8,
severity = Severity.ERROR,
implementation = Implementation(
OnPreDrawListenerDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
fun get(): Issue = issue
fun fullDescription(): String = "$DESCRIPTION\n$EXPLANATION"
}
@@ -0,0 +1 @@
com.yandex.div.lint.DivKitIssueRegistry
@@ -0,0 +1,273 @@
package com.yandex.div.lint
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestMode
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Issue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class OnPreDrawListenerDetectorTest : LintDetectorTest() {
private val declarationFile: TestFile = kotlin(
"""
package android.view
class ViewTreeObserver {
fun interface OnPreDrawListener {
override fun onPreDraw(): Boolean
}
}
"""
).indented()
override fun getDetector(): Detector = OnPreDrawListenerDetector()
override fun getIssues(): List<Issue> = listOf(OnPreDrawListenerIssue.get())
@Test
fun `issue detected in implementation`() {
val testFile = kotlin(
"""
package test
import android.view.ViewTreeObserver
class OnPreDrawListenerImpl : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean = true
}
val onPreDrawListener = OnPreDrawListenerImpl()
"""
).indented()
lint()
.files(declarationFile, testFile)
.allowMissingSdk()
.run()
.expect(
"""
src/test/OnPreDrawListenerImpl.kt:5: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe
Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen.
Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue]
class OnPreDrawListenerImpl : ViewTreeObserver.OnPreDrawListener {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""".trimIndent()
)
}
@Test
fun `issue detected in anonymous implementation`() {
val testFile = kotlin(
"""
package test
import android.view.ViewTreeObserver
val onPreDrawListener = object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean = true
}
"""
).indented()
lint()
.files(declarationFile, testFile)
.allowMissingSdk()
.run()
.expect(
"""
src/test/test.kt:5: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe
Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen.
Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue]
val onPreDrawListener = object : ViewTreeObserver.OnPreDrawListener {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""".trimIndent()
)
}
@Test
fun `issue does not detected in implicit implementation`() {
val testFile = kotlin("""
package test
import android.view.ViewTreeObserver
@Suppress("OnPreDrawListenerIssue")
class OnPreDrawListenerImpl : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean = true
}
fun onPreDrawListener(): ViewTreeObserver.OnPreDrawListener {
return OnPreDrawListenerImpl()
}
"""
).indented()
lint()
.files(declarationFile, testFile)
.allowMissingSdk()
.skipTestModes(TestMode.SUPPRESSIBLE)
.run()
.expectClean()
}
@Test
fun `issue detected in SAM constructor`() {
val testFile = kotlin(
"""
package test
import android.view.ViewTreeObserver.OnPreDrawListener
val onPreDrawListener = OnPreDrawListener { true }
"""
).indented()
lint()
.files(declarationFile, testFile)
.allowMissingSdk()
.skipTestModes(TestMode.SUPPRESSIBLE)
.run()
.expect(
"""
src/test/test.kt:5: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe
Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen.
Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue]
val onPreDrawListener = OnPreDrawListener { true }
~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""".trimIndent()
)
}
@Test
fun `issue detected in qualified SAM constructor`() {
val testFile = kotlin(
"""
package test
import android.view.ViewTreeObserver
val onPreDrawListener = ViewTreeObserver.OnPreDrawListener { true }
"""
).indented()
lint()
.files(declarationFile, testFile)
.allowMissingSdk()
.skipTestModes(TestMode.SUPPRESSIBLE)
.run()
.expect(
"""
src/test/test.kt:5: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe
Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen.
Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue]
val onPreDrawListener = ViewTreeObserver.OnPreDrawListener { true }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""".trimIndent()
)
}
@Test
fun `issue detected in lambda SAM conversion`() {
val testFile = kotlin(
"""
package test
import android.view.ViewTreeObserver
class Wrapper(
private val count: Int,
private val listener: ViewTreeObserver.OnPreDrawListener
)
fun wrap(listener: ViewTreeObserver.OnPreDrawListener): Wrapper {
return Wrapper(0, listener)
}
fun doWrapConstructorCall() {
Wrapper(0, listener = { true })
}
fun doWrapMethodCall() {
wrap(listener = { true })
}
"""
).indented()
lint()
.files(declarationFile, testFile)
.allowMissingSdk()
.skipTestModes(TestMode.SUPPRESSIBLE, TestMode.SOURCE_TRANSFORMATION_GROUP)
.run()
.expect(
"""
src/test/Wrapper.kt:15: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe
Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen.
Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue]
Wrapper(0, listener = { true })
~~~~~~~~
src/test/Wrapper.kt:19: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe
Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen.
Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue]
wrap(listener = { true })
~~~~~~~~
2 errors, 0 warnings
""".trimIndent()
)
}
@Test
fun `issue detected in method reference SAM conversion`() {
val testFile = kotlin(
"""
package test
import android.view.ViewTreeObserver
class Wrapper(private val listener: ViewTreeObserver.OnPreDrawListener)
fun wrap(listener: ViewTreeObserver.OnPreDrawListener): Wrapper {
return Wrapper(listener)
}
fun onPreDraw(): Boolean = true
fun doWrapConstructorCall() {
Wrapper(listener = ::onPreDraw)
}
fun doWrapMethodCall() {
wrap(listener = ::onPreDraw)
}
"""
).indented()
lint()
.files(declarationFile, testFile)
.allowMissingSdk()
.skipTestModes(TestMode.SUPPRESSIBLE, TestMode.SOURCE_TRANSFORMATION_GROUP)
.run()
.expect(
"""
src/test/Wrapper.kt:14: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe
Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen.
Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue]
Wrapper(listener = ::onPreDraw)
~~~~~~~~~~~
src/test/Wrapper.kt:18: Error: ViewTreeObserver.OnPreDrawListener is potentially unsafe
Return of false from ViewTreeObserver.OnPreDrawListener.onPreDraw() may break the drawing of the entire screen.
Replace it with com.yandex.div.core.view.OverridableOnPreDrawListener or com.yandex.div.core.view.OnPreDrawListeners.onPreDrawListener(...) function. [OnPreDrawListenerIssue]
wrap(listener = ::onPreDraw)
~~~~~~~~~~~
2 errors, 0 warnings
""".trimIndent()
)
}
}
+1
View File
@@ -50,6 +50,7 @@ include ':divkit-regression-testing'
include ':expression-test-common'
include ':fonts'
include ':glide'
include ':lint-rules'
include ':logging'
include ':picasso'
include ':sample'