Manga repository authorization support

This commit is contained in:
Koitharu
2021-09-08 07:27:25 +03:00
parent c4585c81e1
commit 593624fdb9
20 changed files with 312 additions and 78 deletions

View File

@@ -23,7 +23,9 @@
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute">
<activity android:name="org.koitharu.kotatsu.main.ui.MainActivity">
<activity
android:name="org.koitharu.kotatsu.main.ui.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -32,12 +34,16 @@
android:name="android.app.default_searchable"
android:value=".ui.search.SearchActivity" />
</activity>
<activity android:name="org.koitharu.kotatsu.details.ui.DetailsActivity">
<activity
android:name="org.koitharu.kotatsu.details.ui.DetailsActivity"
android:exported="true">
<intent-filter>
<action android:name="${applicationId}.action.VIEW_MANGA" />
</intent-filter>
</activity>
<activity android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity">
<activity
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
android:exported="true">
<intent-filter>
<action android:name="${applicationId}.action.READ_MANGA" />
</intent-filter>
@@ -50,13 +56,19 @@
android:label="@string/settings" />
<activity
android:name="org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity"
android:exported="true"
android:label="@string/settings">
<intent-filter>
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name="org.koitharu.kotatsu.browser.BrowserActivity" />
<activity
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.koitharu.kotatsu.core.ui.CrashActivity"
android:label="@string/error_occurred"

View File

@@ -67,6 +67,23 @@ open class MangaLoaderContext(
})
}
fun getCookies(domain: String): List<Cookie> {
val url = HttpUrl.Builder()
.scheme(SCHEME_HTTP)
.host(domain)
.build()
return cookieJar.loadForRequest(url)
}
fun copyCookies(oldDomain: String, newDomain: String) {
val url = HttpUrl.Builder()
.scheme(SCHEME_HTTP)
.host(oldDomain)
val cookies = cookieJar.loadForRequest(url.build())
url.host(newDomain)
cookieJar.saveFromResponse(url.build(), cookies)
}
private companion object {
private const val SCHEME_HTTP = "http"

View File

@@ -38,6 +38,7 @@ abstract class BaseFragment<B : ViewBinding> : Fragment(), OnApplyWindowInsetsLi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lastInsets = Insets.NONE
ViewCompat.setOnApplyWindowInsetsListener(view, this)
}

View File

@@ -1,10 +1,8 @@
package org.koitharu.kotatsu.browser
import android.graphics.Bitmap
import android.webkit.WebResourceResponse
import android.webkit.WebView
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koitharu.kotatsu.core.network.WebViewClientCompat
@@ -27,19 +25,4 @@ class BrowserClient(private val callback: BrowserCallback) : WebViewClientCompat
super.onPageCommitVisible(view, url)
callback.onTitleChanged(view.title.orEmpty(), url)
}
override fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? {
return runCatching {
val request = Request.Builder()
.url(url)
.build()
val response = okHttp.newCall(request).execute()
val ct = response.body?.contentType()
WebResourceResponse(
"${ct?.type}/${ct?.subtype}",
ct?.charset()?.name() ?: "utf-8",
response.body?.byteStream()
)
}.getOrNull()
}
}

View File

@@ -5,6 +5,6 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class MangaFilter(
val sortOrder: SortOrder,
val tag: MangaTag?
val sortOrder: SortOrder?,
val tag: MangaTag?,
) : Parcelable

View File

@@ -0,0 +1,8 @@
package org.koitharu.kotatsu.core.parser
interface MangaRepositoryAuthProvider {
val authUrl: String
fun isAuthorized(): Boolean
}

View File

@@ -27,5 +27,6 @@ interface SourceSettings {
const val KEY_DOMAIN = "domain"
const val KEY_USE_SSL = "ssl"
const val KEY_AUTH = "auth"
}
}

View File

