1
0
Fork 0

For #1114: Show playing tab

master
Sawyer Blatz 2019-09-06 16:11:40 -07:00
parent 215e66fb08
commit 999d3cb963
8 changed files with 254 additions and 43 deletions

View File

@ -6,13 +6,16 @@ package org.mozilla.fenix.ext
import android.content.Context import android.content.Context
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.feature.media.state.MediaState
import org.mozilla.fenix.home.sessioncontrol.Tab import org.mozilla.fenix.home.sessioncontrol.Tab
fun Session.toTab(context: Context, selected: Boolean? = null): Tab { fun Session.toTab(context: Context, selected: Boolean? = null, mediaState: MediaState? = null): Tab {
return Tab( return Tab(
this.id, this.id,
this.url, this.url,
this.url.urlToTrimmedHost(context), this.url.urlToTrimmedHost(context),
this.title, this.title,
selected) selected,
mediaState
)
} }

View File

@ -48,6 +48,11 @@ import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile import mozilla.components.concept.sync.Profile
import mozilla.components.feature.media.ext.getSession
import mozilla.components.feature.media.ext.pauseIfPlaying
import mozilla.components.feature.media.ext.playIfPaused
import mozilla.components.feature.media.state.MediaState
import mozilla.components.feature.media.state.MediaStateMachine
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.BOTTOM import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.BOTTOM
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.END import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.END
@ -144,6 +149,7 @@ class HomeFragment : Fragment(), AccountObserver {
val sessionObserver = BrowserSessionsObserver(sessionManager, singleSessionObserver) { val sessionObserver = BrowserSessionsObserver(sessionManager, singleSessionObserver) {
emitSessionChanges() emitSessionChanges()
} }
lifecycle.addObserver(sessionObserver) lifecycle.addObserver(sessionObserver)
if (!onboarding.userHasBeenOnboarded()) { if (!onboarding.userHasBeenOnboarded()) {
@ -372,6 +378,12 @@ class HomeFragment : Fragment(), AccountObserver {
share(session.url) share(session.url)
} }
} }
is TabAction.PauseMedia -> {
MediaStateMachine.state.pauseIfPlaying()
}
is TabAction.PlayMedia -> {
MediaStateMachine.state.playIfPaused()
}
is TabAction.CloseAll -> { is TabAction.CloseAll -> {
if (pendingSessionDeletion?.deletionJob == null) { if (pendingSessionDeletion?.deletionJob == null) {
removeAllTabsWithUndo( removeAllTabsWithUndo(
@ -926,7 +938,17 @@ class HomeFragment : Fragment(), AccountObserver {
private fun List<Session>.toTabs(): List<Tab> { private fun List<Session>.toTabs(): List<Tab> {
val selected = sessionManager.selectedSession val selected = sessionManager.selectedSession
return this.map { it.toTab(requireContext(), it == selected) } val mediaStateSession = MediaStateMachine.state.getSession()
return this.map {
val mediaState = if (mediaStateSession?.id == it.id) {
MediaStateMachine.state
} else {
null
}
it.toTab(requireContext(), it == selected, mediaState)
}
} }
companion object { companion object {
@ -935,8 +957,6 @@ class HomeFragment : Fragment(), AccountObserver {
private const val ANIM_ON_SCREEN_DELAY = 200L private const val ANIM_ON_SCREEN_DELAY = 200L
private const val FADE_ANIM_DURATION = 150L private const val FADE_ANIM_DURATION = 150L
private const val ANIM_SNACKBAR_DELAY = 100L private const val ANIM_SNACKBAR_DELAY = 100L
private const val ACCESSIBILITY_FOCUS_DELAY = 2000L
private const val TELEMETRY_HOME_IDENITIFIER = "home"
private const val SHARED_TRANSITION_MS = 200L private const val SHARED_TRANSITION_MS = 200L
private const val TAB_ITEM_TRANSITION_NAME = "tab_item" private const val TAB_ITEM_TRANSITION_NAME = "tab_item"
private const val CFR_WIDTH_DIVIDER = 1.7 private const val CFR_WIDTH_DIVIDER = 1.7
@ -966,6 +986,7 @@ private class BrowserSessionsObserver(
*/ */
@OnLifecycleEvent(Lifecycle.Event.ON_START) @OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() { fun onStart() {
MediaStateMachine.register(managerObserver)
manager.register(managerObserver) manager.register(managerObserver)
subscribeToAll() subscribeToAll()
} }
@ -975,6 +996,7 @@ private class BrowserSessionsObserver(
*/ */
@OnLifecycleEvent(Lifecycle.Event.ON_STOP) @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() { fun onStop() {
MediaStateMachine.unregister(managerObserver)
manager.unregister(managerObserver) manager.unregister(managerObserver)
unsubscribeFromAll() unsubscribeFromAll()
} }
@ -995,7 +1017,11 @@ private class BrowserSessionsObserver(
session.unregister(observer) session.unregister(observer)
} }
private val managerObserver = object : SessionManager.Observer { private val managerObserver = object : SessionManager.Observer, MediaStateMachine.Observer {
override fun onStateChanged(state: MediaState) {
onChanged()
}
override fun onSessionAdded(session: Session) { override fun onSessionAdded(session: Session) {
subscribeTo(session) subscribeTo(session)
onChanged() onChanged()

View File

@ -6,6 +6,7 @@ package org.mozilla.fenix.home.sessioncontrol
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
@ -14,6 +15,8 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer import io.reactivex.Observer
import kotlinx.android.synthetic.main.tab_list_row.*
import mozilla.components.feature.media.state.MediaState
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoContentMessageViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.NoContentMessageViewHolder
@ -37,7 +40,28 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
data class TabHeader(val isPrivate: Boolean, val hasTabs: Boolean) : AdapterItem(TabHeaderViewHolder.LAYOUT_ID) data class TabHeader(val isPrivate: Boolean, val hasTabs: Boolean) : AdapterItem(TabHeaderViewHolder.LAYOUT_ID)
data class TabItem(val tab: Tab) : AdapterItem(TabViewHolder.LAYOUT_ID) { data class TabItem(val tab: Tab) : AdapterItem(TabViewHolder.LAYOUT_ID) {
override fun sameAs(other: AdapterItem) = other is TabItem && tab.sessionId == other.tab.sessionId override fun sameAs(other: AdapterItem) = other is TabItem && tab.sessionId == other.tab.sessionId
// Tell the adapter exactly what values have changed so it only has to draw those
override fun getChangePayload(newItem: AdapterItem): Any? {
(newItem as TabItem).let {
val shouldUpdateUrl = newItem.tab.url != this.tab.url
val shouldUpdateHostname = newItem.tab.hostname != this.tab.hostname
val shouldUpdateTitle = newItem.tab.title != this.tab.title
val shouldUpdateSelected = newItem.tab.selected != this.tab.selected
val shouldUpdateMediaState = newItem.tab.mediaState != this.tab.mediaState
return AdapterItemDiffCallback.TabChangePayload(
tab = newItem.tab,
shouldUpdateUrl = shouldUpdateUrl,
shouldUpdateHostname = shouldUpdateHostname,
shouldUpdateTitle = shouldUpdateTitle,
shouldUpdateSelected = shouldUpdateSelected,
shouldUpdateMediaState = shouldUpdateMediaState
)
}
}
} }
object SaveTabGroup : AdapterItem(SaveTabGroupViewHolder.LAYOUT_ID) object SaveTabGroup : AdapterItem(SaveTabGroupViewHolder.LAYOUT_ID)
object PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID) object PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID)
@ -85,6 +109,11 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
* True if this item represents the same value as other. Used by [AdapterItemDiffCallback]. * True if this item represents the same value as other. Used by [AdapterItemDiffCallback].
*/ */
open fun sameAs(other: AdapterItem) = this::class == other::class open fun sameAs(other: AdapterItem) = this::class == other::class
/**
* Returns a payload if there's been a change, or null if not
*/
open fun getChangePayload(newItem: AdapterItem): Any? = null
} }
class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() { class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
@ -92,6 +121,19 @@ class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
@Suppress("DiffUtilEquals") @Suppress("DiffUtilEquals")
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = oldItem == newItem override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = oldItem == newItem
override fun getChangePayload(oldItem: AdapterItem, newItem: AdapterItem): Any? {
return oldItem.getChangePayload(newItem) ?: return super.getChangePayload(oldItem, newItem)
}
data class TabChangePayload(
val tab: Tab,
val shouldUpdateUrl: Boolean,
val shouldUpdateHostname: Boolean,
val shouldUpdateTitle: Boolean,
val shouldUpdateSelected: Boolean,
val shouldUpdateMediaState: Boolean
)
} }
class SessionControlAdapter( class SessionControlAdapter(
@ -133,9 +175,9 @@ class SessionControlAdapter(
val tabHeader = item as AdapterItem.TabHeader val tabHeader = item as AdapterItem.TabHeader
holder.bind(tabHeader.isPrivate, tabHeader.hasTabs) holder.bind(tabHeader.isPrivate, tabHeader.hasTabs)
} }
is TabViewHolder -> holder.bindSession( is TabViewHolder -> {
(item as AdapterItem.TabItem).tab holder.bindSession((item as AdapterItem.TabItem).tab)
) }
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)
@ -152,13 +194,36 @@ class SessionControlAdapter(
(item as AdapterItem.OnboardingSectionHeader).labelBuilder (item as AdapterItem.OnboardingSectionHeader).labelBuilder
) )
is OnboardingManualSignInViewHolder -> holder.bind() is OnboardingManualSignInViewHolder -> holder.bind()
is OnboardingAutomaticSignInViewHolder -> holder.bind( is OnboardingAutomaticSignInViewHolder -> holder.bind((
( (item as AdapterItem.OnboardingAutomaticSignIn).state
( as OnboardingState.SignedOutCanAutoSignIn).withAccount
item as AdapterItem.OnboardingAutomaticSignIn
).state as OnboardingState.SignedOutCanAutoSignIn
).withAccount
) )
} }
} }
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position)
return
}
(payloads[0] as AdapterItemDiffCallback.TabChangePayload).let {
(holder as TabViewHolder).updateTab(it.tab)
// Always set the visibility to GONE to avoid the play button sticking around from previous draws
holder.play_pause_button.visibility = View.GONE
if (it.shouldUpdateHostname) { holder.updateHostname(it.tab.hostname) }
if (it.shouldUpdateTitle) { holder.updateTitle(it.tab.title) }
if (it.shouldUpdateUrl) { holder.updateFavIcon(it.tab.url) }
if (it.shouldUpdateSelected) { holder.updateSelected(it.tab.selected ?: false) }
if (it.shouldUpdateMediaState) {
holder.updatePlayPauseButton(it.tab.mediaState ?: MediaState.None)
}
}
}
} }

View File

@ -5,13 +5,12 @@
package org.mozilla.fenix.home.sessioncontrol package org.mozilla.fenix.home.sessioncontrol
import android.content.Context import android.content.Context
import android.os.Parcelable
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer import io.reactivex.Observer
import kotlinx.android.parcel.Parcelize
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.feature.media.state.MediaState
import mozilla.components.service.fxa.sharing.ShareableAccount import mozilla.components.service.fxa.sharing.ShareableAccount
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
@ -46,14 +45,14 @@ class SessionControlComponent(
} }
} }
@Parcelize
data class Tab( data class Tab(
val sessionId: String, val sessionId: String,
val url: String, val url: String,
val hostname: String, val hostname: String,
val title: String, val title: String,
val selected: Boolean? = null val selected: Boolean? = null,
) : Parcelable var mediaState: MediaState? = null
)
fun List<Tab>.toSessionBundle(context: Context): MutableList<Session> { fun List<Tab>.toSessionBundle(context: Context): MutableList<Session> {
val sessionBundle = mutableListOf<Session>() val sessionBundle = mutableListOf<Session>()
@ -62,7 +61,6 @@ fun List<Tab>.toSessionBundle(context: Context): MutableList<Session> {
sessionBundle.add(session) sessionBundle.add(session)
} }
} }
return sessionBundle return sessionBundle
} }
@ -108,6 +106,8 @@ sealed class TabAction : Action {
data class Select(val tabView: View, val sessionId: String) : TabAction() data class Select(val tabView: View, val sessionId: String) : TabAction()
data class Close(val sessionId: String) : TabAction() data class Close(val sessionId: String) : TabAction()
data class Share(val sessionId: String) : TabAction() data class Share(val sessionId: String) : TabAction()
data class PauseMedia(val sessionId: String) : TabAction()
data class PlayMedia(val sessionId: String) : TabAction()
object PrivateBrowsingLearnMore : TabAction() object PrivateBrowsingLearnMore : TabAction()
} }

View File

@ -14,10 +14,12 @@ import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.tab_list_row.* import kotlinx.android.synthetic.main.tab_list_row.*
import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.feature.media.state.MediaState
import mozilla.components.support.ktx.android.util.dpToFloat import mozilla.components.support.ktx.android.util.dpToFloat
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.loadIntoView import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
import org.mozilla.fenix.home.sessioncontrol.Tab import org.mozilla.fenix.home.sessioncontrol.Tab
@ -31,7 +33,7 @@ class TabViewHolder(
) : ) :
RecyclerView.ViewHolder(view), LayoutContainer { RecyclerView.ViewHolder(view), LayoutContainer {
var tab: Tab? = null internal var tab: Tab? = null
private var tabMenu: TabItemMenu private var tabMenu: TabItemMenu
init { init {
@ -52,9 +54,21 @@ class TabViewHolder(
true true
} }
close_tab_button?.run { close_tab_button.setOnClickListener {
setOnClickListener { actionEmitter.onNext(TabAction.Close(tab?.sessionId!!))
actionEmitter.onNext(TabAction.Close(tab?.sessionId!!)) }
play_pause_button.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS)
play_pause_button.setOnClickListener {
when (tab?.mediaState) {
is MediaState.Playing -> {
actionEmitter.onNext(TabAction.PauseMedia(tab?.sessionId!!))
}
is MediaState.Paused -> {
actionEmitter.onNext(TabAction.PlayMedia(tab?.sessionId!!))
}
} }
} }
@ -72,27 +86,59 @@ class TabViewHolder(
} }
} }
fun bindSession(tab: Tab) { internal fun bindSession(tab: Tab) {
this.tab = tab updateTab(tab)
updateTabUI(tab) updateTitle(tab.title)
item_tab.transitionName = "$TAB_ITEM_TRANSITION_NAME${tab.sessionId}" updateHostname(tab.hostname)
updateFavIcon(tab.url)
updateSelected(tab.selected ?: false) updateSelected(tab.selected ?: false)
updatePlayPauseButton(tab.mediaState ?: MediaState.None)
item_tab.transitionName = "$TAB_ITEM_TRANSITION_NAME${tab.sessionId}"
} }
private fun updateTabUI(tab: Tab) { internal fun updatePlayPauseButton(mediaState: MediaState) {
hostname.text = tab.hostname with(play_pause_button) {
tab_title.text = tab.title visibility = if (mediaState is MediaState.Playing || mediaState is MediaState.Paused) {
favicon_image.context.components.core.icons.loadIntoView(favicon_image, tab.url) View.VISIBLE
} else {
View.GONE
}
if (mediaState is MediaState.Playing) {
play_pause_button.contentDescription =
context.getString(R.string.mozac_feature_media_notification_action_pause)
setImageDrawable(context.getDrawable(R.drawable.pause_with_background))
} else {
play_pause_button.contentDescription =
context.getString(R.string.mozac_feature_media_notification_action_play)
setImageDrawable(context.getDrawable(R.drawable.play_with_background))
}
}
} }
fun updateSelected(selected: Boolean) { internal fun updateTab(tab: Tab) {
this.tab = tab
}
internal fun updateTitle(text: String) {
tab_title.text = text
}
internal fun updateHostname(text: String) {
hostname.text = text
}
internal fun updateFavIcon(url: String) {
favicon_image.context.components.core.icons.loadIntoView(favicon_image, url)
}
internal fun updateSelected(selected: Boolean) {
selected_border.visibility = if (selected) View.VISIBLE else View.GONE selected_border.visibility = if (selected) View.VISIBLE else View.GONE
} }
companion object { companion object {
private const val TAB_ITEM_TRANSITION_NAME = "tab_item" private const val TAB_ITEM_TRANSITION_NAME = "tab_item"
private const val PLAY_PAUSE_BUTTON_EXTRA_DPS = 24
const val LAYOUT_ID = R.layout.tab_list_row const val LAYOUT_ID = R.layout.tab_list_row
const val buttonIncreaseDps = 12
const val favIconBorderRadiusInPx = 4 const val favIconBorderRadiusInPx = 4
} }
} }

View File

@ -0,0 +1,27 @@
<?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/. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<size
android:width="24dp"
android:height="24dp" />
<solid android:color="?above" />
<stroke android:color="?above"
android:width="2dp"/>
</shape>
</item>
<item>
<vector
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?accent"
android:pathData="M12,2a10,10 0,1 0,10 10A10,10 0,0 0,12 2zM10.5,16.125a0.375,0.375 0,0 1,-0.375 0.375h-2.25A0.375,0.375 0,0 1,7.5 16.125v-8.25A0.375,0.375 0,0 1,7.875 7.5h2.25A0.375,0.375 0,0 1,10.5 7.875zM16.5,16.125a0.375,0.375 0,0 1,-0.375 0.375h-2.25a0.375,0.375 0,0 1,-0.375 -0.375v-8.25A0.375,0.375 0,0 1,13.875 7.5h2.25A0.375,0.375 0,0 1,16.5 7.875z"/>
</vector>
</item>
</layer-list>

View File

@ -0,0 +1,27 @@
<?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/. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<size
android:width="24dp"
android:height="24dp" />
<solid android:color="?above" />
<stroke android:color="?above"
android:width="2dp"/>
</shape>
</item>
<item>
<vector
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?accent"
android:pathData="M12,22a10,10 0,1 1,10 -10,10 10,0 0,1 -10,10zM10,16.363l6,-3.5a1,1 0,0 0,0 -1.732l-6,-3.5A1,1 0,0 0,8.5 8.5v7a1,1 0,0 0,1.5 0.863z"/>
</vector>
</item>
</layer-list>

View File

@ -35,6 +35,21 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/play_pause_button"
android:contentDescription="@string/mozac_feature_media_notification_action_pause"
android:layout_width="24dp"
android:layout_height="24dp"
android:elevation="10dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/pause_with_background"
android:visibility="gone"
app:layout_constraintCircleAngle="45"
app:layout_constraintCircleRadius="30dp"
app:layout_constraintCircle="@id/favicon_image"
app:layout_constraintEnd_toEndOf="@id/favicon_image"
app:layout_constraintTop_toTopOf="@id/favicon_image" />
<TextView <TextView
android:id="@+id/hostname" android:id="@+id/hostname"
android:layout_width="0dp" android:layout_width="0dp"
@ -75,13 +90,15 @@
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent"/>
<View <View
android:id="@+id/selected_border" android:id="@+id/selected_border"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:background="@drawable/session_border" android:background="@drawable/session_border"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>