Skip to content
This repository has been archived by the owner on Feb 20, 2023. It is now read-only.

Commit

Permalink
For #13926 - MP migration
Browse files Browse the repository at this point in the history
  • Loading branch information
ekager committed Aug 31, 2020
1 parent 70e853a commit 7f297f4
Show file tree
Hide file tree
Showing 11 changed files with 429 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package org.mozilla.fenix.components.tips

import android.graphics.drawable.Drawable

sealed class TipType {
data class Button(val text: String, val action: () -> Unit) : TipType()
}
Expand All @@ -13,7 +15,8 @@ open class Tip(
val identifier: String,
val title: String,
val description: String,
val learnMoreURL: String?
val learnMoreURL: String?,
val titleDrawable: Drawable? = null
)

interface TipProvider {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/* 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.components.tips.providers

import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import io.sentry.Sentry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.appservices.logins.IdCollisionException
import mozilla.appservices.logins.InvalidRecordException
import mozilla.appservices.logins.LoginsStorageException
import mozilla.appservices.logins.ServerPassword
import mozilla.components.concept.storage.Login
import mozilla.components.support.migration.FennecLoginsMPImporter
import mozilla.components.support.migration.FennecProfile
import org.mozilla.fenix.R
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.components.tips.TipProvider
import org.mozilla.fenix.components.tips.TipType
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings

/**
* Tip explaining to master password users how to migrate their logins.
*/
class MasterPasswordTipProvider(
private val context: Context,
private val navigateToLogins: () -> Unit,
private val dismissTip: (Tip) -> Unit
) : TipProvider {

private val fennecLoginsMPImporter: FennecLoginsMPImporter? by lazy {
FennecProfile.findDefault(
context,
context.components.analytics.crashReporter
)?.let {
FennecLoginsMPImporter(
it
)
}
}

override val tip: Tip? by lazy { masterPasswordMigrationTip() }

override val shouldDisplay: Boolean by lazy {
context.settings().shouldDisplayMasterPasswordMigrationTip && fennecLoginsMPImporter?.hasMasterPassword() == true
}

private fun masterPasswordMigrationTip(): Tip =
Tip(
type = TipType.Button(
text = context.getString(R.string.mp_homescreen_button),
action = ::showMasterPasswordMigration
),
identifier = context.getString(R.string.pref_key_master_password_tip),
title = context.getString(R.string.mp_homescreen_tip_title),
description = context.getString(R.string.mp_homescreen_tip_message),
learnMoreURL = null,
titleDrawable = ContextCompat.getDrawable(context, R.drawable.ic_login)
)

private fun showMasterPasswordMigration() {
val dialogView = LayoutInflater.from(context).inflate(R.layout.mp_migration_dialog, null)

val dialogBuilder = AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.mp_dialog_title_recovery_transfer_saved_logins))
setMessage(context.getString(R.string.mp_dialog_message_recovery_transfer_saved_logins))
setView(dialogView)
create()
}

val dialog = dialogBuilder.show()

val passwordErrorText = context.getString(R.string.mp_dialog_error_transfer_saved_logins)
val migrationContinueButton =
dialogView.findViewById<MaterialButton>(R.id.migration_continue)
val passwordView = dialogView.findViewById<TextInputEditText>(R.id.password_field)
val passwordLayout =
dialogView.findViewById<TextInputLayout>(R.id.password_text_input_layout)
passwordView.addTextChangedListener(
object : TextWatcher {
var isValid = false
override fun afterTextChanged(p: Editable?) {
when {
p.toString().isEmpty() -> {
isValid = false
passwordLayout.error = passwordErrorText
}
else -> {
val possiblePassword = passwordView.text.toString()
isValid =
fennecLoginsMPImporter?.checkPassword(possiblePassword) == true
passwordLayout.error = if (isValid) null else passwordErrorText
}
}
migrationContinueButton.alpha = if (isValid) 1F else HALF_OPACITY
migrationContinueButton.isEnabled = isValid
}

override fun beforeTextChanged(
p: CharSequence?,
start: Int,
count: Int,
after: Int
) {
// NOOP
}

override fun onTextChanged(p: CharSequence?, start: Int, before: Int, count: Int) {
// NOOP
}
})

migrationContinueButton.apply {
setOnClickListener {
// Step 1: Verify the password again before trying to use it
val possiblePassword = passwordView.text.toString()
val isValid = fennecLoginsMPImporter?.checkPassword(possiblePassword) == true

// Step 2: With valid MP, get logins and complete the migration
if (isValid) {
val logins = fennecLoginsMPImporter?.getLoginRecords(
possiblePassword,
context.components.analytics.crashReporter
) ?: return@setOnClickListener

saveLogins(logins, dialog)
} else {
passwordView.error =
context?.getString(R.string.mp_dialog_error_transfer_saved_logins)
}
}
}

dialogView.findViewById<MaterialButton>(R.id.migration_cancel).apply {
setOnClickListener {
dialog.dismiss()
}
}
}

private fun saveLogins(logins: List<ServerPassword>, dialog: AlertDialog) {
CoroutineScope(IO).launch {
logins.map { it.toLogin() }.forEach {
try {
context.components.core.passwordsStorage.add(it)
} catch (e: InvalidRecordException) {
// This record was invalid and we couldn't save this login
Sentry.capture("Master Password migration add login error $e for reason ${e.reason}")
} catch (e: IdCollisionException) {
// Nonempty ID was provided
Sentry.capture("Master Password migration add login error $e")
} catch (e: LoginsStorageException) {
// Some other error occurred
Sentry.capture("Master Password migration add login error $e")
}
}
withContext(Dispatchers.Main) {
// Step 3: Dismiss this dialog and show the success dialog
showSuccessDialog()
dialog.dismiss()
}
}
}

