1
0
Fork 0

Copione merged onto master
continuous-integration/drone/push Build is passing Details

master
blallo 2020-07-08 00:00:42 +02:00
commit e13bbe159b
30 changed files with 302 additions and 246 deletions

View File

@ -216,7 +216,7 @@ class SettingsPrivacyTest {
}.enterURLAndEnterToBrowser(saveLoginTest.url) {
verifySaveLoginPromptIsShown()
// Don't save the login
saveLoginFromPrompt("Dont save")
saveLoginFromPrompt("Never save")
}.openTabDrawer {
}.openHomeScreen {
}.openThreeDotMenu {

View File

@ -7,6 +7,8 @@ package org.mozilla.fenix.ext
import android.app.Activity
import android.view.View
import android.view.WindowManager
import mozilla.components.support.base.log.Log
import org.mozilla.fenix.perf.Performance
/**
* Attempts to call immersive mode using the View to hide the status bar and navigation buttons.
@ -22,3 +24,17 @@ fun Activity.enterToImmersiveMode() {
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
}
/**
* Calls [Activity.reportFullyDrawn] while also preventing crashes under some circumstances.
*/
fun Activity.reportFullyDrawnSafe() {
try {
reportFullyDrawn()
} catch (e: SecurityException) {
// This exception is throw on some Samsung devices. We were unable to identify the root
// cause but suspect it's related to Samsung security features. See
// https://github.com/mozilla-mobile/fenix/issues/12345#issuecomment-655058864 for details.
Log.log(Log.Priority.ERROR, Performance.TAG, e, "Unable to call reportFullyDrawn")
}
}

View File

@ -1,98 +0,0 @@
/* 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.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.text.SpannableString
import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
import androidx.annotation.StringRes
import java.util.Formatter
// Credit to Michael Spitsin https://medium.com/@programmerr47/working-with-spans-in-android-ca4ab1327bc4
@Suppress("SpreadOperator")
fun Resources.getSpannable(@StringRes id: Int, spanParts: List<Pair<Any, Iterable<Any>>>): CharSequence {
val resultCreator = SpannableStringCreator()
Formatter(
SpannableAppendable(resultCreator, spanParts),
getLocale(configuration)
).format(getString(id), *spanParts.map { it.first }.toTypedArray())
return resultCreator.toSpannableString()
}
private fun getLocale(configuration: Configuration) =
if (SDK_INT >= Build.VERSION_CODES.N) {
configuration.locales[0]
} else {
@Suppress("Deprecation")
configuration.locale
}
class SpannableStringCreator {
private val parts = ArrayList<CharSequence>()
private var length = 0
private val spanMap: MutableMap<IntRange, Iterable<Any>> = HashMap()
fun append(newText: CharSequence, spans: Iterable<Any>) = apply {
val end = newText.length
parts.add(newText)
spanMap[(length..length + end)] = spans
length += end
}
fun append(newText: CharSequence) = apply {
parts.add(newText)
length += newText.length
}
fun toSpannableString() = SpannableString(parts.joinToString("")).apply {
spanMap.forEach { entry ->
val range = entry.key
entry.value.forEach {
setSpan(it, range.first, range.last, SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
}
class SpannableAppendable(
private val creator: SpannableStringCreator,
spanParts: List<Pair<Any, Iterable<Any>>>
) : Appendable {
private val spansMap = spanParts.toMap().mapKeys { entry -> entry.key.let { it as? CharSequence ?: it.toString() } }
override fun append(csq: CharSequence?) = apply { creator.appendSmart(csq, spansMap) }
override fun append(csq: CharSequence?, start: Int, end: Int) = apply {
if (csq != null) {
if (start in 0 until end && end <= csq.length) {
append(csq.subSequence(start, end))
} else {
throw IndexOutOfBoundsException("start " + start + ", end " + end + ", s.length() " + csq.length)
}
}
}
override fun append(c: Char) = apply { creator.append(c.toString()) }
private fun SpannableStringCreator.appendSmart(csq: CharSequence?, spanDict: Map<CharSequence, Iterable<Any>>) {
if (csq != null) {
if (csq in spanDict) {
append(csq, spanDict.getValue(csq))
} else {
val possibleMatchDict = spanDict.filter { it.key.toString() == csq }
if (possibleMatchDict.isNotEmpty()) {
val spanDictEntry = possibleMatchDict.entries.toList()[0]
append(spanDictEntry.key, spanDictEntry.value)
} else {
append(csq)
}
}
}
}
}

View File

@ -62,6 +62,7 @@ import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
@ -390,15 +391,9 @@ class HomeFragment : Fragment() {
}
consumeFrom(requireComponents.core.store) {
val tabCount = if (browsingModeManager.mode.isPrivate) {
it.privateTabs.size
} else {
it.normalTabs.size
}
view.tab_button?.setCountWithAnimation(tabCount)
view.add_tabs_to_collections_button?.isVisible = tabCount > 0
updateTabCounter(it)
}
updateTabCounter(requireComponents.core.store.state)
}
override fun onDestroyView() {
@ -927,6 +922,17 @@ class HomeFragment : Fragment() {
TabTrayDialogFragment.show(parentFragmentManager)
}
private fun updateTabCounter(browserState: BrowserState) {
val tabCount = if (browsingModeManager.mode.isPrivate) {
browserState.privateTabs.size
} else {
browserState.normalTabs.size
}
view?.tab_button?.setCountWithAnimation(tabCount)
view?.add_tabs_to_collections_button?.isVisible = tabCount > 0
}
companion object {
private const val ANIMATION_DELAY = 100L

View File

@ -14,7 +14,7 @@ import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.feature.tab.collections.TabCollection
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.ViewHolder
import org.mozilla.fenix.utils.view.ViewHolder
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getIconColor
import org.mozilla.fenix.ext.increaseTapArea

View File

@ -6,9 +6,9 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders
import android.view.View
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.no_collections_message.view.*
import kotlinx.android.synthetic.main.no_collections_message.*
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.ViewHolder
import org.mozilla.fenix.utils.view.ViewHolder
import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor
open class NoCollectionsMessageViewHolder(
@ -18,11 +18,12 @@ open class NoCollectionsMessageViewHolder(
) : ViewHolder(view) {
init {
view.add_tabs_to_collections_button.setOnClickListener {
add_tabs_to_collections_button.setOnClickListener {
interactor.onAddTabsToCollectionTapped()
}
view.add_tabs_to_collections_button.isVisible = hasNormalTabsOpened
add_tabs_to_collections_button.isVisible = hasNormalTabsOpened
}
companion object {
const val LAYOUT_ID = R.layout.no_collections_message
}

View File

@ -13,7 +13,7 @@ import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.util.dpToFloat
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.ViewHolder
import org.mozilla.fenix.utils.view.ViewHolder
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.loadIntoView

View File

@ -9,18 +9,14 @@ import android.os.Build.VERSION.SDK_INT
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.onboarding_theme_picker.view.clickable_region_automatic
import kotlinx.android.synthetic.main.onboarding_theme_picker.view.theme_automatic_radio_button
import kotlinx.android.synthetic.main.onboarding_theme_picker.view.theme_dark_image
import kotlinx.android.synthetic.main.onboarding_theme_picker.view.theme_dark_radio_button
import kotlinx.android.synthetic.main.onboarding_theme_picker.view.theme_light_image
import kotlinx.android.synthetic.main.onboarding_theme_picker.view.theme_light_radio_button
import kotlinx.android.synthetic.main.onboarding_theme_picker.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event.OnboardingThemePicker.Theme
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.onboarding.OnboardingRadioButton
import org.mozilla.fenix.utils.view.addToRadioGroup
class OnboardingThemePickerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
@ -35,17 +31,14 @@ class OnboardingThemePickerViewHolder(view: View) : RecyclerView.ViewHolder(view
R.string.pref_key_auto_battery_theme
}
radioLightTheme.addToRadioGroup(radioDarkTheme)
radioLightTheme.addToRadioGroup(radioFollowDeviceTheme)
addToRadioGroup(
radioLightTheme,
radioDarkTheme,
radioFollowDeviceTheme
)
radioLightTheme.addIllustration(view.theme_light_image)
radioDarkTheme.addToRadioGroup(radioLightTheme)
radioDarkTheme.addToRadioGroup(radioFollowDeviceTheme)
radioDarkTheme.addIllustration(view.theme_dark_image)
radioFollowDeviceTheme.addToRadioGroup(radioDarkTheme)
radioFollowDeviceTheme.addToRadioGroup(radioLightTheme)
view.theme_dark_image.setOnClickListener {
it.context.components.analytics.metrics.track(Event.OnboardingThemePicker(Theme.DARK))
radioDarkTheme.performClick()

View File

@ -6,10 +6,7 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.onboarding_toolbar_position_picker.view.toolbar_bottom_image
import kotlinx.android.synthetic.main.onboarding_toolbar_position_picker.view.toolbar_bottom_radio_button
import kotlinx.android.synthetic.main.onboarding_toolbar_position_picker.view.toolbar_top_image
import kotlinx.android.synthetic.main.onboarding_toolbar_position_picker.view.toolbar_top_radio_button
import kotlinx.android.synthetic.main.onboarding_toolbar_position_picker.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event.OnboardingToolbarPosition.Position
@ -17,6 +14,7 @@ import org.mozilla.fenix.ext.asActivity
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.onboarding.OnboardingRadioButton
import org.mozilla.fenix.utils.view.addToRadioGroup
class OnboardingToolbarPositionPickerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
@ -25,10 +23,8 @@ class OnboardingToolbarPositionPickerViewHolder(view: View) : RecyclerView.ViewH
val radioBottomToolbar = view.toolbar_bottom_radio_button
val radio: OnboardingRadioButton
radioTopToolbar.addToRadioGroup(radioBottomToolbar)
addToRadioGroup(radioTopToolbar, radioBottomToolbar)
radioTopToolbar.addIllustration(view.toolbar_top_image)
radioBottomToolbar.addToRadioGroup(radioTopToolbar)
radioBottomToolbar.addIllustration(view.toolbar_bottom_image)
radio = if (view.context.settings().shouldUseBottomToolbar) {

View File

@ -14,6 +14,7 @@ import org.mozilla.fenix.components.metrics.Event.OnboardingTrackingProtection.S
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.onboarding.OnboardingRadioButton
import org.mozilla.fenix.utils.view.addToRadioGroup
class OnboardingTrackingProtectionViewHolder(view: View) : RecyclerView.ViewHolder(view) {
@ -48,8 +49,7 @@ class OnboardingTrackingProtectionViewHolder(view: View) : RecyclerView.ViewHold
updateRadioGroupState(view, isChecked)
standardTrackingProtection.addToRadioGroup(strictTrackingProtection)
strictTrackingProtection.addToRadioGroup(standardTrackingProtection)
addToRadioGroup(standardTrackingProtection, strictTrackingProtection)
strictTrackingProtection.isChecked =
itemView.context.settings().useStrictTrackingProtection

View File

@ -15,7 +15,7 @@ import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.ViewHolder
import org.mozilla.fenix.utils.view.ViewHolder
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor

View File

@ -12,9 +12,14 @@ import androidx.core.content.edit
import androidx.core.content.withStyledAttributes
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.view.GroupableRadioButton
import org.mozilla.fenix.utils.view.uncheckAll
class OnboardingRadioButton(context: Context, attrs: AttributeSet) : AppCompatRadioButton(context, attrs) {
private val radioGroups = mutableListOf<OnboardingRadioButton>()
class OnboardingRadioButton(
context: Context,
attrs: AttributeSet
) : AppCompatRadioButton(context, attrs), GroupableRadioButton {
private val radioGroups = mutableListOf<GroupableRadioButton>()
private var illustration: ImageView? = null
private var clickListener: (() -> Unit)? = null
var key: Int = 0
@ -29,7 +34,7 @@ class OnboardingRadioButton(context: Context, attrs: AttributeSet) : AppCompatRa
}
}
fun addToRadioGroup(radioButton: OnboardingRadioButton) {
override fun addToRadioGroup(radioButton: GroupableRadioButton) {
radioGroups.add(radioButton)
}
@ -49,7 +54,7 @@ class OnboardingRadioButton(context: Context, attrs: AttributeSet) : AppCompatRa
}
}
fun updateRadioValue(isChecked: Boolean) {
override fun updateRadioValue(isChecked: Boolean) {
this.isChecked = isChecked
illustration?.let {
it.isSelected = isChecked
@ -61,7 +66,7 @@ class OnboardingRadioButton(context: Context, attrs: AttributeSet) : AppCompatRa
private fun toggleRadioGroups() {
if (isChecked) {
radioGroups.forEach { it.updateRadioValue(false) }
radioGroups.uncheckAll()
}
}
}

View File

@ -9,6 +9,7 @@ import android.view.View
import androidx.core.view.doOnPreDraw
import kotlinx.android.synthetic.main.activity_home.*
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.ext.reportFullyDrawnSafe
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSiteItemViewHolder
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.APP_LINK
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.HOMESCREEN
@ -64,6 +65,6 @@ class StartupReportFullyDrawn {
// - the difference in timing is minimal (< 7ms on Pixel 2)
// - if we compare against another app using a preDrawListener, as we are with Fennec, it
// should be comparable
view.doOnPreDraw { activity.reportFullyDrawn() }
view.doOnPreDraw { activity.reportFullyDrawnSafe() }
}
}

View File

@ -40,6 +40,7 @@ import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.content.hasCamera
import mozilla.components.support.ktx.android.content.isPermissionGranted
import mozilla.components.support.ktx.android.content.res.getSpanned
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.ui.autocomplete.InlineAutocompleteEditText
import org.mozilla.fenix.BrowserDirection
@ -50,7 +51,6 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getSpannable
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
@ -225,12 +225,10 @@ class SearchFragment : Fragment(), UserInteractionHandler {
search_scan_button.isChecked = false
activity?.let {
AlertDialog.Builder(it).apply {
val spannable = resources.getSpannable(
val spannable = resources.getSpanned(
R.string.qr_scanner_confirmation_dialog_message,
listOf(
getString(R.string.app_name) to listOf(StyleSpan(BOLD)),
result to listOf(StyleSpan(ITALIC))
)
getString(R.string.app_name) to StyleSpan(BOLD),
result to StyleSpan(ITALIC)
)
setMessage(spannable)
setNegativeButton(R.string.qr_scanner_dialog_negative) { dialog: DialogInterface, _ ->

View File

@ -16,6 +16,7 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.utils.view.addToRadioGroup
/**
* Lets the user customize the UI.
@ -48,23 +49,15 @@ class CustomizationFragment : PreferenceFragmentCompat() {
}
private fun setupRadioGroups() {
radioLightTheme.addToRadioGroup(radioDarkTheme)
radioDarkTheme.addToRadioGroup(radioLightTheme)
if (SDK_INT >= Build.VERSION_CODES.P) {
radioLightTheme.addToRadioGroup(radioFollowDeviceTheme)
radioDarkTheme.addToRadioGroup(radioFollowDeviceTheme)
radioFollowDeviceTheme.addToRadioGroup(radioDarkTheme)
radioFollowDeviceTheme.addToRadioGroup(radioLightTheme)
} else {
radioLightTheme.addToRadioGroup(radioAutoBatteryTheme)
radioDarkTheme.addToRadioGroup(radioAutoBatteryTheme)
radioAutoBatteryTheme.addToRadioGroup(radioLightTheme)
radioAutoBatteryTheme.addToRadioGroup(radioDarkTheme)
}
addToRadioGroup(
radioLightTheme,
radioDarkTheme,
if (SDK_INT >= Build.VERSION_CODES.P) {
radioFollowDeviceTheme
} else {
radioAutoBatteryTheme
}
)
}
private fun bindLightTheme() {
@ -132,7 +125,6 @@ class CustomizationFragment : PreferenceFragmentCompat() {
topPreference.setCheckedWithoutClickListener(!requireContext().settings().shouldUseBottomToolbar)
bottomPreference.setCheckedWithoutClickListener(requireContext().settings().shouldUseBottomToolbar)
topPreference.addToRadioGroup(bottomPreference)
bottomPreference.addToRadioGroup(topPreference)
addToRadioGroup(topPreference, bottomPreference)
}
}

View File

@ -16,12 +16,15 @@ import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.view.GroupableRadioButton
import org.mozilla.fenix.utils.view.uncheckAll
@Suppress("RestrictedApi", "PrivateResource")
open class RadioButtonPreference @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : Preference(context, attrs) {
private val radioGroups = mutableListOf<RadioButtonPreference>()
) : Preference(context, attrs), GroupableRadioButton {
private val radioGroups = mutableListOf<GroupableRadioButton>()
private var summaryView: TextView? = null
private var titleView: TextView? = null
private var radioButton: RadioButton? = null
@ -55,8 +58,8 @@ open class RadioButtonPreference @JvmOverloads constructor(
}
}
fun addToRadioGroup(radioPreference: RadioButtonPreference) {
radioGroups.add(radioPreference)
override fun addToRadioGroup(radioButton: GroupableRadioButton) {
radioGroups.add(radioButton)
}
fun onClickListener(listener: (() -> Unit)) {
@ -97,7 +100,7 @@ open class RadioButtonPreference @JvmOverloads constructor(
toggleRadioGroups()
}
private fun updateRadioValue(isChecked: Boolean) {
override fun updateRadioValue(isChecked: Boolean) {
persistBoolean(isChecked)
radioButton?.isChecked = isChecked
context.settings().preferences.edit().putBoolean(key, isChecked)
@ -113,7 +116,7 @@ open class RadioButtonPreference @JvmOverloads constructor(
private fun toggleRadioGroups() {
if (radioButton?.isChecked == true) {
radioGroups.forEach { it.updateRadioValue(false) }
radioGroups.uncheckAll()
}
}

View File

@ -21,6 +21,7 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.trackingprotection.TrackingProtectionMode
import org.mozilla.fenix.utils.view.addToRadioGroup
/**
* Displays the toggle for tracking protection, options for tracking protection policy and a button
@ -46,7 +47,7 @@ class TrackingProtectionFragment : PreferenceFragmentCompat() {
val radioStrict = bindTrackingProtectionRadio(TrackingProtectionMode.STRICT)
val radioStandard = bindTrackingProtectionRadio(TrackingProtectionMode.STANDARD)
val radioCustom = bindCustom()
setupRadioGroups(radioStrict, radioStandard, radioCustom)
addToRadioGroup(radioStrict, radioStandard, radioCustom)
updateCustomOptionsVisibility()
}
@ -208,21 +209,6 @@ class TrackingProtectionFragment : PreferenceFragmentCompat() {
}
}
private fun setupRadioGroups(
radioStrict: RadioButtonInfoPreference,
radioStandard: RadioButtonInfoPreference,
radioCustom: RadioButtonInfoPreference
) {
radioStandard.addToRadioGroup(radioStrict)
radioStrict.addToRadioGroup(radioStandard)
radioStandard.addToRadioGroup(radioCustom)
radioCustom.addToRadioGroup(radioStandard)
radioStrict.addToRadioGroup(radioCustom)
radioCustom.addToRadioGroup(radioStrict)
}
private fun updateCustomOptionsVisibility() {
val isCustomSelected = requireContext().settings().useCustomTrackingProtection
customCookies.isVisible = isCustomSelected

View File

@ -17,6 +17,7 @@ import android.util.Log
import androidx.appcompat.app.AlertDialog
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
@ -31,6 +32,7 @@ import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.service.fxa.SyncEngine
import mozilla.components.service.fxa.manager.SyncEnginesStorage
import org.mozilla.fenix.R
import org.mozilla.fenix.addons.runIfFragmentIsAttached
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
@ -39,7 +41,7 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.settings.SharedPreferenceUpdater
import org.mozilla.fenix.settings.requirePreference
import java.util.concurrent.Executors
import java.util.concurrent.Executor
@Suppress("TooManyFunctions", "LargeClass")
class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
@ -48,7 +50,7 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
private lateinit var biometricPromptCallback: BiometricPrompt.AuthenticationCallback
@TargetApi(M)
private val executor = Executors.newSingleThreadExecutor()
private lateinit var executor: Executor
@TargetApi(M)
private lateinit var biometricPrompt: BiometricPrompt
@ -60,6 +62,28 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
setPreferencesFromResource(R.xml.logins_preferences, rootKey)
}
/**
* There is a bug where while the biometric prompt is showing, you were able to quickly navigate
* so we are disabling the settings that navigate while authenticating.
* https://github.com/mozilla-mobile/fenix/issues/12312
*/
private fun togglePrefsEnabledWhileAuthenticating(enabled: Boolean) {
requirePreference<Preference>(R.string.pref_key_password_sync_logins).isEnabled = enabled
requirePreference<Preference>(R.string.pref_key_save_logins_settings).isEnabled = enabled
requirePreference<Preference>(R.string.pref_key_saved_logins).isEnabled = enabled
}
private fun navigateToSavedLogins() {
runIfFragmentIsAttached {
viewLifecycleOwner.lifecycleScope.launch(Main) {
// Workaround for likely biometric library bug
// https://github.com/mozilla-mobile/fenix/issues/8438
delay(SHORT_DELAY_MS)
navigateToSavedLoginsFragment()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -67,23 +91,22 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
Log.e(LOG_TAG, "onAuthenticationError $errString")
togglePrefsEnabledWhileAuthenticating(enabled = true)
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.d(LOG_TAG, "onAuthenticationSucceeded")
viewLifecycleOwner.lifecycleScope.launch(Main) {
// Workaround for likely biometric library bug
// https://github.com/mozilla-mobile/fenix/issues/8438
delay(SHORT_DELAY_MS)
navigateToSavedLoginsFragment()
}
navigateToSavedLogins()
}
override fun onAuthenticationFailed() {
Log.e(LOG_TAG, "onAuthenticationFailed")
togglePrefsEnabledWhileAuthenticating(enabled = true)
}
}
executor = ContextCompat.getMainExecutor(requireContext())
biometricPrompt = BiometricPrompt(this, executor, biometricPromptCallback)
promptInfo = BiometricPrompt.PromptInfo.Builder()
@ -122,6 +145,7 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
requirePreference<Preference>(R.string.pref_key_saved_logins).setOnPreferenceClickListener {
if (Build.VERSION.SDK_INT >= M && isHardwareAvailable && hasBiometricEnrolled) {
togglePrefsEnabledWhileAuthenticating(enabled = false)
biometricPrompt.authenticate(promptInfo)
} else {
verifyPinOrShowSetupWarning()
@ -148,7 +172,7 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
override fun onAuthenticationProblems() = updateSyncPreferenceNeedsReauth()
val isHardwareAvailable: Boolean by lazy {
private val isHardwareAvailable: Boolean by lazy {
if (Build.VERSION.SDK_INT >= M) {
context?.let {
val bm = BiometricManager.from(it)
@ -161,7 +185,7 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
}
}
val hasBiometricEnrolled: Boolean by lazy {
private val hasBiometricEnrolled: Boolean by lazy {
if (Build.VERSION.SDK_INT >= M) {
context?.let {
val bm = BiometricManager.from(it)
@ -252,7 +276,8 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
getString(R.string.logins_biometric_prompt_message_pin),
getString(R.string.logins_biometric_prompt_message)
)
startActivityForResult(intent,
startActivityForResult(
intent,
PIN_REQUEST
)
}
@ -267,7 +292,8 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
private fun navigateToSavedLoginsFragment() {
context?.components?.analytics?.metrics?.track(Event.OpenLogins)
val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToLoginsListFragment()
val directions =
SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToLoginsListFragment()
findNavController().navigate(directions)
}
@ -283,7 +309,8 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat(), AccountObserver {
}
private fun navigateToTurnOnSyncFragment() {
val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToTurnOnSyncFragment()
val directions =
SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToTurnOnSyncFragment()
findNavController().navigate(directions)
}

View File

@ -15,6 +15,7 @@ import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.settings.RadioButtonPreference
import org.mozilla.fenix.settings.SharedPreferenceUpdater
import org.mozilla.fenix.settings.requirePreference
import org.mozilla.fenix.utils.view.addToRadioGroup
class SavedLoginsSettingFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -26,7 +27,7 @@ class SavedLoginsSettingFragment : PreferenceFragmentCompat() {
showToolbar(getString(R.string.preferences_passwords_save_logins))
val save = bindSave()
val neverSave = bindNeverSave()
setupRadioGroups(save, neverSave)
addToRadioGroup(save, neverSave)
}
private fun bindSave(): RadioButtonPreference {
@ -66,12 +67,4 @@ class SavedLoginsSettingFragment : PreferenceFragmentCompat() {
}
return preferenceNeverSave
}
private fun setupRadioGroups(
radioNeverSave: RadioButtonPreference,
radioSave: RadioButtonPreference
) {
radioNeverSave.addToRadioGroup(radioSave)
radioSave.addToRadioGroup(radioNeverSave)
}
}

View File

@ -0,0 +1,28 @@
/* 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.utils.view
interface GroupableRadioButton {
fun updateRadioValue(isChecked: Boolean)
fun addToRadioGroup(radioButton: GroupableRadioButton)
}
/**
* Connect all the given radio buttons into a group,
* so that when one radio is checked the others are unchecked.
*/
fun addToRadioGroup(vararg radios: GroupableRadioButton) {
for (i in 0..radios.lastIndex) {
for (j in (i + 1)..radios.lastIndex) {
radios[i].addToRadioGroup(radios[j])
radios[j].addToRadioGroup(radios[i])
}
}
}
fun Iterable<GroupableRadioButton>.uncheckAll() {
forEach { it.updateRadioValue(isChecked = false) }
}

View File

@ -1,4 +1,4 @@
package org.mozilla.fenix.ext
package org.mozilla.fenix.utils.view
import android.view.View
import androidx.recyclerview.widget.RecyclerView

View File

@ -110,6 +110,7 @@
android:layout_height="0dp"
android:paddingBottom="140dp"
android:clipToPadding="false"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@ -834,11 +834,17 @@
<!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header_preview_installed">Firefox Nightly пераехаў</string>
<!-- text for firefox preview moving tip description -->
<string name="tip_firefox_preview_moved_description_preview_installed">Гэта праграма больш не будзе атрымліваць абнаўленні бяспекі. Перастаньце карыстацца гэтай праграмай і перайдзіце на новы Nightly.
\n\nКаб перанесці закладкі, лагіны і гісторыю ў другую праграму, стварыце ўліковы запіс Firefox.</string>
<!-- text for firefox preview moving tip button -->
<string name="tip_firefox_preview_moved_button_preview_installed">Пераключыцца на новы Nightly</string>
<!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header_preview_not_installed">Firefox Nightly пераехаў</string>
<!-- text for firefox preview moving tip description -->
<string name="tip_firefox_preview_moved_description_preview_not_installed">Гэта праграма больш не будзе атрымліваць абнаўленні бяспекі. Атрымайце новы Nightly і больш не карыстайцеся гэтай праграмай.
\n\nКаб перанесці закладкі, лагіны і гісторыю ў другую праграму, стварыце ўліковы запіс Firefox.</string>
<!-- text for firefox preview moving tip button -->
<string name="tip_firefox_preview_moved_button_preview_not_installed">Атрымаць новы Nightly</string>
@ -1024,6 +1030,8 @@
<string name="etp_fingerprinters_title">Збіральнікі лічбавых адбіткаў</string>
<!-- Category of trackers (tracking content) that can be blocked by Enhanced Tracking Protection -->
<string name="etp_tracking_content_title">Змест з элементамі сачэння</string>
<!-- Enhanced Tracking Protection message that protection is currently on for this site -->
<string name="etp_panel_on">Ахова ўключана на гэтым сайце</string>
<!-- Header for exceptions list for which sites enhanced tracking protection is always off -->
<string name="enhanced_tracking_protection_exceptions">Узмоцненая ахова ад сачэння выключана на гэтых сайтах</string>

View File

@ -15,8 +15,6 @@
<string name="content_description_disable_private_browsing_button">Tichup ri ichinan okem pa k\'amaya\'l</string>
<!-- Placeholder text shown in the search bar before a user enters text -->
<string name="search_hint">Tikanöx o ketz\'ib\'äx ri taq ochochib\'äl</string>
<!-- No Open Tabs Message Header -->
<string name="no_open_tabs_header_2">Majun ruwi\' ejaqon</string>
<!-- No Open Tabs Message Description -->
<string name="no_open_tabs_description">Wawe\' xkeq\'alajin pe ri jaqäl taq ruwi\'.</string>
@ -301,8 +299,6 @@
<string name="preferences_sync_bookmarks">Taq yaketal</string>
<!-- Preference for syncing logins -->
<string name="preferences_sync_logins">Ketikirisäx molojri\'ïl</string>
<!-- Preference for syncing tabs -->
<string name="preferences_sync_tabs">Taq ruwi\'</string>
<!-- Preference for signing out -->
<string name="preferences_sign_out">Titz\'apïx molojri\'ïl</string>
<!-- Preference displays and allows changing current FxA device name -->
@ -480,7 +476,7 @@
<!-- Shortcut action to open the home screen -->
<string name="tab_tray_menu_home">Tib\'e pa tikirib\'äl</string>
<!-- Shortcut action to toggle private mode -->
<string name="tab_tray_menu_toggle">Tik\'ex rik\'in ruwi\' rub\'anikil</string>
<string name="tab_tray_menu_toggle">Tik\'exlöx pa ruwi\' b\'anikil</string>
<!-- Content description (not visible, for screen readers etc.): Removes tab from collection button. Removes the selected tab from collection when pressed -->
<string name="remove_tab_from_collection">Tiyuj ruwi\' pa molb\'äl</string>
<!-- Content description (not visible, for screen readers etc.): Close tab button. Closes the current session when pressed -->
@ -493,8 +489,6 @@
<string name="tabs_menu_close_all_tabs">Titz\'apïx ronojel ri taq ruwi\'</string>
<!-- Open tabs menu item to share all tabs -->
<string name="tabs_menu_share_tabs">Kekomonïx taq ruwi\'</string>
<!-- Open tabs menu item to save tabs to collection -->
<string name="tabs_menu_save_to_collection">Tiyak pa mol</string>
<!-- Content description (not visible, for screen readers etc.): Opens the tab menu when pressed -->
<string name="tab_menu">Ruk\'utsamaj ruwi\'</string>
<!-- Tab menu item to share the tab -->
@ -711,17 +705,11 @@
<string name="delete_browsing_data_quit_off">Tichup</string>
<!-- Collections -->
<!-- Label to describe what collections are to a new user without any collections -->
<string name="collections_description">Ke\'ayaka\' ri nimaläj taq awachinäq. Richin natikirisaj, ke\'ayaka\' ri jaqon taq ruwi\' pa jun k\'ak\'a\' mol.</string>
<!-- Collections header on home fragment -->
<string name="collections_header">Taq mol</string>
<!-- Content description (not visible, for screen readers etc.): Opens the collection menu when pressed -->
<string name="collection_menu_button_content_description">Ruk\'utsamaj mol</string>
<!-- No Open Tabs Message Header -->
<string name="no_collections_header">Majun mol</string>
<!-- No Open Tabs Message Description -->
<string name="no_collections_description">Xkewachin wawe\' ri taq amol.</string>
<!-- Title for the "select tabs" step of the collection creator -->
<string name="create_collection_select_tabs">Kecha\' taq Ruwi\'</string>
@ -909,8 +897,6 @@
<string name="preference_summary_delete_browsing_data_on_quit">Keyuj ri taq tzij okem pa k\'amaya\'l pa ruyonil toq xtacha\' &quot;Tel&quot; pa ri nïm k\'utsamajib\'äl</string>
<!-- Summary for the Delete browsing data on quit preference. "Quit" translation should match delete_browsing_data_on_quit_action translation. -->
<string name="preference_summary_delete_browsing_data_on_quit_2">Keyuj ri taq tzij okem pa k\'amaya\'l pa ruyonil toq xtacha\' \&quot;Tel\&quot; pa ri nïm k\'utsamajib\'äl</string>
<!-- Category for history items to delete on quit in delete browsing data on quit -->
<string name="preferences_delete_browsing_data_on_quit_browsing_history">Runatab\'al okem pa k\'amaya\'l</string>
<!-- Action item in menu for the Delete browsing data on quit feature -->
<string name="delete_browsing_data_on_quit_action">Tel</string>
@ -934,8 +920,6 @@
<!-- text for firefox preview moving tip description -->
<string name="tip_firefox_preview_moved_description">Firefox Nightly nuk\'ëx ri\' ronojel taq aq\'a\' chuqa\' k\'o k\'ak\'a\' tojtob\'enel taq rusamaj.
Po rik\'in jub\'a\' man kan ta jikïl. Taqasaj ri beta qokik\'amaya\'l richin jun jikïl samaj.</string>
<!-- text for firefox preview moving tip button. "Mozilla Firefox Browser" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_button">Tak\'ulu\' ri Mozilla Firefox Okik\'amaya\'l</string>
<!-- text for firefox preview moving tip header. "Firefox Nightly" is intentionally hardcoded -->
<string name="tip_firefox_preview_moved_header_preview_installed">Firefox Nightly xsilöx</string>
@ -1402,7 +1386,7 @@ Achi\'el: \nhttps://www.google.com/search?q=%s</string>
<!-- Second step for the allowing a permission -->
<string name="phone_feature_blocked_step_permissions"><![CDATA[2. Tachapa\' <b>Taq Ya\'oj Q\'ij</b>]]></string>
<!-- Third step for the allowing a permission (Fore example: Camera) -->
<string name="phone_feature_blocked_step_feature"><![CDATA[3. Tajala\' <b>%1$s</b> rik\'in TZIJÏL]]></string>
<string name="phone_feature_blocked_step_feature"><![CDATA[3. Tik\'exlöx <b>%1$s</b> rik\'in TZIJÏL]]></string>
<!-- Label that indicates a site is using a secure connection -->
<string name="quick_settings_sheet_secure_connection">Ütz Okem</string>
@ -1464,4 +1448,4 @@ Achi\'el: \nhttps://www.google.com/search?q=%s</string>
<!-- Text displayed when user has disabled tab syncing in Firefox Sync Account -->
<string name="synced_tabs_enable_tab_syncing">Tatzija\' ri kiximik taq ruwi\'.</string>
</resources>
</resources>

View File

@ -530,13 +530,13 @@
<!-- Text for the button to clear all history -->
<string name="history_delete_all">기록 삭제</string>
<!-- Text for the dialog to confirm clearing all history -->
<string name="history_delete_all_dialog">기록을 삭제하시겠습니까?</string>
<string name="history_delete_all_dialog">기록을 지우시겠습니까?</string>
<!-- Text for the snackbar to confirm that multiple browsing history items has been deleted -->
<string name="history_delete_multiple_items_snackbar">기록 삭제됨</string>
<!-- Text for the snackbar to confirm that a single browsing history item has been deleted. The first parameter is the shortened URL of the deleted history item. -->
<string name="history_delete_single_item_snackbar">%1$s 삭제됨</string>
<!-- Text for positive action to delete history in deleting history dialog -->
<string name="history_clear_dialog">삭제</string>
<string name="history_clear_dialog">지우기</string>
<!-- History overflow menu copy button -->
<string name="history_menu_copy_button">복사</string>
<!-- History overflow menu share button -->
@ -672,9 +672,9 @@
<!-- button that allows editing site permissions settings -->
<string name="quick_settings_sheet_manage_site_permissions">사이트 권한 관리</string>
<!-- Button label for clearing all the information of site permissions-->
<string name="clear_permissions">모든 권한 삭제</string>
<string name="clear_permissions">모든 권한 지우기</string>
<!-- Button label for clearing a site permission-->
<string name="clear_permission">권한 삭제</string>
<string name="clear_permission">권한 지우기</string>
<!-- Button label for clearing all the information on all sites-->
<string name="clear_permissions_on_all_sites">모든 사이트에 대해 권한 지우기</string>
<!-- Preference for altering video and audio autoplay for all websites -->
@ -928,8 +928,6 @@
<string name="preference_summary_delete_browsing_data_on_quit">주 메뉴에서 &quot;종료&quot;를 선택하면 탐색 데이터를 자동으로 삭제합니다</string>
<!-- Summary for the Delete browsing data on quit preference. "Quit" translation should match delete_browsing_data_on_quit_action translation. -->
<string name="preference_summary_delete_browsing_data_on_quit_2">주 메뉴에서 \&quot;종료\&quot;를 선택하면 탐색 데이터를 자동으로 삭제합니다</string>
<!-- Category for history items to delete on quit in delete browsing data on quit -->
<string name="preferences_delete_browsing_data_on_quit_browsing_history">방문 기록</string>
<!-- Action item in menu for the Delete browsing data on quit feature -->
<string name="delete_browsing_data_on_quit_action">종료</string>

File diff suppressed because one or more lines are too long

View File

@ -1297,7 +1297,7 @@
<!-- Saved logins sorting strategy menu item -by name- (if selected, it will sort saved logins alphabetically) -->
<string name="saved_logins_sort_strategy_alphabetically">Nome (A-Z)</string>
<!-- Saved logins sorting strategy menu item -by last used- (if selected, it will sort saved logins by last used) -->
<string name="saved_logins_sort_strategy_last_used">Usados por último</string>
<string name="saved_logins_sort_strategy_last_used">Data de uso</string>
<!-- Content description (not visible, for screen readers etc.): Sort saved logins dropdown menu chevron icon -->
<string name="saved_logins_menu_dropdown_chevron_icon_content_description">Menu de ordenação de contas</string>

View File

@ -0,0 +1,66 @@
/* 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.utils.view
import io.mockk.Called
import io.mockk.mockk
import io.mockk.verify
import io.mockk.verifySequence
import org.junit.Test
class GroupableRadioButtonTest {
@Test
fun `test add 1 radio to group`() {
val radio = mockk<GroupableRadioButton>(relaxed = true)
addToRadioGroup(radio)
verify { radio wasNot Called }
}
@Test
fun `test add 2 radios to group`() {
val radio1 = mockk<GroupableRadioButton>(relaxed = true)
val radio2 = mockk<GroupableRadioButton>(relaxed = true)
addToRadioGroup(radio1, radio2)
verifySequence {
radio1.addToRadioGroup(radio2)
radio2.addToRadioGroup(radio1)
}
}
@Test
fun `test add 3 radios to group`() {
val radio1 = mockk<GroupableRadioButton>(relaxed = true)
val radio2 = mockk<GroupableRadioButton>(relaxed = true)
val radio3 = mockk<GroupableRadioButton>(relaxed = true)
addToRadioGroup(radio1, radio2, radio3)
verifySequence {
radio1.addToRadioGroup(radio2)
radio2.addToRadioGroup(radio1)
radio1.addToRadioGroup(radio3)
radio3.addToRadioGroup(radio1)
radio2.addToRadioGroup(radio3)
radio3.addToRadioGroup(radio2)
}
}
@Test
fun `test uncheck all`() {
val radio1 = mockk<GroupableRadioButton>(relaxed = true)
val radio2 = mockk<GroupableRadioButton>(relaxed = true)
val radio3 = mockk<GroupableRadioButton>(relaxed = true)
listOf(radio1, radio2, radio3).uncheckAll()
verifySequence {
radio1.updateRadioValue(false)
radio2.updateRadioValue(false)
radio3.updateRadioValue(false)
}
}
}

View File

@ -3,5 +3,5 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
object AndroidComponents {
const val VERSION = "49.0.20200706130124"
const val VERSION = "49.0.20200707131055"
}

View File

@ -17,11 +17,8 @@ primary-dependency: signing
group-by: build-type
only-for-build-types:
- fennec-nightly
- fennec-beta
- fennec-production
- nightly
- beta
- production
job-template:
@ -34,11 +31,8 @@ job-template:
default: true
channel:
by-build-type:
fennec-nightly: fennec-nightly
fennec-beta: fennec-beta
fennec-production: fennec-production
nightly: nightly
beta: beta
production: production
dep:
by-level: