mirror of
				https://github.com/recloudstream/cloudstream.git
				synced 2024-08-15 01:53:11 +00:00 
			
		
		
		
	Added fallback webview to login pages and made account selectable on TV
This commit is contained in:
		
							parent
							
								
									9402a28041
								
							
						
					
					
						commit
						0f492c2c82
					
				
					 11 changed files with 143 additions and 96 deletions
				
			
		|  | @ -7,10 +7,12 @@ import android.content.ContextWrapper | |||
| import android.content.Intent | ||||
| import android.widget.Toast | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import com.google.auto.service.AutoService | ||||
| import com.lagradost.cloudstream3.mvvm.normalSafeApiCall | ||||
| import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall | ||||
| 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.Coroutines.runOnMainThread | ||||
| 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) { | ||||
|         ACRA.errorReporter.handleException(error) | ||||
|         try { | ||||
|             PrintStream(errorFile).use { ps -> | ||||
|                 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) | ||||
|             } | ||||
|         } catch (ignored: FileNotFoundException) { } | ||||
|         } catch (ignored: FileNotFoundException) { | ||||
|         } | ||||
|         try { | ||||
|             onError.invoke() | ||||
|         } catch (ignored: Exception) { } | ||||
|         } catch (ignored: Exception) { | ||||
|         } | ||||
|         exitProcess(1) | ||||
|     } | ||||
| 
 | ||||
|  | @ -95,7 +106,7 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.U | |||
| class AcraApplication : Application() { | ||||
|     override fun onCreate() { | ||||
|         super.onCreate() | ||||
|         Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")){ | ||||
|         Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) { | ||||
|             val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) | ||||
|             startActivity(Intent.makeRestartActivityTask(intent!!.component)) | ||||
|         }) | ||||
|  | @ -183,5 +194,15 @@ class AcraApplication : Application() { | |||
|         fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) { | ||||
|             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() | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -40,6 +40,7 @@ object APIHolder { | |||
| 
 | ||||
|     private const val defProvider = 0 | ||||
| 
 | ||||
|     // ConcurrentModificationException is possible!!! | ||||
|     val allProviders: MutableList<MainAPI> = arrayListOf() | ||||
| 
 | ||||
|     fun initAll() { | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ import androidx.annotation.IdRes | |||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import androidx.navigation.NavController | ||||
| import androidx.navigation.NavDestination | ||||
| import androidx.navigation.NavDestination.Companion.hierarchy | ||||
|  | @ -144,6 +145,68 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | |||
|         val mainPluginsLoadedEvent = | ||||
|             Event<Boolean>() // homepage api, used to speed up time to load for homepage | ||||
|         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) { | ||||
|  | @ -348,56 +411,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | |||
|         if (intent == null) return | ||||
|         val str = intent.dataString | ||||
|         loadCache() | ||||
|         if (str != null) { | ||||
|             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 | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         handleAppIntentUrl(this, str, false) | ||||
|     } | ||||
| 
 | ||||
|     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 | ||||
|                     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 | ||||
|                 } catch (e: Exception) { | ||||
|                     logError(e) | ||||
|  | @ -465,9 +480,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | |||
|             lastError = errorFile.readText(Charset.defaultCharset()) | ||||
|             errorFile.delete() | ||||
|         } | ||||
|          | ||||
| 
 | ||||
|         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 | ||||
| 
 | ||||
|  | @ -501,7 +517,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | |||
|                 } | ||||
| 
 | ||||
|                 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) | ||||
|                     } else { | ||||
|                         PluginManager.loadAllOnlinePlugins(this@MainActivity) | ||||
|  |  | |||
|  | @ -1,9 +1,11 @@ | |||
| package com.lagradost.cloudstream3.syncproviders | ||||
| 
 | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| 
 | ||||
| interface OAuth2API : AuthAPI { | ||||
|     val key: String | ||||
|     val redirectUrl: String | ||||
| 
 | ||||
|     suspend fun handleRedirect(url: String) : Boolean | ||||
|     fun authenticate() | ||||
|     fun authenticate(activity: FragmentActivity?) | ||||
| } | ||||
|  | @ -1,5 +1,6 @@ | |||
| package com.lagradost.cloudstream3.syncproviders.providers | ||||
| 
 | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import com.fasterxml.jackson.annotation.JsonProperty | ||||
| import com.fasterxml.jackson.databind.DeserializationFeature | ||||
| import com.fasterxml.jackson.databind.json.JsonMapper | ||||
|  | @ -48,9 +49,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         removeAccountKeys() | ||||
|     } | ||||
| 
 | ||||
|     override fun authenticate() { | ||||
|     override fun authenticate(activity: FragmentActivity?) { | ||||
|         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 { | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| package com.lagradost.cloudstream3.syncproviders.providers | ||||
| 
 | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import com.lagradost.cloudstream3.syncproviders.AuthAPI | ||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API | ||||
| 
 | ||||
|  | @ -15,7 +16,7 @@ class Dropbox : OAuth2API { | |||
|     override val icon: Int | ||||
|         get() = TODO("Not yet implemented") | ||||
| 
 | ||||
|     override fun authenticate() { | ||||
|     override fun authenticate(activity: FragmentActivity?) { | ||||
|         TODO("Not yet implemented") | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| package com.lagradost.cloudstream3.syncproviders.providers | ||||
| 
 | ||||
| import android.util.Base64 | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import com.fasterxml.jackson.annotation.JsonProperty | ||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.getKey | ||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser | ||||
|  | @ -281,7 +282,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     override fun authenticate() { | ||||
|     override fun authenticate(activity: FragmentActivity?) { | ||||
|         // It is recommended to use a URL-safe string as code_verifier. | ||||
|         // See section 4 of RFC 7636 for more details. | ||||
| 
 | ||||
|  | @ -294,7 +295,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         val codeChallenge = codeVerifier | ||||
|         val request = | ||||
|             "$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 | ||||
|  |  | |||
|  | @ -11,12 +11,12 @@ import android.webkit.WebViewClient | |||
| import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import androidx.navigation.fragment.findNavController | ||||
| import com.lagradost.cloudstream3.MainActivity | ||||
| import com.lagradost.cloudstream3.R | ||||
| import com.lagradost.cloudstream3.USER_AGENT | ||||
| import com.lagradost.cloudstream3.network.WebViewResolver | ||||
| import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.loadRepository | ||||
| import kotlinx.android.synthetic.main.fragment_webview.* | ||||
| import java.net.URI | ||||
| 
 | ||||
| class WebviewFragment : Fragment() { | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|  | @ -31,16 +31,8 @@ class WebviewFragment : Fragment() { | |||
|                 request: WebResourceRequest? | ||||
|             ): Boolean { | ||||
|                 val requestUrl = request?.url.toString() | ||||
|                 val repoUrl = if (requestUrl.startsWith("https://cs.repo")) { | ||||
|                     "https://" + requestUrl.substringAfter("?") | ||||
|                 } else if (URI(requestUrl).scheme == appStringRepo) { | ||||
|                     requestUrl.replaceFirst(appStringRepo, "https") | ||||
|                 } else { | ||||
|                     null | ||||
|                 } | ||||
| 
 | ||||
|                 if (repoUrl != null) { | ||||
|                     activity?.loadRepository(repoUrl) | ||||
|                 val performedAction = MainActivity.handleAppIntentUrl(activity, requestUrl, true) | ||||
|                 if (performedAction) { | ||||
|                     findNavController().popBackStack() | ||||
|                     return true | ||||
|                 } | ||||
|  | @ -50,6 +42,7 @@ class WebviewFragment : Fragment() { | |||
|         } | ||||
|         web_view.addJavascriptInterface(RepoApi(activity), "RepoApi") | ||||
|         web_view.settings.javaScriptEnabled = true | ||||
|         web_view.settings.userAgentString = USER_AGENT | ||||
|         web_view.settings.domStorageEnabled = true | ||||
| 
 | ||||
|         WebViewResolver.webViewUserAgent = web_view.settings.userAgentString | ||||
|  |  | |||
|  | @ -1,8 +1,5 @@ | |||
| package com.lagradost.cloudstream3.ui.settings | ||||
| 
 | ||||
| import android.app.Activity | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import android.view.View | ||||
| import android.view.View.* | ||||
|  | @ -12,8 +9,10 @@ import androidx.annotation.UiThread | |||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.core.view.isGone | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import androidx.preference.PreferenceFragmentCompat | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser | ||||
| import com.lagradost.cloudstream3.CommonActivity.showToast | ||||
| import com.lagradost.cloudstream3.R | ||||
| import com.lagradost.cloudstream3.mvvm.logError | ||||
|  | @ -39,7 +38,11 @@ import kotlinx.android.synthetic.main.add_account_input.* | |||
| class SettingsAccount : PreferenceFragmentCompat() { | ||||
|     companion object { | ||||
|         /** 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 = | ||||
|                 AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom) | ||||
|                     .setView(R.layout.account_managment) | ||||
|  | @ -62,9 +65,13 @@ class SettingsAccount : PreferenceFragmentCompat() { | |||
|                 dialog.dismissSafe(activity) | ||||
|                 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 builder = | ||||
|  | @ -98,11 +105,11 @@ class SettingsAccount : PreferenceFragmentCompat() { | |||
|         } | ||||
| 
 | ||||
|         @UiThread | ||||
|         fun addAccount(activity: Activity?, api: AccountManager) { | ||||
|         fun addAccount(activity: FragmentActivity?, api: AccountManager) { | ||||
|             try { | ||||
|                 when (api) { | ||||
|                     is OAuth2API -> { | ||||
|                         api.authenticate() | ||||
|                         api.authenticate(activity) | ||||
|                     } | ||||
|                     is InAppAuthAPI -> { | ||||
|                         val builder = | ||||
|  | @ -144,13 +151,11 @@ class SettingsAccount : PreferenceFragmentCompat() { | |||
|                         dialog.login_username_input?.isVisible = api.requiresUsername | ||||
|                         dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank() | ||||
|                         dialog.create_account?.setOnClickListener { | ||||
|                             val i = Intent(Intent.ACTION_VIEW) | ||||
|                             i.data = Uri.parse(api.createAccountUrl) | ||||
|                             try { | ||||
|                                 activity.startActivity(i) | ||||
|                             } catch (e: Exception) { | ||||
|                                 logError(e) | ||||
|                             } | ||||
|                             openBrowser( | ||||
|                                 api.createAccountUrl ?: return@setOnClickListener, | ||||
|                                 activity | ||||
|                             ) | ||||
|                             dialog.dismissSafe() | ||||
|                         } | ||||
|                         dialog.text1?.text = api.name | ||||
| 
 | ||||
|  | @ -181,9 +186,10 @@ class SettingsAccount : PreferenceFragmentCompat() { | |||
|                                     try { | ||||
|                                         showToast( | ||||
|                                             activity, | ||||
|                                             activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format( | ||||
|                                                 api.name | ||||
|                                             ) | ||||
|                                             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 | ||||
|  |  | |||
|  | @ -31,6 +31,7 @@ import androidx.core.content.ContextCompat | |||
| import androidx.core.text.HtmlCompat | ||||
| import androidx.core.text.toSpanned | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import androidx.navigation.fragment.findNavController | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
|  | @ -415,7 +416,7 @@ object AppUtils { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun AppCompatActivity.loadResult( | ||||
|     fun FragmentActivity.loadResult( | ||||
|         url: String, | ||||
|         apiName: String, | ||||
|         startAction: Int = 0, | ||||
|  |  | |||
|  | @ -101,7 +101,7 @@ | |||
|             <TextView | ||||
|                 android:id="@+id/settings_extensions" | ||||
|                 style="@style/SettingsItem" | ||||
|                 android:nextFocusUp="@id/settings_updates" | ||||
|                 android:nextFocusUp="@id/settings_credits" | ||||
|                 android:text="@string/extensions" /> | ||||
| 
 | ||||
|             <LinearLayout | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue