For #5547 - Add top level auth when accessing passwords
parent
218763f9be
commit
3e2b88cc91
|
@ -405,6 +405,7 @@ dependencies {
|
||||||
debugImplementation Deps.leakcanary
|
debugImplementation Deps.leakcanary
|
||||||
|
|
||||||
implementation Deps.androidx_legacy
|
implementation Deps.androidx_legacy
|
||||||
|
implementation Deps.androidx_biometric
|
||||||
implementation Deps.androidx_paging
|
implementation Deps.androidx_paging
|
||||||
implementation Deps.androidx_preference
|
implementation Deps.androidx_preference
|
||||||
implementation Deps.androidx_fragment
|
implementation Deps.androidx_fragment
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
|
||||||
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" android:requiredFeature="false"/>
|
||||||
|
|
||||||
<!-- Needed to prompt the user to give permission to install a downloaded apk -->
|
<!-- Needed to prompt the user to give permission to install a downloaded apk -->
|
||||||
<uses-permission-sdk-23 android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission-sdk-23 android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.text.InputType
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.fragment_saved_login_site_info.*
|
import kotlinx.android.synthetic.main.fragment_saved_login_site_info.*
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
@ -24,6 +25,12 @@ class SavedLoginSiteInfoFragment : Fragment(R.layout.fragment_saved_login_site_i
|
||||||
).savedLoginItem
|
).savedLoginItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
// If we pause this fragment, we want to pop users back to reauth
|
||||||
|
findNavController().popBackStack(R.id.loginsFragment, false)
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,12 @@ class SavedLoginsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
// If we pause this fragment, we want to pop users back to reauth
|
||||||
|
findNavController().popBackStack(R.id.loginsFragment, false)
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
private fun itemClicked(item: SavedLoginsItem) {
|
private fun itemClicked(item: SavedLoginsItem) {
|
||||||
val directions =
|
val directions =
|
||||||
SavedLoginsFragmentDirections.actionSavedLoginsFragmentToSavedLoginSiteInfoFragment(item)
|
SavedLoginsFragmentDirections.actionSavedLoginsFragmentToSavedLoginSiteInfoFragment(item)
|
||||||
|
|
|
@ -4,11 +4,25 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.settings
|
package org.mozilla.fenix.settings
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.app.KeyguardManager
|
||||||
|
import android.content.Context.KEYGUARD_SERVICE
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Build.VERSION_CODES.M
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import mozilla.components.concept.sync.AccountObserver
|
import mozilla.components.concept.sync.AccountObserver
|
||||||
import mozilla.components.concept.sync.AuthType
|
import mozilla.components.concept.sync.AuthType
|
||||||
import mozilla.components.concept.sync.OAuthAccount
|
import mozilla.components.concept.sync.OAuthAccount
|
||||||
|
@ -17,6 +31,8 @@ import mozilla.components.service.fxa.manager.SyncEnginesStorage
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.ext.getPreferenceKey
|
import org.mozilla.fenix.ext.getPreferenceKey
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
|
import org.mozilla.fenix.ext.settings
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
|
class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
|
||||||
|
@ -31,7 +47,11 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
|
||||||
|
|
||||||
val savedLoginsKey = getPreferenceKey(R.string.pref_key_saved_logins)
|
val savedLoginsKey = getPreferenceKey(R.string.pref_key_saved_logins)
|
||||||
findPreference<Preference>(savedLoginsKey)?.setOnPreferenceClickListener {
|
findPreference<Preference>(savedLoginsKey)?.setOnPreferenceClickListener {
|
||||||
navigateToLoginsSettingsFragment()
|
if (Build.VERSION.SDK_INT >= M && isHardwareAvailable && hasBiometricEnrolled) {
|
||||||
|
showBiometricPrompt()
|
||||||
|
} else {
|
||||||
|
verifyPinOrShowSetupWarning()
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +74,31 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
|
||||||
|
|
||||||
override fun onAuthenticationProblems() = updateSyncPreferenceNeedsReauth()
|
override fun onAuthenticationProblems() = updateSyncPreferenceNeedsReauth()
|
||||||
|
|
||||||
|
val isHardwareAvailable: Boolean by lazy {
|
||||||
|
if (Build.VERSION.SDK_INT >= M) {
|
||||||
|
context?.let {
|
||||||
|
val bm = BiometricManager.from(it)
|
||||||
|
val canAuthenticate = bm.canAuthenticate()
|
||||||
|
!(canAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ||
|
||||||
|
canAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE)
|
||||||
|
} ?: false
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasBiometricEnrolled: Boolean by lazy {
|
||||||
|
if (Build.VERSION.SDK_INT >= M) {
|
||||||
|
context?.let {
|
||||||
|
val bm = BiometricManager.from(it)
|
||||||
|
val canAuthenticate = bm.canAuthenticate()
|
||||||
|
(canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||||
|
} ?: false
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateSyncPreferenceStatus() {
|
private fun updateSyncPreferenceStatus() {
|
||||||
val syncLogins = getPreferenceKey(R.string.pref_key_password_sync_logins)
|
val syncLogins = getPreferenceKey(R.string.pref_key_password_sync_logins)
|
||||||
findPreference<Preference>(syncLogins)?.apply {
|
findPreference<Preference>(syncLogins)?.apply {
|
||||||
|
@ -92,7 +137,93 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToLoginsSettingsFragment() {
|
@TargetApi(M)
|
||||||
|
private fun showBiometricPrompt() {
|
||||||
|
val biometricPromptCallback = object : BiometricPrompt.AuthenticationCallback() {
|
||||||
|
|
||||||
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
|
// Authentication Error
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||||
|
lifecycleScope.launch(Main) {
|
||||||
|
navigateToSavedLoginsFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationFailed() {
|
||||||
|
// Authenticated Failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val executor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
|
val biometricPrompt = BiometricPrompt(this, executor, biometricPromptCallback)
|
||||||
|
|
||||||
|
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||||
|
.setTitle(getString(R.string.logins_biometric_prompt_message))
|
||||||
|
.setDeviceCredentialAllowed(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
biometricPrompt.authenticate(promptInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyPinOrShowSetupWarning() {
|
||||||
|
val manager = activity?.getSystemService(KEYGUARD_SERVICE) as KeyguardManager
|
||||||
|
if (manager.isKeyguardSecure) {
|
||||||
|
showPinVerification()
|
||||||
|
} else {
|
||||||
|
if (context?.settings()?.shouldShowSecurityPinWarning == true) {
|
||||||
|
showPinDialogWarning()
|
||||||
|
} else {
|
||||||
|
navigateToSavedLoginsFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPinDialogWarning() {
|
||||||
|
context?.let {
|
||||||
|
AlertDialog.Builder(it).apply {
|
||||||
|
setTitle(getString(R.string.logins_warning_dialog_title))
|
||||||
|
setMessage(
|
||||||
|
getString(R.string.logins_warning_dialog_message)
|
||||||
|
)
|
||||||
|
|
||||||
|
setNegativeButton(getString(R.string.logins_warning_dialog_later)) { _: DialogInterface, _ ->
|
||||||
|
navigateToSavedLoginsFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
setPositiveButton(getString(R.string.logins_warning_dialog_set_up_now)) { it: DialogInterface, _ ->
|
||||||
|
it.dismiss()
|
||||||
|
val intent = Intent(
|
||||||
|
android.provider.Settings.ACTION_SECURITY_SETTINGS
|
||||||
|
)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
create()
|
||||||
|
}.show()
|
||||||
|
it.settings().incrementShowLoginsSecureWarningCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPinVerification() {
|
||||||
|
val manager = activity?.getSystemService(KEYGUARD_SERVICE) as KeyguardManager
|
||||||
|
val intent = manager.createConfirmDeviceCredentialIntent(
|
||||||
|
getString(R.string.logins_biometric_prompt_message_pin),
|
||||||
|
getString(R.string.logins_biometric_prompt_message)
|
||||||
|
)
|
||||||
|
startActivityForResult(intent, PIN_REQUEST)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
if (requestCode == PIN_REQUEST && resultCode == RESULT_OK) {
|
||||||
|
navigateToSavedLoginsFragment()
|
||||||
|
} else {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToSavedLoginsFragment() {
|
||||||
val directions = LoginsFragmentDirections.actionLoginsFragmentToSavedLoginsFragment()
|
val directions = LoginsFragmentDirections.actionLoginsFragmentToSavedLoginsFragment()
|
||||||
findNavController().navigate(directions)
|
findNavController().navigate(directions)
|
||||||
}
|
}
|
||||||
|
@ -111,4 +242,8 @@ class LoginsFragment : PreferenceFragmentCompat(), AccountObserver {
|
||||||
val directions = LoginsFragmentDirections.actionLoginsFragmentToTurnOnSyncFragment()
|
val directions = LoginsFragmentDirections.actionLoginsFragmentToTurnOnSyncFragment()
|
||||||
findNavController().navigate(directions)
|
findNavController().navigate(directions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PIN_REQUEST = 303
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,16 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.settings.account
|
package org.mozilla.fenix.settings.account
|
||||||
|
|
||||||
|
import android.app.KeyguardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
import android.text.InputFilter
|
import android.text.InputFilter
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
@ -36,6 +42,7 @@ import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.getPreferenceKey
|
import org.mozilla.fenix.ext.getPreferenceKey
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
|
import org.mozilla.fenix.ext.settings
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
class AccountSettingsFragment : PreferenceFragmentCompat() {
|
class AccountSettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
@ -180,14 +187,27 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
|
||||||
val loginsNameKey = getPreferenceKey(R.string.pref_key_sync_logins)
|
val loginsNameKey = getPreferenceKey(R.string.pref_key_sync_logins)
|
||||||
findPreference<CheckBoxPreference>(loginsNameKey)?.apply {
|
findPreference<CheckBoxPreference>(loginsNameKey)?.apply {
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
SyncEnginesStorage(context).setStatus(SyncEngine.Passwords, newValue as Boolean)
|
val manager =
|
||||||
@Suppress("DeferredResultUnused")
|
activity?.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||||
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
|
if (manager.isKeyguardSecure ||
|
||||||
|
newValue == false ||
|
||||||
|
!context.settings().shouldShowSecurityPinWarningSync
|
||||||
|
) {
|
||||||
|
SyncEnginesStorage(context).setStatus(SyncEngine.Passwords, newValue as Boolean)
|
||||||
|
@Suppress("DeferredResultUnused")
|
||||||
|
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
|
||||||
|
} else {
|
||||||
|
showPinDialogWarning(newValue as Boolean)
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceConstellation?.registerDeviceObserver(deviceConstellationObserver, owner = this, autoPause = true)
|
deviceConstellation?.registerDeviceObserver(
|
||||||
|
deviceConstellationObserver,
|
||||||
|
owner = this,
|
||||||
|
autoPause = true
|
||||||
|
)
|
||||||
|
|
||||||
// NB: ObserverRegistry will take care of cleaning up internal references to 'observer' and
|
// NB: ObserverRegistry will take care of cleaning up internal references to 'observer' and
|
||||||
// 'owner' when appropriate.
|
// 'owner' when appropriate.
|
||||||
|
@ -196,6 +216,33 @@ class AccountSettingsFragment : PreferenceFragmentCompat() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showPinDialogWarning(newValue: Boolean) {
|
||||||
|
context?.let {
|
||||||
|
AlertDialog.Builder(it).apply {
|
||||||
|
setTitle(getString(R.string.logins_warning_dialog_title))
|
||||||
|
setMessage(
|
||||||
|
getString(R.string.logins_warning_dialog_message)
|
||||||
|
)
|
||||||
|
|
||||||
|
setNegativeButton(getString(R.string.logins_warning_dialog_later)) { _: DialogInterface, _ ->
|
||||||
|
SyncEnginesStorage(context).setStatus(SyncEngine.Passwords, newValue)
|
||||||
|
@Suppress("DeferredResultUnused")
|
||||||
|
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.EngineChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPositiveButton(getString(R.string.logins_warning_dialog_set_up_now)) { it: DialogInterface, _ ->
|
||||||
|
it.dismiss()
|
||||||
|
val intent = Intent(
|
||||||
|
Settings.ACTION_SECURITY_SETTINGS
|
||||||
|
)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
create()
|
||||||
|
}.show()
|
||||||
|
it.settings().incrementShowLoginsSecureWarningSyncCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateSyncEngineStates() {
|
private fun updateSyncEngineStates() {
|
||||||
val syncEnginesStatus = SyncEnginesStorage(context!!).getStatus()
|
val syncEnginesStatus = SyncEnginesStorage(context!!).getStatus()
|
||||||
val bookmarksNameKey = getPreferenceKey(R.string.pref_key_sync_bookmarks)
|
val bookmarksNameKey = getPreferenceKey(R.string.pref_key_sync_bookmarks)
|
||||||
|
|
|
@ -37,6 +37,8 @@ class Settings private constructor(
|
||||||
private val isCrashReportEnabledInBuild: Boolean
|
private val isCrashReportEnabledInBuild: Boolean
|
||||||
) : PreferencesHolder {
|
) : PreferencesHolder {
|
||||||
companion object {
|
companion object {
|
||||||
|
const val showLoginsSecureWarningSyncMaxCount = 1
|
||||||
|
const val showLoginsSecureWarningMaxCount = 1
|
||||||
const val autoBounceMaximumCount = 2
|
const val autoBounceMaximumCount = 2
|
||||||
const val trackingProtectionOnboardingMaximumCount = 2
|
const val trackingProtectionOnboardingMaximumCount = 2
|
||||||
const val FENIX_PREFERENCES = "fenix_preferences"
|
const val FENIX_PREFERENCES = "fenix_preferences"
|
||||||
|
@ -133,6 +135,12 @@ class Settings private constructor(
|
||||||
val shouldAutoBounceQuickActionSheet: Boolean
|
val shouldAutoBounceQuickActionSheet: Boolean
|
||||||
get() = autoBounceQuickActionSheetCount < autoBounceMaximumCount
|
get() = autoBounceQuickActionSheetCount < autoBounceMaximumCount
|
||||||
|
|
||||||
|
val shouldShowSecurityPinWarningSync: Boolean
|
||||||
|
get() = loginsSecureWarningSyncCount < showLoginsSecureWarningSyncMaxCount
|
||||||
|
|
||||||
|
val shouldShowSecurityPinWarning: Boolean
|
||||||
|
get() = loginsSecureWarningCount < showLoginsSecureWarningMaxCount
|
||||||
|
|
||||||
var shouldUseLightTheme by booleanPreference(
|
var shouldUseLightTheme by booleanPreference(
|
||||||
appContext.getPreferenceKey(R.string.pref_key_light_theme),
|
appContext.getPreferenceKey(R.string.pref_key_light_theme),
|
||||||
default = false
|
default = false
|
||||||
|
@ -249,6 +257,32 @@ class Settings private constructor(
|
||||||
default = 0
|
default = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = PRIVATE)
|
||||||
|
internal val loginsSecureWarningSyncCount by intPreference(
|
||||||
|
appContext.getPreferenceKey(R.string.pref_key_logins_secure_warning_sync),
|
||||||
|
default = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = PRIVATE)
|
||||||
|
internal val loginsSecureWarningCount by intPreference(
|
||||||
|
appContext.getPreferenceKey(R.string.pref_key_logins_secure_warning),
|
||||||
|
default = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
fun incrementShowLoginsSecureWarningCount() {
|
||||||
|
preferences.edit().putInt(
|
||||||
|
appContext.getPreferenceKey(R.string.pref_key_logins_secure_warning),
|
||||||
|
loginsSecureWarningCount + 1
|
||||||
|
).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun incrementShowLoginsSecureWarningSyncCount() {
|
||||||
|
preferences.edit().putInt(
|
||||||
|
appContext.getPreferenceKey(R.string.pref_key_logins_secure_warning_sync),
|
||||||
|
loginsSecureWarningSyncCount + 1
|
||||||
|
).apply()
|
||||||
|
}
|
||||||
|
|
||||||
fun incrementAutomaticBounceQuickActionSheetCount() {
|
fun incrementAutomaticBounceQuickActionSheetCount() {
|
||||||
preferences.edit().putInt(
|
preferences.edit().putInt(
|
||||||
appContext.getPreferenceKey(R.string.pref_key_bounce_quick_action),
|
appContext.getPreferenceKey(R.string.pref_key_bounce_quick_action),
|
||||||
|
|
|
@ -104,6 +104,8 @@
|
||||||
<!-- Logins Settings -->
|
<!-- Logins Settings -->
|
||||||
<string name="pref_key_saved_logins" translatable="false">pref_key_saved_logins</string>
|
<string name="pref_key_saved_logins" translatable="false">pref_key_saved_logins</string>
|
||||||
<string name="pref_key_password_sync_logins" translatable="false">pref_key_password_sync_logins</string>
|
<string name="pref_key_password_sync_logins" translatable="false">pref_key_password_sync_logins</string>
|
||||||
|
<string name="pref_key_logins_secure_warning_sync" translatable="false">pref_key_logins_secure_warning_sync</string>
|
||||||
|
<string name="pref_key_logins_secure_warning" translatable="false">pref_key_logins_secure_warning</string>
|
||||||
|
|
||||||
<!-- Privacy Settings -->
|
<!-- Privacy Settings -->
|
||||||
<string name="pref_key_open_links_in_a_private_tab" translatable="false">pref_key_open_links_in_a_private_tab</string>
|
<string name="pref_key_open_links_in_a_private_tab" translatable="false">pref_key_open_links_in_a_private_tab</string>
|
||||||
|
|
|
@ -1009,4 +1009,16 @@
|
||||||
<string name="saved_login_reveal_password">Show password</string>
|
<string name="saved_login_reveal_password">Show password</string>
|
||||||
<!-- Content Description (for screenreaders etc) read for the button to hide a password in logins -->
|
<!-- Content Description (for screenreaders etc) read for the button to hide a password in logins -->
|
||||||
<string name="saved_login_hide_password">Hide password</string>
|
<string name="saved_login_hide_password">Hide password</string>
|
||||||
|
<!-- Message displayed in biometric prompt displayed for authentication before allowing users to view their logins -->
|
||||||
|
<string name="logins_biometric_prompt_message">Unlock to view your saved logins</string>
|
||||||
|
<!-- Title of warning dialog if users have no device authentication set up -->
|
||||||
|
<string name="logins_warning_dialog_title">Secure your logins and passwords</string>
|
||||||
|
<!-- Message of warning dialog if users have no device authentication set up -->
|
||||||
|
<string name="logins_warning_dialog_message">Set up a device lock pattern, PIN, or password to protect your saved logins and passwords from being accessed if someone else has your device.</string>
|
||||||
|
<!-- Negative button to ignore warning dialog if users have no device authentication set up -->
|
||||||
|
<string name="logins_warning_dialog_later">Later</string>
|
||||||
|
<!-- Positive button to send users to set up a pin of warning dialog if users have no device authentication set up -->
|
||||||
|
<string name="logins_warning_dialog_set_up_now">Set up now</string>
|
||||||
|
<!-- Title of PIN verification dialog to direct users to re-enter their device credentials to access their logins -->
|
||||||
|
<string name="logins_biometric_prompt_message_pin">Unlock your device</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -173,6 +173,58 @@ class SettingsTest {
|
||||||
assertFalse(settings.shouldAutoBounceQuickActionSheet)
|
assertFalse(settings.shouldAutoBounceQuickActionSheet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showLoginsDialogWarningSync() {
|
||||||
|
// When just created
|
||||||
|
// Then
|
||||||
|
assertEquals(0, settings.loginsSecureWarningSyncCount)
|
||||||
|
|
||||||
|
// When
|
||||||
|
settings.incrementShowLoginsSecureWarningSyncCount()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(1, settings.loginsSecureWarningSyncCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldShowLoginsDialogWarningSync() {
|
||||||
|
// When just created
|
||||||
|
// Then
|
||||||
|
assertTrue(settings.shouldShowSecurityPinWarningSync)
|
||||||
|
|
||||||
|
// When
|
||||||
|
settings.incrementShowLoginsSecureWarningSyncCount()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertFalse(settings.shouldShowSecurityPinWarningSync)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun showLoginsDialogWarning() {
|
||||||
|
// When just created
|
||||||
|
// Then
|
||||||
|
assertEquals(0, settings.loginsSecureWarningCount)
|
||||||
|
|
||||||
|
// When
|
||||||
|
settings.incrementShowLoginsSecureWarningCount()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(1, settings.loginsSecureWarningCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldShowLoginsDialogWarning() {
|
||||||
|
// When just created
|
||||||
|
// Then
|
||||||
|
assertTrue(settings.shouldShowSecurityPinWarning)
|
||||||
|
|
||||||
|
// When
|
||||||
|
settings.incrementShowLoginsSecureWarningCount()
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertFalse(settings.shouldShowSecurityPinWarning)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldUseLightTheme() {
|
fun shouldUseLightTheme() {
|
||||||
// When just created
|
// When just created
|
||||||
|
@ -298,7 +350,10 @@ class SettingsTest {
|
||||||
fun sitePermissionsPhoneFeatureCameraAction() {
|
fun sitePermissionsPhoneFeatureCameraAction() {
|
||||||
// When just created
|
// When just created
|
||||||
// Then
|
// Then
|
||||||
assertEquals(ASK_TO_ALLOW, settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.CAMERA))
|
assertEquals(
|
||||||
|
ASK_TO_ALLOW,
|
||||||
|
settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.CAMERA)
|
||||||
|
)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.CAMERA, BLOCKED)
|
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.CAMERA, BLOCKED)
|
||||||
|
@ -311,33 +366,48 @@ class SettingsTest {
|
||||||
fun sitePermissionsPhoneFeatureMicrophoneAction() {
|
fun sitePermissionsPhoneFeatureMicrophoneAction() {
|
||||||
// When just created
|
// When just created
|
||||||
// Then
|
// Then
|
||||||
assertEquals(ASK_TO_ALLOW, settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.MICROPHONE))
|
assertEquals(
|
||||||
|
ASK_TO_ALLOW,
|
||||||
|
settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.MICROPHONE)
|
||||||
|
)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.MICROPHONE, BLOCKED)
|
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.MICROPHONE, BLOCKED)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
assertEquals(BLOCKED, settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.MICROPHONE))
|
assertEquals(
|
||||||
|
BLOCKED,
|
||||||
|
settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.MICROPHONE)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun sitePermissionsPhoneFeatureNotificationAction() {
|
fun sitePermissionsPhoneFeatureNotificationAction() {
|
||||||
// When just created
|
// When just created
|
||||||
// Then
|
// Then
|
||||||
assertEquals(ASK_TO_ALLOW, settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.NOTIFICATION))
|
assertEquals(
|
||||||
|
ASK_TO_ALLOW,
|
||||||
|
settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.NOTIFICATION)
|
||||||
|
)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.NOTIFICATION, BLOCKED)
|
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.NOTIFICATION, BLOCKED)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
assertEquals(BLOCKED, settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.NOTIFICATION))
|
assertEquals(
|
||||||
|
BLOCKED,
|
||||||
|
settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.NOTIFICATION)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun sitePermissionsPhoneFeatureLocation() {
|
fun sitePermissionsPhoneFeatureLocation() {
|
||||||
// When just created
|
// When just created
|
||||||
// Then
|
// Then
|
||||||
assertEquals(ASK_TO_ALLOW, settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.LOCATION))
|
assertEquals(
|
||||||
|
ASK_TO_ALLOW,
|
||||||
|
settings.getSitePermissionsPhoneFeatureAction(PhoneFeature.LOCATION)
|
||||||
|
)
|
||||||
|
|
||||||
// When
|
// When
|
||||||
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.LOCATION, BLOCKED)
|
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.LOCATION, BLOCKED)
|
||||||
|
|
|
@ -18,6 +18,7 @@ object Versions {
|
||||||
const val osslicenses_library = "17.0.0"
|
const val osslicenses_library = "17.0.0"
|
||||||
|
|
||||||
const val androidx_appcompat = "1.1.0"
|
const val androidx_appcompat = "1.1.0"
|
||||||
|
const val androidx_biometric = "1.0.0-rc02"
|
||||||
const val androidx_coordinator_layout = "1.1.0-beta01"
|
const val androidx_coordinator_layout = "1.1.0-beta01"
|
||||||
const val androidx_constraint_layout = "2.0.0-beta2"
|
const val androidx_constraint_layout = "2.0.0-beta2"
|
||||||
const val androidx_preference = "1.1.0"
|
const val androidx_preference = "1.1.0"
|
||||||
|
@ -166,6 +167,7 @@ object Deps {
|
||||||
const val leanplum = "com.leanplum:leanplum-core:${Versions.leanplum}"
|
const val leanplum = "com.leanplum:leanplum-core:${Versions.leanplum}"
|
||||||
|
|
||||||
const val androidx_annotation = "androidx.annotation:annotation:${Versions.androidx_annotation}"
|
const val androidx_annotation = "androidx.annotation:annotation:${Versions.androidx_annotation}"
|
||||||
|
const val androidx_biometric = "androidx.biometric:biometric:${Versions.androidx_biometric}"
|
||||||
const val androidx_fragment = "androidx.fragment:fragment-ktx:${Versions.androidx_fragment}"
|
const val androidx_fragment = "androidx.fragment:fragment-ktx:${Versions.androidx_fragment}"
|
||||||
const val androidx_appcompat = "androidx.appcompat:appcompat:${Versions.androidx_appcompat}"
|
const val androidx_appcompat = "androidx.appcompat:appcompat:${Versions.androidx_appcompat}"
|
||||||
const val androidx_coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:${Versions.androidx_coordinator_layout}"
|
const val androidx_coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:${Versions.androidx_coordinator_layout}"
|
||||||
|
|
Loading…
Reference in New Issue