private fun showSuccessDialog() {
tip?.let { dismissTip(it) }

val dialogView =
LayoutInflater.from(context).inflate(R.layout.mp_migration_success_dialog, null)

val dialogBuilder = AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.mp_dialog_title_transfer_success))
setMessage(context.getString(R.string.mp_dialog_message_transfer_success))
setView(dialogView)
create()
}

val dialog = dialogBuilder.show()

dialogView.findViewById<MaterialButton>(R.id.view_saved).apply {
setOnClickListener {
navigateToLogins()
dialog.dismiss()
}
}
dialogView.findViewById<MaterialButton>(R.id.migration_close).apply {
setOnClickListener {
dialog.dismiss()
}
}
}

/**
* Converts an Application Services [ServerPassword] to an Android Components [Login]
*/
fun ServerPassword.toLogin() = Login(
origin = hostname,
formActionOrigin = formSubmitURL,
httpRealm = httpRealm,
username = username,
password = password,
timesUsed = timesUsed,
timeCreated = timeCreated,
timeLastUsed = timeLastUsed,
timePasswordChanged = timePasswordChanged,
usernameField = usernameField,
passwordField = passwordField
)

companion object {
private const val HALF_OPACITY = .5F
}
}
36 changes: 33 additions & 3 deletions app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.tips.FenixTipManager
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.components.tips.providers.MasterPasswordTipProvider
import org.mozilla.fenix.components.tips.providers.MigrationTipProvider
import org.mozilla.fenix.components.toolbar.TabCounterMenu
import org.mozilla.fenix.components.toolbar.ToolbarPosition
Expand Down Expand Up @@ -197,7 +199,16 @@ class HomeFragment : Fragment() {
expandedCollections = emptySet(),
mode = currentMode.getCurrentMode(),
topSites = components.core.topSiteStorage.cachedTopSites,
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip(),
tip = FenixTipManager(
listOf(
MasterPasswordTipProvider(
requireContext(),
::navToSavedLogins,
::dismissTip
),
MigrationTipProvider(requireContext())
)
).getTip(),
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
)
)
Expand Down Expand Up @@ -232,6 +243,7 @@ class HomeFragment : Fragment() {
handleSwipedItemDeletionCancel = ::handleSwipedItemDeletionCancel
)
)

updateLayout(view)
sessionControlView = SessionControlView(
view.sessionControlRecyclerView,
Expand All @@ -246,6 +258,10 @@ class HomeFragment : Fragment() {
return view
}

private fun dismissTip(tip: Tip) {
sessionControlInteractor.onCloseTip(tip)
}

/**
* Returns a [TopSitesConfig] which specifies how many top sites to display and whether or
* not frequently visited sites should be displayed.
Expand Down Expand Up @@ -411,7 +427,8 @@ class HomeFragment : Fragment() {
// We call this onLayout so that the bottom bar width is correctly set for us to center
// the CFR in.
view.toolbar_wrapper.doOnLayout {
val willNavigateToSearch = !bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience
val willNavigateToSearch =
!bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience
if (!browsingModeManager.mode.isPrivate && !willNavigateToSearch) {
SearchWidgetCFR(
context = view.context,
Expand Down Expand Up @@ -540,7 +557,16 @@ class HomeFragment : Fragment() {
collections = components.core.tabCollectionStorage.cachedTabCollections,
mode = currentMode.getCurrentMode(),
topSites = components.core.topSiteStorage.cachedTopSites,
tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip(),
tip = FenixTipManager(
listOf(
MasterPasswordTipProvider(
requireContext(),
::navToSavedLogins,
::dismissTip
),
MigrationTipProvider(requireContext())
)
).getTip(),
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
)
)
Expand Down Expand Up @@ -587,6 +613,10 @@ class HomeFragment : Fragment() {
}
}

private fun navToSavedLogins() {
findNavController().navigate(HomeFragmentDirections.actionGlobalSavedLoginsAuthFragment())
}

private fun dispatchModeChanges(mode: Mode) {
if (mode != Mode.fromBrowsingMode(browsingModeManager.mode)) {
homeFragmentStore.dispatch(HomeFragmentAction.ModeChange(mode))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class ButtonTipViewHolder(
metrics.track(Event.TipDisplayed(tip.identifier))

tip_header_text.text = tip.title
tip.titleDrawable?.let {
tip_header_text.setCompoundDrawablesWithIntrinsicBounds(it, null, null, null)
}
tip_description_text.text = tip.description
tip_button.text = tip.type.text

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ open class SavedLoginsStorageController(

fun findPotentialDuplicates(loginId: String) {
var deferredLogin: Deferred<List<Login>>? = null
// What scope should be used here?
val fetchLoginJob = viewLifecycleScope.launch(ioDispatcher) {
deferredLogin = async {
val login = getLogin(loginId)
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/org/mozilla/fenix/utils/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = false
)

var shouldDisplayMasterPasswordMigrationTip by booleanPreference(
appContext.getString(R.string.pref_key_master_password_tip),
true
)

// If any of the prefs have been modified, quit displaying the fenix moved tip
fun shouldDisplayFenixMovingTip(): Boolean =
preferences.getBoolean(
Expand Down

0 comments on commit 7f297f4

Please sign in to comment.