Unverified Commit 1d28f637 authored by Tiger Oakes's avatar Tiger Oakes Committed by GitHub
Browse files

Closes #12522: Reuse exceptions code (#13047)

parent 8b923fc7
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -67,6 +67,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
@@ -98,7 +99,6 @@ import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.trackingprotectionexceptions.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.utils.BrowsersCache

/**
+100 −0
Original line number Diff line number Diff line
@@ -2,82 +2,99 @@
 * 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.loginexceptions
package org.mozilla.fenix.exceptions

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.feature.logins.exceptions.LoginException
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsHeaderViewHolder
import org.mozilla.fenix.loginexceptions.viewholders.LoginExceptionsListItemViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsDeleteButtonViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsHeaderViewHolder
import org.mozilla.fenix.exceptions.viewholders.ExceptionsListItemViewHolder

/**
 * Adapter for a list of sites that are exempted from saving logins,
 * Adapter for a list of sites that are exempted from saving logins or tracking protection,
 * along with controls to remove the exception.
 */
class LoginExceptionsAdapter(
    private val interactor: LoginExceptionsInteractor
) : ListAdapter<LoginExceptionsAdapter.AdapterItem, RecyclerView.ViewHolder>(DiffCallback) {
abstract class ExceptionsAdapter<T : Any>(
    private val interactor: ExceptionsInteractor<T>,
    diffCallback: DiffUtil.ItemCallback<AdapterItem>
) : ListAdapter<ExceptionsAdapter.AdapterItem, RecyclerView.ViewHolder>(diffCallback) {

    /**
     * Change the list of items that are displayed.
     * Header and footer items are added to the list as well.
     */
    fun updateData(exceptions: List<LoginException>) {
    fun updateData(exceptions: List<T>) {
        val adapterItems: List<AdapterItem> = listOf(AdapterItem.Header) +
            exceptions.map { AdapterItem.Item(it) } +
            exceptions.map { wrapAdapterItem(it) } +
            listOf(AdapterItem.DeleteButton)
        submitList(adapterItems)
    }

    override fun getItemViewType(position: Int) = when (getItem(position)) {
        AdapterItem.DeleteButton -> LoginExceptionsDeleteButtonViewHolder.LAYOUT_ID
        AdapterItem.Header -> LoginExceptionsHeaderViewHolder.LAYOUT_ID
        is AdapterItem.Item -> LoginExceptionsListItemViewHolder.LAYOUT_ID
    /**
     * Layout to use for the delete button.
     */
    @get:LayoutRes
    abstract val deleteButtonLayoutId: Int

    /**
     * String to use for the exceptions list header.
     */
    @get:StringRes
    abstract val headerDescriptionResource: Int

    /**
     * Converts an item from [updateData] into an adapter item.
     */
    abstract fun wrapAdapterItem(item: T): AdapterItem.Item<T>

    final override fun getItemViewType(position: Int) = when (getItem(position)) {
        AdapterItem.DeleteButton -> deleteButtonLayoutId
        AdapterItem.Header -> ExceptionsHeaderViewHolder.LAYOUT_ID
        is AdapterItem.Item<*> -> ExceptionsListItemViewHolder.LAYOUT_ID
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)

        return when (viewType) {
            LoginExceptionsDeleteButtonViewHolder.LAYOUT_ID -> LoginExceptionsDeleteButtonViewHolder(
                view,
                interactor
            )
            LoginExceptionsHeaderViewHolder.LAYOUT_ID -> LoginExceptionsHeaderViewHolder(view)
            LoginExceptionsListItemViewHolder.LAYOUT_ID -> LoginExceptionsListItemViewHolder(
                view,
                interactor
            )
            deleteButtonLayoutId ->
                ExceptionsDeleteButtonViewHolder(view, interactor)
            ExceptionsHeaderViewHolder.LAYOUT_ID ->
                ExceptionsHeaderViewHolder(view, headerDescriptionResource)
            ExceptionsListItemViewHolder.LAYOUT_ID ->
                ExceptionsListItemViewHolder(view, interactor)
            else -> throw IllegalStateException()
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is LoginExceptionsListItemViewHolder) {
            val adapterItem = getItem(position) as AdapterItem.Item
            holder.bind(adapterItem.item)
    @Suppress("Unchecked_Cast")
    final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is ExceptionsListItemViewHolder<*>) {
            holder as ExceptionsListItemViewHolder<T>
            val adapterItem = getItem(position) as AdapterItem.Item<T>
            holder.bind(adapterItem.item, adapterItem.url)
        }
    }

    /**
     * Internal items for [ExceptionsAdapter]
     */
    sealed class AdapterItem {
        object DeleteButton : AdapterItem()
        object Header : AdapterItem()
        data class Item(val item: LoginException) : AdapterItem()
    }

    internal object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
        override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
            when (oldItem) {
                AdapterItem.DeleteButton, AdapterItem.Header -> oldItem === newItem
                is AdapterItem.Item -> newItem is AdapterItem.Item && oldItem.item.id == newItem.item.id
        /**
         * Represents an item to display in [ExceptionsAdapter].
         * [T] should refer to the same value as in the [ExceptionsAdapter] and [ExceptionsInteractor].
         */
        abstract class Item<T> : AdapterItem() {
            abstract val item: T
            abstract val url: String
        }

        @Suppress("DiffUtilEquals")
        override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
            oldItem == newItem
    }
}
+21 −0
Original line number Diff line number Diff line
@@ -2,16 +2,20 @@
 * 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.trackingprotectionexceptions.viewholders
package org.mozilla.fenix.exceptions

import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.R
/**
 * Interface for exceptions view interactors. This interface is implemented by objects that want
 * to respond to user interaction on the [ExceptionsView].
 */
interface ExceptionsInteractor<T> {
    /**
     * Called whenever all exception items are deleted
     */
    fun onDeleteAll()

class ExceptionsHeaderViewHolder(
    view: View
) : RecyclerView.ViewHolder(view) {
    companion object {
        const val LAYOUT_ID = R.layout.exceptions_description
    }
    /**
     * Called whenever one exception item is deleted
     */
    fun onDeleteOne(item: T)
}
+41 −0
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@
 * 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.loginexceptions
package org.mozilla.fenix.exceptions

import android.view.LayoutInflater
import android.view.ViewGroup
@@ -11,52 +11,31 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_exceptions.*
import mozilla.components.feature.logins.exceptions.LoginException
import org.mozilla.fenix.R

/**
 * Interface for the ExceptionsViewInteractor. This interface is implemented by objects that want
 * to respond to user interaction on the ExceptionsView
 */
interface ExceptionsViewInteractor {
    /**
     * Called whenever all exception items are deleted
     */
    fun onDeleteAll()

    /**
     * Called whenever one exception item is deleted
     */
    fun onDeleteOne(item: LoginException)
}

/**
 * View that contains and configures the Exceptions List
 */
class LoginExceptionsView(
abstract class ExceptionsView<T : Any>(
    container: ViewGroup,
    val interactor: LoginExceptionsInteractor
    protected val interactor: ExceptionsInteractor<T>
) : LayoutContainer {

    override val containerView: FrameLayout = LayoutInflater.from(container.context)
        .inflate(R.layout.component_exceptions, container, true)
        .findViewById(R.id.exceptions_wrapper)

    private val exceptionsAdapter = LoginExceptionsAdapter(interactor)
    protected abstract val exceptionsAdapter: ExceptionsAdapter<T>

    init {
        exceptions_learn_more.isVisible = false
        exceptions_empty_message.text =
            containerView.context.getString(R.string.preferences_passwords_exceptions_description_empty)
        exceptions_list.apply {
            adapter = exceptionsAdapter
            layoutManager = LinearLayoutManager(containerView.context)
        }
    }

    fun update(state: ExceptionsFragmentState) {
        exceptions_empty_view.isVisible = state.items.isEmpty()
        exceptions_list.isVisible = state.items.isNotEmpty()
        exceptionsAdapter.updateData(state.items)
    fun update(items: List<T>) {
        exceptions_empty_view.isVisible = items.isEmpty()
        exceptions_list.isVisible = items.isNotEmpty()
        exceptionsAdapter.updateData(items)
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@
 * 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.loginexceptions
package org.mozilla.fenix.exceptions.login

import mozilla.components.feature.logins.exceptions.LoginException
import mozilla.components.lib.state.Action
@@ -26,7 +26,7 @@ sealed class ExceptionsFragmentAction : Action {
 * The state for the Exceptions Screen
 * @property items List of exceptions to display
 */
data class ExceptionsFragmentState(val items: List<LoginException>) : State
data class ExceptionsFragmentState(val items: List<LoginException> = emptyList()) : State

/**
 * The ExceptionsState Reducer.
Loading