Various fixes for android to android pairing.

This commit is contained in:
Koen J 2025-05-06 16:54:58 +02:00
commit ac3a8da002
4 changed files with 53 additions and 33 deletions

View file

@ -7,6 +7,9 @@ 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
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
//Syntax sugaring //Syntax sugaring
inline fun <reified T> Any.assume(): T?{ inline fun <reified T> Any.assume(): T?{
@ -51,3 +54,19 @@ fun InetAddress?.toUrlAddress(): String {
} }
} }
} }
fun Long?.sToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(this), ZoneOffset.UTC)
}
fun Long?.msToOffsetDateTimeUTC(): OffsetDateTime {
if (this == null || this < 0)
return OffsetDateTime.MIN
if(this > 4070912400)
return OffsetDateTime.MAX;
return OffsetDateTime.ofInstant(Instant.ofEpochMilli(this), ZoneOffset.UTC)
}

View file

@ -19,6 +19,7 @@ import com.futo.platformplayer.exceptions.ReconstructionException
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.ImportCache import com.futo.platformplayer.models.ImportCache
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.sToOffsetDateTimeUTC
import com.futo.platformplayer.smartMerge import com.futo.platformplayer.smartMerge
import com.futo.platformplayer.states.StateSubscriptionGroups.Companion import com.futo.platformplayer.states.StateSubscriptionGroups.Companion
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
@ -85,7 +86,7 @@ class StatePlaylists {
if(value.isEmpty()) if(value.isEmpty())
return OffsetDateTime.MIN; return OffsetDateTime.MIN;
val tryParse = value.toLongOrNull() ?: 0; val tryParse = value.toLongOrNull() ?: 0;
return OffsetDateTime.ofInstant(Instant.ofEpochSecond(tryParse), ZoneOffset.UTC); return tryParse.sToOffsetDateTimeUTC();
} }
private fun setWatchLaterReorderTime() { private fun setWatchLaterReorderTime() {
val now = OffsetDateTime.now(ZoneOffset.UTC); val now = OffsetDateTime.now(ZoneOffset.UTC);

View file

@ -25,6 +25,7 @@ import com.futo.platformplayer.models.HistoryVideo
import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.noise.protocol.DHState import com.futo.platformplayer.noise.protocol.DHState
import com.futo.platformplayer.noise.protocol.Noise import com.futo.platformplayer.noise.protocol.Noise
import com.futo.platformplayer.sToOffsetDateTimeUTC
import com.futo.platformplayer.smartMerge import com.futo.platformplayer.smartMerge
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStringMapStorage import com.futo.platformplayer.stores.StringStringMapStorage
@ -731,7 +732,7 @@ class StateSync {
} }
for(removal in pack.groupRemovals) { for(removal in pack.groupRemovals) {
val creation = StateSubscriptionGroups.instance.getSubscriptionGroup(removal.key); val creation = StateSubscriptionGroups.instance.getSubscriptionGroup(removal.key);
val removalTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(removal.value, 0), ZoneOffset.UTC); val removalTime = removal.value.sToOffsetDateTimeUTC();
if(creation != null && creation.creationTime < removalTime) if(creation != null && creation.creationTime < removalTime)
StateSubscriptionGroups.instance.deleteSubscriptionGroup(removal.key, false); StateSubscriptionGroups.instance.deleteSubscriptionGroup(removal.key, false);
} }
@ -759,7 +760,7 @@ class StateSync {
} }
for(removal in pack.playlistRemovals) { for(removal in pack.playlistRemovals) {
val creation = StatePlaylists.instance.getPlaylist(removal.key); val creation = StatePlaylists.instance.getPlaylist(removal.key);
val removalTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(removal.value, 0), ZoneOffset.UTC); val removalTime = removal.value.sToOffsetDateTimeUTC();
if(creation != null && creation.dateCreation < removalTime) if(creation != null && creation.dateCreation < removalTime)
StatePlaylists.instance.removePlaylist(creation, false); StatePlaylists.instance.removePlaylist(creation, false);
@ -777,7 +778,7 @@ class StateSync {
val allExisting = StatePlaylists.instance.getWatchLater(); val allExisting = StatePlaylists.instance.getWatchLater();
for(video in pack.videos) { for(video in pack.videos) {
val existing = allExisting.firstOrNull { it.url == video.url }; val existing = allExisting.firstOrNull { it.url == video.url };
val time = if(pack.videoAdds != null && pack.videoAdds.containsKey(video.url)) OffsetDateTime.ofInstant(Instant.ofEpochSecond(pack.videoAdds[video.url] ?: 0), ZoneOffset.UTC) else OffsetDateTime.MIN; val time = if(pack.videoAdds.containsKey(video.url)) (pack.videoAdds[video.url] ?: 0).sToOffsetDateTimeUTC() else OffsetDateTime.MIN;
if(existing == null) { if(existing == null) {
StatePlaylists.instance.addToWatchLater(video, false); StatePlaylists.instance.addToWatchLater(video, false);
@ -788,12 +789,12 @@ class StateSync {
for(removal in pack.videoRemovals) { for(removal in pack.videoRemovals) {
val watchLater = allExisting.firstOrNull { it.url == removal.key } ?: continue; val watchLater = allExisting.firstOrNull { it.url == removal.key } ?: continue;
val creation = StatePlaylists.instance.getWatchLaterRemovalTime(watchLater.url) ?: OffsetDateTime.MIN; val creation = StatePlaylists.instance.getWatchLaterRemovalTime(watchLater.url) ?: OffsetDateTime.MIN;
val removalTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(removal.value), ZoneOffset.UTC); val removalTime = removal.value.sToOffsetDateTimeUTC()
if(creation < removalTime) if(creation < removalTime)
StatePlaylists.instance.removeFromWatchLater(watchLater, false, removalTime); StatePlaylists.instance.removeFromWatchLater(watchLater, false, removalTime);
} }
val packReorderTime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(pack.reorderTime), ZoneOffset.UTC); val packReorderTime = pack.reorderTime.sToOffsetDateTimeUTC()
val localReorderTime = StatePlaylists.instance.getWatchLaterLastReorderTime(); val localReorderTime = StatePlaylists.instance.getWatchLaterLastReorderTime();
if(localReorderTime < packReorderTime && pack.ordering != null) { if(localReorderTime < packReorderTime && pack.ordering != null) {
StatePlaylists.instance.updateWatchLaterOrdering(smartMerge(pack.ordering!!, StatePlaylists.instance.getWatchLaterOrdering()), true); StatePlaylists.instance.updateWatchLaterOrdering(smartMerge(pack.ordering!!, StatePlaylists.instance.getWatchLaterOrdering()), true);
@ -830,22 +831,15 @@ class StateSync {
} }
} }
private fun onAuthorized(remotePublicKey: String) { private fun createNewSyncSession(rpk: String, remoteDeviceName: String?): SyncSession {
synchronized(_remotePendingStatusUpdate) { val remotePublicKey = rpk.base64ToByteArray().toBase64()
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(true, "Authorized")
}
}
private fun onUnuthorized(remotePublicKey: String) {
synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Unauthorized")
}
}
private fun createNewSyncSession(remotePublicKey: String, remoteDeviceName: String?): SyncSession {
return SyncSession( return SyncSession(
remotePublicKey, remotePublicKey,
onAuthorized = { it, isNewlyAuthorized, isNewSession -> onAuthorized = { it, isNewlyAuthorized, isNewSession ->
synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(true, "Authorized")
}
if (!isNewSession) { if (!isNewSession) {
return@SyncSession return@SyncSession
} }
@ -857,7 +851,6 @@ class StateSync {
} }
Logger.i(TAG, "$remotePublicKey authorized (name: ${it.displayName})") Logger.i(TAG, "$remotePublicKey authorized (name: ${it.displayName})")
onAuthorized(remotePublicKey)
_authorizedDevices.addDistinct(remotePublicKey) _authorizedDevices.addDistinct(remotePublicKey)
_authorizedDevices.save() _authorizedDevices.save()
deviceUpdatedOrAdded.emit(it.remotePublicKey, it) deviceUpdatedOrAdded.emit(it.remotePublicKey, it)
@ -865,10 +858,12 @@ class StateSync {
checkForSync(it); checkForSync(it);
}, },
onUnauthorized = { onUnauthorized = {
unauthorize(remotePublicKey) synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate.remove(remotePublicKey)?.invoke(false, "Unauthorized")
}
unauthorize(remotePublicKey)
Logger.i(TAG, "$remotePublicKey unauthorized (name: ${it.displayName})") Logger.i(TAG, "$remotePublicKey unauthorized (name: ${it.displayName})")
onUnuthorized(remotePublicKey)
synchronized(_sessions) { synchronized(_sessions) {
it.close() it.close()
@ -1117,7 +1112,7 @@ class StateSync {
runBlocking { runBlocking {
if (onStatusUpdate != null) { if (onStatusUpdate != null) {
synchronized(_remotePendingStatusUpdate) { synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate[deviceInfo.publicKey] = onStatusUpdate _remotePendingStatusUpdate[deviceInfo.publicKey.base64ToByteArray().toBase64()] = onStatusUpdate
} }
} }
relaySession.startRelayedChannel(deviceInfo.publicKey, APP_ID, deviceInfo.pairingCode) relaySession.startRelayedChannel(deviceInfo.publicKey, APP_ID, deviceInfo.pairingCode)
@ -1136,7 +1131,7 @@ class StateSync {
val session = createSocketSession(socket, false) val session = createSocketSession(socket, false)
if (onStatusUpdate != null) { if (onStatusUpdate != null) {
synchronized(_remotePendingStatusUpdate) { synchronized(_remotePendingStatusUpdate) {
_remotePendingStatusUpdate[publicKey] = onStatusUpdate _remotePendingStatusUpdate[publicKey.base64ToByteArray().toBase64()] = onStatusUpdate
} }
} }

View file

@ -11,6 +11,8 @@ import com.futo.platformplayer.noise.protocol.DHState
import com.futo.platformplayer.noise.protocol.HandshakeState import com.futo.platformplayer.noise.protocol.HandshakeState
import com.futo.platformplayer.states.StateSync import com.futo.platformplayer.states.StateSync
import com.futo.platformplayer.sync.internal.ChannelRelayed.Companion import com.futo.platformplayer.sync.internal.ChannelRelayed.Companion
import com.futo.polycentric.core.base64ToByteArray
import com.futo.polycentric.core.toBase64
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@ -169,7 +171,7 @@ class SyncSocketSession {
var totalBytesReceived: Int = 0 var totalBytesReceived: Int = 0
while (totalBytesReceived < size) { while (totalBytesReceived < size) {
val bytesReceived = _inputStream.read(buffer, offset + totalBytesReceived, size - totalBytesReceived) val bytesReceived = _inputStream.read(buffer, offset + totalBytesReceived, size - totalBytesReceived)
if (bytesReceived == 0) if (bytesReceived <= 0)
throw Exception("Socket disconnected") throw Exception("Socket disconnected")
totalBytesReceived += bytesReceived totalBytesReceived += bytesReceived
} }
@ -291,7 +293,7 @@ class SyncSocketSession {
_cipherStatePair = initiator.split() _cipherStatePair = initiator.split()
val remoteKeyBytes = ByteArray(initiator.remotePublicKey.publicKeyLength) val remoteKeyBytes = ByteArray(initiator.remotePublicKey.publicKeyLength)
initiator.remotePublicKey.getPublicKey(remoteKeyBytes, 0) initiator.remotePublicKey.getPublicKey(remoteKeyBytes, 0)
_remotePublicKey = Base64.getEncoder().encodeToString(remoteKeyBytes) _remotePublicKey = Base64.getEncoder().encodeToString(remoteKeyBytes).base64ToByteArray().toBase64()
} }
private fun handshakeAsResponder(): Boolean { private fun handshakeAsResponder(): Boolean {
@ -345,7 +347,7 @@ class SyncSocketSession {
_outputStream.write(responseBuffer, 0, 4 + responseLength) _outputStream.write(responseBuffer, 0, 4 + responseLength)
_cipherStatePair = responder.split() _cipherStatePair = responder.split()
_remotePublicKey = remotePublicKey _remotePublicKey = remotePublicKey.base64ToByteArray().toBase64()
return true return true
} }
@ -440,7 +442,7 @@ class SyncSocketSession {
ByteBuffer.wrap(_sendBufferEncrypted, 0, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(len) ByteBuffer.wrap(_sendBufferEncrypted, 0, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(len)
_outputStream.write(_sendBufferEncrypted, 0, 4 + len) _outputStream.write(_sendBufferEncrypted, 0, 4 + len)
} }
//Logger.v(TAG, "_outputStream.write (opcode: ${opcode}, subOpcode: ${subOpcode}, processedData.remaining(): ${processedData.remaining()}, sendDuration: ${sendDuration})") Logger.v(TAG, "_outputStream.write (opcode: ${opcode}, subOpcode: ${subOpcode}, processedData.remaining(): ${processedData.remaining()}, sendDuration: ${sendDuration})")
} }
} }
} }
@ -840,11 +842,14 @@ class SyncSocketSession {
if (!isGzipSupported) if (!isGzipSupported)
throw Exception("Failed to handle packet, gzip is not supported for this opcode (opcode = ${opcode}, subOpcode = ${subOpcode}, data.length = ${data.remaining()}).") throw Exception("Failed to handle packet, gzip is not supported for this opcode (opcode = ${opcode}, subOpcode = ${subOpcode}, data.length = ${data.remaining()}).")
val compressedStream = ByteArrayInputStream(data.array(), data.position(), data.remaining()); val compressedStream = ByteArrayInputStream(data.array(), data.position(), data.remaining())
var outputStream = ByteArrayOutputStream(); val outputStream = ByteArrayOutputStream()
GZIPInputStream(compressedStream).use { gzipStream -> GZIPInputStream(compressedStream).use { gzipStream ->
gzipStream.copyToOutputStream(outputStream); val buffer = ByteArray(8192) // 8KB buffer
gzipStream.close(); var bytesRead: Int
while (gzipStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
} }
data = ByteBuffer.wrap(outputStream.toByteArray()) data = ByteBuffer.wrap(outputStream.toByteArray())
} }
@ -933,7 +938,7 @@ class SyncSocketSession {
throw Exception("After sync stream end, the stream must be complete") throw Exception("After sync stream end, the stream must be complete")
} }
handlePacket(syncStream.opcode, syncStream.subOpcode, syncStream.getBytes().let { ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN) }, contentEncoding, sourceChannel) handlePacket(syncStream.opcode, syncStream.subOpcode, syncStream.getBytes().let { ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN) }, syncStream.contentEncoding, sourceChannel)
} }
} }
Opcode.DATA.value -> { Opcode.DATA.value -> {