diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ba3fcc35..ed7a2988 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -61,6 +61,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
index f9dd9185..86d73e28 100644
--- a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
+++ b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
@@ -1,5 +1,6 @@
package com.futo.platformplayer
+import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
@@ -337,6 +338,10 @@ class UIDialogs {
} else {
val dialog = ConnectCastingDialog(context);
registerDialogOpened(dialog);
+ val c = context
+ if (c is Activity) {
+ dialog.setOwnerActivity(c);
+ }
dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show();
}
diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
index a385170a..84dbb104 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
@@ -7,7 +7,6 @@ import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
-import android.preference.PreferenceManager
import android.util.Log
import android.util.TypedValue
import android.view.View
@@ -25,11 +24,9 @@ import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.*
-import com.futo.platformplayer.api.media.PlatformID
-import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event1
-import com.futo.platformplayer.constructs.Event3
+import com.futo.platformplayer.dialogs.ConnectCastingDialog
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.fragment.mainactivity.main.*
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
@@ -45,6 +42,7 @@ import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.SubscriptionStorage
import com.futo.platformplayer.stores.v2.ManagedStore
import com.google.gson.JsonParser
+import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
@@ -124,6 +122,24 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
private var _isVisible = true;
private var _wasStopped = false;
+ private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
+ scanResult?.let {
+ val content = it.contents
+ if (content == null) {
+ UIDialogs.toast(this, getString(R.string.failed_to_scan_qr_code))
+ return@let
+ }
+
+ try {
+ handleUrlAll(content)
+ } catch (e: Throwable) {
+ Logger.i(TAG, "Failed to handle URL.", e)
+ UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
+ }
+ }
+ }
+
constructor() : super() {
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
val writer = StringWriter();
@@ -409,6 +425,23 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work");
}*/
+ fun showUrlQrCodeScanner() {
+ try {
+ val integrator = IntentIntegrator(this)
+ integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
+ integrator.setPrompt(getString(R.string.scan_a_qr_code))
+ integrator.setOrientationLocked(true);
+ integrator.setCameraId(0)
+ integrator.setBeepEnabled(false)
+ integrator.setBarcodeImageEnabled(true)
+ integrator.captureActivity = QRCaptureActivity::class.java
+ _urlQrCodeResultLauncher.launch(integrator.createScanIntent())
+ } catch (e: Throwable) {
+ Logger.i(TAG, "Failed to handle show QR scanner.", e)
+ UIDialogs.toast(this, "Failed to show QR scanner: ${e.message}")
+ }
+ }
+
override fun onResume() {
super.onResume();
Logger.v(TAG, "onResume")
@@ -496,76 +529,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
try {
if (targetData != null) {
- when(intent.scheme) {
- "grayjay" -> {
- if(targetData.startsWith("grayjay://license/")) {
- if(StatePayment.instance.setPaymentLicenseUrl(targetData))
- {
- UIDialogs.showDialogOk(this, R.drawable.ic_check, getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
-
- if(fragCurrent is BuyFragment)
- closeSegment(fragCurrent);
- }
- else
- UIDialogs.toast(getString(R.string.invalid_license_format));
-
- }
- else if(targetData.startsWith("grayjay://plugin/")) {
- val intent = Intent(this, AddSourceActivity::class.java).apply {
- data = Uri.parse(targetData.substring("grayjay://plugin/".length));
- };
- startActivity(intent);
- }
- else if(targetData.startsWith("grayjay://video/")) {
- val videoUrl = targetData.substring("grayjay://video/".length);
- navigate(_fragVideoDetail, videoUrl);
- }
- else if(targetData.startsWith("grayjay://channel/")) {
- val channelUrl = targetData.substring("grayjay://channel/".length);
- navigate(_fragMainChannel, channelUrl);
- }
- }
- "content" -> {
- if(!handleContent(targetData, intent.type)) {
- UIDialogs.showSingleButtonDialog(
- this,
- R.drawable.ic_play,
- getString(R.string.unknown_content_format) + " [${targetData}]",
- "Ok",
- { });
- }
- }
- "file" -> {
- if(!handleFile(targetData)) {
- UIDialogs.showSingleButtonDialog(
- this,
- R.drawable.ic_play,
- getString(R.string.unknown_file_format) + " [${targetData}]",
- "Ok",
- { });
- }
- }
- "polycentric" -> {
- if(!handlePolycentric(targetData)) {
- UIDialogs.showSingleButtonDialog(
- this,
- R.drawable.ic_play,
- getString(R.string.unknown_polycentric_format) + " [${targetData}]",
- "Ok",
- { });
- }
- }
- else -> {
- if (!handleUrl(targetData)) {
- UIDialogs.showSingleButtonDialog(
- this,
- R.drawable.ic_play,
- getString(R.string.unknown_url_format) + " [${targetData}]",
- "Ok",
- { });
- }
- }
- }
+ handleUrlAll(targetData)
}
}
catch(ex: Throwable) {
@@ -573,6 +537,90 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}
}
+ fun handleUrlAll(url: String) {
+ val uri = Uri.parse(url)
+ when (uri.scheme) {
+ "grayjay" -> {
+ if(url.startsWith("grayjay://license/")) {
+ if(StatePayment.instance.setPaymentLicenseUrl(url))
+ {
+ UIDialogs.showDialogOk(this, R.drawable.ic_check, getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
+
+ if(fragCurrent is BuyFragment)
+ closeSegment(fragCurrent);
+ }
+ else
+ UIDialogs.toast(getString(R.string.invalid_license_format));
+
+ }
+ else if(url.startsWith("grayjay://plugin/")) {
+ val intent = Intent(this, AddSourceActivity::class.java).apply {
+ data = Uri.parse(url.substring("grayjay://plugin/".length));
+ };
+ startActivity(intent);
+ }
+ else if(url.startsWith("grayjay://video/")) {
+ val videoUrl = url.substring("grayjay://video/".length);
+ navigate(_fragVideoDetail, videoUrl);
+ }
+ else if(url.startsWith("grayjay://channel/")) {
+ val channelUrl = url.substring("grayjay://channel/".length);
+ navigate(_fragMainChannel, channelUrl);
+ }
+ }
+ "content" -> {
+ if(!handleContent(url, intent.type)) {
+ UIDialogs.showSingleButtonDialog(
+ this,
+ R.drawable.ic_play,
+ getString(R.string.unknown_content_format) + " [${url}]",
+ "Ok",
+ { });
+ }
+ }
+ "file" -> {
+ if(!handleFile(url)) {
+ UIDialogs.showSingleButtonDialog(
+ this,
+ R.drawable.ic_play,
+ getString(R.string.unknown_file_format) + " [${url}]",
+ "Ok",
+ { });
+ }
+ }
+ "polycentric" -> {
+ if(!handlePolycentric(url)) {
+ UIDialogs.showSingleButtonDialog(
+ this,
+ R.drawable.ic_play,
+ getString(R.string.unknown_polycentric_format) + " [${url}]",
+ "Ok",
+ { });
+ }
+ }
+ "fcast" -> {
+ if(!handleFCast(url)) {
+ UIDialogs.showSingleButtonDialog(
+ this,
+ R.drawable.ic_cast,
+ "Unknown FCast format [${url}]",
+ "Ok",
+ { });
+ }
+ }
+ else -> {
+ if (!handleUrl(url)) {
+ UIDialogs.showSingleButtonDialog(
+ this,
+ R.drawable.ic_play,
+ getString(R.string.unknown_url_format) + " [${url}]",
+ "Ok",
+ { });
+ }
+ }
+ }
+ }
+
fun handleUrl(url: String): Boolean {
Logger.i(TAG, "handleUrl(url=$url)")
@@ -719,6 +767,20 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
startActivity(Intent(this, PolycentricImportProfileActivity::class.java).apply { putExtra("url", url) })
return true;
}
+
+ fun handleFCast(url: String): Boolean {
+ Logger.i(TAG, "handleFCast");
+
+ try {
+ StateCasting.instance.handleUrl(this, url)
+ return true;
+ } catch (e: Throwable) {
+ Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
+ }
+
+ return false
+ }
+
private fun readSharedContent(contentPath: String): ByteArray {
return contentResolver.openInputStream(Uri.parse(contentPath))?.use {
return it.readBytes();
diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt
index 399c3817..f59b55ad 100644
--- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt
+++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt
@@ -2,8 +2,11 @@ package com.futo.platformplayer.casting
import android.content.ContentResolver
import android.content.Context
+import android.net.Uri
import android.os.Looper
+import android.util.Base64
import com.futo.platformplayer.*
+import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.http.server.ManagedHttpServer
import com.futo.platformplayer.api.http.server.handlers.*
@@ -27,6 +30,9 @@ import javax.jmdns.ServiceListener
import kotlin.collections.HashMap
import com.futo.platformplayer.stores.CastingDeviceInfoStorage
import com.futo.platformplayer.stores.FragmentedStorage
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
import javax.jmdns.ServiceTypeListener
class StateCasting {
@@ -147,6 +153,32 @@ class StateCasting {
}
}
+ fun handleUrl(context: Context, url: String) {
+ val uri = Uri.parse(url)
+ if (uri.scheme != "fcast") {
+ throw Exception("Expected scheme to be FCast")
+ }
+
+ val type = uri.host
+ if (type != "r") {
+ throw Exception("Expected type r")
+ }
+
+ val connectionInfo = uri.pathSegments[0]
+ val json = Base64.decode(connectionInfo, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP).toString(Charsets.UTF_8)
+ val networkConfig = Json.decodeFromString(json)
+ val tcpService = networkConfig.services.first { v -> v.type == 0 }
+
+ addRememberedDevice(CastingDeviceInfo(
+ name = networkConfig.name,
+ type = CastProtocolType.FCAST,
+ addresses = networkConfig.addresses.toTypedArray(),
+ port = tcpService.port
+ ))
+
+ UIDialogs.toast(context,"FCast device '${networkConfig.name}' added")
+ }
+
fun onStop() {
val ad = activeDevice ?: return;
Logger.i(TAG, "Stopping active device because of onStop.");
@@ -1167,6 +1199,19 @@ class StateCasting {
}
}
+ @Serializable
+ private data class FCastNetworkConfig(
+ val name: String,
+ val addresses: List,
+ val services: List
+ )
+
+ @Serializable
+ private data class FCastService(
+ val port: Int,
+ val type: Int
+ )
+
companion object {
val instance: StateCasting = StateCasting();
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt
index dca38091..8f13545c 100644
--- a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt
@@ -1,24 +1,33 @@
package com.futo.platformplayer.dialogs
+import android.app.Activity
import android.app.AlertDialog
import android.content.Context
+import android.content.Intent
import android.graphics.drawable.Animatable
+import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
+import com.futo.platformplayer.activities.AddSourceActivity
+import com.futo.platformplayer.activities.MainActivity
+import com.futo.platformplayer.activities.QRCaptureActivity
import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.CastingDevice
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.views.adapters.DeviceAdapter
+import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -28,6 +37,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
private lateinit var _imageLoader: ImageView;
private lateinit var _buttonClose: Button;
private lateinit var _buttonAdd: Button;
+ private lateinit var _buttonScanQR: Button;
private lateinit var _textNoDevicesFound: TextView;
private lateinit var _textNoDevicesRemembered: TextView;
private lateinit var _recyclerDevices: RecyclerView;
@@ -44,6 +54,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
_imageLoader = findViewById(R.id.image_loader);
_buttonClose = findViewById(R.id.button_close);
_buttonAdd = findViewById(R.id.button_add);
+ _buttonScanQR = findViewById(R.id.button_scan_qr);
_recyclerDevices = findViewById(R.id.recycler_devices);
_recyclerRememberedDevices = findViewById(R.id.recycler_remembered_devices);
_textNoDevicesFound = findViewById(R.id.text_no_devices_found);
@@ -77,6 +88,17 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
UIDialogs.showCastingAddDialog(context);
dismiss();
};
+
+ val c = ownerActivity
+ if (c is MainActivity) {
+ _buttonScanQR.visibility = View.VISIBLE
+ _buttonScanQR.setOnClickListener {
+ c.showUrlQrCodeScanner()
+ dismiss()
+ };
+ } else {
+ _buttonScanQR.visibility = View.GONE
+ }
}
override fun show() {
diff --git a/app/src/main/res/layout/dialog_casting_connect.xml b/app/src/main/res/layout/dialog_casting_connect.xml
index ecadd61f..5cb43c35 100644
--- a/app/src/main/res/layout/dialog_casting_connect.xml
+++ b/app/src/main/res/layout/dialog_casting_connect.xml
@@ -89,18 +89,32 @@
android:textColor="@color/white"
android:fontFamily="@font/inter_regular" />
-
+