1
0
Fork 0

Fixes #351 Create home screen component for multitasking

master
Colin Lee 2019-02-08 12:35:48 -06:00
parent 77883c0f30
commit f09dc2453f
28 changed files with 527 additions and 45 deletions

View File

@ -6,6 +6,7 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply from: "$project.rootDir/automation/gradle/versionCode.gradle" apply from: "$project.rootDir/automation/gradle/versionCode.gradle"
apply plugin: 'androidx.navigation.safeargs'
android { android {
compileSdkVersion 28 compileSdkVersion 28

View File

@ -17,7 +17,6 @@ import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.intent.IntentProcessor import mozilla.components.feature.intent.IntentProcessor
import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.BackHandler
import mozilla.components.support.utils.SafeIntent import mozilla.components.support.utils.SafeIntent
import org.mozilla.fenix.browser.BrowserFragment
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
open class HomeActivity : AppCompatActivity() { open class HomeActivity : AppCompatActivity() {
@ -71,10 +70,8 @@ open class HomeActivity : AppCompatActivity() {
private fun openToBrowser() { private fun openToBrowser() {
val sessionId = SafeIntent(intent).getStringExtra(IntentProcessor.ACTIVE_SESSION_ID) val sessionId = SafeIntent(intent).getStringExtra(IntentProcessor.ACTIVE_SESSION_ID)
val host = supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment val host = supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment
val directions = NavGraphDirections.actionGlobalBrowser(sessionId)
host.navController.navigate(R.id.action_global_browser, Bundle().apply { host.navController.navigate(directions)
putString(BrowserFragment.SESSION_ID, sessionId)
})
} }
companion object { companion object {

View File

@ -55,17 +55,19 @@ class BrowserFragment : Fragment(), BackHandler {
private val findInPageIntegration = ViewBoundFeatureWrapper<FindInPageIntegration>() private val findInPageIntegration = ViewBoundFeatureWrapper<FindInPageIntegration>()
private val customTabsToolbarFeature = ViewBoundFeatureWrapper<CustomTabsToolbarFeature>() private val customTabsToolbarFeature = ViewBoundFeatureWrapper<CustomTabsToolbarFeature>()
private val toolbarIntegration = ViewBoundFeatureWrapper<ToolbarIntegration>() private val toolbarIntegration = ViewBoundFeatureWrapper<ToolbarIntegration>()
var sessionId: String? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
sessionId = BrowserFragmentArgs.fromBundle(arguments!!).sessionId
val view = inflater.inflate(R.layout.fragment_browser, container, false) val view = inflater.inflate(R.layout.fragment_browser, container, false)
toolbarComponent = ToolbarComponent( toolbarComponent = ToolbarComponent(
view.browserLayout, view.browserLayout,
ActionBusFactory.get(this), ActionBusFactory.get(this), sessionId,
SearchState("", isEditing = false) SearchState("", isEditing = false)
) )
@ -98,7 +100,7 @@ class BrowserFragment : Fragment(), BackHandler {
.subscribe { .subscribe {
when (it) { when (it) {
is SearchAction.ToolbarTapped -> Navigation.findNavController(toolbar) is SearchAction.ToolbarTapped -> Navigation.findNavController(toolbar)
.navigate(R.id.action_browserFragment_to_searchFragment, null, null) .navigate(BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(null))
is SearchAction.ToolbarMenuItemTapped -> handleToolbarItemInteraction(it) is SearchAction.ToolbarMenuItemTapped -> handleToolbarItemInteraction(it)
} }
} }
@ -109,8 +111,6 @@ class BrowserFragment : Fragment(), BackHandler {
(activity as AppCompatActivity).supportActionBar?.hide() (activity as AppCompatActivity).supportActionBar?.hide()
val sessionId = arguments?.getString(SESSION_ID)
val sessionManager = requireComponents.core.sessionManager val sessionManager = requireComponents.core.sessionManager
contextMenuFeature.set( contextMenuFeature.set(
@ -182,8 +182,6 @@ class BrowserFragment : Fragment(), BackHandler {
if (sessionFeature.onBackPressed()) return true if (sessionFeature.onBackPressed()) return true
if (customTabsToolbarFeature.onBackPressed()) return true if (customTabsToolbarFeature.onBackPressed()) return true
// We'll want to improve this when we add multitasking
requireComponents.core.sessionManager.remove()
return false return false
} }
@ -211,9 +209,9 @@ class BrowserFragment : Fragment(), BackHandler {
is ToolbarMenu.Item.Forward -> sessionUseCases.goForward.invoke() is ToolbarMenu.Item.Forward -> sessionUseCases.goForward.invoke()
is ToolbarMenu.Item.Reload -> sessionUseCases.reload.invoke() is ToolbarMenu.Item.Reload -> sessionUseCases.reload.invoke()
is ToolbarMenu.Item.Settings -> Navigation.findNavController(toolbar) is ToolbarMenu.Item.Settings -> Navigation.findNavController(toolbar)
.navigate(R.id.action_browserFragment_to_settingsFragment, null, null) .navigate(BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment())
is ToolbarMenu.Item.Library -> Navigation.findNavController(toolbar) is ToolbarMenu.Item.Library -> Navigation.findNavController(toolbar)
.navigate(R.id.action_browserFragment_to_libraryFragment, null, null) .navigate(BrowserFragmentDirections.actionBrowserFragmentToLibraryFragment())
is ToolbarMenu.Item.RequestDesktop -> sessionUseCases.requestDesktopSite.invoke(action.item.isChecked) is ToolbarMenu.Item.RequestDesktop -> sessionUseCases.requestDesktopSite.invoke(action.item.isChecked)
is ToolbarMenu.Item.Share -> requireComponents.core.sessionManager is ToolbarMenu.Item.Share -> requireComponents.core.sessionManager
.selectedSession?.url?.apply { requireContext().share(this) } .selectedSession?.url?.apply { requireContext().share(this) }
@ -231,7 +229,6 @@ class BrowserFragment : Fragment(), BackHandler {
} }
companion object { companion object {
const val SESSION_ID = "session_id"
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1 private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1
private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2 private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2
private const val TOOLBAR_HEIGHT = 56f private const val TOOLBAR_HEIGHT = 56f

View File

@ -0,0 +1,21 @@
/* 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.ext
import android.graphics.Rect
import android.view.TouchDelegate
import android.view.View
fun View?.increaseTapArea(extraDps: Int) {
this!!.post {
val touchRect = Rect()
getHitRect(touchRect)
touchRect.top -= extraDps
touchRect.left -= extraDps
touchRect.right += extraDps
touchRect.bottom += extraDps
(parent as View).touchDelegate = TouchDelegate(touchRect, this)
}
}

View File

@ -4,6 +4,7 @@
package org.mozilla.fenix.home package org.mozilla.fenix.home
import android.annotation.SuppressLint
import android.content.res.Resources import android.content.res.Resources
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.os.Bundle import android.os.Bundle
@ -16,28 +17,42 @@ import androidx.fragment.app.Fragment
import androidx.navigation.Navigation import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.fragment_home.view.* import kotlinx.android.synthetic.main.fragment_home.view.*
import kotlinx.android.synthetic.main.tab_list_header.view.*
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ThemeManager import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.home.sessions.SessionsComponent import org.mozilla.fenix.home.sessions.SessionsComponent
import org.mozilla.fenix.home.sessions.layoutComponents import org.mozilla.fenix.home.tabs.TabsAction
import org.mozilla.fenix.home.tabs.TabsChange
import org.mozilla.fenix.home.tabs.TabsComponent
import org.mozilla.fenix.home.tabs.TabsState
import org.mozilla.fenix.isPrivate import org.mozilla.fenix.isPrivate
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getManagedEmitter
import org.mozilla.fenix.mvi.getSafeManagedObservable
import kotlin.math.roundToInt import kotlin.math.roundToInt
class HomeFragment : Fragment() { class HomeFragment : Fragment() {
private val bus = ActionBusFactory.get(this)
private var sessionObserver: SessionManager.Observer? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false) val view = inflater.inflate(R.layout.fragment_home, container, false)
SessionsComponent(view.homeLayout, ActionBusFactory.get(this)) TabsComponent(view.homeLayout, bus, TabsState(requireComponents.core.sessionManager.sessions))
SessionsComponent(view.homeLayout, bus)
ActionBusFactory.get(this).logMergedObservables() ActionBusFactory.get(this).logMergedObservables()
return view return view
} }
@SuppressLint("CheckResult")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -45,20 +60,41 @@ class HomeFragment : Fragment() {
layoutComponents(view.homeLayout) layoutComponents(view.homeLayout)
getSafeManagedObservable<TabsAction>()
.subscribe {
when (it) {
is TabsAction.Select -> {
requireComponents.core.sessionManager.select(it.session)
val directions = HomeFragmentDirections.actionHomeFragmentToBrowserFragment(it.session.id)
Navigation.findNavController(view).navigate(directions)
}
is TabsAction.Close -> {
requireComponents.core.sessionManager.remove(it.session)
}
}
}
val searchIcon = requireComponents.search.searchEngineManager.getDefaultSearchEngine(requireContext()).let { val searchIcon = requireComponents.search.searchEngineManager.getDefaultSearchEngine(requireContext()).let {
BitmapDrawable(resources, it.icon) BitmapDrawable(resources, it.icon)
} }
// Temporary so we can easily test settings // Temporary so we can easily test settings
view.menuButton.setOnClickListener { view.menuButton.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_homeFragment_to_settingsFragment, null, null) val directions = HomeFragmentDirections.actionHomeFragmentToSettingsFragment()
Navigation.findNavController(it).navigate(directions)
} }
view.toolbar.setCompoundDrawablesWithIntrinsicBounds(searchIcon, null, null, null) view.toolbar.setCompoundDrawablesWithIntrinsicBounds(searchIcon, null, null, null)
val roundToInt = (toolbarPaddingDp * Resources.getSystem().displayMetrics.density).roundToInt() val roundToInt = (toolbarPaddingDp * Resources.getSystem().displayMetrics.density).roundToInt()
view.toolbar.compoundDrawablePadding = roundToInt view.toolbar.compoundDrawablePadding = roundToInt
view.toolbar.setOnClickListener { it -> view.toolbar.setOnClickListener { it ->
Navigation.findNavController(it).navigate(R.id.action_homeFragment_to_searchFragment, null, null) val directions = HomeFragmentDirections.actionHomeFragmentToSearchFragment(null)
Navigation.findNavController(it).navigate(directions)
}
view.add_tab_button.increaseTapArea(addTabButtonIncreaseDps)
view.add_tab_button.setOnClickListener {
val directions = HomeFragmentDirections.actionHomeFragmentToSearchFragment(null)
Navigation.findNavController(it).navigate(directions)
} }
// There is currently an issue with visibility changes in ConstraintLayout 2.0.0-alpha3 // There is currently an issue with visibility changes in ConstraintLayout 2.0.0-alpha3
@ -85,6 +121,7 @@ class HomeFragment : Fragment() {
firstKeyTrigger.conditionallyFire(progress) firstKeyTrigger.conditionallyFire(progress)
secondKeyTrigger.conditionallyFire(progress) secondKeyTrigger.conditionallyFire(progress)
} }
override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) { } override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) { }
}) })
@ -148,7 +185,56 @@ class HomeFragment : Fragment() {
} }
} }
override fun onResume() {
super.onResume()
sessionObserver = subscribeToSessions()
}
override fun onPause() {
super.onPause()
sessionObserver?.let {
requireComponents.core.sessionManager.unregister(it)
}
}
private fun subscribeToSessions(): SessionManager.Observer {
val observer = object : SessionManager.Observer {
override fun onSessionAdded(session: Session) {
super.onSessionAdded(session)
getManagedEmitter<TabsChange>().onNext(
TabsChange.Changed(requireComponents.core.sessionManager.sessions))
}
override fun onSessionRemoved(session: Session) {
super.onSessionRemoved(session)
getManagedEmitter<TabsChange>().onNext(
TabsChange.Changed(requireComponents.core.sessionManager.sessions))
}
override fun onSessionSelected(session: Session) {
super.onSessionSelected(session)
getManagedEmitter<TabsChange>().onNext(
TabsChange.Changed(requireComponents.core.sessionManager.sessions))
}
override fun onSessionsRestored() {
super.onSessionsRestored()
getManagedEmitter<TabsChange>().onNext(
TabsChange.Changed(requireComponents.core.sessionManager.sessions))
}
override fun onAllSessionsRemoved() {
super.onAllSessionsRemoved()
getManagedEmitter<TabsChange>().onNext(
TabsChange.Changed(requireComponents.core.sessionManager.sessions))
}
}
requireComponents.core.sessionManager.register(observer)
return observer
}
companion object { companion object {
const val addTabButtonIncreaseDps = 8
const val toolbarPaddingDp = 12f const val toolbarPaddingDp = 12f
const val firstKeyTriggerFrame = 55 const val firstKeyTriggerFrame = 55
const val secondKeyTriggerFrame = 90 const val secondKeyTriggerFrame = 90

View File

@ -2,27 +2,41 @@
License, v. 2.0. If a copy of the MPL was not distributed with this 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/. */ file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.sessions package org.mozilla.fenix.home
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
import kotlinx.android.synthetic.main.component_sessions.* import kotlinx.android.synthetic.main.component_sessions.*
import kotlinx.android.synthetic.main.component_tabs.*
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.tab_list_header.*
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
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.START import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.START
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.TOP import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.TOP
import org.jetbrains.anko.constraint.layout.applyConstraintSet import org.jetbrains.anko.constraint.layout.applyConstraintSet
import org.mozilla.fenix.home.HomeFragment
fun HomeFragment.layoutComponents(layout: ConstraintLayout) { fun HomeFragment.layoutComponents(layout: ConstraintLayout) {
layout.applyConstraintSet { layout.applyConstraintSet {
tabs_header {
connect(
TOP to BOTTOM of homeDivider,
START to START of tabs_list,
END to END of PARENT_ID
)
}
tabs_list {
connect(
TOP to BOTTOM of tabs_header,
START to START of PARENT_ID,
END to END of PARENT_ID
)
}
session_list { session_list {
connect( connect(
BOTTOM to BOTTOM of PARENT_ID, TOP to BOTTOM of tabs_list,
START to START of PARENT_ID, START to START of PARENT_ID,
END to END of PARENT_ID, END to END of PARENT_ID
TOP to BOTTOM of toolbar_wrapper
) )
} }
} }

View File

@ -24,7 +24,7 @@ class SessionsComponent(
override val reducer: (SessionsState, SessionsChange) -> SessionsState = { state, change -> override val reducer: (SessionsState, SessionsChange) -> SessionsState = { state, change ->
when (change) { when (change) {
is SessionsChange.SessionsChanged -> state // copy state with changes here is SessionsChange.Changed -> state // copy state with changes here
} }
} }
@ -42,5 +42,5 @@ sealed class SessionsAction : Action {
} }
sealed class SessionsChange : Change { sealed class SessionsChange : Change {
object SessionsChanged : SessionsChange() object Changed : SessionsChange()
} }

View File

@ -0,0 +1,106 @@
/* 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.tabs
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.tab_list_row.*
import mozilla.components.browser.session.Session
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.increaseTapArea
class TabsAdapter(private val actionEmitter: Observer<TabsAction>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var sessions = listOf<Session>()
set(value) {
val diffResult = DiffUtil.calculateDiff(TabsDiffCallback(field, value), true)
field = value
diffResult.dispatchUpdatesTo(this@TabsAdapter)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return TabViewHolder(view, actionEmitter)
}
override fun getItemViewType(position: Int) = TabViewHolder.LAYOUT_ID
override fun getItemCount(): Int = sessions.size
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is TabViewHolder -> {
holder.bindSession(sessions[position])
}
}
}
private class TabViewHolder(
view: View,
actionEmitter: Observer<TabsAction>,
override val containerView: View? = view
) :
RecyclerView.ViewHolder(view), LayoutContainer {
var session: Session? = null
init {
item_tab.setOnClickListener {
actionEmitter.onNext(TabsAction.Select(session!!))
}
close_tab_button?.run {
increaseTapArea(closeButtonIncreaseDps)
setOnClickListener {
actionEmitter.onNext(TabsAction.Close(session!!))
}
}
}
fun bindSession(session: Session) {
this.session = session
text_url.text = session.url
}
companion object {
const val closeButtonIncreaseDps = 12
const val LAYOUT_ID = R.layout.tab_list_row
}
}
}
class TabsDiffCallback(
private val oldList: List<Session>,
private val newList: List<Session>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size
override fun getNewListSize(): Int = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].id == newList[newItemPosition].id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldSession = oldList[oldItemPosition]
val newSession = newList[newItemPosition]
val diffBundle = Bundle()
if (oldSession.url != newSession.url) {
diffBundle.putString("url", newSession.url)
}
return if (diffBundle.size() == 0) null else diffBundle
}
}

View File

@ -0,0 +1,47 @@
/* 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.tabs
import android.view.ViewGroup
import mozilla.components.browser.session.Session
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.ViewState
class TabsComponent(
private val container: ViewGroup,
bus: ActionBusFactory,
override var initialState: TabsState = TabsState(listOf())
) :
UIComponent<TabsState, TabsAction, TabsChange>(
bus.getManagedEmitter(TabsAction::class.java),
bus.getSafeManagedObservable(TabsChange::class.java)
) {
override val reducer: (TabsState, TabsChange) -> TabsState = { state, change ->
when (change) {
is TabsChange.Changed -> state.copy(sessions = change.sessions)
}
}
override fun initView() = TabsUIView(container, actionEmitter, changesObservable)
init {
render(reducer)
}
}
data class TabsState(val sessions: List<Session>) : ViewState
sealed class TabsAction : Action {
data class Select(val session: Session) : TabsAction()
data class Close(val session: Session) : TabsAction()
}
sealed class TabsChange : Change {
data class Changed(val sessions: List<Session>) : TabsChange()
}

View File

@ -0,0 +1,49 @@
/* 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.tabs
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer
import org.mozilla.fenix.R
import org.mozilla.fenix.mvi.UIView
class TabsUIView(
container: ViewGroup,
actionEmitter: Observer<TabsAction>,
changesObservable: Observable<TabsChange>
) :
UIView<TabsState, TabsAction, TabsChange>(container, actionEmitter, changesObservable) {
private val header: ConstraintLayout = LayoutInflater.from(container.context)
.inflate(R.layout.tab_list_header, container, true)
.findViewById(R.id.tabs_header)
override val view: RecyclerView = LayoutInflater.from(container.context)
.inflate(R.layout.component_tabs, container, true)
.findViewById(R.id.tabs_list)
private val tabsAdapter = TabsAdapter(actionEmitter)
init {
view.apply {
layoutManager = LinearLayoutManager(container.context)
adapter = tabsAdapter
itemAnimator = DefaultItemAnimator()
}
}
override fun updateView() = Consumer<TabsState> {
tabsAdapter.sessions = it.sessions
header.visibility = if (it.sessions.isEmpty()) View.GONE else View.VISIBLE
}
}

View File

@ -15,10 +15,12 @@ import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_search.view.* import kotlinx.android.synthetic.main.fragment_search.view.*
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getManagedEmitter
import org.mozilla.fenix.mvi.getSafeManagedObservable import org.mozilla.fenix.mvi.getSafeManagedObservable
import org.mozilla.fenix.search.awesomebar.AwesomeBarAction import org.mozilla.fenix.search.awesomebar.AwesomeBarAction
import org.mozilla.fenix.search.awesomebar.AwesomeBarChange import org.mozilla.fenix.search.awesomebar.AwesomeBarChange
import org.mozilla.fenix.search.awesomebar.AwesomeBarComponent import org.mozilla.fenix.search.awesomebar.AwesomeBarComponent
import org.mozilla.fenix.search.awesomebar.AwesomeBarState
import org.mozilla.fenix.search.toolbar.SearchAction import org.mozilla.fenix.search.toolbar.SearchAction
import org.mozilla.fenix.search.toolbar.SearchState import org.mozilla.fenix.search.toolbar.SearchState
import org.mozilla.fenix.search.toolbar.ToolbarComponent import org.mozilla.fenix.search.toolbar.ToolbarComponent
@ -33,13 +35,18 @@ class SearchFragment : Fragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
val sessionId = SearchFragmentArgs.fromBundle(arguments!!).sessionId
val view = inflater.inflate(R.layout.fragment_search, container, false) val view = inflater.inflate(R.layout.fragment_search, container, false)
toolbarComponent = ToolbarComponent( toolbarComponent = ToolbarComponent(
view.toolbar_wrapper, view.toolbar_wrapper,
ActionBusFactory.get(this), ActionBusFactory.get(this),
sessionId,
SearchState("", isEditing = true) SearchState("", isEditing = true)
) )
awesomeBarComponent = AwesomeBarComponent(view.search_layout, ActionBusFactory.get(this)) awesomeBarComponent = AwesomeBarComponent(
view.search_layout, ActionBusFactory.get(this),
AwesomeBarState("", sessionId == null)
)
ActionBusFactory.get(this).logMergedObservables() ActionBusFactory.get(this).logMergedObservables()
return view return view
} }
@ -61,8 +68,7 @@ class SearchFragment : Fragment() {
when (it) { when (it) {
is SearchAction.UrlCommitted -> transitionToBrowser() is SearchAction.UrlCommitted -> transitionToBrowser()
is SearchAction.TextChanged -> { is SearchAction.TextChanged -> {
ActionBusFactory.get(this) getManagedEmitter<AwesomeBarChange>().onNext(AwesomeBarChange.UpdateQuery(it.query))
.emit(AwesomeBarChange::class.java, AwesomeBarChange.UpdateQuery(it.query))
} }
} }
} }
@ -76,7 +82,8 @@ class SearchFragment : Fragment() {
} }
private fun transitionToBrowser() { private fun transitionToBrowser() {
Navigation.findNavController(view!!.search_layout) val sessionId = SearchFragmentArgs.fromBundle(arguments!!).sessionId
.navigate(R.id.action_searchFragment_to_browserFragment, null, null) val directions = SearchFragmentDirections.actionSearchFragmentToBrowserFragment(sessionId)
Navigation.findNavController(view!!.search_layout).navigate(directions)
} }
} }

View File

@ -11,7 +11,7 @@ import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.ViewState import org.mozilla.fenix.mvi.ViewState
data class AwesomeBarState(val query: String) : ViewState data class AwesomeBarState(val query: String, val useNewTab: Boolean = false) : ViewState
sealed class AwesomeBarAction : Action { sealed class AwesomeBarAction : Action {
object ItemSelected : AwesomeBarAction() object ItemSelected : AwesomeBarAction()
@ -35,7 +35,7 @@ class AwesomeBarComponent(
} }
} }
override fun initView() = AwesomeBarUIView(container, actionEmitter, changesObservable) override fun initView() = AwesomeBarUIView(initialState.useNewTab, container, actionEmitter, changesObservable)
init { init {
render(reducer) render(reducer)

View File

@ -19,6 +19,7 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.UIView
class AwesomeBarUIView( class AwesomeBarUIView(
useNewTab: Boolean,
container: ViewGroup, container: ViewGroup,
actionEmitter: Observer<AwesomeBarAction>, actionEmitter: Observer<AwesomeBarAction>,
changesObservable: Observable<AwesomeBarChange> changesObservable: Observable<AwesomeBarChange>
@ -32,7 +33,11 @@ class AwesomeBarUIView(
with(container.context) { with(container.context) {
view.addProviders(ClipboardSuggestionProvider( view.addProviders(ClipboardSuggestionProvider(
this, this,
components.useCases.sessionUseCases.loadUrl, if (useNewTab) {
components.useCases.tabsUseCases.addTab
} else {
components.useCases.sessionUseCases.loadUrl
},
getDrawable(R.drawable.ic_link).toBitmap(), getDrawable(R.drawable.ic_link).toBitmap(),
getString(R.string.awesomebar_clipboard_title) getString(R.string.awesomebar_clipboard_title)
) )

View File

@ -20,6 +20,7 @@ import org.mozilla.fenix.mvi.ViewState
class ToolbarComponent( class ToolbarComponent(
private val container: ViewGroup, private val container: ViewGroup,
bus: ActionBusFactory, bus: ActionBusFactory,
private val sessionId: String?,
override var initialState: SearchState = SearchState("", false) override var initialState: SearchState = SearchState("", false)
) : ) :
UIComponent<SearchState, SearchAction, SearchChange>( UIComponent<SearchState, SearchAction, SearchChange>(
@ -33,7 +34,7 @@ class ToolbarComponent(
} }
} }
override fun initView() = ToolbarUIView(container, actionEmitter, changesObservable) override fun initView() = ToolbarUIView(sessionId, container, actionEmitter, changesObservable)
init { init {
render(reducer) render(reducer)
applyTheme() applyTheme()

View File

@ -16,6 +16,7 @@ import mozilla.components.feature.toolbar.ToolbarFeature
import org.mozilla.fenix.DefaultThemeManager import org.mozilla.fenix.DefaultThemeManager
import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.base.feature.LifecycleAwareFeature
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.ext.application import org.mozilla.fenix.ext.application
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
@ -38,7 +39,8 @@ class ToolbarIntegration(
context.getString(R.string.browser_home_button), context.getString(R.string.browser_home_button),
visible = { sessionManager.runWithSession(sessionId) { it.isCustomTabSession().not() } } visible = { sessionManager.runWithSession(sessionId) { it.isCustomTabSession().not() } }
) { ) {
Navigation.findNavController(toolbar).navigate(R.id.action_browserFragment_to_homeFragment) Navigation.findNavController(toolbar)
.navigate(BrowserFragmentDirections.actionBrowserFragmentToHomeFragment())
} }
toolbar.addBrowserAction(home) toolbar.addBrowserAction(home)

View File

@ -18,6 +18,7 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.mvi.UIView import org.mozilla.fenix.mvi.UIView
class ToolbarUIView( class ToolbarUIView(
sessionId: String?,
container: ViewGroup, container: ViewGroup,
actionEmitter: Observer<SearchAction>, actionEmitter: Observer<SearchAction>,
changesObservable: Observable<SearchChange> changesObservable: Observable<SearchChange>
@ -51,11 +52,12 @@ class ToolbarUIView(
setOnEditListener(object : mozilla.components.concept.toolbar.Toolbar.OnEditListener { setOnEditListener(object : mozilla.components.concept.toolbar.Toolbar.OnEditListener {
override fun onTextChanged(text: String) { override fun onTextChanged(text: String) {
url = text
actionEmitter.onNext(SearchAction.TextChanged(text)) actionEmitter.onNext(SearchAction.TextChanged(text))
} }
override fun onStopEditing() { override fun onStopEditing() {
actionEmitter.onNext(SearchAction.UrlCommitted("foo")) actionEmitter.onNext(SearchAction.UrlCommitted(url))
} }
}) })
} }
@ -71,7 +73,7 @@ class ToolbarUIView(
ShippedDomainsProvider().also { it.initialize(this) }, ShippedDomainsProvider().also { it.initialize(this) },
components.core.historyStorage, components.core.historyStorage,
components.core.sessionManager, components.core.sessionManager,
components.core.sessionManager.selectedSession?.id sessionId ?: components.core.sessionManager.selectedSession?.id
) )
} }
} }

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View File

@ -1,12 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/session_list" android:id="@+id/session_list"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"/>
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar_wrapper"/>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/tabs_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar_wrapper"/>

View File

@ -74,4 +74,16 @@
android:textSize="14sp" /> android:textSize="14sp" />
</org.mozilla.fenix.home.SearchView> </org.mozilla.fenix.home.SearchView>
<View
android:id="@+id/homeDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/photonGrey30"
app:layout_constraintTop_toBottomOf="@id/toolbar_wrapper"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="30dp"
android:layout_marginStart="40dp"
android:layout_marginEnd="40dp"/>
</androidx.constraintlayout.motion.widget.MotionLayout> </androidx.constraintlayout.motion.widget.MotionLayout>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<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/tabs_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent">
<TextView
android:id="@+id/header_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/tabs_header_title"
android:textSize="24sp"
android:textColor="@android:color/black"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<ImageView
android:id="@+id/add_tab_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:src="@drawable/ic_add_black_24dp"
android:baselineAlignBottom="true"
app:layout_constraintBaseline_toBaselineOf="@id/header_text"
app:layout_constraintEnd_toStartOf="@id/tabs_overflow_button"/>
<ImageView
android:id="@+id/tabs_overflow_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:src="@drawable/ic_menu"
android:baselineAlignBottom="true"
app:layout_constraintBaseline_toBaselineOf="@id/header_text"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:padding="10dp"
android:elevation="5dp"
app:cardCornerRadius="10dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/item_tab"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="10dp">
<ImageView android:id="@+id/favicon_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_link"
android:tint="@android:color/black"
android:paddingStart="10dp"
android:paddingEnd="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/favicon_content_description"/>
<TextView android:id="@+id/text_url"
android:textSize="18sp"
android:maxLines="1"
android:ellipsize="end"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/favicon_image"
app:layout_constraintEnd_toStartOf="@id/close_tab_button"/>
<ImageView android:id="@+id/close_tab_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_close_black_24dp"
android:paddingEnd="10dp"
android:contentDescription="@string/close_tab"
app:layout_constraintBottom_toBottomOf="@id/text_url"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView android:layout_width="0dp"
android:layout_height="100dp"
android:src="@color/photonBlue40"
app:layout_constraintTop_toBottomOf="@id/text_url"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:contentDescription="TODO"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -37,6 +37,7 @@
android:id="@+id/action_searchFragment_to_browserFragment" android:id="@+id/action_searchFragment_to_browserFragment"
app:destination="@id/browserFragment" app:destination="@id/browserFragment"
app:popUpTo="@id/homeFragment" /> app:popUpTo="@id/homeFragment" />
<argument android:name="session_id" app:argType="string" app:nullable="true"/>
</fragment> </fragment>
<fragment <fragment

View File

@ -125,4 +125,7 @@
<string name="library_title">Library</string> <string name="library_title">Library</string>
<!-- Settings Page Title --> <!-- Settings Page Title -->
<string name="settings_title">Settings</string> <string name="settings_title">Settings</string>
<string name="tabs_header_title">Current Session</string>
<string name="close_tab">Close tab</string>
<string name="favicon_content_description">Favorite icon</string>
</resources> </resources>

View File

@ -8,6 +8,7 @@ buildscript {
dependencies { dependencies {
classpath Deps.tools_androidgradle classpath Deps.tools_androidgradle
classpath Deps.tools_kotlingradle classpath Deps.tools_kotlingradle
classpath Deps.androidx_safeargs
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

View File

@ -18,6 +18,7 @@ private object Versions {
const val androidx_annotation = "1.0.1" const val androidx_annotation = "1.0.1"
const val androidx_lifecycle = "2.0.0" const val androidx_lifecycle = "2.0.0"
const val androidx_fragment = "1.1.0-alpha04" const val androidx_fragment = "1.1.0-alpha04"
const val androidx_safeargs = "1.0.0-beta01"
const val mozilla_android_components = "0.42.0-SNAPSHOT" const val mozilla_android_components = "0.42.0-SNAPSHOT"
@ -99,6 +100,7 @@ object Deps {
const val androidx_legacy = "androidx.legacy:legacy-support-v4:${Versions.androidx_legacy}" const val androidx_legacy = "androidx.legacy:legacy-support-v4:${Versions.androidx_legacy}"
const val androidx_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime:${Versions.androidx_lifecycle}" const val androidx_lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime:${Versions.androidx_lifecycle}"
const val androidx_preference = "androidx.preference:preference-ktx:${Versions.androidx_preference}" const val androidx_preference = "androidx.preference:preference-ktx:${Versions.androidx_preference}"
const val androidx_safeargs = "android.arch.navigation:navigation-safe-args-gradle-plugin:${Versions.androidx_safeargs}"
const val android_arch_navigation = "android.arch.navigation:navigation-fragment:${Versions.android_arch_navigation}" const val android_arch_navigation = "android.arch.navigation:navigation-fragment:${Versions.android_arch_navigation}"
const val android_arch_navigation_ui = "android.arch.navigation:navigation-ui:${Versions.android_arch_navigation}" const val android_arch_navigation_ui = "android.arch.navigation:navigation-ui:${Versions.android_arch_navigation}"

View File

@ -1,5 +1,6 @@
#Wed Feb 06 16:31:29 CST 2019
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip