Compare commits

...

4 Commits

Author SHA1 Message Date
Kelvin K 1755d03a6b Fcast clearer connection/reconnection overlay, disable ipv6 by default 2025-04-09 00:56:49 +02:00
Kelvin K 869b1fc15e Fix pager for landscape 2025-04-08 00:34:52 +02:00
Kelvin K ce2a2f8582 submods 2025-04-07 23:32:57 +02:00
Kelvin K 7b355139fb Subscription persistence fixes, home toggle fixes, subs exchange gzip, etc 2025-04-07 23:31:00 +02:00
24 changed files with 266 additions and 98 deletions
@@ -216,9 +216,14 @@ private fun ByteArray.toInetAddress(): InetAddress {
return InetAddress.getByAddress(this);
}
fun getConnectedSocket(addresses: List<InetAddress>, port: Int): Socket? {
fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket? {
val timeout = 2000
val addresses = if(!Settings.instance.casting.allowIpv6) attemptAddresses.filterIsInstance<Inet4Address>() else attemptAddresses;
if(addresses.isEmpty())
throw IllegalStateException("No valid addresses found (ipv6: ${(if(Settings.instance.casting.allowIpv6) "enabled" else "disabled")})");
if (addresses.isEmpty()) {
return null;
}
@@ -583,10 +583,15 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class)
var keepScreenOn: Boolean = true;
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 1)
@FormField(R.string.always_proxy_requests, FieldForm.TOGGLE, R.string.always_proxy_requests_description, 3)
@Serializable(with = FlexibleBooleanSerializer::class)
var alwaysProxyRequests: Boolean = false;
@FormField(R.string.allow_ipv6, FieldForm.TOGGLE, R.string.allow_ipv6_description, 4)
@Serializable(with = FlexibleBooleanSerializer::class)
var allowIpv6: Boolean = false;
/*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array)
@@ -5,6 +5,7 @@ import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Animatable
import android.net.Uri
import android.text.Layout
import android.text.method.ScrollingMovementMethod
@@ -199,16 +200,21 @@ class UIDialogs {
dialog.show();
}
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
}
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
val builder = AlertDialog.Builder(context);
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
builder.setView(view);
builder.setCancelable(defaultCloseAction > -2);
val dialog = builder.create();
registerDialogOpened(dialog);
view.findViewById<ImageView>(R.id.dialog_icon).apply {
this.setImageResource(icon);
if(animated)
this.drawable.assume<Animatable, Unit> { it.start() };
}
view.findViewById<TextView>(R.id.dialog_text).apply {
this.text = text;
@@ -275,6 +281,7 @@ class UIDialogs {
registerDialogClosed(dialog);
}
dialog.show();
return dialog;
}
fun showGeneralErrorDialog(context: Context, msg: String, ex: Throwable? = null, button: String = "Ok", onOk: (()->Unit)? = null) {
@@ -27,14 +27,18 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.others.PlatformLinkMovementMethod
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.time.OffsetDateTime
import java.util.*
import java.util.concurrent.ThreadLocalRandom
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
private val _allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ";
fun getRandomString(sizeOfRandomString: Int): String {
@@ -279,3 +283,34 @@ fun ByteBuffer.toUtf8String(): String {
get(remainingBytes)
return String(remainingBytes, Charsets.UTF_8)
}
fun ByteArray.toGzip(): ByteArray {
if (this == null || this.isEmpty()) return ByteArray(0)
val gzipTimeStart = OffsetDateTime.now();
val outputStream = ByteArrayOutputStream()
GZIPOutputStream(outputStream).use { gzip ->
gzip.write(this)
}
val result = outputStream.toByteArray();
Logger.i("Utility", "Gzip compression time: ${gzipTimeStart.getNowDiffMiliseconds()}ms");
return result;
}
fun ByteArray.fromGzip(): ByteArray {
if (this == null || this.isEmpty()) return ByteArray(0)
val inputStream = ByteArrayInputStream(this)
val outputStream = ByteArrayOutputStream()
GZIPInputStream(inputStream).use { gzip ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (gzip.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
}
return outputStream.toByteArray()
}
@@ -3,6 +3,7 @@ package com.futo.platformplayer.casting
import android.os.Looper
import android.util.Base64
import android.util.Log
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
@@ -32,6 +33,7 @@ import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.math.BigInteger
import java.net.Inet4Address
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
@@ -1,5 +1,6 @@
package com.futo.platformplayer.casting
import android.app.AlertDialog
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
@@ -9,6 +10,7 @@ import android.util.Log
import android.util.Xml
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient
@@ -239,6 +241,9 @@ class StateCasting {
Logger.i(TAG, "CastingService stopped.")
}
private val _castingDialogLock = Any();
private var _currentDialog: AlertDialog? = null;
@Synchronized
fun connectDevice(device: CastingDevice) {
if (activeDevice == device)
@@ -272,10 +277,39 @@ class StateCasting {
invokeInMainScopeIfRequired {
StateApp.withContext(false) { context ->
context.let {
Logger.i(TAG, "Casting state changed to ${castConnectionState}");
when (castConnectionState) {
CastConnectionState.CONNECTED -> UIDialogs.toast(it, "Connected to device")
CastConnectionState.CONNECTING -> UIDialogs.toast(it, "Connecting to device...")
CastConnectionState.DISCONNECTED -> UIDialogs.toast(it, "Disconnected from device")
CastConnectionState.CONNECTED -> {
Logger.i(TAG, "Casting connected to [${device.name}]");
UIDialogs.appToast("Connected to device")
synchronized(_castingDialogLock) {
if(_currentDialog != null) {
_currentDialog?.hide();
_currentDialog = null;
}
}
}
CastConnectionState.CONNECTING -> {
Logger.i(TAG, "Casting connecting to [${device.name}]");
UIDialogs.toast(it, "Connecting to device...")
synchronized(_castingDialogLock) {
if(_currentDialog == null) {
_currentDialog = UIDialogs.showDialog(context, R.drawable.ic_loader_animated, true, "Connecting to [${device.name}]", "Make sure you are on the same network", null, -2,
UIDialogs.Action("Disconnect", {
device.stop();
}));
}
}
}
CastConnectionState.DISCONNECTED -> {
UIDialogs.toast(it, "Disconnected from device")
synchronized(_castingDialogLock) {
if(_currentDialog != null) {
_currentDialog?.hide();
_currentDialog = null;
}
}
}
}
}
};
@@ -73,11 +73,11 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
};
_rememberedAdapter.onConnect.subscribe { _ ->
dismiss()
UIDialogs.showCastingDialog(context)
//UIDialogs.showCastingDialog(context)
}
_adapter.onConnect.subscribe { _ ->
dismiss()
UIDialogs.showCastingDialog(context)
//UIDialogs.showCastingDialog(context)
}
_recyclerRememberedDevices.adapter = _rememberedAdapter;
_recyclerRememberedDevices.layoutManager = LinearLayoutManager(context);
@@ -197,10 +197,12 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
val firstVisibleItemView = if(firstVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(firstVisibleItemPosition) else null;
val lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition();
val lastVisibleItemView = if(lastVisibleItemPosition != RecyclerView.NO_POSITION) layoutManager.findViewByPosition(lastVisibleItemPosition) else null;
val rows = if(recyclerData.layoutManager is GridLayoutManager) Math.max(1, recyclerData.results.size / recyclerData.layoutManager.spanCount) else 1;
val rowsHeight = (firstVisibleItemView?.height ?: 0) * rows;
if(lastVisibleItemView != null && lastVisibleItemPosition == (recyclerData.results.size - 1)) {
false;
}
else if (firstVisibleItemView != null && height != null && firstVisibleItemView.height * recyclerData.results.size < height) {
else if (firstVisibleItemView != null && height != null && rowsHeight < height) {
false;
} else {
true;
@@ -250,39 +250,53 @@ class HomeFragment : MainFragment() {
layoutParams =
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
fragment._togglePluginsDisabled.clear();
synchronized(_filterLock) {
val buttonsPlugins = (if (_togglesConfig.contains("plugins"))
var buttonsPlugins: List<ToggleBar.Toggle> = listOf()
buttonsPlugins = (if (_togglesConfig.contains("plugins"))
(StatePlatform.instance.getEnabledClients()
.filter { it is JSClient && it.enableInHome }
.map { plugin ->
ToggleBar.Toggle(if(Settings.instance.home.showHomeFiltersPluginNames) plugin.name else "", plugin.icon, !fragment._togglePluginsDisabled.contains(plugin.id), {
if (it) {
ToggleBar.Toggle(if(Settings.instance.home.showHomeFiltersPluginNames) plugin.name else "", plugin.icon, !fragment._togglePluginsDisabled.contains(plugin.id), { view, active ->
var dontSwap = false;
if (active) {
if (fragment._togglePluginsDisabled.contains(plugin.id))
fragment._togglePluginsDisabled.remove(plugin.id);
} else {
if (!fragment._togglePluginsDisabled.contains(plugin.id))
fragment._togglePluginsDisabled.add(plugin.id);
if (!fragment._togglePluginsDisabled.contains(plugin.id)) {
val enabledClients = StatePlatform.instance.getEnabledClients();
val availableAfterDisable = enabledClients.count { !fragment._togglePluginsDisabled.contains(it.id) && it.id != plugin.id };
if(availableAfterDisable > 0)
fragment._togglePluginsDisabled.add(plugin.id);
else {
UIDialogs.appToast("Home needs atleast 1 plugin active");
dontSwap = true;
}
}
}
if(!dontSwap)
reloadForFilters();
else {
view.setToggle(!active);
}
reloadForFilters();
}).withTag("plugins")
})
else listOf())
val buttons = (listOf<ToggleBar.Toggle?>(
(if (_togglesConfig.contains("today"))
ToggleBar.Toggle("Today", fragment._toggleRecent) {
fragment._toggleRecent = it; reloadForFilters()
ToggleBar.Toggle("Today", fragment._toggleRecent) { view, active ->
fragment._toggleRecent = active; reloadForFilters()
}
.withTag("today") else null),
(if (_togglesConfig.contains("watched"))
ToggleBar.Toggle("Unwatched", fragment._toggleWatched) {
fragment._toggleWatched = it; reloadForFilters()
ToggleBar.Toggle("Unwatched", fragment._toggleWatched) { view, active ->
fragment._toggleWatched = active; reloadForFilters()
}
.withTag("watched") else null),
).filterNotNull() + buttonsPlugins)
.sortedBy { _togglesConfig.indexOf(it.tag ?: "") } ?: listOf()
val buttonSettings = ToggleBar.Toggle("", R.drawable.ic_settings, true, {
val buttonSettings = ToggleBar.Toggle("", R.drawable.ic_settings, true, { view, active ->
showOrderOverlay(_overlayContainer,
"Visible home filters",
listOf(
@@ -18,6 +18,7 @@ import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.exceptions.RateLimitException
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment.SubscriptionsFeedView.FeedFilterSettings
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.SearchType
import com.futo.platformplayer.models.SubscriptionGroup
@@ -56,6 +57,9 @@ class SubscriptionsFeedFragment : MainFragment() {
private var _group: SubscriptionGroup? = null;
private var _cachedRecyclerData: FeedView.RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, GridLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null;
private val _filterLock = Object();
private val _filterSettings = FragmentedStorage.get<FeedFilterSettings>("subFeedFilter");
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack);
_view?.onShown();
@@ -184,8 +188,6 @@ class SubscriptionsFeedFragment : MainFragment() {
return Json.encodeToString(this);
}
}
private val _filterLock = Object();
private val _filterSettings = FragmentedStorage.get<FeedFilterSettings>("subFeedFilter");
private var _bypassRateLimit = false;
private val _lastExceptions: List<Throwable>? = null;
@@ -284,13 +286,18 @@ class SubscriptionsFeedFragment : MainFragment() {
fragment.navigate<SubscriptionGroupFragment>(g);
};
synchronized(_filterLock) {
synchronized(fragment._filterLock) {
_subscriptionBar?.setToggles(
SubscriptionBar.Toggle(context.getString(R.string.videos), _filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), it); },
SubscriptionBar.Toggle(context.getString(R.string.posts), _filterSettings.allowContentTypes.contains(ContentType.POST)) { toggleFilterContentType(ContentType.POST, it); },
SubscriptionBar.Toggle(context.getString(R.string.live), _filterSettings.allowLive) { _filterSettings.allowLive = it; _filterSettings.save(); loadResults(false); },
SubscriptionBar.Toggle(context.getString(R.string.planned), _filterSettings.allowPlanned) { _filterSettings.allowPlanned = it; _filterSettings.save(); loadResults(false); },
SubscriptionBar.Toggle(context.getString(R.string.watched), _filterSettings.allowWatched) { _filterSettings.allowWatched = it; _filterSettings.save(); loadResults(false); }
SubscriptionBar.Toggle(context.getString(R.string.videos), fragment._filterSettings.allowContentTypes.contains(ContentType.MEDIA)) { view, active ->
toggleFilterContentTypes(listOf(ContentType.MEDIA, ContentType.NESTED_VIDEO), active); },
SubscriptionBar.Toggle(context.getString(R.string.posts), fragment._filterSettings.allowContentTypes.contains(ContentType.POST)) { view, active ->
toggleFilterContentType(ContentType.POST, active); },
SubscriptionBar.Toggle(context.getString(R.string.live), fragment._filterSettings.allowLive) { view, active ->
fragment._filterSettings.allowLive = active; fragment._filterSettings.save(); loadResults(false); },
SubscriptionBar.Toggle(context.getString(R.string.planned), fragment._filterSettings.allowPlanned) { view, active ->
fragment._filterSettings.allowPlanned = active; fragment._filterSettings.save(); loadResults(false); },
SubscriptionBar.Toggle(context.getString(R.string.watched), fragment._filterSettings.allowWatched) { view, active ->
fragment._filterSettings.allowWatched = active; fragment._filterSettings.save(); loadResults(false); }
);
}
@@ -301,13 +308,13 @@ class SubscriptionsFeedFragment : MainFragment() {
toggleFilterContentType(contentType, isTrue);
}
private fun toggleFilterContentType(contentType: ContentType, isTrue: Boolean) {
synchronized(_filterLock) {
synchronized(fragment._filterLock) {
if(!isTrue) {
_filterSettings.allowContentTypes.remove(contentType);
} else if(!_filterSettings.allowContentTypes.contains(contentType)) {
_filterSettings.allowContentTypes.add(contentType)
fragment._filterSettings.allowContentTypes.remove(contentType);
} else if(!fragment._filterSettings.allowContentTypes.contains(contentType)) {
fragment._filterSettings.allowContentTypes.add(contentType)
}
_filterSettings.save();
fragment._filterSettings.save();
};
if(Settings.instance.subscriptions.fetchOnTabOpen) { //TODO: Do this different, temporary workaround
loadResults(false);
@@ -320,9 +327,9 @@ class SubscriptionsFeedFragment : MainFragment() {
val nowSoon = OffsetDateTime.now().plusMinutes(5);
val filterGroup = subGroup;
return results.filter {
val allowedContentType = _filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
val allowedContentType = fragment._filterSettings.allowContentTypes.contains(if(it.contentType == ContentType.NESTED_VIDEO || it.contentType == ContentType.LOCKED) ContentType.MEDIA else it.contentType);
if(it is IPlatformVideo && it.duration > 0 && !_filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration))
if(it is IPlatformVideo && it.duration > 0 && !fragment._filterSettings.allowWatched && StateHistory.instance.isHistoryWatched(it.url, it.duration))
return@filter false;
//TODO: Check against a sub cache
@@ -331,11 +338,11 @@ class SubscriptionsFeedFragment : MainFragment() {
if(it.datetime?.isAfter(nowSoon) == true) {
if(!_filterSettings.allowPlanned)
if(!fragment._filterSettings.allowPlanned)
return@filter false;
}
if(_filterSettings.allowLive) { //If allowLive, always show live
if(fragment._filterSettings.allowLive) { //If allowLive, always show live
if(it is IPlatformVideo && it.isLive)
return@filter true;
}
@@ -15,12 +15,14 @@ import com.futo.platformplayer.api.media.structures.EmptyPager
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
import com.futo.platformplayer.api.media.structures.PlatformContentPager
import com.futo.platformplayer.debug.Stopwatch
import com.futo.platformplayer.engine.exceptions.PluginException
import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
import com.futo.platformplayer.exceptions.ChannelException
import com.futo.platformplayer.findNonRuntimeException
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
import com.futo.platformplayer.getNowDiffMiliseconds
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.Subscription
import com.futo.platformplayer.states.StateApp
@@ -32,6 +34,8 @@ import com.futo.platformplayer.subsexchange.ChannelRequest
import com.futo.platformplayer.subsexchange.ChannelResolve
import com.futo.platformplayer.subsexchange.ExchangeContract
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.OffsetDateTime
import java.util.concurrent.ExecutionException
import java.util.concurrent.ForkJoinPool
@@ -149,42 +153,56 @@ abstract class SubscriptionsTaskFetchAlgorithm(
//Resolve Subscription Exchange
if(contract != null) {
try {
resolveTime = measureTimeMillis {
val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract!!.required.contains(it.task.url) }.map {
ChannelResolve(
it.task.url,
it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) }
)
}.toTypedArray()
val resolve = subsExchangeClient?.resolveContract(
contract!!,
*resolves
);
if (resolve != null) {
resolveCount = resolves.size;
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size}")
for(result in resolve){
val task = providedTasks?.find { it.url == result.channelUrl };
if(task != null) {
taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null));
providedTasks?.remove(task);
fun resolve() {
try {
resolveTime = measureTimeMillis {
val resolves = taskResults.filter { it.pager != null && (it.task.type == ResultCapabilities.TYPE_MIXED || it.task.type == ResultCapabilities.TYPE_VIDEOS) && contract!!.required.contains(it.task.url) }.map {
ChannelResolve(
it.task.url,
it.pager!!.getResults().filter { it is IPlatformVideo }.map { SerializedPlatformVideo.fromVideo(it as IPlatformVideo) }
)
}.toTypedArray()
val resolveRequestStart = OffsetDateTime.now();
val resolve = subsExchangeClient?.resolveContract(
contract!!,
*resolves
);
Logger.i(TAG, "Subscription Exchange contract resolved request in ${resolveRequestStart.getNowDiffMiliseconds()}ms");
if (resolve != null) {
resolveCount = resolves.size;
UIDialogs.appToast("SubsExchange (Res: ${resolves.size}, Prov: ${resolve.size}")
for(result in resolve){
val task = providedTasks?.find { it.url == result.channelUrl };
if(task != null) {
taskResults.add(SubscriptionTaskResult(task, PlatformContentPager(result.content, result.content.size), null));
providedTasks?.remove(task);
}
}
}
if (providedTasks != null) {
for(task in providedTasks!!) {
taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange")));
}
}
}
if (providedTasks != null) {
for(task in providedTasks!!) {
taskResults.add(SubscriptionTaskResult(task, null, IllegalStateException("No data received from exchange")));
}
}
}
Logger.i(TAG, "Subscription Exchange contract resolved in ${resolveTime}ms");
Logger.i(TAG, "Subscription Exchange contract resolved in ${resolveTime}ms");
}
catch(ex: Throwable) {
//TODO: fetch remainder after all?
Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex);
}
}
catch(ex: Throwable) {
//TODO: fetch remainder after all?
Logger.e(TAG, "Failed to resolve Subscription Exchange contract due to: " + ex.message, ex);
}
if(providedTasks?.size ?: 0 == 0)
scope.launch(Dispatchers.IO) {
resolve();
}
else
resolve();
}
}
@@ -1,10 +1,14 @@
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.getNowDiffMiliseconds
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm.Companion.TAG
import com.futo.platformplayer.subsexchange.ChannelRequest
import com.futo.platformplayer.subsexchange.ChannelResolve
import com.futo.platformplayer.subsexchange.ChannelResult
import com.futo.platformplayer.subsexchange.ExchangeContract
import com.futo.platformplayer.subsexchange.ExchangeContractResolve
import com.futo.platformplayer.toGzip
import com.futo.platformplayer.toHumanBytesSize
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.coroutines.Dispatchers
@@ -26,6 +30,7 @@ import java.nio.charset.StandardCharsets
import java.security.KeyPairGenerator
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.RSAPublicKeySpec
import java.time.OffsetDateTime
class SubsExchangeClient(private val server: String, private val privateKey: String, private val contractTimeout: Int = 1000) {
@@ -40,24 +45,27 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
// Endpoint: Contract
fun requestContract(vararg channels: ChannelRequest): ExchangeContract {
val data = post("/api/Channel/Contract", Json.encodeToString(channels), "application/json", contractTimeout)
val data = post("/api/Channel/Contract", Json.encodeToString(channels).toByteArray(Charsets.UTF_8), "application/json", contractTimeout)
return Json.decodeFromString(data)
}
suspend fun requestContractAsync(vararg channels: ChannelRequest): ExchangeContract {
val data = postAsync("/api/Channel/Contract", Json.encodeToString(channels), "application/json")
val data = postAsync("/api/Channel/Contract", Json.encodeToString(channels).toByteArray(Charsets.UTF_8), "application/json")
return Json.decodeFromString(data)
}
// Endpoint: Resolve
fun resolveContract(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
val contractResolve = convertResolves(*resolves)
val result = post("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json")
Logger.v("SubsExchangeClient", "Resolve:" + result);
val contractResolveJson = Serializer.json.encodeToString(contractResolve);
val contractResolveTimeStart = OffsetDateTime.now();
val result = post("/api/Channel/Resolve?contractId=${contract.id}", contractResolveJson.toByteArray(Charsets.UTF_8), "application/json", 0, true)
val contractResolveTime = contractResolveTimeStart.getNowDiffMiliseconds();
Logger.v("SubsExchangeClient", "Subscription Exchange Resolve Request [${contractResolveTime}ms]:" + result);
return Serializer.json.decodeFromString(result)
}
suspend fun resolveContractAsync(contract: ExchangeContract, vararg resolves: ChannelResolve): Array<ChannelResult> {
val contractResolve = convertResolves(*resolves)
val result = postAsync("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve), "application/json")
val result = postAsync("/api/Channel/Resolve?contractId=${contract.id}", Serializer.json.encodeToString(contractResolve).toByteArray(Charsets.UTF_8), "application/json", true)
return Serializer.json.decodeFromString(result)
}
@@ -74,7 +82,7 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
}
// IO methods
private fun post(query: String, body: String, contentType: String, timeout: Int = 0): String {
private fun post(query: String, body: ByteArray, contentType: String, timeout: Int = 0, gzip: Boolean = false): String {
val url = URL("${server.trim('/')}$query")
with(url.openConnection() as HttpURLConnection) {
if(timeout > 0)
@@ -82,7 +90,16 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
requestMethod = "POST"
setRequestProperty("Content-Type", contentType)
doOutput = true
OutputStreamWriter(outputStream, StandardCharsets.UTF_8).use { it.write(body); it.flush() }
if(gzip) {
val gzipData = body.toGzip();
setRequestProperty("Content-Encoding", "gzip");
outputStream.write(gzipData);
Logger.i("SubsExchangeClient", "SubsExchange using gzip (${body.size.toHumanBytesSize()} => ${gzipData.size.toHumanBytesSize()}");
}
else
outputStream.write(body);
val status = responseCode;
Logger.i("SubsExchangeClient", "POST [${url}]: ${status}");
@@ -105,9 +122,9 @@ class SubsExchangeClient(private val server: String, private val privateKey: Str
}
}
}
private suspend fun postAsync(query: String, body: String, contentType: String): String {
private suspend fun postAsync(query: String, body: ByteArray, contentType: String, gzip: Boolean = false): String {
return withContext(Dispatchers.IO) {
post(query, body, contentType)
post(query, body, contentType, 0, gzip)
}
}
@@ -53,7 +53,7 @@ class ToggleBar : LinearLayout {
this.setInfo(button.iconVariable, button.name, button.isActive, button.isButton);
else
this.setInfo(button.name, button.isActive, button.isButton);
this.onClick.subscribe { button.action(it); };
this.onClick.subscribe({ view, enabled -> button.action(view, enabled); });
});
}
}
@@ -62,27 +62,27 @@ class ToggleBar : LinearLayout {
val name: String;
val icon: Int;
val iconVariable: ImageVariable?;
val action: (Boolean)->Unit;
val action: (ToggleTagView, Boolean)->Unit;
val isActive: Boolean;
var isButton: Boolean = false
private set;
var tag: String? = null;
constructor(name: String, icon: ImageVariable?, isActive: Boolean = false, action: (Boolean)->Unit) {
constructor(name: String, icon: ImageVariable?, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
this.name = name;
this.icon = 0;
this.iconVariable = icon;
this.action = action;
this.isActive = isActive;
}
constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) {
constructor(name: String, icon: Int, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
this.name = name;
this.icon = icon;
this.iconVariable = null;
this.action = action;
this.isActive = isActive;
}
constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) {
constructor(name: String, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
this.name = name;
this.icon = 0;
this.iconVariable = null;
@@ -12,8 +12,10 @@ import android.widget.TextView
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.images.GlideHelper
import com.futo.platformplayer.models.ImageVariable
import com.futo.platformplayer.views.ToggleBar
class ToggleTagView : LinearLayout {
private val _root: FrameLayout;
@@ -26,7 +28,7 @@ class ToggleTagView : LinearLayout {
var isButton: Boolean = false
private set;
var onClick = Event1<Boolean>();
var onClick = Event2<ToggleTagView, Boolean>();
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_toggle_tag, this, true);
@@ -36,7 +38,7 @@ class ToggleTagView : LinearLayout {
_root.setOnClickListener {
if(!isButton)
setToggle(!isActive);
onClick.emit(isActive);
onClick.emit(this, isActive);
}
}
@@ -52,6 +54,24 @@ class ToggleTagView : LinearLayout {
}
}
fun setInfo(toggle: ToggleBar.Toggle){
_text = toggle.name;
_textTag.text = toggle.name;
setToggle(toggle.isActive);
if(toggle.iconVariable != null) {
toggle.iconVariable.setImageView(_image, R.drawable.ic_error_pred);
_image.visibility = View.GONE;
}
else if(toggle.icon > 0) {
_image.setImageResource(toggle.icon);
_image.visibility = View.GONE;
}
else
_image.visibility = View.VISIBLE;
_textTag.visibility = if(!toggle.name.isNullOrEmpty()) View.VISIBLE else View.GONE;
this.isButton = isButton;
}
fun setInfo(imageResource: Int, text: String, isActive: Boolean, isButton: Boolean = false) {
_text = text;
_textTag.text = text;
@@ -158,7 +158,7 @@ class SubscriptionBar : LinearLayout {
for(button in buttons) {
_tagsContainer.addView(ToggleTagView(context).apply {
this.setInfo(button.name, button.isActive);
this.onClick.subscribe { button.action(it); };
this.onClick.subscribe({ view, value -> button.action(view, value); });
});
}
}
@@ -166,16 +166,16 @@ class SubscriptionBar : LinearLayout {
class Toggle {
val name: String;
val icon: Int;
val action: (Boolean)->Unit;
val action: (ToggleTagView, Boolean)->Unit;
val isActive: Boolean;
constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) {
constructor(name: String, icon: Int, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
this.name = name;
this.icon = icon;
this.action = action;
this.isActive = isActive;
}
constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) {
constructor(name: String, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
this.name = name;
this.icon = 0;
this.action = action;
+2
View File
@@ -72,6 +72,8 @@
<string name="keep_screen_on_while_casting">Keep screen on while casting</string>
<string name="always_proxy_requests">Always proxy requests</string>
<string name="always_proxy_requests_description">Always proxy requests when casting data through the device.</string>
<string name="allow_ipv6">Allow IPV6</string>
<string name="allow_ipv6_description">If casting over IPV6 is allowed, can cause issues on some networks</string>
<string name="discover">Discover</string>
<string name="find_new_video_sources_to_add">Find new video sources to add</string>
<string name="these_sources_have_been_disabled">These sources have been disabled</string>