David Liu

temp commit

1 <?xml version="1.0" encoding="UTF-8"?> 1 <?xml version="1.0" encoding="UTF-8"?>
2 <project version="4"> 2 <project version="4">
3 <component name="CompilerConfiguration"> 3 <component name="CompilerConfiguration">
4 - <bytecodeTargetLevel target="11" /> 4 + <bytecodeTargetLevel target="1.8">
  5 + <module name="livekit-android.livekit-android-sdk" target="11" />
  6 + <module name="livekit-android.sample-app" target="11" />
  7 + </bytecodeTargetLevel>
5 </component> 8 </component>
6 </project> 9 </project>
@@ -55,6 +55,9 @@ ext { @@ -55,6 +55,9 @@ ext {
55 androidx_lifecycle: "2.4.0", 55 androidx_lifecycle: "2.4.0",
56 dagger : "2.27", 56 dagger : "2.27",
57 groupie : "2.9.0", 57 groupie : "2.9.0",
  58 + junit : "4.13.2",
  59 + junitJupiter : "5.5.0",
  60 + lint : "30.0.1",
58 protobuf : "3.15.1", 61 protobuf : "3.15.1",
59 ] 62 ]
60 generated = [ 63 generated = [
@@ -63,6 +66,17 @@ ext { @@ -63,6 +66,17 @@ ext {
63 deps = [ 66 deps = [
64 kotlinx_coroutines: "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2", 67 kotlinx_coroutines: "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2",
65 timber : "com.github.ajalt:timberkt:1.5.1", 68 timber : "com.github.ajalt:timberkt:1.5.1",
  69 +
  70 + // lint
  71 + lint : "com.android.tools.lint:lint:${versions.lint}",
  72 + lintApi : "com.android.tools.lint:lint-api:${versions.lint}",
  73 + lintChecks : "com.android.tools.lint:lint-checks:${versions.lint}",
  74 + lintTests : "com.android.tools.lint:lint-tests:${versions.lint}",
  75 +
  76 + // tests
  77 + junit : "junit:junit:${versions.junit}",
  78 + junitJupiterApi : "org.junit.jupiter:junit-jupiter-api:${versions.junitJupiter}",
  79 + junitJupiterEngine: "org.junit.jupiter:junit-jupiter-engine:${versions.junitJupiter}",
66 ] 80 ]
67 annotations = [ 81 annotations = [
68 ] 82 ]
  1 +plugins {
  2 + id 'java-library'
  3 + id 'kotlin'
  4 +}
  5 +
  6 +java {
  7 + sourceCompatibility = JavaVersion.VERSION_1_8
  8 + targetCompatibility = JavaVersion.VERSION_1_8
  9 +}
  10 +
  11 +dependencies {
  12 + // used for lint rules
  13 + compileOnly deps.lintApi
  14 + compileOnly deps.lintChecks
  15 + compileOnly deps.lintTests
  16 +
  17 + // test lint
  18 + testImplementation deps.lint
  19 + testImplementation deps.lintTests
  20 +
  21 + // test runners
  22 + testImplementation deps.junit
  23 + testImplementation deps.junitJupiterApi
  24 + testRuntimeOnly deps.junitJupiterEngine
  25 +}
  26 +test {
  27 + environment "LINT_TEST_KOTLINC", ""
  28 +}
  1 +@file:Suppress("UnstableApiUsage") // We know that Lint API's aren't final.
  2 +
  3 +/*
  4 + * Copyright (C) 2018 The Android Open Source Project
  5 + *
  6 + * Licensed under the Apache License, Version 2.0 (the "License");
  7 + * you may not use this file except in compliance with the License.
  8 + * You may obtain a copy of the License at
  9 + *
  10 + * http://www.apache.org/licenses/LICENSE-2.0
  11 + *
  12 + * Unless required by applicable law or agreed to in writing, software
  13 + * distributed under the License is distributed on an "AS IS" BASIS,
  14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15 + * See the License for the specific language governing permissions and
  16 + * limitations under the License.
  17 + */
  18 +
  19 +package io.livekit.lint
  20 +
  21 +import com.android.tools.lint.client.api.UElementHandler
  22 +import com.android.tools.lint.detector.api.*
  23 +import com.intellij.psi.CommonClassNames.JAVA_LANG_OBJECT
  24 +import com.intellij.psi.PsiClassType
  25 +import com.intellij.psi.PsiElement
  26 +import com.intellij.psi.PsiMethod
  27 +import org.jetbrains.uast.*
  28 +import org.jetbrains.uast.util.isMethodCall
  29 +
  30 +/** Checks related to DiffUtil computation. */
  31 +class FlowDelegateUsageDetector : Detector(), SourceCodeScanner {
  32 +
  33 + override fun getApplicableUastTypes() =
  34 + listOf(UBinaryExpression::class.java, UCallExpression::class.java)
  35 +
  36 + override fun createUastHandler(context: JavaContext): UElementHandler? {
  37 + return object : UElementHandler() {
  38 +
  39 + override fun visitBinaryExpression(node: UBinaryExpression) {
  40 + checkExpression(context, node)
  41 + }
  42 +
  43 + override fun visitCallExpression(node: UCallExpression) {
  44 + checkCall(context, node)
  45 + }
  46 + }
  47 + }
  48 +
  49 + private fun defaultEquals(context: JavaContext, node: UElement): Boolean {
  50 + val resolved: PsiMethod?
  51 +
  52 + when (node) {
  53 + is UBinaryExpression -> {
  54 + resolved = node.resolveOperator()
  55 + if (resolved == null) {
  56 + val left = node.leftOperand.getExpressionType() as? PsiClassType
  57 + return defaultEquals(context, left)
  58 + }
  59 + }
  60 + is UCallExpression -> {
  61 + resolved = node.takeIf { it.isMethodCall() }
  62 + ?.resolve()
  63 + }
  64 + is UParenthesizedExpression -> {
  65 + return defaultEquals(context, node.expression)
  66 + }
  67 + else -> {
  68 + // We don't know any better
  69 + return false
  70 + }
  71 + }
  72 +
  73 + return resolved?.containingClass?.qualifiedName == MEDIA_STREAM_TRACK
  74 + }
  75 +
  76 + private fun defaultEquals(
  77 + context: JavaContext,
  78 + type: PsiClassType?
  79 + ): Boolean {
  80 + val cls = type?.resolve() ?: return false
  81 +
  82 + if (isKotlin(cls) && (context.evaluator.isSealed(cls) || context.evaluator.isData(cls))) {
  83 + // Sealed class doesn't guarantee that it defines equals/hashCode
  84 + // but it's likely (we'd need to go look at each inner class)
  85 + return false
  86 + }
  87 +
  88 + for (m in cls.findMethodsByName("equals", true)) {
  89 + if (m is PsiMethod) {
  90 + val parameters = m.parameterList.parameters
  91 + if (parameters.size == 1 &&
  92 + parameters[0].type.canonicalText == MEDIA_STREAM_TRACK
  93 + ) {
  94 + return m.containingClass?.qualifiedName == MEDIA_STREAM_TRACK
  95 + }
  96 + }
  97 + }
  98 +
  99 + return false
  100 + }
  101 +
  102 + private fun checkCall(context: JavaContext, node: UCallExpression) {
  103 + if (defaultEquals(context, node)) {
  104 + // Within cast or instanceof check which implies a more specific type
  105 + // which provides an equals implementation?
  106 + if (withinCastWithEquals(context, node)) {
  107 + return
  108 + }
  109 +
  110 + val message = DEFAULT_MSG
  111 + val location = context.getCallLocation(
  112 + node,
  113 + includeReceiver = false,
  114 + includeArguments = true
  115 + )
  116 + context.report(ISSUE, node, location, message)
  117 + }
  118 + }
  119 +
  120 + /**
  121 + * Is this .equals() call within another if check which checks
  122 + * instanceof on a more specific type than we're calling equals on?
  123 + * If so, does that more specific type define its own equals?
  124 + *
  125 + * Also handle an implicit check via short circuit evaluation; e.g.
  126 + * something like "return a is A && b is B && a.equals(b)".
  127 + */
  128 + private fun withinCastWithEquals(context: JavaContext, node: UExpression): Boolean {
  129 + var parent = skipParenthesizedExprUp(node.uastParent)
  130 + if (parent is UQualifiedReferenceExpression) {
  131 + parent = skipParenthesizedExprUp(parent.uastParent)
  132 + }
  133 + val target: PsiElement? = when (node) {
  134 + is UCallExpression -> node.receiver?.tryResolve()
  135 + is UBinaryExpression -> node.leftOperand.tryResolve()
  136 + else -> null
  137 + }
  138 +
  139 + if (parent is UPolyadicExpression && parent.operator == UastBinaryOperator.LOGICAL_AND) {
  140 + val operands = parent.operands
  141 + for (operand in operands) {
  142 + if (operand === node) {
  143 + break
  144 + }
  145 + if (isCastWithEquals(context, operand, target)) {
  146 + return true
  147 + }
  148 + }
  149 + }
  150 + val ifStatement = node.getParentOfType<UElement>(UIfExpression::class.java, false, UMethod::class.java)
  151 + as? UIfExpression ?: return false
  152 + val condition = ifStatement.condition
  153 + return isCastWithEquals(context, condition, target)
  154 + }
  155 +
  156 + private fun isCastWithEquals(context: JavaContext, node: UExpression, target: PsiElement?): Boolean {
  157 + when {
  158 + node is UBinaryExpressionWithType -> {
  159 + if (target != null) {
  160 + val resolved = node.operand.tryResolve()
  161 + // Unfortunately in some scenarios isEquivalentTo returns false for equal instances
  162 + //noinspection LintImplPsiEquals
  163 + if (resolved != null && !(target == resolved || target.isEquivalentTo(resolved))) {
  164 + return false
  165 + }
  166 + }
  167 + return !defaultEquals(context, node.type as? PsiClassType)
  168 + }
  169 + node is UPolyadicExpression && node.operator == UastBinaryOperator.LOGICAL_AND -> {
  170 + for (operand in node.operands) {
  171 + if (isCastWithEquals(context, operand, target)) {
  172 + return true
  173 + }
  174 + }
  175 + }
  176 + node is UParenthesizedExpression -> {
  177 + return isCastWithEquals(context, node.expression, target)
  178 + }
  179 + }
  180 + return false
  181 + }
  182 +
  183 + private fun checkExpression(context: JavaContext, node: UBinaryExpression) {
  184 + if (node.operator == UastBinaryOperator.IDENTITY_EQUALS ||
  185 + node.operator == UastBinaryOperator.EQUALS
  186 + ) {
  187 + val left = node.leftOperand.getExpressionType() ?: return
  188 + val right = node.rightOperand.getExpressionType() ?: return
  189 + if (left is PsiClassType && right is PsiClassType
  190 + && (left.className == "MediaStreamTrack" || right.className == "MediaStreamTrack")
  191 + ) {
  192 + if (node.operator == UastBinaryOperator.EQUALS) {
  193 + if (defaultEquals(context, node)) {
  194 + if (withinCastWithEquals(context, node)) {
  195 + return
  196 + }
  197 +
  198 + val message = DEFAULT_MSG
  199 + val location = node.operatorIdentifier?.let {
  200 + context.getLocation(it)
  201 + } ?: context.getLocation(node)
  202 + context.report(ISSUE, node, location, message)
  203 + }
  204 + } else {
  205 + val message = DEFAULT_MSG
  206 + val location = node.operatorIdentifier?.let {
  207 + context.getLocation(it)
  208 + } ?: context.getLocation(node)
  209 + context.report(ISSUE, node, location, message)
  210 + }
  211 + }
  212 + }
  213 + }
  214 +
  215 + companion object {
  216 + private const val MEDIA_STREAM_TRACK = "org.webrtc.MediaStreamTrack"
  217 +
  218 + private const val DEFAULT_MSG =
  219 + "Suspicious equality check: MediaStreamTracks should not be checked for equality. Check id() instead."
  220 +
  221 + private val IMPLEMENTATION =
  222 + Implementation(FlowDelegateUsageDetector::class.java, Scope.JAVA_FILE_SCOPE)
  223 +
  224 + @JvmField
  225 + val ISSUE = Issue.create(
  226 + id = "MediaTrackEqualsDetector",
  227 + briefDescription = "Suspicious DiffUtil Equality",
  228 + explanation = """
  229 + `areContentsTheSame` is used by `DiffUtil` to produce diffs. If the \
  230 + method is implemented incorrectly, such as using identity equals \
  231 + instead of equals, or calling equals on a class that has not implemented \
  232 + it, weird visual artifacts can occur.
  233 + """,
  234 + category = Category.CORRECTNESS,
  235 + priority = 4,
  236 + androidSpecific = true,
  237 + moreInfo = "https://issuetracker.google.com/116789824",
  238 + severity = Severity.ERROR,
  239 + implementation = IMPLEMENTATION
  240 + )
  241 + }
  242 +}
  1 +package io.livekit.lint
  2 +
  3 +import com.android.tools.lint.client.api.IssueRegistry
  4 +import com.android.tools.lint.detector.api.CURRENT_API
  5 +import com.android.tools.lint.detector.api.Issue
  6 +
  7 +class IssueRegistry : IssueRegistry() {
  8 +
  9 + override val api: Int = CURRENT_API
  10 +
  11 + override val issues: List<Issue>
  12 + get() = listOf(MediaTrackEqualsDetector.ISSUE)
  13 +}
  1 +@file:Suppress("UnstableApiUsage") // We know that Lint API's aren't final.
  2 +
  3 +/*
  4 + * Copyright (C) 2018 The Android Open Source Project
  5 + *
  6 + * Licensed under the Apache License, Version 2.0 (the "License");
  7 + * you may not use this file except in compliance with the License.
  8 + * You may obtain a copy of the License at
  9 + *
  10 + * http://www.apache.org/licenses/LICENSE-2.0
  11 + *
  12 + * Unless required by applicable law or agreed to in writing, software
  13 + * distributed under the License is distributed on an "AS IS" BASIS,
  14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15 + * See the License for the specific language governing permissions and
  16 + * limitations under the License.
  17 + */
  18 +
  19 +package io.livekit.lint
  20 +
  21 +import com.android.tools.lint.client.api.UElementHandler
  22 +import com.android.tools.lint.detector.api.*
  23 +import com.intellij.psi.CommonClassNames.JAVA_LANG_OBJECT
  24 +import com.intellij.psi.PsiClassType
  25 +import com.intellij.psi.PsiElement
  26 +import com.intellij.psi.PsiMethod
  27 +import org.jetbrains.uast.*
  28 +import org.jetbrains.uast.util.isMethodCall
  29 +
  30 +/** Checks related to DiffUtil computation. */
  31 +class MediaTrackEqualsDetector : Detector(), SourceCodeScanner {
  32 +
  33 + override fun getApplicableUastTypes() =
  34 + listOf(UBinaryExpression::class.java, UCallExpression::class.java)
  35 +
  36 + override fun createUastHandler(context: JavaContext): UElementHandler? {
  37 + return object : UElementHandler() {
  38 +
  39 + override fun visitBinaryExpression(node: UBinaryExpression) {
  40 + checkExpression(context, node)
  41 + }
  42 +
  43 + override fun visitCallExpression(node: UCallExpression) {
  44 + checkCall(context, node)
  45 + }
  46 + }
  47 + }
  48 +
  49 + private fun defaultEquals(context: JavaContext, node: UElement): Boolean {
  50 + val resolved: PsiMethod?
  51 +
  52 + when (node) {
  53 + is UBinaryExpression -> {
  54 + resolved = node.resolveOperator()
  55 + if (resolved == null) {
  56 + val left = node.leftOperand.getExpressionType() as? PsiClassType
  57 + return defaultEquals(context, left)
  58 + }
  59 + }
  60 + is UCallExpression -> {
  61 + resolved = node.takeIf { it.isMethodCall() }
  62 + ?.resolve()
  63 + }
  64 + is UParenthesizedExpression -> {
  65 + return defaultEquals(context, node.expression)
  66 + }
  67 + else -> {
  68 + // We don't know any better
  69 + return false
  70 + }
  71 + }
  72 +
  73 + return resolved?.containingClass?.qualifiedName == MEDIA_STREAM_TRACK
  74 + }
  75 +
  76 + private fun defaultEquals(
  77 + context: JavaContext,
  78 + type: PsiClassType?
  79 + ): Boolean {
  80 + val cls = type?.resolve() ?: return false
  81 +
  82 + if (isKotlin(cls) && (context.evaluator.isSealed(cls) || context.evaluator.isData(cls))) {
  83 + // Sealed class doesn't guarantee that it defines equals/hashCode
  84 + // but it's likely (we'd need to go look at each inner class)
  85 + return false
  86 + }
  87 +
  88 + for (m in cls.findMethodsByName("equals", true)) {
  89 + if (m is PsiMethod) {
  90 + val parameters = m.parameterList.parameters
  91 + if (parameters.size == 1 &&
  92 + parameters[0].type.canonicalText == MEDIA_STREAM_TRACK
  93 + ) {
  94 + return m.containingClass?.qualifiedName == MEDIA_STREAM_TRACK
  95 + }
  96 + }
  97 + }
  98 +
  99 + return false
  100 + }
  101 +
  102 + private fun checkCall(context: JavaContext, node: UCallExpression) {
  103 + if (defaultEquals(context, node)) {
  104 + // Within cast or instanceof check which implies a more specific type
  105 + // which provides an equals implementation?
  106 + if (withinCastWithEquals(context, node)) {
  107 + return
  108 + }
  109 +
  110 + val message = DEFAULT_MSG
  111 + val location = context.getCallLocation(
  112 + node,
  113 + includeReceiver = false,
  114 + includeArguments = true
  115 + )
  116 + context.report(ISSUE, node, location, message)
  117 + }
  118 + }
  119 +
  120 + /**
  121 + * Is this .equals() call within another if check which checks
  122 + * instanceof on a more specific type than we're calling equals on?
  123 + * If so, does that more specific type define its own equals?
  124 + *
  125 + * Also handle an implicit check via short circuit evaluation; e.g.
  126 + * something like "return a is A && b is B && a.equals(b)".
  127 + */
  128 + private fun withinCastWithEquals(context: JavaContext, node: UExpression): Boolean {
  129 + var parent = skipParenthesizedExprUp(node.uastParent)
  130 + if (parent is UQualifiedReferenceExpression) {
  131 + parent = skipParenthesizedExprUp(parent.uastParent)
  132 + }
  133 + val target: PsiElement? = when (node) {
  134 + is UCallExpression -> node.receiver?.tryResolve()
  135 + is UBinaryExpression -> node.leftOperand.tryResolve()
  136 + else -> null
  137 + }
  138 +
  139 + if (parent is UPolyadicExpression && parent.operator == UastBinaryOperator.LOGICAL_AND) {
  140 + val operands = parent.operands
  141 + for (operand in operands) {
  142 + if (operand === node) {
  143 + break
  144 + }
  145 + if (isCastWithEquals(context, operand, target)) {
  146 + return true
  147 + }
  148 + }
  149 + }
  150 + val ifStatement = node.getParentOfType<UElement>(UIfExpression::class.java, false, UMethod::class.java)
  151 + as? UIfExpression ?: return false
  152 + val condition = ifStatement.condition
  153 + return isCastWithEquals(context, condition, target)
  154 + }
  155 +
  156 + private fun isCastWithEquals(context: JavaContext, node: UExpression, target: PsiElement?): Boolean {
  157 + when {
  158 + node is UBinaryExpressionWithType -> {
  159 + if (target != null) {
  160 + val resolved = node.operand.tryResolve()
  161 + // Unfortunately in some scenarios isEquivalentTo returns false for equal instances
  162 + //noinspection LintImplPsiEquals
  163 + if (resolved != null && !(target == resolved || target.isEquivalentTo(resolved))) {
  164 + return false
  165 + }
  166 + }
  167 + return !defaultEquals(context, node.type as? PsiClassType)
  168 + }
  169 + node is UPolyadicExpression && node.operator == UastBinaryOperator.LOGICAL_AND -> {
  170 + for (operand in node.operands) {
  171 + if (isCastWithEquals(context, operand, target)) {
  172 + return true
  173 + }
  174 + }
  175 + }
  176 + node is UParenthesizedExpression -> {
  177 + return isCastWithEquals(context, node.expression, target)
  178 + }
  179 + }
  180 + return false
  181 + }
  182 +
  183 + private fun checkExpression(context: JavaContext, node: UBinaryExpression) {
  184 + if (node.operator == UastBinaryOperator.IDENTITY_EQUALS ||
  185 + node.operator == UastBinaryOperator.EQUALS
  186 + ) {
  187 + val left = node.leftOperand.getExpressionType() ?: return
  188 + val right = node.rightOperand.getExpressionType() ?: return
  189 + if (left is PsiClassType && right is PsiClassType
  190 + && (left.className == "MediaStreamTrack" || right.className == "MediaStreamTrack")
  191 + ) {
  192 + if (node.operator == UastBinaryOperator.EQUALS) {
  193 + if (defaultEquals(context, node)) {
  194 + if (withinCastWithEquals(context, node)) {
  195 + return
  196 + }
  197 +
  198 + val message = DEFAULT_MSG
  199 + val location = node.operatorIdentifier?.let {
  200 + context.getLocation(it)
  201 + } ?: context.getLocation(node)
  202 + context.report(ISSUE, node, location, message)
  203 + }
  204 + } else {
  205 + val message = DEFAULT_MSG
  206 + val location = node.operatorIdentifier?.let {
  207 + context.getLocation(it)
  208 + } ?: context.getLocation(node)
  209 + context.report(ISSUE, node, location, message)
  210 + }
  211 + }
  212 + }
  213 + }
  214 +
  215 + companion object {
  216 + private const val MEDIA_STREAM_TRACK = "org.webrtc.MediaStreamTrack"
  217 +
  218 + private const val DEFAULT_MSG =
  219 + "Suspicious equality check: MediaStreamTracks should not be checked for equality. Check id() instead."
  220 +
  221 + private val IMPLEMENTATION =
  222 + Implementation(MediaTrackEqualsDetector::class.java, Scope.JAVA_FILE_SCOPE)
  223 +
  224 + @JvmField
  225 + val ISSUE = Issue.create(
  226 + id = "MediaTrackEqualsDetector",
  227 + briefDescription = "Suspicious DiffUtil Equality",
  228 + explanation = """
  229 + `areContentsTheSame` is used by `DiffUtil` to produce diffs. If the \
  230 + method is implemented incorrectly, such as using identity equals \
  231 + instead of equals, or calling equals on a class that has not implemented \
  232 + it, weird visual artifacts can occur.
  233 + """,
  234 + category = Category.CORRECTNESS,
  235 + priority = 4,
  236 + androidSpecific = true,
  237 + moreInfo = "https://issuetracker.google.com/116789824",
  238 + severity = Severity.ERROR,
  239 + implementation = IMPLEMENTATION
  240 + )
  241 + }
  242 +}
  1 +@file:Suppress("UnstableApiUsage", "NewObjectEquality")
  2 +
  3 +package io.livekit.lint
  4 +
  5 +import com.android.tools.lint.checks.infrastructure.LintDetectorTest.bytes
  6 +import com.android.tools.lint.checks.infrastructure.TestFile
  7 +import com.android.tools.lint.checks.infrastructure.TestFiles.java
  8 +import com.android.tools.lint.checks.infrastructure.TestLintTask
  9 +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
  10 +import org.junit.Test
  11 +
  12 +class FlowDelegateUsageDetectorTest {
  13 + @Test
  14 + fun objectEquals() {
  15 + lint()
  16 + .allowMissingSdk()
  17 + .files(
  18 + java(
  19 + """
  20 + package foo;
  21 +
  22 + class Example {
  23 + public boolean foo() {
  24 + Object a = new Object();
  25 + Object b = new Object();
  26 + return a.equals(b);
  27 + }
  28 + }"""
  29 + ).indented()
  30 + )
  31 + .issues(MediaTrackEqualsDetector.ISSUE)
  32 + .run()
  33 + .expectClean()
  34 + }
  35 +
  36 + @Test
  37 + fun objectEqualityOperator() {
  38 + lint()
  39 + .allowMissingSdk()
  40 + .files(
  41 + java(
  42 + """
  43 + package foo;
  44 +
  45 + class Example {
  46 + public boolean foo() {
  47 + Object a = new Object();
  48 + Object b = new Object();
  49 + return a == b;
  50 + }
  51 + }"""
  52 + ).indented()
  53 + )
  54 + .issues(MediaTrackEqualsDetector.ISSUE)
  55 + .run()
  56 + .expectClean()
  57 + }
  58 +
  59 + @Test
  60 + fun mediaTrackEquals() {
  61 + lint()
  62 + .allowMissingSdk()
  63 + .files(
  64 + mediaStreamTrack(),
  65 + java(
  66 + """
  67 + package foo;
  68 + import org.webrtc.MediaStreamTrack;
  69 +
  70 + class Example {
  71 + public boolean foo() {
  72 + MediaStreamTrack a = new MediaStreamTrack();
  73 + MediaStreamTrack b = new MediaStreamTrack();
  74 + return a.equals(b);
  75 + }
  76 + }"""
  77 + ).indented()
  78 + )
  79 + .issues(MediaTrackEqualsDetector.ISSUE)
  80 + .run()
  81 + .expectErrorCount(1)
  82 + }
  83 +
  84 + @Test
  85 + fun mediaTrackEqualityOperator() {
  86 + lint()
  87 + .allowMissingSdk()
  88 + .files(
  89 + mediaStreamTrack(),
  90 + java(
  91 + """
  92 + package foo;
  93 + import org.webrtc.MediaStreamTrack;
  94 +
  95 + class Example {
  96 + public boolean foo() {
  97 + ABC a = new ABC();
  98 + MediaStreamTrack b = new MediaStreamTrack();
  99 + a.equals(b);
  100 + return a == b;
  101 + }
  102 + public boolean equals(Object o){
  103 + return false;
  104 + }
  105 + }"""
  106 + ).indented()
  107 + )
  108 + .issues(MediaTrackEqualsDetector.ISSUE)
  109 + .run()
  110 + .expectErrorCount(1)
  111 + }
  112 +
  113 + @Test
  114 + fun properMediaTrackEquality() {
  115 + lint()
  116 + .allowMissingSdk()
  117 + .files(
  118 + mediaStreamTrack(),
  119 + java(
  120 + """
  121 + package foo;
  122 +
  123 + class Example {
  124 + public boolean foo() {
  125 + MediaStreamTrack a = new MediaStreamTrack();
  126 + MediaStreamTrack b = new MediaStreamTrack();
  127 + return a.getId() == b.getId();
  128 + }
  129 + }"""
  130 + ).indented()
  131 + )
  132 + .issues(MediaTrackEqualsDetector.ISSUE)
  133 + .run()
  134 + .expectClean()
  135 + }
  136 +}
  1 +@file:Suppress("UnstableApiUsage", "NewObjectEquality")
  2 +
  3 +package io.livekit.lint
  4 +
  5 +import com.android.tools.lint.checks.infrastructure.TestFile
  6 +import com.android.tools.lint.checks.infrastructure.TestFiles.java
  7 +import com.android.tools.lint.checks.infrastructure.TestLintTask
  8 +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
  9 +import org.junit.Test
  10 +
  11 +class MediaTrackEqualsDetectorTest {
  12 + @Test
  13 + fun objectEquals() {
  14 + lint()
  15 + .allowMissingSdk()
  16 + .files(
  17 + java(
  18 + """
  19 + package foo;
  20 +
  21 + class Example {
  22 + public boolean foo() {
  23 + Object a = new Object();
  24 + Object b = new Object();
  25 + return a.equals(b);
  26 + }
  27 + }"""
  28 + ).indented()
  29 + )
  30 + .issues(MediaTrackEqualsDetector.ISSUE)
  31 + .run()
  32 + .expectClean()
  33 + }
  34 +
  35 + @Test
  36 + fun objectEqualityOperator() {
  37 + lint()
  38 + .allowMissingSdk()
  39 + .files(
  40 + java(
  41 + """
  42 + package foo;
  43 +
  44 + class Example {
  45 + public boolean foo() {
  46 + Object a = new Object();
  47 + Object b = new Object();
  48 + return a == b;
  49 + }
  50 + }"""
  51 + ).indented()
  52 + )
  53 + .issues(MediaTrackEqualsDetector.ISSUE)
  54 + .run()
  55 + .expectClean()
  56 + }
  57 +
  58 + @Test
  59 + fun badMediaTrackEquals() {
  60 + lint()
  61 + .allowMissingSdk()
  62 + .files(
  63 + mediaStreamTrack(),
  64 + java(
  65 + """
  66 + package foo;
  67 + import org.webrtc.MediaStreamTrack;
  68 +
  69 + class Example {
  70 + public boolean foo() {
  71 + MediaStreamTrack a = new MediaStreamTrack();
  72 + MediaStreamTrack b = new MediaStreamTrack();
  73 + return a.equals(b);
  74 + }
  75 + }"""
  76 + ).indented()
  77 + )
  78 + .issues(MediaTrackEqualsDetector.ISSUE)
  79 + .run()
  80 + .expectErrorCount(1)
  81 + }
  82 +
  83 + @Test
  84 + fun mediaTrackEqualityOperator() {
  85 + lint()
  86 + .allowMissingSdk()
  87 + .files(
  88 + mediaStreamTrack(),
  89 + java(
  90 + """
  91 + package foo;
  92 + import org.webrtc.MediaStreamTrack;
  93 +
  94 + class Example {
  95 + public boolean foo() {
  96 + ABC a = new ABC();
  97 + MediaStreamTrack b = new MediaStreamTrack();
  98 + return a == b;
  99 + }
  100 + public boolean equals(Object o){
  101 + return false;
  102 + }
  103 + }"""
  104 + ).indented()
  105 + )
  106 + .issues(MediaTrackEqualsDetector.ISSUE)
  107 + .run()
  108 + .expectErrorCount(1)
  109 + }
  110 +
  111 + @Test
  112 + fun properMediaTrackEquality() {
  113 + lint()
  114 + .allowMissingSdk()
  115 + .files(
  116 + mediaStreamTrack(),
  117 + java(
  118 + """
  119 + package foo;
  120 +
  121 + class Example {
  122 + public boolean foo() {
  123 + MediaStreamTrack a = new MediaStreamTrack();
  124 + MediaStreamTrack b = new MediaStreamTrack();
  125 + return a.getId() == b.getId();
  126 + }
  127 + }"""
  128 + ).indented()
  129 + )
  130 + .issues(MediaTrackEqualsDetector.ISSUE)
  131 + .run()
  132 + .expectClean()
  133 + }
  134 +}
  135 +
  136 +fun TestLintTask.mediaStreamTrack(): TestLintTask {
  137 + return this.files(
  138 + java(
  139 + """
  140 + package org.webrtc;
  141 +
  142 + class MediaStreamTrack {
  143 + int getId(){
  144 + return 0;
  145 + }
  146 + }
  147 + """
  148 + ).indented()
  149 + )
  150 +}
  151 +
  152 +fun Any.mediaStreamTrack(): TestFile {
  153 + return java(
  154 + """
  155 + package org.webrtc;
  156 +
  157 + class MediaStreamTrack {
  158 + int getId(){
  159 + return 0;
  160 + }
  161 + }
  162 + """
  163 + ).indented()
  164 +}
  1 +package org.webrtc;
  2 +
  3 +class MediaStreamTrack {
  4 + int getId() {
  5 + return 0;
  6 + }
  7 +}
@@ -7,3 +7,4 @@ pluginManagement { @@ -7,3 +7,4 @@ pluginManagement {
7 include ':sample-app', ':sample-app-compose', ':livekit-android-sdk' 7 include ':sample-app', ':sample-app-compose', ':livekit-android-sdk'
8 rootProject.name='livekit-android' 8 rootProject.name='livekit-android'
9 include ':sample-app-common' 9 include ':sample-app-common'
  10 +include ':livekit-lint'