@@ -7,7 +7,6 @@ import org.koitharu.kotatsu.core.model.MangaFilter
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.model.SortOrder
import java.util.*
import kotlin.collections.ArrayList
class FilterAdapter(
sortOrders: List<SortOrder> = emptyList(),
@@ -19,7 +18,7 @@ class FilterAdapter(
private val sortOrders = ArrayList<SortOrder>(sortOrders)
private val tags = ArrayList(Collections.singletonList(null) + tags)
private var currentState = state ?: MangaFilter(sortOrders.first(), null)
private var currentState = state ?: MangaFilter(sortOrders.firstOrNull(), null)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
VIEW_TYPE_SORT -> FilterSortHolder(parent).apply {

View File

@@ -61,7 +61,7 @@ class LocalListViewModel(
launchLoadingJob(Dispatchers.Default) {
try {
listError.value = null
mangaList.value = repository.getList(0)
mangaList.value = repository.getList(0, tags = null)
} catch (e: Throwable) {
listError.value = e
}

View File

@@ -3,16 +3,19 @@ package org.koitharu.kotatsu.reader.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.databinding.ActivitySettingsSimpleBinding
import org.koitharu.kotatsu.settings.MainSettingsFragment
import org.koitharu.kotatsu.settings.NetworkSettingsFragment
import org.koitharu.kotatsu.settings.ReaderSettingsFragment
import org.koitharu.kotatsu.settings.SourceSettingsFragment
class SimpleSettingsActivity : BaseActivity<ActivitySettingsSimpleBinding>() {
@@ -25,6 +28,9 @@ class SimpleSettingsActivity : BaseActivity<ActivitySettingsSimpleBinding>() {
R.id.container, when (intent?.action) {
Intent.ACTION_MANAGE_NETWORK_USAGE -> NetworkSettingsFragment()
ACTION_READER -> ReaderSettingsFragment()
ACTION_SOURCE -> SourceSettingsFragment.newInstance(
intent.getParcelableExtra(EXTRA_SOURCE) ?: MangaSource.LOCAL
)
else -> MainSettingsFragment()
}
)
@@ -43,9 +49,17 @@ class SimpleSettingsActivity : BaseActivity<ActivitySettingsSimpleBinding>() {
private const val ACTION_READER =
"${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS"
private const val ACTION_SOURCE =
"${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS"
private const val EXTRA_SOURCE = "source"
fun newReaderSettingsIntent(context: Context) =
Intent(context, SimpleSettingsActivity::class.java)
.setAction(ACTION_READER)
fun newSourceSettingsIntent(context: Context, source: MangaSource) =
Intent(context, SimpleSettingsActivity::class.java)
.setAction(ACTION_SOURCE)
.putExtra(EXTRA_SOURCE, source as Parcelable)
}
}

View File

@@ -1,10 +1,15 @@
package org.koitharu.kotatsu.remotelist.ui
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaFilter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.reader.ui.SimpleSettingsActivity
import org.koitharu.kotatsu.utils.ext.parcelableArgument
import org.koitharu.kotatsu.utils.ext.withArgs
@@ -29,6 +34,26 @@ class RemoteListFragment : MangaListFragment() {
super.onFilterChanged(filter)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.opt_list_remote, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_source_settings -> {
startActivity(
SimpleSettingsActivity.newSourceSettingsIntent(
context ?: return false,
source,
)
)
true
}
else -> super.onOptionsItemSelected(item)
}
}
companion object {
private const val ARG_SOURCE = "provider"

View File

@@ -90,6 +90,9 @@ class RemoteListViewModel(
}
hasNextPage.value = list.isNotEmpty()
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
listError.value = e
}
}

View File

@@ -12,7 +12,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import java.util.*
class SearchViewModel(
private val repository: MangaRepository,
@@ -74,6 +73,7 @@ class SearchViewModel(
listError.value = null
val list = repository.getList(
offset = if (append) mangaList.value?.size ?: 0 else 0,
tags = null,
query = query
)
if (!append) {

View File

@@ -76,15 +76,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
true
}
AppSettings.KEY_COOKIES_CLEAR -> {
viewLifecycleScope.launch {
val cookieJar = get<AndroidCookieJar>()
cookieJar.clear()
Snackbar.make(
listView ?: return@launch,
R.string.cookies_cleared,
Snackbar.LENGTH_SHORT
).show()
}
clearCookies()
true
}
AppSettings.KEY_SEARCH_HISTORY_CLEAR -> {
@@ -144,4 +136,22 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
}
}.show()
}
private fun clearCookies() {
AlertDialog.Builder(context ?: return)
.setTitle(R.string.clear_cookies)
.setMessage(R.string.text_clear_cookies_prompt)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.clear) { _, _ ->
viewLifecycleScope.launch {
val cookieJar = get<AndroidCookieJar>()
cookieJar.clear()
Snackbar.make(
listView ?: return@launch,
R.string.cookies_cleared,
Snackbar.LENGTH_SHORT
).show()
}
}.show()
}
}

View File

@@ -9,8 +9,10 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.TwoStatePreference
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import org.koitharu.kotatsu.settings.utils.EditTextBindListener
import org.koitharu.kotatsu.settings.utils.EditTextDefaultSummaryProvider
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
@@ -20,6 +22,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
class SourceSettingsFragment : PreferenceFragmentCompat() {
private val source by parcelableArgument<MangaSource>(EXTRA_SOURCE)
private var repository: RemoteMangaRepository? = null
override fun onResume() {
super.onResume()
@@ -29,6 +32,7 @@ class SourceSettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.sharedPreferencesName = source.name
val repo = mangaRepositoryOf(source) as? RemoteMangaRepository ?: return
repository = repo
addPreferencesFromResource(R.xml.pref_source)
val screen = preferenceScreen
val prefsMap = ArrayMap<String, Any>(screen.preferenceCount)
@@ -41,13 +45,32 @@ class SourceSettingsFragment : PreferenceFragmentCompat() {
initPreferenceWithDefaultValue(pref, defValue)
}
}
findPreference<Preference>(SourceSettings.KEY_AUTH)?.run {
isVisible = repo is MangaRepositoryAuthProvider
isEnabled = (repo as? MangaRepositoryAuthProvider)?.isAuthorized() == false
}
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
return when (preference?.key) {
SourceSettings.KEY_AUTH -> {
startActivity(
SourceAuthActivity.newIntent(
context ?: return false,
source,
)
)
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
private fun initPreferenceWithDefaultValue(preference: Preference, defaultValue: Any) {
when(preference) {
when (preference) {
is EditTextPreference -> {
preference.summaryProvider = EditTextDefaultSummaryProvider(defaultValue.toString())
when(preference.key) {
when (preference.key) {
SourceSettings.KEY_DOMAIN -> preference.setOnBindEditTextListener(
EditTextBindListener(
EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_URI,

View File

@@ -0,0 +1,114 @@
package org.koitharu.kotatsu.settings.sources.auth
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.MenuItem
import android.widget.Toast
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.browser.BrowserCallback
import org.koitharu.kotatsu.browser.BrowserClient
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaRepositoryAuthProvider
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
private lateinit var repository: MangaRepositoryAuthProvider
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityBrowserBinding.inflate(layoutInflater))
val source = intent?.getParcelableExtra<MangaSource>(EXTRA_SOURCE)
if (source == null) {
finish()
return
}
repository = mangaRepositoryOf(source) as? MangaRepositoryAuthProvider ?: run {
Toast.makeText(
this,
getString(R.string.auth_not_supported_by, source.title),
Toast.LENGTH_SHORT
).show()
finishAfterTransition()
return
}
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_cross)
}
with(binding.webView.settings) {
javaScriptEnabled = true
}
binding.webView.webViewClient = BrowserClient(this)
val url = repository.authUrl
onTitleChanged(
source.title,
getString(R.string.loading_)
)
binding.webView.loadUrl(url)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
binding.webView.stopLoading()
finishAfterTransition()
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
if (binding.webView.canGoBack()) {
binding.webView.goBack()
} else {
super.onBackPressed()
}
}
override fun onPause() {
binding.webView.onPause()
super.onPause()
}
override fun onResume() {
super.onResume()
binding.webView.onResume()
}
override fun onLoadingStateChanged(isLoading: Boolean) {
binding.progressBar.isVisible = isLoading
if (!isLoading && repository.isAuthorized()) {
Toast.makeText(this, R.string.auth_complete, Toast.LENGTH_SHORT).show()
finishAfterTransition()
}
}
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
this.title = title
supportActionBar?.subtitle = subtitle
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.appbar.updatePadding(top = insets.top)
binding.webView.updatePadding(bottom = insets.bottom)
}
companion object {
private const val EXTRA_SOURCE = "source"
fun newIntent(context: Context, source: MangaSource): Intent {
return Intent(context, SourceAuthActivity::class.java)
.putExtra(EXTRA_SOURCE, source as Parcelable)
}
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_source_settings"
android:orderInCategory="50"
android:title="@string/settings"
app:showAsAction="never" />
</menu>

View File

@@ -213,24 +213,27 @@
<string name="other">Другие</string>
<string name="description">Описание</string>
<string name="languages">Языки</string>
<string name="welcome">Добро пожаловать</string>
<string name="text_clear_search_history_prompt">Вы действительно хотите удалить все недавние поисковые запросы? Это действие не может быть отменено.</string>
<string name="backup_saved">Резервная копия успешно сохранена</string>
<string name="tracker_warning">Некоторые производители могут изменять поведение системы, нарушая работу фоновых задач.</string>
<string name="read_more">Подробнее</string>
<string name="queued">В очереди</string>
<string name="text_downloads_holder">На данный момент нет активных загрузок</string>
<string name="chapter_is_missing">Глава отсутствует</string>
<string name="chapter_is_missing_text">Эта глава отсутствует на вашем устройстве. Скачайте или прочитайте её онлайн.</string>
<string name="about_app_translation_summary">Помочь с переводом приложения</string>
<string name="about_app_translation">Перевод</string>
<string name="about_author">Автор</string>
<string name="about_feedback_4pda">Тема на 4PDA</string>
<string name="about_feedback">Обратная связь</string>
<string name="about_support_developer">Поддержать разработчика</string>
<string name="about_support_developer_summary">Если вам нравится это приложение, вы можете помочь финансово с помощью ЮMoney (бывш. Яндекс.Деньги)</string>
<string name="about_gratitudes">Благодарности</string>
<string name="about_gratitudes_summary">Эти люди помогают Kotatsu стать лучше!</string>
<string name="about_copyright_and_licenses">Авторские права и лицензии</string>
<string name="about_license">Лицензия</string>
<string name="welcome">Добро пожаловать</string>
<string name="text_clear_search_history_prompt">Вы действительно хотите удалить все недавние поисковые запросы? Это действие не может быть отменено.</string>
<string name="backup_saved">Резервная копия успешно сохранена</string>
<string name="tracker_warning">Некоторые производители могут изменять поведение системы, нарушая работу фоновых задач.</string>
<string name="read_more">Подробнее</string>
<string name="queued">В очереди</string>
<string name="text_downloads_holder">На данный момент нет активных загрузок</string>
<string name="chapter_is_missing">Глава отсутствует</string>
<string name="chapter_is_missing_text">Эта глава отсутствует на вашем устройстве. Скачайте или прочитайте её онлайн.</string>
<string name="about_app_translation_summary">Помочь с переводом приложения</string>
<string name="about_app_translation">Перевод</string>
<string name="about_author">Автор</string>
<string name="about_feedback_4pda">Тема на 4PDA</string>
<string name="about_feedback">Обратная связь</string>
<string name="about_support_developer">Поддержать разработчика</string>
<string name="about_support_developer_summary">Если вам нравится это приложение, вы можете помочь финансово с помощью ЮMoney (бывш. Яндекс.Деньги)</string>
<string name="about_gratitudes">Благодарности</string>
<string name="about_gratitudes_summary">Эти люди помогают Kotatsu стать лучше!</string>
<string name="about_copyright_and_licenses">Авторские права и лицензии</string>
<string name="about_license">Лицензия</string>
<string name="auth_complete">Авторизация выполнена</string>
<string name="auth_not_supported_by">Авторизация в %s не поддерживается</string>
<string name="text_clear_cookies_prompt">Вы выйдете из всех источников, в которых Вы авторизованы</string>
</resources>

View File

@@ -216,24 +216,27 @@
<string name="text_clear_search_history_prompt">Do you really want to remove all recent search queries? This action cannot be undone.</string>
<string name="other">Other</string>
<string name="languages">Languages</string>
<string name="welcome">Welcome</string>
<string name="description">Description</string>
<string name="backup_saved">Backup saved successfully</string>
<string name="tracker_warning">Some manufacturers can change the system behavior, which may breaks background tasks.</string>
<string name="read_more">Read more</string>
<string name="queued">Queued</string>
<string name="text_downloads_holder">There are currently no active downloads</string>
<string name="chapter_is_missing_text">This chapter is missing on your device. Download or read it online.</string>
<string name="chapter_is_missing">Chapter is missing</string>
<string name="about_app_translation_summary">Translate this app</string>
<string name="about_app_translation">Translation</string>
<string name="about_author">Author</string>
<string name="about_feedback">Feedback</string>
<string name="about_feedback_4pda">Topic on 4PDA</string>
<string name="about_support_developer">Support the developer</string>
<string name="about_support_developer_summary">If you like this app, you can help financially through Yoomoney (ex. Yandex.Money)</string>
<string name="about_gratitudes">Gratitudes</string>
<string name="about_gratitudes_summary">These people make Kotatsu become better!</string>
<string name="about_copyright_and_licenses">Copyright &amp; Licenses</string>
<string name="about_license">License</string>
<string name="welcome">Welcome</string>
<string name="description">Description</string>
<string name="backup_saved">Backup saved successfully</string>
<string name="tracker_warning">Some manufacturers can change the system behavior, which may breaks background tasks.</string>
<string name="read_more">Read more</string>
<string name="queued">Queued</string>
<string name="text_downloads_holder">There are currently no active downloads</string>
<string name="chapter_is_missing_text">This chapter is missing on your device. Download or read it online.</string>
<string name="chapter_is_missing">Chapter is missing</string>
<string name="about_app_translation_summary">Translate this app</string>
<string name="about_app_translation">Translation</string>
<string name="about_author">Author</string>
<string name="about_feedback">Feedback</string>
<string name="about_feedback_4pda">Topic on 4PDA</string>
<string name="about_support_developer">Support the developer</string>
<string name="about_support_developer_summary">If you like this app, you can help financially through Yoomoney (ex. Yandex.Money)</string>
<string name="about_gratitudes">Gratitudes</string>
<string name="about_gratitudes_summary">These people make Kotatsu become better!</string>
<string name="about_copyright_and_licenses">Copyright &amp; Licenses</string>
<string name="about_license">License</string>
<string name="auth_complete">Authorization complete</string>
<string name="auth_not_supported_by">Authorization on %s is not supported</string>
<string name="text_clear_cookies_prompt">You will be logged out from all sources that you are authorized in</string>
</resources>

View File

@@ -14,4 +14,11 @@
android:title="@string/use_ssl"
app:iconSpaceReserved="false" />
<Preference
android:key="auth"
android:persistent="false"
android:title="@string/sign_in"
app:allowDividerAbove="true"
app:iconSpaceReserved="false" />
</PreferenceScreen>