This commit is contained in:
Kelvin 2024-11-22 20:35:01 +01:00
commit e989590c08
12 changed files with 299 additions and 167 deletions

View File

@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.text.Layout
import android.text.method.ScrollingMovementMethod import android.text.method.ScrollingMovementMethod
import android.util.TypedValue import android.util.TypedValue
import android.view.Gravity import android.view.Gravity
@ -198,7 +199,6 @@ class UIDialogs {
dialog.show(); 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) {
val builder = AlertDialog.Builder(context); val builder = AlertDialog.Builder(context);
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null); val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
@ -214,18 +214,20 @@ class UIDialogs {
this.text = text; this.text = text;
}; };
view.findViewById<TextView>(R.id.dialog_text_details).apply { view.findViewById<TextView>(R.id.dialog_text_details).apply {
if(textDetails == null) if (textDetails == null)
this.visibility = View.GONE; this.visibility = View.GONE;
else else {
this.text = textDetails; this.text = textDetails;
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
}
}; };
view.findViewById<TextView>(R.id.dialog_text_code).apply { view.findViewById<TextView>(R.id.dialog_text_code).apply {
if(code == null) if (code == null) this.visibility = View.GONE;
this.visibility = View.GONE;
else { else {
this.text = code; this.text = code;
this.movementMethod = ScrollingMovementMethod.getInstance(); this.movementMethod = ScrollingMovementMethod.getInstance();
this.visibility = View.VISIBLE; this.visibility = View.VISIBLE;
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
} }
}; };
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply { view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {

View File

@ -32,6 +32,7 @@ import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.* import java.util.*
import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.ThreadLocalRandom
@ -272,3 +273,9 @@ fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
else else
return newIndex; return newIndex;
} }
fun ByteBuffer.toUtf8String(): String {
val remainingBytes = ByteArray(remaining())
get(remainingBytes)
return String(remainingBytes, Charsets.UTF_8)
}

View File

