[fenix] For https://github.com/mozilla-mobile/fenix/issues/10834 - Adding Sync Tabs Feature in Fenix
parent
ed7a7b66fc
commit
d7faa8c2c5
@ -0,0 +1,60 @@
|
||||
/* 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.sync
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import mozilla.components.concept.sync.Device as SyncDevice
|
||||
import mozilla.components.browser.storage.sync.Tab as SyncTab
|
||||
import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder
|
||||
import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder
|
||||
|
||||
class SyncedTabsAdapter(
|
||||
private val listener: (SyncTab) -> Unit
|
||||
) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(
|
||||
DiffCallback
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder {
|
||||
val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||
|
||||
return when (viewType) {
|
||||
DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView)
|
||||
TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SyncedTabsViewHolder, position: Int) {
|
||||
val item = when (holder) {
|
||||
is DeviceViewHolder -> getItem(position) as AdapterItem.Device
|
||||
is TabViewHolder -> getItem(position) as AdapterItem.Tab
|
||||
}
|
||||
holder.bind(item, listener)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (getItem(position)) {
|
||||
is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID
|
||||
is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID
|
||||
}
|
||||
}
|
||||
|
||||
private object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
|
||||
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
|
||||
areContentsTheSame(oldItem, newItem)
|
||||
|
||||
@Suppress("DiffUtilEquals")
|
||||
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
|
||||
oldItem == newItem
|
||||
}
|
||||
|
||||
sealed class AdapterItem {
|
||||
data class Device(val device: SyncDevice) : AdapterItem()
|
||||
data class Tab(val tab: SyncTab) : AdapterItem()
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/* 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.sync
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.fragment_synced_tabs.*
|
||||
import mozilla.components.browser.storage.sync.Tab
|
||||
import mozilla.components.feature.syncedtabs.SyncedTabsFeature
|
||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.library.LibraryPageFragment
|
||||
|
||||
class SyncedTabsFragment : LibraryPageFragment<Tab>() {
|
||||
private val syncedTabsFeature = ViewBoundFeatureWrapper<SyncedTabsFeature>()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_synced_tabs, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val backgroundServices = requireContext().components.backgroundServices
|
||||
|
||||
syncedTabsFeature.set(
|
||||
feature = SyncedTabsFeature(
|
||||
storage = backgroundServices.syncedTabsStorage,
|
||||
accountManager = backgroundServices.accountManager,
|
||||
view = synced_tabs_layout,
|
||||
lifecycleOwner = this.viewLifecycleOwner,
|
||||
onTabClicked = ::handleTabClicked
|
||||
),
|
||||
owner = this,
|
||||
view = view
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
showToolbar(getString(R.string.library_synced_tabs))
|
||||
}
|
||||
|
||||
private fun handleTabClicked(tab: Tab) {
|
||||
|
||||
(activity as HomeActivity).openToBrowserAndLoad(
|
||||
searchTermOrURL = tab.active().url,
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromSyncedTabs
|
||||
)
|
||||
}
|
||||
|
||||
override val selectedItems: Set<Tab>
|
||||
get() = emptySet()
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/* 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.sync
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import mozilla.components.concept.sync.AccountObserver
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
||||
class SyncedTabsIntegration(
|
||||
private val context: Context,
|
||||
private val accountManager: FxaAccountManager
|
||||
) {
|
||||
fun launch() {
|
||||
val accountObserver = SyncedTabsAccountObserver(context)
|
||||
|
||||
accountManager.register(
|
||||
accountObserver,
|
||||
owner = ProcessLifecycleOwner.get(),
|
||||
autoPause = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal class SyncedTabsAccountObserver(private val context: Context) : AccountObserver {
|
||||
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
|
||||
context.components.backgroundServices.syncedTabsStorage.start()
|
||||
}
|
||||
|
||||
override fun onLoggedOut() {
|
||||
context.components.backgroundServices.syncedTabsStorage.stop()
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/* 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.sync
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.android.synthetic.main.component_sync_tabs.view.*
|
||||
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
|
||||
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
class SyncedTabsLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr), SyncedTabsView {
|
||||
|
||||
override var listener: SyncedTabsView.Listener? = null
|
||||
|
||||
private val adapter = SyncedTabsAdapter { listener?.onTabClicked(it) }
|
||||
|
||||
init {
|
||||
inflate(getContext(), R.layout.component_sync_tabs, this)
|
||||
|
||||
synced_tabs_list.layoutManager = LinearLayoutManager(context)
|
||||
synced_tabs_list.adapter = adapter
|
||||
|
||||
synced_tabs_pull_to_refresh.setOnRefreshListener { listener?.onRefresh() }
|
||||
}
|
||||
|
||||
override fun onError(error: SyncedTabsView.ErrorType) {
|
||||
val stringResId = when (error) {
|
||||
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device
|
||||
SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing
|
||||
SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_connect_to_sync_account
|
||||
SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth
|
||||
}
|
||||
|
||||
sync_tabs_status.text = context.getText(stringResId)
|
||||
|
||||
synced_tabs_list.visibility = View.GONE
|
||||
sync_tabs_status.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) {
|
||||
synced_tabs_list.visibility = View.VISIBLE
|
||||
sync_tabs_status.visibility = View.GONE
|
||||
|
||||
val allDeviceTabs = emptyList<SyncedTabsAdapter.AdapterItem>().toMutableList()
|
||||
|
||||
syncedTabs.forEach { (device, tabs) ->
|
||||
if (tabs.isEmpty()) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val deviceTabs = tabs.map { SyncedTabsAdapter.AdapterItem.Tab(it) }
|
||||
|
||||
allDeviceTabs += listOf(SyncedTabsAdapter.AdapterItem.Device(device)) + deviceTabs
|
||||
}
|
||||
|
||||
adapter.submitList(allDeviceTabs)
|
||||
}
|
||||
|
||||
override fun startLoading() {
|
||||
synced_tabs_list.visibility = View.VISIBLE
|
||||
sync_tabs_status.visibility = View.GONE
|
||||
|
||||
synced_tabs_pull_to_refresh.isRefreshing = true
|
||||
}
|
||||
|
||||
override fun stopLoading() {
|
||||
synced_tabs_pull_to_refresh.isRefreshing = false
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/* 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.sync
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.sync_tabs_list_item.view.*
|
||||
import kotlinx.android.synthetic.main.view_synced_tabs_group.view.*
|
||||
import mozilla.components.browser.storage.sync.Tab
|
||||
import mozilla.components.concept.sync.DeviceType
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem
|
||||
|
||||
sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
abstract fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit)
|
||||
|
||||
class TabViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
|
||||
|
||||
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) {
|
||||
bindTab(item as AdapterItem.Tab)
|
||||
|
||||
itemView.setOnClickListener {
|
||||
interactor(item.tab)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindTab(tab: AdapterItem.Tab) {
|
||||
val active = tab.tab.active()
|
||||
itemView.synced_tab_item_title.text = active.title
|
||||
itemView.synced_tab_item_url.text = active.url
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LAYOUT_ID = R.layout.sync_tabs_list_item
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) {
|
||||
|
||||
override fun <T : AdapterItem> bind(item: T, interactor: (Tab) -> Unit) {
|
||||
bindHeader(item as AdapterItem.Device)
|
||||
}
|
||||
|
||||
private fun bindHeader(device: AdapterItem.Device) {
|
||||
|
||||
val deviceLogoDrawable = when (device.device.deviceType) {
|
||||
DeviceType.DESKTOP -> { R.drawable.mozac_ic_device_desktop }
|
||||
else -> { R.drawable.mozac_ic_device_mobile }
|
||||
}
|
||||
|
||||
itemView.synced_tabs_group_name.text = device.device.displayName
|
||||
itemView.synced_tabs_group_name.setCompoundDrawablesWithIntrinsicBounds(deviceLogoDrawable, 0, 0, 0)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LAYOUT_ID = R.layout.view_synced_tabs_group
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/history_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<ProgressBar
|
||||
android:id="@+id/sync_tabs_progress_bar"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:indeterminate="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="8dp"
|
||||
android:translationY="-3dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sync_tabs_status"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/sync_connect_device"
|
||||
android:textColor="?secondaryText"
|
||||
android:textSize="16sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/synced_tabs_pull_to_refresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/synced_tabs_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/history_list_item"/>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,9 @@
|
||||
<?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/. -->
|
||||
<org.mozilla.fenix.sync.SyncedTabsLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/synced_tabs_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="60dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:minHeight="@dimen/library_item_height"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/synced_tab_item_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?primaryText"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/synced_tab_item_url"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Tab Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/synced_tab_item_url"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?secondaryText"
|
||||
android:textSize="12sp"
|
||||
tools:text="https://example.com/"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/synced_tab_item_title"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/synced_tab_item_separator"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="2dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:background="?neutralFaded"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,31 @@
|
||||
<?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/. -->
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/synced_tabs_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/synced_tabs_group_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="15dp"
|
||||
app:drawableStartCompat="@drawable/mozac_ic_device_desktop"
|
||||
app:drawableTint="?primaryText"
|
||||
android:textSize="17sp"
|
||||
android:textAppearance="@style/Header14TextStyle"
|
||||
android:textColor="?primaryText"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
tools:text="Header" />
|
||||
|
||||
</RelativeLayout>
|
Loading…
Reference in New Issue