Compare commits

..

6 Commits

Author SHA1 Message Date
Kelvin 6a8ac0bfaa Refs 2026-02-02 18:46:01 +01:00
Kelvin 772bff6bc0 Browser package fixes, advanced settings for plugin support 2026-02-02 18:41:51 +01:00
Kelvin b6b04054b9 Clear cookies on startup & after login 2026-01-31 21:20:31 +01:00
Kelvin 1ea794459c refs 2026-01-31 19:27:57 +01:00
Kelvin c27f5e4096 Cleanup fixes, v8 locking 2026-01-31 19:23:32 +01:00
Kelvin 8469f17b4c Fix threading for callbacks from browser 2026-01-31 13:15:09 +01:00
8 changed files with 120 additions and 24 deletions
@@ -1047,8 +1047,12 @@ class Settings : FragmentedStorageFileJson() {
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 7)
var polycentricLocalCache: Boolean = true;
var showPrivacyModeDialog: Boolean = true;
fun shouldClearWebviewCookies(): Boolean {
return false;
}
}
@FormField(R.string.gesture_controls, FieldForm.GROUP, -1, 19)
@@ -235,7 +235,8 @@ class SourcePluginConfig(
val variable: String? = null,
val dependency: String? = null,
val warningDialog: String? = null,
val options: List<String>? = null
val options: List<String>? = null,
val isAdvanced: Boolean? = null
) {
val variableOrName: String get() = variable ?: name;
}
@@ -5,10 +5,12 @@ import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.collection.emptyLongSet
import com.caoccao.javet.annotations.V8Function
import com.caoccao.javet.utils.JavetResourceUtils
import com.caoccao.javet.values.reference.V8ValueFunction
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
@@ -23,11 +25,14 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.json.Json
class PackageBrowser: V8Package {
override val name: String get() = "Browser";
override val variableName: String = "browser";
private val _json = Json { };
@Transient
private var _readySemaphore: Semaphore? = null;
@Transient
@@ -64,6 +69,10 @@ class PackageBrowser: V8Package {
_readySemaphore = null;
Logger.i("PackageBrowser", "Browser loaded");
}
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
return false;
}
}
_browser?.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
@@ -89,7 +98,9 @@ class PackageBrowser: V8Package {
}
@V8Function
fun deinitialize() {
_browser?.destroy();
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
_browser?.destroy();
}
_browser = null;
}
@@ -129,7 +140,9 @@ class PackageBrowser: V8Package {
Logger.i("PackageBrowser", "Browser loading url [${url}]");
_readySemaphore = Semaphore(1, 1);
StateApp.instance.scope.launch(Dispatchers.Main) {
browser.loadUrl(url);
try {
browser.loadUrl(url);
} catch(ex: Throwable) {}
}
}
@@ -140,18 +153,27 @@ class PackageBrowser: V8Package {
if(callbackId != null && callback != null) {
synchronized(_callbacks) {
_callbacks.put(callbackId, {
funcClone?.callVoid(null, arrayOf(it));
_plugin.busy {
funcClone?.callVoid(null, arrayOf(it));
}
if (!_plugin.isStopped)
JavetResourceUtils.safeClose(funcClone);
});
}
}
StateApp.instance.scope.launch(Dispatchers.Main) {
try {
Logger.i("PackageBrowser", "Browser running JS with callback [${callbackId}]\n${(if(js.length > 200) (js.substring(0, 200) + "...") else js)})");
browser.evaluateJavascript(js, object : ValueCallback<String> {
override fun onReceiveValue(value: String?) {
Logger.i("PackageBrowser", "Browser run finished");
}
})
try {
Logger.i("PackageBrowser", "Browser running JS with callback [${callbackId}]\n${(if(js.length > 200) (js.substring(0, 200) + "...") else js)})");
browser.evaluateJavascript(js, object : ValueCallback<String> {
override fun onReceiveValue(value: String?) {
Logger.i("PackageBrowser", "Browser run finished");
}
})
}
catch(ex: Throwable) {
Logger.e("PackageBrowser", "Browser running failed: " + ex.message, ex);
}
}
catch(ex: Throwable) {
Logger.e("PackageBrowser", "Failed to invoke browser", ex);
@@ -168,12 +190,29 @@ class PackageBrowser: V8Package {
browser.evaluateJavascript(js, object : ValueCallback<String> {
override fun onReceiveValue(value: String?) {
Logger.i("PackageBrowser", "Browser run returned: " + (value ?: ""));
funcClone?.callVoid(null, arrayOf(value));
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
Logger.i("PackageBrowser", "Invoking V8 with result (${funcClone != null})");
try {
_plugin.busy {
if (value != null) {
val json = _json.decodeFromString<String>(value);
funcClone?.callVoid(null, arrayOf(json));
} else
funcClone?.callVoid(null, arrayOf((null as String?)));
}
if (!_plugin.isStopped)
JavetResourceUtils.safeClose(funcClone);
}
catch(ex: Throwable) {
Logger.e("PackageBrowser", "Browser Failed to callback: " + ex.message, ex);
}
}
}
})
}
catch(ex: Throwable) {
Logger.e("PackageBrowser", "Failed to invoke browser", ex);
Logger.e("PackageBrowser", "Browser Failed to invoke browser", ex);
}
}
}
@@ -184,8 +223,11 @@ class PackageBrowser: V8Package {
fun callback(id: String, result: String) {
Logger.i("PackageBrowser", "Browser Callback [${id}]: ${result}");
val callback = synchronized(pack._callbacks) { pack._callbacks.remove(id); };
if(callback != null)
callback.invoke(result);
if(callback != null) {
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
callback.invoke(result);
}
}
}
@JavascriptInterface
@@ -309,13 +309,14 @@ class SourceDetailFragment : MainFragment() {
BigButton(c, context.getString(R.string.logout), context.getString(R.string.sign_out_of_the_platform), R.drawable.ic_logout) {
logoutSource();
},
if(!Settings.instance.other.shouldClearWebviewCookies())
BigButton(c, "Logout without Clear", "Logout but keep the browser cookies.\nThis allows for quick re-logging.", R.drawable.ic_logout) {
logoutSource(false);
}.apply {
this.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
};
}
} else null
)
);
@@ -518,6 +519,17 @@ class SourceDetailFragment : MainFragment() {
}
Logger.e(TAG, "Failed to set plugin authentication (loginSource, loginWarning)", e)
}
finally {
if(Settings.instance.other.shouldClearWebviewCookies()) {
try {
val cookieManager: CookieManager =
CookieManager.getInstance();
cookieManager.removeAllCookies(null);
} catch (ex: Throwable) {
Logger.e(TAG, "Failed to clear cookies", ex);
}
}
}
};
}, UIDialogs.ActionStyle.PRIMARY))
}
@@ -16,6 +16,7 @@ import android.net.Uri
import android.provider.DocumentsContract
import android.util.DisplayMetrics
import android.util.Log
import android.webkit.CookieManager
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
@@ -448,6 +449,16 @@ class StateApp {
_cacheDirectory?.let { ApiMethods.initCache(it) };
}
if(Settings.instance.other.shouldClearWebviewCookies()) {
try {
val cookieManager: CookieManager =
CookieManager.getInstance();
cookieManager.removeAllCookies(null);
} catch (ex: Throwable) {
Logger.e(SourceDetailFragment.Companion.TAG, "Failed to clear cookies", ex);
}
}
Logger.i(TAG, "MainApp Starting: Initializing [ModerationsManager]");
ModerationsManager.initialize(context);
@@ -50,7 +50,7 @@ class FieldForm : LinearLayout {
}
}
fun updateSettingsVisibility(group: GroupField? = null) {
fun updateSettingsVisibility(group: GroupField? = null, allowEmptyGroups: Boolean = false) {
val settings = group?.getFields() ?: _fields;
val query = _editSearch.text.toString().lowercase();
@@ -58,7 +58,8 @@ class FieldForm : LinearLayout {
val isGroupMatch = query.isEmpty() || group?.searchContent?.lowercase()?.contains(query) == true;
for(field in settings) {
if(field is GroupField) {
updateSettingsVisibility(field);
if(!allowEmptyGroups)
updateSettingsVisibility(field);
} else if(field is View && field.descriptor != null) {
if(field.isAdvanced && !_showAdvancedSettings)
{
@@ -73,15 +74,21 @@ class FieldForm : LinearLayout {
}
}
}
else if(field is View) {
if(field.isAdvanced && !_showAdvancedSettings)
field.visibility = View.GONE;
else
field.visibility = VISIBLE;
}
}
if(group != null) {
group.visibility = if (groupVisible) View.VISIBLE else View.GONE;
}
}
fun setShowAdvancedSettings(show: Boolean) {
fun setShowAdvancedSettings(show: Boolean, allowEmptyGroups: Boolean = false) {
_showAdvancedSettings = show;
updateSettingsVisibility();
updateSettingsVisibility(null, allowEmptyGroups);
}
fun setSearchQuery(query: String) {
_editSearch.setText(query);
@@ -141,7 +148,9 @@ class FieldForm : LinearLayout {
}
fun fromPluginSettings(settings: List<SourcePluginConfig.Setting>, values: HashMap<String, String?>, groupTitle: String? = null, groupDescription: String? = null) {
_fieldsContainer.removeAllViews();
val newFields = getFieldsFromPluginSettings(context, settings, values);
val newFields = getFieldsFromPluginSettings(context, settings, values, {
setShowAdvancedSettings(it, true);
});
if (newFields.isEmpty()) {
return;
}
@@ -157,6 +166,7 @@ class FieldForm : LinearLayout {
_fieldsContainer.addView(v);
}
_fields = newFields.map { it.second };
updateSettingsVisibility(null, true);
} else {
for(field in newFields) {
finalizePluginSettingField(field.first, field.second, newFields);
@@ -164,6 +174,8 @@ class FieldForm : LinearLayout {
val group = GroupField(context, groupTitle, groupDescription)
.withFields(newFields.map { it.second });
_fieldsContainer.addView(group as View);
_fields = newFields.map { it.second };
updateSettingsVisibility(null, true);
}
}
private fun finalizePluginSettingField(setting: SourcePluginConfig.Setting, field: IField, others: List<Pair<SourcePluginConfig.Setting, IField>>) {
@@ -234,7 +246,7 @@ class FieldForm : LinearLayout {
private val _json = Json;
fun getFieldsFromPluginSettings(context: Context, settings: List<SourcePluginConfig.Setting>, values: HashMap<String, String?>): List<Pair<SourcePluginConfig.Setting, IField>> {
fun getFieldsFromPluginSettings(context: Context, settings: List<SourcePluginConfig.Setting>, values: HashMap<String, String?>, onAdvancedChanged: ((newVal: Boolean)->Unit)? = null): List<Pair<SourcePluginConfig.Setting, IField>> {
val fields = mutableListOf<Pair<SourcePluginConfig.Setting, IField>>()
for(setting in settings) {
@@ -243,6 +255,7 @@ class FieldForm : LinearLayout {
val field = when(setting.type.lowercase()) {
"header" -> {
val groupField = GroupField(context, setting.name, setting.description);
groupField.isAdvanced = (setting.isAdvanced ?: false);
groupField;
}
"boolean" -> {
@@ -252,6 +265,7 @@ class FieldForm : LinearLayout {
field.onChanged.subscribe { _, v, _ ->
values[setting.variableOrName] = _json.encodeToString (v == 1 || v == true);
}
field.isAdvanced = (setting.isAdvanced ?: false);
field;
}
"dropdown" -> {
@@ -261,6 +275,7 @@ class FieldForm : LinearLayout {
field.onChanged.subscribe { _, v, _ ->
values[setting.variableOrName] = v.toString();
}
field.isAdvanced = (setting.isAdvanced ?: false);
field;
}
else null;
@@ -272,6 +287,17 @@ class FieldForm : LinearLayout {
fields.add(Pair(setting, field));
}
}
if(onAdvancedChanged != null && settings.any { it.isAdvanced == true }) {
val setting = SourcePluginConfig.Setting("Show Advanced", "See advanced settings, which may be counter productive to change", "boolean", "false");
val field = ToggleField(context).withValue(setting.name, setting.description, false);
field.onChanged.subscribe { field, new, old ->
onAdvancedChanged?.invoke(new as Boolean);
}
fields.add(Pair(setting, field));
}
return fields;
}