This commit is contained in:
Kelvin 2025-06-07 16:44:57 +02:00
commit de39451f67
14 changed files with 110 additions and 53 deletions

View file

@ -241,8 +241,11 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
return null; return null;
} }
val sortedAddresses: List<InetAddress> = addresses
.sortedBy { addr -> addressScore(addr) }
val sockets: ArrayList<Socket> = arrayListOf(); val sockets: ArrayList<Socket> = arrayListOf();
for (i in addresses.indices) { for (i in sortedAddresses.indices) {
sockets.add(Socket()); sockets.add(Socket());
} }
@ -250,7 +253,7 @@ fun getConnectedSocket(attemptAddresses: List<InetAddress>, port: Int): Socket?
var connectedSocket: Socket? = null; var connectedSocket: Socket? = null;
val threads: ArrayList<Thread> = arrayListOf(); val threads: ArrayList<Thread> = arrayListOf();
for (i in 0 until sockets.size) { for (i in 0 until sockets.size) {
val address = addresses[i]; val address = sortedAddresses[i];
val socket = sockets[i]; val socket = sockets[i];
val thread = Thread { val thread = Thread {
try { try {

View file

@ -681,6 +681,11 @@ class Settings : FragmentedStorageFileJson() {
@Serializable(with = FlexibleBooleanSerializer::class) @Serializable(with = FlexibleBooleanSerializer::class)
var allowIpv6: Boolean = true; var allowIpv6: Boolean = true;
@AdvancedField
@FormField(R.string.allow_ipv4, FieldForm.TOGGLE, R.string.allow_ipv4_description, 5)
@Serializable(with = FlexibleBooleanSerializer::class)
var allowLinkLocalIpv4: Boolean = false;
/*TODO: Should we have a different casting quality? /*TODO: Should we have a different casting quality?
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3) @FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
@DropdownFieldOptionsId(R.array.preferred_quality_array) @DropdownFieldOptionsId(R.array.preferred_quality_array)

View file

@ -434,7 +434,7 @@ private fun interfaceScore(nif: NetworkInterface): Int {
} }
} }
private fun addressScore(addr: InetAddress): Int { fun addressScore(addr: InetAddress): Int {
return when (addr) { return when (addr) {
is Inet4Address -> { is Inet4Address -> {
val octets = addr.address.map { it.toInt() and 0xFF } val octets = addr.address.map { it.toInt() and 0xFF }

View file

@ -115,6 +115,7 @@ import java.io.StringWriter
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.util.LinkedList import java.util.LinkedList
import java.util.Queue import java.util.Queue
import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@ -218,6 +219,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
val mainId = UUID.randomUUID().toString().substring(0, 5)
constructor() : super() { constructor() : super() {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy( StrictMode.setVmPolicy(
@ -269,8 +272,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
@UnstableApi @UnstableApi
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Logger.i(TAG, "MainActivity Starting"); Logger.w(TAG, "MainActivity Starting [$mainId]");
StateApp.instance.setGlobalContext(this, lifecycleScope); StateApp.instance.setGlobalContext(this, lifecycleScope, mainId);
StateApp.instance.mainAppStarting(this); StateApp.instance.mainAppStarting(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -671,13 +674,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
override fun onResume() { override fun onResume() {
super.onResume(); super.onResume();
Logger.v(TAG, "onResume") Logger.w(TAG, "onResume [$mainId]")
_isVisible = true; _isVisible = true;
} }
override fun onPause() { override fun onPause() {
super.onPause(); super.onPause();
Logger.v(TAG, "onPause") Logger.w(TAG, "onPause [$mainId]")
_isVisible = false; _isVisible = false;
_qrCodeLoadingDialog?.dismiss() _qrCodeLoadingDialog?.dismiss()
@ -686,7 +689,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
Logger.v(TAG, "_wasStopped = true"); Logger.w(TAG, "onStop [$mainId]");
_wasStopped = true; _wasStopped = true;
} }
@ -1103,8 +1106,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy(); super.onDestroy();
Logger.v(TAG, "onDestroy") Logger.w(TAG, "onDestroy [$mainId]")
StateApp.instance.mainAppDestroyed(this); StateApp.instance.mainAppDestroyed(this, mainId);
} }
inline fun <reified T> isFragmentActive(): Boolean { inline fun <reified T> isFragmentActive(): Boolean {

View file

@ -166,10 +166,11 @@ class StateCasting {
Logger.i(TAG, "CastingService started."); Logger.i(TAG, "CastingService started.");
_nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager _nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
startDiscovering()
} }
@Synchronized @Synchronized
fun startDiscovering() { private fun startDiscovering() {
_nsdManager?.apply { _nsdManager?.apply {
_discoveryListeners.forEach { _discoveryListeners.forEach {
discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value) discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value)
@ -178,7 +179,7 @@ class StateCasting {
} }
@Synchronized @Synchronized
fun stopDiscovering() { private fun stopDiscovering() {
_nsdManager?.apply { _nsdManager?.apply {
_discoveryListeners.forEach { _discoveryListeners.forEach {
try { try {
@ -1220,9 +1221,16 @@ class StateCasting {
private fun getLocalUrl(ad: CastingDevice): String { private fun getLocalUrl(ad: CastingDevice): String {
var address = ad.localAddress!! var address = ad.localAddress!!
if (address.isLinkLocalAddress) { if (Settings.instance.casting.allowLinkLocalIpv4) {
address = findPreferredAddress() ?: address if (address.isLinkLocalAddress && address is Inet6Address) {
Logger.i(TAG, "Selected casting address: $address") address = findPreferredAddress() ?: address
Logger.i(TAG, "Selected casting address: $address")
}
} else {
if (address.isLinkLocalAddress) {
address = findPreferredAddress() ?: address
Logger.i(TAG, "Selected casting address: $address")
}
} }
return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}"; return "http://${address.toUrlAddress().trim('/')}:${_castServer.port}";
} }

View file

@ -103,7 +103,6 @@ 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();
synchronized(StateCasting.instance.devices) { synchronized(StateCasting.instance.devices) {
@ -148,7 +147,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
override fun dismiss() { override fun dismiss() {
super.dismiss() super.dismiss()
(_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)

View file

@ -16,6 +16,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UISlideOverlays import com.futo.platformplayer.UISlideOverlays
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.views.adapters.SubscriptionAdapter import com.futo.platformplayer.views.adapters.SubscriptionAdapter
class CreatorsFragment : MainFragment() { class CreatorsFragment : MainFragment() {
@ -29,6 +31,8 @@ class CreatorsFragment : MainFragment() {
private var _editSearch: EditText? = null; private var _editSearch: EditText? = null;
private var _textMeta: TextView? = null; private var _textMeta: TextView? = null;
private var _buttonClearSearch: ImageButton? = null private var _buttonClearSearch: ImageButton? = null
private var _ordering = FragmentedStorage.get<StringStorage>("creators_ordering")
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_creators, container, false); val view = inflater.inflate(R.layout.fragment_creators, container, false);
@ -44,7 +48,7 @@ class CreatorsFragment : MainFragment() {
_buttonClearSearch?.visibility = View.INVISIBLE; _buttonClearSearch?.visibility = View.INVISIBLE;
} }
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)) { subs -> val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription), _ordering?.value?.toIntOrNull() ?: 5) { subs ->
_textMeta?.let { _textMeta?.let {
it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}"; it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}";
} }
@ -61,6 +65,7 @@ class CreatorsFragment : MainFragment() {
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
adapter.sortBy = pos; adapter.sortBy = pos;
_ordering.setAndSave(pos.toString())
} }
override fun onNothingSelected(parent: AdapterView<*>?) = Unit override fun onNothingSelected(parent: AdapterView<*>?) = Unit
}; };

View file

@ -8,11 +8,14 @@ import android.text.method.LinkMovementMethod
import android.text.style.URLSpan import android.text.style.URLSpan
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.TextView import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.receivers.MediaControlReceiver import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.timestampRegex import com.futo.platformplayer.timestampRegex
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMethod() { class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMethod() {
@ -60,31 +63,39 @@ class PlatformLinkMovementMethod(private val _context: Context) : LinkMovementMe
val dx = event.x - downX val dx = event.x - downX
val dy = event.y - downY val dy = event.y - downY
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(widget, event)) { if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(widget, event)) {
runBlocking { for (link in pressedLinks!!) {
for (link in pressedLinks!!) { Logger.i(TAG) { "Link clicked '${link.url}'." }
Logger.i(TAG) { "Link clicked '${link.url}'." }
if (_context is MainActivity) { val c = _context
if (_context.handleUrl(link.url)) continue if (c is MainActivity) {
if (timestampRegex.matches(link.url)) { c.lifecycleScope.launch(Dispatchers.IO) {
val tokens = link.url.split(':') if (c.handleUrl(link.url)) {
var time_s = -1L return@launch
when (tokens.size) { }
2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong() if (timestampRegex.matches(link.url)) {
3 -> time_s = tokens[0].toLong() * 3600 + val tokens = link.url.split(':')
tokens[1].toLong() * 60 + var time_s = -1L
tokens[2].toLong() when (tokens.size) {
} 2 -> time_s = tokens[0].toLong() * 60 + tokens[1].toLong()
3 -> time_s = tokens[0].toLong() * 3600 +
tokens[1].toLong() * 60 +
tokens[2].toLong()
}
if (time_s != -1L) { if (time_s != -1L) {
withContext(Dispatchers.Main) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000) MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
continue
} }
return@launch
} }
} }
_context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
withContext(Dispatchers.Main) {
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
}
} }
} }
}
pressedLinks = null pressedLinks = null
linkPressed = false linkPressed = false
return true return true

View file

@ -156,6 +156,8 @@ class StateApp {
return thisContext; return thisContext;
} }
private var _mainId: String? = null;
//Files //Files
private var _tempDirectory: File? = null; private var _tempDirectory: File? = null;
private var _cacheDirectory: File? = null; private var _cacheDirectory: File? = null;
@ -295,9 +297,12 @@ class StateApp {
} }
//Lifecycle //Lifecycle
fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null) { fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null, mainId: String? = null) {
_mainId = mainId;
_context = context; _context = context;
_scope = coroutineScope _scope = coroutineScope
Logger.w(TAG, "Scope initialized ${(coroutineScope != null)}\n ${Log.getStackTraceString(Throwable())}")
} }
fun initializeFiles(force: Boolean = false) { fun initializeFiles(force: Boolean = false) {
@ -719,7 +724,9 @@ class StateApp {
migrateStores(context, managedStores, index + 1); migrateStores(context, managedStores, index + 1);
} }
fun mainAppDestroyed(context: Context) { fun mainAppDestroyed(context: Context, mainId: String? = null) {
if (mainId != null && (_mainId != mainId || _mainId == null))
return
Logger.i(TAG, "App ended"); Logger.i(TAG, "App ended");
_receiverBecomingNoisy?.let { _receiverBecomingNoisy?.let {
_receiverBecomingNoisy = null; _receiverBecomingNoisy = null;
@ -743,7 +750,8 @@ class StateApp {
fun dispose(){ fun dispose(){
_context = null; _context = null;
_scope = null; // _scope = null;
Logger.w(TAG, "StateApp disposed: ${Log.getStackTraceString(Throwable())}")
} }
private val _connectivityEvents = object : ConnectivityManager.NetworkCallback() { private val _connectivityEvents = object : ConnectivityManager.NetworkCallback() {

View file

@ -31,10 +31,11 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
updateDataset(); updateDataset();
} }
constructor(inflater: LayoutInflater, confirmationMessage: String, onDatasetChanged: ((List<Subscription>)->Unit)? = null) : super() { constructor(inflater: LayoutInflater, confirmationMessage: String, sortByDefault: Int, onDatasetChanged: ((List<Subscription>)->Unit)? = null) : super() {
_inflater = inflater; _inflater = inflater;
_confirmationMessage = confirmationMessage; _confirmationMessage = confirmationMessage;
_onDatasetChanged = onDatasetChanged; _onDatasetChanged = onDatasetChanged;
sortBy = sortByDefault
StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper()) StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper())
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { updateDataset() } StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { updateDataset() }

View file

@ -8,12 +8,16 @@ import android.text.Spannable
import android.text.style.URLSpan import android.text.style.URLSpan
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.others.PlatformLinkMovementMethod import com.futo.platformplayer.others.PlatformLinkMovementMethod
import com.futo.platformplayer.receivers.MediaControlReceiver import com.futo.platformplayer.receivers.MediaControlReceiver
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.timestampRegex import com.futo.platformplayer.timestampRegex
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView { class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
private var _lastTouchedLinks: Array<URLSpan>? = null private var _lastTouchedLinks: Array<URLSpan>? = null
@ -77,12 +81,14 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
val dx = event.x - downX val dx = event.x - downX
val dy = event.y - downY val dy = event.y - downY
if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(event)) { if (Math.abs(dx) <= touchSlop && Math.abs(dy) <= touchSlop && isTouchInside(event)) {
runBlocking { for (link in _lastTouchedLinks!!) {
for (link in _lastTouchedLinks!!) { Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." }
Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." } val c = context
val c = context if (c is MainActivity) {
if (c is MainActivity) { c.lifecycleScope.launch(Dispatchers.IO) {
if (c.handleUrl(link.url)) continue if (c.handleUrl(link.url)) {
return@launch
}
if (timestampRegex.matches(link.url)) { if (timestampRegex.matches(link.url)) {
val tokens = link.url.split(':') val tokens = link.url.split(':')
var time_s = -1L var time_s = -1L
@ -92,13 +98,21 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView {
tokens[1].toLong() * 60 + tokens[1].toLong() * 60 +
tokens[2].toLong() tokens[2].toLong()
} }
if (time_s != -1L) { if (time_s != -1L) {
MediaControlReceiver.onSeekToReceived.emit(time_s * 1000) withContext(Dispatchers.Main) {
continue MediaControlReceiver.onSeekToReceived.emit(time_s * 1000)
}
return@launch
} }
} }
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
} else { withContext(Dispatchers.Main) {
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
}
}
} else {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))) c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url)))
} }
} }

View file

@ -90,7 +90,6 @@ class ToggleField : TableRow, IField {
val advancedFieldAttr = field.getAnnotation(AdvancedField::class.java) val advancedFieldAttr = field.getAnnotation(AdvancedField::class.java)
if(advancedFieldAttr != null || advanced) { if(advancedFieldAttr != null || advanced) {
Logger.w("ToggleField", "Found cccadvanced field: " + field.name);
isAdvanced = true; isAdvanced = true;
} }

View file

@ -76,6 +76,8 @@
<string name="always_proxy_requests_description">Always proxy requests when casting data through the device.</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">Allow IPV6</string>
<string name="allow_ipv6_description">If casting over IPV6 is allowed, can cause issues on some networks</string> <string name="allow_ipv6_description">If casting over IPV6 is allowed, can cause issues on some networks</string>
<string name="allow_ipv4">Allow Link Local IPV4</string>
<string name="allow_ipv4_description">If casting over IPV4 link local is allowed, can cause issues on some networks</string>
<string name="discover">Discover</string> <string name="discover">Discover</string>
<string name="find_new_video_sources_to_add">Find new video sources to add</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> <string name="these_sources_have_been_disabled">These sources have been disabled</string>

View file

@ -7,7 +7,7 @@
<application> <application>
<receiver android:name=".receivers.InstallReceiver" /> <receiver android:name=".receivers.InstallReceiver" />
<activity android:name=".activities.MainActivity"> <activity android:name=".activities.MainActivity" android:launchMode="singleInstance">
<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" />