mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-05-24 10:32:09 +02:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay
This commit is contained in:
commit
a54a5081e6
@ -2,7 +2,7 @@ plugins {
|
|||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.21'
|
||||||
id 'org.ajoberstar.grgit' version '1.7.2'
|
id 'org.ajoberstar.grgit' version '5.2.2'
|
||||||
id 'com.google.protobuf'
|
id 'com.google.protobuf'
|
||||||
id 'kotlin-parcelize'
|
id 'kotlin-parcelize'
|
||||||
id 'com.google.devtools.ksp'
|
id 'com.google.devtools.ksp'
|
||||||
@ -144,10 +144,18 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
buildConfig true
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
assets {
|
||||||
|
srcDirs 'src/main/assets', 'src/tests/assets', 'src/test/assets'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.google.dagger:dagger:2.48'
|
implementation 'com.google.dagger:dagger:2.48'
|
||||||
|
implementation 'androidx.test:monitor:1.7.2'
|
||||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||||
|
|
||||||
//Core
|
//Core
|
||||||
@ -186,7 +194,6 @@ dependencies {
|
|||||||
implementation 'androidx.media:media:1.7.0'
|
implementation 'androidx.media:media:1.7.0'
|
||||||
|
|
||||||
//Other
|
//Other
|
||||||
implementation 'org.jmdns:jmdns:3.5.1'
|
|
||||||
implementation 'org.jsoup:jsoup:1.15.3'
|
implementation 'org.jsoup:jsoup:1.15.3'
|
||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
|
@ -51,7 +51,6 @@
|
|||||||
android:name=".activities.MainActivity"
|
android:name=".activities.MainActivity"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
android:theme="@style/Theme.FutoVideo.NoActionBar"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
@ -153,27 +152,21 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.SettingsActivity"
|
android:name=".activities.SettingsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.DeveloperActivity"
|
android:name=".activities.DeveloperActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ExceptionActivity"
|
android:name=".activities.ExceptionActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.CaptchaActivity"
|
android:name=".activities.CaptchaActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.LoginActivity"
|
android:name=".activities.LoginActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceActivity"
|
android:name=".activities.AddSourceActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
android:theme="@style/Theme.FutoVideo.NoActionBar">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@ -187,44 +180,34 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.AddSourceOptionsActivity"
|
android:name=".activities.AddSourceOptionsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricHomeActivity"
|
android:name=".activities.PolycentricHomeActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricBackupActivity"
|
android:name=".activities.PolycentricBackupActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricCreateProfileActivity"
|
android:name=".activities.PolycentricCreateProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricProfileActivity"
|
android:name=".activities.PolycentricProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricWhyActivity"
|
android:name=".activities.PolycentricWhyActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.PolycentricImportProfileActivity"
|
android:name=".activities.PolycentricImportProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ManageTabsActivity"
|
android:name=".activities.ManageTabsActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.QRCaptureActivity"
|
android:name=".activities.QRCaptureActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.FCastGuideActivity"
|
android:name=".activities.FCastGuideActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
@ -1,6 +1,9 @@
|
|||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
|
import java.net.InetAddress
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
@ -26,3 +29,17 @@ fun String?.yesNoToBoolean(): Boolean {
|
|||||||
fun Boolean?.toYesNo(): String {
|
fun Boolean?.toYesNo(): String {
|
||||||
return if (this == true) "YES" else "NO"
|
return if (this == true) "YES" else "NO"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun InetAddress?.toUrlAddress(): String {
|
||||||
|
return when (this) {
|
||||||
|
is Inet6Address -> {
|
||||||
|
"[${toString()}]"
|
||||||
|
}
|
||||||
|
is Inet4Address -> {
|
||||||
|
toString()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw Exception("Invalid address type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -210,6 +210,20 @@ class HttpContext : AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun respondBytes(status: Int, headers: HttpHeaders, body: ByteArray? = null) {
|
||||||
|
if(headers.get("content-length").isNullOrEmpty()) {
|
||||||
|
if (body != null) {
|
||||||
|
headers.put("content-length", body.size.toString());
|
||||||
|
} else {
|
||||||
|
headers.put("content-length", "0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respond(status, headers) { responseStream ->
|
||||||
|
if(body != null) {
|
||||||
|
responseStream.write(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) {
|
fun respond(status: Int, headers: HttpHeaders, writing: (OutputStream)->Unit) {
|
||||||
val responseStream = _responseStream ?: throw IllegalStateException("No response stream set");
|
val responseStream = _responseStream ?: throw IllegalStateException("No response stream set");
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
|
import com.futo.platformplayer.api.http.server.exceptions.EmptyRequestException
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpOptionsAllowHandler
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@ -208,20 +208,20 @@ class ManagedHttpServer(private val _requestedPort: Int = 0) {
|
|||||||
|
|
||||||
for(getMethod in getMethods)
|
for(getMethod in getMethods)
|
||||||
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1)
|
if(getMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && getMethod.first.parameterCount == 1)
|
||||||
addHandler(HttpFuntionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
addHandler(HttpFunctionHandler("GET", getMethod.second.path) { getMethod.first.invoke(obj, it) }).apply {
|
||||||
if(!getMethod.second.contentType.isEmpty())
|
if(!getMethod.second.contentType.isEmpty())
|
||||||
this.withContentType(getMethod.second.contentType);
|
this.withContentType(getMethod.second.contentType);
|
||||||
}.withContentType(getMethod.second.contentType);
|
}.withContentType(getMethod.second.contentType);
|
||||||
for(postMethod in postMethods)
|
for(postMethod in postMethods)
|
||||||
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
|
if(postMethod.first.parameterTypes.firstOrNull() == HttpContext::class.java && postMethod.first.parameterCount == 1)
|
||||||
addHandler(HttpFuntionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
addHandler(HttpFunctionHandler("POST", postMethod.second.path) { postMethod.first.invoke(obj, it) }).apply {
|
||||||
if(!postMethod.second.contentType.isEmpty())
|
if(!postMethod.second.contentType.isEmpty())
|
||||||
this.withContentType(postMethod.second.contentType);
|
this.withContentType(postMethod.second.contentType);
|
||||||
}.withContentType(postMethod.second.contentType);
|
}.withContentType(postMethod.second.contentType);
|
||||||
|
|
||||||
for(getField in getFields) {
|
for(getField in getFields) {
|
||||||
getField.first.isAccessible = true;
|
getField.first.isAccessible = true;
|
||||||
addHandler(HttpFuntionHandler("GET", getField.second.path) {
|
addHandler(HttpFunctionHandler("GET", getField.second.path) {
|
||||||
val value = getField.first.get(obj) as String?;
|
val value = getField.first.get(obj) as String?;
|
||||||
if(value != null) {
|
if(value != null) {
|
||||||
val headers = HttpHeaders(
|
val headers = HttpHeaders(
|
||||||
|
@ -2,7 +2,7 @@ package com.futo.platformplayer.api.http.server.handlers
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.http.server.HttpContext
|
import com.futo.platformplayer.api.http.server.HttpContext
|
||||||
|
|
||||||
class HttpFuntionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
|
class HttpFunctionHandler(method: String, path: String, val handler: (HttpContext)->Unit) : HttpHandler(method, path) {
|
||||||
override fun handle(httpContext: HttpContext) {
|
override fun handle(httpContext: HttpContext) {
|
||||||
httpContext.setResponseHeaders(this.headers);
|
httpContext.setResponseHeaders(this.headers);
|
||||||
handler(httpContext);
|
handler(httpContext);
|
||||||
|
@ -23,7 +23,7 @@ enum class ChapterType(val value: Int) {
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): ChapterType
|
fun fromInt(value: Int): ChapterType
|
||||||
{
|
{
|
||||||
val result = ChapterType.values().firstOrNull { it.value == value };
|
val result = ChapterType.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw UnknownPlatformException(value.toString());
|
throw UnknownPlatformException(value.toString());
|
||||||
return result;
|
return result;
|
||||||
|
@ -21,7 +21,7 @@ enum class ContentType(val value: Int) {
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): ContentType
|
fun fromInt(value: Int): ContentType
|
||||||
{
|
{
|
||||||
val result = ContentType.values().firstOrNull { it.value == value };
|
val result = ContentType.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw UnknownPlatformException(value.toString());
|
throw UnknownPlatformException(value.toString());
|
||||||
return result;
|
return result;
|
||||||
|
@ -10,7 +10,7 @@ enum class LiveEventType(val value : Int) {
|
|||||||
|
|
||||||
companion object{
|
companion object{
|
||||||
fun fromInt(value : Int) : LiveEventType{
|
fun fromInt(value : Int) : LiveEventType{
|
||||||
return LiveEventType.values().first { it.value == value };
|
return LiveEventType.entries.first { it.value == value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,7 +10,7 @@ enum class TextType(val value: Int) {
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromInt(value: Int): TextType
|
fun fromInt(value: Int): TextType
|
||||||
{
|
{
|
||||||
val result = TextType.values().firstOrNull { it.value == value };
|
val result = TextType.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw IllegalArgumentException("Unknown Texttype: $value");
|
throw IllegalArgumentException("Unknown Texttype: $value");
|
||||||
return result;
|
return result;
|
||||||
|
@ -8,7 +8,7 @@ enum class RatingType(val value : Int) {
|
|||||||
|
|
||||||
companion object{
|
companion object{
|
||||||
fun fromInt(value : Int) : RatingType{
|
fun fromInt(value : Int) : RatingType{
|
||||||
return RatingType.values().first { it.value == value };
|
return RatingType.entries.first { it.value == value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,14 +6,17 @@ import android.net.Uri
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.futo.platformplayer.BuildConfig
|
import android.util.Xml
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.http.server.HttpHeaders
|
||||||
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
import com.futo.platformplayer.api.http.server.ManagedHttpServer
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFileHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpFuntionHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpFunctionHandler
|
||||||
import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler
|
import com.futo.platformplayer.api.http.server.handlers.HttpProxyHandler
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||||
@ -26,16 +29,23 @@ import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSou
|
|||||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestMergingRawSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
import com.futo.platformplayer.builders.DashBuilder
|
import com.futo.platformplayer.builders.DashBuilder
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.mdns.DnsService
|
||||||
|
import com.futo.platformplayer.mdns.ServiceDiscoverer
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.parsers.HLS
|
import com.futo.platformplayer.parsers.HLS
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.stores.CastingDeviceInfoStorage
|
import com.futo.platformplayer.stores.CastingDeviceInfoStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.toUrlAddress
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
@ -43,17 +53,15 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.net.URLEncoder
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.jmdns.JmDNS
|
|
||||||
import javax.jmdns.ServiceEvent
|
|
||||||
import javax.jmdns.ServiceListener
|
|
||||||
import javax.jmdns.ServiceTypeListener
|
|
||||||
|
|
||||||
class StateCasting {
|
class StateCasting {
|
||||||
private val _scopeIO = CoroutineScope(Dispatchers.IO);
|
private val _scopeIO = CoroutineScope(Dispatchers.IO);
|
||||||
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
||||||
private var _jmDNS: JmDNS? = null;
|
|
||||||
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
||||||
|
|
||||||
private val _castServer = ManagedHttpServer(9999);
|
private val _castServer = ManagedHttpServer(9999);
|
||||||
@ -70,105 +78,51 @@ class StateCasting {
|
|||||||
val onActiveDeviceDurationChanged = Event1<Double>();
|
val onActiveDeviceDurationChanged = Event1<Double>();
|
||||||
val onActiveDeviceVolumeChanged = Event1<Double>();
|
val onActiveDeviceVolumeChanged = Event1<Double>();
|
||||||
var activeDevice: CastingDevice? = null;
|
var activeDevice: CastingDevice? = null;
|
||||||
|
private var _videoExecutor: JSRequestExecutor? = null
|
||||||
|
private var _audioExecutor: JSRequestExecutor? = null
|
||||||
private val _client = ManagedHttpClient();
|
private val _client = ManagedHttpClient();
|
||||||
var _resumeCastingDevice: CastingDeviceInfo? = null;
|
var _resumeCastingDevice: CastingDeviceInfo? = null;
|
||||||
|
val _serviceDiscoverer = ServiceDiscoverer(arrayOf(
|
||||||
|
"_googlecast._tcp.local",
|
||||||
|
"_airplay._tcp.local",
|
||||||
|
"_fastcast._tcp.local",
|
||||||
|
"_fcast._tcp.local"
|
||||||
|
)) { handleServiceUpdated(it) }
|
||||||
|
|
||||||
val isCasting: Boolean get() = activeDevice != null;
|
val isCasting: Boolean get() = activeDevice != null;
|
||||||
|
|
||||||
private val _chromecastServiceListener = object : ServiceListener {
|
private fun handleServiceUpdated(services: List<DnsService>) {
|
||||||
override fun serviceAdded(event: ServiceEvent) {
|
for (s in services) {
|
||||||
Logger.i(TAG, "ChromeCast service added: " + event.info);
|
//TODO: Addresses IPv4 only?
|
||||||
addOrUpdateDevice(event);
|
val addresses = s.addresses.toTypedArray()
|
||||||
}
|
val port = s.port.toInt()
|
||||||
|
var name = s.texts.firstOrNull { it.startsWith("md=") }?.substring("md=".length)
|
||||||
override fun serviceRemoved(event: ServiceEvent) {
|
if (s.name.endsWith("._googlecast._tcp.local")) {
|
||||||
Logger.i(TAG, "ChromeCast service removed: " + event.info);
|
if (name == null) {
|
||||||
synchronized(devices) {
|
name = s.name.substring(0, s.name.length - "._googlecast._tcp.local".length)
|
||||||
val device = devices[event.info.name];
|
|
||||||
if (device != null) {
|
|
||||||
onDeviceRemoved.emit(device);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serviceResolved(event: ServiceEvent) {
|
addOrUpdateChromeCastDevice(name, addresses, port)
|
||||||
Logger.v(TAG, "ChromeCast service resolved: " + event.info);
|
} else if (s.name.endsWith("._airplay._tcp.local")) {
|
||||||
addOrUpdateDevice(event);
|
if (name == null) {
|
||||||
}
|
name = s.name.substring(0, s.name.length - "._airplay._tcp.local".length)
|
||||||
|
|
||||||
fun addOrUpdateDevice(event: ServiceEvent) {
|
|
||||||
addOrUpdateChromeCastDevice(event.info.name, event.info.inetAddresses, event.info.port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _airPlayServiceListener = object : ServiceListener {
|
|
||||||
override fun serviceAdded(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "AirPlay service added: " + event.info);
|
|
||||||
addOrUpdateDevice(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serviceRemoved(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "AirPlay service removed: " + event.info);
|
|
||||||
synchronized(devices) {
|
|
||||||
val device = devices[event.info.name];
|
|
||||||
if (device != null) {
|
|
||||||
onDeviceRemoved.emit(device);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serviceResolved(event: ServiceEvent) {
|
addOrUpdateAirPlayDevice(name, addresses, port)
|
||||||
Logger.i(TAG, "AirPlay service resolved: " + event.info);
|
} else if (s.name.endsWith("._fastcast._tcp.local")) {
|
||||||
addOrUpdateDevice(event);
|
if (name == null) {
|
||||||
}
|
name = s.name.substring(0, s.name.length - "._fastcast._tcp.local".length)
|
||||||
|
|
||||||
fun addOrUpdateDevice(event: ServiceEvent) {
|
|
||||||
addOrUpdateAirPlayDevice(event.info.name, event.info.inetAddresses, event.info.port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _fastCastServiceListener = object : ServiceListener {
|
|
||||||
override fun serviceAdded(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "FastCast service added: " + event.info);
|
|
||||||
addOrUpdateDevice(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun serviceRemoved(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "FastCast service removed: " + event.info);
|
|
||||||
synchronized(devices) {
|
|
||||||
val device = devices[event.info.name];
|
|
||||||
if (device != null) {
|
|
||||||
onDeviceRemoved.emit(device);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addOrUpdateFastCastDevice(name, addresses, port)
|
||||||
|
} else if (s.name.endsWith("._fcast._tcp.local")) {
|
||||||
|
if (name == null) {
|
||||||
|
name = s.name.substring(0, s.name.length - "._fcast._tcp.local".length)
|
||||||
|
}
|
||||||
|
|
||||||
|
addOrUpdateFastCastDevice(name, addresses, port)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serviceResolved(event: ServiceEvent) {
|
|
||||||
Logger.i(TAG, "FastCast service resolved: " + event.info);
|
|
||||||
addOrUpdateDevice(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addOrUpdateDevice(event: ServiceEvent) {
|
|
||||||
addOrUpdateFastCastDevice(event.info.name, event.info.inetAddresses, event.info.port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _serviceTypeListener = object : ServiceTypeListener {
|
|
||||||
override fun serviceTypeAdded(event: ServiceEvent?) {
|
|
||||||
if (event == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Service type added (name: ${event.name}, type: ${event.type})");
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun subTypeForServiceTypeAdded(event: ServiceEvent?) {
|
|
||||||
if (event == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.i(TAG, "Sub type for service type added (name: ${event.name}, type: ${event.type})");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUrl(context: Context, url: String) {
|
fun handleUrl(context: Context, url: String) {
|
||||||
@ -237,29 +191,30 @@ class StateCasting {
|
|||||||
rememberedDevices.clear();
|
rememberedDevices.clear();
|
||||||
rememberedDevices.addAll(_storage.deviceInfos.map { deviceFromCastingDeviceInfo(it) });
|
rememberedDevices.addAll(_storage.deviceInfos.map { deviceFromCastingDeviceInfo(it) });
|
||||||
|
|
||||||
_scopeIO.launch {
|
|
||||||
try {
|
|
||||||
val jmDNS = JmDNS.create(InetAddress.getLocalHost());
|
|
||||||
jmDNS.addServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
|
||||||
jmDNS.addServiceListener("_airplay._tcp.local.", _airPlayServiceListener);
|
|
||||||
jmDNS.addServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
|
||||||
jmDNS.addServiceListener("_fcast._tcp.local.", _fastCastServiceListener);
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
jmDNS.addServiceTypeListener(_serviceTypeListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
_jmDNS = jmDNS;
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to start casting service.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_castServer.start();
|
_castServer.start();
|
||||||
enableDeveloper(true);
|
enableDeveloper(true);
|
||||||
|
|
||||||
Logger.i(TAG, "CastingService started.");
|
Logger.i(TAG, "CastingService started.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun startDiscovering() {
|
||||||
|
try {
|
||||||
|
_serviceDiscoverer.start()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to start ServiceDiscoverer", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun stopDiscovering() {
|
||||||
|
try {
|
||||||
|
_serviceDiscoverer.stop()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Failed to stop ServiceDiscoverer", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun stop() {
|
fun stop() {
|
||||||
if (!_started)
|
if (!_started)
|
||||||
@ -269,25 +224,7 @@ class StateCasting {
|
|||||||
|
|
||||||
Logger.i(TAG, "CastingService stopping.")
|
Logger.i(TAG, "CastingService stopping.")
|
||||||
|
|
||||||
val jmDNS = _jmDNS;
|
stopDiscovering()
|
||||||
if (jmDNS != null) {
|
|
||||||
_scopeIO.launch {
|
|
||||||
try {
|
|
||||||
jmDNS.removeServiceListener("_googlecast._tcp.local.", _chromecastServiceListener);
|
|
||||||
jmDNS.removeServiceListener("_airplay._tcp", _airPlayServiceListener);
|
|
||||||
jmDNS.removeServiceListener("_fastcast._tcp.local.", _fastCastServiceListener);
|
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
jmDNS.removeServiceTypeListener(_serviceTypeListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
jmDNS.close();
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to stop mDNS.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_scopeIO.cancel();
|
_scopeIO.cancel();
|
||||||
_scopeMain.cancel();
|
_scopeMain.cancel();
|
||||||
|
|
||||||
@ -437,15 +374,26 @@ class StateCasting {
|
|||||||
} else {
|
} else {
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
if (ad is FCastCastingDevice) {
|
val isRawDash = videoSource is JSDashManifestRawSource || audioSource is JSDashManifestRawAudioSource
|
||||||
Logger.i(TAG, "Casting as DASH direct");
|
if (isRawDash) {
|
||||||
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
Logger.i(TAG, "Casting as raw DASH");
|
||||||
} else if (ad is AirPlayCastingDevice) {
|
|
||||||
Logger.i(TAG, "Casting as HLS indirect");
|
try {
|
||||||
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, audioSource as JSDashManifestRawAudioSource?, subtitleSource, resumePosition, speed);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.i(TAG, "Casting as DASH indirect");
|
if (ad is FCastCastingDevice) {
|
||||||
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
Logger.i(TAG, "Casting as DASH direct");
|
||||||
|
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||||
|
} else if (ad is AirPlayCastingDevice) {
|
||||||
|
Logger.i(TAG, "Casting as HLS indirect");
|
||||||
|
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||||
|
} else {
|
||||||
|
Logger.i(TAG, "Casting as DASH indirect");
|
||||||
|
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition, speed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
|
Logger.e(TAG, "Failed to start casting DASH videoSource=${videoSource} audioSource=${audioSource}.", e);
|
||||||
@ -454,7 +402,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests;
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
if (videoSource is IVideoUrlSource) {
|
if (videoSource is IVideoUrlSource) {
|
||||||
@ -489,6 +437,26 @@ class StateCasting {
|
|||||||
} else if (audioSource is LocalAudioSource) {
|
} else if (audioSource is LocalAudioSource) {
|
||||||
Logger.i(TAG, "Casting as local audio");
|
Logger.i(TAG, "Casting as local audio");
|
||||||
castLocalAudio(video, audioSource, resumePosition, speed);
|
castLocalAudio(video, audioSource, resumePosition, speed);
|
||||||
|
} else if (videoSource is JSDashManifestRawSource) {
|
||||||
|
Logger.i(TAG, "Casting as JSDashManifestRawSource video");
|
||||||
|
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
castDashRaw(contentResolver, video, videoSource as JSDashManifestRawSource?, null, null, resumePosition, speed);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to start casting DASH raw videoSource=${videoSource}.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (audioSource is JSDashManifestRawAudioSource) {
|
||||||
|
Logger.i(TAG, "Casting as JSDashManifestRawSource audio");
|
||||||
|
|
||||||
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
castDashRaw(contentResolver, video, null, audioSource as JSDashManifestRawAudioSource?, null, resumePosition, speed);
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to start casting DASH raw audioSource=${audioSource}.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var str = listOf(
|
var str = listOf(
|
||||||
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
if(videoSource != null) "Video: ${videoSource::class.java.simpleName}" else null,
|
||||||
@ -529,7 +497,7 @@ class StateCasting {
|
|||||||
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalVideo(video: IPlatformVideoDetails, videoSource: LocalVideoSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val videoPath = "/video-${id}"
|
val videoPath = "/video-${id}"
|
||||||
val videoUrl = url + videoPath;
|
val videoUrl = url + videoPath;
|
||||||
@ -548,7 +516,7 @@ class StateCasting {
|
|||||||
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalAudio(video: IPlatformVideoDetails, audioSource: LocalAudioSource, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val audioPath = "/audio-${id}"
|
val audioPath = "/audio-${id}"
|
||||||
val audioUrl = url + audioPath;
|
val audioUrl = url + audioPath;
|
||||||
@ -567,7 +535,7 @@ class StateCasting {
|
|||||||
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
private fun castLocalHls(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?): List<String> {
|
||||||
val ad = activeDevice ?: return listOf()
|
val ad = activeDevice ?: return listOf()
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}"
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}"
|
||||||
val id = UUID.randomUUID()
|
val id = UUID.randomUUID()
|
||||||
|
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
@ -663,7 +631,7 @@ class StateCasting {
|
|||||||
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private fun castLocalDash(video: IPlatformVideoDetails, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
@ -713,7 +681,7 @@ class StateCasting {
|
|||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val videoPath = "/video-${id}"
|
val videoPath = "/video-${id}"
|
||||||
@ -771,20 +739,21 @@ class StateCasting {
|
|||||||
Logger.v(TAG) { "Dash manifest: $content" };
|
Logger.v(TAG) { "Dash manifest: $content" };
|
||||||
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed);
|
ad.loadContent("application/dash+xml", content, resumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
return listOf(videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString()); }
|
return listOf(videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
|
private fun castProxiedHls(video: IPlatformVideoDetails, sourceUrl: String, codec: String?, resumePosition: Double, speed: Double?): List<String> {
|
||||||
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
_castServer.removeAllHandlers("castProxiedHlsMaster")
|
||||||
|
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
|
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
val hlsUrl = url + hlsPath
|
val hlsUrl = url + hlsPath
|
||||||
Logger.i(TAG, "HLS url: $hlsUrl");
|
Logger.i(TAG, "HLS url: $hlsUrl");
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", hlsPath) { masterContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", hlsPath) { masterContext ->
|
||||||
_castServer.removeAllHandlers("castProxiedHlsVariant")
|
_castServer.removeAllHandlers("castProxiedHlsVariant")
|
||||||
|
|
||||||
val headers = masterContext.headers.clone()
|
val headers = masterContext.headers.clone()
|
||||||
@ -811,7 +780,7 @@ class StateCasting {
|
|||||||
val proxiedVariantPlaylist = proxyVariantPlaylist(url, id, variantPlaylist, video.isLive)
|
val proxiedVariantPlaylist = proxyVariantPlaylist(url, id, variantPlaylist, video.isLive)
|
||||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||||
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
masterContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||||
return@HttpFuntionHandler
|
return@HttpFunctionHandler
|
||||||
} else {
|
} else {
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
@ -828,7 +797,7 @@ class StateCasting {
|
|||||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||||
val newPlaylistUrl = url + newPlaylistPath;
|
val newPlaylistUrl = url + newPlaylistPath;
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext ->
|
||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
|
||||||
@ -858,7 +827,7 @@ class StateCasting {
|
|||||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||||
newPlaylistUrl = url + newPlaylistPath
|
newPlaylistUrl = url + newPlaylistPath
|
||||||
|
|
||||||
_castServer.addHandlerWithAllowAllOptions(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
_castServer.addHandlerWithAllowAllOptions(HttpFunctionHandler("GET", newPlaylistPath) { vpContext ->
|
||||||
val vpHeaders = vpContext.headers.clone()
|
val vpHeaders = vpContext.headers.clone()
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
|
||||||
@ -947,7 +916,7 @@ class StateCasting {
|
|||||||
|
|
||||||
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val hlsPath = "/hls-${id}"
|
val hlsPath = "/hls-${id}"
|
||||||
@ -1077,7 +1046,7 @@ class StateCasting {
|
|||||||
val ad = activeDevice ?: return listOf();
|
val ad = activeDevice ?: return listOf();
|
||||||
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
val proxyStreams = Settings.instance.casting.alwaysProxyRequests || ad !is FCastCastingDevice;
|
||||||
|
|
||||||
val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
val id = UUID.randomUUID();
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
val dashPath = "/dash-${id}"
|
val dashPath = "/dash-${id}"
|
||||||
@ -1151,6 +1120,166 @@ class StateCasting {
|
|||||||
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
return listOf(dashUrl, videoUrl ?: "", audioUrl ?: "", subtitlesUrl ?: "", videoSource?.getVideoUrl() ?: "", audioSource?.getAudioUrl() ?: "", subtitlesUri.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanExecutors() {
|
||||||
|
if (_videoExecutor != null) {
|
||||||
|
_videoExecutor?.cleanup()
|
||||||
|
_videoExecutor = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_audioExecutor != null) {
|
||||||
|
_audioExecutor?.cleanup()
|
||||||
|
_audioExecutor = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
private suspend fun castDashRaw(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: JSDashManifestRawSource?, audioSource: JSDashManifestRawAudioSource?, subtitleSource: ISubtitleSource?, resumePosition: Double, speed: Double?) : List<String> {
|
||||||
|
val ad = activeDevice ?: return listOf();
|
||||||
|
|
||||||
|
cleanExecutors()
|
||||||
|
_castServer.removeAllHandlers("castDashRaw")
|
||||||
|
|
||||||
|
val url = "http://${ad.localAddress.toUrlAddress().trim('/')}:${_castServer.port}";
|
||||||
|
val id = UUID.randomUUID();
|
||||||
|
|
||||||
|
val dashPath = "/dash-${id}"
|
||||||
|
val videoPath = "/video-${id}"
|
||||||
|
val audioPath = "/audio-${id}"
|
||||||
|
val subtitlePath = "/subtitle-${id}"
|
||||||
|
|
||||||
|
val dashUrl = url + dashPath;
|
||||||
|
Logger.i(TAG, "DASH url: $dashUrl");
|
||||||
|
|
||||||
|
val videoUrl = url + videoPath
|
||||||
|
val audioUrl = url + audioPath
|
||||||
|
|
||||||
|
val subtitlesUri = if (subtitleSource != null) withContext(Dispatchers.IO) {
|
||||||
|
return@withContext subtitleSource.getSubtitlesURI();
|
||||||
|
} else null;
|
||||||
|
|
||||||
|
var subtitlesUrl: String? = null;
|
||||||
|
if (subtitlesUri != null) {
|
||||||
|
if(subtitlesUri.scheme == "file") {
|
||||||
|
var content: String? = null;
|
||||||
|
val inputStream = contentResolver.openInputStream(subtitlesUri);
|
||||||
|
inputStream?.use { stream ->
|
||||||
|
val reader = stream.bufferedReader();
|
||||||
|
content = reader.use { it.readText() };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content != null) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpConstantHandler("GET", subtitlePath, content!!, subtitleSource?.format ?: "text/vtt")
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("cast");
|
||||||
|
}
|
||||||
|
|
||||||
|
subtitlesUrl = url + subtitlePath;
|
||||||
|
} else {
|
||||||
|
subtitlesUrl = subtitlesUri.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dashContent = withContext(Dispatchers.IO) {
|
||||||
|
//TODO: Include subtitlesURl in the future
|
||||||
|
return@withContext if (audioSource != null && videoSource != null) {
|
||||||
|
JSDashManifestMergingRawSource(videoSource, audioSource).generate()
|
||||||
|
} else if (audioSource != null) {
|
||||||
|
audioSource.generate()
|
||||||
|
} else if (videoSource != null) {
|
||||||
|
videoSource.generate()
|
||||||
|
} else {
|
||||||
|
Logger.e(TAG, "Expected at least audio or video to be set")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} ?: throw Exception("Dash is null")
|
||||||
|
|
||||||
|
for (representation in representationRegex.findAll(dashContent)) {
|
||||||
|
val mediaType = representation.groups[1]?.value ?: throw Exception("Media type should be found")
|
||||||
|
dashContent = mediaInitializationRegex.replace(dashContent) {
|
||||||
|
if (it.range.first < representation.range.first || it.range.last > representation.range.last) {
|
||||||
|
return@replace it.value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaType.startsWith("video/")) {
|
||||||
|
return@replace "${it.groups[1]!!.value}=\"${videoUrl}?url=${URLEncoder.encode(it.groups[2]!!.value, "UTF-8").replace("%24Number%24", "\$Number\$")}&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\""
|
||||||
|
} else if (mediaType.startsWith("audio/")) {
|
||||||
|
return@replace "${it.groups[1]!!.value}=\"${audioUrl}?url=${URLEncoder.encode(it.groups[2]!!.value, "UTF-8").replace("%24Number%24", "\$Number\$")}&mediaType=${URLEncoder.encode(mediaType, "UTF-8")}\""
|
||||||
|
} else {
|
||||||
|
throw Exception("Expected audio or video")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoSource != null && !videoSource.hasRequestExecutor) {
|
||||||
|
throw Exception("Video source without request executor not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioSource != null && !audioSource.hasRequestExecutor) {
|
||||||
|
throw Exception("Audio source without request executor not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioSource != null && audioSource.hasRequestExecutor) {
|
||||||
|
_audioExecutor = audioSource.getRequestExecutor()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoSource != null && videoSource.hasRequestExecutor) {
|
||||||
|
_videoExecutor = videoSource.getRequestExecutor()
|
||||||
|
}
|
||||||
|
|
||||||
|
//TOOD: Else also handle the non request executor case, perhaps add ?url=$originalUrl to the query parameters, ... propagate this for all other flows also
|
||||||
|
|
||||||
|
Logger.v(TAG) { "Dash manifest: $dashContent" };
|
||||||
|
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpConstantHandler("GET", dashPath, dashContent,
|
||||||
|
"application/dash+xml")
|
||||||
|
.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("castDashRaw");
|
||||||
|
|
||||||
|
if (videoSource != null) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpFunctionHandler("GET", videoPath) { httpContext ->
|
||||||
|
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
|
val mediaType = httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
|
|
||||||
|
val videoExecutor = _videoExecutor;
|
||||||
|
if (videoExecutor != null) {
|
||||||
|
val data = videoExecutor.executeRequest(originalUrl, httpContext.headers)
|
||||||
|
httpContext.respondBytes(200, HttpHeaders().apply {
|
||||||
|
put("Content-Type", mediaType)
|
||||||
|
}, data);
|
||||||
|
} else {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("castDashRaw");
|
||||||
|
}
|
||||||
|
if (audioSource != null) {
|
||||||
|
_castServer.addHandlerWithAllowAllOptions(
|
||||||
|
HttpFunctionHandler("GET", audioPath) { httpContext ->
|
||||||
|
val originalUrl = httpContext.query["url"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
|
val mediaType = httpContext.query["mediaType"]?.let { URLDecoder.decode(it, "UTF-8") } ?: return@HttpFunctionHandler
|
||||||
|
|
||||||
|
val audioExecutor = _audioExecutor;
|
||||||
|
if (audioExecutor != null) {
|
||||||
|
val data = audioExecutor.executeRequest(originalUrl, httpContext.headers)
|
||||||
|
httpContext.respondBytes(200, HttpHeaders().apply {
|
||||||
|
put("Content-Type", mediaType)
|
||||||
|
}, data);
|
||||||
|
} else {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
}.withHeader("Access-Control-Allow-Origin", "*"), true
|
||||||
|
).withTag("castDashRaw");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "added new castDash handlers (dashPath: $dashPath, videoPath: $videoPath, audioPath: $audioPath).");
|
||||||
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/dash+xml", dashUrl, resumePosition, video.duration.toDouble(), speed);
|
||||||
|
|
||||||
|
return listOf()
|
||||||
|
}
|
||||||
|
|
||||||
private fun deviceFromCastingDeviceInfo(deviceInfo: CastingDeviceInfo): CastingDevice {
|
private fun deviceFromCastingDeviceInfo(deviceInfo: CastingDeviceInfo): CastingDevice {
|
||||||
return when (deviceInfo.type) {
|
return when (deviceInfo.type) {
|
||||||
CastProtocolType.CHROMECAST -> {
|
CastProtocolType.CHROMECAST -> {
|
||||||
@ -1245,7 +1374,7 @@ class StateCasting {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val newDevice = deviceFactory();
|
val newDevice = deviceFactory();
|
||||||
devices[name] = newDevice;
|
this.devices[name] = newDevice;
|
||||||
|
|
||||||
invokeEvents = {
|
invokeEvents = {
|
||||||
onDeviceAdded.emit(newDevice);
|
onDeviceAdded.emit(newDevice);
|
||||||
@ -1259,7 +1388,7 @@ class StateCasting {
|
|||||||
fun enableDeveloper(enableDev: Boolean){
|
fun enableDeveloper(enableDev: Boolean){
|
||||||
_castServer.removeAllHandlers("dev");
|
_castServer.removeAllHandlers("dev");
|
||||||
if(enableDev) {
|
if(enableDev) {
|
||||||
_castServer.addHandler(HttpFuntionHandler("GET", "/dashPlayer") { context ->
|
_castServer.addHandler(HttpFunctionHandler("GET", "/dashPlayer") { context ->
|
||||||
if (context.query.containsKey("dashUrl")) {
|
if (context.query.containsKey("dashUrl")) {
|
||||||
val dashUrl = context.query["dashUrl"];
|
val dashUrl = context.query["dashUrl"];
|
||||||
val html = "<div>\n" +
|
val html = "<div>\n" +
|
||||||
@ -1299,6 +1428,9 @@ class StateCasting {
|
|||||||
companion object {
|
companion object {
|
||||||
val instance: StateCasting = StateCasting();
|
val instance: StateCasting = StateCasting();
|
||||||
|
|
||||||
|
private val representationRegex = Regex("<Representation .*?mimeType=\"(.*?)\".*?>(.*?)<\\/Representation>", RegexOption.DOT_MATCHES_ALL)
|
||||||
|
private val mediaInitializationRegex = Regex("(media|initiali[sz]ation)=\"([^\"]+)\"", RegexOption.DOT_MATCHES_ALL);
|
||||||
|
|
||||||
private val TAG = "StateCasting";
|
private val TAG = "StateCasting";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,8 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
super.show();
|
super.show();
|
||||||
Logger.i(TAG, "Dialog shown.");
|
Logger.i(TAG, "Dialog shown.");
|
||||||
|
|
||||||
|
StateCasting.instance.startDiscovering()
|
||||||
|
|
||||||
(_imageLoader.drawable as Animatable?)?.start();
|
(_imageLoader.drawable as Animatable?)?.start();
|
||||||
|
|
||||||
_devices.clear();
|
_devices.clear();
|
||||||
@ -169,6 +171,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
(_imageLoader.drawable as Animatable?)?.stop();
|
(_imageLoader.drawable as Animatable?)?.stop();
|
||||||
|
|
||||||
|
StateCasting.instance.stopDiscovering()
|
||||||
StateCasting.instance.onDeviceAdded.remove(this);
|
StateCasting.instance.onDeviceAdded.remove(this);
|
||||||
StateCasting.instance.onDeviceChanged.remove(this);
|
StateCasting.instance.onDeviceChanged.remove(this);
|
||||||
StateCasting.instance.onDeviceRemoved.remove(this);
|
StateCasting.instance.onDeviceRemoved.remove(this);
|
||||||
|
@ -7,6 +7,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -56,6 +57,12 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
return _view?.onBackPressed() ?: false;
|
return _view?.onBackPressed() ?: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
_view?.updateAllButtonVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
class MenuBottomBarView : LinearLayout {
|
class MenuBottomBarView : LinearLayout {
|
||||||
private val _fragment: MenuBottomBarFragment;
|
private val _fragment: MenuBottomBarFragment;
|
||||||
@ -251,9 +258,19 @@ class MenuBottomBarFragment : MainActivityFragment() {
|
|||||||
button.updateActive(_fragment);
|
button.updateActive(_fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
updateAllButtonVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
fun updateAllButtonVisibility() {
|
fun updateAllButtonVisibility() {
|
||||||
|
if(_moreVisible) {
|
||||||
|
setMoreVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
val defs = currentButtonDefinitions?.toMutableList() ?: return
|
||||||
val metrics = StateApp.instance.displayMetrics ?: resources.displayMetrics;
|
val metrics = resources.displayMetrics;
|
||||||
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
_buttonsVisible = floor(metrics.widthPixels.toDouble() / 65.dp(resources).toDouble()).roundToInt();
|
||||||
if (_buttonsVisible >= defs.size) {
|
if (_buttonsVisible >= defs.size) {
|
||||||
updateBottomMenuButtons(defs.toMutableList(), false);
|
updateBottomMenuButtons(defs.toMutableList(), false);
|
||||||
|
@ -4,8 +4,10 @@ import android.content.Context
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
@ -45,7 +47,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
private var _videoOptionsOverlay: SlideUpMenuOverlay? = null;
|
private var _videoOptionsOverlay: SlideUpMenuOverlay? = null;
|
||||||
protected open val shouldShowTimeBar: Boolean get() = true
|
protected open val shouldShowTimeBar: Boolean get() = true
|
||||||
|
|
||||||
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, StaggeredGridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,18 +55,34 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<IPlatformContent>): InsertedViewAdapterWithLoader<ContentPreviewViewHolder> {
|
override fun createAdapter(
|
||||||
|
recyclerResults: RecyclerView,
|
||||||
|
context: Context,
|
||||||
|
dataset: ArrayList<IPlatformContent>
|
||||||
|
): InsertedViewAdapterWithLoader<ContentPreviewViewHolder> {
|
||||||
val player = StatePlayer.instance.getThumbnailPlayerOrCreate(context);
|
val player = StatePlayer.instance.getThumbnailPlayerOrCreate(context);
|
||||||
player.modifyState("ThumbnailPlayer", { state -> state.muted = true });
|
player.modifyState("ThumbnailPlayer", { state -> state.muted = true });
|
||||||
_exoPlayer = player;
|
_exoPlayer = player;
|
||||||
|
|
||||||
val v = LinearLayout(context).apply {
|
val v = LinearLayout(context).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
orientation = LinearLayout.VERTICAL;
|
orientation = LinearLayout.VERTICAL;
|
||||||
};
|
};
|
||||||
headerView = v;
|
headerView = v;
|
||||||
|
|
||||||
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(v), arrayListOf(), shouldShowTimeBar).apply {
|
return PreviewContentListAdapter(
|
||||||
|
context,
|
||||||
|
feedStyle,
|
||||||
|
dataset,
|
||||||
|
player,
|
||||||
|
_previewsEnabled,
|
||||||
|
arrayListOf(v),
|
||||||
|
arrayListOf(),
|
||||||
|
shouldShowTimeBar
|
||||||
|
).apply {
|
||||||
attachAdapterEvents(this);
|
attachAdapterEvents(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,10 +178,13 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
adapter.onLongPress.remove(this);
|
adapter.onLongPress.remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
|
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, StaggeredGridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
|
||||||
super.onRestoreCachedData(cachedData)
|
super.onRestoreCachedData(cachedData)
|
||||||
val v = LinearLayout(context).apply {
|
val v = LinearLayout(context).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
orientation = LinearLayout.VERTICAL;
|
orientation = LinearLayout.VERTICAL;
|
||||||
};
|
};
|
||||||
headerView = v;
|
headerView = v;
|
||||||
@ -171,10 +192,16 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
(cachedData.adapter as PreviewContentListAdapter?)?.let { attachAdapterEvents(it) };
|
(cachedData.adapter as PreviewContentListAdapter?)?.let { attachAdapterEvents(it) };
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager {
|
override fun createLayoutManager(
|
||||||
val llmResults = LinearLayoutManager(context);
|
recyclerResults: RecyclerView,
|
||||||
llmResults.orientation = LinearLayoutManager.VERTICAL;
|
context: Context
|
||||||
return llmResults;
|
): StaggeredGridLayoutManager {
|
||||||
|
val glmResults =
|
||||||
|
StaggeredGridLayoutManager(
|
||||||
|
if (resources.configuration.screenWidthDp >= resources.getDimension(R.dimen.landscape_threshold)) 2 else 1,
|
||||||
|
StaggeredGridLayoutManager.VERTICAL
|
||||||
|
);
|
||||||
|
return glmResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrollStateChanged(newState: Int) {
|
override fun onScrollStateChanged(newState: Int) {
|
||||||
@ -220,8 +247,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
|||||||
if(feedStyle == FeedStyle.THUMBNAIL)
|
if(feedStyle == FeedStyle.THUMBNAIL)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
val firstVisible = recyclerData.layoutManager.findFirstVisibleItemPosition();
|
val firstVisible = recyclerData.layoutManager.findFirstVisibleItemPositions(IntArray(recyclerData.layoutManager.spanCount))[0]
|
||||||
val lastVisible = recyclerData.layoutManager.findLastVisibleItemPosition();
|
val lastVisible = recyclerData.layoutManager.findLastVisibleItemPositions(IntArray(recyclerData.layoutManager.spanCount))[0]
|
||||||
val itemsVisible = lastVisible - firstVisible + 1;
|
val itemsVisible = lastVisible - firstVisible + 1;
|
||||||
val autoPlayIndex = (firstVisible + floor(itemsVisible / 2.0 + 0.49).toInt()).coerceAtLeast(0).coerceAtMost((recyclerData.results.size - 1));
|
val autoPlayIndex = (firstVisible + floor(itemsVisible / 2.0 + 0.49).toInt()).coerceAtLeast(0).coerceAtMost((recyclerData.results.size - 1));
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import android.view.ViewGroup.MarginLayoutParams
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
@ -34,9 +35,8 @@ abstract class CreatorFeedView<TFragment> : FeedView<TFragment, PlatformAuthorLi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager {
|
override fun createLayoutManager(recyclerResults: RecyclerView, context: Context): StaggeredGridLayoutManager {
|
||||||
val glmResults = GridLayoutManager(context, 2);
|
val glmResults = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
|
||||||
glmResults.orientation = LinearLayoutManager.VERTICAL;
|
|
||||||
|
|
||||||
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
_swipeRefresh.layoutParams = (_swipeRefresh.layoutParams as MarginLayoutParams?)?.apply {
|
||||||
rightMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8.0f, context.resources.displayMetrics).toInt();
|
rightMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8.0f, context.resources.displayMetrics).toInt();
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.LayoutManager
|
import androidx.recyclerview.widget.RecyclerView.LayoutManager
|
||||||
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
@ -58,14 +61,14 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
private var _activeTags: List<String>? = null;
|
private var _activeTags: List<String>? = null;
|
||||||
|
|
||||||
private var _nextPageHandler: TaskHandler<TPager, List<TResult>>;
|
private var _nextPageHandler: TaskHandler<TPager, List<TResult>>;
|
||||||
val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>;
|
val recyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, StaggeredGridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>;
|
||||||
|
|
||||||
val fragment: TFragment;
|
val fragment: TFragment;
|
||||||
|
|
||||||
private val _scrollListener: RecyclerView.OnScrollListener;
|
private val _scrollListener: RecyclerView.OnScrollListener;
|
||||||
private var _automaticNextPageCounter = 0;
|
private var _automaticNextPageCounter = 0;
|
||||||
|
|
||||||
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) {
|
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, StaggeredGridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) {
|
||||||
this.fragment = fragment;
|
this.fragment = fragment;
|
||||||
inflater.inflate(R.layout.fragment_feed, this);
|
inflater.inflate(R.layout.fragment_feed, this);
|
||||||
|
|
||||||
@ -158,7 +161,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
super.onScrolled(recyclerView, dx, dy);
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
|
|
||||||
val visibleItemCount = _recyclerResults.childCount;
|
val visibleItemCount = _recyclerResults.childCount;
|
||||||
val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition();
|
val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPositions(IntArray(recyclerData.layoutManager.spanCount))[0]
|
||||||
//Logger.i(TAG, "onScrolled loadNextPage visibleItemCount=$visibleItemCount firstVisibleItem=$visibleItemCount")
|
//Logger.i(TAG, "onScrolled loadNextPage visibleItemCount=$visibleItemCount firstVisibleItem=$visibleItemCount")
|
||||||
|
|
||||||
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size && firstVisibleItem > 0) {
|
if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size && firstVisibleItem > 0) {
|
||||||
@ -174,7 +177,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
|
private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
|
||||||
val canScroll = if (recyclerData.results.isEmpty()) false else {
|
val canScroll = if (recyclerData.results.isEmpty()) false else {
|
||||||
val layoutManager = recyclerData.layoutManager
|
val layoutManager = recyclerData.layoutManager
|
||||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPositions(IntArray(recyclerData.layoutManager.spanCount))[0]
|
||||||
|
|
||||||
if (firstVisibleItemPosition != RecyclerView.NO_POSITION) {
|
if (firstVisibleItemPosition != RecyclerView.NO_POSITION) {
|
||||||
val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
|
val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
|
||||||
@ -226,7 +229,23 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateSpanCount(){
|
||||||
|
if (resources.configuration.screenWidthDp >= resources.getDimension(R.dimen.landscape_threshold) && recyclerData.layoutManager.spanCount != 2){
|
||||||
|
recyclerData.layoutManager.spanCount = 2
|
||||||
|
} else if (resources.configuration.screenWidthDp < resources.getDimension(R.dimen.landscape_threshold) && recyclerData.layoutManager.spanCount != 1){
|
||||||
|
recyclerData.layoutManager.spanCount = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
updateSpanCount()
|
||||||
|
}
|
||||||
|
|
||||||
fun onResume() {
|
fun onResume() {
|
||||||
|
updateSpanCount()
|
||||||
|
|
||||||
//Reload the pager if the plugin was killed
|
//Reload the pager if the plugin was killed
|
||||||
val pager = recyclerData.pager;
|
val pager = recyclerData.pager;
|
||||||
if((pager is MultiPager<*> && pager.findPager { it is JSPager<*> && !it.isAvailable } != null) ||
|
if((pager is MultiPager<*> && pager.findPager { it is JSPager<*> && !it.isAvailable } != null) ||
|
||||||
@ -277,8 +296,8 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected abstract fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<TConverted>): InsertedViewAdapterWithLoader<TViewHolder>;
|
protected abstract fun createAdapter(recyclerResults: RecyclerView, context: Context, dataset: ArrayList<TConverted>): InsertedViewAdapterWithLoader<TViewHolder>;
|
||||||
protected abstract fun createLayoutManager(recyclerResults: RecyclerView, context: Context): LinearLayoutManager;
|
protected abstract fun createLayoutManager(recyclerResults: RecyclerView, context: Context): StaggeredGridLayoutManager;
|
||||||
protected open fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>) {}
|
protected open fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, StaggeredGridLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>) {}
|
||||||
|
|
||||||
protected fun setProgress(fin: Int, total: Int) {
|
protected fun setProgress(fin: Int, total: Int) {
|
||||||
val progress = (fin.toFloat() / total);
|
val progress = (fin.toFloat() / total);
|
||||||
|
@ -6,7 +6,9 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
@ -44,7 +46,7 @@ class HomeFragment : MainFragment() {
|
|||||||
override val hasBottomBar: Boolean get() = true;
|
override val hasBottomBar: Boolean get() = true;
|
||||||
|
|
||||||
private var _view: HomeView? = null;
|
private var _view: HomeView? = null;
|
||||||
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, StaggeredGridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
||||||
|
|
||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
super.onShownWithView(parameter, isBack);
|
super.onShownWithView(parameter, isBack);
|
||||||
@ -98,7 +100,7 @@ class HomeFragment : MainFragment() {
|
|||||||
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
||||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
|
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
|
||||||
|
|
||||||
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, StaggeredGridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||||
_announcementsView = AnnouncementView(context, null).apply {
|
_announcementsView = AnnouncementView(context, null).apply {
|
||||||
headerView.addView(this);
|
headerView.addView(this);
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,9 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
@ -57,7 +59,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
|
|
||||||
private var _view: SubscriptionsFeedView? = null;
|
private var _view: SubscriptionsFeedView? = null;
|
||||||
private var _group: SubscriptionGroup? = null;
|
private var _group: SubscriptionGroup? = null;
|
||||||
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, StaggeredGridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
|
||||||
|
|
||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
super.onShownWithView(parameter, isBack);
|
super.onShownWithView(parameter, isBack);
|
||||||
@ -110,7 +112,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
|
|
||||||
var subGroup: SubscriptionGroup? = null;
|
var subGroup: SubscriptionGroup? = null;
|
||||||
|
|
||||||
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, StaggeredGridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
|
||||||
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
||||||
StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total ->
|
StateSubscriptions.instance.global.onUpdateProgress.subscribe(this) { progress, total ->
|
||||||
};
|
};
|
||||||
@ -395,7 +397,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
_taskGetPager.run(withRefetch);
|
_taskGetPager.run(withRefetch);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
|
override fun onRestoreCachedData(cachedData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, StaggeredGridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>) {
|
||||||
super.onRestoreCachedData(cachedData);
|
super.onRestoreCachedData(cachedData);
|
||||||
setEmptyPager(cachedData.results.isEmpty());
|
setEmptyPager(cachedData.results.isEmpty());
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ScrollView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.futo.platformplayer.*
|
import com.futo.platformplayer.*
|
||||||
import com.futo.platformplayer.api.media.IPlatformClient
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
@ -58,7 +59,15 @@ class TutorialFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
class TutorialView : LinearLayout {
|
class TutorialView(fragment: TutorialFragment, inflater: LayoutInflater) :
|
||||||
|
ScrollView(inflater.context) {
|
||||||
|
init {
|
||||||
|
addView(TutorialContainer(fragment, inflater))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ViewConstructor")
|
||||||
|
class TutorialContainer : LinearLayout {
|
||||||
val fragment: TutorialFragment
|
val fragment: TutorialFragment
|
||||||
|
|
||||||
constructor(fragment: TutorialFragment, inflater: LayoutInflater) : super(inflater.context) {
|
constructor(fragment: TutorialFragment, inflater: LayoutInflater) : super(inflater.context) {
|
||||||
|
@ -29,6 +29,7 @@ import com.futo.platformplayer.models.PlatformVideoWithTime
|
|||||||
import com.futo.platformplayer.models.UrlVideoWithTime
|
import com.futo.platformplayer.models.UrlVideoWithTime
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
class VideoDetailFragment : MainFragment {
|
class VideoDetailFragment : MainFragment {
|
||||||
@ -96,17 +97,18 @@ class VideoDetailFragment : MainFragment {
|
|||||||
val currentOrientation = _currentOrientation
|
val currentOrientation = _currentOrientation
|
||||||
val isFs = isFullscreen
|
val isFs = isFullscreen
|
||||||
|
|
||||||
if (isFs && isMaximized) {
|
if (StatePlayer.instance.rotationLock && isMaximized) {
|
||||||
if (isFullScreenPortraitAllowed) {
|
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
|
} else if (isFullscreen && !isFullScreenPortraitAllowed) {
|
||||||
} else {
|
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "updateOrientation (isFs = ${isFs}, currentOrientation = ${currentOrientation}, isMaximized = ${isMaximized}, isFullScreenPortraitAllowed = ${isFullScreenPortraitAllowed}) resulted in requested orientation ${activity?.requestedOrientation}");
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"updateOrientation (isFs = ${isFs}, currentOrientation = ${currentOrientation}, isMaximized = ${isMaximized}, isFullScreenPortraitAllowed = ${isFullScreenPortraitAllowed}) resulted in requested orientation ${activity?.requestedOrientation}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
@ -157,7 +159,7 @@ class VideoDetailFragment : MainFragment {
|
|||||||
_viewDetail?.preventPictureInPicture = true;
|
_viewDetail?.preventPictureInPicture = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun minimizeVideoDetail(){
|
fun minimizeVideoDetail() {
|
||||||
_viewDetail?.setFullscreen(false);
|
_viewDetail?.setFullscreen(false);
|
||||||
if(_view != null)
|
if(_view != null)
|
||||||
_view!!.transitionToStart();
|
_view!!.transitionToStart();
|
||||||
@ -284,15 +286,23 @@ class VideoDetailFragment : MainFragment {
|
|||||||
_orientationListener = SimpleOrientationListener(requireActivity(), lifecycleScope)
|
_orientationListener = SimpleOrientationListener(requireActivity(), lifecycleScope)
|
||||||
_orientationListener.onOrientationChanged.subscribe {
|
_orientationListener.onOrientationChanged.subscribe {
|
||||||
_currentOrientation = it
|
_currentOrientation = it
|
||||||
Logger.i(TAG, "Current orientation changed (_currentOrientation = ${_currentOrientation})")
|
Logger.i(
|
||||||
|
TAG,
|
||||||
|
"Current orientation changed (_currentOrientation = ${_currentOrientation})"
|
||||||
|
)
|
||||||
|
|
||||||
if (Settings.instance.playback.isAutoRotate()) {
|
val isSmallWindow = min(
|
||||||
if (!isFullscreen && (it == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || it == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE)) {
|
resources.configuration.screenWidthDp,
|
||||||
|
resources.configuration.screenHeightDp
|
||||||
|
) < resources.getDimension(R.dimen.landscape_threshold)
|
||||||
|
|
||||||
|
if (Settings.instance.playback.isAutoRotate() && isSmallWindow) {
|
||||||
|
if (!isFullscreen && (it == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || it == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE || it == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)) {
|
||||||
_viewDetail?.setFullscreen(true)
|
_viewDetail?.setFullscreen(true)
|
||||||
return@subscribe
|
return@subscribe
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFullscreen && !Settings.instance.playback.fullscreenPortrait && (it == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || it == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT)) {
|
if (isFullscreen && !Settings.instance.playback.fullscreenPortrait && (it == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)) {
|
||||||
_viewDetail?.setFullscreen(false)
|
_viewDetail?.setFullscreen(false)
|
||||||
return@subscribe
|
return@subscribe
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.futo.platformplayer.mdns
|
||||||
|
|
||||||
|
data class BroadcastService(
|
||||||
|
val deviceName: String,
|
||||||
|
val serviceName: String,
|
||||||
|
val port: UShort,
|
||||||
|
val ttl: UInt,
|
||||||
|
val weight: UShort,
|
||||||
|
val priority: UShort,
|
||||||
|
val texts: List<String>? = null
|
||||||
|
)
|
93
app/src/main/java/com/futo/platformplayer/mdns/DnsPacket.kt
Normal file
93
app/src/main/java/com/futo/platformplayer/mdns/DnsPacket.kt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package com.futo.platformplayer.mdns
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
|
||||||
|
enum class QueryResponse(val value: Byte) {
|
||||||
|
Query(0),
|
||||||
|
Response(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DnsOpcode(val value: Byte) {
|
||||||
|
StandardQuery(0),
|
||||||
|
InverseQuery(1),
|
||||||
|
ServerStatusRequest(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DnsResponseCode(val value: Byte) {
|
||||||
|
NoError(0),
|
||||||
|
FormatError(1),
|
||||||
|
ServerFailure(2),
|
||||||
|
NameError(3),
|
||||||
|
NotImplemented(4),
|
||||||
|
Refused(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DnsPacketHeader(
|
||||||
|
val identifier: UShort,
|
||||||
|
val queryResponse: Int,
|
||||||
|
val opcode: Int,
|
||||||
|
val authoritativeAnswer: Boolean,
|
||||||
|
val truncated: Boolean,
|
||||||
|
val recursionDesired: Boolean,
|
||||||
|
val recursionAvailable: Boolean,
|
||||||
|
val answerAuthenticated: Boolean,
|
||||||
|
val nonAuthenticatedData: Boolean,
|
||||||
|
val responseCode: DnsResponseCode
|
||||||
|
)
|
||||||
|
|
||||||
|
data class DnsPacket(
|
||||||
|
val header: DnsPacketHeader,
|
||||||
|
val questions: List<DnsQuestion>,
|
||||||
|
val answers: List<DnsResourceRecord>,
|
||||||
|
val authorities: List<DnsResourceRecord>,
|
||||||
|
val additionals: List<DnsResourceRecord>
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun parse(data: ByteArray): DnsPacket {
|
||||||
|
val span = data.asUByteArray()
|
||||||
|
val flags = (span[2].toInt() shl 8 or span[3].toInt()).toUShort()
|
||||||
|
val questionCount = (span[4].toInt() shl 8 or span[5].toInt()).toUShort()
|
||||||
|
val answerCount = (span[6].toInt() shl 8 or span[7].toInt()).toUShort()
|
||||||
|
val authorityCount = (span[8].toInt() shl 8 or span[9].toInt()).toUShort()
|
||||||
|
val additionalCount = (span[10].toInt() shl 8 or span[11].toInt()).toUShort()
|
||||||
|
|
||||||
|
var position = 12
|
||||||
|
|
||||||
|
val questions = List(questionCount.toInt()) {
|
||||||
|
DnsQuestion.parse(data, position).also { position = it.second }
|
||||||
|
}.map { it.first }
|
||||||
|
|
||||||
|
val answers = List(answerCount.toInt()) {
|
||||||
|
DnsResourceRecord.parse(data, position).also { position = it.second }
|
||||||
|
}.map { it.first }
|
||||||
|
|
||||||
|
val authorities = List(authorityCount.toInt()) {
|
||||||
|
DnsResourceRecord.parse(data, position).also { position = it.second }
|
||||||
|
}.map { it.first }
|
||||||
|
|
||||||
|
val additionals = List(additionalCount.toInt()) {
|
||||||
|
DnsResourceRecord.parse(data, position).also { position = it.second }
|
||||||
|
}.map { it.first }
|
||||||
|
|
||||||
|
return DnsPacket(
|
||||||
|
header = DnsPacketHeader(
|
||||||
|
identifier = (span[0].toInt() shl 8 or span[1].toInt()).toUShort(),
|
||||||
|
queryResponse = ((flags.toUInt() shr 15) and 0b1u).toInt(),
|
||||||
|
opcode = ((flags.toUInt() shr 11) and 0b1111u).toInt(),
|
||||||
|
authoritativeAnswer = (flags.toInt() shr 10) and 0b1 != 0,
|
||||||
|
truncated = (flags.toInt() shr 9) and 0b1 != 0,
|
||||||
|
recursionDesired = (flags.toInt() shr 8) and 0b1 != 0,
|
||||||
|
recursionAvailable = (flags.toInt() shr 7) and 0b1 != 0,
|
||||||
|
answerAuthenticated = (flags.toInt() shr 5) and 0b1 != 0,
|
||||||
|
nonAuthenticatedData = (flags.toInt() shr 4) and 0b1 != 0,
|
||||||
|
responseCode = DnsResponseCode.entries[flags.toInt() and 0b1111]
|
||||||
|
),
|
||||||
|
questions = questions,
|
||||||
|
answers = answers,
|
||||||
|
authorities = authorities,
|
||||||
|
additionals = additionals
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
110
app/src/main/java/com/futo/platformplayer/mdns/DnsQuestion.kt
Normal file
110
app/src/main/java/com/futo/platformplayer/mdns/DnsQuestion.kt
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package com.futo.platformplayer.mdns
|
||||||
|
|
||||||
|
import com.futo.platformplayer.mdns.Extensions.readDomainName
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
|
||||||
|
|
||||||
|
enum class QuestionType(val value: UShort) {
|
||||||
|
A(1u),
|
||||||
|
NS(2u),
|
||||||
|
MD(3u),
|
||||||
|
MF(4u),
|
||||||
|
CNAME(5u),
|
||||||
|
SOA(6u),
|
||||||
|
MB(7u),
|
||||||
|
MG(8u),
|
||||||
|
MR(9u),
|
||||||
|
NULL(10u),
|
||||||
|
WKS(11u),
|
||||||
|
PTR(12u),
|
||||||
|
HINFO(13u),
|
||||||
|
MINFO(14u),
|
||||||
|
MX(15u),
|
||||||
|
TXT(16u),
|
||||||
|
RP(17u),
|
||||||
|
AFSDB(18u),
|
||||||
|
SIG(24u),
|
||||||
|
KEY(25u),
|
||||||
|
AAAA(28u),
|
||||||
|
LOC(29u),
|
||||||
|
SRV(33u),
|
||||||
|
NAPTR(35u),
|
||||||
|
KX(36u),
|
||||||
|
CERT(37u),
|
||||||
|
DNAME(39u),
|
||||||
|
APL(42u),
|
||||||
|
DS(43u),
|
||||||
|
SSHFP(44u),
|
||||||
|
IPSECKEY(45u),
|
||||||
|
RRSIG(46u),
|
||||||
|
NSEC(47u),
|
||||||
|
DNSKEY(48u),
|
||||||
|
DHCID(49u),
|
||||||
|
NSEC3(50u),
|
||||||
|
NSEC3PARAM(51u),
|
||||||
|
TSLA(52u),
|
||||||
|
SMIMEA(53u),
|
||||||
|
HIP(55u),
|
||||||
|
CDS(59u),
|
||||||
|
CDNSKEY(60u),
|
||||||
|
OPENPGPKEY(61u),
|
||||||
|
CSYNC(62u),
|
||||||
|
ZONEMD(63u),
|
||||||
|
SVCB(64u),
|
||||||
|
HTTPS(65u),
|
||||||
|
EUI48(108u),
|
||||||
|
EUI64(109u),
|
||||||
|
TKEY(249u),
|
||||||
|
TSIG(250u),
|
||||||
|
URI(256u),
|
||||||
|
CAA(257u),
|
||||||
|
TA(32768u),
|
||||||
|
DLV(32769u),
|
||||||
|
AXFR(252u),
|
||||||
|
IXFR(251u),
|
||||||
|
OPT(41u),
|
||||||
|
MAILB(253u),
|
||||||
|
MALA(254u),
|
||||||
|
All(252u)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class QuestionClass(val value: UShort) {
|
||||||
|
IN(1u),
|
||||||
|
CS(2u),
|
||||||
|
CH(3u),
|
||||||
|
HS(4u),
|
||||||
|
All(255u)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DnsQuestion(
|
||||||
|
override val name: String,
|
||||||
|
override val type: Int,
|
||||||
|
override val clazz: Int,
|
||||||
|
val queryUnicast: Boolean
|
||||||
|
) : DnsResourceRecordBase(name, type, clazz) {
|
||||||
|
companion object {
|
||||||
|
fun parse(data: ByteArray, startPosition: Int): Pair<DnsQuestion, Int> {
|
||||||
|
val span = data.asUByteArray()
|
||||||
|
var position = startPosition
|
||||||
|
val qname = span.readDomainName(position).also { position = it.second }
|
||||||
|
val qtype = (span[position].toInt() shl 8 or span[position + 1].toInt()).toUShort()
|
||||||
|
position += 2
|
||||||
|
val qclass = (span[position].toInt() shl 8 or span[position + 1].toInt()).toUShort()
|
||||||
|
position += 2
|
||||||
|
|
||||||
|
return DnsQuestion(
|
||||||
|
name = qname.first,
|
||||||
|
type = qtype.toInt(),
|
||||||
|
queryUnicast = ((qclass.toInt() shr 15) and 0b1) != 0,
|
||||||
|
clazz = qclass.toInt() and 0b111111111111111
|
||||||
|
) to position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class DnsResourceRecordBase(
|
||||||
|
open val name: String,
|
||||||
|
open val type: Int,
|
||||||
|
open val clazz: Int
|
||||||
|
)
|
514
app/src/main/java/com/futo/platformplayer/mdns/DnsReader.kt
Normal file
514
app/src/main/java/com/futo/platformplayer/mdns/DnsReader.kt
Normal file
@ -0,0 +1,514 @@
|
|||||||
|
package com.futo.platformplayer.mdns
|
||||||
|
|
||||||
|
import com.futo.platformplayer.mdns.Extensions.readDomainName
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import kotlin.math.pow
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
data class PTRRecord(val domainName: String)
|
||||||
|
|
||||||
|
data class ARecord(val address: InetAddress)
|
||||||
|
|
||||||
|
data class AAAARecord(val address: InetAddress)
|
||||||
|
|
||||||
|
data class MXRecord(val preference: UShort, val exchange: String)
|
||||||
|
|
||||||
|
data class CNAMERecord(val cname: String)
|
||||||
|
|
||||||
|
data class TXTRecord(val texts: List<String>)
|
||||||
|
|
||||||
|
data class SOARecord(
|
||||||
|
val primaryNameServer: String,
|
||||||
|
val responsibleAuthorityMailbox: String,
|
||||||
|
val serialNumber: Int,
|
||||||
|
val refreshInterval: Int,
|
||||||
|
val retryInterval: Int,
|
||||||
|
val expiryLimit: Int,
|
||||||
|
val minimumTTL: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SRVRecord(val priority: UShort, val weight: UShort, val port: UShort, val target: String)
|
||||||
|
|
||||||
|
data class NSRecord(val nameServer: String)
|
||||||
|
|
||||||
|
data class CAARecord(val flags: Byte, val tag: String, val value: String)
|
||||||
|
|
||||||
|
data class HINFORecord(val cpu: String, val os: String)
|
||||||
|
|
||||||
|
data class RPRecord(val mailbox: String, val txtDomainName: String)
|
||||||
|
|
||||||
|
|
||||||
|
data class AFSDBRecord(val subtype: UShort, val hostname: String)
|
||||||
|
data class LOCRecord(
|
||||||
|
val version: Byte,
|
||||||
|
val size: Double,
|
||||||
|
val horizontalPrecision: Double,
|
||||||
|
val verticalPrecision: Double,
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double,
|
||||||
|
val altitude: Double
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun decodeSizeOrPrecision(coded: Byte): Double {
|
||||||
|
val baseValue = (coded.toInt() shr 4) and 0x0F
|
||||||
|
val exponent = coded.toInt() and 0x0F
|
||||||
|
return baseValue * 10.0.pow(exponent.toDouble())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeLatitudeOrLongitude(coded: Int): Double {
|
||||||
|
val arcSeconds = coded / 1E3
|
||||||
|
return arcSeconds / 3600.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeAltitude(coded: Int): Double {
|
||||||
|
return (coded / 100.0) - 100000.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class NAPTRRecord(
|
||||||
|
val order: UShort,
|
||||||
|
val preference: UShort,
|
||||||
|
val flags: String,
|
||||||
|
val services: String,
|
||||||
|
val regexp: String,
|
||||||
|
val replacement: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RRSIGRecord(
|
||||||
|
val typeCovered: UShort,
|
||||||
|
val algorithm: Byte,
|
||||||
|
val labels: Byte,
|
||||||
|
val originalTTL: UInt,
|
||||||
|
val signatureExpiration: UInt,
|
||||||
|
val signatureInception: UInt,
|
||||||
|
val keyTag: UShort,
|
||||||
|
val signersName: String,
|
||||||
|
val signature: ByteArray
|
||||||
|
)
|
||||||
|
|
||||||
|
data class KXRecord(val preference: UShort, val exchanger: String)
|
||||||
|
|
||||||
|
data class CERTRecord(val type: UShort, val keyTag: UShort, val algorithm: Byte, val certificate: ByteArray)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
data class DNAMERecord(val target: String)
|
||||||
|
|
||||||
|
data class DSRecord(val keyTag: UShort, val algorithm: Byte, val digestType: Byte, val digest: ByteArray)
|
||||||
|
|
||||||
|
data class SSHFPRecord(val algorithm: Byte, val fingerprintType: Byte, val fingerprint: ByteArray)
|
||||||
|
|
||||||
|
data class TLSARecord(val usage: Byte, val selector: Byte, val matchingType: Byte, val certificateAssociationData: ByteArray)
|
||||||
|
|
||||||
|
data class SMIMEARecord(val usage: Byte, val selector: Byte, val matchingType: Byte, val certificateAssociationData: ByteArray)
|
||||||
|
|
||||||
|
data class URIRecord(val priority: UShort, val weight: UShort, val target: String)
|
||||||
|
|
||||||
|
data class NSECRecord(val ownerName: String, val typeBitMaps: List<Pair<Byte, ByteArray>>)
|
||||||
|
data class NSEC3Record(
|
||||||
|
val hashAlgorithm: Byte,
|
||||||
|
val flags: Byte,
|
||||||
|
val iterations: UShort,
|
||||||
|
val salt: ByteArray,
|
||||||
|
val nextHashedOwnerName: ByteArray,
|
||||||
|
val typeBitMaps: List<UShort>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class NSEC3PARAMRecord(val hashAlgorithm: Byte, val flags: Byte, val iterations: UShort, val salt: ByteArray)
|
||||||
|
data class SPFRecord(val texts: List<String>)
|
||||||
|
data class TKEYRecord(
|
||||||
|
val algorithm: String,
|
||||||
|
val inception: UInt,
|
||||||
|
val expiration: UInt,
|
||||||
|
val mode: UShort,
|
||||||
|
val error: UShort,
|
||||||
|
val keyData: ByteArray,
|
||||||
|
val otherData: ByteArray
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TSIGRecord(
|
||||||
|
val algorithmName: String,
|
||||||
|
val timeSigned: UInt,
|
||||||
|
val fudge: UShort,
|
||||||
|
val mac: ByteArray,
|
||||||
|
val originalID: UShort,
|
||||||
|
val error: UShort,
|
||||||
|
val otherData: ByteArray
|
||||||
|
)
|
||||||
|
|
||||||
|
data class OPTRecordOption(val code: UShort, val data: ByteArray)
|
||||||
|
data class OPTRecord(val options: List<OPTRecordOption>)
|
||||||
|
|
||||||
|
class DnsReader(private val data: ByteArray, private var position: Int = 0, private val length: Int = data.size) {
|
||||||
|
|
||||||
|
private val endPosition: Int = position + length
|
||||||
|
|
||||||
|
fun readDomainName(): String {
|
||||||
|
return data.asUByteArray().readDomainName(position).also { position = it.second }.first
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readDouble(): Double {
|
||||||
|
checkRemainingBytes(Double.SIZE_BYTES)
|
||||||
|
val result = ByteBuffer.wrap(data, position, Double.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).double
|
||||||
|
position += Double.SIZE_BYTES
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readInt16(): Short {
|
||||||
|
checkRemainingBytes(Short.SIZE_BYTES)
|
||||||
|
val result = ByteBuffer.wrap(data, position, Short.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).short
|
||||||
|
position += Short.SIZE_BYTES
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readInt32(): Int {
|
||||||
|
checkRemainingBytes(Int.SIZE_BYTES)
|
||||||
|
val result = ByteBuffer.wrap(data, position, Int.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).int
|
||||||
|
position += Int.SIZE_BYTES
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readInt64(): Long {
|
||||||
|
checkRemainingBytes(Long.SIZE_BYTES)
|
||||||
|
val result = ByteBuffer.wrap(data, position, Long.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).long
|
||||||
|
position += Long.SIZE_BYTES
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readSingle(): Float {
|
||||||
|
checkRemainingBytes(Float.SIZE_BYTES)
|
||||||
|
val result = ByteBuffer.wrap(data, position, Float.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).float
|
||||||
|
position += Float.SIZE_BYTES
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readByte(): Byte {
|
||||||
|
checkRemainingBytes(Byte.SIZE_BYTES)
|
||||||
|
return data[position++]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readBytes(length: Int): ByteArray {
|
||||||
|
checkRemainingBytes(length)
|
||||||
|
return ByteArray(length).also { data.copyInto(it, startIndex = position, endIndex = position + length) }
|
||||||
|
.also { position += length }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readUInt16(): UShort {
|
||||||
|
checkRemainingBytes(Short.SIZE_BYTES)
|
||||||
|
val result = ByteBuffer.wrap(data, position, Short.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).short.toUShort()
|
||||||
|
position += Short.SIZE_BYTES
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readUInt32(): UInt {
|
||||||
|
checkRemainingBytes(Int.SIZE_BYTES)
|
||||||
|
val result = ByteBuffer.wrap(data, position, Int.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).int.toUInt()
|
||||||
|
position += Int.SIZE_BYTES
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readUInt64(): ULong {
|
||||||
|
checkRemainingBytes(Long.SIZE_BYTES)
|
||||||
|
val result = ByteBuffer.wrap(data, position, Long.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).long.toULong()
|
||||||
|
position += Long.SIZE_BYTES
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readString(): String {
|
||||||
|
val length = data[position++].toInt()
|
||||||
|
checkRemainingBytes(length)
|
||||||
|
return String(data, position, length, StandardCharsets.UTF_8).also { position += length }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkRemainingBytes(requiredBytes: Int) {
|
||||||
|
if (position + requiredBytes > endPosition) throw IndexOutOfBoundsException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readRPRecord(): RPRecord {
|
||||||
|
return RPRecord(readDomainName(), readDomainName())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readKXRecord(): KXRecord {
|
||||||
|
val preference = readUInt16()
|
||||||
|
val exchanger = readDomainName()
|
||||||
|
return KXRecord(preference, exchanger)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readCERTRecord(): CERTRecord {
|
||||||
|
val type = readUInt16()
|
||||||
|
val keyTag = readUInt16()
|
||||||
|
val algorithm = readByte()
|
||||||
|
val certificateLength = readUInt16().toInt() - 5
|
||||||
|
val certificate = readBytes(certificateLength)
|
||||||
|
return CERTRecord(type, keyTag, algorithm, certificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readPTRRecord(): PTRRecord {
|
||||||
|
return PTRRecord(readDomainName())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readARecord(): ARecord {
|
||||||
|
val address = readBytes(4)
|
||||||
|
return ARecord(InetAddress.getByAddress(address))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readAAAARecord(): AAAARecord {
|
||||||
|
val address = readBytes(16)
|
||||||
|
return AAAARecord(InetAddress.getByAddress(address))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readMXRecord(): MXRecord {
|
||||||
|
val preference = readUInt16()
|
||||||
|
val exchange = readDomainName()
|
||||||
|
return MXRecord(preference, exchange)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readCNAMERecord(): CNAMERecord {
|
||||||
|
return CNAMERecord(readDomainName())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readTXTRecord(): TXTRecord {
|
||||||
|
val texts = mutableListOf<String>()
|
||||||
|
while (position < endPosition) {
|
||||||
|
val textLength = data[position++].toInt()
|
||||||
|
checkRemainingBytes(textLength)
|
||||||
|
val text = String(data, position, textLength, StandardCharsets.UTF_8)
|
||||||
|
texts.add(text)
|
||||||
|
position += textLength
|
||||||
|
}
|
||||||
|
return TXTRecord(texts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readSOARecord(): SOARecord {
|
||||||
|
val primaryNameServer = readDomainName()
|
||||||
|
val responsibleAuthorityMailbox = readDomainName()
|
||||||
|
val serialNumber = readInt32()
|
||||||
|
val refreshInterval = readInt32()
|
||||||
|
val retryInterval = readInt32()
|
||||||
|
val expiryLimit = readInt32()
|
||||||
|
val minimumTTL = readInt32()
|
||||||
|
return SOARecord(primaryNameServer, responsibleAuthorityMailbox, serialNumber, refreshInterval, retryInterval, expiryLimit, minimumTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readSRVRecord(): SRVRecord {
|
||||||
|
val priority = readUInt16()
|
||||||
|
val weight = readUInt16()
|
||||||
|
val port = readUInt16()
|
||||||
|
val target = readDomainName()
|
||||||
|
return SRVRecord(priority, weight, port, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readNSRecord(): NSRecord {
|
||||||
|
return NSRecord(readDomainName())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readCAARecord(): CAARecord {
|
||||||
|
val length = readUInt16().toInt()
|
||||||
|
val flags = readByte()
|
||||||
|
val tagLength = readByte().toInt()
|
||||||
|
val tag = String(data, position, tagLength, StandardCharsets.US_ASCII).also { position += tagLength }
|
||||||
|
val valueLength = length - 1 - 1 - tagLength
|
||||||
|
val value = String(data, position, valueLength, StandardCharsets.US_ASCII).also { position += valueLength }
|
||||||
|
return CAARecord(flags, tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readHINFORecord(): HINFORecord {
|
||||||
|
val cpuLength = readByte().toInt()
|
||||||
|
val cpu = String(data, position, cpuLength, StandardCharsets.US_ASCII).also { position += cpuLength }
|
||||||
|
val osLength = readByte().toInt()
|
||||||
|
val os = String(data, position, osLength, StandardCharsets.US_ASCII).also { position += osLength }
|
||||||
|
return HINFORecord(cpu, os)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readAFSDBRecord(): AFSDBRecord {
|
||||||
|
return AFSDBRecord(readUInt16(), readDomainName())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readLOCRecord(): LOCRecord {
|
||||||
|
val version = readByte()
|
||||||
|
val size = LOCRecord.decodeSizeOrPrecision(readByte())
|
||||||
|
val horizontalPrecision = LOCRecord.decodeSizeOrPrecision(readByte())
|
||||||
|
val verticalPrecision = LOCRecord.decodeSizeOrPrecision(readByte())
|
||||||
|
val latitudeCoded = readInt32()
|
||||||
|
val longitudeCoded = readInt32()
|
||||||
|
val altitudeCoded = readInt32()
|
||||||
|
val latitude = LOCRecord.decodeLatitudeOrLongitude(latitudeCoded)
|
||||||
|
val longitude = LOCRecord.decodeLatitudeOrLongitude(longitudeCoded)
|
||||||
|
val altitude = LOCRecord.decodeAltitude(altitudeCoded)
|
||||||
|
return LOCRecord(version, size, horizontalPrecision, verticalPrecision, latitude, longitude, altitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readNAPTRRecord(): NAPTRRecord {
|
||||||
|
val order = readUInt16()
|
||||||
|
val preference = readUInt16()
|
||||||
|
val flags = readString()
|
||||||
|
val services = readString()
|
||||||
|
val regexp = readString()
|
||||||
|
val replacement = readDomainName()
|
||||||
|
return NAPTRRecord(order, preference, flags, services, regexp, replacement)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readDNAMERecord(): DNAMERecord {
|
||||||
|
return DNAMERecord(readDomainName())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readDSRecord(): DSRecord {
|
||||||
|
val keyTag = readUInt16()
|
||||||
|
val algorithm = readByte()
|
||||||
|
val digestType = readByte()
|
||||||
|
val digestLength = readUInt16().toInt() - 4
|
||||||
|
val digest = readBytes(digestLength)
|
||||||
|
return DSRecord(keyTag, algorithm, digestType, digest)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readSSHFPRecord(): SSHFPRecord {
|
||||||
|
val algorithm = readByte()
|
||||||
|
val fingerprintType = readByte()
|
||||||
|
val fingerprintLength = readUInt16().toInt() - 2
|
||||||
|
val fingerprint = readBytes(fingerprintLength)
|
||||||
|
return SSHFPRecord(algorithm, fingerprintType, fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readTLSARecord(): TLSARecord {
|
||||||
|
val usage = readByte()
|
||||||
|
val selector = readByte()
|
||||||
|
val matchingType = readByte()
|
||||||
|
val dataLength = readUInt16().toInt() - 3
|
||||||
|
val certificateAssociationData = readBytes(dataLength)
|
||||||
|
return TLSARecord(usage, selector, matchingType, certificateAssociationData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readSMIMEARecord(): SMIMEARecord {
|
||||||
|
val usage = readByte()
|
||||||
|
val selector = readByte()
|
||||||
|
val matchingType = readByte()
|
||||||
|
val dataLength = readUInt16().toInt() - 3
|
||||||
|
val certificateAssociationData = readBytes(dataLength)
|
||||||
|
return SMIMEARecord(usage, selector, matchingType, certificateAssociationData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readURIRecord(): URIRecord {
|
||||||
|
val priority = readUInt16()
|
||||||
|
val weight = readUInt16()
|
||||||
|
val length = readUInt16().toInt()
|
||||||
|
val target = String(data, position, length, StandardCharsets.US_ASCII).also { position += length }
|
||||||
|
return URIRecord(priority, weight, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readRRSIGRecord(): RRSIGRecord {
|
||||||
|
val typeCovered = readUInt16()
|
||||||
|
val algorithm = readByte()
|
||||||
|
val labels = readByte()
|
||||||
|
val originalTTL = readUInt32()
|
||||||
|
val signatureExpiration = readUInt32()
|
||||||
|
val signatureInception = readUInt32()
|
||||||
|
val keyTag = readUInt16()
|
||||||
|
val signersName = readDomainName()
|
||||||
|
val signatureLength = readUInt16().toInt()
|
||||||
|
val signature = readBytes(signatureLength)
|
||||||
|
return RRSIGRecord(
|
||||||
|
typeCovered,
|
||||||
|
algorithm,
|
||||||
|
labels,
|
||||||
|
originalTTL,
|
||||||
|
signatureExpiration,
|
||||||
|
signatureInception,
|
||||||
|
keyTag,
|
||||||
|
signersName,
|
||||||
|
signature
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readNSECRecord(): NSECRecord {
|
||||||
|
val ownerName = readDomainName()
|
||||||
|
val typeBitMaps = mutableListOf<Pair<Byte, ByteArray>>()
|
||||||
|
while (position < endPosition) {
|
||||||
|
val windowBlock = readByte()
|
||||||
|
val bitmapLength = readByte().toInt()
|
||||||
|
val bitmap = readBytes(bitmapLength)
|
||||||
|
typeBitMaps.add(windowBlock to bitmap)
|
||||||
|
}
|
||||||
|
return NSECRecord(ownerName, typeBitMaps)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readNSEC3Record(): NSEC3Record {
|
||||||
|
val hashAlgorithm = readByte()
|
||||||
|
val flags = readByte()
|
||||||
|
val iterations = readUInt16()
|
||||||
|
val saltLength = readByte().toInt()
|
||||||
|
val salt = readBytes(saltLength)
|
||||||
|
val hashLength = readByte().toInt()
|
||||||
|
val nextHashedOwnerName = readBytes(hashLength)
|
||||||
|
val bitMapLength = readUInt16().toInt()
|
||||||
|
val typeBitMaps = mutableListOf<UShort>()
|
||||||
|
val endPos = position + bitMapLength
|
||||||
|
while (position < endPos) {
|
||||||
|
typeBitMaps.add(readUInt16())
|
||||||
|
}
|
||||||
|
return NSEC3Record(hashAlgorithm, flags, iterations, salt, nextHashedOwnerName, typeBitMaps)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readNSEC3PARAMRecord(): NSEC3PARAMRecord {
|
||||||
|
val hashAlgorithm = readByte()
|
||||||
|
val flags = readByte()
|
||||||
|
val iterations = readUInt16()
|
||||||
|
val saltLength = readByte().toInt()
|
||||||
|
val salt = readBytes(saltLength)
|
||||||
|
return NSEC3PARAMRecord(hashAlgorithm, flags, iterations, salt)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun readSPFRecord(): SPFRecord {
|
||||||
|
val length = readUInt16().toInt()
|
||||||
|
val texts = mutableListOf<String>()
|
||||||
|
val endPos = position + length
|
||||||
|
while (position < endPos) {
|
||||||
|
val textLength = readByte().toInt()
|
||||||
|
val text = String(data, position, textLength, StandardCharsets.US_ASCII).also { position += textLength }
|
||||||
|
texts.add(text)
|
||||||
|
}
|
||||||
|
return SPFRecord(texts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readTKEYRecord(): TKEYRecord {
|
||||||
|
val algorithm = readDomainName()
|
||||||
|
val inception = readUInt32()
|
||||||
|
val expiration = readUInt32()
|
||||||
|
val mode = readUInt16()
|
||||||
|
val error = readUInt16()
|
||||||
|
val keySize = readUInt16().toInt()
|
||||||
|
val keyData = readBytes(keySize)
|
||||||
|
val otherSize = readUInt16().toInt()
|
||||||
|
val otherData = readBytes(otherSize)
|
||||||
|
return TKEYRecord(algorithm, inception, expiration, mode, error, keyData, otherData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readTSIGRecord(): TSIGRecord {
|
||||||
|
val algorithmName = readDomainName()
|
||||||
|
val timeSigned = readUInt32()
|
||||||
|
val fudge = readUInt16()
|
||||||
|
val macSize = readUInt16().toInt()
|
||||||
|
val mac = readBytes(macSize)
|
||||||
|
val originalID = readUInt16()
|
||||||
|
val error = readUInt16()
|
||||||
|
val otherSize = readUInt16().toInt()
|
||||||
|
val otherData = readBytes(otherSize)
|
||||||
|
return TSIGRecord(algorithmName, timeSigned, fudge, mac, originalID, error, otherData)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fun readOPTRecord(): OPTRecord {
|
||||||
|
val options = mutableListOf<OPTRecordOption>()
|
||||||
|
while (position < endPosition) {
|
||||||
|
val optionCode = readUInt16()
|
||||||
|
val optionLength = readUInt16().toInt()
|
||||||
|
val optionData = readBytes(optionLength)
|
||||||
|
options.add(OPTRecordOption(optionCode, optionData))
|
||||||
|
}
|
||||||
|
return OPTRecord(options)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
package com.futo.platformplayer.mdns
|
||||||
|
|
||||||
|
import com.futo.platformplayer.mdns.Extensions.readDomainName
|
||||||
|
|
||||||
|
enum class ResourceRecordType(val value: UShort) {
|
||||||
|
None(0u),
|
||||||
|
A(1u),
|
||||||
|
NS(2u),
|
||||||
|
MD(3u),
|
||||||
|
MF(4u),
|
||||||
|
CNAME(5u),
|
||||||
|
SOA(6u),
|
||||||
|
MB(7u),
|
||||||
|
MG(8u),
|
||||||
|
MR(9u),
|
||||||
|
NULL(10u),
|
||||||
|
WKS(11u),
|
||||||
|
PTR(12u),
|
||||||
|
HINFO(13u),
|
||||||
|
MINFO(14u),
|
||||||
|
MX(15u),
|
||||||
|
TXT(16u),
|
||||||
|
RP(17u),
|
||||||
|
AFSDB(18u),
|
||||||
|
SIG(24u),
|
||||||
|
KEY(25u),
|
||||||
|
AAAA(28u),
|
||||||
|
LOC(29u),
|
||||||
|
SRV(33u),
|
||||||
|
NAPTR(35u),
|
||||||
|
KX(36u),
|
||||||
|
CERT(37u),
|
||||||
|
DNAME(39u),
|
||||||
|
APL(42u),
|
||||||
|
DS(43u),
|
||||||
|
SSHFP(44u),
|
||||||
|
IPSECKEY(45u),
|
||||||
|
RRSIG(46u),
|
||||||
|
NSEC(47u),
|
||||||
|
DNSKEY(48u),
|
||||||
|
DHCID(49u),
|
||||||
|
NSEC3(50u),
|
||||||
|
NSEC3PARAM(51u),
|
||||||
|
TSLA(52u),
|
||||||
|
SMIMEA(53u),
|
||||||
|
HIP(55u),
|
||||||
|
CDS(59u),
|
||||||
|
CDNSKEY(60u),
|
||||||
|
OPENPGPKEY(61u),
|
||||||
|
CSYNC(62u),
|
||||||
|
ZONEMD(63u),
|
||||||
|
SVCB(64u),
|
||||||
|
HTTPS(65u),
|
||||||
|
EUI48(108u),
|
||||||
|
EUI64(109u),
|
||||||
|
TKEY(249u),
|
||||||
|
TSIG(250u),
|
||||||
|
URI(256u),
|
||||||
|
CAA(257u),
|
||||||
|
TA(32768u),
|
||||||
|
DLV(32769u),
|
||||||
|
AXFR(252u),
|
||||||
|
IXFR(251u),
|
||||||
|
OPT(41u)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ResourceRecordClass(val value: UShort) {
|
||||||
|
IN(1u),
|
||||||
|
CS(2u),
|
||||||
|
CH(3u),
|
||||||
|
HS(4u)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DnsResourceRecord(
|
||||||
|
override val name: String,
|
||||||
|
override val type: Int,
|
||||||
|
override val clazz: Int,
|
||||||
|
val timeToLive: UInt,
|
||||||
|
val cacheFlush: Boolean,
|
||||||
|
val dataPosition: Int = -1,
|
||||||
|
val dataLength: Int = -1,
|
||||||
|
private val data: ByteArray? = null
|
||||||
|
) : DnsResourceRecordBase(name, type, clazz) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun parse(data: ByteArray, startPosition: Int): Pair<DnsResourceRecord, Int> {
|
||||||
|
val span = data.asUByteArray()
|
||||||
|
var position = startPosition
|
||||||
|
val name = span.readDomainName(position).also { position = it.second }
|
||||||
|
val type = (span[position].toInt() shl 8 or span[position + 1].toInt()).toUShort()
|
||||||
|
position += 2
|
||||||
|
val clazz = (span[position].toInt() shl 8 or span[position + 1].toInt()).toUShort()
|
||||||
|
position += 2
|
||||||
|
val ttl = (span[position].toInt() shl 24 or (span[position + 1].toInt() shl 16) or
|
||||||
|
(span[position + 2].toInt() shl 8) or span[position + 3].toInt()).toUInt()
|
||||||
|
position += 4
|
||||||
|
val rdlength = (span[position].toInt() shl 8 or span[position + 1].toInt()).toUShort()
|
||||||
|
val rdposition = position + 2
|
||||||
|
position += 2 + rdlength.toInt()
|
||||||
|
|
||||||
|
return DnsResourceRecord(
|
||||||
|
name = name.first,
|
||||||
|
type = type.toInt(),
|
||||||
|
clazz = clazz.toInt() and 0b1111111_11111111,
|
||||||
|
timeToLive = ttl,
|
||||||
|
cacheFlush = ((clazz.toInt() shr 15) and 0b1) != 0,
|
||||||
|
dataPosition = rdposition,
|
||||||
|
dataLength = rdlength.toInt(),
|
||||||
|
data = data
|
||||||
|
) to position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDataReader(): DnsReader {
|
||||||
|
return DnsReader(data!!, dataPosition, dataLength)
|
||||||
|
}
|
||||||
|
}
|
208
app/src/main/java/com/futo/platformplayer/mdns/DnsWriter.kt
Normal file
208
app/src/main/java/com/futo/platformplayer/mdns/DnsWriter.kt
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
package com.futo.platformplayer.mdns
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
class DnsWriter {
|
||||||
|
private val data = mutableListOf<Byte>()
|
||||||
|
private val namePositions = mutableMapOf<String, Int>()
|
||||||
|
|
||||||
|
fun toByteArray(): ByteArray = data.toByteArray()
|
||||||
|
|
||||||
|
fun writePacket(
|
||||||
|
header: DnsPacketHeader,
|
||||||
|
questionCount: Int? = null, questionWriter: ((DnsWriter, Int) -> Unit)? = null,
|
||||||
|
answerCount: Int? = null, answerWriter: ((DnsWriter, Int) -> Unit)? = null,
|
||||||
|
authorityCount: Int? = null, authorityWriter: ((DnsWriter, Int) -> Unit)? = null,
|
||||||
|
additionalsCount: Int? = null, additionalWriter: ((DnsWriter, Int) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
if (questionCount != null && questionWriter == null || questionCount == null && questionWriter != null)
|
||||||
|
throw Exception("When question count is given, question writer should also be given.")
|
||||||
|
if (answerCount != null && answerWriter == null || answerCount == null && answerWriter != null)
|
||||||
|
throw Exception("When answer count is given, answer writer should also be given.")
|
||||||
|
if (authorityCount != null && authorityWriter == null || authorityCount == null && authorityWriter != null)
|
||||||
|
throw Exception("When authority count is given, authority writer should also be given.")
|
||||||
|
if (additionalsCount != null && additionalWriter == null || additionalsCount == null && additionalWriter != null)
|
||||||
|
throw Exception("When additionals count is given, additional writer should also be given.")
|
||||||
|
|
||||||
|
writeHeader(header, questionCount ?: 0, answerCount ?: 0, authorityCount ?: 0, additionalsCount ?: 0)
|
||||||
|
|
||||||
|
repeat(questionCount ?: 0) { questionWriter?.invoke(this, it) }
|
||||||
|
repeat(answerCount ?: 0) { answerWriter?.invoke(this, it) }
|
||||||
|
repeat(authorityCount ?: 0) { authorityWriter?.invoke(this, it) }
|
||||||
|
repeat(additionalsCount ?: 0) { additionalWriter?.invoke(this, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeHeader(header: DnsPacketHeader, questionCount: Int, answerCount: Int, authorityCount: Int, additionalsCount: Int) {
|
||||||
|
write(header.identifier)
|
||||||
|
|
||||||
|
var flags: UShort = 0u
|
||||||
|
flags = flags or ((header.queryResponse.toUInt() and 0xFFFFu) shl 15).toUShort()
|
||||||
|
flags = flags or ((header.opcode.toUInt() and 0xFFFFu) shl 11).toUShort()
|
||||||
|
flags = flags or ((if (header.authoritativeAnswer) 1u else 0u) shl 10).toUShort()
|
||||||
|
flags = flags or ((if (header.truncated) 1u else 0u) shl 9).toUShort()
|
||||||
|
flags = flags or ((if (header.recursionDesired) 1u else 0u) shl 8).toUShort()
|
||||||
|
flags = flags or ((if (header.recursionAvailable) 1u else 0u) shl 7).toUShort()
|
||||||
|
flags = flags or ((if (header.answerAuthenticated) 1u else 0u) shl 5).toUShort()
|
||||||
|
flags = flags or ((if (header.nonAuthenticatedData) 1u else 0u) shl 4).toUShort()
|
||||||
|
flags = flags or header.responseCode.value.toUShort()
|
||||||
|
write(flags)
|
||||||
|
|
||||||
|
write(questionCount.toUShort())
|
||||||
|
write(answerCount.toUShort())
|
||||||
|
write(authorityCount.toUShort())
|
||||||
|
write(additionalsCount.toUShort())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeDomainName(name: String) {
|
||||||
|
synchronized(namePositions) {
|
||||||
|
val labels = name.split('.')
|
||||||
|
for (label in labels) {
|
||||||
|
val nameAtOffset = name.substring(name.indexOf(label))
|
||||||
|
if (namePositions.containsKey(nameAtOffset)) {
|
||||||
|
val position = namePositions[nameAtOffset]!!
|
||||||
|
val pointer = (0b11000000_00000000 or position).toUShort()
|
||||||
|
write(pointer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (label.isNotEmpty()) {
|
||||||
|
val labelBytes = label.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
val nameStartPos = data.size
|
||||||
|
write(labelBytes.size.toByte())
|
||||||
|
write(labelBytes)
|
||||||
|
namePositions[nameAtOffset] = nameStartPos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write(0.toByte()) // End of domain name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: DnsResourceRecord, dataWriter: (DnsWriter) -> Unit) {
|
||||||
|
writeDomainName(value.name)
|
||||||
|
write(value.type.toUShort())
|
||||||
|
val cls = ((if (value.cacheFlush) 1u else 0u) shl 15).toUShort() or value.clazz.toUShort()
|
||||||
|
write(cls)
|
||||||
|
write(value.timeToLive)
|
||||||
|
|
||||||
|
val lengthOffset = data.size
|
||||||
|
write(0.toUShort())
|
||||||
|
dataWriter(this)
|
||||||
|
val rdLength = data.size - lengthOffset - 2
|
||||||
|
val rdLengthBytes = ByteBuffer.allocate(2).order(ByteOrder.BIG_ENDIAN).putShort(rdLength.toShort()).array()
|
||||||
|
data[lengthOffset] = rdLengthBytes[0]
|
||||||
|
data[lengthOffset + 1] = rdLengthBytes[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: DnsQuestion) {
|
||||||
|
writeDomainName(value.name)
|
||||||
|
write(value.type.toUShort())
|
||||||
|
write(((if (value.queryUnicast) 1u else 0u shl 15).toUShort() or value.clazz.toUShort()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: Double) {
|
||||||
|
val bytes = ByteBuffer.allocate(Double.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putDouble(value).array()
|
||||||
|
write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: Short) {
|
||||||
|
val bytes = ByteBuffer.allocate(Short.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putShort(value).array()
|
||||||
|
write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: Int) {
|
||||||
|
val bytes = ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putInt(value).array()
|
||||||
|
write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: Long) {
|
||||||
|
val bytes = ByteBuffer.allocate(Long.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putLong(value).array()
|
||||||
|
write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: Float) {
|
||||||
|
val bytes = ByteBuffer.allocate(Float.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putFloat(value).array()
|
||||||
|
write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: Byte) {
|
||||||
|
data.add(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: ByteArray) {
|
||||||
|
data.addAll(value.asIterable())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: ByteArray, offset: Int, length: Int) {
|
||||||
|
data.addAll(value.slice(offset until offset + length))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: UShort) {
|
||||||
|
val bytes = ByteBuffer.allocate(Short.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putShort(value.toShort()).array()
|
||||||
|
write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: UInt) {
|
||||||
|
val bytes = ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putInt(value.toInt()).array()
|
||||||
|
write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: ULong) {
|
||||||
|
val bytes = ByteBuffer.allocate(Long.SIZE_BYTES).order(ByteOrder.BIG_ENDIAN).putLong(value.toLong()).array()
|
||||||
|
write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: String) {
|
||||||
|
val bytes = value.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
write(bytes.size.toByte())
|
||||||
|
write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: PTRRecord) {
|
||||||
|
writeDomainName(value.domainName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: ARecord) {
|
||||||
|
val bytes = value.address.address
|
||||||
|
if (bytes.size != 4) throw Exception("Unexpected amount of address bytes.")
|
||||||
|
write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: AAAARecord) {
|
||||||
|
val bytes = value.address.address
|
||||||
|
if (bytes.size != 16) throw Exception("Unexpected amount of address bytes.")
|
||||||
|
write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: TXTRecord) {
|
||||||
|
value.texts.forEach {
|
||||||
|
val bytes = it.toByteArray(StandardCharsets.UTF_8)
|
||||||
|
write(bytes.size.toByte())
|
||||||
|
write(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: SRVRecord) {
|
||||||
|
write(value.priority)
|
||||||
|
write(value.weight)
|
||||||
|
write(value.port)
|
||||||
|
writeDomainName(value.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: NSECRecord) {
|
||||||
|
writeDomainName(value.ownerName)
|
||||||
|
value.typeBitMaps.forEach { (windowBlock, bitmap) ->
|
||||||
|
write(windowBlock)
|
||||||
|
write(bitmap.size.toByte())
|
||||||
|
write(bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun write(value: OPTRecord) {
|
||||||
|
value.options.forEach { option ->
|
||||||
|
write(option.code)
|
||||||
|
write(option.data.size.toUShort())
|
||||||
|
write(option.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
app/src/main/java/com/futo/platformplayer/mdns/Extensions.kt
Normal file
63
app/src/main/java/com/futo/platformplayer/mdns/Extensions.kt
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package com.futo.platformplayer.mdns
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
object Extensions {
|
||||||
|
fun ByteArray.toByteDump(): String {
|
||||||
|
val result = StringBuilder()
|
||||||
|
for (i in indices) {
|
||||||
|
result.append(String.format("%02X ", this[i]))
|
||||||
|
|
||||||
|
if ((i + 1) % 16 == 0 || i == size - 1) {
|
||||||
|
val padding = 3 * (16 - (i % 16 + 1))
|
||||||
|
if (i == size - 1 && (i + 1) % 16 != 0) result.append(" ".repeat(padding))
|
||||||
|
|
||||||
|
result.append("; ")
|
||||||
|
val start = i - (i % 16)
|
||||||
|
val end = minOf(i, size - 1)
|
||||||
|
for (j in start..end) {
|
||||||
|
val ch = if (this[j] in 32..127) this[j].toChar() else '.'
|
||||||
|
result.append(ch)
|
||||||
|
}
|
||||||
|
if (i != size - 1) result.appendLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UByteArray.readDomainName(startPosition: Int): Pair<String, Int> {
|
||||||
|
var position = startPosition
|
||||||
|
return readDomainName(position, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun UByteArray.readDomainName(position: Int, depth: Int = 0): Pair<String, Int> {
|
||||||
|
if (depth > 16) throw Exception("Exceeded maximum recursion depth in DNS packet. Possible circular reference.")
|
||||||
|
|
||||||
|
val domainParts = mutableListOf<String>()
|
||||||
|
var newPosition = position
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (newPosition < 0)
|
||||||
|
println()
|
||||||
|
|
||||||
|
val length = this[newPosition].toUByte()
|
||||||
|
if ((length and 0b11000000u).toUInt() == 0b11000000u) {
|
||||||
|
val offset = (((length and 0b00111111u).toUInt()) shl 8) or this[newPosition + 1].toUInt()
|
||||||
|
val (part, _) = this.readDomainName(offset.toInt(), depth + 1)
|
||||||
|
domainParts.add(part)
|
||||||
|
newPosition += 2
|
||||||
|
break
|
||||||
|
} else if (length.toUInt() == 0u) {
|
||||||
|
newPosition++
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
newPosition++
|
||||||
|
val part = String(this.asByteArray(), newPosition, length.toInt(), Charsets.UTF_8)
|
||||||
|
domainParts.add(part)
|
||||||
|
newPosition += length.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return domainParts.joinToString(".") to newPosition
|
||||||
|
}
|
||||||
|
}
|
482
app/src/main/java/com/futo/platformplayer/mdns/MDNSListener.kt
Normal file
482
app/src/main/java/com/futo/platformplayer/mdns/MDNSListener.kt
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
package com.futo.platformplayer.mdns
|
||||||
|
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.net.*
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import kotlin.concurrent.withLock
|
||||||
|
|
||||||
|
class MDNSListener {
|
||||||
|
companion object {
|
||||||
|
private val TAG = "MDNSListener"
|
||||||
|
const val MulticastPort = 5353
|
||||||
|
val MulticastAddressIPv4: InetAddress = InetAddress.getByName("224.0.0.251")
|
||||||
|
val MulticastAddressIPv6: InetAddress = InetAddress.getByName("FF02::FB")
|
||||||
|
val MdnsEndpointIPv6: InetSocketAddress = InetSocketAddress(MulticastAddressIPv6, MulticastPort)
|
||||||
|
val MdnsEndpointIPv4: InetSocketAddress = InetSocketAddress(MulticastAddressIPv4, MulticastPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _lockObject = ReentrantLock()
|
||||||
|
private var _receiver4: DatagramSocket? = null
|
||||||
|
private var _receiver6: DatagramSocket? = null
|
||||||
|
private val _senders = mutableListOf<DatagramSocket>()
|
||||||
|
private val _nicMonitor = NICMonitor()
|
||||||
|
private val _serviceRecordAggregator = ServiceRecordAggregator()
|
||||||
|
private var _started = false
|
||||||
|
private var _threadReceiver4: Thread? = null
|
||||||
|
private var _threadReceiver6: Thread? = null
|
||||||
|
private var _scope: CoroutineScope? = null
|
||||||
|
|
||||||
|
var onPacket: ((DnsPacket) -> Unit)? = null
|
||||||
|
var onServicesUpdated: ((List<DnsService>) -> Unit)? = null
|
||||||
|
|
||||||
|
private val _recordLockObject = ReentrantLock()
|
||||||
|
private val _recordsA = mutableListOf<Pair<DnsResourceRecord, ARecord>>()
|
||||||
|
private val _recordsAAAA = mutableListOf<Pair<DnsResourceRecord, AAAARecord>>()
|
||||||
|
private val _recordsPTR = mutableListOf<Pair<DnsResourceRecord, PTRRecord>>()
|
||||||
|
private val _recordsTXT = mutableListOf<Pair<DnsResourceRecord, TXTRecord>>()
|
||||||
|
private val _recordsSRV = mutableListOf<Pair<DnsResourceRecord, SRVRecord>>()
|
||||||
|
private val _services = mutableListOf<BroadcastService>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
_nicMonitor.added = { onNicsAdded(it) }
|
||||||
|
_nicMonitor.removed = { onNicsRemoved(it) }
|
||||||
|
_serviceRecordAggregator.onServicesUpdated = { onServicesUpdated?.invoke(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
if (_started) throw Exception("Already running.")
|
||||||
|
_started = true
|
||||||
|
|
||||||
|
_scope = CoroutineScope(Dispatchers.IO);
|
||||||
|
|
||||||
|
Logger.i(TAG, "Starting")
|
||||||
|
_lockObject.withLock {
|
||||||
|
val receiver4 = DatagramSocket(null).apply {
|
||||||
|
reuseAddress = true
|
||||||
|
bind(InetSocketAddress(InetAddress.getByName("0.0.0.0"), MulticastPort))
|
||||||
|
}
|
||||||
|
_receiver4 = receiver4
|
||||||
|
|
||||||
|
val receiver6 = DatagramSocket(null).apply {
|
||||||
|
reuseAddress = true
|
||||||
|
bind(InetSocketAddress(InetAddress.getByName("::"), MulticastPort))
|
||||||
|
}
|
||||||
|
_receiver6 = receiver6
|
||||||
|
|
||||||
|
_nicMonitor.start()
|
||||||
|
_serviceRecordAggregator.start()
|
||||||
|
onNicsAdded(_nicMonitor.current)
|
||||||
|
|
||||||
|
_threadReceiver4 = Thread {
|
||||||
|
receiveLoop(receiver4)
|
||||||
|
}.apply { start() }
|
||||||
|
|
||||||
|
_threadReceiver6 = Thread {
|
||||||
|
receiveLoop(receiver6)
|
||||||
|
}.apply { start() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryServices(names: Array<String>) {
|
||||||
|
if (names.isEmpty()) throw IllegalArgumentException("At least one name must be specified.")
|
||||||
|
|
||||||
|
val writer = DnsWriter()
|
||||||
|
writer.writePacket(
|
||||||
|
DnsPacketHeader(
|
||||||
|
identifier = 0u,
|
||||||
|
queryResponse = QueryResponse.Query.value.toInt(),
|
||||||
|
opcode = DnsOpcode.StandardQuery.value.toInt(),
|
||||||
|
truncated = false,
|
||||||
|
nonAuthenticatedData = false,
|
||||||
|
recursionDesired = false,
|
||||||
|
answerAuthenticated = false,
|
||||||
|
authoritativeAnswer = false,
|
||||||
|
recursionAvailable = false,
|
||||||
|
responseCode = DnsResponseCode.NoError
|
||||||
|
),
|
||||||
|
questionCount = names.size,
|
||||||
|
questionWriter = { w, i ->
|
||||||
|
w.write(
|
||||||
|
DnsQuestion(
|
||||||
|
name = names[i],
|
||||||
|
type = QuestionType.PTR.value.toInt(),
|
||||||
|
clazz = QuestionClass.IN.value.toInt(),
|
||||||
|
queryUnicast = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
send(writer.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun send(data: ByteArray) {
|
||||||
|
_lockObject.withLock {
|
||||||
|
for (sender in _senders) {
|
||||||
|
try {
|
||||||
|
val endPoint = if (sender.localAddress is Inet4Address) MdnsEndpointIPv4 else MdnsEndpointIPv6
|
||||||
|
sender.send(DatagramPacket(data, data.size, endPoint))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.i(TAG, "Failed to send on ${sender.localSocketAddress}: ${e.message}.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryAllQuestions(names: Array<String>) {
|
||||||
|
if (names.isEmpty()) throw IllegalArgumentException("At least one name must be specified.")
|
||||||
|
|
||||||
|
val questions = names.flatMap { _serviceRecordAggregator.getAllQuestions(it) }
|
||||||
|
questions.groupBy { it.name }.forEach { (_, questionsForHost) ->
|
||||||
|
val writer = DnsWriter()
|
||||||
|
writer.writePacket(
|
||||||
|
DnsPacketHeader(
|
||||||
|
identifier = 0u,
|
||||||
|
queryResponse = QueryResponse.Query.value.toInt(),
|
||||||
|
opcode = DnsOpcode.StandardQuery.value.toInt(),
|
||||||
|
truncated = false,
|
||||||
|
nonAuthenticatedData = false,
|
||||||
|
recursionDesired = false,
|
||||||
|
answerAuthenticated = false,
|
||||||
|
authoritativeAnswer = false,
|
||||||
|
recursionAvailable = false,
|
||||||
|
responseCode = DnsResponseCode.NoError
|
||||||
|
),
|
||||||
|
questionCount = questionsForHost.size,
|
||||||
|
questionWriter = { w, i -> w.write(questionsForHost[i]) }
|
||||||
|
)
|
||||||
|
send(writer.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onNicsAdded(nics: List<NetworkInterface>) {
|
||||||
|
_lockObject.withLock {
|
||||||
|
if (!_started) return
|
||||||
|
|
||||||
|
val addresses = nics.flatMap { nic ->
|
||||||
|
nic.interfaceAddresses.map { it.address }
|
||||||
|
.filter { it is Inet4Address || (it is Inet6Address && it.isLinkLocalAddress) }
|
||||||
|
}
|
||||||
|
|
||||||
|
addresses.forEach { address ->
|
||||||
|
Logger.i(TAG, "New address discovered $address")
|
||||||
|
|
||||||
|
try {
|
||||||
|
when (address) {
|
||||||
|
is Inet4Address -> {
|
||||||
|
val sender = MulticastSocket(null).apply {
|
||||||
|
reuseAddress = true
|
||||||
|
bind(InetSocketAddress(address, MulticastPort))
|
||||||
|
joinGroup(InetSocketAddress(MulticastAddressIPv4, MulticastPort), NetworkInterface.getByInetAddress(address))
|
||||||
|
}
|
||||||
|
_senders.add(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Inet6Address -> {
|
||||||
|
val sender = MulticastSocket(null).apply {
|
||||||
|
reuseAddress = true
|
||||||
|
bind(InetSocketAddress(address, MulticastPort))
|
||||||
|
joinGroup(InetSocketAddress(MulticastAddressIPv6, MulticastPort), NetworkInterface.getByInetAddress(address))
|
||||||
|
}
|
||||||
|
_senders.add(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw UnsupportedOperationException("Address type ${address.javaClass.name} is not supported.")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.i(TAG, "Exception occurred when processing added address $address: ${e.message}.")
|
||||||
|
// Close the socket if there was an error
|
||||||
|
(_senders.lastOrNull() as? MulticastSocket)?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nics.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
updateBroadcastRecords()
|
||||||
|
broadcastRecords()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.i(TAG, "Exception occurred when broadcasting records: ${e.message}.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onNicsRemoved(nics: List<NetworkInterface>) {
|
||||||
|
_lockObject.withLock {
|
||||||
|
if (!_started) return
|
||||||
|
//TODO: Cleanup?
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nics.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
updateBroadcastRecords()
|
||||||
|
broadcastRecords()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.e(TAG, "Exception occurred when broadcasting records", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun receiveLoop(client: DatagramSocket) {
|
||||||
|
Logger.i(TAG, "Started receive loop")
|
||||||
|
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
val packet = DatagramPacket(buffer, buffer.size)
|
||||||
|
while (_started) {
|
||||||
|
try {
|
||||||
|
client.receive(packet)
|
||||||
|
handleResult(packet)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.e(TAG, "An exception occurred while handling UDP result:", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "Stopped receive loop")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun broadcastService(
|
||||||
|
deviceName: String,
|
||||||
|
serviceName: String,
|
||||||
|
port: UShort,
|
||||||
|
ttl: UInt = 120u,
|
||||||
|
weight: UShort = 0u,
|
||||||
|
priority: UShort = 0u,
|
||||||
|
texts: List<String>? = null
|
||||||
|
) {
|
||||||
|
_recordLockObject.withLock {
|
||||||
|
_services.add(
|
||||||
|
BroadcastService(
|
||||||
|
deviceName = deviceName,
|
||||||
|
port = port,
|
||||||
|
priority = priority,
|
||||||
|
serviceName = serviceName,
|
||||||
|
texts = texts,
|
||||||
|
ttl = ttl,
|
||||||
|
weight = weight
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBroadcastRecords()
|
||||||
|
broadcastRecords()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateBroadcastRecords() {
|
||||||
|
_recordLockObject.withLock {
|
||||||
|
_recordsSRV.clear()
|
||||||
|
_recordsPTR.clear()
|
||||||
|
_recordsA.clear()
|
||||||
|
_recordsAAAA.clear()
|
||||||
|
_recordsTXT.clear()
|
||||||
|
|
||||||
|
_services.forEach { service ->
|
||||||
|
val id = UUID.randomUUID().toString()
|
||||||
|
val deviceDomainName = "${service.deviceName}.${service.serviceName}"
|
||||||
|
val addressName = "$id.local"
|
||||||
|
|
||||||
|
_recordsSRV.add(
|
||||||
|
DnsResourceRecord(
|
||||||
|
clazz = ResourceRecordClass.IN.value.toInt(),
|
||||||
|
type = ResourceRecordType.SRV.value.toInt(),
|
||||||
|
timeToLive = service.ttl,
|
||||||
|
name = deviceDomainName,
|
||||||
|
cacheFlush = false
|
||||||
|
) to SRVRecord(
|
||||||
|
target = addressName,
|
||||||
|
port = service.port,
|
||||||
|
priority = service.priority,
|
||||||
|
weight = service.weight
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_recordsPTR.add(
|
||||||
|
DnsResourceRecord(
|
||||||
|
clazz = ResourceRecordClass.IN.value.toInt(),
|
||||||
|
type = ResourceRecordType.PTR.value.toInt(),
|
||||||
|
timeToLive = service.ttl,
|
||||||
|
name = service.serviceName,
|
||||||
|
cacheFlush = false
|
||||||
|
) to PTRRecord(
|
||||||
|
domainName = deviceDomainName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val addresses = _nicMonitor.current.flatMap { nic ->
|
||||||
|
nic.interfaceAddresses.map { it.address }
|
||||||
|
}
|
||||||
|
|
||||||
|
addresses.forEach { address ->
|
||||||
|
when (address) {
|
||||||
|
is Inet4Address -> _recordsA.add(
|
||||||
|
DnsResourceRecord(
|
||||||
|
clazz = ResourceRecordClass.IN.value.toInt(),
|
||||||
|
type = ResourceRecordType.A.value.toInt(),
|
||||||
|
timeToLive = service.ttl,
|
||||||
|
name = addressName,
|
||||||
|
cacheFlush = false
|
||||||
|
) to ARecord(
|
||||||
|
address = address
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
is Inet6Address -> _recordsAAAA.add(
|
||||||
|
DnsResourceRecord(
|
||||||
|
clazz = ResourceRecordClass.IN.value.toInt(),
|
||||||
|
type = ResourceRecordType.AAAA.value.toInt(),
|
||||||
|
timeToLive = service.ttl,
|
||||||
|
name = addressName,
|
||||||
|
cacheFlush = false
|
||||||
|
) to AAAARecord(
|
||||||
|
address = address
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> Logger.i(TAG, "Invalid address type: $address.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (service.texts != null) {
|
||||||
|
_recordsTXT.add(
|
||||||
|
DnsResourceRecord(
|
||||||
|
clazz = ResourceRecordClass.IN.value.toInt(),
|
||||||
|
type = ResourceRecordType.TXT.value.toInt(),
|
||||||
|
timeToLive = service.ttl,
|
||||||
|
name = deviceDomainName,
|
||||||
|
cacheFlush = false
|
||||||
|
) to TXTRecord(
|
||||||
|
texts = service.texts
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun broadcastRecords(questions: List<DnsQuestion>? = null) {
|
||||||
|
val writer = DnsWriter()
|
||||||
|
_recordLockObject.withLock {
|
||||||
|
val recordsA: List<Pair<DnsResourceRecord, ARecord>>
|
||||||
|
val recordsAAAA: List<Pair<DnsResourceRecord, AAAARecord>>
|
||||||
|
val recordsPTR: List<Pair<DnsResourceRecord, PTRRecord>>
|
||||||
|
val recordsTXT: List<Pair<DnsResourceRecord, TXTRecord>>
|
||||||
|
val recordsSRV: List<Pair<DnsResourceRecord, SRVRecord>>
|
||||||
|
|
||||||
|
if (questions != null) {
|
||||||
|
recordsA = _recordsA.filter { r -> questions.any { q -> q.name == r.first.name && q.clazz == r.first.clazz && q.type == r.first.type } }
|
||||||
|
recordsAAAA = _recordsAAAA.filter { r -> questions.any { q -> q.name == r.first.name && q.clazz == r.first.clazz && q.type == r.first.type } }
|
||||||
|
recordsPTR = _recordsPTR.filter { r -> questions.any { q -> q.name == r.first.name && q.clazz == r.first.clazz && q.type == r.first.type } }
|
||||||
|
recordsSRV = _recordsSRV.filter { r -> questions.any { q -> q.name == r.first.name && q.clazz == r.first.clazz && q.type == r.first.type } }
|
||||||
|
recordsTXT = _recordsTXT.filter { r -> questions.any { q -> q.name == r.first.name && q.clazz == r.first.clazz && q.type == r.first.type } }
|
||||||
|
} else {
|
||||||
|
recordsA = _recordsA
|
||||||
|
recordsAAAA = _recordsAAAA
|
||||||
|
recordsPTR = _recordsPTR
|
||||||
|
recordsSRV = _recordsSRV
|
||||||
|
recordsTXT = _recordsTXT
|
||||||
|
}
|
||||||
|
|
||||||
|
val answerCount = recordsA.size + recordsAAAA.size + recordsPTR.size + recordsSRV.size + recordsTXT.size
|
||||||
|
if (answerCount < 1) return
|
||||||
|
|
||||||
|
val txtOffset = recordsA.size + recordsAAAA.size + recordsPTR.size + recordsSRV.size
|
||||||
|
val srvOffset = recordsA.size + recordsAAAA.size + recordsPTR.size
|
||||||
|
val ptrOffset = recordsA.size + recordsAAAA.size
|
||||||
|
val aaaaOffset = recordsA.size
|
||||||
|
|
||||||
|
writer.writePacket(
|
||||||
|
DnsPacketHeader(
|
||||||
|
identifier = 0u,
|
||||||
|
queryResponse = QueryResponse.Response.value.toInt(),
|
||||||
|
opcode = DnsOpcode.StandardQuery.value.toInt(),
|
||||||
|
truncated = false,
|
||||||
|
nonAuthenticatedData = false,
|
||||||
|
recursionDesired = false,
|
||||||
|
answerAuthenticated = false,
|
||||||
|
authoritativeAnswer = true,
|
||||||
|
recursionAvailable = false,
|
||||||
|
responseCode = DnsResponseCode.NoError
|
||||||
|
),
|
||||||
|
answerCount = answerCount,
|
||||||
|
answerWriter = { w, i ->
|
||||||
|
when {
|
||||||
|
i >= txtOffset -> {
|
||||||
|
val record = recordsTXT[i - txtOffset]
|
||||||
|
w.write(record.first) { it.write(record.second) }
|
||||||
|
}
|
||||||
|
|
||||||
|
i >= srvOffset -> {
|
||||||
|
val record = recordsSRV[i - srvOffset]
|
||||||
|
w.write(record.first) { it.write(record.second) }
|
||||||
|
}
|
||||||
|
|
||||||
|
i >= ptrOffset -> {
|
||||||
|
val record = recordsPTR[i - ptrOffset]
|
||||||
|
w.write(record.first) { it.write(record.second) }
|
||||||
|
}
|
||||||
|
|
||||||
|
i >= aaaaOffset -> {
|
||||||
|
val record = recordsAAAA[i - aaaaOffset]
|
||||||
|
w.write(record.first) { it.write(record.second) }
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val record = recordsA[i]
|
||||||
|
w.write(record.first) { it.write(record.second) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
send(writer.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleResult(result: DatagramPacket) {
|
||||||
|
try {
|
||||||
|
val packet = DnsPacket.parse(result.data)
|
||||||
|
if (packet.questions.isNotEmpty()) {
|
||||||
|
_scope?.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
broadcastRecords(packet.questions)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Broadcasting records failed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
_serviceRecordAggregator.add(packet)
|
||||||
|
onPacket?.invoke(packet)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.v(TAG, "Failed to handle packet: ${Base64.getEncoder().encodeToString(result.data.slice(IntRange(0, result.length - 1)).toByteArray())}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
_lockObject.withLock {
|
||||||
|
_started = false
|
||||||
|
|
||||||
|
_scope?.cancel()
|
||||||
|
_scope = null
|
||||||
|
|
||||||
|
_nicMonitor.stop()
|
||||||
|
_serviceRecordAggregator.stop()
|
||||||
|
|
||||||
|
_receiver4?.close()
|
||||||
|
_receiver4 = null
|
||||||
|
|
||||||
|
_receiver6?.close()
|
||||||
|
_receiver6 = null
|
||||||
|
|
||||||
|
_senders.forEach { it.close() }
|
||||||
|
_senders.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
_threadReceiver4?.join()
|
||||||
|
_threadReceiver4 = null
|
||||||
|
|
||||||
|
_threadReceiver6?.join()
|
||||||
|
_threadReceiver6 = null
|
||||||
|
}
|
||||||
|
}
|
66
app/src/main/java/com/futo/platformplayer/mdns/NICMonitor.kt
Normal file
66
app/src/main/java/com/futo/platformplayer/mdns/NICMonitor.kt
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package com.futo.platformplayer.mdns
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
|
||||||
|
class NICMonitor {
|
||||||
|
private val lockObject = Any()
|
||||||
|
private val nics = mutableListOf<NetworkInterface>()
|
||||||
|
private var cts: Job? = null
|
||||||
|
|
||||||
|
val current: List<NetworkInterface>
|
||||||
|
get() = synchronized(nics) { nics.toList() }
|
||||||
|
|
||||||
|
var added: ((List<NetworkInterface>) -> Unit)? = null
|
||||||
|
var removed: ((List<NetworkInterface>) -> Unit)? = null
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
synchronized(lockObject) {
|
||||||
|
if (cts != null) throw Exception("Already started.")
|
||||||
|
|
||||||
|
cts = CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
loopAsync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nics.clear()
|
||||||
|
nics.addAll(getCurrentInterfaces().toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
synchronized(lockObject) {
|
||||||
|
cts?.cancel()
|
||||||
|
cts = null
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(nics) {
|
||||||
|
nics.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loopAsync() {
|
||||||
|
while (cts?.isActive == true) {
|
||||||
|
try {
|
||||||
|
val currentNics = getCurrentInterfaces().toList()
|
||||||
|
removed?.invoke(nics.filter { k -> currentNics.none { n -> k.name == n.name } })
|
||||||
|
added?.invoke(currentNics.filter { nic -> nics.none { k -> k.name == nic.name } })
|
||||||
|
|
||||||
|
synchronized(nics) {
|
||||||
|
nics.clear()
|
||||||
|
nics.addAll(currentNics)
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
delay(5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCurrentInterfaces(): List<NetworkInterface> {
|
||||||
|
val nics = NetworkInterface.getNetworkInterfaces().toList()
|
||||||
|
.filter { it.isUp && !it.isLoopback }
|
||||||
|
|
||||||
|
return if (nics.isNotEmpty()) nics else NetworkInterface.getNetworkInterfaces().toList()
|
||||||
|
.filter { it.isUp }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
package com.futo.platformplayer.mdns
|
||||||
|
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import java.lang.Thread.sleep
|
||||||
|
|
||||||
|
class ServiceDiscoverer(names: Array<String>, private val _onServicesUpdated: (List<DnsService>) -> Unit) {
|
||||||
|
private val _names: Array<String>
|
||||||
|
private var _listener: MDNSListener? = null
|
||||||
|
private var _started = false
|
||||||
|
private var _thread: Thread? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (names.isEmpty()) throw IllegalArgumentException("At least one name must be specified.")
|
||||||
|
_names = names
|
||||||
|
}
|
||||||
|
|
||||||
|
fun broadcastService(
|
||||||
|
deviceName: String,
|
||||||
|
serviceName: String,
|
||||||
|
port: UShort,
|
||||||
|
ttl: UInt = 120u,
|
||||||
|
weight: UShort = 0u,
|
||||||
|
priority: UShort = 0u,
|
||||||
|
texts: List<String>? = null
|
||||||
|
) {
|
||||||
|
_listener?.let {
|
||||||
|
it.broadcastService(deviceName, serviceName, port, ttl, weight, priority, texts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
_started = false
|
||||||
|
_listener?.stop()
|
||||||
|
_listener = null
|
||||||
|
_thread?.join()
|
||||||
|
_thread = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
if (_started) throw Exception("Already running.")
|
||||||
|
_started = true
|
||||||
|
|
||||||
|
val listener = MDNSListener()
|
||||||
|
_listener = listener
|
||||||
|
listener.onServicesUpdated = { _onServicesUpdated?.invoke(it) }
|
||||||
|
listener.start()
|
||||||
|
|
||||||
|
_thread = Thread {
|
||||||
|
try {
|
||||||
|
sleep(2000)
|
||||||
|
|
||||||
|
while (_started) {
|
||||||
|
listener.queryServices(_names)
|
||||||
|
sleep(2000)
|
||||||
|
listener.queryAllQuestions(_names)
|
||||||
|
sleep(2000)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.i(TAG, "Exception in loop thread", e)
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}.apply { start() }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "ServiceDiscoverer"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,219 @@
|
|||||||
|
package com.futo.platformplayer.mdns
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
data class DnsService(
|
||||||
|
var name: String,
|
||||||
|
var target: String,
|
||||||
|
var port: UShort,
|
||||||
|
val addresses: MutableList<InetAddress> = mutableListOf(),
|
||||||
|
val pointers: MutableList<String> = mutableListOf(),
|
||||||
|
val texts: MutableList<String> = mutableListOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CachedDnsAddressRecord(
|
||||||
|
val expirationTime: Date,
|
||||||
|
val address: InetAddress
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CachedDnsTxtRecord(
|
||||||
|
val expirationTime: Date,
|
||||||
|
val texts: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CachedDnsPtrRecord(
|
||||||
|
val expirationTime: Date,
|
||||||
|
val target: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CachedDnsSrvRecord(
|
||||||
|
val expirationTime: Date,
|
||||||
|
val service: SRVRecord
|
||||||
|
)
|
||||||
|
|
||||||
|
class ServiceRecordAggregator {
|
||||||
|
private val _lockObject = Any()
|
||||||
|
private val _cachedAddressRecords = mutableMapOf<String, MutableList<CachedDnsAddressRecord>>()
|
||||||
|
private val _cachedTxtRecords = mutableMapOf<String, CachedDnsTxtRecord>()
|
||||||
|
private val _cachedPtrRecords = mutableMapOf<String, MutableList<CachedDnsPtrRecord>>()
|
||||||
|
private val _cachedSrvRecords = mutableMapOf<String, CachedDnsSrvRecord>()
|
||||||
|
private val _currentServices = mutableListOf<DnsService>()
|
||||||
|
private var _cts: Job? = null
|
||||||
|
|
||||||
|
var onServicesUpdated: ((List<DnsService>) -> Unit)? = null
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
synchronized(_lockObject) {
|
||||||
|
if (_cts != null) throw Exception("Already started.")
|
||||||
|
|
||||||
|
_cts = CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
while (isActive) {
|
||||||
|
val now = Date()
|
||||||
|
synchronized(_currentServices) {
|
||||||
|
_cachedAddressRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
|
||||||
|
_cachedTxtRecords.entries.removeIf { now.after(it.value.expirationTime) }
|
||||||
|
_cachedSrvRecords.entries.removeIf { now.after(it.value.expirationTime) }
|
||||||
|
_cachedPtrRecords.forEach { it.value.removeAll { record -> now.after(record.expirationTime) } }
|
||||||
|
|
||||||
|
val newServices = getCurrentServices()
|
||||||
|
_currentServices.clear()
|
||||||
|
_currentServices.addAll(newServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
onServicesUpdated?.invoke(_currentServices)
|
||||||
|
delay(5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
synchronized(_lockObject) {
|
||||||
|
_cts?.cancel()
|
||||||
|
_cts = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(packet: DnsPacket) {
|
||||||
|
val dnsResourceRecords = packet.answers + packet.additionals + packet.authorities
|
||||||
|
val txtRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.TXT.value.toInt() }.map { it to it.getDataReader().readTXTRecord() }
|
||||||
|
val aRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.A.value.toInt() }.map { it to it.getDataReader().readARecord() }
|
||||||
|
val aaaaRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.AAAA.value.toInt() }.map { it to it.getDataReader().readAAAARecord() }
|
||||||
|
val srvRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.SRV.value.toInt() }.map { it to it.getDataReader().readSRVRecord() }
|
||||||
|
val ptrRecords = dnsResourceRecords.filter { it.type == ResourceRecordType.PTR.value.toInt() }.map { it to it.getDataReader().readPTRRecord() }
|
||||||
|
|
||||||
|
/*val builder = StringBuilder()
|
||||||
|
builder.appendLine("Received records:")
|
||||||
|
srvRecords.forEach { builder.appendLine(" ${it.first.name} ${it.first.type} ${it.first.clazz} TTL ${it.first.timeToLive}: (Port: ${it.second.port}, Target: ${it.second.target}, Priority: ${it.second.priority}, Weight: ${it.second.weight})") }
|
||||||
|
ptrRecords.forEach { builder.appendLine(" ${it.first.name} ${it.first.type} ${it.first.clazz} TTL ${it.first.timeToLive}: ${it.second.domainName}") }
|
||||||
|
txtRecords.forEach { builder.appendLine(" ${it.first.name} ${it.first.type} ${it.first.clazz} TTL ${it.first.timeToLive}: ${it.second.texts.joinToString(", ")}") }
|
||||||
|
aRecords.forEach { builder.appendLine(" ${it.first.name} ${it.first.type} ${it.first.clazz} TTL ${it.first.timeToLive}: ${it.second.address}") }
|
||||||
|
aaaaRecords.forEach { builder.appendLine(" ${it.first.name} ${it.first.type} ${it.first.clazz} TTL ${it.first.timeToLive}: ${it.second.address}") }
|
||||||
|
synchronized(lockObject) {
|
||||||
|
// Save to file if necessary
|
||||||
|
}*/
|
||||||
|
|
||||||
|
val currentServices: MutableList<DnsService>
|
||||||
|
synchronized(this._currentServices) {
|
||||||
|
ptrRecords.forEach { record ->
|
||||||
|
val cachedPtrRecord = _cachedPtrRecords.getOrPut(record.first.name) { mutableListOf() }
|
||||||
|
val newPtrRecord = CachedDnsPtrRecord(Date(System.currentTimeMillis() + record.first.timeToLive.toLong() * 1000L), record.second.domainName)
|
||||||
|
cachedPtrRecord.replaceOrAdd(newPtrRecord) { it.target == record.second.domainName }
|
||||||
|
|
||||||
|
aRecords.forEach { aRecord ->
|
||||||
|
val cachedARecord = _cachedAddressRecords.getOrPut(aRecord.first.name) { mutableListOf() }
|
||||||
|
val newARecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aRecord.first.timeToLive.toLong() * 1000L), aRecord.second.address)
|
||||||
|
cachedARecord.replaceOrAdd(newARecord) { it.address == newARecord.address }
|
||||||
|
}
|
||||||
|
|
||||||
|
aaaaRecords.forEach { aaaaRecord ->
|
||||||
|
val cachedAaaaRecord = _cachedAddressRecords.getOrPut(aaaaRecord.first.name) { mutableListOf() }
|
||||||
|
val newAaaaRecord = CachedDnsAddressRecord(Date(System.currentTimeMillis() + aaaaRecord.first.timeToLive.toLong() * 1000L), aaaaRecord.second.address)
|
||||||
|
cachedAaaaRecord.replaceOrAdd(newAaaaRecord) { it.address == newAaaaRecord.address }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
txtRecords.forEach { txtRecord ->
|
||||||
|
_cachedTxtRecords[txtRecord.first.name] = CachedDnsTxtRecord(Date(System.currentTimeMillis() + txtRecord.first.timeToLive.toLong() * 1000L), txtRecord.second.texts)
|
||||||
|
}
|
||||||
|
|
||||||
|
srvRecords.forEach { srvRecord ->
|
||||||
|
_cachedSrvRecords[srvRecord.first.name] = CachedDnsSrvRecord(Date(System.currentTimeMillis() + srvRecord.first.timeToLive.toLong() * 1000L), srvRecord.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentServices = getCurrentServices()
|
||||||
|
this._currentServices.clear()
|
||||||
|
this._currentServices.addAll(currentServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
onServicesUpdated?.invoke(currentServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllQuestions(serviceName: String): List<DnsQuestion> {
|
||||||
|
val questions = mutableListOf<DnsQuestion>()
|
||||||
|
synchronized(_currentServices) {
|
||||||
|
val servicePtrRecords = _cachedPtrRecords[serviceName] ?: return emptyList()
|
||||||
|
|
||||||
|
val ptrWithoutSrvRecord = servicePtrRecords.filterNot { _cachedSrvRecords.containsKey(it.target) }.map { it.target }
|
||||||
|
questions.addAll(ptrWithoutSrvRecord.flatMap { s ->
|
||||||
|
listOf(
|
||||||
|
DnsQuestion(
|
||||||
|
name = s,
|
||||||
|
type = QuestionType.SRV.value.toInt(),
|
||||||
|
clazz = QuestionClass.IN.value.toInt(),
|
||||||
|
queryUnicast = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
val incompleteCurrentServices = _currentServices.filter { it.addresses.isEmpty() && it.name.endsWith(serviceName) }
|
||||||
|
questions.addAll(incompleteCurrentServices.flatMap { s ->
|
||||||
|
listOf(
|
||||||
|
DnsQuestion(
|
||||||
|
name = s.name,
|
||||||
|
type = QuestionType.TXT.value.toInt(),
|
||||||
|
clazz = QuestionClass.IN.value.toInt(),
|
||||||
|
queryUnicast = false
|
||||||
|
),
|
||||||
|
DnsQuestion(
|
||||||
|
name = s.target,
|
||||||
|
type = QuestionType.A.value.toInt(),
|
||||||
|
clazz = QuestionClass.IN.value.toInt(),
|
||||||
|
queryUnicast = false
|
||||||
|
),
|
||||||
|
DnsQuestion(
|
||||||
|
name = s.target,
|
||||||
|
type = QuestionType.AAAA.value.toInt(),
|
||||||
|
clazz = QuestionClass.IN.value.toInt(),
|
||||||
|
queryUnicast = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return questions
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCurrentServices(): MutableList<DnsService> {
|
||||||
|
val currentServices = _cachedSrvRecords.map { (key, value) ->
|
||||||
|
DnsService(
|
||||||
|
name = key,
|
||||||
|
target = value.service.target,
|
||||||
|
port = value.service.port
|
||||||
|
)
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
|
currentServices.forEach { service ->
|
||||||
|
_cachedAddressRecords[service.target]?.let {
|
||||||
|
service.addresses.addAll(it.map { record -> record.address })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentServices.forEach { service ->
|
||||||
|
service.pointers.addAll(_cachedPtrRecords.filter { it.value.any { ptr -> ptr.target == service.name } }.map { it.key })
|
||||||
|
}
|
||||||
|
|
||||||
|
currentServices.forEach { service ->
|
||||||
|
_cachedTxtRecords[service.name]?.let {
|
||||||
|
service.texts.addAll(it.texts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentServices
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> MutableList<T>.replaceOrAdd(newElement: T, predicate: (T) -> Boolean) {
|
||||||
|
val index = indexOfFirst(predicate)
|
||||||
|
if (index >= 0) {
|
||||||
|
this[index] = newElement
|
||||||
|
} else {
|
||||||
|
add(newElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,7 +16,7 @@ enum class FeedStyle(val value: Int) {
|
|||||||
|
|
||||||
fun fromInt(value: Int): FeedStyle
|
fun fromInt(value: Int): FeedStyle
|
||||||
{
|
{
|
||||||
val result = FeedStyle.values().firstOrNull { it.value == value };
|
val result = FeedStyle.entries.firstOrNull { it.value == value };
|
||||||
if(result == null)
|
if(result == null)
|
||||||
throw UnknownPlatformException(value.toString());
|
throw UnknownPlatformException(value.toString());
|
||||||
return result;
|
return result;
|
||||||
|
9
app/src/main/res/drawable/battery_full_24px.xml
Normal file
9
app/src/main/res/drawable/battery_full_24px.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M320,880Q303,880 291.5,868.5Q280,857 280,840L280,200Q280,183 291.5,171.5Q303,160 320,160L400,160L400,80L560,80L560,160L640,160Q657,160 668.5,171.5Q680,183 680,200L680,840Q680,857 668.5,868.5Q657,880 640,880L320,880Z"/>
|
||||||
|
</vector>
|
@ -2,4 +2,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<dimen name="video_view_right_padding"></dimen>
|
<dimen name="video_view_right_padding"></dimen>
|
||||||
<dimen name="app_bar_height">200dp</dimen>
|
<dimen name="app_bar_height">200dp</dimen>
|
||||||
|
<dimen name="landscape_threshold">300dp</dimen>
|
||||||
</resources>
|
</resources>
|
394
app/src/test/java/com/futo/platformplayer/MdnsTests.kt
Normal file
394
app/src/test/java/com/futo/platformplayer/MdnsTests.kt
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import com.futo.platformplayer.mdns.DnsOpcode
|
||||||
|
import com.futo.platformplayer.mdns.DnsPacket
|
||||||
|
import com.futo.platformplayer.mdns.DnsPacketHeader
|
||||||
|
import com.futo.platformplayer.mdns.DnsQuestion
|
||||||
|
import com.futo.platformplayer.mdns.DnsReader
|
||||||
|
import com.futo.platformplayer.mdns.DnsResponseCode
|
||||||
|
import com.futo.platformplayer.mdns.DnsWriter
|
||||||
|
import com.futo.platformplayer.mdns.QueryResponse
|
||||||
|
import com.futo.platformplayer.mdns.QuestionClass
|
||||||
|
import com.futo.platformplayer.mdns.QuestionType
|
||||||
|
import com.futo.platformplayer.mdns.ResourceRecordClass
|
||||||
|
import com.futo.platformplayer.mdns.ResourceRecordType
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import junit.framework.TestCase.assertTrue
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.net.InetAddress
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertContentEquals
|
||||||
|
|
||||||
|
|
||||||
|
class MdnsTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `BasicOperation`() {
|
||||||
|
val expectedData = byteArrayOf(
|
||||||
|
0x00, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 0x02,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
|
||||||
|
0x00, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 0x02,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03
|
||||||
|
)
|
||||||
|
|
||||||
|
val writer = DnsWriter()
|
||||||
|
writer.write(1.toUShort())
|
||||||
|
writer.write(2.toUInt())
|
||||||
|
writer.write(3.toULong())
|
||||||
|
writer.write(1.toShort())
|
||||||
|
writer.write(2)
|
||||||
|
writer.write(3L)
|
||||||
|
|
||||||
|
assertContentEquals(expectedData, writer.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `DnsQuestionFormat`() {
|
||||||
|
val expectedBytes = ubyteArrayOf(
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x08u, 0x5fu, 0x61u, 0x69u, 0x72u, 0x70u, 0x6cu, 0x61u, 0x79u, 0x04u, 0x5fu, 0x74u, 0x63u, 0x70u, 0x05u, 0x6cu, 0x6fu, 0x63u, 0x61u, 0x6cu, 0x00u, 0x00u, 0x0cu, 0x00u, 0x01u
|
||||||
|
).asByteArray()
|
||||||
|
|
||||||
|
val writer = DnsWriter()
|
||||||
|
writer.writePacket(
|
||||||
|
header = DnsPacketHeader(
|
||||||
|
identifier = 0.toUShort(),
|
||||||
|
queryResponse = QueryResponse.Query.value.toInt(),
|
||||||
|
opcode = DnsOpcode.StandardQuery.value.toInt(),
|
||||||
|
authoritativeAnswer = false,
|
||||||
|
truncated = false,
|
||||||
|
recursionDesired = false,
|
||||||
|
recursionAvailable = false,
|
||||||
|
answerAuthenticated = false,
|
||||||
|
nonAuthenticatedData = false,
|
||||||
|
responseCode = DnsResponseCode.NoError
|
||||||
|
),
|
||||||
|
questionCount = 1,
|
||||||
|
questionWriter = { w, _ ->
|
||||||
|
w.write(DnsQuestion(
|
||||||
|
name = "_airplay._tcp.local",
|
||||||
|
type = QuestionType.PTR.value.toInt(),
|
||||||
|
clazz = QuestionClass.IN.value.toInt(),
|
||||||
|
queryUnicast = false
|
||||||
|
))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assertContentEquals(expectedBytes, writer.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `BeyondTests`() {
|
||||||
|
val data = byteArrayOf(
|
||||||
|
0x00, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 0x02,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
|
||||||
|
0x00, 0x01,
|
||||||
|
0x00, 0x00, 0x00, 0x02,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03
|
||||||
|
)
|
||||||
|
|
||||||
|
val reader = DnsReader(data)
|
||||||
|
assertEquals(1, reader.readInt16())
|
||||||
|
assertEquals(2, reader.readInt32())
|
||||||
|
assertEquals(3L, reader.readInt64())
|
||||||
|
assertEquals(1.toUShort(), reader.readUInt16())
|
||||||
|
assertEquals(2.toUInt(), reader.readUInt32())
|
||||||
|
assertEquals(3.toULong(), reader.readUInt64())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ParseDnsPrinter`() {
|
||||||
|
val data = ubyteArrayOf(
|
||||||
|
0x00u, 0x00u,
|
||||||
|
0x84u, 0x00u, 0x00u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x00u, 0x06u, 0x04u, 0x5fu, 0x69u, 0x70u, 0x70u, 0x04u,
|
||||||
|
0x5fu, 0x74u, 0x63u, 0x70u, 0x05u, 0x6cu, 0x6fu, 0x63u, 0x61u, 0x6cu, 0x00u, 0x00u, 0x0cu, 0x00u, 0x01u, 0x00u,
|
||||||
|
0x00u, 0x11u, 0x94u, 0x00u, 0x1eu, 0x1bu, 0x42u, 0x72u, 0x6fu, 0x74u, 0x68u, 0x65u, 0x72u, 0x20u, 0x44u, 0x43u,
|
||||||
|
0x50u, 0x2du, 0x4cu, 0x33u, 0x35u, 0x35u, 0x30u, 0x43u, 0x44u, 0x57u, 0x20u, 0x73u, 0x65u, 0x72u, 0x69u, 0x65u,
|
||||||
|
0x73u, 0xc0u, 0x0cu, 0xc0u, 0x27u, 0x00u, 0x10u, 0x80u, 0x01u, 0x00u, 0x00u, 0x11u, 0x94u, 0x02u, 0x53u, 0x09u,
|
||||||
|
0x74u, 0x78u, 0x74u, 0x76u, 0x65u, 0x72u, 0x73u, 0x3du, 0x31u, 0x08u, 0x71u, 0x74u, 0x6fu, 0x74u, 0x61u, 0x6cu,
|
||||||
|
0x3du, 0x31u, 0x42u, 0x70u, 0x64u, 0x6cu, 0x3du, 0x61u, 0x70u, 0x70u, 0x6cu, 0x69u, 0x63u, 0x61u, 0x74u, 0x69u,
|
||||||
|
0x6fu, 0x6eu, 0x2fu, 0x6fu, 0x63u, 0x74u, 0x65u, 0x74u, 0x2du, 0x73u, 0x74u, 0x72u, 0x65u, 0x61u, 0x6du, 0x2cu,
|
||||||
|
0x69u, 0x6du, 0x61u, 0x67u, 0x65u, 0x2fu, 0x75u, 0x72u, 0x66u, 0x2cu, 0x69u, 0x6du, 0x61u, 0x67u, 0x65u, 0x2fu,
|
||||||
|
0x6au, 0x70u, 0x65u, 0x67u, 0x2cu, 0x69u, 0x6du, 0x61u, 0x67u, 0x65u, 0x2fu, 0x70u, 0x77u, 0x67u, 0x2du, 0x72u,
|
||||||
|
0x61u, 0x73u, 0x74u, 0x65u, 0x72u, 0x0cu, 0x72u, 0x70u, 0x3du, 0x69u, 0x70u, 0x70u, 0x2fu, 0x70u, 0x72u, 0x69u,
|
||||||
|
0x6eu, 0x74u, 0x05u, 0x6eu, 0x6fu, 0x74u, 0x65u, 0x3du, 0x1eu, 0x74u, 0x79u, 0x3du, 0x42u, 0x72u, 0x6fu, 0x74u,
|
||||||
|
0x68u, 0x65u, 0x72u, 0x20u, 0x44u, 0x43u, 0x50u, 0x2du, 0x4cu, 0x33u, 0x35u, 0x35u, 0x30u, 0x43u, 0x44u, 0x57u,
|
||||||
|
0x20u, 0x73u, 0x65u, 0x72u, 0x69u, 0x65u, 0x73u, 0x25u, 0x70u, 0x72u, 0x6fu, 0x64u, 0x75u, 0x63u, 0x74u, 0x3du,
|
||||||
|
0x28u, 0x42u, 0x72u, 0x6fu, 0x74u, 0x68u, 0x65u, 0x72u, 0x20u, 0x44u, 0x43u, 0x50u, 0x2du, 0x4cu, 0x33u, 0x35u,
|
||||||
|
0x35u, 0x30u, 0x43u, 0x44u, 0x57u, 0x20u, 0x73u, 0x65u, 0x72u, 0x69u, 0x65u, 0x73u, 0x29u, 0x3cu, 0x61u, 0x64u,
|
||||||
|
0x6du, 0x69u, 0x6eu, 0x75u, 0x72u, 0x6cu, 0x3du, 0x68u, 0x74u, 0x74u, 0x70u, 0x3au, 0x2fu, 0x2fu, 0x42u, 0x52u,
|
||||||
|
0x57u, 0x31u, 0x30u, 0x35u, 0x42u, 0x41u, 0x44u, 0x34u, 0x41u, 0x31u, 0x35u, 0x37u, 0x30u, 0x2eu, 0x6cu, 0x6fu,
|
||||||
|
0x63u, 0x61u, 0x6cu, 0x2eu, 0x2fu, 0x6eu, 0x65u, 0x74u, 0x2fu, 0x6eu, 0x65u, 0x74u, 0x2fu, 0x61u, 0x69u, 0x72u,
|
||||||
|
0x70u, 0x72u, 0x69u, 0x6eu, 0x74u, 0x2eu, 0x68u, 0x74u, 0x6du, 0x6cu, 0x0bu, 0x70u, 0x72u, 0x69u, 0x6fu, 0x72u,
|
||||||
|
0x69u, 0x74u, 0x79u, 0x3du, 0x32u, 0x35u, 0x0fu, 0x75u, 0x73u, 0x62u, 0x5fu, 0x4du, 0x46u, 0x47u, 0x3du, 0x42u,
|
||||||
|
0x72u, 0x6fu, 0x74u, 0x68u, 0x65u, 0x72u, 0x1bu, 0x75u, 0x73u, 0x62u, 0x5fu, 0x4du, 0x44u, 0x4cu, 0x3du, 0x44u,
|
||||||
|
0x43u, 0x50u, 0x2du, 0x4cu, 0x33u, 0x35u, 0x35u, 0x30u, 0x43u, 0x44u, 0x57u, 0x20u, 0x73u, 0x65u, 0x72u, 0x69u,
|
||||||
|
0x65u, 0x73u, 0x19u, 0x75u, 0x73u, 0x62u, 0x5fu, 0x43u, 0x4du, 0x44u, 0x3du, 0x50u, 0x4au, 0x4cu, 0x2cu, 0x50u,
|
||||||
|
0x43u, 0x4cu, 0x2cu, 0x50u, 0x43u, 0x4cu, 0x58u, 0x4cu, 0x2cu, 0x55u, 0x52u, 0x46u, 0x07u, 0x43u, 0x6fu, 0x6cu,
|
||||||
|
0x6fu, 0x72u, 0x3du, 0x54u, 0x08u, 0x43u, 0x6fu, 0x70u, 0x69u, 0x65u, 0x73u, 0x3du, 0x54u, 0x08u, 0x44u, 0x75u,
|
||||||
|
0x70u, 0x6cu, 0x65u, 0x78u, 0x3du, 0x54u, 0x05u, 0x46u, 0x61u, 0x78u, 0x3du, 0x46u, 0x06u, 0x53u, 0x63u, 0x61u,
|
||||||
|
0x6eu, 0x3du, 0x54u, 0x0du, 0x50u, 0x61u, 0x70u, 0x65u, 0x72u, 0x43u, 0x75u, 0x73u, 0x74u, 0x6fu, 0x6du, 0x3du,
|
||||||
|
0x54u, 0x08u, 0x42u, 0x69u, 0x6eu, 0x61u, 0x72u, 0x79u, 0x3du, 0x54u, 0x0du, 0x54u, 0x72u, 0x61u, 0x6eu, 0x73u,
|
||||||
|
0x70u, 0x61u, 0x72u, 0x65u, 0x6eu, 0x74u, 0x3du, 0x54u, 0x06u, 0x54u, 0x42u, 0x43u, 0x50u, 0x3du, 0x46u, 0x3eu,
|
||||||
|
0x55u, 0x52u, 0x46u, 0x3du, 0x53u, 0x52u, 0x47u, 0x42u, 0x32u, 0x34u, 0x2cu, 0x57u, 0x38u, 0x2cu, 0x43u, 0x50u,
|
||||||
|
0x31u, 0x2cu, 0x49u, 0x53u, 0x34u, 0x2du, 0x31u, 0x2cu, 0x4du, 0x54u, 0x31u, 0x2du, 0x33u, 0x2du, 0x34u, 0x2du,
|
||||||
|
0x35u, 0x2du, 0x38u, 0x2du, 0x31u, 0x31u, 0x2cu, 0x4fu, 0x42u, 0x31u, 0x30u, 0x2cu, 0x50u, 0x51u, 0x34u, 0x2cu,
|
||||||
|
0x52u, 0x53u, 0x36u, 0x30u, 0x30u, 0x2cu, 0x56u, 0x31u, 0x2eu, 0x34u, 0x2cu, 0x44u, 0x4du, 0x31u, 0x25u, 0x6bu,
|
||||||
|
0x69u, 0x6eu, 0x64u, 0x3du, 0x64u, 0x6fu, 0x63u, 0x75u, 0x6du, 0x65u, 0x6eu, 0x74u, 0x2cu, 0x65u, 0x6eu, 0x76u,
|
||||||
|
0x65u, 0x6cu, 0x6fu, 0x70u, 0x65u, 0x2cu, 0x6cu, 0x61u, 0x62u, 0x65u, 0x6cu, 0x2cu, 0x70u, 0x6fu, 0x73u, 0x74u,
|
||||||
|
0x63u, 0x61u, 0x72u, 0x64u, 0x11u, 0x50u, 0x61u, 0x70u, 0x65u, 0x72u, 0x4du, 0x61u, 0x78u, 0x3du, 0x6cu, 0x65u,
|
||||||
|
0x67u, 0x61u, 0x6cu, 0x2du, 0x41u, 0x34u, 0x29u, 0x55u, 0x55u, 0x49u, 0x44u, 0x3du, 0x65u, 0x33u, 0x32u, 0x34u,
|
||||||
|
0x38u, 0x30u, 0x30u, 0x30u, 0x2du, 0x38u, 0x30u, 0x63u, 0x65u, 0x2du, 0x31u, 0x31u, 0x64u, 0x62u, 0x2du, 0x38u,
|
||||||
|
0x30u, 0x30u, 0x30u, 0x2du, 0x33u, 0x63u, 0x32u, 0x61u, 0x66u, 0x34u, 0x61u, 0x61u, 0x63u, 0x30u, 0x61u, 0x34u,
|
||||||
|
0x0cu, 0x70u, 0x72u, 0x69u, 0x6eu, 0x74u, 0x5fu, 0x77u, 0x66u, 0x64u, 0x73u, 0x3du, 0x54u, 0x14u, 0x6du, 0x6fu,
|
||||||
|
0x70u, 0x72u, 0x69u, 0x61u, 0x2du, 0x63u, 0x65u, 0x72u, 0x74u, 0x69u, 0x66u, 0x69u, 0x65u, 0x64u, 0x3du, 0x31u,
|
||||||
|
0x2eu, 0x33u, 0x0fu, 0x42u, 0x52u, 0x57u, 0x31u, 0x30u, 0x35u, 0x42u, 0x41u, 0x44u, 0x34u, 0x41u, 0x31u, 0x35u,
|
||||||
|
0x37u, 0x30u, 0xc0u, 0x16u, 0x00u, 0x01u, 0x80u, 0x01u, 0x00u, 0x00u, 0x00u, 0x78u, 0x00u, 0x04u, 0xc0u, 0xa8u,
|
||||||
|
0x01u, 0xc5u, 0xc2u, 0xa4u, 0x00u, 0x1cu, 0x80u, 0x01u, 0x00u, 0x00u, 0x00u, 0x78u, 0x00u, 0x10u, 0xfeu, 0x80u,
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x12u, 0x5bu, 0xadu, 0xffu, 0xfeu, 0x4au, 0x15u, 0x70u, 0xc0u, 0x27u,
|
||||||
|
0x00u, 0x21u, 0x80u, 0x01u, 0x00u, 0x00u, 0x00u, 0x78u, 0x00u, 0x08u, 0x00u, 0x00u, 0x00u, 0x00u, 0x02u, 0x77u,
|
||||||
|
0xc2u, 0xa4u, 0xc0u, 0x27u, 0x00u, 0x2fu, 0x80u, 0x01u, 0x00u, 0x00u, 0x11u, 0x94u, 0x00u, 0x09u, 0xc0u, 0x27u,
|
||||||
|
0x00u, 0x05u, 0x00u, 0x00u, 0x80u, 0x00u, 0x40u, 0xc2u, 0xa4u, 0x00u, 0x2fu, 0x80u, 0x01u, 0x00u, 0x00u, 0x00u,
|
||||||
|
0x78u, 0x00u, 0x08u, 0xc2u, 0xa4u, 0x00u, 0x04u, 0x40u, 0x00u, 0x00u, 0x08u
|
||||||
|
)
|
||||||
|
|
||||||
|
val packet = DnsPacket.parse(data.asByteArray())
|
||||||
|
assertEquals(QueryResponse.Response.value.toInt(), packet.header.queryResponse)
|
||||||
|
assertEquals(DnsOpcode.StandardQuery.value.toInt(), packet.header.opcode)
|
||||||
|
assertTrue(packet.header.authoritativeAnswer)
|
||||||
|
assertEquals(false, packet.header.truncated)
|
||||||
|
assertEquals(false, packet.header.recursionDesired)
|
||||||
|
assertEquals(false, packet.header.recursionAvailable)
|
||||||
|
assertEquals(false, packet.header.answerAuthenticated)
|
||||||
|
assertEquals(false, packet.header.nonAuthenticatedData)
|
||||||
|
assertEquals(DnsResponseCode.NoError, packet.header.responseCode)
|
||||||
|
assertEquals(0, packet.questions.size)
|
||||||
|
assertEquals(1, packet.answers.size)
|
||||||
|
assertEquals(0, packet.authorities.size)
|
||||||
|
assertEquals(6, packet.additionals.size)
|
||||||
|
|
||||||
|
val firstAnswer = packet.answers[0]
|
||||||
|
assertEquals("_ipp._tcp.local", firstAnswer.name)
|
||||||
|
assertEquals(ResourceRecordType.PTR.value.toInt(), firstAnswer.type)
|
||||||
|
assertEquals(ResourceRecordClass.IN.value.toInt(), firstAnswer.clazz)
|
||||||
|
assertEquals(false, firstAnswer.cacheFlush)
|
||||||
|
assertEquals(4500u, firstAnswer.timeToLive)
|
||||||
|
assertEquals(30, firstAnswer.dataLength)
|
||||||
|
assertEquals("Brother DCP-L3550CDW series._ipp._tcp.local", firstAnswer.getDataReader().readPTRRecord().domainName)
|
||||||
|
|
||||||
|
val firstAdditional = packet.additionals[0]
|
||||||
|
assertEquals("Brother DCP-L3550CDW series._ipp._tcp.local", firstAdditional.name)
|
||||||
|
assertEquals(ResourceRecordType.TXT.value.toInt(), firstAdditional.type)
|
||||||
|
assertEquals(ResourceRecordClass.IN.value.toInt(), firstAdditional.clazz)
|
||||||
|
assertEquals(true, firstAdditional.cacheFlush)
|
||||||
|
assertEquals(4500u, firstAdditional.timeToLive)
|
||||||
|
assertEquals(595, firstAdditional.dataLength)
|
||||||
|
|
||||||
|
val txtRecord = firstAdditional.getDataReader().readTXTRecord()
|
||||||
|
assertContentEquals(arrayOf(
|
||||||
|
"txtvers=1",
|
||||||
|
"qtotal=1",
|
||||||
|
"pdl=application/octet-stream,image/urf,image/jpeg,image/pwg-raster",
|
||||||
|
"rp=ipp/print",
|
||||||
|
"note=",
|
||||||
|
"ty=Brother DCP-L3550CDW series",
|
||||||
|
"product=(Brother DCP-L3550CDW series)",
|
||||||
|
"adminurl=http://BRW105BAD4A1570.local./net/net/airprint.html",
|
||||||
|
"priority=25",
|
||||||
|
"usb_MFG=Brother",
|
||||||
|
"usb_MDL=DCP-L3550CDW series",
|
||||||
|
"usb_CMD=PJL,PCL,PCLXL,URF",
|
||||||
|
"Color=T",
|
||||||
|
"Copies=T",
|
||||||
|
"Duplex=T",
|
||||||
|
"Fax=F",
|
||||||
|
"Scan=T",
|
||||||
|
"PaperCustom=T",
|
||||||
|
"Binary=T",
|
||||||
|
"Transparent=T",
|
||||||
|
"TBCP=F",
|
||||||
|
"URF=SRGB24,W8,CP1,IS4-1,MT1-3-4-5-8-11,OB10,PQ4,RS600,V1.4,DM1",
|
||||||
|
"kind=document,envelope,label,postcard",
|
||||||
|
"PaperMax=legal-A4",
|
||||||
|
"UUID=e3248000-80ce-11db-8000-3c2af4aac0a4",
|
||||||
|
"print_wfds=T",
|
||||||
|
"mopria-certified=1.3"
|
||||||
|
), txtRecord.texts.toTypedArray())
|
||||||
|
|
||||||
|
val aRecord = packet.additionals[1].getDataReader().readARecord()
|
||||||
|
assertEquals(InetAddress.getByName("192.168.1.197"), aRecord.address)
|
||||||
|
|
||||||
|
val aaaaRecord = packet.additionals[2].getDataReader().readAAAARecord()
|
||||||
|
assertEquals(InetAddress.getByName("fe80::125b:adff:fe4a:1570"), aaaaRecord.address)
|
||||||
|
|
||||||
|
val srvRecord = packet.additionals[3].getDataReader().readSRVRecord()
|
||||||
|
assertEquals("BRW105BAD4A1570.local", srvRecord.target)
|
||||||
|
assertEquals(0, srvRecord.weight.toInt())
|
||||||
|
assertEquals(0, srvRecord.priority.toInt())
|
||||||
|
assertEquals(631, srvRecord.port.toInt())
|
||||||
|
|
||||||
|
val nSECRecord = packet.additionals[4].getDataReader().readNSECRecord()
|
||||||
|
assertEquals("Brother DCP-L3550CDW series._ipp._tcp.local", nSECRecord.ownerName)
|
||||||
|
assertEquals(1, nSECRecord.typeBitMaps.size)
|
||||||
|
assertEquals(0, nSECRecord.typeBitMaps[0].first)
|
||||||
|
assertContentEquals(byteArrayOf(0, 0, 128.toByte(), 0, 64), nSECRecord.typeBitMaps[0].second)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ParseSamsungTV`() {
|
||||||
|
val data = loadByteArray("samsung-airplay.hex")
|
||||||
|
val packet = DnsPacket.parse(data)
|
||||||
|
assertEquals(QueryResponse.Response.value.toInt(), packet.header.queryResponse)
|
||||||
|
assertEquals(DnsOpcode.StandardQuery.value.toInt(), packet.header.opcode)
|
||||||
|
assertTrue(packet.header.authoritativeAnswer)
|
||||||
|
assertEquals(false, packet.header.truncated)
|
||||||
|
assertEquals(false, packet.header.recursionDesired)
|
||||||
|
assertEquals(false, packet.header.recursionAvailable)
|
||||||
|
assertEquals(false, packet.header.answerAuthenticated)
|
||||||
|
assertEquals(false, packet.header.nonAuthenticatedData)
|
||||||
|
assertEquals(DnsResponseCode.NoError, packet.header.responseCode)
|
||||||
|
assertEquals(0, packet.questions.size)
|
||||||
|
assertEquals(6, packet.answers.size)
|
||||||
|
assertEquals(0, packet.authorities.size)
|
||||||
|
assertEquals(4, packet.additionals.size)
|
||||||
|
|
||||||
|
assertEquals("9.1.168.192.in-addr.arpa", packet.answers[0].name)
|
||||||
|
assertEquals(ResourceRecordType.PTR.value.toInt(), packet.answers[0].type)
|
||||||
|
assertEquals(ResourceRecordClass.IN.value.toInt(), packet.answers[0].clazz)
|
||||||
|
assertTrue(packet.answers[0].cacheFlush)
|
||||||
|
assertEquals(120u, packet.answers[0].timeToLive)
|
||||||
|
assertEquals(15, packet.answers[0].dataLength)
|
||||||
|
assertEquals("Samsung.local", packet.answers[0].getDataReader().readPTRRecord().domainName)
|
||||||
|
|
||||||
|
val txtRecord = packet.answers[1].getDataReader().readTXTRecord()
|
||||||
|
assertContentEquals(arrayOf(
|
||||||
|
"acl=0",
|
||||||
|
"deviceid=D4:9D:C0:2F:52:16",
|
||||||
|
"features=0x7F8AD0,0x38BCB46",
|
||||||
|
"rsf=0x3",
|
||||||
|
"fv=p20.0.1",
|
||||||
|
"flags=0x244",
|
||||||
|
"model=URU8000",
|
||||||
|
"manufacturer=Samsung",
|
||||||
|
"serialNumber=0EQC3HDM900064X",
|
||||||
|
"protovers=1.1",
|
||||||
|
"srcvers=377.17.24.6",
|
||||||
|
"pi=ED:0C:A5:ED:10:08",
|
||||||
|
"psi=00000000-0000-0000-0000-ED0CA5ED1008",
|
||||||
|
"gid=00000000-0000-0000-0000-ED0CA5ED1008",
|
||||||
|
"gcgl=0",
|
||||||
|
"pk=d25488cbff1334756165cd7229a235475ef591f2595f38ed251d46b8a4d2345d"
|
||||||
|
), txtRecord.texts.toTypedArray())
|
||||||
|
|
||||||
|
val srvRecord = packet.answers[4].getDataReader().readSRVRecord()
|
||||||
|
assertEquals(33482, srvRecord.port.toInt())
|
||||||
|
assertEquals(0, srvRecord.priority.toInt())
|
||||||
|
assertEquals(0, srvRecord.weight.toInt())
|
||||||
|
assertEquals("Samsung.local", srvRecord.target)
|
||||||
|
|
||||||
|
val aRecord = packet.answers[5].getDataReader().readARecord()
|
||||||
|
assertEquals(InetAddress.getByName("192.168.1.9"), aRecord.address)
|
||||||
|
|
||||||
|
val nSECRecord = packet.additionals[0].getDataReader().readNSECRecord()
|
||||||
|
assertEquals("9.1.168.192.in-addr.arpa", nSECRecord.ownerName)
|
||||||
|
assertEquals(1, nSECRecord.typeBitMaps.size)
|
||||||
|
assertEquals(0, nSECRecord.typeBitMaps[0].first)
|
||||||
|
assertContentEquals(byteArrayOf(0, 8), nSECRecord.typeBitMaps[0].second)
|
||||||
|
|
||||||
|
val optRecord = packet.additionals[3].getDataReader().readOPTRecord()
|
||||||
|
assertEquals(1, optRecord.options.size)
|
||||||
|
assertEquals(65001, optRecord.options[0].code.toInt())
|
||||||
|
assertEquals(5, optRecord.options[0].data.size)
|
||||||
|
assertContentEquals(byteArrayOf(0, 0, 116, 206.toByte(), 97), optRecord.options[0].data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `UnicodeTest`() {
|
||||||
|
val data = ubyteArrayOf(
|
||||||
|
0x00u, 0x00u, 0x84u, 0x00u, 0x00u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x00u, 0x01u, 0x15u, 0x41u, 0x69u, 0x64u,
|
||||||
|
0x61u, 0x6Eu, 0xE2u, 0x80u, 0x99u, 0x73u, 0x20u, 0x4Du, 0x61u, 0x63u, 0x42u, 0x6Fu, 0x6Fu, 0x6Bu, 0x20u, 0x50u,
|
||||||
|
0x72u, 0x6Fu, 0x0Fu, 0x5Fu, 0x63u, 0x6Fu, 0x6Du, 0x70u, 0x61u, 0x6Eu, 0x69u, 0x6Fu, 0x6Eu, 0x2Du, 0x6Cu, 0x69u,
|
||||||
|
0x6Eu, 0x6Bu, 0x04u, 0x5Fu, 0x74u, 0x63u, 0x70u, 0x05u, 0x6Cu, 0x6Fu, 0x63u, 0x61u, 0x6Cu, 0x00u, 0x00u, 0x10u,
|
||||||
|
0x80u, 0x01u, 0x00u, 0x00u, 0x11u, 0x94u, 0x00u, 0x5Bu, 0x16u, 0x72u, 0x70u, 0x42u, 0x41u, 0x3Du, 0x30u, 0x33u,
|
||||||
|
0x3Au, 0x43u, 0x32u, 0x3Au, 0x33u, 0x33u, 0x3Au, 0x38u, 0x36u, 0x3Au, 0x33u, 0x43u, 0x3Au, 0x45u, 0x45u, 0x11u,
|
||||||
|
0x72u, 0x70u, 0x41u, 0x44u, 0x3Du, 0x66u, 0x33u, 0x33u, 0x37u, 0x61u, 0x38u, 0x61u, 0x32u, 0x38u, 0x64u, 0x35u,
|
||||||
|
0x31u, 0x0Cu, 0x72u, 0x70u, 0x46u, 0x6Cu, 0x3Du, 0x30u, 0x78u, 0x32u, 0x30u, 0x30u, 0x30u, 0x30u, 0x11u, 0x72u,
|
||||||
|
0x70u, 0x48u, 0x4Eu, 0x3Du, 0x31u, 0x66u, 0x66u, 0x64u, 0x64u, 0x64u, 0x66u, 0x33u, 0x63u, 0x39u, 0x65u, 0x33u,
|
||||||
|
0x07u, 0x72u, 0x70u, 0x4Du, 0x61u, 0x63u, 0x3Du, 0x30u, 0x0Au, 0x72u, 0x70u, 0x56u, 0x72u, 0x3Du, 0x33u, 0x36u,
|
||||||
|
0x30u, 0x2Eu, 0x34u, 0xC0u, 0x0Cu, 0x00u, 0x2Fu, 0x80u, 0x01u, 0x00u, 0x00u, 0x11u, 0x94u, 0x00u, 0x09u, 0xC0u,
|
||||||
|
0x0Cu, 0x00u, 0x05u, 0x00u, 0x00u, 0x80u, 0x00u, 0x40u
|
||||||
|
)
|
||||||
|
|
||||||
|
val packet = DnsPacket.parse(data.asByteArray())
|
||||||
|
assertEquals("Aidan’s MacBook Pro._companion-link._tcp.local", packet.additionals[0].name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@Test
|
||||||
|
fun `TestReadDomainName`() {
|
||||||
|
val data = ubyteArrayOf(
|
||||||
|
0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x04u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x00u, 0x0Bu, 0x5Fu, 0x67u, 0x6Fu,
|
||||||
|
0x6Fu, 0x67u, 0x6Cu, 0x65u, 0x63u, 0x61u, 0x73u, 0x74u, 0x04u, 0x5Fu, 0x74u, 0x63u, 0x70u, 0x05u, 0x6Cu, 0x6Fu,
|
||||||
|
0x63u, 0x61u, 0x6Cu, 0xC0u, 0x0Cu, 0x00u, 0x0Cu, 0x00u, 0x01u, 0x08u, 0x5Fu, 0x61u, 0x69u, 0x72u, 0x70u, 0x6Cu,
|
||||||
|
0x61u, 0x79u, 0xC0u, 0x18u, 0x00u, 0x0Cu, 0x00u, 0x01u, 0x09u, 0x5Fu, 0x66u, 0x61u, 0x73u, 0x74u, 0x63u, 0x61u,
|
||||||
|
0x73u, 0x74u, 0xC0u, 0x18u, 0x00u, 0x0Cu, 0x00u, 0x01u, 0x06u, 0x5Fu, 0x66u, 0x63u, 0x61u, 0x73u, 0x74u, 0xC0u,
|
||||||
|
0x18u, 0x00u, 0x0Cu, 0x00u, 0x01u
|
||||||
|
)
|
||||||
|
|
||||||
|
val packet = DnsPacket.parse(data.asByteArray())
|
||||||
|
println()
|
||||||
|
}*/
|
||||||
|
|
||||||
|
private fun loadByteArray(name: String): ByteArray {
|
||||||
|
javaClass.classLoader.getResourceAsStream(name).use { input ->
|
||||||
|
requireNotNull(input) { "File not found: $name" }
|
||||||
|
val result = ByteArrayOutputStream()
|
||||||
|
val buffer = ByteArray(4096)
|
||||||
|
var length: Int
|
||||||
|
|
||||||
|
while ((input.read(buffer).also { length = it }) > 0) {
|
||||||
|
result.write(buffer, 0, length)
|
||||||
|
}
|
||||||
|
return result.toByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ReserializeDnsPrinter`() {
|
||||||
|
val data = loadByteArray("samsung-airplay.hex")
|
||||||
|
val packet = DnsPacket.parse(data)
|
||||||
|
val writer = DnsWriter()
|
||||||
|
writer.writePacket(
|
||||||
|
header = packet.header,
|
||||||
|
questionCount = packet.questions.size,
|
||||||
|
questionWriter = { _, _ -> },
|
||||||
|
answerCount = packet.answers.size,
|
||||||
|
answerWriter = { w, i ->
|
||||||
|
w.write(packet.answers[i]) { v ->
|
||||||
|
val reader = packet.answers[i].getDataReader()
|
||||||
|
when (i) {
|
||||||
|
0, 2, 3 -> v.write(reader.readPTRRecord())
|
||||||
|
1 -> v.write(reader.readTXTRecord())
|
||||||
|
4 -> v.write(reader.readSRVRecord())
|
||||||
|
5 -> v.write(reader.readARecord())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
authorityCount = packet.authorities.size,
|
||||||
|
authorityWriter = { _, _ -> },
|
||||||
|
additionalsCount = packet.additionals.size,
|
||||||
|
additionalWriter = { w, i ->
|
||||||
|
w.write(packet.additionals[i]) { v ->
|
||||||
|
val reader = packet.additionals[i].getDataReader()
|
||||||
|
when (i) {
|
||||||
|
0, 1, 2 -> v.write(reader.readNSECRecord())
|
||||||
|
3 -> v.write(reader.readOPTRecord())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assertContentEquals(data, writer.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
BIN
app/src/test/resources/samsung-airplay.hex
Normal file
BIN
app/src/test/resources/samsung-airplay.hex
Normal file
Binary file not shown.
@ -7,7 +7,8 @@
|
|||||||
<application>
|
<application>
|
||||||
<receiver android:name=".receivers.InstallReceiver" />
|
<receiver android:name=".receivers.InstallReceiver" />
|
||||||
|
|
||||||
<activity android:name=".activities.MainActivity">
|
<activity android:name=".activities.MainActivity"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '8.2.0' apply false
|
id 'com.android.application' version '8.5.2' apply false
|
||||||
id 'com.android.library' version '8.2.0' apply false
|
id 'com.android.library' version '8.5.2' apply false
|
||||||
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
|
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
|
||||||
id 'com.google.protobuf' version '0.9.4' apply false
|
id 'com.google.protobuf' version '0.9.4' apply false
|
||||||
id 'com.google.devtools.ksp' version '1.9.0-1.0.13' apply false
|
id 'com.google.devtools.ksp' version '1.9.0-1.0.13' apply false
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 4268917697b975e92dc74e45b4018042ef35c509
|
Subproject commit c8992e6a0ef462d11dfaf716ebe1caf46c926611
|
@ -1 +1 @@
|
|||||||
Subproject commit cedbb52d33a87bbff7b3e713347700138b715b69
|
Subproject commit a7063a300c40dd2310325716f2300ac9259f47aa
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
#Fri Nov 11 13:25:09 CET 2022
|
#Fri Nov 11 13:25:09 CET 2022
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
@ -3,7 +3,6 @@ pluginManagement {
|
|||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
jcenter()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
@ -11,7 +10,6 @@ dependencyResolutionManagement {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
jcenter()
|
|
||||||
maven { url 'https://jitpack.io' }
|
maven { url 'https://jitpack.io' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user