Skip to content

Commit

Permalink
For mozilla-mobile#20764 add screen for opting out of experiments
Browse files Browse the repository at this point in the history
  • Loading branch information
Amejia481 authored and Shahen Antonyan committed Aug 12, 2021
1 parent 4d93b8f commit a28d123
Show file tree
Hide file tree
Showing 20 changed files with 956 additions and 10 deletions.
1 change: 1 addition & 0 deletions app/src/main/java/org/mozilla/fenix/BrowserDirection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromAddSearchEngineFragment(R.id.addSearchEngineFragment),
FromEditCustomSearchEngineFragment(R.id.editCustomSearchEngineFragment),
FromAddonDetailsFragment(R.id.addonDetailsFragment),
FromStudiesFragment(R.id.studiesFragment),
FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment),
FromLoginDetailFragment(R.id.loginDetailFragment),
FromTabsTray(R.id.tabsTrayFragment),
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/org/mozilla/fenix/HomeActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ import org.mozilla.fenix.settings.logins.fragment.LoginDetailFragmentDirections
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections
import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections
import org.mozilla.fenix.settings.studies.StudiesFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.tabstray.TabsTrayFragment
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
Expand Down Expand Up @@ -785,6 +786,9 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
TabsTrayFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromRecentlyClosed ->
RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromStudiesFragment -> StudiesFragmentDirections.actionGlobalBrowser(
customTabSessionId
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
package org.mozilla.fenix.settings

import android.os.Bundle
import androidx.navigation.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar

Expand Down Expand Up @@ -40,16 +43,14 @@ class DataChoicesFragment : PreferenceFragmentCompat() {
} else {
context.components.analytics.metrics.stop(MetricServiceType.Marketing)
}
} else if (key == getPreferenceKey(R.string.pref_key_experimentation)) {
val enabled = context.settings().isExperimentationEnabled
context.components.analytics.experiments.globalUserParticipation = enabled
}
}
}

override fun onResume() {
super.onResume()
showToolbar(getString(R.string.preferences_data_collection))
updateStudiesSection()
}

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
Expand All @@ -68,10 +69,22 @@ class DataChoicesFragment : PreferenceFragmentCompat() {
isChecked = context.settings().isMarketingTelemetryEnabled
onPreferenceChangeListener = SharedPreferenceUpdater()
}
}

requirePreference<SwitchPreference>(R.string.pref_key_experimentation).apply {
isChecked = context.settings().isExperimentationEnabled
onPreferenceChangeListener = SharedPreferenceUpdater()
private fun updateStudiesSection() {
val studiesPreference = requirePreference<Preference>(R.string.pref_key_studies_section)
val settings = requireContext().settings()
val stringId = if (settings.isExperimentationEnabled) {
R.string.studies_on
} else {
R.string.studies_off
}
studiesPreference.summary = getString(stringId)

studiesPreference.setOnPreferenceClickListener {
val action = DataChoicesFragmentDirections.actionDataChoicesFragmentToStudiesFragment()
view?.findNavController()?.nav(R.id.dataChoicesFragment, action)
true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ object SupportUtils {
YOUR_RIGHTS("your-rights"),
TRACKING_PROTECTION("tracking-protection-firefox-android"),
WHATS_NEW("whats-new-firefox-preview"),
OPT_OUT_STUDIES("how-opt-out-studies-firefox-android"),
SEND_TABS("send-tab-preview"),
SET_AS_DEFAULT_BROWSER("set-firefox-preview-default"),
SEARCH_SUGGESTION("how-search-firefox-preview"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* 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.settings.studies

import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton

/**
* A base view holder for Studies.
*/
sealed class CustomViewHolder(view: View) : RecyclerView.ViewHolder(view) {
/**
* A view holder for displaying section items.
*/
class SectionViewHolder(
view: View,
val titleView: TextView,
val divider: View
) : CustomViewHolder(view)

/**
* A view holder for displaying study items.
*/
class StudyViewHolder(
view: View,
val titleView: TextView,
val summaryView: TextView,
val deleteButton: MaterialButton,
) : CustomViewHolder(view)
}
164 changes: 164 additions & 0 deletions app/src/main/java/org/mozilla/fenix/settings/studies/StudiesAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/* 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.settings.studies

import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.google.android.material.button.MaterialButton
import org.mozilla.experiments.nimbus.internal.EnrolledExperiment
import org.mozilla.fenix.R
import org.mozilla.fenix.settings.studies.CustomViewHolder.SectionViewHolder
import org.mozilla.fenix.settings.studies.CustomViewHolder.StudyViewHolder

private const val VIEW_HOLDER_TYPE_SECTION = 0
private const val VIEW_HOLDER_TYPE_STUDY = 1

/**
* An adapter for displaying studies items. This will display information related to the state of
* a study such as active. In addition, it will perform actions such as removing a study.
*
* @property studiesDelegate Delegate that will provides method for handling
* the studies actions items.
* @param studies The list of studies.
* * @property studiesDelegate Delegate that will provides method for handling
* the studies actions items.
* @param shouldSubmitOnInit The sole purpose of this property is to prevent the submitList function
* to run on init, it should only be used from tests.
*/
@Suppress("LargeClass")
class StudiesAdapter(
private val studiesDelegate: StudiesAdapterDelegate,
studies: List<EnrolledExperiment>,
@VisibleForTesting
internal val shouldSubmitOnInit: Boolean = true
) : ListAdapter<Any, CustomViewHolder>(DifferCallback) {
/**
* Represents all the studies that will be distributed in multiple headers like
* active, and completed, this helps to have the data source of the items,
* displayed in the UI.
*/
@VisibleForTesting
internal var studiesMap: MutableMap<String, EnrolledExperiment> =
studies.associateBy({ it.slug }, { it }).toMutableMap()

init {
if (shouldSubmitOnInit) {
submitList(createListWithSections(studies))
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
return when (viewType) {
VIEW_HOLDER_TYPE_STUDY -> createStudiesViewHolder(parent)
VIEW_HOLDER_TYPE_SECTION -> createSectionViewHolder(parent)
else -> throw IllegalArgumentException("Unrecognized viewType")
}
}

private fun createSectionViewHolder(parent: ViewGroup): CustomViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.studies_section_item, parent, false)
val titleView = view.findViewById<TextView>(R.id.title)
val divider = view.findViewById<View>(R.id.divider)
return SectionViewHolder(view, titleView, divider)
}

private fun createStudiesViewHolder(parent: ViewGroup): StudyViewHolder {
val context = parent.context
val view = LayoutInflater.from(context).inflate(R.layout.study_item, parent, false)
val titleView = view.findViewById<TextView>(R.id.studyTitle)
val summaryView = view.findViewById<TextView>(R.id.study_description)
val removeButton = view.findViewById<MaterialButton>(R.id.remove_button)
return StudyViewHolder(
view,
titleView,
summaryView,
removeButton
)
}

override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is EnrolledExperiment -> VIEW_HOLDER_TYPE_STUDY
is Section -> VIEW_HOLDER_TYPE_SECTION
else -> throw IllegalArgumentException("items[position] has unrecognized type")
}
}

override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
val item = getItem(position)

when (holder) {
is SectionViewHolder -> bindSection(holder, item as Section)
is StudyViewHolder -> bindStudy(holder, item as EnrolledExperiment)
}
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun bindSection(holder: SectionViewHolder, section: Section) {
holder.titleView.setText(section.title)
holder.divider.isVisible = section.visibleDivider
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun bindStudy(holder: StudyViewHolder, study: EnrolledExperiment) {
holder.titleView.text = study.userFacingName
holder.summaryView.text = study.userFacingDescription

holder.deleteButton.setOnClickListener {
studiesDelegate.onRemoveButtonClicked(study)
}
}

internal fun createListWithSections(studies: List<EnrolledExperiment>): List<Any> {
val itemsWithSections = ArrayList<Any>()
val activeStudies = ArrayList<EnrolledExperiment>()

activeStudies.addAll(studies)

if (activeStudies.isNotEmpty()) {
itemsWithSections.add(Section(R.string.studies_active, true))
itemsWithSections.addAll(activeStudies)
}

return itemsWithSections
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal data class Section(@StringRes val title: Int, val visibleDivider: Boolean = true)

/**
* Removes the portion of the list that contains the provided [study].
* @property study The study to be removed.
*/
fun removeStudy(study: EnrolledExperiment) {
studiesMap.remove(study.slug)
submitList(createListWithSections(studiesMap.values.toList()))
}

internal object DifferCallback : DiffUtil.ItemCallback<Any>() {
override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
return when {
oldItem is EnrolledExperiment && newItem is EnrolledExperiment -> oldItem.slug == newItem.slug
oldItem is Section && newItem is Section -> oldItem.title == newItem.title
else -> false
}
}

@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
return oldItem == newItem
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* 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.settings.studies

import org.mozilla.experiments.nimbus.internal.EnrolledExperiment

/**
* Provides methods for handling the studies items.
*/
interface StudiesAdapterDelegate {
/**
* Handler for when the remove button is clicked.
*
* @param experiment The [EnrolledExperiment] to remove.
*/
fun onRemoveButtonClicked(experiment: EnrolledExperiment) = Unit
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* 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.settings.studies

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.databinding.SettingsStudiesBinding
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings

/**
* Lets the users control studies settings.
*/
class StudiesFragment : Fragment() {
private var _binding: SettingsStudiesBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val experiments = requireComponents.analytics.experiments
_binding = SettingsStudiesBinding.inflate(inflater, container, false)
val interactor = DefaultStudiesInteractor((activity as HomeActivity), experiments)
StudiesView(
lifecycleScope,
requireContext(),
binding,
interactor,
requireContext().settings(),
experiments,
::isAttached
).bind()

return binding.root
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

private fun isAttached(): Boolean = context != null
}
Loading

0 comments on commit a28d123

Please sign in to comment.