Compare commits

...

10 Commits
5.3.1 ... 5.3.4

Author SHA1 Message Date
tom5079
5ee1bb11a0 Merge remote-tracking branch 'origin/master' 2022-02-01 19:11:25 +09:00
tom5079
c1de45abce use webp by default 2022-02-01 19:10:54 +09:00
tom5079
8805033c8d Update README.md 2022-02-01 17:47:46 +09:00
tom5079
0ed59bb8a9 Merge remote-tracking branch 'origin/master' 2022-02-01 17:46:44 +09:00
tom5079
8163f2fd28 Bug fix 2022-02-01 17:46:35 +09:00
tom5079
521a65c9d2 Update README.md 2022-02-01 17:37:50 +09:00
tom5079
eb98424668 Bug fix 2022-02-01 17:36:44 +09:00
tom5079
961c731743 Merge remote-tracking branch 'origin/master' 2022-02-01 11:45:54 +09:00
tom5079
5188769fb6 Fuck hitomi 2022-02-01 11:45:45 +09:00
tom5079
8f27d9e30f Update README.md 2022-02-01 11:45:35 +09:00
14 changed files with 144 additions and 145 deletions

View File

@@ -1,17 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="deploymentTargetDropDown"> <component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_2_API_31.avd" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<targetSelectedWithDropDown> <targetSelectedWithDropDown>
<Target> <Target>
<type value="QUICK_BOOT_TARGET" /> <type value="QUICK_BOOT_TARGET" />
@@ -23,6 +12,6 @@
</deviceKey> </deviceKey>
</Target> </Target>
</targetSelectedWithDropDown> </targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-02-01T02:15:22.286886Z" /> <timeTargetWasSelectedWithDropDown value="2022-02-01T08:00:57.223690Z" />
</component> </component>
</project> </project>

View File

