basic offline manga fragment

This commit is contained in:
Finnley Somdahl 2023-11-05 02:17:49 -06:00
parent c75df942f2
commit 91d869005c
14 changed files with 502 additions and 32 deletions

View file

@ -207,7 +207,6 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
fun isOnline(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return tryWith {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return@tryWith if (cap != null) {
when {
@ -223,7 +222,6 @@ fun isOnline(context: Context): Boolean {
else -> false
}
} else false
} else true
} ?: false
}
@ -732,7 +730,7 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
if (s != null) {
(activity ?: currActivity())?.apply {
runOnUiThread {
val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_LONG)
val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_SHORT)
snackBar.view.apply {
updateLayoutParams<FrameLayout.LayoutParams> {
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)

View file

@ -41,6 +41,7 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding
import ani.dantotsu.databinding.SplashScreenBinding
import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment
import ani.dantotsu.home.LoginFragment

View file

@ -33,6 +33,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PR
import java.net.HttpURLConnection
import java.net.URL
import androidx.core.content.ContextCompat
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
@ -169,6 +170,7 @@ class MangaDownloaderService : Service() {
"Please grant notification permission",
Toast.LENGTH_SHORT
).show()
broadcastDownloadFailed(task.chapter)
return@withContext
}
@ -223,14 +225,14 @@ class MangaDownloaderService : Service() {
saveMediaInfo(task)
downloadsManager.addDownload(Download(task.title, task.chapter, Download.Type.MANGA))
downloadsManager.exportDownloads(Download(task.title, task.chapter, Download.Type.MANGA))
//downloadsManager.exportDownloads(Download(task.title, task.chapter, Download.Type.MANGA))
broadcastDownloadFinished(task.chapter)
snackString("${task.title} - ${task.chapter} Download finished")
}
}
fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
try {
// Define the directory within the private external storage space
val directory = File(
@ -262,7 +264,7 @@ class MangaDownloaderService : Service() {
GlobalScope.launch(Dispatchers.IO) {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${task.title}/${task.chapter}"
"Dantotsu/Manga/${task.title}"
)
if (!directory.exists()) directory.mkdirs()
@ -272,7 +274,7 @@ class MangaDownloaderService : Service() {
SChapterImpl() // Provide an instance of SChapterImpl
})
.create()
val mediaJson = gson.toJson(task.sourceMedia) // Assuming sourceMedia is part of DownloadTask
val mediaJson = gson.toJson(task.sourceMedia)
val media = gson.fromJson(mediaJson, Media::class.java)
if (media != null) {
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
@ -329,6 +331,13 @@ class MangaDownloaderService : Service() {
sendBroadcast(intent)
}
private fun broadcastDownloadFailed(chapterNumber: String) {
val intent = Intent(ACTION_DOWNLOAD_FAILED).apply {
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
}
sendBroadcast(intent)
}
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_CANCEL_DOWNLOAD) {

View file

@ -0,0 +1,52 @@
package ani.dantotsu.download.manga
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.TextView
import androidx.cardview.widget.CardView
import ani.dantotsu.R
class OfflineMangaAdapter(private val context: Context, private val items: List<OfflineMangaModel>) : BaseAdapter() {
private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
override fun getCount(): Int {
return items.size
}
override fun getItem(position: Int): Any? {
return items[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
var view = convertView
if (view == null) {
view = inflater.inflate(R.layout.item_media_compact, parent, false)
}
val item = getItem(position) as OfflineMangaModel
val imageView = view!!.findViewById<ImageView>(R.id.itemCompactImage)
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
// Bind item data to the views
// For example:
imageView.setImageURI(item.image)
titleTextView.text = item.title
itemScore.text = item.score
if (item.isOngoing) {
ongoing.visibility = View.VISIBLE
} else {
ongoing.visibility = View.GONE
}
return view
}
}

View file

@ -0,0 +1,201 @@
package ani.dantotsu.download.manga
import android.animation.ObjectAnimator
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.OvershootInterpolator
import android.widget.GridView
import androidx.cardview.widget.CardView
import androidx.core.view.updatePaddingRelative
import androidx.fragment.app.Fragment
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.currContext
import ani.dantotsu.databinding.FragmentMangaBinding
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.px
import ani.dantotsu.statusBarHeight
import com.google.firebase.crashlytics.FirebaseCrashlytics
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlin.math.max
import kotlin.math.min
class OfflineMangaFragment: Fragment() {
private val downloadManager = Injekt.get<DownloadsManager>()
private var downloads: List<OfflineMangaModel> = listOf()
private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_manga_offline, container, false)
gridView = view.findViewById(R.id.gridView)
getDownloads()
adapter = OfflineMangaAdapter(requireContext(), downloads)
gridView.adapter = adapter
gridView.setOnItemClickListener { parent, view, position, id ->
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val media = downloadManager.mangaDownloads.filter { it.title == item.title }.first()
startActivity(
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(media))
)
}
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var height = statusBarHeight
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) {
height = max(
statusBarHeight,
min(
displayCutout.boundingRects[0].width(),
displayCutout.boundingRects[0].height()
)
)
}
}
}
val refreshLayout = view.findViewById<SwipeRefreshLayout>(R.id.mangaRefresh)
refreshLayout.setSlingshotDistance(height + 128)
refreshLayout.setProgressViewEndTarget(false, height + 128)
refreshLayout.setOnRefreshListener {
Refresh.activity[this.hashCode()]!!.postValue(true)
}
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
var visible = false
fun animate() {
val start = if (visible) 0f else 1f
val end = if (!visible) 0f else 1f
ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
}
scrollTop.setOnClickListener {
//TODO: scroll to top
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
super.onPause()
downloads = listOf()
}
override fun onDestroy() {
super.onDestroy()
downloads = listOf()
}
override fun onStop() {
super.onStop()
downloads = listOf()
}
private fun getDownloads() {
val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
val newDownloads = mutableListOf<OfflineMangaModel>()
for (title in titles) {
val _downloads = downloadManager.mangaDownloads.filter { it.title == title }
val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newDownloads += offlineMangaModel
}
downloads = newDownloads
}
private fun getMedia(download: Download): Media? {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}"
)
//load media.json and convert to media class with gson
try {
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.create()
val media = File(directory, "media.json")
val mediaJson = media.readText()
return gson.fromJson(mediaJson, Media::class.java)
}
catch (e: Exception){
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
return null
}
}
private fun loadOfflineMangaModel(download: Download): OfflineMangaModel{
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}"
)
//load media.json and convert to media class with gson
try {
val media = File(directory, "media.json")
val mediaJson = media.readText()
val mediaModel = getMedia(download)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
} else {
null
}
val title = mediaModel.nameMAL?:"unknown"
val score = if (mediaModel.userScore != 0) mediaModel.userScore.toString() else
if (mediaModel.meanScore == null) "?" else mediaModel.meanScore.toString()
val isOngoing = false
val isUserScored = mediaModel.userScore != 0
return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri)
}
catch (e: Exception){
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
return OfflineMangaModel("unknown", "0", false, false, null)
}
}
}

