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 mozilla.components.browser.session.Session
import mozilla.components.feature.media.state.MediaState
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(
this.id,
this.url,
this.url.urlToTrimmedHost(context),
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.OAuthAccount
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 org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.BOTTOM
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.END
@ -144,6 +149,7 @@ class HomeFragment : Fragment(), AccountObserver {
val sessionObserver = BrowserSessionsObserver(sessionManager, singleSessionObserver) {
emitSessionChanges()
}
lifecycle.addObserver(sessionObserver)
if (!onboarding.userHasBeenOnboarded()) {
@ -372,6 +378,12 @@ class HomeFragment : Fragment(), AccountObserver {
share(session.url)
}
}
is TabAction.PauseMedia -> {
MediaStateMachine.state.pauseIfPlaying()
}
is TabAction.PlayMedia -> {
MediaStateMachine.state.playIfPaused()
}
is TabAction.CloseAll -> {
if (pendingSessionDeletion?.deletionJob == null) {
removeAllTabsWithUndo(
@ -926,7 +938,17 @@ class HomeFragment : Fragment(), AccountObserver {
private fun List<Session>.toTabs(): List<Tab> {
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 {
@ -935,8 +957,6 @@ class HomeFragment : Fragment(), AccountObserver {
private const val ANIM_ON_SCREEN_DELAY = 200L
private const val FADE_ANIM_DURATION = 150L
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 TAB_ITEM_TRANSITION_NAME = "tab_item"
private const val CFR_WIDTH_DIVIDER = 1.7
@ -966,6 +986,7 @@ private class BrowserSessionsObserver(
*/
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() {
MediaStateMachine.register(managerObserver)
manager.register(managerObserver)
subscribeToAll()
}
@ -975,6 +996,7 @@ private class BrowserSessionsObserver(
*/
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
MediaStateMachine.unregister(managerObserver)
manager.unregister(managerObserver)
unsubscribeFromAll()
}
@ -995,7 +1017,11 @@ private class BrowserSessionsObserver(
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) {
subscribeTo(session)
onChanged()

View File

@ -6,6 +6,7 @@ package org.mozilla.fenix.home.sessioncontrol
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.annotation.LayoutRes
@ -14,6 +15,8 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
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.CollectionViewHolder
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 TabItem(val tab: Tab) : AdapterItem(TabViewHolder.LAYOUT_ID) {
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 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].
*/
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>() {
@ -92,6 +121,19 @@ class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
@Suppress("DiffUtilEquals")
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(
@ -133,9 +175,9 @@ class SessionControlAdapter(
val tabHeader = item as AdapterItem.TabHeader
holder.bind(tabHeader.isPrivate, tabHeader.hasTabs)
}
is TabViewHolder -> holder.bindSession(
(item as AdapterItem.TabItem).tab
)
is TabViewHolder -> {
holder.bindSession((item as AdapterItem.TabItem).tab)
}
is NoContentMessageViewHolder -> {
val (icon, header, description) = item as AdapterItem.NoContentMessage
holder.bind(icon, header, description)
@ -152,13 +194,36 @@ class SessionControlAdapter(
(item as AdapterItem.OnboardingSectionHeader).labelBuilder
)
is OnboardingManualSignInViewHolder -> holder.bind()
is OnboardingAutomaticSignInViewHolder -> holder.bind(
(
(
item as AdapterItem.OnboardingAutomaticSignIn
).state as OnboardingState.SignedOutCanAutoSignIn
).withAccount
is OnboardingAutomaticSignInViewHolder -> holder.bind((
(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
import android.content.Context
import android.os.Parcelable
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import kotlinx.android.parcel.Parcelize
import mozilla.components.browser.session.Session
import mozilla.components.feature.media.state.MediaState
import mozilla.components.service.fxa.sharing.ShareableAccount
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.ext.components
@ -46,14 +45,14 @@ class SessionControlComponent(
}
}
@Parcelize
data class Tab(
val sessionId: String,
val url: String,
val hostname: String,
val title: String,
val selected: Boolean? = null
) : Parcelable
val selected: Boolean? = null,
var mediaState: MediaState? = null
)
fun List<Tab>.toSessionBundle(context: Context): MutableList<Session> {
val sessionBundle = mutableListOf<Session>()
@ -62,7 +61,6 @@ fun List<Tab>.toSessionBundle(context: Context): MutableList<Session> {
sessionBundle.add(session)
}
}
return sessionBundle
}
@ -108,6 +106,8 @@ sealed class TabAction : Action {
data class Select(val tabView: View, val sessionId: String) : TabAction()
data class Close(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()
}

View File

@ -14,10 +14,12 @@ import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.tab_list_row.*
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.feature.media.state.MediaState
import mozilla.components.support.ktx.android.util.dpToFloat
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
import org.mozilla.fenix.home.sessioncontrol.Tab
@ -31,7 +33,7 @@ class TabViewHolder(
) :
RecyclerView.ViewHolder(view), LayoutContainer {
var tab: Tab? = null
internal var tab: Tab? = null
private var tabMenu: TabItemMenu
init {
@ -52,9 +54,21 @@ class TabViewHolder(
true
}
close_tab_button?.run {
setOnClickListener {
actionEmitter.onNext(TabAction.Close(tab?.sessionId!!))
close_tab_button.setOnClickListener {
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) {
this.tab = tab
updateTabUI(tab)
item_tab.transitionName = "$TAB_ITEM_TRANSITION_NAME${tab.sessionId}"
internal fun bindSession(tab: Tab) {
updateTab(tab)
updateTitle(tab.title)
updateHostname(tab.hostname)
updateFavIcon(tab.url)
updateSelected(tab.selected ?: false)
updatePlayPauseButton(tab.mediaState ?: MediaState.None)
item_tab.transitionName = "$TAB_ITEM_TRANSITION_NAME${tab.sessionId}"
}
private fun updateTabUI(tab: Tab) {
hostname.text = tab.hostname
tab_title.text = tab.title
favicon_image.context.components.core.icons.loadIntoView(favicon_image, tab.url)
internal fun updatePlayPauseButton(mediaState: MediaState) {
with(play_pause_button) {
visibility = if (mediaState is MediaState.Playing || mediaState is MediaState.Paused) {
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
}
companion object {
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 buttonIncreaseDps = 12
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_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
android:id="@+id/hostname"
android:layout_width="0dp"
@ -75,13 +90,15 @@
app:layout_constraintTop_toTopOf="parent"/>
<View
android:id="@+id/selected_border"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/session_border"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:id="@+id/selected_border"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/session_border"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>