For #6758 - Part 6: Add top site view
parent
f68f89f2bf
commit
9ecb67e783
|
@ -33,6 +33,12 @@ data class Tab(
|
||||||
val icon: Bitmap? = null
|
val icon: Bitmap? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class TopSiteItem(
|
||||||
|
override val id: Long,
|
||||||
|
override val title: String,
|
||||||
|
override val url: String
|
||||||
|
) : TopSite
|
||||||
|
|
||||||
fun List<Tab>.toSessionBundle(sessionManager: SessionManager): List<Session> {
|
fun List<Tab>.toSessionBundle(sessionManager: SessionManager): List<Session> {
|
||||||
return this.mapNotNull { sessionManager.findSessionById(it.sessionId) }
|
return this.mapNotNull { sessionManager.findSessionById(it.sessionId) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.android.synthetic.main.tab_list_row.*
|
import kotlinx.android.synthetic.main.tab_list_row.*
|
||||||
import mozilla.components.feature.media.state.MediaState
|
import mozilla.components.feature.media.state.MediaState
|
||||||
import mozilla.components.feature.tab.collections.TabCollection
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
|
import mozilla.components.feature.top.sites.TopSite
|
||||||
import org.mozilla.fenix.home.OnboardingState
|
import org.mozilla.fenix.home.OnboardingState
|
||||||
import org.mozilla.fenix.home.Tab
|
import org.mozilla.fenix.home.Tab
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
|
||||||
|
@ -27,6 +28,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.SaveTabGroupViewHolder
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabHeaderViewHolder
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabHeaderViewHolder
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSiteViewHolder
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingAutomaticSignInViewHolder
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingAutomaticSignInViewHolder
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingFinishViewHolder
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingFinishViewHolder
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder
|
||||||
|
@ -65,6 +67,8 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class TopSiteList(val topSites: List<TopSite>) : AdapterItem(TopSiteViewHolder.LAYOUT_ID)
|
||||||
|
|
||||||
object SaveTabGroup : AdapterItem(SaveTabGroupViewHolder.LAYOUT_ID)
|
object SaveTabGroup : AdapterItem(SaveTabGroupViewHolder.LAYOUT_ID)
|
||||||
|
|
||||||
object PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID)
|
object PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID)
|
||||||
|
@ -148,6 +152,7 @@ class SessionControlAdapter(
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
TabHeaderViewHolder.LAYOUT_ID -> TabHeaderViewHolder(view, interactor)
|
TabHeaderViewHolder.LAYOUT_ID -> TabHeaderViewHolder(view, interactor)
|
||||||
TabViewHolder.LAYOUT_ID -> TabViewHolder(view, interactor)
|
TabViewHolder.LAYOUT_ID -> TabViewHolder(view, interactor)
|
||||||
|
TopSiteViewHolder.LAYOUT_ID -> TopSiteViewHolder(view, interactor)
|
||||||
SaveTabGroupViewHolder.LAYOUT_ID -> SaveTabGroupViewHolder(view, interactor)
|
SaveTabGroupViewHolder.LAYOUT_ID -> SaveTabGroupViewHolder(view, interactor)
|
||||||
PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(view, interactor)
|
PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(view, interactor)
|
||||||
NoContentMessageViewHolder.LAYOUT_ID -> NoContentMessageViewHolder(view)
|
NoContentMessageViewHolder.LAYOUT_ID -> NoContentMessageViewHolder(view)
|
||||||
|
@ -169,6 +174,7 @@ class SessionControlAdapter(
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) = getItem(position).viewType
|
override fun getItemViewType(position: Int) = getItem(position).viewType
|
||||||
|
|
||||||
|
@SuppressWarnings("ComplexMethod")
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
val item = getItem(position)
|
val item = getItem(position)
|
||||||
when (holder) {
|
when (holder) {
|
||||||
|
@ -179,6 +185,9 @@ class SessionControlAdapter(
|
||||||
is TabViewHolder -> {
|
is TabViewHolder -> {
|
||||||
holder.bindSession((item as AdapterItem.TabItem).tab)
|
holder.bindSession((item as AdapterItem.TabItem).tab)
|
||||||
}
|
}
|
||||||
|
is TopSiteViewHolder -> {
|
||||||
|
holder.bind((item as AdapterItem.TopSiteList).topSites)
|
||||||
|
}
|
||||||
is NoContentMessageViewHolder -> {
|
is NoContentMessageViewHolder -> {
|
||||||
val (icon, header, description) = item as AdapterItem.NoContentMessage
|
val (icon, header, description) = item as AdapterItem.NoContentMessage
|
||||||
holder.bind(icon, header, description)
|
holder.bind(icon, header, description)
|
||||||
|
|
|
@ -112,6 +112,11 @@ interface SessionControlController {
|
||||||
*/
|
*/
|
||||||
fun handleSelectTab(tabView: View, sessionId: String)
|
fun handleSelectTab(tabView: View, sessionId: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [TopSiteInteractor.onSelectTopSite]
|
||||||
|
*/
|
||||||
|
fun handleSelectTopSite(url: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see [TabSessionInteractor.onShareTabs]
|
* @see [TabSessionInteractor.onShareTabs]
|
||||||
*/
|
*/
|
||||||
|
@ -298,6 +303,14 @@ class DefaultSessionControlController(
|
||||||
navController.nav(R.id.homeFragment, directions, extras)
|
navController.nav(R.id.homeFragment, directions, extras)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun handleSelectTopSite(url: String) {
|
||||||
|
activity.components.useCases.tabsUseCases.addTab.invoke(url, true, true)
|
||||||
|
navController.nav(
|
||||||
|
R.id.homeFragment,
|
||||||
|
HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun handleShareTabs() {
|
override fun handleShareTabs() {
|
||||||
invokePendingDeleteJobs()
|
invokePendingDeleteJobs()
|
||||||
val shareData = sessionManager
|
val shareData = sessionManager
|
||||||
|
|
|
@ -149,15 +149,27 @@ interface TabSessionInteractor {
|
||||||
fun onShareTabs()
|
fun onShareTabs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for top site related actions in the [SessionControlInteractor].
|
||||||
|
*/
|
||||||
|
interface TopSiteInteractor {
|
||||||
|
/**
|
||||||
|
* Selects the given top site. Called when a user clicks on a top site.
|
||||||
|
*
|
||||||
|
* @param url The URL of the top site.
|
||||||
|
*/
|
||||||
|
fun onSelectTopSite(url: String)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interactor for the Home screen.
|
* Interactor for the Home screen.
|
||||||
* Provides implementations for the CollectionInteractor, OnboardingInteractor and
|
* Provides implementations for the CollectionInteractor, OnboardingInteractor,
|
||||||
* TabSessionInteractor.
|
* TabSessionInteractor and TopSiteInteractor.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("TooManyFunctions")
|
@SuppressWarnings("TooManyFunctions")
|
||||||
class SessionControlInteractor(
|
class SessionControlInteractor(
|
||||||
private val controller: SessionControlController
|
private val controller: SessionControlController
|
||||||
) : CollectionInteractor, OnboardingInteractor, TabSessionInteractor {
|
) : CollectionInteractor, OnboardingInteractor, TabSessionInteractor, TopSiteInteractor {
|
||||||
override fun onCloseTab(sessionId: String) {
|
override fun onCloseTab(sessionId: String) {
|
||||||
controller.handleCloseTab(sessionId)
|
controller.handleCloseTab(sessionId)
|
||||||
}
|
}
|
||||||
|
@ -214,6 +226,10 @@ class SessionControlInteractor(
|
||||||
controller.handleSelectTab(tabView, sessionId)
|
controller.handleSelectTab(tabView, sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSelectTopSite(url: String) {
|
||||||
|
controller.handleSelectTopSite(url)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onShareTabs() {
|
override fun onShareTabs() {
|
||||||
controller.handleShareTabs()
|
controller.handleShareTabs()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import mozilla.components.feature.tab.collections.TabCollection
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
|
import mozilla.components.feature.top.sites.TopSite
|
||||||
import mozilla.components.lib.state.ext.consumeFrom
|
import mozilla.components.lib.state.ext.consumeFrom
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.home.HomeFragmentState
|
import org.mozilla.fenix.home.HomeFragmentState
|
||||||
|
@ -37,10 +38,16 @@ val noCollectionMessage = AdapterItem.NoContentMessage(
|
||||||
|
|
||||||
private fun normalModeAdapterItems(
|
private fun normalModeAdapterItems(
|
||||||
tabs: List<Tab>,
|
tabs: List<Tab>,
|
||||||
|
topSites: List<TopSite>,
|
||||||
collections: List<TabCollection>,
|
collections: List<TabCollection>,
|
||||||
expandedCollections: Set<Long>
|
expandedCollections: Set<Long>
|
||||||
): List<AdapterItem> {
|
): List<AdapterItem> {
|
||||||
val items = mutableListOf<AdapterItem>()
|
val items = mutableListOf<AdapterItem>()
|
||||||
|
|
||||||
|
if (topSites.isNotEmpty()) {
|
||||||
|
items.add(AdapterItem.TopSiteList(topSites))
|
||||||
|
}
|
||||||
|
|
||||||
items.add(AdapterItem.TabHeader(false, tabs.isNotEmpty()))
|
items.add(AdapterItem.TabHeader(false, tabs.isNotEmpty()))
|
||||||
|
|
||||||
if (tabs.isNotEmpty()) {
|
if (tabs.isNotEmpty()) {
|
||||||
|
@ -52,7 +59,6 @@ private fun normalModeAdapterItems(
|
||||||
|
|
||||||
items.add(AdapterItem.CollectionHeader)
|
items.add(AdapterItem.CollectionHeader)
|
||||||
if (collections.isNotEmpty()) {
|
if (collections.isNotEmpty()) {
|
||||||
|
|
||||||
// If the collection is expanded, we want to add all of its tabs beneath it in the adapter
|
// If the collection is expanded, we want to add all of its tabs beneath it in the adapter
|
||||||
collections.map {
|
collections.map {
|
||||||
AdapterItem.CollectionItem(it, expandedCollections.contains(it.id), tabs.isNotEmpty())
|
AdapterItem.CollectionItem(it, expandedCollections.contains(it.id), tabs.isNotEmpty())
|
||||||
|
@ -116,7 +122,7 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List<Adapt
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun HomeFragmentState.toAdapterList(): List<AdapterItem> = when (mode) {
|
private fun HomeFragmentState.toAdapterList(): List<AdapterItem> = when (mode) {
|
||||||
is Mode.Normal -> normalModeAdapterItems(tabs, collections, expandedCollections)
|
is Mode.Normal -> normalModeAdapterItems(tabs, topSites, collections, expandedCollections)
|
||||||
is Mode.Private -> privateModeAdapterItems(tabs)
|
is Mode.Private -> privateModeAdapterItems(tabs)
|
||||||
is Mode.Onboarding -> onboardingAdapterItems(mode.state)
|
is Mode.Onboarding -> onboardingAdapterItems(mode.state)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.home.sessioncontrol.viewholders
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
|
import kotlinx.android.synthetic.main.component_top_sites.view.*
|
||||||
|
import mozilla.components.feature.top.sites.TopSite
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSitesAdapter
|
||||||
|
|
||||||
|
class TopSiteViewHolder(
|
||||||
|
view: View,
|
||||||
|
interactor: TopSiteInteractor,
|
||||||
|
override val containerView: View? = view
|
||||||
|
) : RecyclerView.ViewHolder(view), LayoutContainer {
|
||||||
|
private val topSitesAdapter = TopSitesAdapter(interactor)
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.top_sites_list.apply {
|
||||||
|
adapter = topSitesAdapter
|
||||||
|
layoutManager = GridLayoutManager(view.context, NUM_COLUMNS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(topSites: List<TopSite>) {
|
||||||
|
topSitesAdapter.submitList(topSites)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LAYOUT_ID = R.layout.component_top_sites
|
||||||
|
const val NUM_COLUMNS = 5
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.home.sessioncontrol.viewholders.topsites
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.android.synthetic.main.top_site_item.view.*
|
||||||
|
import mozilla.components.feature.top.sites.TopSite
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.loadIntoView
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
|
||||||
|
|
||||||
|
class TopSiteItemViewHolder(
|
||||||
|
private val view: View,
|
||||||
|
private val interactor: TopSiteInteractor
|
||||||
|
) : RecyclerView.ViewHolder(view) {
|
||||||
|
private lateinit var topSite: TopSite
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.top_site_item.setOnClickListener {
|
||||||
|
interactor.onSelectTopSite(topSite.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(topSite: TopSite) {
|
||||||
|
this.topSite = topSite
|
||||||
|
view.top_site_title.text = topSite.title
|
||||||
|
view.context.components.core.icons.loadIntoView(view.favicon_image, topSite.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LAYOUT_ID = R.layout.top_site_item
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.home.sessioncontrol.viewholders.topsites
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import mozilla.components.feature.top.sites.TopSite
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
|
||||||
|
|
||||||
|
class TopSitesAdapter(
|
||||||
|
private val interactor: TopSiteInteractor
|
||||||
|
) : ListAdapter<TopSite, TopSiteItemViewHolder>(DiffCallback) {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopSiteItemViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(TopSiteItemViewHolder.LAYOUT_ID, parent, false)
|
||||||
|
return TopSiteItemViewHolder(view, interactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: TopSiteItemViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
private object DiffCallback : DiffUtil.ItemCallback<TopSite>() {
|
||||||
|
override fun areItemsTheSame(oldItem: TopSite, newItem: TopSite) =
|
||||||
|
oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: TopSite, newItem: TopSite) =
|
||||||
|
oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/top_sites_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:listitem="@layout/top_site_item" />
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/top_site_item"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:paddingBottom="6dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/favicon_image"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginBottom="2dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/top_site_title"
|
||||||
|
app:layout_constraintDimensionRatio="1:1"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/top_site_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textColor="?primaryText"
|
||||||
|
android:textSize="10sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/favicon_image" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in New Issue