View file

@ -0,0 +1,6 @@
package ani.dantotsu.download.manga
import android.net.Uri
data class OfflineMangaModel(val title: String, val score: String, val isOngoing: Boolean, val isUserScored: Boolean, val image: Uri?) {
}

View file

@ -80,6 +80,16 @@ class MangaChapterAdapter(
}
}
fun removeDownload(chapterNumber: String) {
activeDownloads.remove(chapterNumber)
downloadedChapters.remove(chapterNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
notifyItemChanged(position)
}
}
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(chapterNumber: String) {
if (activeDownloads.contains(chapterNumber)) {

View file

@ -417,6 +417,12 @@ open class MangaReadFragment : Fragment() {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
chapterNumber?.let { chapterAdapter.stopDownload(it) }
}
ACTION_DOWNLOAD_FAILED -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
chapterNumber?.let {
chapterAdapter.removeDownload(it)
}
}
}
}
}
@ -468,6 +474,7 @@ open class MangaReadFragment : Fragment() {
companion object {
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
const val EXTRA_CHAPTER_NUMBER = "extra_chapter_number"
}
}

View file

@ -50,7 +50,7 @@ abstract class BaseParser {
* Isn't necessary to override, but recommended, if you want to improve auto search results
* **/
open suspend fun autoSearch(mediaObj: Media): ShowResponse? {
var response = loadSavedShowResponse(mediaObj.id)
var response: ShowResponse? = null//loadSavedShowResponse(mediaObj.id)
if (response != null) {
saveShowResponse(mediaObj.id, response, true)
} else {

View file

@ -7,6 +7,7 @@ import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.Media
import ani.dantotsu.tryWithSuspend
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.source.model.SManga
abstract class WatchSources : BaseSources() {
@ -23,14 +24,28 @@ abstract class WatchSources : BaseSources() {
} ?: mutableMapOf()
}
suspend fun loadEpisodes(i: Int, showLink: String, extra: Map<String, String>?, sAnime: SAnime?): MutableMap<String, Episode> {
suspend fun loadEpisodes(
i: Int,
showLink: String,
extra: Map<String, String>?,
sAnime: SAnime?
): MutableMap<String, Episode> {
println("finder333 $showLink")
val map = mutableMapOf<String, Episode>()
val parser = get(i)
tryWithSuspend(true) {
if (sAnime != null) {
parser.loadEpisodes(showLink,extra, sAnime).forEach {
map[it.number] = Episode(it.number, it.link, it.title, it.description, it.thumbnail, it.isFiller, extra = it.extra, sEpisode = it.sEpisode)
parser.loadEpisodes(showLink, extra, sAnime).forEach {
map[it.number] = Episode(
it.number,
it.link,
it.title,
it.description,
it.thumbnail,
it.isFiller,
extra = it.extra,
sEpisode = it.sEpisode
)
}
}
}
@ -42,7 +57,7 @@ abstract class WatchSources : BaseSources() {
abstract class MangaReadSources : BaseSources() {
override operator fun get(i: Int): MangaParser {
return (list.getOrNull(i)?:list.firstOrNull())?.get?.value as? MangaParser
return (list.getOrNull(i) ?: list.firstOrNull())?.get?.value as? MangaParser
?: EmptyMangaParser()
}
@ -56,6 +71,7 @@ abstract class MangaReadSources : BaseSources() {
suspend fun loadChapters(i: Int, show: ShowResponse): MutableMap<String, MangaChapter> {
val map = mutableMapOf<String, MangaChapter>()
val parser = get(i)
show.sManga?.let { sManga ->
tryWithSuspend(true) {
parser.loadChapters(show.link, show.extra, sManga).forEach {
@ -63,15 +79,28 @@ abstract class MangaReadSources : BaseSources() {
}
}
}
if(show.sManga == null) {
//must be downloaded
if (show.sManga == null) {
logger("sManga is null")
}
if (parser is OfflineMangaParser && show.sManga == null) {
tryWithSuspend(true) {
// Since we've checked, we can safely cast parser to OfflineMangaParser and call its methods
parser.loadChapters(show.link, show.extra, SManga.create()).forEach {
map[it.number] = MangaChapter(it)
}
}
} else {
logger("Parser is not an instance of OfflineMangaParser")
}
logger("map size ${map.size}")
return map
}
}
abstract class NovelReadSources : BaseSources(){
abstract class NovelReadSources : BaseSources() {
override operator fun get(i: Int): NovelParser? {
return if (list.isNotEmpty()) {
(list.getOrNull(i) ?: list[0]).get.value as NovelParser
@ -87,7 +116,7 @@ class EmptyNovelParser : NovelParser() {
override val volumeRegex: Regex = Regex("")
override suspend fun loadBook(link: String, extra: Map<String, String>?): Book {
return Book("","", null, emptyList()) // Return an empty Book object or some default value
return Book("", "", null, emptyList()) // Return an empty Book object or some default value
}
override suspend fun search(query: String): List<ShowResponse> {

View file

@ -7,16 +7,19 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
object MangaSources : MangaReadSources() {
// Instantiate the static parser
private val offlineMangaParser by lazy { OfflineMangaParser() }
override var list: List<Lazier<BaseParser>> = emptyList()
suspend fun init(fromExtensions: StateFlow<List<MangaExtension.Installed>>) {
// Initialize with the first value from StateFlow
val initialExtensions = fromExtensions.first()
list = createParsersFromExtensions(initialExtensions)
list = createParsersFromExtensions(initialExtensions) + Lazier({ OfflineMangaParser() }, "Downloaded")
// Update as StateFlow emits new values
fromExtensions.collect { extensions ->
list = createParsersFromExtensions(extensions)
list = createParsersFromExtensions(extensions) + Lazier({ OfflineMangaParser() }, "Downloaded")
}
}

View file

@ -0,0 +1,75 @@
package ani.dantotsu.parsers
import android.os.Environment
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadsManager
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import me.xdrop.fuzzywuzzy.FuzzySearch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class OfflineMangaParser: MangaParser() {
private val downloadManager = Injekt.get<DownloadsManager>()
override val hostUrl: String = "Offline"
override val name: String = "Offline"
override val saveName: String = "Offline"
override suspend fun loadChapters(
mangaLink: String,
extra: Map<String, String>?,
sManga: SManga
): List<MangaChapter> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$mangaLink"
)
//get all of the folder names and add them to the list
val chapters = mutableListOf<MangaChapter>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isDirectory) {
val chapter = MangaChapter(it.name, "$mangaLink/${it.name}", it.name, null, SChapter.create())
chapters.add(chapter)
}
}
return chapters
}
return emptyList()
}
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$chapterLink"
)
val images = mutableListOf<MangaImage>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isFile) {
val image = MangaImage(it.absolutePath, false, null)
images.add(image)
}
}
return images
}
return emptyList()
}
override suspend fun search(query: String): List<ShowResponse> {
val titles = downloadManager.mangaDownloads.map { it.title }.distinct()
val returnTitles: MutableList<String> = mutableListOf()
for (title in titles) {
if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) {
returnTitles.add(title)
}
}
val returnList: MutableList<ShowResponse> = mutableListOf()
for (title in returnTitles) {
returnList.add(ShowResponse(title, title, title))
}
return returnList
}
}

View file

@ -20,6 +20,7 @@
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/noInternetSad"
android:layout_width="wrap_content"
@ -49,7 +50,7 @@
android:layout_width="150dp"
android:layout_height="64dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_margin="32dp"
android:layout_margin="128dp"
android:fontFamily="@font/poppins_bold"
android:text="@string/refresh"
app:cornerRadius="16dp"

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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="match_parent"
tools:context=".home.MangaFragment">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/mangaRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false">
<!-- Add a LinearLayout or other ViewGroup as a direct child of SwipeRefreshLayout -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/mangaOfflineTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:text="Offline Manga"
android:textColor="?attr/colorOnSurface"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="@font/poppins"
android:gravity="center_horizontal" />
<!-- This TextView might overlap with GridView if GridView has items -->
<TextView
android:id="@+id/noMangaOffline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="No offline manga found"
android:textColor="?attr/colorOnSurface"
android:textSize="18sp"
android:visibility="gone" />
<GridView
android:id="@+id/gridView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:numColumns="auto_fit"
android:columnWidth="108dp"
android:verticalSpacing="10dp"
android:horizontalSpacing="10dp"
android:padding="10dp"
android:gravity="center" />
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<androidx.cardview.widget.CardView
android:id="@+id/mangaPageScrollTop"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="?android:colorBackground"
app:cardCornerRadius="24dp"
app:contentPadding="12dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:rotation="90"
app:srcCompat="@drawable/ic_round_arrow_back_ios_new_24"
app:tint="?attr/colorOnSurface"
tools:ignore="ContentDescription" />
</androidx.cardview.widget.CardView>
</FrameLayout>