pull/1994/head
kunfei 2 years ago
parent 6f4bb0f387
commit d62c757cc4
  1. 28
      app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt
  2. 4
      app/src/main/java/io/legado/app/ui/book/local/ImportBookAdapter.kt
  3. 2
      app/src/main/java/io/legado/app/ui/book/local/ImportBookViewModel.kt
  4. 2
      app/src/main/java/io/legado/app/ui/book/remote/RemoteBook.kt
  5. 88
      app/src/main/java/io/legado/app/ui/book/remote/RemoteBookActivity.kt
  6. 118
      app/src/main/java/io/legado/app/ui/book/remote/RemoteBookAdapter.kt
  7. 32
      app/src/main/java/io/legado/app/ui/book/remote/RemoteBookViewModel.kt
  8. 5
      app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt
  9. 3
      app/src/main/res/layout/activity_import_book.xml
  10. 67
      app/src/main/res/layout/activity_remote_book.xml
  11. 108
      app/src/main/res/layout/item_remote_book.xml

@ -41,8 +41,6 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
override val binding by viewBinding(ActivityImportBookBinding::inflate)
override val viewModel by viewModels<ImportBookViewModel>()
private var rootDoc: FileDoc? = null
private val subDocs = arrayListOf<FileDoc>()
private val adapter by lazy { ImportBookAdapter(this, this) }
private var scanDocJob: Job? = null
@ -59,6 +57,7 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
binding.titleBar.setTitle(R.string.book_local)
initView()
initEvent()
initData()
@ -148,6 +147,7 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
private fun initRootDoc() {
val lastPath = AppConfig.importBookPath
when {
viewModel.rootDoc != null -> upPath()
lastPath.isNullOrEmpty() -> {
binding.tvEmptyMsg.visible()
selectFolder.launch()
@ -160,9 +160,9 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
binding.tvEmptyMsg.visible()
selectFolder.launch()
} else {
subDocs.clear()
rootDoc = FileDoc.fromDocumentFile(doc)
upDocs(rootDoc!!)
viewModel.subDocs.clear()
viewModel.rootDoc = FileDoc.fromDocumentFile(doc)
upDocs(viewModel.rootDoc!!)
}
}.onFailure {
binding.tvEmptyMsg.visible()
@ -184,8 +184,8 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
.rationale(R.string.tip_perm_request_storage)
.onGranted {
kotlin.runCatching {
rootDoc = FileDoc.fromFile(File(path))
subDocs.clear()
viewModel.rootDoc = FileDoc.fromFile(File(path))
viewModel.subDocs.clear()
upPath()
}.onFailure {
binding.tvEmptyMsg.visible()
@ -205,7 +205,7 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
@Synchronized
private fun upPath() {
rootDoc?.let {
viewModel.rootDoc?.let {
scanDocJob?.cancel()
upDocs(it)
}
@ -215,7 +215,7 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
binding.tvEmptyMsg.gone()
var path = rootDoc.name + File.separator
var lastDoc = rootDoc
for (doc in subDocs) {
for (doc in viewModel.subDocs) {
lastDoc = doc
path = path + doc.name + File.separator
}
@ -229,9 +229,9 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
* 扫描当前文件夹及所有子文件夹
*/
private fun scanFolder() {
rootDoc?.let { doc ->
viewModel.rootDoc?.let { doc ->
adapter.clearItems()
val lastDoc = subDocs.lastOrNull() ?: doc
val lastDoc = viewModel.subDocs.lastOrNull() ?: doc
binding.refreshProgressBar.isAutoLoading = true
scanDocJob?.cancel()
scanDocJob = launch(IO) {
@ -261,14 +261,14 @@ class ImportBookActivity : VMBaseActivity<ActivityImportBookBinding, ImportBookV
@Synchronized
override fun nextDoc(fileDoc: FileDoc) {
subDocs.add(fileDoc)
viewModel.subDocs.add(fileDoc)
upPath()
}
@Synchronized
private fun goBackDir(): Boolean {
return if (subDocs.isNotEmpty()) {
subDocs.removeAt(subDocs.lastIndex)
return if (viewModel.subDocs.isNotEmpty()) {
viewModel.subDocs.removeAt(viewModel.subDocs.lastIndex)
upPath()
true
} else {

@ -14,9 +14,9 @@ import io.legado.app.utils.*
class ImportBookAdapter(context: Context, val callBack: CallBack) :
RecyclerAdapter<FileDoc, ItemImportBookBinding>(context) {
var selectedUris = hashSetOf<String>()
val selectedUris = hashSetOf<String>()
var checkableCount = 0
private var bookFileNames = arrayListOf<String>()
private val bookFileNames = arrayListOf<String>()
override fun getViewBinding(parent: ViewGroup): ItemImportBookBinding {
return ItemImportBookBinding.inflate(inflater, parent, false)

@ -21,6 +21,8 @@ import java.io.File
import java.util.*
class ImportBookViewModel(application: Application) : BaseViewModel(application) {
var rootDoc: FileDoc? = null
val subDocs = arrayListOf<FileDoc>()
var sort = context.getPrefInt(PreferKey.localBookImportSort)
var dataCallback: DataCallback? = null
var dataFlowStart: (() -> Unit)? = null

@ -6,7 +6,7 @@ data class RemoteBook(
val size: Long,
val contentType: String,
val lastModify: Long,
val isOnBookShelf: Boolean
var isOnBookShelf: Boolean
) {
val isDir by lazy {

@ -1,6 +1,5 @@
package io.legado.app.ui.book.remote
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
@ -9,37 +8,42 @@ import androidx.core.view.isGone
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.databinding.ActivityRemoteBookBinding
import io.legado.app.databinding.ActivityImportBookBinding
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.ui.book.remote.manager.RemoteBookWebDav
import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.dialog.WaitDialog
import io.legado.app.utils.toastOnUi
import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.launch
import java.io.File
/**
* 展示远程书籍
* @author qianfanguojin
* @time 2022/05/12
*/
class RemoteBookActivity : VMBaseActivity<ActivityRemoteBookBinding, RemoteBookViewModel>(),
RemoteBookAdapter.CallBack {
override val binding by viewBinding(ActivityRemoteBookBinding::inflate)
class RemoteBookActivity : VMBaseActivity<ActivityImportBookBinding, RemoteBookViewModel>(),
RemoteBookAdapter.CallBack,
SelectActionBar.CallBack {
override val binding by viewBinding(ActivityImportBookBinding::inflate)
override val viewModel by viewModels<RemoteBookViewModel>()
private val adapter by lazy { RemoteBookAdapter(this, this) }
private val waitDialog by lazy { WaitDialog(this) }
override fun onActivityCreated(savedInstanceState: Bundle?) {
binding.titleBar.setTitle(R.string.remote_book)
initView()
initData()
initEvent()
}
private fun initView() {
binding.layTop.setBackgroundColor(backgroundColor)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
binding.selectActionBar.setMainActionText(R.string.add_to_shelf)
binding.selectActionBar.setCallBack(this)
}
private fun initData() {
@ -51,7 +55,14 @@ class RemoteBookActivity : VMBaseActivity<ActivityRemoteBookBinding, RemoteBookV
adapter.setItems(remoteBooks)
}
}
viewModel.loadRemoteBookList(RemoteBookWebDav.rootBookUrl)
upPath()
}
private fun initEvent() {
binding.tvGoBack.setOnClickListener {
viewModel.dirList.removeLastOrNull()
upPath()
}
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
@ -62,46 +73,47 @@ class RemoteBookActivity : VMBaseActivity<ActivityRemoteBookBinding, RemoteBookV
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_refresh -> {
viewModel.loadRemoteBookList(
viewModel.dirList.lastOrNull()?.path ?: RemoteBookWebDav.rootBookUrl
)
upPath()
}
}
return super.onCompatOptionsItemSelected(item)
}
override fun finish() {
if (viewModel.dirList.isEmpty()) {
super.finish()
} else {
viewModel.dirList.removeLastOrNull()
val remoteBook = viewModel.dirList.lastOrNull()
viewModel.dataCallback?.clear()
if (remoteBook != null) {
binding.titleBar.title = remoteBook.filename
viewModel.loadRemoteBookList(remoteBook.path)
} else {
binding.titleBar.setTitle(R.string.remote_book)
viewModel.loadRemoteBookList(RemoteBookWebDav.rootBookUrl)
}
}
override fun revertSelection() {
adapter.revertSelection()
}
override fun openDir(remoteBook: RemoteBook) {
viewModel.dirList.add(remoteBook)
binding.titleBar.title = remoteBook.filename
viewModel.dataCallback?.clear()
viewModel.loadRemoteBookList(remoteBook.path)
override fun selectAll(selectAll: Boolean) {
adapter.selectAll(selectAll)
}
@SuppressLint("NotifyDataSetChanged")
override fun addToBookshelf(remoteBook: RemoteBook) {
override fun onClickSelectBarMainAction() {
waitDialog.show()
viewModel.addToBookshelf(remoteBook, success = {
toastOnUi(getString(R.string.download_book_success))
viewModel.addToBookshelf(adapter.selected) {
adapter.notifyDataSetChanged()
}) {
waitDialog.dismiss()
}
}
private fun upPath() {
var path = "books" + File.separator
viewModel.dirList.forEach {
path = path + it.filename + File.separator
}
binding.tvPath.text = path
viewModel.dataCallback?.clear()
adapter.selected.clear()
viewModel.loadRemoteBookList(
viewModel.dirList.lastOrNull()?.path ?: RemoteBookWebDav.rootBookUrl
)
}
override fun openDir(remoteBook: RemoteBook) {
viewModel.dirList.add(remoteBook)
upPath()
}
override fun upCountView() {
binding.selectActionBar.upCountView(adapter.selected.size, adapter.checkableCount)
}
}

@ -1,13 +1,17 @@
package io.legado.app.ui.book.remote
import android.annotation.SuppressLint
import android.content.Context
import android.view.ViewGroup
import androidx.core.view.isGone
import cn.hutool.core.date.LocalDateTimeUtil
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.databinding.ItemRemoteBookBinding
import io.legado.app.constant.AppConst
import io.legado.app.databinding.ItemImportBookBinding
import io.legado.app.utils.ConvertUtils
import io.legado.app.utils.gone
import io.legado.app.utils.invisible
import io.legado.app.utils.visible
/**
@ -15,14 +19,16 @@ import io.legado.app.utils.ConvertUtils
* @author qianfanguojin
*/
class RemoteBookAdapter(context: Context, val callBack: CallBack) :
RecyclerAdapter<RemoteBook, ItemRemoteBookBinding>(context) {
RecyclerAdapter<RemoteBook, ItemImportBookBinding>(context) {
var selected = hashSetOf<RemoteBook>()
var checkableCount = 0
override fun getViewBinding(parent: ViewGroup): ItemRemoteBookBinding {
return ItemRemoteBookBinding.inflate(inflater, parent, false)
override fun getViewBinding(parent: ViewGroup): ItemImportBookBinding {
return ItemImportBookBinding.inflate(inflater, parent, false)
}
override fun onCurrentListChanged() {
upCheckableCount()
}
/**
@ -30,39 +36,107 @@ class RemoteBookAdapter(context: Context, val callBack: CallBack) :
*/
override fun convert(
holder: ItemViewHolder,
binding: ItemRemoteBookBinding,
binding: ItemImportBookBinding,
item: RemoteBook,
payloads: MutableList<Any>
) {
binding.run {
tvName.text = item.filename.substringBeforeLast(".")
tvContentType.text = item.contentType
tvSize.text = ConvertUtils.formatFileSize(item.size)
tvDate.text =
LocalDateTimeUtil.format(LocalDateTimeUtil.of(item.lastModify), "yyyy-MM-dd")
llInfo.isGone = item.isDir
tvContentType.isGone = item.isDir
btnDownload.isGone = item.isDir
if (payloads.isEmpty()) {
if (item.isDir) {
ivIcon.setImageResource(R.drawable.ic_folder)
ivIcon.visible()
cbSelect.invisible()
llBrief.gone()
cbSelect.isChecked = false
} else {
if (item.isOnBookShelf) {
ivIcon.setImageResource(R.drawable.ic_book_has)
ivIcon.visible()
cbSelect.invisible()
} else {
ivIcon.invisible()
cbSelect.visible()
}
llBrief.visible()
tvTag.text = item.contentType
tvSize.text = ConvertUtils.formatFileSize(item.size)
tvDate.text = AppConst.dateFormat.format(item.lastModify)
cbSelect.isChecked = selected.contains(item)
}
tvName.text = item.filename
} else {
cbSelect.isChecked = selected.contains(item)
}
}
}
override fun registerListener(holder: ItemViewHolder, binding: ItemRemoteBookBinding) {
binding.root.setOnClickListener {
override fun registerListener(holder: ItemViewHolder, binding: ItemImportBookBinding) {
holder.itemView.setOnClickListener {
getItem(holder.layoutPosition)?.let {
if (it.isDir) {
callBack.openDir(it)
} else if (!it.isOnBookShelf) {
if (!selected.contains(it)) {
selected.add(it)
} else {
selected.remove(it)
}
notifyItemChanged(holder.layoutPosition, true)
callBack.upCountView()
}
}
}
binding.btnDownload.setOnClickListener {
getItem(holder.layoutPosition)?.let {
callBack.addToBookshelf(it)
}
private fun upCheckableCount() {
checkableCount = 0
getItems().forEach {
if (!it.isDir && !it.isOnBookShelf) {
checkableCount++
}
}
callBack.upCountView()
}
@SuppressLint("NotifyDataSetChanged")
fun selectAll(selectAll: Boolean) {
if (selectAll) {
getItems().forEach {
if (!it.isDir && !it.isOnBookShelf) {
selected.add(it)
}
}
} else {
selected.clear()
}
notifyDataSetChanged()
callBack.upCountView()
}
fun revertSelection() {
getItems().forEach {
if (!it.isDir) {
if (selected.contains(it)) {
selected.remove(it)
} else {
selected.add(it)
}
}
}
callBack.upCountView()
}
fun removeSelection() {
for (i in getItems().lastIndex downTo 0) {
if (getItem(i) in selected) {
removeItem(i)
}
}
}
interface CallBack {
fun openDir(remoteBook: RemoteBook)
fun addToBookshelf(remoteBook: RemoteBook)
fun upCountView()
}
}

@ -1,8 +1,8 @@
package io.legado.app.ui.book.remote
import android.app.Application
import android.net.Uri
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog
import io.legado.app.model.localBook.LocalBook
import io.legado.app.ui.book.remote.manager.RemoteBookWebDav
import io.legado.app.utils.toastOnUi
@ -62,33 +62,23 @@ class RemoteBookViewModel(application: Application): BaseViewModel(application){
}
}
fun addToBookshelf(uriList: HashSet<String>, finally: () -> Unit) {
fun addToBookshelf(remoteBooks: HashSet<RemoteBook>, finally: () -> Unit) {
execute {
uriList.forEach {
LocalBook.importFile(Uri.parse(it))
remoteBooks.forEach { remoteBook ->
val downloadBookPath = RemoteBookWebDav.getRemoteBook(remoteBook)
downloadBookPath?.let {
LocalBook.importFile(it)
remoteBook.isOnBookShelf = true
}
}
}.onFinally {
finally.invoke()
}
}
/**
* 添加书籍到本地书架
*/
fun addToBookshelf(remoteBook: RemoteBook, success: () -> Unit, finally: () -> Unit) {
execute {
val downloadBookPath = RemoteBookWebDav.getRemoteBook(remoteBook)
downloadBookPath?.let {
LocalBook.importFile(it)
}
}.onSuccess {
success.invoke()
}.onError {
context.toastOnUi(it.localizedMessage)
AppLog.put("导入出错\n${it.localizedMessage}", it)
context.toastOnUi("导入出错\n${it.localizedMessage}")
}.onFinally {
finally.invoke()
}
}
interface DataCallback {
fun setItems(remoteFiles: List<RemoteBook>)

@ -108,7 +108,10 @@ object RemoteBookWebDav : RemoteBookManager() {
}
override suspend fun delete(remoteBookUrl: String): Boolean {
TODO("Not yet implemented")
AppWebDav.authorization?.let {
return WebDav(remoteBookUrl, it).delete()
}
return false
}
}

@ -9,8 +9,7 @@
android:id="@+id/titleBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/book_local" />
app:layout_constraintTop_toTopOf="parent" />
<!--path-->
<LinearLayout

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<io.legado.app.ui.widget.TitleBar
android:id="@+id/title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/remote_book" />
<io.legado.app.ui.widget.anima.RefreshProgressBar
android:id="@+id/refresh_progress_bar"
android:layout_width="match_parent"
android:layout_height="2dp"
app:layout_constraintTop_toBottomOf="@id/title_bar" />
<io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout
android:id="@+id/content_view"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/refresh_progress_bar">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<io.legado.app.ui.widget.anima.RotateLoading
android:id="@+id/rotate_loading"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_margin="6dp"
android:visibility="gone"
android:layout_gravity="center"
app:loading_width="2dp" />
</io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout>
<!-- <io.legado.app.ui.widget.SelectActionBar-->
<!-- android:id="@+id/select_action_bar"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
<!-- app:layout_constraintTop_toBottomOf="@id/content_view"-->
<!-- />-->
<TextView
android:id="@+id/tv_empty_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center"
android:visibility="gone"
android:text="webDav books文件夹为空"
app:layout_constraintTop_toBottomOf="@+id/title_bar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
tools:text="Empty" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -1,108 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<io.legado.app.ui.widget.image.CoverImageView
android:id="@+id/iv_cover"
android:layout_width="66dp"
android:layout_height="90dp"
android:layout_margin="8dp"
android:contentDescription="@string/img_cover"
android:scaleType="centerCrop"
android:src="@drawable/image_cover_default"
android:transitionName="img_cover"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="UnusedAttribute" />
<TextView
android:id="@+id/tv_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/app_name"
android:textColor="@color/primaryText"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@+id/ll_info"
app:layout_constraintStart_toEndOf="@+id/iv_cover"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/ll_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:gravity="top"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@+id/tv_content_type"
app:layout_constraintLeft_toRightOf="@+id/iv_cover"
app:layout_constraintTop_toBottomOf="@+id/tv_name"
app:layout_constraintRight_toLeftOf="@+id/btn_download">
<TextView
android:id="@+id/tv_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:textColor="@color/tv_text_summary"
android:textSize="13sp"
tools:ignore="TextContrastCheck"
tools:text="128kb" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:text="@string/separator"
android:textColor="@color/tv_text_summary"
android:textSize="11sp"
tools:ignore="TextContrastCheck" />
<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:textColor="@color/tv_text_summary"
android:textSize="13sp"
tools:ignore="TextContrastCheck"
tools:text="2022-12-7" />
</LinearLayout>
<io.legado.app.ui.widget.text.AccentBgTextView
android:id="@+id/tv_content_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:maxWidth="50dp"
android:singleLine="true"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:text="TXT"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/iv_cover"
app:layout_constraintTop_toBottomOf="@id/ll_info"
app:radius="2dp"
tools:ignore="HardcodedText,RtlHardcoded" />
<io.legado.app.ui.widget.text.AccentStrokeTextView
android:id="@+id/btn_download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:padding="6dp"
android:text="@string/nb_file_add_shelf"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_name"
tools:text="加入书架" />
</androidx.constraintlayout.widget.ConstraintLayout>
Loading…
Cancel
Save