Added fallback webview to login pages and made account selectable on TV

This commit is contained in:
Blatzar 2022-09-17 13:03:41 +02:00
parent 9402a28041
commit 0f492c2c82
11 changed files with 143 additions and 96 deletions

View file

@ -7,10 +7,12 @@ import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.google.auto.service.AutoService import com.google.auto.service.AutoService
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKey
@ -74,19 +76,28 @@ class CustomSenderFactory : ReportSenderFactory {
} }
} }
class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.UncaughtExceptionHandler { class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread, error: Throwable) { override fun uncaughtException(thread: Thread, error: Throwable) {
ACRA.errorReporter.handleException(error) ACRA.errorReporter.handleException(error)
try { try {
PrintStream(errorFile).use { ps -> PrintStream(errorFile).use { ps ->
ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")) ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}"))
ps.println(String.format("Fatal exception on thread %s (%d)", thread.name, thread.id)) ps.println(
String.format(
"Fatal exception on thread %s (%d)",
thread.name,
thread.id
)
)
error.printStackTrace(ps) error.printStackTrace(ps)
} }
} catch (ignored: FileNotFoundException) { } } catch (ignored: FileNotFoundException) {
}
try { try {
onError.invoke() onError.invoke()
} catch (ignored: Exception) { } } catch (ignored: Exception) {
}
exitProcess(1) exitProcess(1)
} }
@ -95,7 +106,7 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.U
class AcraApplication : Application() { class AcraApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")){ Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component)) startActivity(Intent.makeRestartActivityTask(intent!!.component))
}) })
@ -183,5 +194,15 @@ class AcraApplication : Application() {
fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) { fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) {
context?.openBrowser(url, fallbackWebview, fragment) context?.openBrowser(url, fallbackWebview, fragment)
} }
/** Will fallback to webview if in TV layout */
fun openBrowser(url: String, activity: FragmentActivity?) {
openBrowser(
url,
isTvSettings(),
activity?.supportFragmentManager?.fragments?.lastOrNull()
)
}
} }
} }

View file

@ -40,6 +40,7 @@ object APIHolder {
private const val defProvider = 0 private const val defProvider = 0
// ConcurrentModificationException is possible!!!
val allProviders: MutableList<MainAPI> = arrayListOf() val allProviders: MutableList<MainAPI> = arrayListOf()
fun initAll() { fun initAll() {

View file

@ -15,6 +15,7 @@ import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
@ -144,6 +145,68 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val mainPluginsLoadedEvent = val mainPluginsLoadedEvent =
Event<Boolean>() // homepage api, used to speed up time to load for homepage Event<Boolean>() // homepage api, used to speed up time to load for homepage
val afterRepositoryLoadedEvent = Event<Boolean>() val afterRepositoryLoadedEvent = Event<Boolean>()
/**
* @return true if the str has launched an app task (be it successful or not)
* @param isWebview does not handle providers and opening download page if true. Can still add repos and login.
* */
fun handleAppIntentUrl(activity: FragmentActivity?, str: String?, isWebview: Boolean): Boolean =
with(activity) {
if (str != null && this != null) {
if (str.startsWith("https://cs.repo")) {
val realUrl = "https://" + str.substringAfter("?")
println("Repository url: $realUrl")
loadRepository(realUrl)
return true
} else if (str.contains(appString)) {
for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) {
ioSafe {
Log.i(TAG, "handleAppIntent $str")
val isSuccessful = api.handleRedirect(str)
if (isSuccessful) {
Log.i(TAG, "authenticated ${api.name}")
} else {
Log.i(TAG, "failed to authenticate ${api.name}")
}
this@with.runOnUiThread {
try {
showToast(
this@with,
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
api.name
)
)
} catch (e: Exception) {
logError(e) // format might fail
}
}
}
return true
}
}
} else if (URI(str).scheme == appStringRepo) {
val url = str.replaceFirst(appStringRepo, "https")
loadRepository(url)
return true
} else if (!isWebview){
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads)
return true
} else {
for (api in apis) {
if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name)
return true
}
}
}
}
}
return false
}
} }
override fun onColorSelected(dialogId: Int, color: Int) { override fun onColorSelected(dialogId: Int, color: Int) {
@ -348,56 +411,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (intent == null) return if (intent == null) return
val str = intent.dataString val str = intent.dataString
loadCache() loadCache()
if (str != null) { handleAppIntentUrl(this, str, false)
if (str.startsWith("https://cs.repo")) {
val realUrl = "https://" + str.substringAfter("?")
println("Repository url: $realUrl")
loadRepository(realUrl)
} else if (str.contains(appString)) {
for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) {
val activity = this
ioSafe {
Log.i(TAG, "handleAppIntent $str")
val isSuccessful = api.handleRedirect(str)
if (isSuccessful) {
Log.i(TAG, "authenticated ${api.name}")
} else {
Log.i(TAG, "failed to authenticate ${api.name}")
}
activity.runOnUiThread {
try {
showToast(
activity,
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
api.name
)
)
} catch (e: Exception) {
logError(e) // format might fail
}
}
}
}
}
} else if (URI(str).scheme == appStringRepo) {
val url = str.replaceFirst(appStringRepo, "https")
loadRepository(url)
} else {
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads)
} else {
for (api in apis) {
if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name)
break
}
}
}
}
}
} }
private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean = private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
@ -445,7 +459,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} }
// it.hashCode() is not enough to make sure they are distinct // it.hashCode() is not enough to make sure they are distinct
apis = allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name } apis =
allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
APIHolder.apiMap = null APIHolder.apiMap = null
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
@ -467,7 +482,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
val settingsForProvider = SettingsJson() val settingsForProvider = SettingsJson()
settingsForProvider.enableAdult = settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false) settingsForProvider.enableAdult =
settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false)
MainAPI.settingsForProvider = settingsForProvider MainAPI.settingsForProvider = settingsForProvider
@ -501,7 +517,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
ioSafe { ioSafe {
if (settingsManager.getBoolean(getString(R.string.auto_update_plugins_key), true)) { if (settingsManager.getBoolean(
getString(R.string.auto_update_plugins_key),
true
)
) {
PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity) PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
} else { } else {
PluginManager.loadAllOnlinePlugins(this@MainActivity) PluginManager.loadAllOnlinePlugins(this@MainActivity)

View file

@ -1,9 +1,11 @@
package com.lagradost.cloudstream3.syncproviders package com.lagradost.cloudstream3.syncproviders
import androidx.fragment.app.FragmentActivity
interface OAuth2API : AuthAPI { interface OAuth2API : AuthAPI {
val key: String val key: String
val redirectUrl: String val redirectUrl: String
suspend fun handleRedirect(url: String) : Boolean suspend fun handleRedirect(url: String) : Boolean
fun authenticate() fun authenticate(activity: FragmentActivity?)
} }

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.json.JsonMapper
@ -48,9 +49,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
removeAccountKeys() removeAccountKeys()
} }
override fun authenticate() { override fun authenticate(activity: FragmentActivity?) {
val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token" val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token"
openBrowser(request) openBrowser(request, activity)
} }
override suspend fun handleRedirect(url: String): Boolean { override suspend fun handleRedirect(url: String): Boolean {

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.syncproviders.OAuth2API
@ -15,7 +16,7 @@ class Dropbox : OAuth2API {
override val icon: Int override val icon: Int
get() = TODO("Not yet implemented") get() = TODO("Not yet implemented")
override fun authenticate() { override fun authenticate(activity: FragmentActivity?) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Base64 import android.util.Base64
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
@ -281,7 +282,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return false return false
} }
override fun authenticate() { override fun authenticate(activity: FragmentActivity?) {
// It is recommended to use a URL-safe string as code_verifier. // It is recommended to use a URL-safe string as code_verifier.
// See section 4 of RFC 7636 for more details. // See section 4 of RFC 7636 for more details.
@ -294,7 +295,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val codeChallenge = codeVerifier val codeChallenge = codeVerifier
val request = val request =
"$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId" "$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId"
openBrowser(request) openBrowser(request, activity)
} }
private var requestId = 0 private var requestId = 0

View file

@ -11,12 +11,12 @@ import android.webkit.WebViewClient
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.network.WebViewResolver
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import kotlinx.android.synthetic.main.fragment_webview.* import kotlinx.android.synthetic.main.fragment_webview.*
import java.net.URI
class WebviewFragment : Fragment() { class WebviewFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -31,16 +31,8 @@ class WebviewFragment : Fragment() {
request: WebResourceRequest? request: WebResourceRequest?
): Boolean { ): Boolean {
val requestUrl = request?.url.toString() val requestUrl = request?.url.toString()
val repoUrl = if (requestUrl.startsWith("https://cs.repo")) { val performedAction = MainActivity.handleAppIntentUrl(activity, requestUrl, true)
"https://" + requestUrl.substringAfter("?") if (performedAction) {
} else if (URI(requestUrl).scheme == appStringRepo) {
requestUrl.replaceFirst(appStringRepo, "https")
} else {
null
}
if (repoUrl != null) {
activity?.loadRepository(repoUrl)
findNavController().popBackStack() findNavController().popBackStack()
return true return true
} }
@ -50,6 +42,7 @@ class WebviewFragment : Fragment() {
} }
web_view.addJavascriptInterface(RepoApi(activity), "RepoApi") web_view.addJavascriptInterface(RepoApi(activity), "RepoApi")
web_view.settings.javaScriptEnabled = true web_view.settings.javaScriptEnabled = true
web_view.settings.userAgentString = USER_AGENT
web_view.settings.domStorageEnabled = true web_view.settings.domStorageEnabled = true
WebViewResolver.webViewUserAgent = web_view.settings.userAgentString WebViewResolver.webViewUserAgent = web_view.settings.userAgentString

View file

@ -1,8 +1,5 @@
package com.lagradost.cloudstream3.ui.settings package com.lagradost.cloudstream3.ui.settings
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.View.* import android.view.View.*
@ -12,8 +9,10 @@ import androidx.annotation.UiThread
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -39,7 +38,11 @@ import kotlinx.android.synthetic.main.add_account_input.*
class SettingsAccount : PreferenceFragmentCompat() { class SettingsAccount : PreferenceFragmentCompat() {
companion object { companion object {
/** Used by nginx plugin too */ /** Used by nginx plugin too */
fun showLoginInfo(activity: Activity?, api: AccountManager, info: AuthAPI.LoginInfo) { fun showLoginInfo(
activity: FragmentActivity?,
api: AccountManager,
info: AuthAPI.LoginInfo
) {
val builder = val builder =
AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom) AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom)
.setView(R.layout.account_managment) .setView(R.layout.account_managment)
@ -62,9 +65,13 @@ class SettingsAccount : PreferenceFragmentCompat() {
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
showAccountSwitch(activity, api) showAccountSwitch(activity, api)
} }
if (isTvSettings()) {
dialog.account_switch_account?.requestFocus()
}
} }
fun showAccountSwitch(activity: Activity, api: AccountManager) { fun showAccountSwitch(activity: FragmentActivity, api: AccountManager) {
val accounts = api.getAccounts() ?: return val accounts = api.getAccounts() ?: return
val builder = val builder =
@ -98,11 +105,11 @@ class SettingsAccount : PreferenceFragmentCompat() {
} }
@UiThread @UiThread
fun addAccount(activity: Activity?, api: AccountManager) { fun addAccount(activity: FragmentActivity?, api: AccountManager) {
try { try {
when (api) { when (api) {
is OAuth2API -> { is OAuth2API -> {
api.authenticate() api.authenticate(activity)
} }
is InAppAuthAPI -> { is InAppAuthAPI -> {
val builder = val builder =
@ -144,13 +151,11 @@ class SettingsAccount : PreferenceFragmentCompat() {
dialog.login_username_input?.isVisible = api.requiresUsername dialog.login_username_input?.isVisible = api.requiresUsername
dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank() dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank()
dialog.create_account?.setOnClickListener { dialog.create_account?.setOnClickListener {
val i = Intent(Intent.ACTION_VIEW) openBrowser(
i.data = Uri.parse(api.createAccountUrl) api.createAccountUrl ?: return@setOnClickListener,
try { activity
activity.startActivity(i) )
} catch (e: Exception) { dialog.dismissSafe()
logError(e)
}
} }
dialog.text1?.text = api.name dialog.text1?.text = api.name
@ -181,7 +186,8 @@ class SettingsAccount : PreferenceFragmentCompat() {
try { try {
showToast( showToast(
activity, activity,
activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format( activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail)
.format(
api.name api.name
) )
) )

View file

@ -31,6 +31,7 @@ import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.text.toSpanned import androidx.core.text.toSpanned
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -415,7 +416,7 @@ object AppUtils {
} }
} }
fun AppCompatActivity.loadResult( fun FragmentActivity.loadResult(
url: String, url: String,
apiName: String, apiName: String,
startAction: Int = 0, startAction: Int = 0,

View file

@ -101,7 +101,7 @@
<TextView <TextView
android:id="@+id/settings_extensions" android:id="@+id/settings_extensions"
style="@style/SettingsItem" style="@style/SettingsItem"
android:nextFocusUp="@id/settings_updates" android:nextFocusUp="@id/settings_credits"
android:text="@string/extensions" /> android:text="@string/extensions" />
<LinearLayout <LinearLayout