Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfe435c4f3 | ||
|
|
69e85f8b90 | ||
|
|
c9bde3c487 | ||
|
|
65e9557d9f | ||
|
|
4f249c07e7 | ||
|
|
5fd35b492c | ||
|
|
9bddf95013 | ||
|
|
03444f070f | ||
|
|
2f1a63eb64 | ||
|
|
9d0898b26c | ||
|
|
994aa99797 | ||
|
|
8204a15276 |
@@ -19,8 +19,8 @@ android {
|
||||
applicationId "xyz.quaver.pupil"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
versionCode 42
|
||||
versionName "4.6"
|
||||
versionCode 45
|
||||
versionName "4.9"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
multiDexEnabled true
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
@@ -1 +1 @@
|
||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":42,"versionName":"4.6","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":45,"versionName":"4.9","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
||||
@@ -28,7 +28,6 @@ import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.ListPreloader
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import io.fabric.sdk.android.Fabric
|
||||
import kotlinx.android.synthetic.main.item_reader.view.*
|
||||
@@ -64,8 +63,7 @@ class ReaderAdapter(private val context: Context,
|
||||
override fun getPreloadRequestBuilder(item: File): RequestBuilder<*>? {
|
||||
return glide
|
||||
.load(item)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.fitCenter()
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
@@ -114,25 +112,28 @@ class ReaderAdapter(private val context: Context,
|
||||
|
||||
if (!isFullScreen)
|
||||
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
|
||||
.dimensionRatio = "${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
|
||||
.dimensionRatio = "W,${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
|
||||
|
||||
holder.view.reader_index.text = (position+1).toString()
|
||||
|
||||
val images = Cache(context).getImage(galleryID, position)
|
||||
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
||||
|
||||
if (progress?.isInfinite() == true && images != null) {
|
||||
holder.view.reader_item_progressbar.visibility = View.INVISIBLE
|
||||
|
||||
holder.view.image.post {
|
||||
glide
|
||||
.load(images)
|
||||
.fitCenter()
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.into(holder.view.image)
|
||||
}
|
||||
|
||||
if (images != null) {
|
||||
glide
|
||||
.load(images)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.image_broken_variant)
|
||||
.apply {
|
||||
if (BuildConfig.CENSOR)
|
||||
override(5, 8)
|
||||
}
|
||||
.into(holder.view.image)
|
||||
} else {
|
||||
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
||||
holder.view.reader_item_progressbar.visibility = View.VISIBLE
|
||||
|
||||
glide.clear(holder.view.image)
|
||||
|
||||
if (progress?.isNaN() == true) {
|
||||
if (Fabric.isInitialized())
|
||||
|
||||
@@ -961,7 +961,9 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
Mode.DOWNLOAD -> {
|
||||
val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
|
||||
file.isDirectory && (file.name.toIntOrNull() != null) && File(file, ".metadata").exists()
|
||||
file.isDirectory && file.name.toIntOrNull() != null
|
||||
}?.sortedByDescending {
|
||||
it.lastModified()
|
||||
}?.map {
|
||||
it.name.toInt()
|
||||
} ?: emptyList()
|
||||
|
||||
@@ -21,23 +21,44 @@ package xyz.quaver.pupil.util.download
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.util.Base64
|
||||
import android.util.SparseArray
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.io.InputStream
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.proxy
|
||||
import xyz.quaver.pupil.util.copyRecursively
|
||||
import xyz.quaver.pupil.util.getCachedGallery
|
||||
import xyz.quaver.pupil.util.getDownloadDirectory
|
||||
import xyz.quaver.pupil.util.json
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.URL
|
||||
import java.util.concurrent.locks.Lock
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
class Cache(context: Context) : ContextWrapper(context) {
|
||||
|
||||
private val locks = SparseArray<Lock>()
|
||||
private fun lock(galleryID: Int) {
|
||||
synchronized(locks) {
|
||||
if (locks.indexOfKey(galleryID) < 0)
|
||||
locks.put(galleryID, ReentrantLock())
|
||||
}
|
||||
|
||||
locks[galleryID].lock()
|
||||
}
|
||||
|
||||
private fun unlock(galleryID: Int) {
|
||||
locks[galleryID]?.unlock()
|
||||
}
|
||||
|
||||
private val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
// Search in this order
|
||||
@@ -78,7 +99,9 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val thumbnails = getGalleryBlock(galleryID)?.thumbnails
|
||||
try {
|
||||
Base64.encodeToString(URL(thumbnails?.firstOrNull()).readBytes(), Base64.DEFAULT)
|
||||
Base64.encodeToString(URL(thumbnails?.firstOrNull()).openConnection(proxy).getInputStream().use {
|
||||
it.readBytes()
|
||||
}, Base64.DEFAULT)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
@@ -212,16 +235,16 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
return null
|
||||
}
|
||||
|
||||
fun putImage(galleryID: Int, name: String, data: ByteArray) {
|
||||
val cache = File(getCachedGallery(galleryID), name).also {
|
||||
|
||||
fun putImage(galleryID: Int, index: Int, ext: String, data: InputStream) {
|
||||
val cache = File(getCachedGallery(galleryID), "%05d.$ext".format(index)).also {
|
||||
if (!it.exists())
|
||||
it.createNewFile()
|
||||
}
|
||||
|
||||
if (!Regex("""^[0-9]+.+$""").matches(name))
|
||||
throw IllegalArgumentException("File name is not a number")
|
||||
|
||||
cache.writeBytes(data)
|
||||
data.use {
|
||||
it.copyTo(FileOutputStream(cache))
|
||||
}
|
||||
}
|
||||
|
||||
fun moveToDownload(galleryID: Int) {
|
||||
@@ -231,7 +254,7 @@ class Cache(context: Context) : ContextWrapper(context) {
|
||||
}
|
||||
val download = File(getDownloadDirectory(this), galleryID.toString())
|
||||
|
||||
cache.copyRecursively(download, true)
|
||||
cache.copyRecursively(download, true) { _, _ -> OnErrorAction.SKIP }
|
||||
cache.deleteRecursively()
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
@@ -36,16 +37,18 @@ import okio.*
|
||||
import xyz.quaver.Code
|
||||
import xyz.quaver.hitomi.Reader
|
||||
import xyz.quaver.hitomi.getReferer
|
||||
import xyz.quaver.hitomi.urlFromUrlFromHash
|
||||
import xyz.quaver.hitomi.imageUrlFromImage
|
||||
import xyz.quaver.hiyobi.cookie
|
||||
import xyz.quaver.hiyobi.createImgList
|
||||
import xyz.quaver.hiyobi.user_agent
|
||||
import xyz.quaver.proxy
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class DownloadWorker private constructor(context: Context) : ContextWrapper(context) {
|
||||
@@ -153,12 +156,14 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
val response = chain.proceed(request)
|
||||
|
||||
response.newBuilder()
|
||||
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
|
||||
.build()
|
||||
.body(ProgressResponseBody(request.tag(), response.body(), progressListener))
|
||||
.build()
|
||||
}
|
||||
fun buildClient() =
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(interceptor)
|
||||
.connectTimeout(0, TimeUnit.SECONDS)
|
||||
.readTimeout(0, TimeUnit.SECONDS)
|
||||
.dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
|
||||
.proxy(proxy)
|
||||
.build()
|
||||
@@ -211,10 +216,10 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
when (reader.code) {
|
||||
Code.HITOMI -> {
|
||||
url(
|
||||
urlFromUrlFromHash(
|
||||
imageUrlFromImage(
|
||||
galleryID,
|
||||
reader.galleryInfo.files[index],
|
||||
if (lowQuality) "webp" else null
|
||||
lowQuality
|
||||
)
|
||||
)
|
||||
addHeader("Referer", getReferer(galleryID))
|
||||
@@ -252,7 +257,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
val cache = Cache(this@DownloadWorker).getImages(galleryID)
|
||||
|
||||
progress.put(galleryID, reader.galleryInfo.files.indices.map { index ->
|
||||
if (cache?.getOrNull(index) != null)
|
||||
if (cache?.firstOrNull { it?.nameWithoutExtension?.toIntOrNull() == index } != null)
|
||||
Float.POSITIVE_INFINITY
|
||||
else
|
||||
0F
|
||||
@@ -279,6 +284,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
for (i in reader.galleryInfo.files.indices) {
|
||||
val callback = object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
Log.i("PUPILD", "FAIL ${call.request().tag()} (${e.message})")
|
||||
if (Fabric.isInitialized() && e.message != "Canceled")
|
||||
Crashlytics.logException(e)
|
||||
|
||||
@@ -287,43 +293,77 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
|
||||
notify(galleryID)
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
with(Cache(this@DownloadWorker)) {
|
||||
if (isDownloading(galleryID)) {
|
||||
moveToDownload(galleryID)
|
||||
setDownloading(galleryID, false)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (isCompleted(galleryID) && clients.indexOfKey(galleryID) >= 0) {
|
||||
clients.remove(galleryID)
|
||||
with(Cache(this@DownloadWorker)) {
|
||||
if (isDownloading(galleryID)) {
|
||||
moveToDownload(galleryID)
|
||||
setDownloading(galleryID, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
clients.remove(galleryID)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
response.body().use {
|
||||
val res = it.bytes()
|
||||
val ext =
|
||||
call.request().url().encodedPath().split('.').last()
|
||||
Log.i("PUPILD", "OK ${call.request().tag()}")
|
||||
|
||||
Cache(this@DownloadWorker).putImage(galleryID, "%05d.%s".format(i, ext), res)
|
||||
val ext = call.request().url().encodedPath().split('.').last()
|
||||
|
||||
try {
|
||||
response.body().use {
|
||||
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it.byteStream())
|
||||
}
|
||||
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
|
||||
}
|
||||
|
||||
notify(galleryID)
|
||||
notify(galleryID)
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
with(Cache(this@DownloadWorker)) {
|
||||
if (isDownloading(galleryID)) {
|
||||
moveToDownload(galleryID)
|
||||
setDownloading(galleryID, false)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (isCompleted(galleryID) && clients.indexOfKey(galleryID) >= 0) {
|
||||
clients.remove(galleryID)
|
||||
with(Cache(this@DownloadWorker)) {
|
||||
if (isDownloading(galleryID)) {
|
||||
moveToDownload(galleryID)
|
||||
setDownloading(galleryID, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clients.remove(galleryID)
|
||||
|
||||
Log.i("PUPILD", "SUCCESS ${call.request().tag()}")
|
||||
} catch (e: Exception) {
|
||||
|
||||
progress[galleryID]?.set(i, Float.NaN)
|
||||
exception[galleryID]?.set(i, e)
|
||||
|
||||
notify(galleryID)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (isCompleted(galleryID) && clients.indexOfKey(galleryID) >= 0) {
|
||||
clients.remove(galleryID)
|
||||
with(Cache(this@DownloadWorker)) {
|
||||
if (isDownloading(galleryID)) {
|
||||
moveToDownload(galleryID)
|
||||
setDownloading(galleryID, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File(Cache(this@DownloadWorker).getCachedGallery(galleryID), "%05d.$ext".format(i)).delete()
|
||||
|
||||
Log.i("PUPILD", "FAIL ON OK ${call.request().tag()} (${e.message})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (progress[galleryID]?.get(i)?.isFinite() == true)
|
||||
if (progress[galleryID]?.get(i)?.isFinite() == true) {
|
||||
queueDownload(galleryID, reader, i, callback)
|
||||
Log.i("PUPILD", "$galleryID QUEUED $i")
|
||||
} else {
|
||||
Log.i("PUPILD", "$galleryID SKIPPED $i (${progress[galleryID]?.get(i)})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,13 +371,17 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
val max = progress[galleryID]?.size ?: 0
|
||||
val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0
|
||||
|
||||
if (isCompleted(galleryID))
|
||||
Log.i("PUPILD", "NOTIFY $galleryID ${isCompleted(galleryID)} $progress/$max")
|
||||
|
||||
if (isCompleted(galleryID)) {
|
||||
notification[galleryID]
|
||||
?.setContentText(getString(R.string.reader_notification_complete))
|
||||
?.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
?.setProgress(0, 0, false)
|
||||
?.setOngoing(false)
|
||||
else
|
||||
|
||||
notificationManager.cancel(galleryID)
|
||||
} else
|
||||
notification[galleryID]
|
||||
?.setProgress(max, progress, false)
|
||||
?.setContentText("$progress/$max")
|
||||
@@ -354,7 +398,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
}
|
||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
||||
addNextIntentWithParentStack(intent)
|
||||
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
getPendingIntent(galleryID, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
|
||||
@@ -369,18 +413,27 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
||||
|
||||
private fun loop() = CoroutineScope(Dispatchers.Default).launch {
|
||||
while (true) {
|
||||
if (queue.isEmpty() || clients.size() > preferences.getInt("max_download", 4))
|
||||
if (queue.isEmpty())
|
||||
continue
|
||||
|
||||
val galleryID = queue.poll() ?: continue
|
||||
val galleryID = queue.peek() ?: continue
|
||||
|
||||
if (clients.indexOfKey(galleryID) >= 0) // Gallery already downloading!
|
||||
continue
|
||||
|
||||
initNotification(galleryID)
|
||||
if (notification[galleryID] == null)
|
||||
initNotification(galleryID)
|
||||
|
||||
if (Cache(this@DownloadWorker).isDownloading(galleryID))
|
||||
notificationManager.notify(galleryID, notification[galleryID].build())
|
||||
|
||||
if (clients.size() >= preferences.getInt("max_download", 4))
|
||||
continue
|
||||
|
||||
Log.i("PUPILD", "QUEUED $galleryID #${clients.size()+1}")
|
||||
|
||||
worker.put(galleryID, download(galleryID))
|
||||
queue.poll()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.os.storage.StorageManager
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.io.IOException
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.lang.reflect.Array
|
||||
@@ -211,4 +212,66 @@ fun Uri.toFile(context: Context): File? {
|
||||
}
|
||||
|
||||
return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName)
|
||||
}
|
||||
|
||||
fun File.copyRecursively(
|
||||
target: File,
|
||||
overwrite: Boolean = false,
|
||||
onError: (File, IOException) -> OnErrorAction = { _, exception -> throw exception }
|
||||
): Boolean {
|
||||
if (!exists()) {
|
||||
return onError(this, NoSuchFileException(file = this, reason = "The source file doesn't exist.")) !=
|
||||
OnErrorAction.TERMINATE
|
||||
}
|
||||
try {
|
||||
// We cannot break for loop from inside a lambda, so we have to use an exception here
|
||||
for (src in walkTopDown().onFail { f, e -> if (onError(f, e) == OnErrorAction.TERMINATE) throw IOException("Walk failed") }) {
|
||||
if (!src.exists()) {
|
||||
if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist.")) ==
|
||||
OnErrorAction.TERMINATE)
|
||||
return false
|
||||
} else {
|
||||
val relPath = src.toRelativeString(this)
|
||||
val dstFile = File(target, relPath)
|
||||
if (dstFile.exists() && !(src.isDirectory && dstFile.isDirectory)) {
|
||||
val stillExists = if (!overwrite) true else {
|
||||
if (dstFile.isDirectory)
|
||||
!dstFile.deleteRecursively()
|
||||
else
|
||||
!dstFile.delete()
|
||||
}
|
||||
|
||||
if (stillExists) {
|
||||
if (onError(dstFile, FileAlreadyExistsException(file = src,
|
||||
other = dstFile,
|
||||
reason = "The destination file already exists.")) == OnErrorAction.TERMINATE)
|
||||
return false
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (src.isDirectory) {
|
||||
dstFile.mkdirs()
|
||||
} else {
|
||||
val length = try {
|
||||
src.copyTo(dstFile, overwrite).length()
|
||||
} catch (e: IOException) {
|
||||
if (onError(src, e) == OnErrorAction.TERMINATE)
|
||||
return false
|
||||
else
|
||||
-1
|
||||
}
|
||||
|
||||
if (length != src.length()) {
|
||||
if (onError(src, IOException("Source file wasn't copied completely, length of destination file differs.")) == OnErrorAction.TERMINATE)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
} catch (e: IOException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,13 @@
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintHeight_max="2000dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:background="@drawable/reader_item_boundary">
|
||||
|
||||
<LinearLayout
|
||||
@@ -61,11 +64,12 @@
|
||||
|
||||
<com.github.chrisbanes.photoview.PhotoView
|
||||
android:id="@+id/image"
|
||||
android:contentDescription="@string/reader_imageview_description"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:paddingBottom="8dp"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
5555
dependencies.txt
Normal file
5555
dependencies.txt
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user