diff --git a/app/src/main/java/org/mozilla/tv/firefox/MainActivity.kt b/app/src/main/java/org/mozilla/tv/firefox/MainActivity.kt index 8a675b20ba..b705892508 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/MainActivity.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/MainActivity.kt @@ -15,7 +15,8 @@ import android.view.KeyEvent import android.view.View import androidx.lifecycle.Observer import io.sentry.Sentry -import kotlinx.android.synthetic.main.activity_main.container_navigation_overlay +import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.fragment_navigation_overlay_top_nav.navButtonBack import kotlinx.android.synthetic.main.overlay_debug.debugLog import mozilla.components.browser.session.Session import mozilla.components.concept.engine.EngineView @@ -104,7 +105,7 @@ class MainActivity : LocaleAwareAppCompatActivity(), OnUrlEnteredListener, Media serviceLocator.intentLiveData.value = Consumable.from(intentData) // Debug logging display for non public users - // TODO: refactor out the debug variant visibility check in #1953 + // TODO: refactor out the debug variant visibility check in #1953\ BuildConstants.debugLogStr?.apply { debugLog.visibility = View.VISIBLE debugLog.text = this @@ -170,6 +171,7 @@ class MainActivity : LocaleAwareAppCompatActivity(), OnUrlEnteredListener, Media super.onStart() // TODO when MainActivity has a VM, route this call through it serviceLocator.pocketRepo.startBackgroundUpdates() + rootView.viewTreeObserver.addOnGlobalFocusChangeListener(serviceLocator.focusRepo) } override fun onStop() { @@ -177,6 +179,7 @@ class MainActivity : LocaleAwareAppCompatActivity(), OnUrlEnteredListener, Media // TODO when MainActivity has a VM, route this call through it serviceLocator.pocketRepo.stopBackgroundUpdates() TelemetryIntegration.INSTANCE.stopMainActivity() + rootView.viewTreeObserver.removeOnGlobalFocusChangeListener(serviceLocator.focusRepo) } override fun onDestroy() { diff --git a/app/src/main/java/org/mozilla/tv/firefox/ScreenController.kt b/app/src/main/java/org/mozilla/tv/firefox/ScreenController.kt index f83711d05e..b5374ea733 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/ScreenController.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/ScreenController.kt @@ -11,7 +11,6 @@ import android.view.KeyEvent import androidx.fragment.app.FragmentManager import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject -import kotlinx.android.synthetic.main.fragment_navigation_overlay.navUrlInput import mozilla.components.browser.session.Session import org.mozilla.tv.firefox.ScreenControllerStateMachine.ActiveScreen import org.mozilla.tv.firefox.ScreenControllerStateMachine.Transition @@ -143,7 +142,6 @@ class ScreenController(private val sessionRepo: SessionRepo) { transaction.show(overlayFragment) MenuInteractionMonitor.menuOpened() - overlayFragment.navUrlInput.requestFocus() // TODO: Disabled until Overlay refactor is complete #1666 // overlayFragment.navOverlayScrollView.updateOverlayForHomescreen(isOnHomeUrl(fragmentManager)) } else { diff --git a/app/src/main/java/org/mozilla/tv/firefox/architecture/ViewModelFactory.kt b/app/src/main/java/org/mozilla/tv/firefox/architecture/ViewModelFactory.kt index 8965df6d75..1ef099d007 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/architecture/ViewModelFactory.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/architecture/ViewModelFactory.kt @@ -17,6 +17,7 @@ import org.mozilla.tv.firefox.navigationoverlay.ToolbarViewModel import org.mozilla.tv.firefox.settings.SettingsViewModel import org.mozilla.tv.firefox.utils.ServiceLocator import org.mozilla.tv.firefox.webrender.WebRenderHintViewModel +import org.mozilla.tv.firefox.webrender.WebRenderViewModel import org.mozilla.tv.firefox.webrender.cursor.CursorViewModel /** @@ -62,19 +63,26 @@ class ViewModelFactory( ) as T NavigationOverlayViewModel::class.java -> NavigationOverlayViewModel( - serviceLocator.sessionRepo + serviceLocator.sessionRepo, + serviceLocator.focusRepo, + serviceLocator.screenController.currentActiveScreen ) as T OverlayHintViewModel::class.java -> OverlayHintViewModel( - serviceLocator.sessionRepo, - hintContentFactory.getCloseMenuHint() + serviceLocator.sessionRepo, + hintContentFactory.getCloseMenuHint() ) as T WebRenderHintViewModel::class.java -> WebRenderHintViewModel( - serviceLocator.sessionRepo, - serviceLocator.cursorEventRepo, - serviceLocator.screenController, - hintContentFactory.getOpenMenuHint() + serviceLocator.sessionRepo, + serviceLocator.cursorEventRepo, + serviceLocator.screenController, + hintContentFactory.getOpenMenuHint() + ) as T + + WebRenderViewModel::class.java -> WebRenderViewModel( + serviceLocator.focusRepo, + serviceLocator.screenController.currentActiveScreen ) as T // This class needs to either return a ViewModel or throw, so we have no good way of silently handling diff --git a/app/src/main/java/org/mozilla/tv/firefox/ext/View.kt b/app/src/main/java/org/mozilla/tv/firefox/ext/View.kt index c1ea71ea05..a717ea5384 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/ext/View.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/ext/View.kt @@ -17,3 +17,14 @@ val View.isEffectivelyVisible: Boolean get() { } return true } + +// TODO test this. likely off by 1 error here +fun View.itAndAncestorsAreVisible(generationsUp: Int = Int.MAX_VALUE): Boolean { + var generations = generationsUp + var node: View? = this + while (node != null && generations-- > 0) { + if (node.visibility != View.VISIBLE) return false + node = node.parent as? View + } + return true +} diff --git a/app/src/main/java/org/mozilla/tv/firefox/focus/FocusRepo.kt b/app/src/main/java/org/mozilla/tv/firefox/focus/FocusRepo.kt new file mode 100644 index 0000000000..3893bc1bb3 --- /dev/null +++ b/app/src/main/java/org/mozilla/tv/firefox/focus/FocusRepo.kt @@ -0,0 +1,477 @@ +/* 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.tv.firefox.focus + +import android.view.View +import android.view.ViewTreeObserver +import androidx.annotation.VisibleForTesting +import io.reactivex.Observable +import io.reactivex.rxkotlin.Observables +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject +import org.mozilla.tv.firefox.R +import org.mozilla.tv.firefox.ScreenController +import org.mozilla.tv.firefox.ScreenControllerStateMachine +import org.mozilla.tv.firefox.ScreenControllerStateMachine.ActiveScreen +import org.mozilla.tv.firefox.ext.itAndAncestorsAreVisible +import org.mozilla.tv.firefox.pinnedtile.PinnedTileRepo +import org.mozilla.tv.firefox.pocket.PocketVideoRepo +import org.mozilla.tv.firefox.session.SessionRepo +import org.mozilla.tv.firefox.utils.URLs + +private const val INVALID_VIEW_ID = -1 + +// TODO ↓ SEVERIN ↓ + +class FocusRequest(private vararg val ids: Int) { + fun requestOnFirstEnabled(root: View) { + ids.toList() + .firstAvailableViewIfAny(root) + ?.requestFocus() + } +} +private val NO_FOCUS_REQUEST = FocusRequest() + +class FocusNode( + val viewId: Int, + private val nextFocusUpIds: List? = null, + private val nextFocusDownIds: List? = null, + private val nextFocusLeftIds: List? = null, + private val nextFocusRightIds: List? = null +) { + fun updateViewNodeTree(focusedView: View) { + assert(focusedView.id == viewId) // todo assert or require? + val root = focusedView.rootView + + fun List?.applyFirstAvailableIdToView(root: View, action: View.(Int) -> Unit) { + this?: return + val firstAvailable = this.firstAvailableViewIfAny(root) ?: return + focusedView.action(firstAvailable.id) + } + + nextFocusUpIds.applyFirstAvailableIdToView(root) { nextFocusUpId = it } + nextFocusDownIds.applyFirstAvailableIdToView(root) { nextFocusDownId = it } + nextFocusLeftIds.applyFirstAvailableIdToView(root) { nextFocusLeftId = it } + nextFocusRightIds.applyFirstAvailableIdToView(root) { nextFocusRightId = it } + } +} + +private val invalidIds = listOf( + -1, + R.id.nested_web_view +) + +// Sequence prevents unnecessary findViewById calls +private fun List.firstAvailableViewIfAny(root: View): View? { + return this.asSequence() + .mapNotNull { root.findViewById(it) } + .filter { it.isEnabled && it.itAndAncestorsAreVisible(3) } + .firstOrNull() +} + +class FocusRepo( + screenController: ScreenController, + sessionRepo: SessionRepo, + pinnedTileRepo: PinnedTileRepo, + pocketRepo: PocketVideoRepo +) : ViewTreeObserver.OnGlobalFocusChangeListener { + + private val focusChanged = PublishSubject.create>() + + // Edge case, url bar is unaffected on startup (works if it regains focus) + val focusNodeForCurrentlyFocusedView: Observable = focusChanged + .map { oldAndNewFocus -> oldAndNewFocus.second } + .map { id -> + when (id) { + R.id.navUrlInput -> getUrlBarFocusNode() + R.id.pocketVideoMegaTileView -> getPocketMegatileFocusNode() + else -> FocusNode(viewId = id) + } + } + + val focusRequests: Observable by lazy { + Observable.merge( + defaultViewAfterScreenChange, + requestAfterFocusLost + ) + } + + private fun getUrlBarFocusNode() = FocusNode( + viewId = R.id.navUrlInput, + nextFocusUpIds = listOf(R.id.navButtonBack, R.id.navButtonForward, R.id.navButtonReload, R.id.turboButton), + nextFocusDownIds = listOf(R.id.pocketVideoMegaTileView, R.id.megaTileTryAgainButton, R.id.tileContainer, R.id.navUrlInput) + ) + + private fun getPocketMegatileFocusNode() = FocusNode( + viewId = R.id.pocketVideoMegaTileView, + nextFocusDownIds = listOf(R.id.tileContainer, R.id.settingsTileContainer) + ) + + private val defaultViewAfterScreenChange: Observable = screenController.currentActiveScreen + .startWith(ActiveScreen.NAVIGATION_OVERLAY) + .buffer(2, 1) // This emits a list of the previous and current screen. See RxTest.kt + .map { (previousScreen, currentScreen) -> + when (currentScreen) { + ActiveScreen.NAVIGATION_OVERLAY -> { + when (previousScreen) { + ActiveScreen.WEB_RENDER -> FocusRequest(R.id.navButtonBack, R.id.navButtonForward, R.id.navButtonReload, R.id.navUrlInput) + ActiveScreen.POCKET -> FocusRequest(R.id.pocketVideoMegaTileView) + ActiveScreen.SETTINGS -> FocusRequest(R.id.settings_tile_telemetry) + ActiveScreen.NAVIGATION_OVERLAY -> FocusRequest(R.id.navUrlInput) + null -> NO_FOCUS_REQUEST + } + } + ActiveScreen.WEB_RENDER -> FocusRequest(R.id.engineView) + ActiveScreen.POCKET -> FocusRequest(R.id.videoFeed) + ActiveScreen.SETTINGS -> NO_FOCUS_REQUEST + null -> NO_FOCUS_REQUEST + } + } + .filter { it != NO_FOCUS_REQUEST } + .replay(1) + .autoConnect(0) + + private val requestAfterFocusLost: Observable = focusChanged + .filter { (_, newFocus) -> invalidIds.contains(newFocus) } + .flatMap { defaultViewAfterScreenChange.take(1) } + + // TODO ^ SEVERIN ^ + + data class State( + val focusNode: OldFocusNode, + val defaultFocusMap: HashMap + ) + + enum class Event { + ScreenChange, + RequestFocus // to handle lost focus + } + + /** + * OldFocusNode describes quasi-directional focusable paths given viewId + */ + data class OldFocusNode( + val viewId: Int, + val nextFocusUpId: Int? = null, + val nextFocusDownId: Int? = null, + val nextFocusLeftId: Int? = null, + val nextFocusRightId: Int? = null + ) { + fun updateViewNodeTree(view: View) { + assert(view.id == viewId) + + nextFocusUpId?.let { view.nextFocusUpId = it } + nextFocusDownId?.let { view.nextFocusDownId = it } + nextFocusLeftId?.let { view.nextFocusLeftId = it } + nextFocusRightId?.let { view.nextFocusRightId = it } + } + } + + // TODO: potential for telemetry? + private val _state: BehaviorSubject = BehaviorSubject.create() + + private val _events: Subject = PublishSubject.create() + val events: Observable = _events.hide() + + // Keep track of prevScreen to identify screen transitions + private var prevScreen: ScreenControllerStateMachine.ActiveScreen = + ScreenControllerStateMachine.ActiveScreen.NAVIGATION_OVERLAY + + private val _focusUpdate = Observables.combineLatest( + _state, + screenController.currentActiveScreen, + sessionRepo.state, + pinnedTileRepo.isEmpty, + pocketRepo.feedState) { state, activeScreen, sessionState, pinnedTilesIsEmpty, pocketState -> + dispatchFocusUpdates(state.focusNode, activeScreen, sessionState, pinnedTilesIsEmpty, pocketState) + } + + val focusUpdate: Observable = _focusUpdate.hide() + + init { + initializeDefaultFocus() + } + + override fun onGlobalFocusChanged(oldFocus: View?, newFocus: View?) { +// fun BehaviorSubject.onNextIfNew(value: T) { +// val currState = this.value as State +// val newState = value as State +// if (currState.focusNode.viewId != newState.focusNode.viewId && +// newState.focusNode.viewId != -1) +// this.onNext(value) +// } + +// val newState = State( +// focusNode = OldFocusNode(it.id), +// defaultFocusMap = _state.value!!.defaultFocusMap) +// +// _state.onNextIfNew(newState) + val oldId = oldFocus?.id ?: -1 + val newId = newFocus?.id ?: -1 + focusChanged.onNext(oldId to newId) + } + + private fun initializeDefaultFocus() { + val focusMap = HashMap() + focusMap[ScreenControllerStateMachine.ActiveScreen.NAVIGATION_OVERLAY] = R.id.navUrlInput + focusMap[ScreenControllerStateMachine.ActiveScreen.WEB_RENDER] = R.id.engineView + focusMap[ScreenControllerStateMachine.ActiveScreen.POCKET] = R.id.videoFeed + + val newState = State( + focusNode = OldFocusNode(R.id.navUrlInput), + defaultFocusMap = focusMap) + + _state.onNext(newState) + } + + @VisibleForTesting + private fun dispatchFocusUpdates( + focusNode: OldFocusNode, + activeScreen: ScreenControllerStateMachine.ActiveScreen, + sessionState: SessionRepo.State, + pinnedTilesIsEmpty: Boolean, + pocketState: PocketVideoRepo.FeedState + ): State { + + var newState = _state.value!! + val focusMap = _state.value!!.defaultFocusMap + when (activeScreen) { + ScreenControllerStateMachine.ActiveScreen.NAVIGATION_OVERLAY -> { + + // Check previous screen for defaultFocusMap updates + when (prevScreen) { + ScreenControllerStateMachine.ActiveScreen.WEB_RENDER -> { + newState = updateDefaultFocusForOverlayWhenTransitioningFromWebRender( + focusMap, + sessionState) + } + ScreenControllerStateMachine.ActiveScreen.POCKET -> { + newState = updateDefaultFocusForOverlayWhenTransitioningFromPocket(focusMap) + } + else -> Unit + } + + when (focusNode.viewId) { + R.id.navUrlInput -> + newState = updateNavUrlInputFocusTree( + focusNode, + sessionState, + pinnedTilesIsEmpty, + pocketState) + R.id.navButtonReload -> { + newState = updateReloadButtonFocusTree(focusNode, sessionState) + } + R.id.navButtonForward -> { + newState = updateForwardButtonFocusTree(focusNode, sessionState) + } + R.id.pocketVideoMegaTileView -> { + newState = updatePocketMegaTileFocusTree(focusNode, pinnedTilesIsEmpty) + } + R.id.megaTileTryAgainButton -> { + newState = handleLostFocusInOverlay( + focusNode, + sessionState, + pinnedTilesIsEmpty, + pocketState) + } + R.id.home_tile -> { + // If pinnedTiles is empty and current focus is on home_tile, we need to + // restore lost focus (this happens when you remove all tiles in the overlay) + if (pinnedTilesIsEmpty) { + newState = handleLostFocusInOverlay( + focusNode, + sessionState, + pinnedTilesIsEmpty, + pocketState) + } + } + INVALID_VIEW_ID -> { + // Focus is lost so default it to navUrlInput and send a [Event.RequestFocus] + val newFocusNode = OldFocusNode(R.id.navUrlInput) + + newState = updateNavUrlInputFocusTree( + newFocusNode, + sessionState, + pinnedTilesIsEmpty, + pocketState) + _events.onNext(Event.RequestFocus) + } + } + } + ScreenControllerStateMachine.ActiveScreen.WEB_RENDER -> {} + ScreenControllerStateMachine.ActiveScreen.POCKET -> {} + ScreenControllerStateMachine.ActiveScreen.SETTINGS -> Unit + } + + if (prevScreen != activeScreen) { + _events.onNext(Event.ScreenChange) + prevScreen = activeScreen + } + + return newState + } + + private fun updateNavUrlInputFocusTree( + focusedNavUrlInputNode: OldFocusNode, + sessionState: SessionRepo.State, + pinnedTilesIsEmpty: Boolean, + pocketState: PocketVideoRepo.FeedState + ): State { + + assert(focusedNavUrlInputNode.viewId == R.id.navUrlInput) + + val nextFocusDownId = when { + pocketState is PocketVideoRepo.FeedState.FetchFailed -> R.id.megaTileTryAgainButton + pocketState is PocketVideoRepo.FeedState.Inactive -> { + if (pinnedTilesIsEmpty) { + R.id.navUrlInput + } else { + R.id.tileContainer + } + } + else -> R.id.pocketVideoMegaTileView + } + + val nextFocusUpId = when { + sessionState.backEnabled -> R.id.navButtonBack + sessionState.forwardEnabled -> R.id.navButtonForward + sessionState.currentUrl != URLs.APP_URL_HOME -> R.id.navButtonReload + else -> R.id.turboButton + } + + return State( + focusNode = OldFocusNode( + focusedNavUrlInputNode.viewId, + nextFocusUpId, + nextFocusDownId), + defaultFocusMap = _state.value!!.defaultFocusMap) + } + + private fun updateReloadButtonFocusTree( + focusedReloadButtonNode: OldFocusNode, + sessionState: SessionRepo.State + ): State { + + assert(focusedReloadButtonNode.viewId == R.id.navButtonReload) + + val nextFocusLeftId = when { + sessionState.forwardEnabled -> R.id.navButtonForward + sessionState.backEnabled -> R.id.navButtonBack + else -> R.id.navButtonReload + } + + return State( + focusNode = OldFocusNode( + focusedReloadButtonNode.viewId, + nextFocusLeftId = nextFocusLeftId), + defaultFocusMap = _state.value!!.defaultFocusMap) + } + + private fun updateForwardButtonFocusTree( + focusedForwardButtonNode: OldFocusNode, + sessionState: SessionRepo.State + ): State { + + assert(focusedForwardButtonNode.viewId == R.id.navButtonForward) + + val nextFocusLeftId = when { + sessionState.backEnabled -> R.id.navButtonBack + else -> R.id.navButtonForward + } + + return State( + focusNode = OldFocusNode( + focusedForwardButtonNode.viewId, + nextFocusLeftId = nextFocusLeftId), + defaultFocusMap = _state.value!!.defaultFocusMap) + } + + private fun updatePocketMegaTileFocusTree( + focusedPocketMegatTileNode: OldFocusNode, + pinnedTilesIsEmpty: Boolean + ): State { + + assert(focusedPocketMegatTileNode.viewId == R.id.pocketVideoMegaTileView || + focusedPocketMegatTileNode.viewId == R.id.megaTileTryAgainButton) + + val nextFocusDownId = when { + pinnedTilesIsEmpty -> R.id.settingsTileContainer + else -> R.id.tileContainer + } + + return State( + focusNode = OldFocusNode( + focusedPocketMegatTileNode.viewId, + nextFocusDownId = nextFocusDownId), + defaultFocusMap = _state.value!!.defaultFocusMap) + } + + /** + * Two possible scenarios for losing focus when in overlay: + * 1. When all the pinned tiles are removed, tilContainer no longer needs focus + * 2. When click on [megaTileTryAgainButton] + */ + private fun handleLostFocusInOverlay( + lostFocusNode: OldFocusNode, + sessionState: SessionRepo.State, + pinnedTilesIsEmpty: Boolean, + pocketState: PocketVideoRepo.FeedState + ): State { + + assert(lostFocusNode.viewId == R.id.tileContainer || + lostFocusNode.viewId == R.id.megaTileTryAgainButton) + + val viewId = when (pocketState) { + PocketVideoRepo.FeedState.FetchFailed -> R.id.megaTileTryAgainButton + PocketVideoRepo.FeedState.Inactive -> R.id.navUrlInput + else -> R.id.pocketVideoMegaTileView + } + + val newFocusNode = OldFocusNode(viewId) + val newState = if (newFocusNode.viewId == R.id.navUrlInput) { + updateNavUrlInputFocusTree(newFocusNode, sessionState, pinnedTilesIsEmpty, pocketState) + } else { + updatePocketMegaTileFocusTree(newFocusNode, pinnedTilesIsEmpty) + } + + // Request focus on newState + if (newFocusNode.viewId != lostFocusNode.viewId) { + _events.onNext(Event.RequestFocus) + } + + return newState + } + + private fun updateDefaultFocusForOverlayWhenTransitioningFromWebRender( + focusMap: HashMap, + sessionState: SessionRepo.State + ): State { + + // It doesn't make sense to be able to transition to WebRender if currUrl == APP_URL_HOME + assert(sessionState.currentUrl != URLs.APP_URL_HOME) + + focusMap[ScreenControllerStateMachine.ActiveScreen.NAVIGATION_OVERLAY] = when { + sessionState.backEnabled -> R.id.navButtonBack + sessionState.forwardEnabled -> R.id.navButtonForward + else -> R.id.navButtonReload + } + + return State( + focusNode = _state.value!!.focusNode, + defaultFocusMap = focusMap) + } + + private fun updateDefaultFocusForOverlayWhenTransitioningFromPocket( + focusMap: HashMap + ): State { + focusMap[ScreenControllerStateMachine.ActiveScreen.NAVIGATION_OVERLAY] = + R.id.pocketVideoMegaTileView + + return State( + focusNode = _state.value!!.focusNode, + defaultFocusMap = focusMap) + } +} diff --git a/app/src/main/java/org/mozilla/tv/firefox/navigationoverlay/NavigationOverlayFragment.kt b/app/src/main/java/org/mozilla/tv/firefox/navigationoverlay/NavigationOverlayFragment.kt index 505613a925..5a071d9dd9 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/navigationoverlay/NavigationOverlayFragment.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/navigationoverlay/NavigationOverlayFragment.kt @@ -20,13 +20,12 @@ import android.view.ViewGroup import android.widget.ScrollView import android.widget.Toast import androidx.core.content.ContextCompat -import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.widget.NestedScrollView import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView +import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable import io.reactivex.rxkotlin.addTo @@ -35,8 +34,6 @@ import kotlinx.android.synthetic.main.fragment_navigation_overlay_orig.pocketVid import kotlinx.android.synthetic.main.fragment_navigation_overlay_orig.settingsTileContainer import kotlinx.android.synthetic.main.fragment_navigation_overlay_orig.tileContainer import kotlinx.android.synthetic.main.fragment_navigation_overlay_top_nav.exitButton -import kotlinx.android.synthetic.main.fragment_navigation_overlay_top_nav.navButtonForward -import kotlinx.android.synthetic.main.fragment_navigation_overlay_top_nav.navButtonReload import kotlinx.android.synthetic.main.hint_bar.hintBarContainer import kotlinx.android.synthetic.main.pocket_video_mega_tile.megaTileTryAgainButton import kotlinx.android.synthetic.main.pocket_video_mega_tile.pocketErrorContainer @@ -46,10 +43,8 @@ import kotlinx.coroutines.Job import org.mozilla.tv.firefox.MainActivity import org.mozilla.tv.firefox.R import org.mozilla.tv.firefox.architecture.FirefoxViewModelProviders -import org.mozilla.tv.firefox.architecture.FocusOnShowDelegate import org.mozilla.tv.firefox.experiments.ExperimentConfig import org.mozilla.tv.firefox.ext.forceExhaustive -import org.mozilla.tv.firefox.ext.isEffectivelyVisible import org.mozilla.tv.firefox.ext.isVoiceViewEnabled import org.mozilla.tv.firefox.ext.serviceLocator import org.mozilla.tv.firefox.hint.HintBinder @@ -59,13 +54,13 @@ import org.mozilla.tv.firefox.navigationoverlay.channels.SettingsChannelAdapter import org.mozilla.tv.firefox.navigationoverlay.channels.SettingsScreen import org.mozilla.tv.firefox.pinnedtile.PinnedTileAdapter import org.mozilla.tv.firefox.pinnedtile.PinnedTileViewModel -import org.mozilla.tv.firefox.pocket.PocketVideoFragment import org.mozilla.tv.firefox.pocket.PocketViewModel import org.mozilla.tv.firefox.telemetry.TelemetryIntegration import org.mozilla.tv.firefox.telemetry.UrlTextInputLocation import org.mozilla.tv.firefox.utils.ServiceLocator import org.mozilla.tv.firefox.widget.InlineAutocompleteEditText import java.lang.ref.WeakReference +import java.util.concurrent.TimeUnit private const val SHOW_UNPIN_TOAST_COUNTER_PREF = "show_upin_toast_counter" private const val MAX_UNPIN_TOAST_COUNT = 3 @@ -140,10 +135,8 @@ class NavigationOverlayFragment : Fragment() { Unit } - private var currFocus: View? = null - get() = activity?.currentFocus - private lateinit var serviceLocator: ServiceLocator + private lateinit var navigationOverlayViewModel: NavigationOverlayViewModel private lateinit var toolbarViewModel: ToolbarViewModel private lateinit var pinnedTileViewModel: PinnedTileViewModel private lateinit var pocketViewModel: PocketViewModel @@ -151,8 +144,8 @@ class NavigationOverlayFragment : Fragment() { private lateinit var tileAdapter: PinnedTileAdapter - // TODO: remove this when FocusRepo is in place #1395 - private var defaultFocusTag = NavigationOverlayFragment.FRAGMENT_TAG + private lateinit var rootView: View + @Deprecated(message = "VM state should be used reactively, not imperatively. See #1395, which will fix this") private var lastPocketState: PocketViewModel.State? = null @@ -161,6 +154,7 @@ class NavigationOverlayFragment : Fragment() { serviceLocator = context!!.serviceLocator + navigationOverlayViewModel = FirefoxViewModelProviders.of(this).get(NavigationOverlayViewModel::class.java) toolbarViewModel = FirefoxViewModelProviders.of(this).get(ToolbarViewModel::class.java) pinnedTileViewModel = FirefoxViewModelProviders.of(this).get(PinnedTileViewModel::class.java) pocketViewModel = FirefoxViewModelProviders.of(this).get(PocketViewModel::class.java) @@ -179,10 +173,11 @@ class NavigationOverlayFragment : Fragment() { ToolbarUiController( toolbarViewModel, ::exitFirefox, - { updateFocusableViews() }, onNavigationEvent ).onCreateView(view, viewLifecycleOwner, fragmentManager!!) + rootView = view + // TODO: Add back in once #1666 is ready to land. /* // Handle split overlay state on homescreen or webrender @@ -210,24 +205,30 @@ class NavigationOverlayFragment : Fragment() { val tintDrawable: (Drawable?) -> Unit = { it?.setTint(ContextCompat.getColor(context!!, R.color.photonGrey10_a60p)) } navUrlInput.compoundDrawablesRelative.forEach(tintDrawable) - // TODO: remove this when FocusRepo is in place #1395 - when (defaultFocusTag) { - PocketVideoFragment.FRAGMENT_TAG -> { - pocketVideoMegaTileView.requestFocus() - defaultFocusTag = NavigationOverlayFragment.FRAGMENT_TAG - } - NavigationOverlayFragment.FRAGMENT_TAG -> navUrlInput.requestFocus() - } - registerForContextMenu(tileContainer) - - updateFocusableViews() } override fun onStart() { super.onStart() + observeRequestFocus() + .addTo(compositeDisposable) + observeFocusState() + .addTo(compositeDisposable) + observeTilesContainer() + .addTo(compositeDisposable) observePocketState() .addTo(compositeDisposable) + navigationOverlayViewModel.focusRequests + .subscribe { focusRequest -> + view?.let { view -> focusRequest.requestOnFirstEnabled(view) } + }.addTo(compositeDisposable) + navigationOverlayViewModel.focusNodeForCurrentlyFocusedView + .subscribe { focusNode -> + rootView.findViewById(focusNode.viewId)?.let { focusedView -> + focusNode.updateViewNodeTree(focusedView) + } + }.addTo(compositeDisposable) + HintBinder.bindHintsToView(hintViewModel, hintBarContainer, animate = false) .forEach { compositeDisposable.add(it) } } @@ -237,15 +238,37 @@ class NavigationOverlayFragment : Fragment() { compositeDisposable.clear() } - override fun onHiddenChanged(hidden: Boolean) { - FocusOnShowDelegate().onHiddenChanged(this, hidden) - super.onHiddenChanged(hidden) - } - private fun exitFirefox() { activity!!.moveTaskToBack(true) } + private fun observeRequestFocus(): Disposable { + return navigationOverlayViewModel.focusRequest + .subscribe { _ -> +// val viewToFocus = rootView.findViewById(viewId) +// viewToFocus?.requestFocus() + } + } + + private fun observeFocusState(): Disposable { + return navigationOverlayViewModel.focusUpdate + .subscribe { _ -> +// rootView.findViewById(focusNode.viewId)?.let { focusedView -> +// focusNode.updateViewNodeTree(focusedView) +// } + } + } + + private fun observeTilesContainer(): Disposable { + return pinnedTileViewModel.isEmpty.subscribe { isEmpty -> + if (isEmpty) { + tileContainer.visibility = View.GONE + } else { + tileContainer.visibility = View.VISIBLE + } + } + } + private fun observePocketState(): Disposable { return pocketViewModel.state .subscribe { state -> @@ -274,6 +297,9 @@ class NavigationOverlayFragment : Fragment() { pocketVideosContainer.visibility = View.GONE pocketErrorContainer.visibility = View.VISIBLE + // View.focusable = INT only available Android API > 26 :( + pocketVideoMegaTileView.setFocusable(false) + pocketMegaTileLoadError.text = resources.getString(R.string.pocket_video_feed_failed_to_load, resources.getString(R.string.pocket_brand_name)) megaTileTryAgainButton.contentDescription = resources.getString(R.string.pocket_video_feed_failed_to_load, @@ -282,16 +308,15 @@ class NavigationOverlayFragment : Fragment() { megaTileTryAgainButton.setOnClickListener { _ -> pocketViewModel.update() initMegaTile() - updateFocusableViews() - pocketVideoMegaTileView.requestFocus() } - updateFocusableViews() } private fun hideMegaTileError() { pocketVideosContainer.visibility = View.VISIBLE pocketErrorContainer.visibility = View.GONE - updateFocusableViews() + + // View.focusable = INT only available Android API > 26 :( + pocketVideoMegaTileView.setFocusable(true) } private fun initMegaTile() { @@ -334,13 +359,12 @@ class NavigationOverlayFragment : Fragment() { pinnedTileViewModel.getTileList().observe(viewLifecycleOwner, Observer { if (it != null) { tileAdapter.setTiles(it) - updateFocusableViews() } }) adapter = tileAdapter - layoutManager = HomeTileManager(context, COL_COUNT) + layoutManager = GridLayoutManager(context, COL_COUNT) setHasFixedSize(true) @@ -370,7 +394,6 @@ class NavigationOverlayFragment : Fragment() { // that the Uri is valid, so we do not do error handling here. // TODO: NavigationOverlayFragment->ViewModel->Repo pinnedTileViewModel.unpin(tileToRemove.url) - checkIfTilesFocusNeedRefresh() TelemetryIntegration.INSTANCE.homeTileRemovedEvent(tileToRemove) return true } @@ -393,67 +416,6 @@ class NavigationOverlayFragment : Fragment() { ) } - private fun updateFocusableViews(focusedView: View? = currFocus) { // TODO this will be replaced when FocusRepo is introduced - val toolbarState = toolbarViewModel.state.value - - // Prevent the focus from looping to the bottom row when reaching the last - // focusable element in the top row - navButtonReload.nextFocusLeftId = when { - toolbarState?.forwardEnabled == true -> R.id.navButtonForward - toolbarState?.backEnabled == true -> R.id.navButtonBack - else -> R.id.navButtonReload - } - navButtonForward.nextFocusLeftId = when { - toolbarState?.backEnabled == true -> R.id.navButtonBack - else -> R.id.navButtonForward - } - - navUrlInput.nextFocusDownId = when { - @Suppress("DEPRECATION") - lastPocketState is PocketViewModel.State.Feed -> R.id.pocketVideoMegaTileView - @Suppress("DEPRECATION") - lastPocketState === PocketViewModel.State.Error -> R.id.megaTileTryAgainButton - tileAdapter.itemCount == 0 -> R.id.navUrlInput - else -> R.id.tileContainer - } - - navUrlInput.nextFocusUpId = when { - toolbarState?.backEnabled == true -> R.id.navButtonBack - toolbarState?.forwardEnabled == true -> R.id.navButtonForward - toolbarState?.refreshEnabled == true -> R.id.navButtonReload - toolbarState?.pinEnabled == true -> R.id.pinButton - else -> R.id.turboButton - } - - pocketVideoMegaTileView.nextFocusDownId = when { - tileAdapter.itemCount == 0 -> R.id.pocketVideoMegaTileView - else -> R.id.tileContainer - } - - // We may have lost focus when disabling the focused view above. - // This looks more complex than is necessary, but the simpler implementation - // led to problems. See the commit message for 45940fa - val isFocusLost = focusedView != null && currFocus == null - if (isFocusLost) { - navUrlInput.requestFocus() - } - } - - /** - * Focus may be lost if all pinned items are removed via onContextItemSelected() - * FIXME: requires OverlayFragment (LifecycleOwner) -> OverlayVM -> FocusRepo - */ - private fun checkIfTilesFocusNeedRefresh() { - if (tileAdapter.itemCount == 0) { - if (pocketVideosContainer.isVisible) { - pocketVideoMegaTileView.requestFocus() - } else { - megaTileTryAgainButton.requestFocus() - } - } - updateFocusableViews() - } - override fun onDestroyView() { super.onDestroyView() @@ -462,28 +424,6 @@ class NavigationOverlayFragment : Fragment() { // but it'll add complexity that I don't think is probably worth it. uiLifecycleCancelJob.cancel() } - - inner class HomeTileManager( - context: Context, - colCount: Int - ) : GridLayoutManager(context, colCount) { - override fun onRequestChildFocus(parent: RecyclerView, state: RecyclerView.State, child: View, focused: View?): Boolean { - var position = spanCount - if (focused != null) { - position = getPosition(focused) - } - - // if position is less than spanCount, implies first row - if (position < spanCount) { - focused?.nextFocusUpId = when { - pocketVideosContainer.isEffectivelyVisible -> R.id.pocketVideoMegaTileView - megaTileTryAgainButton.isEffectivelyVisible -> R.id.megaTileTryAgainButton - else -> R.id.navUrlInput - } - } - return super.onRequestChildFocus(parent, state, child, focused) - } - } } /** diff --git a/app/src/main/java/org/mozilla/tv/firefox/navigationoverlay/NavigationOverlayViewModel.kt b/app/src/main/java/org/mozilla/tv/firefox/navigationoverlay/NavigationOverlayViewModel.kt index daa3bad5cd..267f67a926 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/navigationoverlay/NavigationOverlayViewModel.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/navigationoverlay/NavigationOverlayViewModel.kt @@ -6,11 +6,40 @@ package org.mozilla.tv.firefox.navigationoverlay import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel +import io.reactivex.Observable +import io.reactivex.rxkotlin.withLatestFrom +import org.mozilla.tv.firefox.R +import org.mozilla.tv.firefox.ScreenControllerStateMachine import org.mozilla.tv.firefox.ext.map +import org.mozilla.tv.firefox.focus.FocusRepo import org.mozilla.tv.firefox.session.SessionRepo import org.mozilla.tv.firefox.utils.URLs -class NavigationOverlayViewModel(sessionRepo: SessionRepo) : ViewModel() { +class NavigationOverlayViewModel( + sessionRepo: SessionRepo, + focusRepo: FocusRepo, + currActiveScreen: Observable +) : ViewModel() { + + val focusRequest: Observable = focusRepo.events.withLatestFrom(focusRepo.focusUpdate, currActiveScreen) + .filter { (_, _, activeScreen) -> + activeScreen == ScreenControllerStateMachine.ActiveScreen.NAVIGATION_OVERLAY + } + .map { (event, state, activeScreen) -> + when (event) { + FocusRepo.Event.ScreenChange -> + state.defaultFocusMap[activeScreen] ?: R.id.navUrlInput + FocusRepo.Event.RequestFocus -> + state.focusNode.viewId + } + } + + val focusUpdate: Observable = focusRepo.focusUpdate.map { it.focusNode } + + val focusRequests = focusRepo.focusRequests + + val focusNodeForCurrentlyFocusedView = focusRepo.focusNodeForCurrentlyFocusedView + @Suppress("DEPRECATION") val viewIsSplit: LiveData = sessionRepo.legacyState.map { it.currentUrl != URLs.APP_URL_HOME diff --git a/app/src/main/java/org/mozilla/tv/firefox/navigationoverlay/ToolbarUiController.kt b/app/src/main/java/org/mozilla/tv/firefox/navigationoverlay/ToolbarUiController.kt index d0ea501806..1a6de196fc 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/navigationoverlay/ToolbarUiController.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/navigationoverlay/ToolbarUiController.kt @@ -39,7 +39,6 @@ private const val WRAP_CONTENT = LinearLayout.LayoutParams.WRAP_CONTENT class ToolbarUiController( private val toolbarViewModel: ToolbarViewModel, private val exitFirefox: () -> Unit, - private val updateFocusableViews: () -> Unit, private val onNavigationEvent: (NavigationEvent, String?, InlineAutocompleteEditText.AutocompleteResult?) -> Unit ) { @@ -142,8 +141,6 @@ class ToolbarUiController( updateOverlayButtonState(it.refreshEnabled, layout.navButtonReload) updateOverlayButtonState(it.desktopModeEnabled, layout.desktopModeButton) - updateFocusableViews() - layout.pinButton.isChecked = it.pinChecked layout.desktopModeButton.isChecked = it.desktopModeChecked layout.turboButton.isChecked = it.turboChecked diff --git a/app/src/main/java/org/mozilla/tv/firefox/pinnedtile/PinnedTileRepo.kt b/app/src/main/java/org/mozilla/tv/firefox/pinnedtile/PinnedTileRepo.kt index f3001cb55a..75b4332f6a 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/pinnedtile/PinnedTileRepo.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/pinnedtile/PinnedTileRepo.kt @@ -37,6 +37,8 @@ class PinnedTileRepo(private val applicationContext: Application) { BehaviorSubject.create() val pinnedTiles: Observable> = _pinnedTiles.hide() + val isEmpty: Observable = _pinnedTiles.map { it.size == 0 }.distinctUntilChanged() + @Deprecated(message = "Use PinnedTileRepo.pinnedTiles for new code") val legacyPinnedTiles = LiveDataReactiveStreams .fromPublisher(pinnedTiles.toFlowable(BackpressureStrategy.LATEST)) diff --git a/app/src/main/java/org/mozilla/tv/firefox/pinnedtile/PinnedTileViewModel.kt b/app/src/main/java/org/mozilla/tv/firefox/pinnedtile/PinnedTileViewModel.kt index a33ab89732..592fd82009 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/pinnedtile/PinnedTileViewModel.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/pinnedtile/PinnedTileViewModel.kt @@ -6,7 +6,6 @@ package org.mozilla.tv.firefox.pinnedtile import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel /** @@ -19,7 +18,7 @@ import androidx.lifecycle.ViewModel class PinnedTileViewModel(private val pinnedTileRepo: PinnedTileRepo) : ViewModel() { private val _tilesList = MediatorLiveData>() - val isEmpty: LiveData = Transformations.map(_tilesList) { input -> input.isEmpty() } + val isEmpty = pinnedTileRepo.isEmpty init { @Suppress("DEPRECATION") diff --git a/app/src/main/java/org/mozilla/tv/firefox/utils/ServiceLocator.kt b/app/src/main/java/org/mozilla/tv/firefox/utils/ServiceLocator.kt index 623ed9e344..b7156e0aed 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/utils/ServiceLocator.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/utils/ServiceLocator.kt @@ -15,6 +15,7 @@ import org.mozilla.tv.firefox.experiments.ExperimentsProvider import org.mozilla.tv.firefox.experiments.FretboardProvider import org.mozilla.tv.firefox.ext.getAccessibilityManager import org.mozilla.tv.firefox.ext.webRenderComponents +import org.mozilla.tv.firefox.focus.FocusRepo import org.mozilla.tv.firefox.framework.FrameworkRepo import org.mozilla.tv.firefox.pinnedtile.PinnedTileRepo import org.mozilla.tv.firefox.pocket.PocketEndpoint @@ -77,6 +78,7 @@ open class ServiceLocator(val app: Application) { val sessionUseCases get() = app.webRenderComponents.sessionUseCases val searchEngineManager by lazy { SearchEngineManagerFactory.create(app) } val cursorEventRepo by lazy { CursorEventRepo(screenController) } + val focusRepo by lazy { FocusRepo(screenController, sessionRepo, pinnedTileRepo, pocketRepo) } open val frameworkRepo = FrameworkRepo.newInstanceAndInit(app.getAccessibilityManager()) open val pinnedTileRepo by lazy { PinnedTileRepo(app) } diff --git a/app/src/main/java/org/mozilla/tv/firefox/webrender/WebRenderFragment.kt b/app/src/main/java/org/mozilla/tv/firefox/webrender/WebRenderFragment.kt index 8d53c58c7b..eea939e064 100644 --- a/app/src/main/java/org/mozilla/tv/firefox/webrender/WebRenderFragment.kt +++ b/app/src/main/java/org/mozilla/tv/firefox/webrender/WebRenderFragment.kt @@ -36,7 +36,6 @@ import org.mozilla.tv.firefox.MediaSessionHolder import org.mozilla.tv.firefox.R import org.mozilla.tv.firefox.ScreenControllerStateMachine.ActiveScreen import org.mozilla.tv.firefox.architecture.FirefoxViewModelProviders -import org.mozilla.tv.firefox.architecture.FocusOnShowDelegate import org.mozilla.tv.firefox.ext.focusedDOMElement import org.mozilla.tv.firefox.ext.forceExhaustive import org.mozilla.tv.firefox.ext.isYoutubeTV @@ -87,9 +86,14 @@ class WebRenderFragment : EngineViewLifecycleFragment(), Session.Observer { // work properly, so we !! private val youtubeBackHandler by lazy { YouTubeBackHandler(engineView!!, activity as MainActivity) } + private lateinit var webRenderViewModel: WebRenderViewModel + private lateinit var rootView: View + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initSession() + + webRenderViewModel = FirefoxViewModelProviders.of(this).get(WebRenderViewModel::class.java) } @SuppressLint("RestrictedApi") @@ -158,19 +162,19 @@ class WebRenderFragment : EngineViewLifecycleFragment(), Session.Observer { return layout } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + rootView = view + } + + // TODO: this method needs to be renamed (#2053); preliminary onStart() setup override fun onEngineViewCreated(engineView: EngineView): Disposable? { return serviceLocator?.screenController?.currentActiveScreen?.subscribe { if (it == ActiveScreen.WEB_RENDER) { // Cache focused DOM element just before WebView gains focus. See comment in // FocusedDOMElementCacheInterface for details engineView.focusedDOMElement.cache() - - // EngineView focus may be lost after waking up from sleep & screen saver. - // Forcibly request focus onStart(), after DOMElement cache, IFF webRenderFragment - // is the current ActiveScreen - // TODO: move this when focus repo is in place (#1395) - // TODO: this method needs to be renamed (#2053); preliminary onStart() setup - engineView.asView().requestFocus() } else { // Pause all the videos when transitioning out of [WebRenderFragment] to mitigate possible // memory leak while clearing data. See [WebViewCache.clear] as well as #1720 @@ -182,6 +186,9 @@ class WebRenderFragment : EngineViewLifecycleFragment(), Session.Observer { override fun onStart() { super.onStart() + observeRequestFocus() + .addTo(startStopCompositeDisposable) + /** * When calling getOrCreateEngineSession(), [SessionManager] lazily creates an [EngineSession] * instance and links it with its respective [Session]. During the linking, [SessionManager] @@ -237,9 +244,15 @@ class WebRenderFragment : EngineViewLifecycleFragment(), Session.Observer { cursor = null } - override fun onHiddenChanged(hidden: Boolean) { - FocusOnShowDelegate().onHiddenChanged(this, hidden) - super.onHiddenChanged(hidden) + private fun observeRequestFocus(): Disposable { + // EngineView focus may be lost after waking up from sleep & screen saver. + // Forcibly request focus onStart(), after DOMElement cache, IFF webRenderFragment + // is the current ActiveScreen + return webRenderViewModel.focusRequest + .subscribe { viewId -> + val viewToFocus = rootView.findViewById(viewId) + viewToFocus.requestFocus() + } } fun loadUrl(url: String) { diff --git a/app/src/main/java/org/mozilla/tv/firefox/webrender/WebRenderViewModel.kt b/app/src/main/java/org/mozilla/tv/firefox/webrender/WebRenderViewModel.kt new file mode 100644 index 0000000000..0173332663 --- /dev/null +++ b/app/src/main/java/org/mozilla/tv/firefox/webrender/WebRenderViewModel.kt @@ -0,0 +1,22 @@ +/* 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.tv.firefox.webrender + +import androidx.lifecycle.ViewModel +import io.reactivex.Observable +import io.reactivex.rxkotlin.withLatestFrom +import org.mozilla.tv.firefox.R +import org.mozilla.tv.firefox.ScreenControllerStateMachine +import org.mozilla.tv.firefox.focus.FocusRepo + +class WebRenderViewModel(focusRepo: FocusRepo, activeScreen: Observable) : ViewModel() { + + val focusRequest: Observable = focusRepo.events.withLatestFrom(activeScreen) + .filter { (_, activeScreen) -> + activeScreen == ScreenControllerStateMachine.ActiveScreen.WEB_RENDER + }.map { + R.id.engineView + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index adb0b335f3..b66cf8a7c9 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -2,35 +2,36 @@ - + + android:orientation="vertical" + android:id="@+id/container_web_render" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + android:orientation="vertical" + android:id="@+id/container_navigation_overlay" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + android:orientation="vertical" + android:id="@+id/container_pocket" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + android:orientation="vertical" + android:id="@+id/container_settings" + android:layout_width="match_parent" + android:layout_height="match_parent"/> - + diff --git a/app/src/main/res/layout/fragment_navigation_overlay_orig.xml b/app/src/main/res/layout/fragment_navigation_overlay_orig.xml index df24610409..4a6e7720b4 100644 --- a/app/src/main/res/layout/fragment_navigation_overlay_orig.xml +++ b/app/src/main/res/layout/fragment_navigation_overlay_orig.xml @@ -78,7 +78,9 @@ android:textColorHint="@color/photonGrey10_a80p" android:textIsSelectable="false" android:fontFamily="@string/font_ember_regular" - tools:ignore="UnusedAttribute" /> + tools:ignore="UnusedAttribute" > + + + diff --git a/app/src/system/java/org/mozilla/tv/firefox/ext/EngineSession.kt b/app/src/system/java/org/mozilla/tv/firefox/ext/EngineSession.kt index 512b659867..d69c601f89 100644 --- a/app/src/system/java/org/mozilla/tv/firefox/ext/EngineSession.kt +++ b/app/src/system/java/org/mozilla/tv/firefox/ext/EngineSession.kt @@ -8,6 +8,7 @@ import android.content.Context import mozilla.components.browser.engine.system.NestedWebView import mozilla.components.browser.engine.system.SystemEngineSession import mozilla.components.concept.engine.EngineSession +import org.mozilla.tv.firefox.R /** * [AmazonWebView] requires ActivityContext in order to show 4K resolution rendering option (#277) @@ -16,5 +17,7 @@ import mozilla.components.concept.engine.EngineSession * override the webView instance */ fun EngineSession.resetView(context: Context) { - (this as SystemEngineSession).webView = NestedWebView(context) + (this as SystemEngineSession).webView = NestedWebView(context).apply { + this.id = R.id.nested_web_view + } } diff --git a/app/src/test/java/org/mozilla/tv/firefox/RxTest.kt b/app/src/test/java/org/mozilla/tv/firefox/RxTest.kt index 15ce5191e7..f40bd801c1 100644 --- a/app/src/test/java/org/mozilla/tv/firefox/RxTest.kt +++ b/app/src/test/java/org/mozilla/tv/firefox/RxTest.kt @@ -4,7 +4,9 @@ package org.mozilla.tv.firefox +import io.reactivex.Observable import io.reactivex.subjects.PublishSubject +import org.junit.Assert.assertEquals import org.junit.Test /** @@ -53,4 +55,21 @@ class RxTest { observable3.assertValues(3) } + + @Test + fun `display buffer behavior`() { + val numbers = Observable.just(1, 2, 3, 4, 5) + + val basicBuffer = numbers.buffer(2) + assertEquals( + listOf(listOf(1, 2), listOf(3, 4), listOf(5)), + basicBuffer.test().values() + ) + + val slidingBuffer = numbers.buffer(2, 1) + assertEquals( + listOf(listOf(1, 2), listOf(2, 3), listOf(3, 4), listOf(4, 5), listOf(5)), + slidingBuffer.test().values() + ) + } }