diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 23cc959e..8af2e2f5 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -49,9 +49,23 @@ We encourage developers to write their own plugins. Please refer to the "Getting ## Contributing to Core -**We are currently not accepting contributions to the core.** -The core is currently licensed under the FUTO Temporary License (FTL). The licensing and ownership of contributions to the core are complex topics that we are still working on. We'll update these guidelines when we have more clarity. +### License + +The core is currently licensed under the [Source First License 1.1](./LICENSE.md). All contributors have to sign FUTO Individual Contributor License Agreement before contributions can be accepted. You can read more about it at [https://cla.futo.org/](https://cla.futo.org/). + +### How to Contribute + +1. Fork the core repository. +2. Clone your fork. +3. Make your changes. +4. Commit and push your changes. +5. Open a pull request. + +### Guidelines + +- Ensure your code adheres to the existing style. +- Include documentation and unit tests (where applicable). --- diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 6c73aaec..b90c3849 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -144,7 +144,6 @@ class Settings : FragmentedStorageFileJson() { fun import() { val act = SettingsActivity.getActivity() ?: return; val intent = MainActivity.getImportOptionsIntent(act); - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK; act.startActivity(intent); } @@ -906,7 +905,7 @@ class Settings : FragmentedStorageFileJson() { var enabled: Boolean = true; @FormField(R.string.broadcast, FieldForm.TOGGLE, R.string.broadcast_description, 1) - var broadcast: Boolean = true; + var broadcast: Boolean = false; @FormField(R.string.connect_discovered, FieldForm.TOGGLE, R.string.connect_discovered_description, 2) var connectDiscovered: Boolean = true; diff --git a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt index 5e73638c..519c9c19 100644 --- a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt +++ b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.graphics.Color import android.net.Uri +import android.text.Layout import android.text.method.ScrollingMovementMethod import android.util.TypedValue import android.view.Gravity @@ -198,7 +199,6 @@ class UIDialogs { dialog.show(); } - 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 view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null); @@ -214,18 +214,20 @@ class UIDialogs { this.text = text; }; view.findViewById(R.id.dialog_text_details).apply { - if(textDetails == null) + if (textDetails == null) this.visibility = View.GONE; - else + else { this.text = textDetails; + this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START + } }; view.findViewById(R.id.dialog_text_code).apply { - if(code == null) - this.visibility = View.GONE; + if (code == null) this.visibility = View.GONE; else { this.text = code; this.movementMethod = ScrollingMovementMethod.getInstance(); this.visibility = View.VISIBLE; + this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START } }; view.findViewById(R.id.dialog_buttons).apply { 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 4142c4d4..0f1869c8 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -32,6 +32,7 @@ import com.futo.platformplayer.BuildConfig import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment @@ -80,6 +81,7 @@ import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StateSubscriptions import com.futo.platformplayer.stores.FragmentedStorage +import com.futo.platformplayer.stores.StringStorage import com.futo.platformplayer.stores.SubscriptionStorage import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.views.ToastView @@ -87,11 +89,14 @@ import com.futo.polycentric.core.ApiMethods import com.google.gson.JsonParser import com.google.zxing.integration.android.IntentIntegrator import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.io.File import java.io.PrintWriter @@ -101,7 +106,6 @@ import java.util.LinkedList import java.util.Queue import java.util.concurrent.ConcurrentLinkedQueue - class MainActivity : AppCompatActivity, IWithResultLauncher { //TODO: Move to dimensions @@ -109,7 +113,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { private val HEIGHT_VIDEO_MINIMIZED_DP = 60f; //Containers - lateinit var rootView : MotionLayout; + lateinit var rootView: MotionLayout; private lateinit var _overlayContainer: FrameLayout; private lateinit var _toastView: ToastView; @@ -166,11 +170,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { lateinit var _fragVideoDetail: VideoDetailFragment; //State - private val _queue : Queue> = LinkedList(); - lateinit var fragCurrent : MainFragment private set; + private val _queue: Queue> = LinkedList(); + lateinit var fragCurrent: MainFragment private set; private var _parameterCurrent: Any? = null; - var fragBeforeOverlay : MainFragment? = null; private set; + var fragBeforeOverlay: MainFragment? = null; private set; val onNavigated = Event1(); @@ -216,15 +220,15 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { Logger.e("Application", "Uncaught", excp); //Resolve invocation chains - while(excp is InvocationTargetException || excp is java.lang.RuntimeException) { + while (excp is InvocationTargetException || excp is java.lang.RuntimeException) { val before = excp; - if(excp is InvocationTargetException) + if (excp is InvocationTargetException) excp = excp.targetException ?: excp.cause ?: excp; - else if(excp is java.lang.RuntimeException) + else if (excp is java.lang.RuntimeException) excp = excp.cause ?: excp; - if(excp == before) + if (excp == before) break; } writer.write((excp.message ?: "Empty error") + "\n\n"); @@ -255,7 +259,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { setContentView(R.layout.activity_main); setNavigationBarColorAndIcons(); 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 { StatePlatform.instance.updateAvailableClients(this@MainActivity); @@ -329,10 +334,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { updateSegmentPaddings(); }; _fragVideoDetail.onTransitioning.subscribe { - if(it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED) - _fragContainerOverlay.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics); + if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED) + _fragContainerOverlay.elevation = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics); 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 { @@ -349,40 +356,39 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { _buttonIncognito.alpha = 0f; StateApp.instance.privateModeChanged.subscribe { //Messing with visibility causes some issues with layout ordering? - if(it) { + if (it) { _buttonIncognito.elevation = 99f; _buttonIncognito.alpha = 1f; - } - else { + } else { _buttonIncognito.elevation = -99f; _buttonIncognito.alpha = 0f; } } _buttonIncognito.setOnClickListener { - if(!StateApp.instance.privateMode) + if (!StateApp.instance.privateMode) 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, UIDialogs.Action("Cancel", { StateApp.instance.setPrivacyMode(true); }, UIDialogs.ActionStyle.NONE), UIDialogs.Action("Disable", { StateApp.instance.setPrivacyMode(false); - }, UIDialogs.ActionStyle.DANGEROUS)); + }, UIDialogs.ActionStyle.DANGEROUS) + ); }; _fragVideoDetail.onFullscreenChanged.subscribe { Logger.i(TAG, "onFullscreenChanged ${it}"); - if(it) { + if (it) { _buttonIncognito.elevation = -99f; _buttonIncognito.alpha = 0f; - } - else { - if(StateApp.instance.privateMode) { + } else { + if (StateApp.instance.privateMode) { _buttonIncognito.elevation = 99f; _buttonIncognito.alpha = 1f; - } - else { + } else { _buttonIncognito.elevation = -99f; _buttonIncognito.alpha = 0f; } @@ -395,7 +401,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { return@subscribe; } - if(_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) { + if (_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) { if (fragCurrent !is VideoDetailFragment) { val toPlay = StatePlayer.instance.getCurrentQueueItem(); navigate(_fragVideoDetail, toPlay); @@ -443,11 +449,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { _fragSubGroupList.topBar = _fragTopBarAdd; _fragBrowser.topBar = _fragTopBarNavigation; - + fragCurrent = _fragMainHome; 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) { return@mapNotNull null; } else { @@ -506,7 +513,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { //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) 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), { @@ -518,6 +526,64 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { _fragVideoDetail.detectWindowSize() _fragVideoDetail.updateOrientation() + + val submissionStatus = FragmentedStorage.get("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) + ) + } } /* @@ -582,39 +648,45 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { } private fun handleIntent(intent: Intent?) { - if(intent == null) + if (intent == null) return; Logger.i(TAG, "handleIntent started by " + intent.action); var targetData: String? = null; - when(intent.action) { + when (intent.action) { 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); } + Intent.ACTION_VIEW -> { targetData = intent.dataString - if(!targetData.isNullOrEmpty()) { + if (!targetData.isNullOrEmpty()) { Logger.i(TAG, "View Received: " + targetData); } } + "VIDEO" -> { val url = intent.getStringExtra("VIDEO"); navigate(_fragVideoDetail, url); } + "IMPORT_OPTIONS" -> { UIDialogs.showImportOptionsDialog(this); } + "ACTION" -> { val action = intent.getStringExtra("ACTION"); StateDeveloper.instance.testState = "TestPlayback"; StateDeveloper.instance.testPlayback(); } + "TAB" -> { - when(intent.getStringExtra("TAB")){ + when (intent.getStringExtra("TAB")) { "Sources" -> { runBlocking { StatePlatform.instance.updateAvailableClients(this@MainActivity, true) //Ideally this is not needed.. @@ -625,7 +697,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { navigate(_fragBrowser, BrowserFragment.NavigateOptions("https://plugins.grayjay.app/phone.html", mapOf( Pair("grayjay") { req -> StateApp.instance.contextOrNull?.let { - if(it is MainActivity) { + if (it is MainActivity) { runBlocking { it.handleUrlAll(req.url.toString()); } @@ -644,8 +716,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { handleUrlAll(targetData) } } - } - catch(ex: Throwable) { + } catch (ex: Throwable) { UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_handle_file), ex); } } @@ -654,35 +725,31 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { val uri = Uri.parse(url) when (uri.scheme) { "grayjay" -> { - if(url.startsWith("grayjay://license/")) { - if(StatePayment.instance.setPaymentLicenseUrl(url)) - { + 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) + if (fragCurrent is BuyFragment) closeSegment(fragCurrent); - } - else + } else 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 { data = Uri.parse(url.substring("grayjay://plugin/".length)); }; startActivity(intent); - } - else if(url.startsWith("grayjay://video/")) { + } else if (url.startsWith("grayjay://video/")) { val videoUrl = url.substring("grayjay://video/".length); navigate(_fragVideoDetail, videoUrl); - } - else if(url.startsWith("grayjay://channel/")) { + } else if (url.startsWith("grayjay://channel/")) { val channelUrl = url.substring("grayjay://channel/".length); navigate(_fragMainChannel, channelUrl); } } + "content" -> { - if(!handleContent(url, intent.type)) { + if (!handleContent(url, intent.type)) { UIDialogs.showSingleButtonDialog( this, R.drawable.ic_play, @@ -691,8 +758,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { { }); } } + "file" -> { - if(!handleFile(url)) { + if (!handleFile(url)) { UIDialogs.showSingleButtonDialog( this, R.drawable.ic_play, @@ -701,8 +769,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { { }); } } + "polycentric" -> { - if(!handlePolycentric(url)) { + if (!handlePolycentric(url)) { UIDialogs.showSingleButtonDialog( this, R.drawable.ic_play, @@ -711,8 +780,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { { }); } } + "fcast" -> { - if(!handleFCast(url)) { + if (!handleFCast(url)) { UIDialogs.showSingleButtonDialog( this, R.drawable.ic_cast, @@ -721,6 +791,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { { }); } } + else -> { if (!handleUrl(url)) { UIDialogs.showSingleButtonDialog( @@ -742,7 +813,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { if (StatePlatform.instance.hasEnabledVideoClient(url)) { Logger.i(TAG, "handleUrl(url=$url) found video client"); lifecycleScope.launch(Dispatchers.Main) { - if(position > 0) + if (position > 0) navigate(_fragVideoDetail, UrlVideoWithTime(url, position.toLong(), true)); else navigate(_fragVideoDetail, url); @@ -770,24 +841,25 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { return@withContext false; } } + fun handleContent(file: String, mime: String? = null): Boolean { Logger.i(TAG, "handleContent(url=$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); - if(!recon.trim().startsWith("[")) + if (!recon.trim().startsWith("[")) return handleUnknownJson(recon); var reconLines = Json.decodeFromString>(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 var cache: ImportCache? = null; try { - if(cacheStr != null) + if (cacheStr != null) cache = Json.decodeFromString(cacheStr); - } - catch(ex: Throwable) { + } catch (ex: Throwable) { Logger.e(TAG, "Failed to deserialize cache"); } @@ -796,32 +868,31 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}"); handleReconstruction(recon, cache); 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); 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 false; } + fun handleFile(file: String): Boolean { Logger.i(TAG, "handleFile(url=$file)"); - if(file.lowercase().endsWith(".json")) { + if (file.lowercase().endsWith(".json")) { var recon = String(readSharedFile(file)); - if(!recon.startsWith("[")) + if (!recon.startsWith("[")) return handleUnknownJson(recon); var reconLines = Json.decodeFromString>(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 var cache: ImportCache? = null; try { - if(cacheStr != null) + if (cacheStr != null) cache = Json.decodeFromString(cacheStr); - } - catch(ex: Throwable) { + } catch (ex: Throwable) { Logger.e(TAG, "Failed to deserialize cache"); } recon = reconLines.joinToString("\n"); @@ -829,19 +900,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}"); handleReconstruction(recon, cache); return true; - } - else if(file.lowercase().endsWith(".zip")) { + } else if (file.lowercase().endsWith(".zip")) { StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file)); return true; - } - else if(file.lowercase().endsWith(".txt")) { + } else if (file.lowercase().endsWith(".txt")) { return handleUnknownText(String(readSharedFile(file))); } return false; } + fun handleReconstruction(recon: String, cache: ImportCache? = null) { val type = ManagedStore.getReconstructionIdentifier(recon); - val store: ManagedStore<*> = when(type) { + val store: ManagedStore<*> = when (type) { "Playlist" -> StatePlaylists.instance.playlistStore else -> { UIDialogs.toast(getString(R.string.unknown_reconstruction_type) + " ${type}", false); @@ -849,13 +919,15 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { }; }; - val name = when(type) { - "Playlist" -> recon.split("\n").filter { !it.startsWith(ManagedStore.RECONSTRUCTION_HEADER_OPERATOR) }.firstOrNull() ?: type; + val name = when (type) { + "Playlist" -> recon.split("\n") + .filter { !it.startsWith(ManagedStore.RECONSTRUCTION_HEADER_OPERATOR) } + .firstOrNull() ?: type; else -> type } - if(!type.isNullOrEmpty()) { + if (!type.isNullOrEmpty()) { UIDialogs.showImportDialog(this, store, name, listOf(recon), cache) { } @@ -864,18 +936,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { fun handleUnknownText(text: String): Boolean { 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() }; navigate(_fragImportSubscriptions, lines); return true; } - } - catch(ex: Throwable) { + } catch (ex: Throwable) { Logger.e(TAG, ex.message, ex); UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_parse_text_file), ex); } return false; } + fun handleUnknownJson(json: String): Boolean { val context = this; @@ -887,8 +959,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { return false;//throw IllegalArgumentException("Invalid NewPipe json structure found"); StateBackup.importNewPipeSubs(this, newPipeSubsParsed); - } - catch(ex: Exception) { + } catch (ex: Exception) { Logger.e(TAG, ex.message, ex); UIDialogs.showGeneralErrorDialog(context, getString(R.string.failed_to_parse_newpipe_subscriptions), ex); } @@ -934,7 +1005,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { private fun readSharedFile(filePath: String): ByteArray { val dataFile = File(filePath); - if(!dataFile.exists()) + if (!dataFile.exists()) throw IllegalArgumentException("Opened file does not exist or not permitted"); val data = dataFile.readBytes(); return data; @@ -943,13 +1014,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { override fun onBackPressed() { Logger.i(TAG, "onBackPressed") - if(_fragBotBarMenu.onBackPressed()) + if (_fragBotBarMenu.onBackPressed()) return; - if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed()) + if (_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed()) return; - if(!fragCurrent.onBackPressed()) + if (!fragCurrent.onBackPressed()) closeSegment(); } @@ -957,7 +1028,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { super.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(); } @@ -993,12 +1064,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { 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)") - if(segment != fragCurrent) { - - if(segment is VideoDetailFragment) { - if(_fragContainerVideoDetail.visibility != View.VISIBLE) + if (segment != fragCurrent) { + + if (segment is VideoDetailFragment) { + if (_fragContainerVideoDetail.visibility != View.VISIBLE) _fragContainerVideoDetail.visibility = View.VISIBLE; - when(segment.state) { + when (segment.state) { VideoDetailFragment.State.MINIMIZED -> segment.maximizeVideoDetail() VideoDetailFragment.State.CLOSED -> segment.maximizeVideoDetail() else -> {} @@ -1006,11 +1077,10 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { segment.onShown(parameter, isBack); return; } - - + fragCurrent.onHide(); - if(segment.isMainView) { + if (segment.isMainView) { var transaction = supportFragmentManager.beginTransaction(); if (segment.topBar != null) { if (segment.topBar != fragCurrent.topBar) { @@ -1019,8 +1089,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { .replace(R.id.fragment_top_bar, segment.topBar as Fragment); fragCurrent.topBar?.onHide(); } - } - else if(fragCurrent.topBar != null) + } else if (fragCurrent.topBar != null) transaction.hide(fragCurrent.topBar as Fragment); transaction = transaction.replace(R.id.fragment_main, segment); @@ -1028,25 +1097,24 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { if (segment.hasBottomBar) { if (!fragCurrent.hasBottomBar) transaction = transaction.show(_fragBotBarMenu); - } - else { - if(fragCurrent.hasBottomBar) + } else { + if (fragCurrent.hasBottomBar) transaction = transaction.hide(_fragBotBarMenu); } transaction.commitNow(); } else { - if(!segment.hasBottomBar) { + if (!segment.hasBottomBar) { supportFragmentManager.beginTransaction() .hide(_fragBotBarMenu) .commitNow(); } } - if(fragCurrent.isHistory && withHistory && _queue.lastOrNull() != fragCurrent) + if (fragCurrent.isHistory && withHistory && _queue.lastOrNull() != fragCurrent) _queue.add(Pair(fragCurrent, _parameterCurrent)); - if(segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory) + if (segment.isOverlay && !fragCurrent.isOverlay && withHistory)// && fragCurrent.isHistory) fragBeforeOverlay = fragCurrent; @@ -1064,12 +1132,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { * If called with a non-null fragment, it will only close if the current fragment is the provided one */ fun closeSegment(fragment: MainFragment? = null) { - if(fragment is VideoDetailFragment) { + if (fragment is VideoDetailFragment) { fragment.onHide(); return; } - if((fragment?.isOverlay ?: false) && fragBeforeOverlay != null) { + if ((fragment?.isOverlay ?: false) && fragBeforeOverlay != null) { navigate(fragBeforeOverlay!!, null, false, true); } else { val last = _queue.lastOrNull(); @@ -1091,8 +1159,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { /** * Provides the fragment instance for the provided fragment class */ - inline fun getFragment() : T { - return when(T::class) { + inline fun getFragment(): T { + return when (T::class) { HomeFragment::class -> _fragMainHome as T; TutorialFragment::class -> _fragMainTutorial as T; ContentSearchResultsFragment::class -> _fragMainVideoSearchResults as T; @@ -1129,15 +1197,21 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { private fun updateSegmentPaddings() { var paddingBottom = 0f; - if(fragCurrent.hasBottomBar) + if (fragCurrent.hasBottomBar) 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; - _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() + ); } @@ -1153,14 +1227,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> { } + 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, UIDialogs.Action("Cancel", {}), UIDialogs.Action("Enable", { requestPermissionLauncher.launch(notifPermission); - }, UIDialogs.ActionStyle.PRIMARY)); + }, UIDialogs.ActionStyle.PRIMARY) + ); } + else -> { requestPermissionLauncher.launch(notifPermission); } @@ -1172,15 +1250,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { fun showAppToast(toast: ToastView.Toast) { synchronized(_toastQueue) { _toastQueue.add(toast); - if(_toastJob?.isActive != true) + if (_toastJob?.isActive != true) _toastJob = lifecycleScope.launch(Dispatchers.Default) { launchAppToastJob(); }; } } + private suspend fun launchAppToastJob() { Logger.i(TAG, "Starting appToast loop"); - while(!_toastQueue.isEmpty()) { + while (!_toastQueue.isEmpty()) { val toast = _toastQueue.poll() ?: continue; Logger.i(TAG, "Showing next toast (${toast.msg})"); @@ -1193,7 +1272,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { _toastView.setToastAnimated(toast); } } - if(toast.long) + if (toast.long) delay(5000); else delay(3000); @@ -1207,18 +1286,19 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { //TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers. - private var resultLauncherMap = mutableMapOfUnit>(); + private var resultLauncherMap = mutableMapOf Unit>(); private var requestCode: Int? = -1; private val resultLauncher: ActivityResultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult()) { - result: ActivityResult -> + ActivityResultContracts.StartActivityForResult() + ) { result: ActivityResult -> val handler = synchronized(resultLauncherMap) { resultLauncherMap.remove(requestCode); } - if(handler != null) + if (handler != null) handler(result); }; - override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit) { + + override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult) -> Unit) { synchronized(resultLauncherMap) { resultLauncherMap[code] = handler; } @@ -1229,32 +1309,34 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { companion object { 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); sourcesIntent.action = "TAB"; sourcesIntent.putExtra("TAB", tab); - sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); return sourcesIntent; } - fun getVideoIntent(context: Context, videoUrl: String) : Intent { + + fun getVideoIntent(context: Context, videoUrl: String): Intent { val sourcesIntent = Intent(context, MainActivity::class.java); sourcesIntent.action = "VIDEO"; sourcesIntent.putExtra("VIDEO", videoUrl); - sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); return sourcesIntent; } - fun getActionIntent(context: Context, action: String) : Intent { + + fun getActionIntent(context: Context, action: String): Intent { val sourcesIntent = Intent(context, MainActivity::class.java); sourcesIntent.action = "ACTION"; sourcesIntent.putExtra("ACTION", action); - sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); return sourcesIntent; } fun getImportOptionsIntent(context: Context): Intent { val sourcesIntent = Intent(context, MainActivity::class.java); sourcesIntent.action = "IMPORT_OPTIONS"; - sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); return sourcesIntent; } } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 73571123..c66dda6d 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -933,7 +933,7 @@ class VideoDetailView : ConstraintLayout { val device = devices.first(); UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non ${device.remotePublicKey}" , { fragment.lifecycleScope.launch(Dispatchers.IO) { - device.sendJson(GJSyncOpcodes.sendToDevices, SendToDevicePackage(videoToSend.url, (lastPositionMilliseconds/1000).toInt())); + device.sendJsonData(GJSyncOpcodes.sendToDevices, SendToDevicePackage(videoToSend.url, (lastPositionMilliseconds/1000).toInt())); } }) } @@ -1362,11 +1362,9 @@ class VideoDetailView : ConstraintLayout { me._playbackTracker = null; } catch (ex: Throwable) { Logger.e(TAG, "Playback tracker failed", ex); - if (me.video?.isLive == true) withContext(Dispatchers.Main) { - UIDialogs.toast( - context, - context.getString(R.string.failed_to_get_playback_tracker) - ); + + 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)); }; else withContext(Dispatchers.Main) { UIDialogs.showGeneralErrorDialog( @@ -1759,7 +1757,7 @@ class VideoDetailView : ConstraintLayout { }); else _player.setArtwork(null); - _player.setSource(videoSource, audioSource, _playWhenReady, false); + _player.setSource(videoSource, audioSource, _playWhenReady, false, resume = resumePositionMs > 0); if(subtitleSource != null) _player.swapSubtitles(fragment.lifecycleScope, subtitleSource); _player.seekTo(resumePositionMs); @@ -2915,13 +2913,15 @@ class VideoDetailView : ConstraintLayout { .exception { Logger.w(ChannelFragment.TAG, "Failed to load video.", it); - handleErrorOrCall { - _retryCount = 0; - _retryJob?.cancel(); - _retryJob = null; - _liveTryJob?.cancel(); - _liveTryJob = null; - UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo, null, fragment); + if(!(it.message?.contains("Unable to resolve host") ?: false && nextVideo())){ + handleErrorOrCall { + _retryCount = 0; + _retryJob?.cancel(); + _retryJob = null; + _liveTryJob?.cancel(); + _liveTryJob = null; + UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo, null, fragment); + } } } else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope}); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/SearchTopBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/SearchTopBarFragment.kt index 088655f9..44d8a9ad 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/SearchTopBarFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/topbar/SearchTopBarFragment.kt @@ -26,6 +26,7 @@ import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.SearchHistoryStorage class SearchTopBarFragment : TopFragment() { + @Suppress("PrivatePropertyName") private val TAG = "SearchTopBarFragment" private var _editSearch: EditText? = null; @@ -191,29 +192,32 @@ class SearchTopBarFragment : TopFragment() { } private fun onDone() { - val editSearch = _editSearch; + val editSearch = _editSearch if (editSearch != null) { - val text = editSearch.text.toString(); - if (text.length < 3) { - UIDialogs.toast(getString(R.string.please_use_at_least_3_characters)); - return; + val text = editSearch.text.toString() + if (text.isEmpty()) { + UIDialogs.toast(getString(R.string.please_use_at_least_1_character)) + return } - editSearch.clearFocus(); - _inputMethodManager?.hideSoftInputFromWindow(editSearch.windowToken, 0); + editSearch.clearFocus() + _inputMethodManager?.hideSoftInputFromWindow(editSearch.windowToken, 0) if (Settings.instance.search.searchHistory) { - val storage = FragmentedStorage.get(); - storage.add(text); + val storage = FragmentedStorage.get() + storage.add(text) } if (_searchType == SearchType.CREATOR) { - onSearch.emit(text); + onSearch.emit(text) } else { - onSearch.emit(text); + onSearch.emit(text) } } 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." + ) } } diff --git a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt index 734248b2..9d1a3faa 100644 --- a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt +++ b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt @@ -182,13 +182,14 @@ class HLS { private fun parseAttributes(content: String): Map { val attributes = mutableMapOf() - val attributePairs = content.substringAfter(":").splitToSequence(',') + val maybeAttributePairs = content.substringAfter(":").splitToSequence(',') var currentPair = StringBuilder() - for (pair in attributePairs) { + for (pair in maybeAttributePairs) { currentPair.append(pair) 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("\"") currentPair = StringBuilder() // Reset for the next attribute } else { diff --git a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt index acd92a67..cf2c032c 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt @@ -89,7 +89,7 @@ class StateHistory { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { if(StateSync.instance.hasAtLeastOneOnlineDevice()) { Logger.i(TAG, "SyncHistory playback broadcasted (${liveObj.name}: ${position})"); - StateSync.instance.broadcastJson( + StateSync.instance.broadcastJsonData( GJSyncOpcodes.syncHistory, listOf(historyVideo) ); diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt index 7fdbe432..dfddd51f 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -537,7 +537,7 @@ class StatePlatform { else getSortedEnabledClient().filter { if (it is JSClient) it.enableInSearch else true }; 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(); if (it.isChannelUrl(channelUrl)) { diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt index 2826cb91..f5a033ab 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt @@ -198,7 +198,7 @@ class StatePlaylists { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { if(StateSync.instance.hasAtLeastOneOnlineDevice()) { Logger.i(StateSubscriptionGroups.TAG, "SyncPlaylist (${playlist.name})"); - StateSync.instance.broadcastJson( + StateSync.instance.broadcastJsonData( GJSyncOpcodes.syncPlaylists, SyncPlaylistsPackage(listOf(playlist), mapOf()) ); @@ -217,7 +217,7 @@ class StatePlaylists { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { if(StateSync.instance.hasAtLeastOneOnlineDevice()) { Logger.i(StateSubscriptionGroups.TAG, "SyncPlaylist (${playlist.name})"); - StateSync.instance.broadcastJson( + StateSync.instance.broadcastJsonData( GJSyncOpcodes.syncPlaylists, SyncPlaylistsPackage(listOf(), mapOf(Pair(playlist.id, OffsetDateTime.now().toEpochSecond()))) ); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt index 2b4883da..5ca521ec 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptionGroups.kt @@ -81,7 +81,7 @@ class StateSubscriptionGroups { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { if(StateSync.instance.hasAtLeastOneOnlineDevice()) { Logger.i(TAG, "SyncSubscriptionGroup (${subGroup.name})"); - StateSync.instance.broadcastJson( + StateSync.instance.broadcastJsonData( GJSyncOpcodes.syncSubscriptionGroups, SyncSubscriptionGroupsPackage(listOf(subGroup), mapOf()) ); @@ -100,7 +100,7 @@ class StateSubscriptionGroups { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { if(StateSync.instance.hasAtLeastOneOnlineDevice()) { Logger.i(TAG, "SyncSubscriptionGroup delete (${group.name})"); - StateSync.instance.broadcastJson( + StateSync.instance.broadcastJsonData( GJSyncOpcodes.syncSubscriptionGroups, SyncSubscriptionGroupsPackage(listOf(), mapOf(Pair(id, OffsetDateTime.now().toEpochSecond()))) ); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt index df680fed..52fb9f2e 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSubscriptions.kt @@ -250,7 +250,7 @@ class StateSubscriptions { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { try { - StateSync.instance.broadcast( + StateSync.instance.broadcastData( GJSyncOpcodes.syncSubscriptions, Json.encodeToString( SyncSubscriptionsPackage( listOf(subObj), @@ -299,7 +299,7 @@ class StateSubscriptions { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { try { - StateSync.instance.broadcast( + StateSync.instance.broadcastData( GJSyncOpcodes.syncSubscriptions, Json.encodeToString( SyncSubscriptionsPackage( listOf(), diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt index b6bf0ca8..4de1b41c 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateSync.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateSync.kt @@ -370,26 +370,29 @@ class StateSync { Logger.i(TAG, "Connection authorized for ${remotePublicKey} because initiator") } }, - onData = { s, opcode, data -> - session?.handlePacket(s, opcode, data) + onData = { s, opcode, subOpcode, data -> + session?.handlePacket(s, opcode, subOpcode, data) }) } - inline fun broadcastJson(opcode: UByte, data: T) { - broadcast(opcode, Json.encodeToString(data)); + inline fun broadcastJsonData(subOpcode: UByte, data: T) { + broadcast(SyncSocketSession.Opcode.DATA.value, subOpcode, Json.encodeToString(data)); } - fun broadcast(opcode: UByte, data: String) { - broadcast(opcode, data.toByteArray(Charsets.UTF_8)); + fun broadcastData(subOpcode: UByte, data: String) { + broadcast(SyncSocketSession.Opcode.DATA.value, subOpcode, data.toByteArray(Charsets.UTF_8)); } - fun broadcast(opcode: UByte, data: ByteArray) { + fun broadcast(opcode: UByte, subOpcode: UByte, data: String) { + broadcast(opcode, subOpcode, data.toByteArray(Charsets.UTF_8)); + } + fun broadcast(opcode: UByte, subOpcode: UByte, data: ByteArray) { for(session in getSessions()) { try { if (session.isAuthorized && session.connected) { - session.send(opcode, data); + session.send(opcode, subOpcode, data); } } catch(ex: Exception) { - Logger.w(TAG, "Failed to broadcast ${opcode} to ${session.remotePublicKey}: ${ex.message}}", ex); + Logger.w(TAG, "Failed to broadcast (opcode = ${opcode}, subOpcode = ${subOpcode}) to ${session.remotePublicKey}: ${ex.message}}", ex); } } } @@ -398,7 +401,7 @@ class StateSync { val time = measureTimeMillis { //val export = StateBackup.export(); //session.send(GJSyncOpcodes.syncExport, export.asZip()); - session.send(GJSyncOpcodes.syncStateExchange, getSyncSessionDataString(session.remotePublicKey)); + session.sendData(GJSyncOpcodes.syncStateExchange, getSyncSessionDataString(session.remotePublicKey)); } Logger.i(TAG, "Generated and sent sync export in ${time}ms"); } diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSession.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSession.kt index 6c46f59d..cca42a54 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSession.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSession.kt @@ -45,6 +45,7 @@ class SyncSession : IAuthorizable { private val _onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit val remotePublicKey: String override val isAuthorized get() = _authorized && _remoteAuthorized + private var _wasAuthorized = false var connected: Boolean = false private set(v) { @@ -94,8 +95,10 @@ class SyncSession : IAuthorizable { } private fun checkAuthorized() { - if (isAuthorized) + if (!_wasAuthorized && isAuthorized) { + _wasAuthorized = true _onAuthorized.invoke(this) + } } fun removeSocketSession(socketSession: SyncSocketSession) { @@ -117,29 +120,34 @@ class SyncSession : IAuthorizable { _onClose.invoke(this) } - fun handlePacket(socketSession: SyncSocketSession, opcode: UByte, data: ByteBuffer) { - Logger.i(TAG, "Handle packet (opcode: ${opcode}, data.length: ${data.remaining()})") - - when (opcode) { - Opcode.NOTIFY_AUTHORIZED.value -> { - _remoteAuthorized = true - checkAuthorized() - } - Opcode.NOTIFY_UNAUTHORIZED.value -> { - _remoteAuthorized = false - _onUnauthorized(this) - } - //TODO: Handle any kind of packet (that is not necessarily authorized) - } - - if (!isAuthorized) { - return - } - - Logger.i(TAG, "Received ${opcode} (${data.remaining()} bytes)") - //TODO: Abstract this out + fun handlePacket(socketSession: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) { try { + Logger.i(TAG, "Handle packet (opcode: ${opcode}, subOpcode: ${subOpcode}, data.length: ${data.remaining()})") + when (opcode) { + Opcode.NOTIFY_AUTHORIZED.value -> { + _remoteAuthorized = true + checkAuthorized() + } + Opcode.NOTIFY_UNAUTHORIZED.value -> { + _remoteAuthorized = false + _onUnauthorized(this) + } + //TODO: Handle any kind of packet (that is not necessarily authorized) + } + + if (!isAuthorized) { + return + } + + if (opcode != Opcode.DATA.value) { + Logger.w(TAG, "Unknown opcode received: (opcode = ${opcode}, subOpcode = ${subOpcode})}") + return + } + + Logger.i(TAG, "Received (opcode = ${opcode}, subOpcode = ${subOpcode}) (${data.remaining()} bytes)") + //TODO: Abstract this out + when (subOpcode) { GJSyncOpcodes.sendToDevices -> { StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { val context = StateApp.instance.contextOrNull; @@ -164,13 +172,13 @@ class SyncSession : IAuthorizable { Logger.i(TAG, "Received SyncSessionData from " + remotePublicKey); - send(GJSyncOpcodes.syncSubscriptions, StateSubscriptions.instance.getSyncSubscriptionsPackageString()); - send(GJSyncOpcodes.syncSubscriptionGroups, StateSubscriptionGroups.instance.getSyncSubscriptionGroupsPackageString()); - send(GJSyncOpcodes.syncPlaylists, StatePlaylists.instance.getSyncPlaylistsPackageString()) + sendData(GJSyncOpcodes.syncSubscriptions, StateSubscriptions.instance.getSyncSubscriptionsPackageString()); + sendData(GJSyncOpcodes.syncSubscriptionGroups, StateSubscriptionGroups.instance.getSyncSubscriptionGroupsPackageString()); + sendData(GJSyncOpcodes.syncPlaylists, StatePlaylists.instance.getSyncPlaylistsPackageString()) val recentHistory = StateHistory.instance.getRecentHistory(syncSessionData.lastHistory); if(recentHistory.size > 0) - sendJson(GJSyncOpcodes.syncHistory, recentHistory); + sendJsonData(GJSyncOpcodes.syncHistory, recentHistory); } GJSyncOpcodes.syncExport -> { @@ -338,16 +346,19 @@ class SyncSession : IAuthorizable { } - inline fun sendJson(opcode: UByte, data: T) { - send(opcode, Json.encodeToString(data)); + inline fun sendJsonData(subOpcode: UByte, data: T) { + send(Opcode.DATA.value, subOpcode, Json.encodeToString(data)); } - fun send(opcode: UByte, data: String) { - send(opcode, data.toByteArray(Charsets.UTF_8)); + fun sendData(subOpcode: UByte, data: String) { + send(Opcode.DATA.value, subOpcode, data.toByteArray(Charsets.UTF_8)); } - fun send(opcode: UByte, data: ByteArray) { + fun send(opcode: UByte, subOpcode: UByte, data: String) { + send(opcode, subOpcode, data.toByteArray(Charsets.UTF_8)); + } + fun send(opcode: UByte, subOpcode: UByte, data: ByteArray) { val sock = _socketSessions.firstOrNull(); if(sock != null){ - sock.send(opcode, ByteBuffer.wrap(data)); + sock.send(opcode, subOpcode, ByteBuffer.wrap(data)); } else throw IllegalStateException("Session has no active sockets"); diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt index b7b5ab79..8b5f305a 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncSocketSession.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.sync.internal import com.futo.platformplayer.LittleEndianDataInputStream import com.futo.platformplayer.LittleEndianDataOutputStream +import com.futo.platformplayer.ensureNotMainThread import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.noise.protocol.CipherStatePair import com.futo.platformplayer.noise.protocol.DHState @@ -18,7 +19,8 @@ class SyncSocketSession { NOTIFY_UNAUTHORIZED(3u), STREAM_START(4u), STREAM_DATA(5u), - STREAM_END(6u) + STREAM_END(6u), + DATA(7u) } private val _inputStream: LittleEndianDataInputStream @@ -41,12 +43,12 @@ class SyncSocketSession { private val _localKeyPair: DHState private var _localPublicKey: String val localPublicKey: String get() = _localPublicKey - private val _onData: (session: SyncSocketSession, opcode: UByte, data: ByteBuffer) -> Unit + private val _onData: (session: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) -> Unit var authorizable: IAuthorizable? = null val remoteAddress: String - constructor(remoteAddress: String, localKeyPair: DHState, inputStream: LittleEndianDataInputStream, outputStream: LittleEndianDataOutputStream, onClose: (session: SyncSocketSession) -> Unit, onHandshakeComplete: (session: SyncSocketSession) -> Unit, onData: (session: SyncSocketSession, opcode: UByte, data: ByteBuffer) -> Unit) { + constructor(remoteAddress: String, localKeyPair: DHState, inputStream: LittleEndianDataInputStream, outputStream: LittleEndianDataOutputStream, onClose: (session: SyncSocketSession) -> Unit, onHandshakeComplete: (session: SyncSocketSession) -> Unit, onData: (session: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) -> Unit) { _inputStream = inputStream _outputStream = outputStream _onClose = onClose @@ -159,10 +161,11 @@ class SyncSocketSession { } private fun performVersionCheck() { - _outputStream.writeInt(1) + val CURRENT_VERSION = 2 + _outputStream.writeInt(CURRENT_VERSION) val version = _inputStream.readInt() Logger.i(TAG, "performVersionCheck (version = $version)") - if (version != 1) + if (version != CURRENT_VERSION) throw Exception("Invalid version") } @@ -205,8 +208,9 @@ class SyncSocketSession { throw Exception("Handshake finished without completing") } + fun send(opcode: UByte, subOpcode: UByte, data: ByteBuffer) { + ensureNotMainThread() - fun send(opcode: UByte, data: ByteBuffer) { if (data.remaining() + HEADER_SIZE > MAXIMUM_PACKET_SIZE) { val segmentSize = MAXIMUM_PACKET_SIZE - HEADER_SIZE val segmentData = ByteArray(segmentSize) @@ -223,8 +227,8 @@ class SyncSocketSession { if (sendOffset == 0) { segmentOpcode = Opcode.STREAM_START.value - bytesToSend = segmentSize - 4 - 4 - 1 - segmentPacketSize = bytesToSend + 4 + 4 + 1 + bytesToSend = segmentSize - 4 - 4 - 1 - 1 + segmentPacketSize = bytesToSend + 4 + 4 + 1 + 1 } else { bytesToSend = minOf(segmentSize - 4 - 4, bytesRemaining) segmentOpcode = if (bytesToSend >= bytesRemaining) Opcode.STREAM_END.value else Opcode.STREAM_DATA.value @@ -236,18 +240,20 @@ class SyncSocketSession { putInt(if (segmentOpcode == Opcode.STREAM_START.value) data.remaining() else sendOffset) if (segmentOpcode == Opcode.STREAM_START.value) { put(opcode.toByte()) + put(subOpcode.toByte()) } put(data.array(), data.position() + sendOffset, bytesToSend) } - send(segmentOpcode, ByteBuffer.wrap(segmentData, 0, segmentPacketSize)) + send(segmentOpcode, 0u, ByteBuffer.wrap(segmentData, 0, segmentPacketSize)) sendOffset += bytesToSend } } else { synchronized(_sendLockObject) { ByteBuffer.wrap(_sendBuffer).order(ByteOrder.LITTLE_ENDIAN).apply { - putInt(data.remaining() + 1) + putInt(data.remaining() + 2) put(opcode.toByte()) + put(subOpcode.toByte()) put(data.array(), data.position(), data.remaining()) } @@ -260,12 +266,15 @@ class SyncSocketSession { } } - fun send(opcode: UByte) { - synchronized(_sendLockObject) { - ByteBuffer.wrap(_sendBuffer, 0, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(1) - _sendBuffer.asUByteArray()[4] = opcode + fun send(opcode: UByte, subOpcode: UByte = 0u) { + ensureNotMainThread() - //Logger.i(TAG, "Encrypting message (size = ${HEADER_SIZE})") + synchronized(_sendLockObject) { + ByteBuffer.wrap(_sendBuffer, 0, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(2) + _sendBuffer.asUByteArray()[4] = opcode + _sendBuffer.asUByteArray()[5] = subOpcode + + //Logger.i(TAG, "Encrypting message (opcode = ${opcode}, subOpcode = ${subOpcode}, size = ${HEADER_SIZE})") val len = _cipherStatePair!!.sender.encryptWithAd(null, _sendBuffer, 0, _sendBufferEncrypted, 0, HEADER_SIZE) //Logger.i(TAG, "Sending encrypted message (size = ${len})") @@ -277,19 +286,19 @@ class SyncSocketSession { private fun handleData(data: ByteArray, length: Int) { if (length < HEADER_SIZE) - throw Exception("Packet must be at least 5 bytes (header size)") + throw Exception("Packet must be at least 6 bytes (header size)") val size = ByteBuffer.wrap(data, 0, 4).order(ByteOrder.LITTLE_ENDIAN).int if (size != length - 4) throw Exception("Incomplete packet received") val opcode = data.asUByteArray()[4] - val packetData = ByteBuffer.wrap(data, HEADER_SIZE, size - 1) - - handlePacket(opcode, packetData.order(ByteOrder.LITTLE_ENDIAN)) + val subOpcode = data.asUByteArray()[5] + val packetData = ByteBuffer.wrap(data, HEADER_SIZE, size - 2) + handlePacket(opcode, subOpcode, packetData.order(ByteOrder.LITTLE_ENDIAN)) } - private fun handlePacket(opcode: UByte, data: ByteBuffer) { + private fun handlePacket(opcode: UByte, subOpcode: UByte, data: ByteBuffer) { when (opcode) { Opcode.PING.value -> { send(Opcode.PONG.value) @@ -302,7 +311,7 @@ class SyncSocketSession { } Opcode.NOTIFY_AUTHORIZED.value, Opcode.NOTIFY_UNAUTHORIZED.value -> { - _onData.invoke(this, opcode, data) + _onData.invoke(this, opcode, subOpcode, data) return } } @@ -316,8 +325,9 @@ class SyncSocketSession { val id = data.int val expectedSize = data.int val op = data.get().toUByte() + val subOp = data.get().toUByte() - val syncStream = SyncStream(expectedSize, op) + val syncStream = SyncStream(expectedSize, op, subOp) if (data.remaining() > 0) { syncStream.add(data.array(), data.position(), data.remaining()) } @@ -362,10 +372,13 @@ class SyncSocketSession { throw Exception("After sync stream end, the stream must be complete") } - handlePacket(syncStream.opcode, syncStream.getBytes().let { ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN) }) + handlePacket(syncStream.opcode, syncStream.subOpcode, syncStream.getBytes().let { ByteBuffer.wrap(it).order(ByteOrder.LITTLE_ENDIAN) }) + } + Opcode.DATA.value -> { + _onData.invoke(this, opcode, subOpcode, data) } else -> { - _onData.invoke(this, opcode, data) + Logger.w(TAG, "Unknown opcode received (opcode = ${opcode}, subOpcode = ${subOpcode})") } } } @@ -374,6 +387,6 @@ class SyncSocketSession { private const val TAG = "SyncSocketSession" const val MAXIMUM_PACKET_SIZE = 65535 - 16 const val MAXIMUM_PACKET_SIZE_ENCRYPTED = MAXIMUM_PACKET_SIZE + 16 - const val HEADER_SIZE = 5 + const val HEADER_SIZE = 6 } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncStream.kt b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncStream.kt index 5a60e295..d558feef 100644 --- a/app/src/main/java/com/futo/platformplayer/sync/internal/SyncStream.kt +++ b/app/src/main/java/com/futo/platformplayer/sync/internal/SyncStream.kt @@ -1,6 +1,6 @@ package com.futo.platformplayer.sync.internal -class SyncStream(expectedSize: Int, val opcode: UByte) { +class SyncStream(expectedSize: Int, val opcode: UByte, val subOpcode: UByte) { companion object { const val MAXIMUM_SIZE = 10_000_000 } diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index 82ad7944..fcc6dc6d 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -327,8 +327,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout { return _chapters?.let { chaps -> chaps.find { pos.toDouble() / 1000 > it.timeStart && pos.toDouble() / 1000 < it.timeEnd && (toIgnore.isEmpty() || !toIgnore.contains(it)) } }; } - fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false) { - swapSources(videoSource, audioSource,false, play, keepSubtitles); + fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false, resume: Boolean = false) { + swapSources(videoSource, audioSource,resume, play, keepSubtitles); } fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean { var videoSourceUsed = videoSource; diff --git a/app/src/main/res/layout/activity_add_source.xml b/app/src/main/res/layout/activity_add_source.xml index 9adf25ed..469a4508 100644 --- a/app/src/main/res/layout/activity_add_source.xml +++ b/app/src/main/res/layout/activity_add_source.xml @@ -22,6 +22,7 @@ android:id="@+id/button_back" android:layout_width="wrap_content" android:layout_height="match_parent" + android:contentDescription="@string/cd_button_back" android:paddingRight="20dp" app:srcCompat="@drawable/ic_back_thin_white_16dp" /> diff --git a/app/src/main/res/layout/activity_add_source_options.xml b/app/src/main/res/layout/activity_add_source_options.xml index 28752940..541d489d 100644 --- a/app/src/main/res/layout/activity_add_source_options.xml +++ b/app/src/main/res/layout/activity_add_source_options.xml @@ -22,6 +22,7 @@ android:id="@+id/button_back" android:layout_width="wrap_content" android:layout_height="match_parent" + android:contentDescription="@string/cd_button_back" android:paddingRight="20dp" app:srcCompat="@drawable/ic_back_thin_white_16dp" /> diff --git a/app/src/main/res/layout/activity_dev.xml b/app/src/main/res/layout/activity_dev.xml index 1780ede6..c1caef04 100644 --- a/app/src/main/res/layout/activity_dev.xml +++ b/app/src/main/res/layout/activity_dev.xml @@ -22,6 +22,7 @@ android:id="@+id/button_back" android:layout_width="wrap_content" android:layout_height="match_parent" + android:contentDescription="@string/cd_button_back" android:paddingRight="20dp" app:srcCompat="@drawable/ic_back_thin_white_16dp" /> diff --git a/app/src/main/res/layout/activity_fcast_guide.xml b/app/src/main/res/layout/activity_fcast_guide.xml index 4d6a2b89..5ccd0fa7 100644 --- a/app/src/main/res/layout/activity_fcast_guide.xml +++ b/app/src/main/res/layout/activity_fcast_guide.xml @@ -10,6 +10,7 @@ android:id="@+id/button_back" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_back" android:padding="10dp" android:scaleType="fitCenter" app:srcCompat="@drawable/ic_back_thin_white_16dp" diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index fd35ed06..6f8fe755 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -14,6 +14,7 @@ android:id="@+id/button_close" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_close" android:scaleType="fitCenter" android:padding="10dp" app:layout_constraintLeft_toLeftOf="parent" diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 34f54a1f..1708eeb4 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -75,6 +75,7 @@ android:id="@+id/incognito_button" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_incognito_button" android:src="@drawable/ic_disabled_visible_purple" android:background="@drawable/background_button_round_black" android:scaleType="fitCenter" diff --git a/app/src/main/res/layout/activity_manage_tabs.xml b/app/src/main/res/layout/activity_manage_tabs.xml index 7a4a1e1f..986dfa11 100644 --- a/app/src/main/res/layout/activity_manage_tabs.xml +++ b/app/src/main/res/layout/activity_manage_tabs.xml @@ -21,6 +21,7 @@ android:id="@+id/button_back" android:layout_width="wrap_content" android:layout_height="match_parent" + android:contentDescription="@string/cd_button_back" android:paddingRight="20dp" app:srcCompat="@drawable/ic_back_thin_white_16dp" /> diff --git a/app/src/main/res/layout/activity_polycentric_backup.xml b/app/src/main/res/layout/activity_polycentric_backup.xml index 3f437667..e31e8584 100644 --- a/app/src/main/res/layout/activity_polycentric_backup.xml +++ b/app/src/main/res/layout/activity_polycentric_backup.xml @@ -9,6 +9,7 @@ android:id="@+id/button_back" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_back" android:padding="10dp" android:scaleType="fitCenter" app:srcCompat="@drawable/ic_back_thin_white_16dp" @@ -19,6 +20,7 @@ android:id="@+id/button_help" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:contentDescription="@string/cd_button_help" app:srcCompat="@drawable/ic_help" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/activity_polycentric_create_profile.xml b/app/src/main/res/layout/activity_polycentric_create_profile.xml index 93583944..57226b68 100644 --- a/app/src/main/res/layout/activity_polycentric_create_profile.xml +++ b/app/src/main/res/layout/activity_polycentric_create_profile.xml @@ -10,6 +10,7 @@ android:id="@+id/button_back" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_back" android:padding="10dp" android:scaleType="fitCenter" app:layout_constraintLeft_toLeftOf="parent" diff --git a/app/src/main/res/layout/activity_polycentric_home.xml b/app/src/main/res/layout/activity_polycentric_home.xml index 36e206cd..a948d54b 100644 --- a/app/src/main/res/layout/activity_polycentric_home.xml +++ b/app/src/main/res/layout/activity_polycentric_home.xml @@ -9,6 +9,7 @@ android:id="@+id/button_back" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_back" android:padding="10dp" android:scaleType="fitCenter" app:srcCompat="@drawable/ic_back_thin_white_16dp" @@ -19,6 +20,7 @@ android:id="@+id/button_help" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:contentDescription="@string/cd_button_help" app:srcCompat="@drawable/ic_help" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/activity_polycentric_import_profile.xml b/app/src/main/res/layout/activity_polycentric_import_profile.xml index 992395cf..b7013f40 100644 --- a/app/src/main/res/layout/activity_polycentric_import_profile.xml +++ b/app/src/main/res/layout/activity_polycentric_import_profile.xml @@ -9,6 +9,7 @@ android:id="@+id/button_back" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_back" android:padding="10dp" android:scaleType="fitCenter" app:srcCompat="@drawable/ic_back_thin_white_16dp" @@ -19,6 +20,7 @@ android:id="@+id/button_help" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_help" app:srcCompat="@drawable/ic_help" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/activity_polycentric_profile.xml b/app/src/main/res/layout/activity_polycentric_profile.xml index e4b81d5d..d9266fd0 100644 --- a/app/src/main/res/layout/activity_polycentric_profile.xml +++ b/app/src/main/res/layout/activity_polycentric_profile.xml @@ -10,6 +10,7 @@ android:id="@+id/button_back" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_back" android:padding="10dp" android:scaleType="fitCenter" app:srcCompat="@drawable/ic_back_thin_white_16dp" @@ -20,6 +21,7 @@ android:id="@+id/button_help" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:contentDescription="@string/cd_button_help" app:srcCompat="@drawable/ic_help" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -28,6 +30,7 @@ android:id="@+id/image_polycentric" android:layout_height="80dp" android:layout_width="80dp" + android:contentDescription="@string/cd_image_polycentric" android:scaleType="centerCrop" app:shapeAppearanceOverlay="@style/roundedCorners_40dp" app:srcCompat="@drawable/placeholder_profile" diff --git a/app/src/main/res/layout/activity_polycentric_why.xml b/app/src/main/res/layout/activity_polycentric_why.xml index 1f4a8872..63d8f19c 100644 --- a/app/src/main/res/layout/activity_polycentric_why.xml +++ b/app/src/main/res/layout/activity_polycentric_why.xml @@ -10,6 +10,7 @@ android:id="@+id/button_back" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_back" android:padding="10dp" android:scaleType="fitCenter" app:srcCompat="@drawable/ic_back_thin_white_16dp" diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index b0bf134b..0c815387 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -24,6 +24,7 @@ android:id="@+id/button_back" android:layout_width="wrap_content" android:layout_height="match_parent" + android:contentDescription="@string/cd_button_back" android:paddingRight="20dp" app:srcCompat="@drawable/ic_back_thin_white_16dp" /> diff --git a/app/src/main/res/layout/activity_sync_home.xml b/app/src/main/res/layout/activity_sync_home.xml index 6cb4872c..0f353fb4 100644 --- a/app/src/main/res/layout/activity_sync_home.xml +++ b/app/src/main/res/layout/activity_sync_home.xml @@ -10,6 +10,7 @@ android:id="@+id/button_back" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_back" android:padding="10dp" android:scaleType="fitCenter" app:srcCompat="@drawable/ic_back_thin_white_16dp" diff --git a/app/src/main/res/layout/activity_sync_pair.xml b/app/src/main/res/layout/activity_sync_pair.xml index e95f324b..e5355ecc 100644 --- a/app/src/main/res/layout/activity_sync_pair.xml +++ b/app/src/main/res/layout/activity_sync_pair.xml @@ -10,6 +10,7 @@ android:id="@+id/button_back" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_back" android:padding="10dp" android:scaleType="fitCenter" app:srcCompat="@drawable/ic_back_thin_white_16dp" diff --git a/app/src/main/res/layout/activity_sync_show_pairing_code.xml b/app/src/main/res/layout/activity_sync_show_pairing_code.xml index a7d5631a..4d92980a 100644 --- a/app/src/main/res/layout/activity_sync_show_pairing_code.xml +++ b/app/src/main/res/layout/activity_sync_show_pairing_code.xml @@ -14,6 +14,7 @@ android:id="@+id/button_back" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_back" android:padding="10dp" android:scaleType="fitCenter" app:srcCompat="@drawable/ic_back_thin_white_16dp" diff --git a/app/src/main/res/layout/dialog_automatic_backup.xml b/app/src/main/res/layout/dialog_automatic_backup.xml index 1f570c4b..bfa7c0f6 100644 --- a/app/src/main/res/layout/dialog_automatic_backup.xml +++ b/app/src/main/res/layout/dialog_automatic_backup.xml @@ -27,6 +27,7 @@ android:id="@+id/button_cancel" android:layout_width="20dp" android:layout_height="20dp" + android:contentDescription="@string/cd_button_close" app:srcCompat="@drawable/ic_close_thin" app:tint="#888888" android:layout_marginEnd="30dp" /> diff --git a/app/src/main/res/layout/dialog_casting_connect.xml b/app/src/main/res/layout/dialog_casting_connect.xml index 24777828..e76b8c65 100644 --- a/app/src/main/res/layout/dialog_casting_connect.xml +++ b/app/src/main/res/layout/dialog_casting_connect.xml @@ -97,6 +97,7 @@ android:id="@+id/button_scan_qr" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_button_scan_qr" android:scaleType="centerCrop" app:srcCompat="@drawable/ic_qr" app:tint="@color/primary" /> @@ -109,6 +110,7 @@ android:id="@+id/button_add" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_button_add" android:scaleType="centerCrop" app:srcCompat="@drawable/ic_add" app:tint="@color/primary" diff --git a/app/src/main/res/layout/dialog_casting_connected.xml b/app/src/main/res/layout/dialog_casting_connected.xml index 8b593e9d..027db929 100644 --- a/app/src/main/res/layout/dialog_casting_connected.xml +++ b/app/src/main/res/layout/dialog_casting_connected.xml @@ -58,6 +58,7 @@ android:id="@+id/image_device" android:layout_width="25dp" android:layout_height="25dp" + android:contentDescription="@string/cd_image_device" app:srcCompat="@drawable/ic_chromecast" android:scaleType="fitCenter" app:layout_constraintLeft_toLeftOf="parent" @@ -197,6 +198,7 @@ android:id="@id/button_previous" android:layout_width="60dp" android:layout_height="60dp" + android:contentDescription="@string/cd_button_previous" android:scaleType="centerCrop" android:clickable="true" android:padding="10dp" @@ -206,6 +208,7 @@ android:id="@+id/button_play" android:layout_width="60dp" android:layout_height="60dp" + android:contentDescription="@string/cd_button_play" android:padding="20dp" android:scaleType="fitCenter" android:clickable="true" @@ -215,6 +218,7 @@ android:id="@+id/button_pause" android:layout_width="60dp" android:layout_height="60dp" + android:contentDescription="@string/cd_button_pause" android:padding="10dp" android:scaleType="fitCenter" android:clickable="true" @@ -224,6 +228,7 @@ android:id="@+id/button_stop" android:layout_width="60dp" android:layout_height="60dp" + android:contentDescription="@string/cd_button_stop" android:scaleType="fitCenter" android:padding="5dp" android:clickable="true" @@ -233,6 +238,7 @@ android:id="@id/button_next" android:layout_width="60dp" android:layout_height="60dp" + android:contentDescription="@string/cd_button_next" android:clickable="true" android:scaleType="centerCrop" android:padding="10dp" diff --git a/app/src/main/res/layout/dialog_update.xml b/app/src/main/res/layout/dialog_update.xml index 051a604c..9dceccd4 100644 --- a/app/src/main/res/layout/dialog_update.xml +++ b/app/src/main/res/layout/dialog_update.xml @@ -23,6 +23,7 @@ android:id="@+id/update_spinner" android:layout_width="100dp" android:layout_height="100dp" + android:contentDescription="@string/cd_update_spinner" app:srcCompat="@drawable/ic_update_animated" /> diff --git a/app/src/main/res/layout/fragment_navigation_top_bar.xml b/app/src/main/res/layout/fragment_navigation_top_bar.xml index ab8d8f87..f7317f6a 100644 --- a/app/src/main/res/layout/fragment_navigation_top_bar.xml +++ b/app/src/main/res/layout/fragment_navigation_top_bar.xml @@ -11,6 +11,7 @@ android:id="@+id/button_back" android:layout_width="wrap_content" android:layout_height="match_parent" + android:contentDescription="@string/cd_button_back" android:paddingLeft="16dp" android:paddingRight="8dp" app:srcCompat="@drawable/ic_back_nav" /> @@ -34,6 +35,7 @@ android:id="@+id/button_cast" android:layout_width="wrap_content" android:layout_height="match_parent" + android:contentDescription="@string/cd_cast_button" android:paddingStart="4dp" android:paddingEnd="4dp" android:paddingTop="9dp" diff --git a/app/src/main/res/layout/fragment_overview_top_bar.xml b/app/src/main/res/layout/fragment_overview_top_bar.xml index c87619bf..1fe9e047 100644 --- a/app/src/main/res/layout/fragment_overview_top_bar.xml +++ b/app/src/main/res/layout/fragment_overview_top_bar.xml @@ -10,6 +10,7 @@ android:id="@+id/app_icon" android:layout_width="35dp" android:layout_height="35dp" + android:contentDescription="@string/cd_app_icon" android:layout_marginStart="16dp" android:layout_marginEnd="4dp" android:scaleType="fitCenter" @@ -37,6 +38,7 @@ android:id="@+id/button_cast" android:layout_width="wrap_content" android:layout_height="match_parent" + android:contentDescription="@string/cd_cast_button" android:paddingStart="16dp" android:paddingEnd="12dp" android:paddingTop="12dp" @@ -49,6 +51,7 @@ android:id="@+id/button_search" android:layout_width="wrap_content" android:layout_height="match_parent" + android:contentDescription="@string/cd_button_search" android:paddingStart="5dp" android:paddingEnd="12dp" android:paddingTop="11dp" diff --git a/app/src/main/res/layout/fragment_playlists.xml b/app/src/main/res/layout/fragment_playlists.xml index b86cfe68..cd2050b6 100644 --- a/app/src/main/res/layout/fragment_playlists.xml +++ b/app/src/main/res/layout/fragment_playlists.xml @@ -34,6 +34,7 @@ android:id="@+id/image_history" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:contentDescription="@string/cd_icon_history" app:srcCompat="@drawable/ic_clock_white" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -119,6 +120,7 @@ android:id="@+id/button_create_playlist" android:layout_width="35dp" android:layout_height="20dp" + android:contentDescription="@string/cd_button_create_playlist" app:srcCompat="@drawable/ic_add_white_16dp" android:paddingEnd="15dp" android:paddingStart="15dp" diff --git a/app/src/main/res/layout/fragment_remote_playlist.xml b/app/src/main/res/layout/fragment_remote_playlist.xml index bee1f714..42b38fb5 100644 --- a/app/src/main/res/layout/fragment_remote_playlist.xml +++ b/app/src/main/res/layout/fragment_remote_playlist.xml @@ -58,6 +58,7 @@ android:id="@+id/button_share" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_button_share" android:background="@drawable/background_button_round" android:gravity="center" android:layout_marginStart="5dp" diff --git a/app/src/main/res/layout/fragment_search_top_bar.xml b/app/src/main/res/layout/fragment_search_top_bar.xml index f4b64615..277eb133 100644 --- a/app/src/main/res/layout/fragment_search_top_bar.xml +++ b/app/src/main/res/layout/fragment_search_top_bar.xml @@ -13,6 +13,7 @@ android:id="@+id/button_back" android:layout_width="wrap_content" android:layout_height="match_parent" + android:contentDescription="@string/cd_button_back" android:paddingLeft="16dp" android:paddingRight="16dp" app:srcCompat="@drawable/ic_back_white_24dp" /> @@ -27,6 +28,7 @@ android:id="@+id/edit_search" android:layout_width="fill_parent" android:layout_height="wrap_content" + android:hint="Search" android:layout_weight="1" android:inputType="text" android:imeOptions="actionDone" @@ -37,6 +39,7 @@ android:id="@+id/button_clear_search" android:layout_width="wrap_content" android:layout_height="match_parent" + android:contentDescription="@string/cd_button_clear_search" android:paddingStart="18dp" android:paddingEnd="18dp" android:layout_gravity="right|center_vertical" @@ -48,6 +51,7 @@ android:id="@+id/button_filter" android:layout_width="wrap_content" android:layout_height="match_parent" + android:contentDescription="@string/cd_button_filter" android:paddingStart="8dp" android:paddingEnd="8dp" android:scaleType="fitCenter" diff --git a/app/src/main/res/layout/fragment_subscriptions_group.xml b/app/src/main/res/layout/fragment_subscriptions_group.xml index a26789cc..31c8d5fa 100644 --- a/app/src/main/res/layout/fragment_subscriptions_group.xml +++ b/app/src/main/res/layout/fragment_subscriptions_group.xml @@ -43,6 +43,7 @@ android:id="@+id/button_delete" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_delete" android:layout_marginLeft="5dp" android:layout_marginRight="0dp" android:src="@drawable/ic_trash" @@ -56,6 +57,7 @@ android:id="@+id/button_settings" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_settings" android:layout_marginLeft="5dp" android:layout_marginRight="5dp" android:src="@drawable/ic_settings" @@ -69,6 +71,7 @@ android:id="@+id/image_group" android:layout_width="110dp" android:layout_height="70dp" + android:contentDescription="@string/cd_image_group" android:adjustViewBounds="true" app:circularflow_defaultRadius="10dp" android:layout_marginLeft="30dp" @@ -90,6 +93,7 @@ + android:layout_height="27dp" + android:contentDescription="@string/cd_creator_thumbnail" /> @@ -285,6 +288,7 @@ android:id="@+id/button_share" android:layout_width="32dp" android:layout_height="32dp" + android:contentDescription="@string/cd_button_share" android:background="@drawable/background_button_round" android:gravity="center" android:layout_marginStart="5dp" diff --git a/app/src/main/res/layout/fragview_video_detail.xml b/app/src/main/res/layout/fragview_video_detail.xml index 71ad3869..d5062c06 100644 --- a/app/src/main/res/layout/fragview_video_detail.xml +++ b/app/src/main/res/layout/fragview_video_detail.xml @@ -103,6 +103,7 @@ android:id="@+id/minimize_play" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_minimize_play" android:padding="10dp" android:clickable="true" android:scaleType="fitCenter" @@ -111,6 +112,7 @@ android:id="@+id/minimize_pause" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_minimize_pause" android:padding="5dp" android:scaleType="fitCenter" android:clickable="true" @@ -119,6 +121,7 @@ android:id="@+id/minimize_close" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_minimize_close" android:padding="5dp" android:scaleType="fitCenter" android:layout_marginStart="2dp" @@ -337,7 +340,8 @@ + android:layout_height="35dp" + android:contentDescription="@string/cd_creator_thumbnail" /> diff --git a/app/src/main/res/layout/list_comment.xml b/app/src/main/res/layout/list_comment.xml index 2141bf7f..ea2c861a 100644 --- a/app/src/main/res/layout/list_comment.xml +++ b/app/src/main/res/layout/list_comment.xml @@ -100,6 +100,7 @@ android:id="@+id/image_like_icon" android:layout_width="18dp" android:layout_height="18dp" + android:contentDescription="@string/cd_image_like_icon" app:srcCompat="@drawable/ic_thumb_up" /> @@ -134,6 +136,7 @@ android:id="@+id/button_replies" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:contentDescription="@string/cd_button_replies" app:pillIcon="@drawable/ic_forum" app:pillText="55 Replies" android:layout_marginStart="15dp" /> diff --git a/app/src/main/res/layout/list_comment_with_reference.xml b/app/src/main/res/layout/list_comment_with_reference.xml index 2e828255..73d53a3f 100644 --- a/app/src/main/res/layout/list_comment_with_reference.xml +++ b/app/src/main/res/layout/list_comment_with_reference.xml @@ -91,6 +91,7 @@ android:id="@+id/button_replies" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:contentDescription="@string/cd_button_replies" app:pillIcon="@drawable/ic_forum" app:pillText="55 Replies" android:layout_marginStart="15dp" /> @@ -112,6 +113,7 @@ android:id="@+id/pill_text" android:layout_width="wrap_content" android:layout_height="match_parent" + android:contentDescription="@string/cd_button_delete" android:textColor="@color/white" android:textSize="13dp" android:gravity="center_vertical" diff --git a/app/src/main/res/layout/list_creator.xml b/app/src/main/res/layout/list_creator.xml index d14ad856..5237ccd9 100644 --- a/app/src/main/res/layout/list_creator.xml +++ b/app/src/main/res/layout/list_creator.xml @@ -56,6 +56,7 @@ android:id="@+id/button_subscribe" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:contentDescription="@string/cd_button_subscribe" android:layout_marginTop="10dp" app:layout_constraintTop_toBottomOf="@id/text_channel_metadata" app:layout_constraintLeft_toLeftOf="parent" @@ -65,6 +66,7 @@ android:id="@+id/platform_indicator" android:layout_width="24dp" android:layout_height="24dp" + android:contentDescription="@string/cd_platform_indicator" android:layout_marginTop="18dp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" diff --git a/app/src/main/res/layout/list_device.xml b/app/src/main/res/layout/list_device.xml index 874adba9..534a9f2f 100644 --- a/app/src/main/res/layout/list_device.xml +++ b/app/src/main/res/layout/list_device.xml @@ -10,6 +10,7 @@ android:id="@+id/image_device" android:layout_width="25dp" android:layout_height="25dp" + android:contentDescription="@string/cd_image_device" app:srcCompat="@drawable/ic_chromecast" android:scaleType="fitCenter" app:layout_constraintLeft_toLeftOf="parent" @@ -63,6 +64,7 @@ android:id="@+id/image_loader" android:layout_width="25dp" android:layout_height="25dp" + android:contentDescription="@string/cd_image_loader" app:srcCompat="@drawable/ic_loader_animated" android:layout_marginEnd="8dp"/> diff --git a/app/src/main/res/layout/list_donation.xml b/app/src/main/res/layout/list_donation.xml index 921cd87a..cf0a05f1 100644 --- a/app/src/main/res/layout/list_donation.xml +++ b/app/src/main/res/layout/list_donation.xml @@ -20,6 +20,7 @@ android:id="@+id/donation_author_image" android:layout_width="20dp" android:layout_height="20dp" + android:contentDescription="@string/cd_donation_author_image" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/list_history.xml b/app/src/main/res/layout/list_history.xml index ec1d943c..88a10b81 100644 --- a/app/src/main/res/layout/list_history.xml +++ b/app/src/main/res/layout/list_history.xml @@ -173,6 +173,7 @@ android:id="@+id/image_trash" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_button_delete" app:srcCompat="@drawable/ic_trash_18dp" android:scaleType="fitCenter" android:paddingTop="10dp" diff --git a/app/src/main/res/layout/list_import_subscription.xml b/app/src/main/res/layout/list_import_subscription.xml index 15424fc3..9d30ead2 100644 --- a/app/src/main/res/layout/list_import_subscription.xml +++ b/app/src/main/res/layout/list_import_subscription.xml @@ -45,7 +45,8 @@ + android:layout_height="25dp" + android:contentDescription="@string/cd_platform_indicator" /> \ No newline at end of file diff --git a/app/src/main/res/layout/list_locked_preview.xml b/app/src/main/res/layout/list_locked_preview.xml index 59cb602a..2413c98c 100644 --- a/app/src/main/res/layout/list_locked_preview.xml +++ b/app/src/main/res/layout/list_locked_preview.xml @@ -194,6 +194,7 @@ android:id="@+id/creator_thumbnail" android:layout_width="32dp" android:layout_height="32dp" + android:contentDescription="@string/cd_creator_thumbnail" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" android:layout_marginStart="10dp" @@ -272,6 +273,7 @@ android:id="@+id/thumbnail_platform" android:layout_width="25dp" android:layout_height="25dp" + android:contentDescription="@string/cd_platform_indicator" android:scaleType="centerInside" tools:src="@drawable/ic_peertube"/> diff --git a/app/src/main/res/layout/list_locked_thumbnail.xml b/app/src/main/res/layout/list_locked_thumbnail.xml index f5fe8a57..462d65d4 100644 --- a/app/src/main/res/layout/list_locked_thumbnail.xml +++ b/app/src/main/res/layout/list_locked_thumbnail.xml @@ -306,6 +306,7 @@ android:id="@+id/thumbnail_platform" android:layout_width="20dp" android:layout_height="20dp" + android:contentDescription="@string/cd_platform_indicator" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_margin="4dp" /> diff --git a/app/src/main/res/layout/list_playlist.xml b/app/src/main/res/layout/list_playlist.xml index 3a46a6be..d51cdfc5 100644 --- a/app/src/main/res/layout/list_playlist.xml +++ b/app/src/main/res/layout/list_playlist.xml @@ -14,6 +14,7 @@ android:id="@+id/image_drag_drop" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_drag_drop" app:srcCompat="@drawable/ic_dragdrop_white" android:scaleType="fitCenter" android:paddingTop="10dp" @@ -116,6 +117,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="fitXY" + android:contentDescription="@string/cd_download_indicator" app:srcCompat="@drawable/download_for_offline" /> @@ -174,6 +176,7 @@ android:id="@+id/image_trash" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_button_delete" app:srcCompat="@drawable/ic_trash_18dp" android:scaleType="fitCenter" android:paddingTop="10dp" diff --git a/app/src/main/res/layout/list_playlist_feed.xml b/app/src/main/res/layout/list_playlist_feed.xml index f2f30845..14a01d39 100644 --- a/app/src/main/res/layout/list_playlist_feed.xml +++ b/app/src/main/res/layout/list_playlist_feed.xml @@ -41,6 +41,7 @@ android:id="@+id/thumbnail_platform" android:layout_width="20dp" android:layout_height="20dp" + android:contentDescription="@string/cd_platform_indicator" android:layout_alignParentStart="true" android:layout_alignParentBottom="true" android:layout_gravity="end" diff --git a/app/src/main/res/layout/list_playlist_feed_preview.xml b/app/src/main/res/layout/list_playlist_feed_preview.xml index 6ce90101..6be094b0 100644 --- a/app/src/main/res/layout/list_playlist_feed_preview.xml +++ b/app/src/main/res/layout/list_playlist_feed_preview.xml @@ -108,6 +108,7 @@ android:id="@+id/creator_thumbnail" android:layout_width="32dp" android:layout_height="32dp" + android:contentDescription="@string/cd_creator_thumbnail" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" android:layout_marginStart="10dp" @@ -161,6 +162,7 @@ android:id="@+id/thumbnail_platform" android:layout_width="25dp" android:layout_height="25dp" + android:contentDescription="@string/cd_platform_indicator" android:scaleType="centerInside" /> diff --git a/app/src/main/res/layout/list_playlists.xml b/app/src/main/res/layout/list_playlists.xml index 52ada168..a180d134 100644 --- a/app/src/main/res/layout/list_playlists.xml +++ b/app/src/main/res/layout/list_playlists.xml @@ -72,6 +72,7 @@ android:id="@+id/button_trash" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_delete" app:srcCompat="@drawable/ic_trash" android:padding="10dp" android:scaleType="fitCenter" diff --git a/app/src/main/res/layout/list_post_preview.xml b/app/src/main/res/layout/list_post_preview.xml index e298fd88..3eb61c55 100644 --- a/app/src/main/res/layout/list_post_preview.xml +++ b/app/src/main/res/layout/list_post_preview.xml @@ -90,6 +90,7 @@ android:id="@+id/platform_indicator" android:layout_width="25dp" android:layout_height="25dp" + android:contentDescription="@string/cd_platform_indicator" android:scaleType="centerInside" android:layout_marginEnd="8dp" app:layout_constraintTop_toTopOf="@id/image_author_thumbnail" @@ -157,6 +158,7 @@ android:id="@+id/image_like_icon" android:layout_width="18dp" android:layout_height="18dp" + android:contentDescription="@string/cd_image_like_icon" app:srcCompat="@drawable/ic_thumb_up" /> @@ -202,6 +205,7 @@ android:id="@+id/image_comments" android:layout_width="18dp" android:layout_height="18dp" + android:contentDescription="@string/Replies" android:layout_marginStart="8dp" android:layout_marginTop="2dp" app:srcCompat="@drawable/ic_forum" /> diff --git a/app/src/main/res/layout/list_post_thumbnail.xml b/app/src/main/res/layout/list_post_thumbnail.xml index e16ab48f..922e0f28 100644 --- a/app/src/main/res/layout/list_post_thumbnail.xml +++ b/app/src/main/res/layout/list_post_thumbnail.xml @@ -90,6 +90,7 @@ android:id="@+id/platform_indicator" android:layout_width="20dp" android:layout_height="20dp" + android:contentDescription="@string/cd_platform_indicator" android:scaleType="centerInside" tools:src="@drawable/ic_peertube" android:layout_marginEnd="8dp" diff --git a/app/src/main/res/layout/list_source_construction.xml b/app/src/main/res/layout/list_source_construction.xml index 8b5cf0af..0da89b04 100644 --- a/app/src/main/res/layout/list_source_construction.xml +++ b/app/src/main/res/layout/list_source_construction.xml @@ -24,6 +24,7 @@ android:id="@+id/image_source" android:layout_width="35dp" android:layout_height="35dp" + android:contentDescription="@string/cd_platform_indicator" app:srcCompat="@drawable/ic_peertube" android:scaleType="fitCenter" /> diff --git a/app/src/main/res/layout/list_source_disabled.xml b/app/src/main/res/layout/list_source_disabled.xml index 4ef72985..3a6de99d 100644 --- a/app/src/main/res/layout/list_source_disabled.xml +++ b/app/src/main/res/layout/list_source_disabled.xml @@ -24,6 +24,7 @@ android:id="@+id/image_source" android:layout_width="35dp" android:layout_height="35dp" + android:contentDescription="@string/cd_platform_indicator" app:srcCompat="@drawable/ic_peertube" android:scaleType="fitCenter" /> diff --git a/app/src/main/res/layout/list_source_enabled.xml b/app/src/main/res/layout/list_source_enabled.xml index 0223971f..630b2bcc 100644 --- a/app/src/main/res/layout/list_source_enabled.xml +++ b/app/src/main/res/layout/list_source_enabled.xml @@ -15,6 +15,7 @@ android:id="@+id/image_drag_drop" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_drag_drop" app:srcCompat="@drawable/ic_dragdrop_white" android:scaleType="fitCenter" android:paddingTop="10dp" @@ -34,6 +35,7 @@ android:id="@+id/image_source" android:layout_width="35dp" android:layout_height="35dp" + android:contentDescription="@string/cd_platform_indicator" app:srcCompat="@drawable/ic_peertube" android:scaleType="fitCenter" /> diff --git a/app/src/main/res/layout/list_subscription.xml b/app/src/main/res/layout/list_subscription.xml index 6abd51c7..ffbdf4ed 100644 --- a/app/src/main/res/layout/list_subscription.xml +++ b/app/src/main/res/layout/list_subscription.xml @@ -14,6 +14,7 @@ android:id="@+id/creator_thumbnail" android:layout_width="46dp" android:layout_height="46dp" + android:contentDescription="@string/cd_creator_thumbnail" android:layout_marginStart="20dp"/> + android:layout_height="25dp" + android:contentDescription="@string/cd_platform_indicator" /> @@ -213,6 +215,7 @@ android:id="@+id/thumbnail_platform" android:layout_width="25dp" android:layout_height="25dp" + android:contentDescription="@string/cd_platform_indicator" android:scaleType="centerInside" tools:src="@drawable/ic_peertube" /> @@ -230,6 +233,7 @@ android:id="@+id/button_add_to_watch_later" android:layout_width="30dp" android:layout_height="30dp" + android:contentDescription="@string/cd_button_add_to_watch_later" android:layout_marginEnd="5dp" android:background="@drawable/edit_text_background" app:srcCompat="@drawable/ic_clock_white" /> diff --git a/app/src/main/res/layout/list_video_preview_nested.xml b/app/src/main/res/layout/list_video_preview_nested.xml index c61c37e6..d321ed81 100644 --- a/app/src/main/res/layout/list_video_preview_nested.xml +++ b/app/src/main/res/layout/list_video_preview_nested.xml @@ -40,6 +40,7 @@ android:id="@+id/thumbnail_platform_nested" android:layout_width="25dp" android:layout_height="25dp" + android:contentDescription="@string/cd_platform_indicator" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_margin="5dp" @@ -173,6 +174,7 @@ android:id="@+id/creator_thumbnail" android:layout_width="32dp" android:layout_height="32dp" + android:contentDescription="@string/cd_creator_thumbnail" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" android:layout_marginStart="10dp" @@ -242,6 +244,7 @@ @@ -250,6 +253,7 @@ android:id="@+id/thumbnail_platform" android:layout_width="25dp" android:layout_height="25dp" + android:contentDescription="@string/cd_platform_indicator" android:scaleType="centerInside" tools:src="@drawable/ic_peertube"/> @@ -267,6 +271,7 @@ android:id="@+id/button_add_to_watch_later" android:layout_width="30dp" android:layout_height="30dp" + android:contentDescription="@string/cd_button_add_to_watch_later" android:layout_marginEnd="5dp" android:background="@drawable/edit_text_background" app:srcCompat="@drawable/ic_clock_white" /> diff --git a/app/src/main/res/layout/list_video_thumbnail.xml b/app/src/main/res/layout/list_video_thumbnail.xml index 51806a95..367730cc 100644 --- a/app/src/main/res/layout/list_video_thumbnail.xml +++ b/app/src/main/res/layout/list_video_thumbnail.xml @@ -104,6 +104,7 @@ @@ -112,6 +113,7 @@ android:id="@+id/thumbnail_platform" android:layout_width="20dp" android:layout_height="20dp" + android:contentDescription="@string/cd_platform_indicator" android:layout_alignParentStart="true" android:layout_alignParentBottom="true" android:layout_gravity="end" @@ -188,6 +190,7 @@ android:id="@+id/button_add_to_watch_later" android:layout_width="wrap_content" android:layout_height="27dp" + android:contentDescription="@string/cd_button_add_to_watch_later" android:src="@drawable/ic_clock_white" android:paddingTop="7dp" android:paddingBottom="6dp" diff --git a/app/src/main/res/layout/list_video_thumbnail_nested.xml b/app/src/main/res/layout/list_video_thumbnail_nested.xml index 4ddc8a61..82018ddb 100644 --- a/app/src/main/res/layout/list_video_thumbnail_nested.xml +++ b/app/src/main/res/layout/list_video_thumbnail_nested.xml @@ -104,6 +104,7 @@ @@ -112,6 +113,7 @@ android:id="@+id/thumbnail_platform" android:layout_width="20dp" android:layout_height="20dp" + android:contentDescription="@string/cd_platform_indicator" android:layout_alignParentStart="true" android:layout_alignParentBottom="true" android:layout_gravity="end" @@ -227,6 +229,7 @@ android:id="@+id/button_add_to_watch_later" android:layout_width="wrap_content" android:layout_height="27dp" + android:contentDescription="@string/cd_button_add_to_watch_later" android:src="@drawable/ic_clock_white" android:paddingTop="7dp" android:paddingBottom="6dp" @@ -320,6 +323,7 @@ android:id="@+id/thumbnail_platform_nested" android:layout_width="20dp" android:layout_height="20dp" + android:contentDescription="@string/cd_platform_indicator" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_margin="4dp" /> diff --git a/app/src/main/res/layout/overlay_livechat.xml b/app/src/main/res/layout/overlay_livechat.xml index 9339c9a5..3e83d8e4 100644 --- a/app/src/main/res/layout/overlay_livechat.xml +++ b/app/src/main/res/layout/overlay_livechat.xml @@ -66,6 +66,7 @@ android:id="@+id/button_close" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_button_close" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" android:layout_margin="7dp" @@ -126,6 +127,7 @@ android:scaleType="fitCenter" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_creator_thumbnail" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" android:layout_margin="20dp" @@ -159,6 +161,7 @@ android:id="@+id/donation_amount" android:layout_width="match_parent" android:layout_height="wrap_content" + android:contentDescription="@string/cd_donation_amount" android:gravity="center" tools:text="$100" /> @@ -229,6 +232,7 @@ android:scaleType="fitCenter" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_creator_thumbnail" android:layout_marginEnd="10dp" android:layout_marginStart="-20dp" android:src="@drawable/placeholder_profile" /> diff --git a/app/src/main/res/layout/overlay_topbar.xml b/app/src/main/res/layout/overlay_topbar.xml index f7a9cae9..53ea310d 100644 --- a/app/src/main/res/layout/overlay_topbar.xml +++ b/app/src/main/res/layout/overlay_topbar.xml @@ -56,6 +56,7 @@ android:id="@+id/button_close" android:layout_width="40dp" android:layout_height="40dp" + android:contentDescription="@string/cd_button_close" android:padding="5dp" android:src="@drawable/ic_close" /> diff --git a/app/src/main/res/layout/thumbnail_player_ui.xml b/app/src/main/res/layout/thumbnail_player_ui.xml index a1df6ede..2b590856 100644 --- a/app/src/main/res/layout/thumbnail_player_ui.xml +++ b/app/src/main/res/layout/thumbnail_player_ui.xml @@ -25,6 +25,7 @@ android:id="@+id/thumbnail_player_unmute" android:layout_width="34dp" android:layout_height="34dp" + android:contentDescription="@string/cd_thumbnail_player_unmute" android:padding="7dp" android:layout_gravity="center_vertical" android:background="@color/transparent" diff --git a/app/src/main/res/layout/video_player_ui.xml b/app/src/main/res/layout/video_player_ui.xml index e849b942..486936a6 100644 --- a/app/src/main/res/layout/video_player_ui.xml +++ b/app/src/main/res/layout/video_player_ui.xml @@ -14,6 +14,7 @@ android:id="@+id/button_minimize" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_minimize" android:scaleType="fitCenter" android:clickable="true" android:padding="3dp" @@ -33,6 +34,7 @@ android:id="@+id/button_autoplay" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_autoplay" android:scaleType="fitCenter" android:clickable="true" android:padding="12dp" @@ -41,6 +43,7 @@ android:id="@+id/button_cast" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_cast_button" android:scaleType="fitCenter" android:clickable="true" android:padding="12dp" @@ -49,6 +52,7 @@ android:id="@+id/button_rotate_lock" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_rotate_lock" android:scaleType="fitCenter" android:clickable="true" android:padding="12dp" @@ -57,6 +61,7 @@ android:id="@+id/button_loop" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_loop" android:scaleType="fitCenter" android:clickable="true" android:padding="12dp" @@ -65,6 +70,7 @@ android:id="@+id/button_settings" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_settings" android:scaleType="fitCenter" android:clickable="true" android:padding="12dp" @@ -75,6 +81,7 @@ android:id="@+id/button_previous" android:layout_width="60dp" android:layout_height="60dp" + android:contentDescription="@string/cd_button_previous" android:scaleType="centerCrop" android:clickable="true" android:layout_marginRight="40dp" @@ -118,6 +125,7 @@ android:id="@+id/button_next" android:layout_width="60dp" android:layout_height="60dp" + android:contentDescription="@string/cd_button_next" android:clickable="true" android:scaleType="centerCrop" android:padding="5dp" @@ -131,6 +139,7 @@ android:id="@+id/button_fullscreen" android:layout_width="55dp" android:layout_height="40dp" + android:contentDescription="@string/cd_button_fullscreen" android:clickable="true" app:srcCompat="@drawable/ic_expand" app:layout_constraintRight_toRightOf="parent" diff --git a/app/src/main/res/layout/video_player_ui_fullscreen.xml b/app/src/main/res/layout/video_player_ui_fullscreen.xml index d4d93182..f291bc1c 100644 --- a/app/src/main/res/layout/video_player_ui_fullscreen.xml +++ b/app/src/main/res/layout/video_player_ui_fullscreen.xml @@ -13,6 +13,7 @@ android:id="@+id/button_minimize" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_minimize" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:scaleType="fitCenter" @@ -61,6 +62,7 @@ android:id="@+id/button_autoplay" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_autoplay" android:scaleType="fitCenter" android:clickable="true" android:padding="12dp" @@ -69,6 +71,7 @@ android:id="@+id/button_cast" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_cast_button" android:scaleType="fitCenter" android:clickable="true" android:padding="12dp" @@ -77,6 +80,7 @@ android:id="@+id/button_rotate_lock" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_rotate_lock" android:scaleType="fitCenter" android:clickable="true" android:padding="12dp" @@ -85,6 +89,7 @@ android:id="@+id/button_loop" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_loop" android:scaleType="fitCenter" android:clickable="true" android:padding="12dp" @@ -93,6 +98,7 @@ android:id="@+id/button_settings" android:layout_width="50dp" android:layout_height="50dp" + android:contentDescription="@string/cd_button_settings" android:scaleType="fitCenter" android:clickable="true" android:padding="12dp" @@ -103,6 +109,7 @@ android:id="@+id/button_previous" android:layout_width="60dp" android:layout_height="60dp" + android:contentDescription="@string/cd_button_previous" android:scaleType="centerCrop" android:clickable="true" android:layout_marginRight="40dp" @@ -146,6 +153,7 @@ android:id="@+id/button_next" android:layout_width="60dp" android:layout_height="60dp" + android:contentDescription="@string/cd_button_next" android:clickable="true" android:scaleType="centerCrop" android:padding="5dp" @@ -159,6 +167,7 @@ android:id="@+id/button_fullscreen" android:layout_width="55dp" android:layout_height="40dp" + android:contentDescription="@string/cd_button_fullscreen" android:clickable="true" app:srcCompat="@drawable/ic_expand" app:layout_constraintRight_toRightOf="parent" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2f1b02d7..fbcc108b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -727,7 +727,7 @@ Not yet available, retrying in {time}s Failed to retry for live stream This app is in development. Please submit bug reports and understand that many features are incomplete. - Please use at least 3 characters + Please use at least 1 character Are you sure you want to delete this video? Tap to open Update available! @@ -811,6 +811,58 @@ Scroll to top Disable Battery Optimization Click to go to battery optimization settings. Disabling battery optimization will prevent the OS from killing media sessions. + Contribute Personal Subscriptions List + \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. + Cast button + Incognito button + Creator thumbnail + Clear search + Search + Search icon + Back button + App icon + History icon + Create playlist + Share + Filter + Delete + Settings + Group image + Edit + Download + Close + Pause + Play + Donation amount + Replies + Like + Dislike + Subscribe + Platform indicator + Device icon + Loader + Donation author image + Edit image + Add + Download indicator + Drag and drop + Add to Watch Later + Close + Unmute + Minimize + Lock rotation + Loop + Previous + Next + Fullscreen + Autoplay + Update spinner + Play + Pause + Stop + Scan QR code + Help + Change Polycentric profile picture Recommendations Subscriptions diff --git a/app/src/stable/assets/sources/twitch b/app/src/stable/assets/sources/twitch index 58ea7722..543a727d 160000 --- a/app/src/stable/assets/sources/twitch +++ b/app/src/stable/assets/sources/twitch @@ -1 +1 @@ -Subproject commit 58ea77229dcdb5c9ce8f1bd642baf29486d0bf21 +Subproject commit 543a727d781fe5780fd0e8f20d53f6a53b285446 diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index 80c9b4d3..0ce91be2 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 80c9b4d3b48739170b40b313be930329dcc59fe4 +Subproject commit 0ce91be276681ab82d26f9471523beab6b2a0a00 diff --git a/app/src/test/java/com/futo/platformplayer/NoiseProtocolTests.kt b/app/src/test/java/com/futo/platformplayer/NoiseProtocolTests.kt index 1597fd64..33b640f9 100644 --- a/app/src/test/java/com/futo/platformplayer/NoiseProtocolTests.kt +++ b/app/src/test/java/com/futo/platformplayer/NoiseProtocolTests.kt @@ -524,8 +524,8 @@ class NoiseProtocolTest { println("Initiator handshake complete") handshakeLatch.countDown() // Handshake complete for initiator }, - onData = { session, opcode, data -> - println("Initiator received: Opcode $opcode, Data Length: ${data.remaining()}") + onData = { session, opcode, subOpcode, data -> + println("Initiator received: Opcode: $opcode, SubOpcode: $subOpcode, Data Length: ${data.remaining()}") when (data.remaining()) { randomBytesExactlyOnePacket.remaining() -> { @@ -556,8 +556,8 @@ class NoiseProtocolTest { println("Responder handshake complete") handshakeLatch.countDown() // Handshake complete for responder }, - onData = { session, opcode, data -> - println("Responder received: Opcode $opcode, Data Length: ${data.remaining()}") + onData = { session, opcode, subOpcode, data -> + println("Responder received: Opcode $opcode, SubOpcode $subOpcode, Data Length: ${data.remaining()}") when (data.remaining()) { randomBytesExactlyOnePacket.remaining() -> { @@ -590,12 +590,12 @@ class NoiseProtocolTest { responderSession.send(SyncSocketSession.Opcode.PONG.value) // Test data transfer - responderSession.send(SyncSocketSession.Opcode.NOTIFY_AUTHORIZED.value, randomBytesExactlyOnePacket) - initiatorSession.send(SyncSocketSession.Opcode.NOTIFY_AUTHORIZED.value, randomBytes) + responderSession.send(SyncSocketSession.Opcode.DATA.value, 0u, randomBytesExactlyOnePacket) + initiatorSession.send(SyncSocketSession.Opcode.DATA.value, 1u, randomBytes) // Send large data to test stream handling val start = System.currentTimeMillis() - responderSession.send(SyncSocketSession.Opcode.NOTIFY_AUTHORIZED.value, randomBytesBig) + responderSession.send(SyncSocketSession.Opcode.DATA.value, 0u, randomBytesBig) println("Sent 10MB in ${System.currentTimeMillis() - start}ms") // Wait for a brief period to simulate delay and allow communication diff --git a/app/src/unstable/assets/sources/twitch b/app/src/unstable/assets/sources/twitch index 58ea7722..543a727d 160000 --- a/app/src/unstable/assets/sources/twitch +++ b/app/src/unstable/assets/sources/twitch @@ -1 +1 @@ -Subproject commit 58ea77229dcdb5c9ce8f1bd642baf29486d0bf21 +Subproject commit 543a727d781fe5780fd0e8f20d53f6a53b285446 diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 80c9b4d3..0ce91be2 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 80c9b4d3b48739170b40b313be930329dcc59fe4 +Subproject commit 0ce91be276681ab82d26f9471523beab6b2a0a00