@@ -2,7 +2,7 @@
*Pupil, Hitomi.la viewer for Android* *Pupil, Hitomi.la viewer for Android*
![](https://img.shields.io/github/downloads/tom5079/Pupil/total) ![](https://img.shields.io/github/downloads/tom5079/Pupil/total)
[![](https://img.shields.io/github/downloads/tom5079/Pupil/5.3.0/Pupil-v5.3.0.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.3.0/Pupil-v5.3.0.apk) [![](https://img.shields.io/github/downloads/tom5079/Pupil/5.3.3/Pupil-v5.3.3.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.3.3/Pupil-v5.3.3.apk)
[![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v) [![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v)
# Features # Features

View File

@@ -38,7 +38,7 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 31 targetSdkVersion 31
versionCode 69 versionCode 69
versionName "5.3.0" versionName "5.3.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }

View File

@@ -33,4 +33,5 @@
} }
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment -keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment -keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
-keep class xyz.quaver.pupil.** { *; } -keep class xyz.quaver.pupil.** { *; }
-keep class app.cash.zipline.** { *; }

View File

@@ -12,7 +12,7 @@
"filters": [], "filters": [],
"attributes": [], "attributes": [],
"versionCode": 69, "versionCode": 69,
"versionName": "5.3.0", "versionName": "5.3.4",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
], ],

View File

@@ -102,19 +102,19 @@ class ExampleInstrumentedTest {
Log.d("PUPILD", r.take(10).toString()) Log.d("PUPILD", r.take(10).toString())
} }
@Test // @Test
fun test_getBlock() { // fun test_getBlock() {
val galleryBlock = getGalleryBlock(2097576) // val galleryBlock = getGalleryBlock(2097576)
//
print(galleryBlock) // print(galleryBlock)
} // }
//
@Test // @Test
fun test_getGallery() { // fun test_getGallery() {
val gallery = getGallery(2097751) // val gallery = getGallery(2097751)
//
print(gallery) // print(gallery)
} // }
@Test @Test
fun test_getGalleryInfo() { fun test_getGalleryInfo() {
@@ -125,42 +125,44 @@ class ExampleInstrumentedTest {
@Test @Test
fun test_getReader() { fun test_getReader() {
val reader = getGalleryInfo(1722144) val reader = getGalleryInfo(2128654)
print(reader) Log.d("PUPILD", reader.toString())
} }
@Test @Test
fun test_getImages() { fun test_getImages() { runBlocking {
val galleryID = 2099306 val galleryID = 2128654
val images = getGalleryInfo(galleryID).files.map { val images = getGalleryInfo(galleryID).files.map {
imageUrlFromImage(galleryID, it,false) imageUrlFromImage(galleryID, it,false)
} }
images.forEachIndexed { index, image -> Log.d("PUPILD", images.toString())
println("Testing $index/${images.size}: $image")
val response = client.newCall(
Request.Builder()
.url(image)
.header("Referer", "https://hitomi.la/")
.build()
).execute()
assertEquals(200, response.code()) // images.forEachIndexed { index, image ->
// println("Testing $index/${images.size}: $image")
// val response = client.newCall(
// Request.Builder()
// .url(image)
// .header("Referer", "https://hitomi.la/")
// .build()
// ).execute()
//
// assertEquals(200, response.code())
//
// println("$index/${images.size} Passed")
// }
} }
println("$index/${images.size} Passed") // @Test
} // fun test_urlFromUrlFromHash() {
} // val url = urlFromUrlFromHash(1531795, GalleryFiles(
// 212, "719d46a7556be0d0021c5105878507129b5b3308b02cf67f18901b69dbb3b5ef", 1, "00.jpg", 300
@Test // ), "webp")
fun test_urlFromUrlFromHash() { //
val url = urlFromUrlFromHash(1531795, GalleryFiles( // print(url)
212, "719d46a7556be0d0021c5105878507129b5b3308b02cf67f18901b69dbb3b5ef", 1, "00.jpg", 300 // }
), "webp")
print(url)
}
// @Test // @Test
// suspend fun test_doSearch_extreme() { // suspend fun test_doSearch_extreme() {
@@ -173,9 +175,9 @@ class ExampleInstrumentedTest {
// print(doSearch("-male:yaoi -female:yaoi -female:loli").size) // print(doSearch("-male:yaoi -female:yaoi -female:loli").size)
// } // }
@Test // @Test
fun test_subdomainFromUrl() { // fun test_subdomainFromUrl() {
val galleryInfo = getGalleryInfo(1929109).files[2] // val galleryInfo = getGalleryInfo(1929109).files[2]
print(urlFromUrlFromHash(1929109, galleryInfo, "webp", null, "a")) // print(urlFromUrlFromHash(1929109, galleryInfo, "webp", null, "a"))
} // }
} }

View File

@@ -81,10 +81,8 @@ private var version = ""
var runtimeReady = false var runtimeReady = false
private set private set
lateinit var runtime: QuickJs lateinit var runtime: QuickJs
private set
class Pupil : Application() { class Pupil : Application() {
companion object { companion object {
lateinit var instance: Pupil lateinit var instance: Pupil
private set private set
@@ -92,7 +90,7 @@ class Pupil : Application() {
init { init {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
withContext(evaluationContext) { withContext(Dispatchers.Main) {
runtime = QuickJs.create() runtime = QuickJs.create()
} }
while (true) { while (true) {
@@ -101,12 +99,15 @@ class Pupil : Application() {
if (version != newVersion) { if (version != newVersion) {
runtimeReady = false runtimeReady = false
version = newVersion
evaluationContext.cancelChildren() evaluationContext.cancelChildren()
withContext(evaluationContext) { kotlin.runCatching {
Log.d("PUPILD", "UPDATE!") URL("https://tom5079.github.io/PupilSources/assets/js/gg.js").readText()
runtime.evaluate(URL("https://tom5079.github.io/PupilSources/assets/js/gg.js").readText()) }.getOrNull()?.also { gg ->
runtimeReady = true withContext(Dispatchers.Main) {
runtime.evaluate(gg)
version = newVersion
runtimeReady = true
}
} }
} }
} }

View File

@@ -16,16 +16,14 @@
package xyz.quaver.pupil.hitomi package xyz.quaver.pupil.hitomi
import kotlinx.coroutines.Job import kotlinx.coroutines.*
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Request import okhttp3.Request
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.runtime import xyz.quaver.pupil.runtime
import xyz.quaver.pupil.runtimeReady
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -84,11 +82,11 @@ data class GalleryInfo(
val groups: List<Group>? = null, val groups: List<Group>? = null,
val parodys: List<Parody>? = null, val parodys: List<Parody>? = null,
val tags: List<Tag>? = null, val tags: List<Tag>? = null,
val related: List<Int>, val related: List<Int> = emptyList(),
val languages: List<Language>, val languages: List<Language> = emptyList(),
val characters: List<Character>? = null, val characters: List<Character>? = null,
val scene_indexes: List<Int>, val scene_indexes: List<Int>? = emptyList(),
val files: List<GalleryFiles> val files: List<GalleryFiles> = emptyList()
) )
val json = Json { val json = Json {
@@ -130,18 +128,21 @@ const val galleryblockextension = ".html"
const val galleryblockdir = "galleryblock" const val galleryblockdir = "galleryblock"
const val nozomiextension = ".nozomi" const val nozomiextension = ".nozomi"
val evaluationContext = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + Job() val evaluationContext = Dispatchers.Main + Job()
object gg { object gg {
suspend fun m(g: Int): Int = withContext(evaluationContext) { suspend fun m(g: Int): Int = withContext(evaluationContext) {
while (!runtimeReady) delay(1000)
runtime.evaluate("gg.m($g)").toString().toInt() runtime.evaluate("gg.m($g)").toString().toInt()
} }
suspend fun b(): String = withContext(evaluationContext) { suspend fun b(): String = withContext(evaluationContext) {
while (!runtimeReady) delay(1000)
runtime.evaluate("gg.b").toString() runtime.evaluate("gg.b").toString()
} }
suspend fun s(h: String): String = withContext(evaluationContext) { suspend fun s(h: String): String = withContext(evaluationContext) {
while (!runtimeReady) delay(1000)
runtime.evaluate("gg.s('$h')").toString() runtime.evaluate("gg.s('$h')").toString()
} }
} }
@@ -197,14 +198,15 @@ suspend fun rewriteTnPaths(html: String) {
} }
suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String { suspend fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
return when { return urlFromUrlFromHash(galleryID, image, "webp", null, "a")
noWebp -> // return when {
urlFromUrlFromHash(galleryID, image) // noWebp ->
// image.hasavif != 0 -> // urlFromUrlFromHash(galleryID, image)
// urlFromUrlFromHash(galleryID, image, "avif", null, "a") //// image.hasavif != 0 ->
image.haswebp != 0 -> //// urlFromUrlFromHash(galleryID, image, "avif", null, "a")
urlFromUrlFromHash(galleryID, image, "webp", null, "a") // image.haswebp != 0 ->
else -> // urlFromUrlFromHash(galleryID, image, "webp", null, "a")
urlFromUrlFromHash(galleryID, image) // else ->
} // urlFromUrlFromHash(galleryID, image)
// }
} }

View File

@@ -71,7 +71,7 @@ data class GalleryBlock(
val type: String, val type: String,
val language: String, val language: String,
val relatedTags: List<String>, val relatedTags: List<String>,
val groups: List<String> val groups: List<String> = emptyList()
) )
suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock { suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock {

View File

@@ -220,28 +220,23 @@ class DownloadService : Service() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
runCatching { runCatching {
response.also { val image = response.also { if (it.code() != 200) throw IOException( "$galleryID $index ${response.request().url()} CODE ${it.code()}" ) }.body()?.use { it.bytes() } ?: throw Exception("Response null")
if (it.code() != 200) throw IOException( val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
"$galleryID $index ${response.request().url()} CODE ${it.code()}"
Cache.getInstance(this@DownloadService, galleryID)
.putImage(index, "${index.toString().padStart(padding, '0')}.$ext", image)
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
notify(galleryID)
if (isCompleted(galleryID)) {
if (DownloadManager.getInstance(this@DownloadService)
.getDownloadFolder(galleryID) != null
) )
}.body()?.use { Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
val padding = ceil(progress[galleryID]?.size?.let { log10(it.toFloat()) } ?: 0F).toInt()
Cache.getInstance(this@DownloadService, galleryID) startId?.let { stopSelf(it) }
.putImage(index, "${index.toString().padStart(padding, '0')}.$ext", it.byteStream()) }
progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
notify(galleryID)
if (isCompleted(galleryID)) {
if (DownloadManager.getInstance(this@DownloadService)
.getDownloadFolder(galleryID) != null
)
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
startId?.let { stopSelf(it) }
}
} ?: throw Exception("Response null")
}.onFailure { }.onFailure {
FirebaseCrashlytics.getInstance().recordException(it) FirebaseCrashlytics.getInstance().recordException(it)
} }
@@ -329,7 +324,8 @@ class DownloadService : Service() {
} }
if (isCompleted(galleryID)) { if (isCompleted(galleryID)) {
Cache.getInstance(this@DownloadService, galleryID).moveToDownload() if (DownloadManager.getInstance(this@DownloadService).getDownloadFolder(galleryID) != null)
Cache.getInstance(this@DownloadService, galleryID).moveToDownload()
notificationManager.cancel(galleryID) notificationManager.cancel(galleryID)
startId?.let { stopSelf(it) } startId?.let { stopSelf(it) }

View File

@@ -43,6 +43,7 @@ import xyz.quaver.io.util.readText
import xyz.quaver.io.util.writeText import xyz.quaver.io.util.writeText
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.histories import xyz.quaver.pupil.histories
import xyz.quaver.pupil.hitomi.json
import xyz.quaver.pupil.util.byteToString import xyz.quaver.pupil.util.byteToString
import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager import xyz.quaver.pupil.util.downloader.DownloadManager
@@ -118,7 +119,7 @@ class ManageStorageFragment : PreferenceFragmentCompat(), Preference.OnPreferenc
if (!metadataFile.exists()) return@forEach if (!metadataFile.exists()) return@forEach
val metadata = metadataFile.readText()?.let { val metadata = metadataFile.readText()?.let {
Json.decodeFromString<Metadata>(it) json.decodeFromString<Metadata>(it)
} ?: return@forEach } ?: return@forEach
val galleryID = metadata.galleryBlock?.id ?: metadata.galleryInfo?.id?.toIntOrNull() ?: return@forEach val galleryID = metadata.galleryBlock?.id ?: metadata.galleryInfo?.id?.toIntOrNull() ?: return@forEach

View File

@@ -21,12 +21,9 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.net.Uri import android.net.Uri
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
@@ -35,35 +32,50 @@ import okhttp3.Request
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.* import xyz.quaver.io.util.*
import xyz.quaver.pupil.client import xyz.quaver.pupil.client
import xyz.quaver.pupil.hitomi.GalleryBlock import xyz.quaver.pupil.hitomi.*
import xyz.quaver.pupil.hitomi.GalleryInfo
import xyz.quaver.pupil.hitomi.getGalleryBlock
import xyz.quaver.pupil.hitomi.getGalleryInfo
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@Serializable @Serializable
data class OldGalleryBlock( data class OldGalleryInfo(
val code: String, val language_localname: String? = null,
val id: Int, val language: String? = null,
val galleryUrl: String, val date: String? = null,
val thumbnails: List<String>, val files: List<OldGalleryFiles>,
val title: String, val id: Int? = null,
val artists: List<String>, val type: String? = null,
val series: List<String>, val title: String? = null
val type: String,
val language: String,
val relatedTags: List<String>
) )
@Serializable
data class OldGalleryFiles(
val width: Int,
val hash: String,
val haswebp: Int = 0,
val name: String,
val height: Int,
val hasavif: Int = 0,
val hasavifsmalltn: Int? = 0
)
@Serializable
data class OldMetadata(
var galleryBlock: GalleryBlock? = null,
var reader: OldGalleryInfo? = null,
var imageList: MutableList<String?>? = null
) {
fun copy(): OldMetadata = OldMetadata(galleryBlock, reader, imageList?.let { MutableList(it.size) { i -> it[i] } })
}
@Serializable @Serializable
data class Metadata( data class Metadata(
var galleryBlock: GalleryBlock? = null, var galleryBlock: GalleryBlock? = null,
var galleryInfo: GalleryInfo? = null, var galleryInfo: GalleryInfo? = null,
var imageList: MutableList<String?>? = null var imageList: MutableList<String?>? = null
) { ) {
constructor(old: OldMetadata) : this(old.galleryBlock, getGalleryInfo(old.galleryBlock?.id ?: throw Exception()), old.imageList)
fun copy(): Metadata = Metadata(galleryBlock, galleryInfo, imageList?.let { MutableList(it.size) { i -> it[i] } }) fun copy(): Metadata = Metadata(galleryBlock, galleryInfo, imageList?.let { MutableList(it.size) { i -> it[i] } })
} }
@@ -90,9 +102,13 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
var metadata = kotlin.runCatching { var metadata = kotlin.runCatching {
findFile(".metadata")?.readText()?.let { metadata -> findFile(".metadata")?.readText()?.let { metadata ->
Json.decodeFromString<Metadata>(metadata) kotlin.runCatching {
json.decodeFromString<Metadata>(metadata)
}.getOrElse {
Metadata(json.decodeFromString<OldMetadata>(metadata))
}
} }
}.getOrNull() ?: Metadata() }.onFailure { it.printStackTrace() }.getOrNull() ?: Metadata()
val downloadFolder: FileX? val downloadFolder: FileX?
get() = DownloadManager.getInstance(this).getDownloadFolder(galleryID) get() = DownloadManager.getInstance(this).getDownloadFolder(galleryID)
@@ -179,14 +195,13 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
metadata.imageList?.getOrNull(index)?.let { findFile(it) } metadata.imageList?.getOrNull(index)?.let { findFile(it) }
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
fun putImage(index: Int, fileName: String, data: InputStream) { suspend fun putImage(index: Int, fileName: String, data: ByteArray) = coroutineScope {
val file = cacheFolder.getChild(fileName) val file = cacheFolder.getChild(fileName)
if (!file.exists()) if (!file.exists())
file.createNewFile() file.createNewFile()
file.outputStream()?.use {
data.copyTo(it) file.writeBytes(data)
}
setMetadata { metadata -> metadata.imageList!![index] = fileName } setMetadata { metadata -> metadata.imageList!![index] = fileName }
} }

View File

@@ -77,8 +77,8 @@ fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply {
val formatMap = mapOf<String, GalleryBlock.() -> (String)>( val formatMap = mapOf<String, GalleryBlock.() -> (String)>(
"-id-" to { id.toString() }, "-id-" to { id.toString() },
"-title-" to { title }, "-title-" to { title },
"-artist-" to { artists.joinToString() }, "-artist-" to { if (artists.isNotEmpty()) artists.joinToString() else "N/A" },
"-group-" to { groups.joinToString() } "-group-" to { if (groups.isNotEmpty()) groups.joinToString() else "N/A" }
// TODO // TODO
) )
/** /**
@@ -100,13 +100,11 @@ fun GalleryBlock.formatDownloadFolderTest(format: String): String =
suspend fun GalleryInfo.getRequestBuilders(): List<Request.Builder> { suspend fun GalleryInfo.getRequestBuilders(): List<Request.Builder> {
val galleryID = this.id.toIntOrNull() ?: 0 val galleryID = this.id.toIntOrNull() ?: 0
val lowQuality = Preferences["low_quality", true]
return this.files.map { return this.files.map {
Request.Builder() Request.Builder()
.url( .url(
runCatching { runCatching {
imageUrlFromImage(galleryID, it, !lowQuality) imageUrlFromImage(galleryID, it, false)
} }
.onFailure { .onFailure {
FirebaseCrashlytics.getInstance().recordException(it) FirebaseCrashlytics.getInstance().recordException(it)

View File

@@ -56,12 +56,6 @@
app:key="nomedia" app:key="nomedia"
app:title="@string/settings_nomedia_title"/> app:title="@string/settings_nomedia_title"/>
<SwitchPreferenceCompat
app:key="low_quality"
app:title="@string/settings_low_quality"
app:summary="@string/settings_low_quality_summary"
app:defaultValue="true"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory