正在显示
15 个修改的文件
包含
829 行增加
和
22 行删除
| 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 | + <module name="livekit-android.sample-app-common" target="11" /> | ||
| 8 | + <module name="livekit-android.sample-app-compose" target="11" /> | ||
| 9 | + </bytecodeTargetLevel> | ||
| 5 | </component> | 10 | </component> |
| 6 | </project> | 11 | </project> |
| @@ -53,16 +53,34 @@ ext { | @@ -53,16 +53,34 @@ ext { | ||
| 53 | versions = [ | 53 | versions = [ |
| 54 | androidx_core : "1.6.0", | 54 | androidx_core : "1.6.0", |
| 55 | androidx_lifecycle: "2.4.0", | 55 | androidx_lifecycle: "2.4.0", |
| 56 | + autoService : '1.0.1', | ||
| 56 | dagger : "2.27", | 57 | dagger : "2.27", |
| 57 | groupie : "2.9.0", | 58 | groupie : "2.9.0", |
| 59 | + junit : "4.13.2", | ||
| 60 | + junitJupiter : "5.5.0", | ||
| 61 | + lint : "30.0.1", | ||
| 58 | protobuf : "3.15.1", | 62 | protobuf : "3.15.1", |
| 59 | ] | 63 | ] |
| 60 | generated = [ | 64 | generated = [ |
| 61 | protoSrc: "$projectDir/protocol", | 65 | protoSrc: "$projectDir/protocol", |
| 62 | ] | 66 | ] |
| 63 | deps = [ | 67 | deps = [ |
| 68 | + auto : [ | ||
| 69 | + 'service' : "com.google.auto.service:auto-service:${versions.autoService}", | ||
| 70 | + 'serviceAnnotations': "com.google.auto.service:auto-service-annotations:${versions.autoService}", | ||
| 71 | + ], | ||
| 64 | kotlinx_coroutines: "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2", | 72 | kotlinx_coroutines: "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2", |
| 65 | - timber : "com.github.ajalt:timberkt:1.5.1", | 73 | + timber : "com.github.ajalt:timberkt:1.5.1", |
| 74 | + // lint | ||
| 75 | + lint : "com.android.tools.lint:lint:${versions.lint}", | ||
| 76 | + lintApi : "com.android.tools.lint:lint-api:${versions.lint}", | ||
| 77 | + lintChecks : "com.android.tools.lint:lint-checks:${versions.lint}", | ||
| 78 | + lintTests : "com.android.tools.lint:lint-tests:${versions.lint}", | ||
| 79 | + | ||
| 80 | + // tests | ||
| 81 | + junit : "junit:junit:${versions.junit}", | ||
| 82 | + junitJupiterApi : "org.junit.jupiter:junit-jupiter-api:${versions.junitJupiter}", | ||
| 83 | + junitJupiterEngine: "org.junit.jupiter:junit-jupiter-engine:${versions.junitJupiter}", | ||
| 66 | ] | 84 | ] |
| 67 | annotations = [ | 85 | annotations = [ |
| 68 | ] | 86 | ] |
| @@ -118,6 +118,9 @@ dependencies { | @@ -118,6 +118,9 @@ dependencies { | ||
| 118 | implementation deps.timber | 118 | implementation deps.timber |
| 119 | implementation 'com.vdurmont:semver4j:3.1.0' | 119 | implementation 'com.vdurmont:semver4j:3.1.0' |
| 120 | 120 | ||
| 121 | + lintChecks project(':livekit-lint') | ||
| 122 | + lintPublish project(':livekit-lint') | ||
| 123 | + | ||
| 121 | testImplementation 'junit:junit:4.13.2' | 124 | testImplementation 'junit:junit:4.13.2' |
| 122 | testImplementation 'org.robolectric:robolectric:4.6' | 125 | testImplementation 'org.robolectric:robolectric:4.6' |
| 123 | testImplementation 'org.mockito:mockito-core:4.0.0' | 126 | testImplementation 'org.mockito:mockito-core:4.0.0' |
| @@ -16,6 +16,7 @@ import io.livekit.android.events.RoomEvent | @@ -16,6 +16,7 @@ import io.livekit.android.events.RoomEvent | ||
| 16 | import io.livekit.android.renderer.TextureViewRenderer | 16 | import io.livekit.android.renderer.TextureViewRenderer |
| 17 | import io.livekit.android.room.participant.* | 17 | import io.livekit.android.room.participant.* |
| 18 | import io.livekit.android.room.track.* | 18 | import io.livekit.android.room.track.* |
| 19 | +import io.livekit.android.util.FlowObservable | ||
| 19 | import io.livekit.android.util.LKLog | 20 | import io.livekit.android.util.LKLog |
| 20 | import io.livekit.android.util.flow | 21 | import io.livekit.android.util.flow |
| 21 | import io.livekit.android.util.flowDelegate | 22 | import io.livekit.android.util.flowDelegate |
| @@ -60,23 +61,18 @@ constructor( | @@ -60,23 +61,18 @@ constructor( | ||
| 60 | @Deprecated("Use events instead.") | 61 | @Deprecated("Use events instead.") |
| 61 | var listener: RoomListener? = null | 62 | var listener: RoomListener? = null |
| 62 | 63 | ||
| 63 | - /** | ||
| 64 | - * Changes can be observed by using [io.livekit.android.util.flow] | ||
| 65 | - */ | 64 | + @get:FlowObservable |
| 66 | var sid: Sid? by flowDelegate(null) | 65 | var sid: Sid? by flowDelegate(null) |
| 67 | - /** | ||
| 68 | - * Changes can be observed by using [io.livekit.android.util.flow] | ||
| 69 | - */ | 66 | + |
| 67 | + @get:FlowObservable | ||
| 70 | var name: String? by flowDelegate(null) | 68 | var name: String? by flowDelegate(null) |
| 71 | private set | 69 | private set |
| 72 | - /** | ||
| 73 | - * Changes can be observed by using [io.livekit.android.util.flow] | ||
| 74 | - */ | 70 | + |
| 71 | + @get:FlowObservable | ||
| 75 | var state: State by flowDelegate(State.DISCONNECTED) | 72 | var state: State by flowDelegate(State.DISCONNECTED) |
| 76 | private set | 73 | private set |
| 77 | - /** | ||
| 78 | - * Changes can be observed by using [io.livekit.android.util.flow] | ||
| 79 | - */ | 74 | + |
| 75 | + @get:FlowObservable | ||
| 80 | var metadata: String? by flowDelegate(null) | 76 | var metadata: String? by flowDelegate(null) |
| 81 | private set | 77 | private set |
| 82 | 78 | ||
| @@ -112,17 +108,16 @@ constructor( | @@ -112,17 +108,16 @@ constructor( | ||
| 112 | 108 | ||
| 113 | lateinit var localParticipant: LocalParticipant | 109 | lateinit var localParticipant: LocalParticipant |
| 114 | private set | 110 | private set |
| 111 | + | ||
| 115 | private var mutableRemoteParticipants by flowDelegate(emptyMap<String, RemoteParticipant>()) | 112 | private var mutableRemoteParticipants by flowDelegate(emptyMap<String, RemoteParticipant>()) |
| 116 | - /** | ||
| 117 | - * Changes can be observed by using [io.livekit.android.util.flow] | ||
| 118 | - */ | 113 | + |
| 114 | + @get:FlowObservable | ||
| 119 | val remoteParticipants: Map<String, RemoteParticipant> | 115 | val remoteParticipants: Map<String, RemoteParticipant> |
| 120 | get() = mutableRemoteParticipants | 116 | get() = mutableRemoteParticipants |
| 121 | 117 | ||
| 122 | private var mutableActiveSpeakers by flowDelegate(emptyList<Participant>()) | 118 | private var mutableActiveSpeakers by flowDelegate(emptyList<Participant>()) |
| 123 | - /** | ||
| 124 | - * Changes can be observed by using [io.livekit.android.util.flow] | ||
| 125 | - */ | 119 | + |
| 120 | + @get:FlowObservable | ||
| 126 | val activeSpeakers: List<Participant> | 121 | val activeSpeakers: List<Participant> |
| 127 | get() = mutableActiveSpeakers | 122 | get() = mutableActiveSpeakers |
| 128 | 123 |
| @@ -7,6 +7,7 @@ import io.livekit.android.room.track.LocalTrackPublication | @@ -7,6 +7,7 @@ import io.livekit.android.room.track.LocalTrackPublication | ||
| 7 | import io.livekit.android.room.track.RemoteTrackPublication | 7 | import io.livekit.android.room.track.RemoteTrackPublication |
| 8 | import io.livekit.android.room.track.Track | 8 | import io.livekit.android.room.track.Track |
| 9 | import io.livekit.android.room.track.TrackPublication | 9 | import io.livekit.android.room.track.TrackPublication |
| 10 | +import io.livekit.android.util.FlowObservable | ||
| 10 | import io.livekit.android.util.flow | 11 | import io.livekit.android.util.flow |
| 11 | import io.livekit.android.util.flowDelegate | 12 | import io.livekit.android.util.flowDelegate |
| 12 | import kotlinx.coroutines.CoroutineDispatcher | 13 | import kotlinx.coroutines.CoroutineDispatcher |
| @@ -32,23 +33,27 @@ open class Participant( | @@ -32,23 +33,27 @@ open class Participant( | ||
| 32 | /** | 33 | /** |
| 33 | * Changes can be observed by using [io.livekit.android.util.flow] | 34 | * Changes can be observed by using [io.livekit.android.util.flow] |
| 34 | */ | 35 | */ |
| 36 | + @get:FlowObservable | ||
| 35 | var participantInfo: LivekitModels.ParticipantInfo? by flowDelegate(null) | 37 | var participantInfo: LivekitModels.ParticipantInfo? by flowDelegate(null) |
| 36 | private set | 38 | private set |
| 37 | 39 | ||
| 38 | /** | 40 | /** |
| 39 | * Changes can be observed by using [io.livekit.android.util.flow] | 41 | * Changes can be observed by using [io.livekit.android.util.flow] |
| 40 | */ | 42 | */ |
| 43 | + @get:FlowObservable | ||
| 41 | var identity: String? by flowDelegate(identity) | 44 | var identity: String? by flowDelegate(identity) |
| 42 | internal set | 45 | internal set |
| 43 | 46 | ||
| 44 | /** | 47 | /** |
| 45 | * Changes can be observed by using [io.livekit.android.util.flow] | 48 | * Changes can be observed by using [io.livekit.android.util.flow] |
| 46 | */ | 49 | */ |
| 50 | + @get:FlowObservable | ||
| 47 | var audioLevel: Float by flowDelegate(0f) | 51 | var audioLevel: Float by flowDelegate(0f) |
| 48 | internal set | 52 | internal set |
| 49 | /** | 53 | /** |
| 50 | * Changes can be observed by using [io.livekit.android.util.flow] | 54 | * Changes can be observed by using [io.livekit.android.util.flow] |
| 51 | */ | 55 | */ |
| 56 | + @get:FlowObservable | ||
| 52 | var isSpeaking: Boolean by flowDelegate(false) { newValue, oldValue -> | 57 | var isSpeaking: Boolean by flowDelegate(false) { newValue, oldValue -> |
| 53 | if (newValue != oldValue) { | 58 | if (newValue != oldValue) { |
| 54 | listener?.onSpeakingChanged(this) | 59 | listener?.onSpeakingChanged(this) |
| @@ -60,6 +65,7 @@ open class Participant( | @@ -60,6 +65,7 @@ open class Participant( | ||
| 60 | /** | 65 | /** |
| 61 | * Changes can be observed by using [io.livekit.android.util.flow] | 66 | * Changes can be observed by using [io.livekit.android.util.flow] |
| 62 | */ | 67 | */ |
| 68 | + @get:FlowObservable | ||
| 63 | var metadata: String? by flowDelegate(null) { newMetadata, oldMetadata -> | 69 | var metadata: String? by flowDelegate(null) { newMetadata, oldMetadata -> |
| 64 | if (newMetadata != oldMetadata) { | 70 | if (newMetadata != oldMetadata) { |
| 65 | listener?.onMetadataChanged(this, oldMetadata) | 71 | listener?.onMetadataChanged(this, oldMetadata) |
| @@ -72,6 +78,7 @@ open class Participant( | @@ -72,6 +78,7 @@ open class Participant( | ||
| 72 | /** | 78 | /** |
| 73 | * Changes can be observed by using [io.livekit.android.util.flow] | 79 | * Changes can be observed by using [io.livekit.android.util.flow] |
| 74 | */ | 80 | */ |
| 81 | + @get:FlowObservable | ||
| 75 | var connectionQuality by flowDelegate(ConnectionQuality.UNKNOWN) | 82 | var connectionQuality by flowDelegate(ConnectionQuality.UNKNOWN) |
| 76 | internal set | 83 | internal set |
| 77 | 84 | ||
| @@ -93,11 +100,13 @@ open class Participant( | @@ -93,11 +100,13 @@ open class Participant( | ||
| 93 | /** | 100 | /** |
| 94 | * Changes can be observed by using [io.livekit.android.util.flow] | 101 | * Changes can be observed by using [io.livekit.android.util.flow] |
| 95 | */ | 102 | */ |
| 103 | + @get:FlowObservable | ||
| 96 | var tracks by flowDelegate(emptyMap<String, TrackPublication>()) | 104 | var tracks by flowDelegate(emptyMap<String, TrackPublication>()) |
| 97 | protected set | 105 | protected set |
| 98 | /** | 106 | /** |
| 99 | * Changes can be observed by using [io.livekit.android.util.flow] | 107 | * Changes can be observed by using [io.livekit.android.util.flow] |
| 100 | */ | 108 | */ |
| 109 | + @get:FlowObservable | ||
| 101 | val audioTracks by flowDelegate( | 110 | val audioTracks by flowDelegate( |
| 102 | stateFlow = ::tracks.flow | 111 | stateFlow = ::tracks.flow |
| 103 | .map { it.filterValues { publication -> publication.kind == Track.Kind.AUDIO } } | 112 | .map { it.filterValues { publication -> publication.kind == Track.Kind.AUDIO } } |
| @@ -106,6 +115,7 @@ open class Participant( | @@ -106,6 +115,7 @@ open class Participant( | ||
| 106 | /** | 115 | /** |
| 107 | * Changes can be observed by using [io.livekit.android.util.flow] | 116 | * Changes can be observed by using [io.livekit.android.util.flow] |
| 108 | */ | 117 | */ |
| 118 | + @get:FlowObservable | ||
| 109 | val videoTracks by flowDelegate( | 119 | val videoTracks by flowDelegate( |
| 110 | stateFlow = ::tracks.flow | 120 | stateFlow = ::tracks.flow |
| 111 | .map { | 121 | .map { |
| 1 | package io.livekit.android.room.track | 1 | package io.livekit.android.room.track |
| 2 | 2 | ||
| 3 | import io.livekit.android.room.participant.Participant | 3 | import io.livekit.android.room.participant.Participant |
| 4 | +import io.livekit.android.util.FlowObservable | ||
| 4 | import io.livekit.android.util.flowDelegate | 5 | import io.livekit.android.util.flowDelegate |
| 5 | import livekit.LivekitModels | 6 | import livekit.LivekitModels |
| 6 | import java.lang.ref.WeakReference | 7 | import java.lang.ref.WeakReference |
| @@ -10,6 +11,7 @@ open class TrackPublication( | @@ -10,6 +11,7 @@ open class TrackPublication( | ||
| 10 | track: Track?, | 11 | track: Track?, |
| 11 | participant: Participant | 12 | participant: Participant |
| 12 | ) { | 13 | ) { |
| 14 | + @get:FlowObservable | ||
| 13 | open var track: Track? by flowDelegate(track) | 15 | open var track: Track? by flowDelegate(track) |
| 14 | internal set | 16 | internal set |
| 15 | var name: String | 17 | var name: String |
| @@ -62,7 +62,16 @@ internal val <T> KProperty0<T>.delegate: Any? | @@ -62,7 +62,16 @@ internal val <T> KProperty0<T>.delegate: Any? | ||
| 62 | val <T> KProperty0<T>.flow: StateFlow<T> | 62 | val <T> KProperty0<T>.flow: StateFlow<T> |
| 63 | get() = delegate as StateFlow<T> | 63 | get() = delegate as StateFlow<T> |
| 64 | 64 | ||
| 65 | -class MutableStateFlowDelegate<T> | 65 | +/** |
| 66 | + * Indicates that the target property changes can be observed with [flow]. | ||
| 67 | + */ | ||
| 68 | +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY_GETTER) | ||
| 69 | +@Retention(AnnotationRetention.BINARY) | ||
| 70 | +@MustBeDocumented | ||
| 71 | +annotation class FlowObservable | ||
| 72 | + | ||
| 73 | +@FlowObservable | ||
| 74 | +internal class MutableStateFlowDelegate<T> | ||
| 66 | internal constructor( | 75 | internal constructor( |
| 67 | private val flow: MutableStateFlow<T>, | 76 | private val flow: MutableStateFlow<T>, |
| 68 | private val onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null | 77 | private val onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null |
| @@ -82,7 +91,8 @@ internal constructor( | @@ -82,7 +91,8 @@ internal constructor( | ||
| 82 | } | 91 | } |
| 83 | } | 92 | } |
| 84 | 93 | ||
| 85 | -class StateFlowDelegate<T> | 94 | +@FlowObservable |
| 95 | +internal class StateFlowDelegate<T> | ||
| 86 | internal constructor( | 96 | internal constructor( |
| 87 | private val flow: StateFlow<T> | 97 | private val flow: StateFlow<T> |
| 88 | ) : StateFlow<T> by flow { | 98 | ) : StateFlow<T> by flow { |
livekit-lint/.gitignore
0 → 100644
| 1 | +/build |
livekit-lint/build.gradle
0 → 100644
| 1 | +plugins { | ||
| 2 | + id 'java-library' | ||
| 3 | + id 'kotlin' | ||
| 4 | + id 'kotlin-kapt' | ||
| 5 | +} | ||
| 6 | + | ||
| 7 | +java { | ||
| 8 | + sourceCompatibility = JavaVersion.VERSION_1_8 | ||
| 9 | + targetCompatibility = JavaVersion.VERSION_1_8 | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +dependencies { | ||
| 13 | + | ||
| 14 | + compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" | ||
| 15 | + // used for lint rules | ||
| 16 | + compileOnly deps.lintApi | ||
| 17 | + compileOnly deps.lintChecks | ||
| 18 | + compileOnly deps.lintTests | ||
| 19 | + | ||
| 20 | + // Handle creating manifests for lint checker | ||
| 21 | + compileOnly deps.auto.serviceAnnotations | ||
| 22 | + kapt deps.auto.service | ||
| 23 | + | ||
| 24 | + // test lint | ||
| 25 | + testImplementation deps.lint | ||
| 26 | + testImplementation deps.lintTests | ||
| 27 | + | ||
| 28 | + compileOnly "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" | ||
| 29 | + // test runners | ||
| 30 | + testImplementation deps.junit | ||
| 31 | + testImplementation deps.junitJupiterApi | ||
| 32 | + testRuntimeOnly deps.junitJupiterEngine | ||
| 33 | +} | ||
| 34 | +test { | ||
| 35 | + environment "LINT_TEST_KOTLINC", "" | ||
| 36 | +} |
| 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.detector.api.* | ||
| 22 | +import com.intellij.psi.PsiClassType | ||
| 23 | +import com.intellij.psi.PsiElement | ||
| 24 | +import com.intellij.psi.PsiField | ||
| 25 | +import com.intellij.psi.PsiMethod | ||
| 26 | +import org.jetbrains.uast.UCallableReferenceExpression | ||
| 27 | +import org.jetbrains.uast.UReferenceExpression | ||
| 28 | +import org.jetbrains.uast.kotlin.KotlinUQualifiedReferenceExpression | ||
| 29 | +import org.jetbrains.uast.tryResolve | ||
| 30 | + | ||
| 31 | +/** Checks related to DiffUtil computation. */ | ||
| 32 | +class FlowDelegateUsageDetector : Detector(), SourceCodeScanner { | ||
| 33 | + | ||
| 34 | + override fun visitReference(context: JavaContext, reference: UReferenceExpression, referenced: PsiElement) { | ||
| 35 | + | ||
| 36 | + // Check if we're actually trying to access the flow delegate | ||
| 37 | + val referencedMethod = referenced as? PsiMethod ?: return | ||
| 38 | + if (referenced.name != GET_FLOW || referencedMethod.containingClass?.qualifiedName != FLOW_DELEGATE) { | ||
| 39 | + return | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + // This should get the getter we're trying to receive the flow from. | ||
| 43 | + val parent = reference.uastParent as? KotlinUQualifiedReferenceExpression | ||
| 44 | + val rec = parent?.receiver | ||
| 45 | + val resolve = rec?.tryResolve() | ||
| 46 | + println("parent: $parent, rec: $rec, resolve: $resolve") | ||
| 47 | + val receiver = ((reference.uastParent as? KotlinUQualifiedReferenceExpression) | ||
| 48 | + ?.receiver as? UCallableReferenceExpression) | ||
| 49 | + ?.resolve() | ||
| 50 | + | ||
| 51 | + val isAnnotated = when (receiver) { | ||
| 52 | + is PsiMethod -> { | ||
| 53 | + println("${receiver.name}, ${receiver.annotations.fold("") { total, next -> "$total, ${next.text}" }}") | ||
| 54 | + receiver.hasAnnotation(FLOW_OBSERVABLE_ANNOTATION) | ||
| 55 | + } | ||
| 56 | + is PsiField -> { | ||
| 57 | + val receiverClass = (receiver.type as? PsiClassType)?.resolve() | ||
| 58 | + println("${receiverClass}, ${receiverClass?.annotations?.fold("") { total, next -> "$total, ${next.text}" }}") | ||
| 59 | + receiverClass?.hasAnnotation(FLOW_OBSERVABLE_ANNOTATION) ?: false | ||
| 60 | + } | ||
| 61 | + else -> { | ||
| 62 | + false | ||
| 63 | + } | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + if (!isAnnotated) { | ||
| 67 | + val message = DEFAULT_MSG | ||
| 68 | + val location = context.getLocation(reference) | ||
| 69 | + context.report(ISSUE, reference, location, message) | ||
| 70 | + } | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + override fun getApplicableReferenceNames(): List<String>? = | ||
| 74 | + listOf("flow") | ||
| 75 | + | ||
| 76 | + | ||
| 77 | + companion object { | ||
| 78 | + | ||
| 79 | + // The name of the method for the flow accessor | ||
| 80 | + private const val GET_FLOW = "getFlow" | ||
| 81 | + | ||
| 82 | + // The containing file and implicitly generated class | ||
| 83 | + private const val FLOW_DELEGATE = "io.livekit.android.util.FlowDelegateKt" | ||
| 84 | + | ||
| 85 | + private const val FLOW_OBSERVABLE_ANNOTATION = "io.livekit.android.util.FlowObservable" | ||
| 86 | + | ||
| 87 | + private const val DEFAULT_MSG = | ||
| 88 | + "Incorrect flow property usage: Only properties marked with the @FlowObservable annotation can be observed using `io.livekit.android.util.flow`. Improper usage will result in a NullPointerException." | ||
| 89 | + | ||
| 90 | + private val IMPLEMENTATION = | ||
| 91 | + Implementation(FlowDelegateUsageDetector::class.java, Scope.JAVA_FILE_SCOPE) | ||
| 92 | + | ||
| 93 | + @JvmField | ||
| 94 | + val ISSUE = Issue.create( | ||
| 95 | + id = "FlowDelegateUsageDetector", | ||
| 96 | + briefDescription = "flow on a non-@FlowObservable property", | ||
| 97 | + explanation = """ | ||
| 98 | + Only properties marked with the @FlowObservable annotation can be observed using | ||
| 99 | + `io.livekit.android.util.flow`. | ||
| 100 | + """, | ||
| 101 | + category = Category.CORRECTNESS, | ||
| 102 | + priority = 4, | ||
| 103 | + androidSpecific = true, | ||
| 104 | + moreInfo = "https://issuetracker.google.com/116789824", | ||
| 105 | + severity = Severity.ERROR, | ||
| 106 | + implementation = IMPLEMENTATION | ||
| 107 | + ) | ||
| 108 | + } | ||
| 109 | +} |
| 1 | +package io.livekit.lint | ||
| 2 | + | ||
| 3 | +import com.android.tools.lint.client.api.IssueRegistry | ||
| 4 | +import com.android.tools.lint.client.api.Vendor | ||
| 5 | +import com.android.tools.lint.detector.api.CURRENT_API | ||
| 6 | +import com.android.tools.lint.detector.api.Issue | ||
| 7 | +import com.google.auto.service.AutoService | ||
| 8 | + | ||
| 9 | +@Suppress("UnstableApiUsage", "unused") | ||
| 10 | +@AutoService(value = [IssueRegistry::class]) | ||
| 11 | +class IssueRegistry : IssueRegistry() { | ||
| 12 | + | ||
| 13 | + override val api: Int = CURRENT_API | ||
| 14 | + | ||
| 15 | + override val vendor: Vendor = Vendor( | ||
| 16 | + vendorName = "LiveKit", | ||
| 17 | + identifier = "io.livekit.android", | ||
| 18 | + feedbackUrl = "https://github.com/livekit/client-sdk-android", | ||
| 19 | + ) | ||
| 20 | + | ||
| 21 | + override val issues: List<Issue> | ||
| 22 | + get() = listOf(MediaTrackEqualsDetector.ISSUE, FlowDelegateUsageDetector.ISSUE) | ||
| 23 | +} |
| 1 | +@file:Suppress("UnstableApiUsage") // We know that Lint API's aren't final. | ||
| 2 | +package io.livekit.lint | ||
| 3 | + | ||
| 4 | +import com.android.tools.lint.client.api.UElementHandler | ||
| 5 | +import com.android.tools.lint.detector.api.* | ||
| 6 | +import com.intellij.psi.PsiClassType | ||
| 7 | +import org.jetbrains.uast.UBinaryExpression | ||
| 8 | +import org.jetbrains.uast.UCallExpression | ||
| 9 | +import org.jetbrains.uast.UastBinaryOperator | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * Detects MediaStreamTrack.equals() usage. This is generally a mistake and should not be used. | ||
| 13 | + */ | ||
| 14 | +class MediaTrackEqualsDetector : Detector(), SourceCodeScanner { | ||
| 15 | + | ||
| 16 | + override fun getApplicableUastTypes() = | ||
| 17 | + listOf(UBinaryExpression::class.java, UCallExpression::class.java) | ||
| 18 | + | ||
| 19 | + override fun createUastHandler(context: JavaContext): UElementHandler? { | ||
| 20 | + return object : UElementHandler() { | ||
| 21 | + | ||
| 22 | + override fun visitBinaryExpression(node: UBinaryExpression) { | ||
| 23 | + checkExpression(context, node) | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + override fun visitCallExpression(node: UCallExpression) { | ||
| 27 | + checkCall(context, node) | ||
| 28 | + } | ||
| 29 | + } | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + private fun checkCall(context: JavaContext, node: UCallExpression) { | ||
| 33 | + if (node.methodName == "equals") { | ||
| 34 | + val left = node.receiverType ?: return | ||
| 35 | + val right = node.valueArguments.takeIf { it.isNotEmpty() } | ||
| 36 | + ?.get(0) | ||
| 37 | + ?.getExpressionType() | ||
| 38 | + ?: return | ||
| 39 | + if (left is PsiClassType && right is PsiClassType | ||
| 40 | + && (left.canonicalText == MEDIA_STREAM_TRACK || right.canonicalText == MEDIA_STREAM_TRACK) | ||
| 41 | + ) { | ||
| 42 | + val message = DEFAULT_MSG | ||
| 43 | + val location = context.getLocation(node) | ||
| 44 | + context.report(ISSUE, node, location, message) | ||
| 45 | + } | ||
| 46 | + } | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + private fun checkExpression(context: JavaContext, node: UBinaryExpression) { | ||
| 50 | + if (node.operator == UastBinaryOperator.IDENTITY_EQUALS || | ||
| 51 | + node.operator == UastBinaryOperator.EQUALS | ||
| 52 | + ) { | ||
| 53 | + val left = node.leftOperand.getExpressionType() ?: return | ||
| 54 | + val right = node.rightOperand.getExpressionType() ?: return | ||
| 55 | + if (left is PsiClassType && right is PsiClassType | ||
| 56 | + && (left.canonicalText == MEDIA_STREAM_TRACK || right.canonicalText == MEDIA_STREAM_TRACK) | ||
| 57 | + ) { | ||
| 58 | + val message = DEFAULT_MSG | ||
| 59 | + val location = node.operatorIdentifier?.let { | ||
| 60 | + context.getLocation(it) | ||
| 61 | + } ?: context.getLocation(node) | ||
| 62 | + context.report(ISSUE, node, location, message) | ||
| 63 | + } | ||
| 64 | + } | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + companion object { | ||
| 68 | + private const val MEDIA_STREAM_TRACK = "org.webrtc.MediaStreamTrack" | ||
| 69 | + | ||
| 70 | + private const val DEFAULT_MSG = | ||
| 71 | + "Suspicious equality check: MediaStreamTracks should not be checked for equality. Check id() instead." | ||
| 72 | + | ||
| 73 | + private val IMPLEMENTATION = | ||
| 74 | + Implementation(MediaTrackEqualsDetector::class.java, Scope.JAVA_FILE_SCOPE) | ||
| 75 | + | ||
| 76 | + @JvmField | ||
| 77 | + val ISSUE = Issue.create( | ||
| 78 | + id = "MediaTrackEqualsDetector", | ||
| 79 | + briefDescription = "Suspicious MediaStreamTrack Equality", | ||
| 80 | + explanation = """ | ||
| 81 | + MediaStreamTrack does not implement `equals`, and therefore cannot be relied upon. | ||
| 82 | + Additionally, many MediaStreamTrack objects may exist for the same underlying stream, | ||
| 83 | + and therefore the identity operator `===` is unreliable. | ||
| 84 | + """, | ||
| 85 | + category = Category.CORRECTNESS, | ||
| 86 | + priority = 4, | ||
| 87 | + androidSpecific = true, | ||
| 88 | + moreInfo = "https://github.com/livekit/client-sdk-android/commit/01152f2ac01dae59759383d587cdc21035718b8e", | ||
| 89 | + severity = Severity.ERROR, | ||
| 90 | + implementation = IMPLEMENTATION | ||
| 91 | + ) | ||
| 92 | + } | ||
| 93 | +} |
| 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.kotlin | ||
| 7 | +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint | ||
| 8 | +import org.junit.Test | ||
| 9 | + | ||
| 10 | +class FlowDelegateUsageDetectorTest { | ||
| 11 | + @Test | ||
| 12 | + fun normalFlowAccess() { | ||
| 13 | + lint() | ||
| 14 | + .allowMissingSdk() | ||
| 15 | + .files( | ||
| 16 | + flowAccess(), | ||
| 17 | + stateFlow(), | ||
| 18 | + kotlin( | ||
| 19 | + """ | ||
| 20 | + package foo | ||
| 21 | + import io.livekit.android.util.FlowObservable | ||
| 22 | + import io.livekit.android.util.flow | ||
| 23 | + import io.livekit.android.util.flowDelegate | ||
| 24 | + class Example { | ||
| 25 | + @get:FlowObservable | ||
| 26 | + val value: Int by flowDelegate(0) | ||
| 27 | + fun foo() { | ||
| 28 | + ::value.flow | ||
| 29 | + return | ||
| 30 | + } | ||
| 31 | + }""" | ||
| 32 | + ).indented() | ||
| 33 | + ) | ||
| 34 | + .issues(FlowDelegateUsageDetector.ISSUE) | ||
| 35 | + .run() | ||
| 36 | + .expectClean() | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + @Test | ||
| 40 | + fun thisColonAccess() { | ||
| 41 | + lint() | ||
| 42 | + .allowMissingSdk() | ||
| 43 | + .files( | ||
| 44 | + flowAccess(), | ||
| 45 | + stateFlow(), | ||
| 46 | + kotlin( | ||
| 47 | + """ | ||
| 48 | + package foo | ||
| 49 | + import io.livekit.android.util.FlowObservable | ||
| 50 | + import io.livekit.android.util.flow | ||
| 51 | + import io.livekit.android.util.flowDelegate | ||
| 52 | + class Example { | ||
| 53 | + @get:FlowObservable | ||
| 54 | + val value: Int by flowDelegate(0) | ||
| 55 | + fun foo() { | ||
| 56 | + this::value.flow | ||
| 57 | + return | ||
| 58 | + } | ||
| 59 | + }""" | ||
| 60 | + ).indented() | ||
| 61 | + ) | ||
| 62 | + .issues(FlowDelegateUsageDetector.ISSUE) | ||
| 63 | + .run() | ||
| 64 | + .expectClean() | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + @Test | ||
| 68 | + fun otherClassAccess() { | ||
| 69 | + lint() | ||
| 70 | + .allowMissingSdk() | ||
| 71 | + .files( | ||
| 72 | + flowAccess(), | ||
| 73 | + stateFlow(), | ||
| 74 | + kotlin( | ||
| 75 | + """ | ||
| 76 | + package foo | ||
| 77 | + import io.livekit.android.util.FlowObservable | ||
| 78 | + import io.livekit.android.util.flow | ||
| 79 | + import io.livekit.android.util.flowDelegate | ||
| 80 | + class FlowContainer { | ||
| 81 | + @get:FlowObservable | ||
| 82 | + val value: Int by flowDelegate(0) | ||
| 83 | + } | ||
| 84 | + class Example { | ||
| 85 | + fun foo() { | ||
| 86 | + val container = FlowContainer() | ||
| 87 | + container::value.flow | ||
| 88 | + return | ||
| 89 | + } | ||
| 90 | + }""" | ||
| 91 | + ).indented() | ||
| 92 | + ) | ||
| 93 | + .issues(FlowDelegateUsageDetector.ISSUE) | ||
| 94 | + .run() | ||
| 95 | + .expectClean() | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + @Test | ||
| 99 | + fun parenthesesClassAccess() { | ||
| 100 | + lint() | ||
| 101 | + .allowMissingSdk() | ||
| 102 | + .files( | ||
| 103 | + flowAccess(), | ||
| 104 | + stateFlow(), | ||
| 105 | + kotlin( | ||
| 106 | + """ | ||
| 107 | + package foo | ||
| 108 | + import io.livekit.android.util.FlowObservable | ||
| 109 | + import io.livekit.android.util.flow | ||
| 110 | + import io.livekit.android.util.flowDelegate | ||
| 111 | + class FlowContainer { | ||
| 112 | + @get:FlowObservable | ||
| 113 | + val value: Int by flowDelegate(0) | ||
| 114 | + } | ||
| 115 | + class Example { | ||
| 116 | + fun foo() { | ||
| 117 | + val container = FlowContainer() | ||
| 118 | + (container)::value.flow | ||
| 119 | + return | ||
| 120 | + } | ||
| 121 | + }""" | ||
| 122 | + ).indented() | ||
| 123 | + ) | ||
| 124 | + .issues(FlowDelegateUsageDetector.ISSUE) | ||
| 125 | + .run() | ||
| 126 | + .expectClean() | ||
| 127 | + } | ||
| 128 | + | ||
| 129 | + @Test | ||
| 130 | + fun roundaboutAccess() { | ||
| 131 | + lint() | ||
| 132 | + .allowMissingSdk() | ||
| 133 | + .files( | ||
| 134 | + flowAccess(), | ||
| 135 | + stateFlow(), | ||
| 136 | + kotlin( | ||
| 137 | + """ | ||
| 138 | + package foo | ||
| 139 | + import io.livekit.android.util.FlowObservable | ||
| 140 | + import io.livekit.android.util.flow | ||
| 141 | + import io.livekit.android.util.flowDelegate | ||
| 142 | + class FlowContainer { | ||
| 143 | + var value: Int by flowDelegate(0) | ||
| 144 | + @get:FlowObservable | ||
| 145 | + val otherValue: Int | ||
| 146 | + get() = value | ||
| 147 | + } | ||
| 148 | + class Example { | ||
| 149 | + fun foo() { | ||
| 150 | + val container = FlowContainer() | ||
| 151 | + container::otherValue.flow | ||
| 152 | + return | ||
| 153 | + } | ||
| 154 | + }""" | ||
| 155 | + ).indented() | ||
| 156 | + ) | ||
| 157 | + .issues(FlowDelegateUsageDetector.ISSUE) | ||
| 158 | + .run() | ||
| 159 | + .expectClean() | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + @Test | ||
| 163 | + fun nonAnnotatedFlowAccess() { | ||
| 164 | + lint() | ||
| 165 | + .allowMissingSdk() | ||
| 166 | + .files( | ||
| 167 | + flowAccess(), | ||
| 168 | + stateFlow(), | ||
| 169 | + kotlin( | ||
| 170 | + """ | ||
| 171 | + package foo | ||
| 172 | + import io.livekit.android.util.FlowObservable | ||
| 173 | + import io.livekit.android.util.flow | ||
| 174 | + import io.livekit.android.util.flowDelegate | ||
| 175 | + class Example { | ||
| 176 | + val value: Int = 0 | ||
| 177 | + fun foo() { | ||
| 178 | + this::value.flow | ||
| 179 | + return | ||
| 180 | + } | ||
| 181 | + }""" | ||
| 182 | + ).indented() | ||
| 183 | + ) | ||
| 184 | + .issues(FlowDelegateUsageDetector.ISSUE) | ||
| 185 | + .run() | ||
| 186 | + .expectErrorCount(1) | ||
| 187 | + } | ||
| 188 | +} | ||
| 189 | + | ||
| 190 | +fun flowAccess(): TestFile { | ||
| 191 | + return kotlin( | ||
| 192 | + "io/livekit/android/util/FlowDelegate.kt", | ||
| 193 | + """ | ||
| 194 | + package io.livekit.android.util | ||
| 195 | + | ||
| 196 | + import kotlin.reflect.KProperty | ||
| 197 | + import kotlin.reflect.KProperty0 | ||
| 198 | + import kotlinx.coroutines.flow.StateFlow | ||
| 199 | + import kotlinx.coroutines.flow.MutableStateFlow | ||
| 200 | + | ||
| 201 | + internal val <T> KProperty0<T>.delegate: Any? | ||
| 202 | + get() { getDelegate() } | ||
| 203 | + | ||
| 204 | + @Suppress("UNCHECKED_CAST") | ||
| 205 | + val <T> KProperty0<T>.flow: StateFlow<T> | ||
| 206 | + get() = delegate as StateFlow<T> | ||
| 207 | + | ||
| 208 | + @Target(AnnotationTarget.PROPERTY_GETTER) | ||
| 209 | + @Retention(AnnotationRetention.SOURCE) | ||
| 210 | + @MustBeDocumented | ||
| 211 | + annotation class FlowObservable | ||
| 212 | + | ||
| 213 | + @FlowObservable | ||
| 214 | + class MutableStateFlowDelegate<T> | ||
| 215 | + internal constructor( | ||
| 216 | + private val flow: MutableStateFlow<T>, | ||
| 217 | + private val onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null | ||
| 218 | + ) : MutableStateFlow<T> by flow { | ||
| 219 | + | ||
| 220 | + operator fun getValue(thisRef: Any?, property: KProperty<*>): T { | ||
| 221 | + return flow.value | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { | ||
| 225 | + val oldValue = flow.value | ||
| 226 | + flow.value = value | ||
| 227 | + onSetValue?.invoke(value, oldValue) | ||
| 228 | + } | ||
| 229 | + } | ||
| 230 | + | ||
| 231 | + public fun <T> flowDelegate( | ||
| 232 | + initialValue: T, | ||
| 233 | + onSetValue: ((newValue: T, oldValue: T) -> Unit)? = null | ||
| 234 | + ): MutableStateFlowDelegate<T> { | ||
| 235 | + return MutableStateFlowDelegate(MutableStateFlow(initialValue), onSetValue) | ||
| 236 | + } | ||
| 237 | + """ | ||
| 238 | + ).indented() | ||
| 239 | + .within("src") | ||
| 240 | +} | ||
| 241 | + | ||
| 242 | +fun stateFlow(): TestFile { | ||
| 243 | + | ||
| 244 | + return kotlin( | ||
| 245 | + """ | ||
| 246 | + package kotlinx.coroutines.flow | ||
| 247 | + interface StateFlow<out T> { | ||
| 248 | + val value: T | ||
| 249 | + } | ||
| 250 | + class MutableStateFlow<T>(override var value: T) : StateFlow<T> | ||
| 251 | + """ | ||
| 252 | + ).indented() | ||
| 253 | +} |
| 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.TestFiles.kotlin | ||
| 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 | + @Test | ||
| 136 | + fun kotlinMediaTrackEqualityOperator() { | ||
| 137 | + lint() | ||
| 138 | + .allowMissingSdk() | ||
| 139 | + .files( | ||
| 140 | + mediaStreamTrack(), | ||
| 141 | + kotlin( | ||
| 142 | + """ | ||
| 143 | + package foo | ||
| 144 | + import org.webrtc.MediaStreamTrack | ||
| 145 | + | ||
| 146 | + class Example { | ||
| 147 | + fun foo() : Boolean { | ||
| 148 | + val a = MediaStreamTrack() | ||
| 149 | + val b = MediaStreamTrack() | ||
| 150 | + return a == b; | ||
| 151 | + } | ||
| 152 | + }""" | ||
| 153 | + ).indented() | ||
| 154 | + ) | ||
| 155 | + .issues(MediaTrackEqualsDetector.ISSUE) | ||
| 156 | + .run() | ||
| 157 | + .expectErrorCount(1) | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + @Test | ||
| 161 | + fun kotlinMediaTrackIdentityEqualityOperator() { | ||
| 162 | + lint() | ||
| 163 | + .allowMissingSdk() | ||
| 164 | + .files( | ||
| 165 | + mediaStreamTrack(), | ||
| 166 | + kotlin( | ||
| 167 | + """ | ||
| 168 | + package foo | ||
| 169 | + import org.webrtc.MediaStreamTrack | ||
| 170 | + | ||
| 171 | + class Example { | ||
| 172 | + fun foo() : Boolean { | ||
| 173 | + val a = MediaStreamTrack() | ||
| 174 | + val b = MediaStreamTrack() | ||
| 175 | + return a === b | ||
| 176 | + } | ||
| 177 | + }""" | ||
| 178 | + ).indented() | ||
| 179 | + ) | ||
| 180 | + .issues(MediaTrackEqualsDetector.ISSUE) | ||
| 181 | + .run() | ||
| 182 | + .expectErrorCount(1) | ||
| 183 | + } | ||
| 184 | + | ||
| 185 | + @Test | ||
| 186 | + fun kotlinMediaTrackEquals() { | ||
| 187 | + lint() | ||
| 188 | + .allowMissingSdk() | ||
| 189 | + .files( | ||
| 190 | + mediaStreamTrack(), | ||
| 191 | + kotlin( | ||
| 192 | + """ | ||
| 193 | + package foo | ||
| 194 | + import org.webrtc.MediaStreamTrack | ||
| 195 | + | ||
| 196 | + class Example { | ||
| 197 | + fun foo() : Boolean { | ||
| 198 | + val a = MediaStreamTrack() | ||
| 199 | + val b = MediaStreamTrack() | ||
| 200 | + return a.equals(b) | ||
| 201 | + } | ||
| 202 | + }""" | ||
| 203 | + ).indented() | ||
| 204 | + ) | ||
| 205 | + .issues(MediaTrackEqualsDetector.ISSUE) | ||
| 206 | + .run() | ||
| 207 | + .expectErrorCount(1) | ||
| 208 | + } | ||
| 209 | + | ||
| 210 | + @Test | ||
| 211 | + fun kotlinProperMediaTrackEquality() { | ||
| 212 | + lint() | ||
| 213 | + .allowMissingSdk() | ||
| 214 | + .files( | ||
| 215 | + mediaStreamTrack(), | ||
| 216 | + kotlin( | ||
| 217 | + """ | ||
| 218 | + package foo | ||
| 219 | + import org.webrtc.MediaStreamTrack | ||
| 220 | + | ||
| 221 | + class Example { | ||
| 222 | + fun foo() : Boolean { | ||
| 223 | + val a = MediaStreamTrack() | ||
| 224 | + val b = MediaStreamTrack() | ||
| 225 | + return a.id() == b.id() | ||
| 226 | + } | ||
| 227 | + }""" | ||
| 228 | + ).indented() | ||
| 229 | + ) | ||
| 230 | + .issues(MediaTrackEqualsDetector.ISSUE) | ||
| 231 | + .run() | ||
| 232 | + .expectClean() | ||
| 233 | + } | ||
| 234 | +} | ||
| 235 | + | ||
| 236 | +fun mediaStreamTrack(): TestFile { | ||
| 237 | + return java( | ||
| 238 | + """ | ||
| 239 | + package org.webrtc; | ||
| 240 | + | ||
| 241 | + class MediaStreamTrack { | ||
| 242 | + int getId(){ | ||
| 243 | + return 0; | ||
| 244 | + } | ||
| 245 | + } | ||
| 246 | + """ | ||
| 247 | + ).indented() | ||
| 248 | +} |
| @@ -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' |
-
请 注册 或 登录 后发表评论