@ -7,7 +7,6 @@ import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import android.net.wifi.WifiManager
import android.os.Bundle import android.os.Bundle
import android.os.StrictMode import android.os.StrictMode
import android.os.StrictMode.VmPolicy import android.os.StrictMode.VmPolicy
@ -33,6 +32,7 @@ import com.futo.platformplayer.BuildConfig
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
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
@ -81,6 +81,7 @@ import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.states.StateSubscriptions
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import com.futo.platformplayer.stores.SubscriptionStorage import com.futo.platformplayer.stores.SubscriptionStorage
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView import com.futo.platformplayer.views.ToastView
@ -88,11 +89,14 @@ import com.futo.polycentric.core.ApiMethods
import com.google.gson.JsonParser import com.google.gson.JsonParser
import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentIntegrator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.io.PrintWriter import java.io.PrintWriter
@ -102,7 +106,6 @@ import java.util.LinkedList
import java.util.Queue import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
class MainActivity : AppCompatActivity, IWithResultLauncher { class MainActivity : AppCompatActivity, IWithResultLauncher {
//TODO: Move to dimensions //TODO: Move to dimensions
@ -110,7 +113,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
private val HEIGHT_VIDEO_MINIMIZED_DP = 60f; private val HEIGHT_VIDEO_MINIMIZED_DP = 60f;
//Containers //Containers
lateinit var rootView : MotionLayout; lateinit var rootView: MotionLayout;
private lateinit var _overlayContainer: FrameLayout; private lateinit var _overlayContainer: FrameLayout;
private lateinit var _toastView: ToastView; private lateinit var _toastView: ToastView;
@ -167,11 +170,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var _fragVideoDetail: VideoDetailFragment; lateinit var _fragVideoDetail: VideoDetailFragment;
//State //State
private val _queue : Queue<Pair<MainFragment, Any?>> = LinkedList(); private val _queue: Queue<Pair<MainFragment, Any?>> = LinkedList();
lateinit var fragCurrent : MainFragment private set; lateinit var fragCurrent: MainFragment private set;
private var _parameterCurrent: Any? = null; private var _parameterCurrent: Any? = null;
var fragBeforeOverlay : MainFragment? = null; private set; var fragBeforeOverlay: MainFragment? = null; private set;
val onNavigated = Event1<MainFragment>(); val onNavigated = Event1<MainFragment>();
@ -217,15 +220,15 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
Logger.e("Application", "Uncaught", excp); Logger.e("Application", "Uncaught", excp);
//Resolve invocation chains //Resolve invocation chains
while(excp is InvocationTargetException || excp is java.lang.RuntimeException) { while (excp is InvocationTargetException || excp is java.lang.RuntimeException) {
val before = excp; val before = excp;
if(excp is InvocationTargetException) if (excp is InvocationTargetException)
excp = excp.targetException ?: excp.cause ?: excp; excp = excp.targetException ?: excp.cause ?: excp;
else if(excp is java.lang.RuntimeException) else if (excp is java.lang.RuntimeException)
excp = excp.cause ?: excp; excp = excp.cause ?: excp;
if(excp == before) if (excp == before)
break; break;
} }
writer.write((excp.message ?: "Empty error") + "\n\n"); writer.write((excp.message ?: "Empty error") + "\n\n");
@ -256,7 +259,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
setNavigationBarColorAndIcons(); setNavigationBarColorAndIcons();
if (Settings.instance.playback.allowVideoToGoUnderCutout) if (Settings.instance.playback.allowVideoToGoUnderCutout)
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
runBlocking { runBlocking {
StatePlatform.instance.updateAvailableClients(this@MainActivity); StatePlatform.instance.updateAvailableClients(this@MainActivity);
@ -330,10 +334,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
updateSegmentPaddings(); updateSegmentPaddings();
}; };
_fragVideoDetail.onTransitioning.subscribe { _fragVideoDetail.onTransitioning.subscribe {
if(it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED) if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED)
_fragContainerOverlay.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics); _fragContainerOverlay.elevation =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics);
else else
_fragContainerOverlay.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics); _fragContainerOverlay.elevation =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
} }
_fragVideoDetail.onCloseEvent.subscribe { _fragVideoDetail.onCloseEvent.subscribe {
@ -350,40 +356,39 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_buttonIncognito.alpha = 0f; _buttonIncognito.alpha = 0f;
StateApp.instance.privateModeChanged.subscribe { StateApp.instance.privateModeChanged.subscribe {
//Messing with visibility causes some issues with layout ordering? //Messing with visibility causes some issues with layout ordering?
if(it) { if (it) {
_buttonIncognito.elevation = 99f; _buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f; _buttonIncognito.alpha = 1f;
} } else {
else {
_buttonIncognito.elevation = -99f; _buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f; _buttonIncognito.alpha = 0f;
} }
} }
_buttonIncognito.setOnClickListener { _buttonIncognito.setOnClickListener {
if(!StateApp.instance.privateMode) if (!StateApp.instance.privateMode)
return@setOnClickListener; return@setOnClickListener;
UIDialogs.showDialog(this, R.drawable.ic_disabled_visible_purple, "Disable Privacy Mode", UIDialogs.showDialog(
this, R.drawable.ic_disabled_visible_purple, "Disable Privacy Mode",
"Do you want to disable privacy mode? New videos will be tracked again.", null, 0, "Do you want to disable privacy mode? New videos will be tracked again.", null, 0,
UIDialogs.Action("Cancel", { UIDialogs.Action("Cancel", {
StateApp.instance.setPrivacyMode(true); StateApp.instance.setPrivacyMode(true);
}, UIDialogs.ActionStyle.NONE), }, UIDialogs.ActionStyle.NONE),
UIDialogs.Action("Disable", { UIDialogs.Action("Disable", {
StateApp.instance.setPrivacyMode(false); StateApp.instance.setPrivacyMode(false);
}, UIDialogs.ActionStyle.DANGEROUS)); }, UIDialogs.ActionStyle.DANGEROUS)
);
}; };
_fragVideoDetail.onFullscreenChanged.subscribe { _fragVideoDetail.onFullscreenChanged.subscribe {
Logger.i(TAG, "onFullscreenChanged ${it}"); Logger.i(TAG, "onFullscreenChanged ${it}");
if(it) { if (it) {
_buttonIncognito.elevation = -99f; _buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f; _buttonIncognito.alpha = 0f;
} } else {
else { if (StateApp.instance.privateMode) {
if(StateApp.instance.privateMode) {
_buttonIncognito.elevation = 99f; _buttonIncognito.elevation = 99f;
_buttonIncognito.alpha = 1f; _buttonIncognito.alpha = 1f;
} } else {
else {
_buttonIncognito.elevation = -99f; _buttonIncognito.elevation = -99f;
_buttonIncognito.alpha = 0f; _buttonIncognito.alpha = 0f;
} }
@ -396,7 +401,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
return@subscribe; return@subscribe;
} }
if(_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) { if (_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
if (fragCurrent !is VideoDetailFragment) { if (fragCurrent !is VideoDetailFragment) {
val toPlay = StatePlayer.instance.getCurrentQueueItem(); val toPlay = StatePlayer.instance.getCurrentQueueItem();
navigate(_fragVideoDetail, toPlay); navigate(_fragVideoDetail, toPlay);
@ -448,7 +453,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
fragCurrent = _fragMainHome; fragCurrent = _fragMainHome;
val defaultTab = Settings.instance.tabs.mapNotNull { val defaultTab = Settings.instance.tabs.mapNotNull {
val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.firstOrNull { bd -> it.id == bd.id }; val buttonDefinition =
MenuBottomBarFragment.buttonDefinitions.firstOrNull { bd -> it.id == bd.id };
if (buttonDefinition == null) { if (buttonDefinition == null) {
return@mapNotNull null; return@mapNotNull null;
} else { } else {
@ -507,7 +513,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
//startActivity(Intent(this, TestActivity::class.java)); //startActivity(Intent(this, TestActivity::class.java));
val sharedPreferences = getSharedPreferences("GrayjayFirstBoot", Context.MODE_PRIVATE) val sharedPreferences =
getSharedPreferences("GrayjayFirstBoot", Context.MODE_PRIVATE)
val isFirstBoot = sharedPreferences.getBoolean("IsFirstBoot", true) val isFirstBoot = sharedPreferences.getBoolean("IsFirstBoot", true)
if (isFirstBoot) { if (isFirstBoot) {
UIDialogs.showConfirmationDialog(this, getString(R.string.do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button), { UIDialogs.showConfirmationDialog(this, getString(R.string.do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button), {
@ -516,6 +523,64 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply() sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
} }
val submissionStatus = FragmentedStorage.get<StringStorage>("subscriptionSubmissionStatus")
val numSubscriptions = StateSubscriptions.instance.getSubscriptionCount()
val subscriptionsThreshold = 20
if (
submissionStatus.value == ""
&& StateApp.instance.getCurrentNetworkState() != StateApp.NetworkState.DISCONNECTED
&& numSubscriptions >= subscriptionsThreshold
) {
UIDialogs.showDialog(
this,
R.drawable.ic_internet,
getString(R.string.contribute_personal_subscriptions_list),
getString(R.string.contribute_personal_subscriptions_list_description),
null,
0,
UIDialogs.Action("Cancel", {
submissionStatus.setAndSave("dismissed")
}, UIDialogs.ActionStyle.NONE),
UIDialogs.Action("Upload", {
submissionStatus.setAndSave("submitted")
GlobalScope.launch(Dispatchers.IO) {
@Serializable
data class CreatorInfo(val pluginId: String, val url: String)
val subscriptions =
StateSubscriptions.instance.getSubscriptions().map { original ->
CreatorInfo(
pluginId = original.channel.id.pluginId ?: "",
url = original.channel.url
)
}
val json = Json.encodeToString(subscriptions)
val url = "https://data.grayjay.app/donate-subscription-list"
val client = ManagedHttpClient();
val headers = hashMapOf(
"Content-Type" to "application/json"
)
try {
val response = client.post(url, json, headers)
// if it failed retry one time
if (!response.isOk) {
client.post(url, json, headers)
}
} catch (e: Exception) {
Logger.i(TAG, "Failed to submit subscription list.", e)
}
}
}, UIDialogs.ActionStyle.PRIMARY)
)
}
} }
/* /*
@ -580,39 +645,45 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
private fun handleIntent(intent: Intent?) { private fun handleIntent(intent: Intent?) {
if(intent == null) if (intent == null)
return; return;
Logger.i(TAG, "handleIntent started by " + intent.action); Logger.i(TAG, "handleIntent started by " + intent.action);
var targetData: String? = null; var targetData: String? = null;
when(intent.action) { when (intent.action) {
Intent.ACTION_SEND -> { Intent.ACTION_SEND -> {
targetData = intent.getStringExtra(Intent.EXTRA_STREAM) ?: intent.getStringExtra(Intent.EXTRA_TEXT); targetData = intent.getStringExtra(Intent.EXTRA_STREAM)
?: intent.getStringExtra(Intent.EXTRA_TEXT);
Logger.i(TAG, "Share Received: " + targetData); Logger.i(TAG, "Share Received: " + targetData);
} }
Intent.ACTION_VIEW -> { Intent.ACTION_VIEW -> {
targetData = intent.dataString targetData = intent.dataString
if(!targetData.isNullOrEmpty()) { if (!targetData.isNullOrEmpty()) {
Logger.i(TAG, "View Received: " + targetData); Logger.i(TAG, "View Received: " + targetData);
} }
} }
"VIDEO" -> { "VIDEO" -> {
val url = intent.getStringExtra("VIDEO"); val url = intent.getStringExtra("VIDEO");
navigate(_fragVideoDetail, url); navigate(_fragVideoDetail, url);
} }
"IMPORT_OPTIONS" -> { "IMPORT_OPTIONS" -> {
UIDialogs.showImportOptionsDialog(this); UIDialogs.showImportOptionsDialog(this);
} }
"ACTION" -> { "ACTION" -> {
val action = intent.getStringExtra("ACTION"); val action = intent.getStringExtra("ACTION");
StateDeveloper.instance.testState = "TestPlayback"; StateDeveloper.instance.testState = "TestPlayback";
StateDeveloper.instance.testPlayback(); StateDeveloper.instance.testPlayback();
} }
"TAB" -> { "TAB" -> {
when(intent.getStringExtra("TAB")){ when (intent.getStringExtra("TAB")) {
"Sources" -> { "Sources" -> {
runBlocking { runBlocking {
StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed.. StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed..
@ -623,7 +694,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf( navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf(
Pair("grayjay") { req -> Pair("grayjay") { req ->
StateApp.instance.contextOrNull?.let { StateApp.instance.contextOrNull?.let {
if(it is MainActivity) { if (it is MainActivity) {
runBlocking { runBlocking {
it.handleUrlAll(req.url.toString()); it.handleUrlAll(req.url.toString());
} }
@ -642,8 +713,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
handleUrlAll(targetData) handleUrlAll(targetData)
} }
} }
} } catch (ex: Throwable) {
catch(ex: Throwable) {
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_handle_file), ex); UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_handle_file), ex);
} }
} }
@ -652,35 +722,31 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
val uri = Uri.parse(url) val uri = Uri.parse(url)
when (uri.scheme) { when (uri.scheme) {
"grayjay" -> { "grayjay" -> {
if(url.startsWith("grayjay://license/")) { if (url.startsWith("grayjay://license/")) {
if(StatePayment.instance.setPaymentLicenseUrl(url)) 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)); 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) if (fragCurrent is BuyFragment)
closeSegment(fragCurrent); closeSegment(fragCurrent);
} } else
else
UIDialogs.toast(getString(R.string.invalid_license_format)); UIDialogs.toast(getString(R.string.invalid_license_format));
} } else if (url.startsWith("grayjay://plugin/")) {
else if(url.startsWith("grayjay://plugin/")) {
val intent = Intent(this, AddSourceActivity::class.java).apply { val intent = Intent(this, AddSourceActivity::class.java).apply {
data = Uri.parse(url.substring("grayjay://plugin/".length)); data = Uri.parse(url.substring("grayjay://plugin/".length));
}; };
startActivity(intent); startActivity(intent);
} } else if (url.startsWith("grayjay://video/")) {
else if(url.startsWith("grayjay://video/")) {
val videoUrl = url.substring("grayjay://video/".length); val videoUrl = url.substring("grayjay://video/".length);
navigate(_fragVideoDetail, videoUrl); navigate(_fragVideoDetail, videoUrl);
} } else if (url.startsWith("grayjay://channel/")) {
else if(url.startsWith("grayjay://channel/")) {
val channelUrl = url.substring("grayjay://channel/".length); val channelUrl = url.substring("grayjay://channel/".length);
navigate(_fragMainChannel, channelUrl); navigate(_fragMainChannel, channelUrl);
} }
} }
"content" -> { "content" -> {
if(!handleContent(url, intent.type)) { if (!handleContent(url, intent.type)) {
UIDialogs.showSingleButtonDialog( UIDialogs.showSingleButtonDialog(
this, this,
R.drawable.ic_play, R.drawable.ic_play,
@ -689,8 +755,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
{ }); { });
} }
} }
"file" -> { "file" -> {
if(!handleFile(url)) { if (!handleFile(url)) {
UIDialogs.showSingleButtonDialog( UIDialogs.showSingleButtonDialog(
this, this,
R.drawable.ic_play, R.drawable.ic_play,
@ -699,8 +766,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
{ }); { });
} }
} }
"polycentric" -> { "polycentric" -> {
if(!handlePolycentric(url)) { if (!handlePolycentric(url)) {
UIDialogs.showSingleButtonDialog( UIDialogs.showSingleButtonDialog(
this, this,
R.drawable.ic_play, R.drawable.ic_play,
@ -709,8 +777,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
{ }); { });
} }
} }
"fcast" -> { "fcast" -> {
if(!handleFCast(url)) { if (!handleFCast(url)) {
UIDialogs.showSingleButtonDialog( UIDialogs.showSingleButtonDialog(
this, this,
R.drawable.ic_cast, R.drawable.ic_cast,
@ -719,6 +788,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
{ }); { });
} }
} }
else -> { else -> {
if (!handleUrl(url)) { if (!handleUrl(url)) {
UIDialogs.showSingleButtonDialog( UIDialogs.showSingleButtonDialog(
@ -740,7 +810,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (StatePlatform.instance.hasEnabledVideoClient(url)) { if (StatePlatform.instance.hasEnabledVideoClient(url)) {
Logger.i(TAG, "handleUrl(url=$url) found video client"); Logger.i(TAG, "handleUrl(url=$url) found video client");
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
if(position > 0) if (position > 0)
navigate(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true)); navigate(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true));
else else
navigate(_fragVideoDetail, url); navigate(_fragVideoDetail, url);
@ -768,24 +838,25 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
return@withContext false; return@withContext false;
} }
} }
fun handleContent(file: String, mime: String? = null): Boolean { fun handleContent(file: String, mime: String? = null): Boolean {
Logger.i(TAG, "handleContent(url=$file)"); Logger.i(TAG, "handleContent(url=$file)");
val data = readSharedContent(file); val data = readSharedContent(file);
if(file.lowercase().endsWith(".json") || mime == "application/json") { if (file.lowercase().endsWith(".json") || mime == "application/json") {
var recon = String(data); var recon = String(data);
if(!recon.trim().startsWith("[")) if (!recon.trim().startsWith("["))
return handleUnknownJson(recon); return handleUnknownJson(recon);
var reconLines = Json.decodeFromString<List<String>>(recon); var reconLines = Json.decodeFromString<List<String>>(recon);
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length); val cacheStr =
reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
var cache: ImportCache? = null; var cache: ImportCache? = null;
try { try {
if(cacheStr != null) if (cacheStr != null)
cache = Json.decodeFromString(cacheStr); cache = Json.decodeFromString(cacheStr);
} } catch (ex: Throwable) {
catch(ex: Throwable) {
Logger.e(TAG, "Failed to deserialize cache"); Logger.e(TAG, "Failed to deserialize cache");
} }
@ -794,32 +865,31 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}"); Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
handleReconstruction(recon, cache); handleReconstruction(recon, cache);
return true; return true;
} } else if (file.lowercase().endsWith(".zip") || mime == "application/zip") {
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
StateBackup.importZipBytes(this, lifecycleScope, data); StateBackup.importZipBytes(this, lifecycleScope, data);
return true; return true;
} } else if (file.lowercase().endsWith(".txt") || mime == "text/plain") {
else if(file.lowercase().endsWith(".txt") || mime == "text/plain") {
return handleUnknownText(String(data)); return handleUnknownText(String(data));
} }
return false; return false;
} }
fun handleFile(file: String): Boolean { fun handleFile(file: String): Boolean {
Logger.i(TAG, "handleFile(url=$file)"); Logger.i(TAG, "handleFile(url=$file)");
if(file.lowercase().endsWith(".json")) { if (file.lowercase().endsWith(".json")) {
var recon = String(readSharedFile(file)); var recon = String(readSharedFile(file));
if(!recon.startsWith("[")) if (!recon.startsWith("["))
return handleUnknownJson(recon); return handleUnknownJson(recon);
var reconLines = Json.decodeFromString<List<String>>(recon); var reconLines = Json.decodeFromString<List<String>>(recon);
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length); val cacheStr =
reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
var cache: ImportCache? = null; var cache: ImportCache? = null;
try { try {
if(cacheStr != null) if (cacheStr != null)
cache = Json.decodeFromString(cacheStr); cache = Json.decodeFromString(cacheStr);
} } catch (ex: Throwable) {
catch(ex: Throwable) {
Logger.e(TAG, "Failed to deserialize cache"); Logger.e(TAG, "Failed to deserialize cache");
} }
recon = reconLines.joinToString("\n"); recon = reconLines.joinToString("\n");
@ -827,19 +897,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}"); Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
handleReconstruction(recon, cache); handleReconstruction(recon, cache);
return true; return true;
} } else if (file.lowercase().endsWith(".zip")) {
else if(file.lowercase().endsWith(".zip")) {
StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file)); StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file));
return true; return true;
} } else if (file.lowercase().endsWith(".txt")) {
else if(file.lowercase().endsWith(".txt")) {
return handleUnknownText(String(readSharedFile(file))); return handleUnknownText(String(readSharedFile(file)));
} }
return false; return false;
} }
fun handleReconstruction(recon: String, cache: ImportCache? = null) { fun handleReconstruction(recon: String, cache: ImportCache? = null) {
val type = ManagedStore.getReconstructionIdentifier(recon); val type = ManagedStore.getReconstructionIdentifier(recon);
val store: ManagedStore<*> = when(type) { val store: ManagedStore<*> = when (type) {
"Playlist" -> StatePlaylists.instance.playlistStore "Playlist" -> StatePlaylists.instance.playlistStore
else -> { else -> {
UIDialogs.toast(getString(R.string.unknown_reconstruction_type) + " ${type}", false); UIDialogs.toast(getString(R.string.unknown_reconstruction_type) + " ${type}", false);
@ -847,13 +916,15 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
}; };
}; };
val name = when(type) { val name = when (type) {
"Playlist" -> recon.split("\n").filter { !it.startsWith(ManagedStore.RECONSTRUCTION_HEADER_OPERATOR) }.firstOrNull() ?: type; "Playlist" -> recon.split("\n")
.filter { !it.startsWith(ManagedStore.RECONSTRUCTION_HEADER_OPERATOR) }
.firstOrNull() ?: type;
else -> type else -> type
} }
if(!type.isNullOrEmpty()) { if (!type.isNullOrEmpty()) {
UIDialogs.showImportDialog(this, store, name, listOf(recon), cache) { UIDialogs.showImportDialog(this, store, name, listOf(recon), cache) {
} }
@ -862,18 +933,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
fun handleUnknownText(text: String): Boolean { fun handleUnknownText(text: String): Boolean {
try { try {
if(text.startsWith("@/Subscription") || text.startsWith("Subscriptions")) { if (text.startsWith("@/Subscription") || text.startsWith("Subscriptions")) {
val lines = text.split("\n").map { it.trim() }.drop(1).filter { it.isNotEmpty() }; val lines = text.split("\n").map { it.trim() }.drop(1).filter { it.isNotEmpty() };
navigate(_fragImportSubscriptions, lines); navigate(_fragImportSubscriptions, lines);
return true; return true;
} }
} } catch (ex: Throwable) {
catch(ex: Throwable) {
Logger.e(TAG, ex.message, ex); Logger.e(TAG, ex.message, ex);
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_parse_text_file), ex); UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_parse_text_file), ex);
} }
return false; return false;
} }
fun handleUnknownJson(json: String): Boolean { fun handleUnknownJson(json: String): Boolean {
val context = this; val context = this;
@ -885,8 +956,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
return false;//throw IllegalArgumentException("Invalid NewPipe json structure found"); return false;//throw IllegalArgumentException("Invalid NewPipe json structure found");
StateBackup.importNewPipeSubs(this, newPipeSubsParsed); StateBackup.importNewPipeSubs(this, newPipeSubsParsed);
} } catch (ex: Exception) {
catch(ex: Exception) {
Logger.e(TAG, ex.message, ex); Logger.e(TAG, ex.message, ex);
UIDialogs.showGeneralErrorDialog(context, getString(R.string.failed_to_parse_newpipe_subscriptions), ex); UIDialogs.showGeneralErrorDialog(context, getString(R.string.failed_to_parse_newpipe_subscriptions), ex);
} }
@ -932,7 +1002,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
private fun readSharedFile(filePath: String): ByteArray { private fun readSharedFile(filePath: String): ByteArray {
val dataFile = File(filePath); val dataFile = File(filePath);
if(!dataFile.exists()) if (!dataFile.exists())
throw IllegalArgumentException("Opened file does not exist or not permitted"); throw IllegalArgumentException("Opened file does not exist or not permitted");
val data = dataFile.readBytes(); val data = dataFile.readBytes();
return data; return data;
@ -941,13 +1011,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
override fun onBackPressed() { override fun onBackPressed() {
Logger.i(TAG, "onBackPressed") Logger.i(TAG, "onBackPressed")
if(_fragBotBarMenu.onBackPressed()) if (_fragBotBarMenu.onBackPressed())
return; return;
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed()) if (_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
return; return;
if(!fragCurrent.onBackPressed()) if (!fragCurrent.onBackPressed())
closeSegment(); closeSegment();
} }
@ -955,7 +1025,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
super.onUserLeaveHint(); super.onUserLeaveHint();
Logger.i(TAG, "onUserLeaveHint") Logger.i(TAG, "onUserLeaveHint")
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED || _fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED) if (_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED || _fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED)
_fragVideoDetail.onUserLeaveHint(); _fragVideoDetail.onUserLeaveHint();
} }
@ -991,12 +1061,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
fun navigate(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) { fun navigate(segment: MainFragment, parameter: Any? = null, withHistory: Boolean = true, isBack: Boolean = false) {
Logger.i(TAG, "Navigate to $segment (parameter=$parameter, withHistory=$withHistory, isBack=$isBack)") Logger.i(TAG, "Navigate to $segment (parameter=$parameter, withHistory=$withHistory, isBack=$isBack)")
if(segment != fragCurrent) { if (segment != fragCurrent) {
if(segment is VideoDetailFragment) { if (segment is VideoDetailFragment) {
if(_fragContainerVideoDetail.visibility != View.VISIBLE) if (_fragContainerVideoDetail.visibility != View.VISIBLE)
_fragContainerVideoDetail.visibility = View.VISIBLE; _fragContainerVideoDetail.visibility = View.VISIBLE;
when(segment.state) { when (segment.state) {
VideoDetailFragment.State.MINIMIZED -> segment.maximizeVideoDetail() VideoDetailFragment.State.MINIMIZED -> segment.maximizeVideoDetail()
VideoDetailFragment.State.CLOSED -> segment.maximizeVideoDetail() VideoDetailFragment.State.CLOSED -> segment.maximizeVideoDetail()
else -> {} else -> {}
@ -1005,10 +1075,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
return; return;
} }
fragCurrent.onHide(); fragCurrent.onHide();
if(segment.isMainView) { if (segment.isMainView) {
var transaction = supportFragmentManager.beginTransaction(); var transaction = supportFragmentManager.beginTransaction();
if (segment.topBar != null) { if (segment.topBar != null) {
if (segment.topBar != fragCurrent.topBar) { if (segment.topBar != fragCurrent.topBar) {
@ -1017,8 +1086,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
.replace(R.id.fragment_top_bar, segment.topBar as Fragment); .replace(R.id.fragment_top_bar, segment.topBar as Fragment);
fragCurrent.topBar?.onHide(); fragCurrent.topBar?.onHide();
} }
} } else if (fragCurrent.topBar != null)
else if(fragCurrent.topBar != null)
transaction.hide(fragCurrent.topBar as Fragment); transaction.hide(fragCurrent.topBar as Fragment);
transaction = transaction.replace(R.id.fragment_main, segment); transaction = transaction.replace(R.id.fragment_main, segment);
@ -1026,25 +1094,24 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if (segment.hasBottomBar) { if (segment.hasBottomBar) {
if (!fragCurrent.hasBottomBar) if (!fragCurrent.hasBottomBar)
transaction = transaction.show(_fragBotBarMenu); transaction = transaction.show(_fragBotBarMenu);
} } else {
else { if (fragCurrent.hasBottomBar)
if(fragCurrent.hasBottomBar)
transaction = transaction.hide(_fragBotBarMenu); transaction = transaction.hide(_fragBotBarMenu);
} }
transaction.commitNow(); transaction.commitNow();
} else { } else {
if(!segment.hasBottomBar) { if (!segment.hasBottomBar) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.hide(_fragBotBarMenu) .hide(_fragBotBarMenu)
.commitNow(); .commitNow();
} }
} }
if(fragCurrent.isHistory && withHistory && _queue.lastOrNull() != fragCurrent) if (fragCurrent.isHistory && withHistory && _queue.lastOrNull() != fragCurrent)
_queue.add(Pair(fragCurrent, _parameterCurrent)); _queue.add(Pair(fragCurrent, _parameterCurrent));
if(segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory) if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory)
fragBeforeOverlay = fragCurrent; fragBeforeOverlay = fragCurrent;
@ -1062,12 +1129,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
* If called with a non-null fragment, it will only close if the current fragment is the provided one * If called with a non-null fragment, it will only close if the current fragment is the provided one
*/ */
fun closeSegment(fragment: MainFragment? = null) { fun closeSegment(fragment: MainFragment? = null) {
if(fragment is VideoDetailFragment) { if (fragment is VideoDetailFragment) {
fragment.onHide(); fragment.onHide();
return; return;
} }
if((fragment?.isOverlay ?: false) && fragBeforeOverlay != null) { if ((fragment?.isOverlay ?: false) && fragBeforeOverlay != null) {
navigate(fragBeforeOverlay!!, null, false, true); navigate(fragBeforeOverlay!!, null, false, true);
} else { } else {
val last = _queue.lastOrNull(); val last = _queue.lastOrNull();
@ -1089,8 +1156,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
/** /**
* Provides the fragment instance for the provided fragment class * Provides the fragment instance for the provided fragment class
*/ */
inline fun <reified T : Fragment> getFragment() : T { inline fun <reified T : Fragment> getFragment(): T {
return when(T::class) { return when (T::class) {
HomeFragment::class -> _fragMainHome as T; HomeFragment::class -> _fragMainHome as T;
TutorialFragment::class -> _fragMainTutorial as T; TutorialFragment::class -> _fragMainTutorial as T;
ContentSearchResultsFragment::class -> _fragMainVideoSearchResults as T; ContentSearchResultsFragment::class -> _fragMainVideoSearchResults as T;
@ -1127,15 +1194,21 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
private fun updateSegmentPaddings() { private fun updateSegmentPaddings() {
var paddingBottom = 0f; var paddingBottom = 0f;
if(fragCurrent.hasBottomBar) if (fragCurrent.hasBottomBar)
paddingBottom += HEIGHT_MENU_DP; paddingBottom += HEIGHT_MENU_DP;
_fragContainerOverlay.setPadding(0,0,0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom - HEIGHT_MENU_DP, resources.displayMetrics).toInt()); _fragContainerOverlay.setPadding(
0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom - HEIGHT_MENU_DP, resources.displayMetrics)
.toInt()
);
if(_fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED) if (_fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED)
paddingBottom += HEIGHT_VIDEO_MINIMIZED_DP; paddingBottom += HEIGHT_VIDEO_MINIMIZED_DP;
_fragContainerMain.setPadding(0,0,0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom, resources.displayMetrics).toInt()); _fragContainerMain.setPadding(
0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom, resources.displayMetrics)
.toInt()
);
} }
@ -1151,14 +1224,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> { ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
} }
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> { ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required", UIDialogs.showDialog(
this, R.drawable.ic_notifications, "Notifications Required",
reason, null, 0, reason, null, 0,
UIDialogs.Action("Cancel", {}), UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Enable", { UIDialogs.Action("Enable", {
requestPermissionLauncher.launch(notifPermission); requestPermissionLauncher.launch(notifPermission);
}, UIDialogs.ActionStyle.PRIMARY)); }, UIDialogs.ActionStyle.PRIMARY)
);
} }
else -> { else -> {
requestPermissionLauncher.launch(notifPermission); requestPermissionLauncher.launch(notifPermission);
} }
@ -1170,15 +1247,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
fun showAppToast(toast: ToastView.Toast) { fun showAppToast(toast: ToastView.Toast) {
synchronized(_toastQueue) { synchronized(_toastQueue) {
_toastQueue.add(toast); _toastQueue.add(toast);
if(_toastJob?.isActive != true) if (_toastJob?.isActive != true)
_toastJob = lifecycleScope.launch(Dispatchers.Default) { _toastJob = lifecycleScope.launch(Dispatchers.Default) {
launchAppToastJob(); launchAppToastJob();
}; };
} }
} }
private suspend fun launchAppToastJob() { private suspend fun launchAppToastJob() {
Logger.i(TAG, "Starting appToast loop"); Logger.i(TAG, "Starting appToast loop");
while(!_toastQueue.isEmpty()) { while (!_toastQueue.isEmpty()) {
val toast = _toastQueue.poll() ?: continue; val toast = _toastQueue.poll() ?: continue;
Logger.i(TAG, "Showing next toast (${toast.msg})"); Logger.i(TAG, "Showing next toast (${toast.msg})");
@ -1191,7 +1269,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_toastView.setToastAnimated(toast); _toastView.setToastAnimated(toast);
} }
} }
if(toast.long) if (toast.long)
delay(5000); delay(5000);
else else
delay(3000); delay(3000);
@ -1205,18 +1283,19 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers. //TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>(); private var resultLauncherMap = mutableMapOf<Int, (ActivityResult) -> Unit>();
private var requestCode: Int? = -1; private var requestCode: Int? = -1;
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult( private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) { ActivityResultContracts.StartActivityForResult()
result: ActivityResult -> ) { result: ActivityResult ->
val handler = synchronized(resultLauncherMap) { val handler = synchronized(resultLauncherMap) {
resultLauncherMap.remove(requestCode); resultLauncherMap.remove(requestCode);
} }
if(handler != null) if (handler != null)
handler(result); handler(result);
}; };
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit) {
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult) -> Unit) {
synchronized(resultLauncherMap) { synchronized(resultLauncherMap) {
resultLauncherMap[code] = handler; resultLauncherMap[code] = handler;
} }
@ -1227,21 +1306,23 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
companion object { companion object {
private val TAG = "MainActivity" private val TAG = "MainActivity"
fun getTabIntent(context: Context, tab: String) : Intent { fun getTabIntent(context: Context, tab: String): Intent {
val sourcesIntent = Intent(context, MainActivity::class.java); val sourcesIntent = Intent(context, MainActivity::class.java);
sourcesIntent.action = "TAB"; sourcesIntent.action = "TAB";
sourcesIntent.putExtra("TAB", tab); sourcesIntent.putExtra("TAB", tab);
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
return sourcesIntent; return sourcesIntent;
} }
fun getVideoIntent(context: Context, videoUrl: String) : Intent {
fun getVideoIntent(context: Context, videoUrl: String): Intent {
val sourcesIntent = Intent(context, MainActivity::class.java); val sourcesIntent = Intent(context, MainActivity::class.java);
sourcesIntent.action = "VIDEO"; sourcesIntent.action = "VIDEO";
sourcesIntent.putExtra("VIDEO", videoUrl); sourcesIntent.putExtra("VIDEO", videoUrl);
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
return sourcesIntent; return sourcesIntent;
} }
fun getActionIntent(context: Context, action: String) : Intent {
fun getActionIntent(context: Context, action: String): Intent {
val sourcesIntent = Intent(context, MainActivity::class.java); val sourcesIntent = Intent(context, MainActivity::class.java);
sourcesIntent.action = "ACTION"; sourcesIntent.action = "ACTION";
sourcesIntent.putExtra("ACTION", action); sourcesIntent.putExtra("ACTION", action);

View File

@ -88,7 +88,8 @@ class DashBuilder : XMLBuilder {
fun withRepresentationOnDemand(id: String, subtitleSource: ISubtitleSource, subtitleUrl: String) { fun withRepresentationOnDemand(id: String, subtitleSource: ISubtitleSource, subtitleUrl: String) {
withRepresentation(id, mapOf( withRepresentation(id, mapOf(
Pair("mimeType", subtitleSource.format ?: "text/vtt"), Pair("mimeType", subtitleSource.format ?: "text/vtt"),
Pair("startWithSAP", "1"), Pair("default", "true"),
Pair("lang", "en"),
Pair("bandwidth", "1000") Pair("bandwidth", "1000")
)) { )) {
it.withBaseURL(subtitleUrl) it.withBaseURL(subtitleUrl)
@ -151,7 +152,7 @@ class DashBuilder : XMLBuilder {
) )
) { ) {
//TODO: Verify if & really should be replaced like this? //TODO: Verify if & really should be replaced like this?
it.withRepresentationOnDemand("1", subtitleSource, subtitleUrl.replace("&", "&amp;")) it.withRepresentationOnDemand("caption_en", subtitleSource, subtitleUrl.replace("&", "&amp;"))
} }
} }
//Video //Video
@ -164,7 +165,7 @@ class DashBuilder : XMLBuilder {
Pair("subsegmentStartsWithSAP", "1") Pair("subsegmentStartsWithSAP", "1")
) )
) { ) {
it.withRepresentationOnDemand("1", vidSource, vidUrl.replace("&", "&amp;")); it.withRepresentationOnDemand("2", vidSource, vidUrl.replace("&", "&amp;"));
} }
} }

View File

@ -1352,7 +1352,7 @@ class VideoDetailView : ConstraintLayout {
} }
catch(ex: Throwable) { catch(ex: Throwable) {
Logger.e(TAG, "Playback tracker failed", ex); Logger.e(TAG, "Playback tracker failed", ex);
if(me.video?.isLive == true) withContext(Dispatchers.Main) { if(me.video?.isLive == true || ex.message?.contains("Unable to resolve host") == true) withContext(Dispatchers.Main) {
UIDialogs.toast(context, context.getString(R.string.failed_to_get_playback_tracker)); UIDialogs.toast(context, context.getString(R.string.failed_to_get_playback_tracker));
}; };
else withContext(Dispatchers.Main) { else withContext(Dispatchers.Main) {
@ -2823,6 +2823,7 @@ class VideoDetailView : ConstraintLayout {
.exception<Throwable> { .exception<Throwable> {
Logger.w(ChannelFragment.TAG, "Failed to load video.", it); Logger.w(ChannelFragment.TAG, "Failed to load video.", it);
if(!(it.message?.contains("Unable to resolve host") ?: false && nextVideo())){
handleErrorOrCall { handleErrorOrCall {
_retryCount = 0; _retryCount = 0;
_retryJob?.cancel(); _retryJob?.cancel();
@ -2831,6 +2832,7 @@ class VideoDetailView : ConstraintLayout {
_liveTryJob = null; _liveTryJob = null;
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo, null, fragment); UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo, null, fragment);
} }
}
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope}); } else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
private val _taskLoadRecommendations = TaskHandler<String?, IPager<IPlatformContent>?>(StateApp.instance.scopeGetter, { private val _taskLoadRecommendations = TaskHandler<String?, IPager<IPlatformContent>?>(StateApp.instance.scopeGetter, {

View File

@ -26,6 +26,7 @@ import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.SearchHistoryStorage import com.futo.platformplayer.stores.SearchHistoryStorage
class SearchTopBarFragment : TopFragment() { class SearchTopBarFragment : TopFragment() {
@Suppress("PrivatePropertyName")
private val TAG = "SearchTopBarFragment" private val TAG = "SearchTopBarFragment"
private var _editSearch: EditText? = null; private var _editSearch: EditText? = null;
@ -191,29 +192,32 @@ class SearchTopBarFragment : TopFragment() {
} }
private fun onDone() { private fun onDone() {
val editSearch = _editSearch; val editSearch = _editSearch
if (editSearch != null) { if (editSearch != null) {
val text = editSearch.text.toString(); val text = editSearch.text.toString()
if (text.length < 3) { if (text.isEmpty()) {
UIDialogs.toast(getString(R.string.please_use_at_least_3_characters)); UIDialogs.toast(getString(R.string.please_use_at_least_1_character))
return; return
} }
editSearch.clearFocus(); editSearch.clearFocus()
_inputMethodManager?.hideSoftInputFromWindow(editSearch.windowToken, 0); _inputMethodManager?.hideSoftInputFromWindow(editSearch.windowToken, 0)
if (Settings.instance.search.searchHistory) { if (Settings.instance.search.searchHistory) {
val storage = FragmentedStorage.get<SearchHistoryStorage>(); val storage = FragmentedStorage.get<SearchHistoryStorage>()
storage.add(text); storage.add(text)
} }
if (_searchType == SearchType.CREATOR) { if (_searchType == SearchType.CREATOR) {
onSearch.emit(text); onSearch.emit(text)
} else { } else {
onSearch.emit(text); onSearch.emit(text)
} }
} else { } else {
Logger.w(TAG, "Unexpected condition happened where done is edit search is null but done is triggered."); Logger.w(
TAG,
"Unexpected condition happened where done is edit search is null but done is triggered."
)
} }
} }

View File

@ -182,13 +182,14 @@ class HLS {
private fun parseAttributes(content: String): Map<String, String> { private fun parseAttributes(content: String): Map<String, String> {
val attributes = mutableMapOf<String, String>() val attributes = mutableMapOf<String, String>()
val attributePairs = content.substringAfter(":").splitToSequence(',') val maybeAttributePairs = content.substringAfter(":").splitToSequence(',')
var currentPair = StringBuilder() var currentPair = StringBuilder()
for (pair in attributePairs) { for (pair in maybeAttributePairs) {
currentPair.append(pair) currentPair.append(pair)
if (currentPair.count { it == '\"' } % 2 == 0) { // Check if the number of quotes is even if (currentPair.count { it == '\"' } % 2 == 0) { // Check if the number of quotes is even
val (key, value) = currentPair.toString().split('=') val key = currentPair.toString().substringBefore("=")
val value = currentPair.toString().substringAfter("=")
attributes[key.trim()] = value.trim().removeSurrounding("\"") attributes[key.trim()] = value.trim().removeSurrounding("\"")
currentPair = StringBuilder() // Reset for the next attribute currentPair = StringBuilder() // Reset for the next attribute
} else { } else {

View File

@ -537,7 +537,7 @@ class StatePlatform {
else getSortedEnabledClient().filter { if (it is JSClient) it.enableInSearch else true }; else getSortedEnabledClient().filter { if (it is JSClient) it.enableInSearch else true };
clients.parallelStream().forEach { clients.parallelStream().forEach {
val searchCapabilities = it.getSearchCapabilities(); val searchCapabilities = it.getSearchChannelContentsCapabilities();
val mappedFilters = filters.map { pair -> Pair(pair.key, pair.value.map { v -> searchCapabilities.filters.first { g -> g.idOrName == pair.key }.filters.first { f -> f.idOrName == v }.value }) }.toMap(); val mappedFilters = filters.map { pair -> Pair(pair.key, pair.value.map { v -> searchCapabilities.filters.first { g -> g.idOrName == pair.key }.filters.first { f -> f.idOrName == v }.value }) }.toMap();
if (it.isChannelUrl(channelUrl)) { if (it.isChannelUrl(channelUrl)) {

View File

@ -284,12 +284,16 @@ class StateSync {
return@SyncSocketSession return@SyncSocketSession
} }
Logger.i(TAG, "Handshake complete with ${s.remotePublicKey}") Logger.i(TAG, "Handshake complete with (LocalPublicKey = ${s.localPublicKey}, RemotePublicKey = ${s.remotePublicKey})")
synchronized(_sessions) { synchronized(_sessions) {
session = _sessions[s.remotePublicKey] session = _sessions[s.remotePublicKey]
if (session == null) { if (session == null) {
session = SyncSession(remotePublicKey, onAuthorized = { session = SyncSession(remotePublicKey, onAuthorized = { it, isNewlyAuthorized, isNewSession ->
if (!isNewSession) {
return@SyncSession
}
Logger.i(TAG, "${s.remotePublicKey} authorized") Logger.i(TAG, "${s.remotePublicKey} authorized")
synchronized(_lastAddressStorage) { synchronized(_lastAddressStorage) {
_lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress) _lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress)
@ -358,6 +362,16 @@ class StateSync {
} }
}) })
} }
} else {
val publicKey = session!!.remotePublicKey
session!!.unauthorize(s)
session!!.close()
synchronized(_sessions) {
_sessions.remove(publicKey)
}
Logger.i(TAG, "Connection unauthorized for ${remotePublicKey} because not authorized and not on pairing activity to ask")
} }
} else { } else {
//Responder does not need to check because already approved //Responder does not need to check because already approved

View File

@ -23,6 +23,7 @@ import com.futo.platformplayer.sync.models.SyncPlaylistsPackage
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
import com.futo.platformplayer.sync.models.SyncWatchLaterPackage import com.futo.platformplayer.sync.models.SyncWatchLaterPackage
import com.futo.platformplayer.toUtf8String
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
@ -32,6 +33,7 @@ import java.nio.ByteBuffer
import java.time.Instant import java.time.Instant
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import java.util.UUID
interface IAuthorizable { interface IAuthorizable {
val isAuthorized: Boolean val isAuthorized: Boolean
@ -41,13 +43,16 @@ class SyncSession : IAuthorizable {
private val _socketSessions: MutableList<SyncSocketSession> = mutableListOf() private val _socketSessions: MutableList<SyncSocketSession> = mutableListOf()
private var _authorized: Boolean = false private var _authorized: Boolean = false
private var _remoteAuthorized: Boolean = false private var _remoteAuthorized: Boolean = false
private val _onAuthorized: (session: SyncSession) -> Unit private val _onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit
private val _onUnauthorized: (session: SyncSession) -> Unit private val _onUnauthorized: (session: SyncSession) -> Unit
private val _onClose: (session: SyncSession) -> Unit private val _onClose: (session: SyncSession) -> Unit
private val _onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit private val _onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit
val remotePublicKey: String val remotePublicKey: String
override val isAuthorized get() = _authorized && _remoteAuthorized override val isAuthorized get() = _authorized && _remoteAuthorized
private var _wasAuthorized = false private var _wasAuthorized = false
private val _id = UUID.randomUUID()
private var _remoteId: UUID? = null
private var _lastAuthorizedRemoteId: UUID? = null
var connected: Boolean = false var connected: Boolean = false
private set(v) { private set(v) {
@ -57,7 +62,7 @@ class SyncSession : IAuthorizable {
} }
} }
constructor(remotePublicKey: String, onAuthorized: (session: SyncSession) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit) { constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit) {
this.remotePublicKey = remotePublicKey this.remotePublicKey = remotePublicKey
_onAuthorized = onAuthorized _onAuthorized = onAuthorized
_onUnauthorized = onUnauthorized _onUnauthorized = onUnauthorized
@ -79,7 +84,8 @@ class SyncSession : IAuthorizable {
} }
fun authorize(socketSession: SyncSocketSession) { fun authorize(socketSession: SyncSocketSession) {
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value) Logger.i(TAG, "Sent AUTHORIZED with session id $_id")
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray()))
_authorized = true _authorized = true
checkAuthorized() checkAuthorized()
} }
@ -97,9 +103,13 @@ class SyncSession : IAuthorizable {
} }
private fun checkAuthorized() { private fun checkAuthorized() {
if (!_wasAuthorized && isAuthorized) { if (isAuthorized) {
val isNewlyAuthorized = !_wasAuthorized;
val isNewSession = _lastAuthorizedRemoteId != _remoteId;
Logger.i(TAG, "onAuthorized (isNewlyAuthorized = $isNewlyAuthorized, isNewSession = $isNewSession)");
_onAuthorized.invoke(this, !_wasAuthorized, _lastAuthorizedRemoteId != _remoteId)
_wasAuthorized = true _wasAuthorized = true
_onAuthorized.invoke(this) _lastAuthorizedRemoteId = _remoteId
} }
} }
@ -128,12 +138,19 @@ class SyncSession : IAuthorizable {
when (opcode) { when (opcode) {
Opcode.NOTIFY_AUTHORIZED.value -> { Opcode.NOTIFY_AUTHORIZED.value -> {
val str = data.toUtf8String()
_remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000")
_remoteAuthorized = true _remoteAuthorized = true
Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId")
checkAuthorized() checkAuthorized()
return
} }
Opcode.NOTIFY_UNAUTHORIZED.value -> { Opcode.NOTIFY_UNAUTHORIZED.value -> {
_remoteId = null
_lastAuthorizedRemoteId = null
_remoteAuthorized = false _remoteAuthorized = false
_onUnauthorized(this) _onUnauthorized(this)
return
} }
//TODO: Handle any kind of packet (that is not necessarily authorized) //TODO: Handle any kind of packet (that is not necessarily authorized)
} }

View File

@ -10,6 +10,7 @@ import com.futo.platformplayer.noise.protocol.HandshakeState
import com.futo.platformplayer.states.StateSync import com.futo.platformplayer.states.StateSync
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import java.util.UUID
class SyncSocketSession { class SyncSocketSession {
enum class Opcode(val value: UByte) { enum class Opcode(val value: UByte) {

View File

@ -726,7 +726,7 @@
<string name="not_yet_available_retrying_in_time_s">Not yet available, retrying in {time}s</string> <string name="not_yet_available_retrying_in_time_s">Not yet available, retrying in {time}s</string>
<string name="failed_to_retry_for_live_stream">Failed to retry for live stream</string> <string name="failed_to_retry_for_live_stream">Failed to retry for live stream</string>
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">This app is in development. Please submit bug reports and understand that many features are incomplete.</string> <string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">This app is in development. Please submit bug reports and understand that many features are incomplete.</string>
<string name="please_use_at_least_3_characters">Please use at least 3 characters</string> <string name="please_use_at_least_1_character">Please use at least 1 character</string>
<string name="are_you_sure_you_want_to_delete_this_video">Are you sure you want to delete this video?</string> <string name="are_you_sure_you_want_to_delete_this_video">Are you sure you want to delete this video?</string>
<string name="tap_to_open">Tap to open</string> <string name="tap_to_open">Tap to open</string>
<string name="update_available_exclamation">Update available!</string> <string name="update_available_exclamation">Update available!</string>
@ -810,6 +810,8 @@
<string name="scroll_to_top">Scroll to top</string> <string name="scroll_to_top">Scroll to top</string>
<string name="disable_battery_optimization">Disable Battery Optimization</string> <string name="disable_battery_optimization">Disable Battery Optimization</string>
<string name="click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions">Click to go to battery optimization settings. Disabling battery optimization will prevent the OS from killing media sessions.</string> <string name="click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions">Click to go to battery optimization settings. Disabling battery optimization will prevent the OS from killing media sessions.</string>
<string name="contribute_personal_subscriptions_list">Contribute Personal Subscriptions List</string>
<string name="contribute_personal_subscriptions_list_description">\nWould you liked to contribute your current creator subscriptions list to FUTO?\n\nThe data will be handled according to the Grayjay privacy policy. That is the list will be anonymized and stored without any reference to whomever the list of creators belonged to.\n\nThe intention is for Grayjay and FUTO to use these data to build a cross platform creator recommendation system to make it easier to find new creators you might like from within Grayjay.</string>
<string name="cd_cast_button">Cast button</string> <string name="cd_cast_button">Cast button</string>
<string name="cd_incognito_button">Incognito button</string> <string name="cd_incognito_button">Incognito button</string>
<string name="cd_creator_thumbnail">Creator thumbnail</string> <string name="cd_creator_thumbnail">Creator thumbnail</string>