Skip to content

Commit cb0165a

Browse files
committed
Launch import flow from Password management screen
1 parent 83c5c62 commit cb0165a

File tree

9 files changed

+413
-15
lines changed

9 files changed

+413
-15
lines changed

autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillScreens.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ sealed interface AutofillScreens {
6464
}
6565

6666
@Parcelize
67-
data class Success(val importedCount: Int) : Result
67+
data class Success(val importedCount: Int, val foundInImport: Int) : Result
6868

6969
@Parcelize
7070
data class UserCancelled(val stage: String) : Result

autofill/autofill-impl/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ plugins {
1818
id 'com.android.library'
1919
id 'kotlin-android'
2020
id 'com.squareup.anvil'
21+
id 'kotlin-parcelize'
2122
}
2223

2324
apply from: "$rootProject.projectDir/gradle/android-library.gradle"

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/CsvPasswordImporter.kt

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.duckduckgo.autofill.impl.importing
1818

1919
import android.net.Uri
2020
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
21+
import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter.ImportResult
2122
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
2223
import com.duckduckgo.common.utils.DispatcherProvider
2324
import com.duckduckgo.di.scopes.AppScope
@@ -26,8 +27,13 @@ import javax.inject.Inject
2627
import kotlinx.coroutines.withContext
2728

2829
interface CsvPasswordImporter {
29-
suspend fun importCsv(fileUri: Uri): List<Long>
30-
suspend fun importCsv(blob: String): List<Long>
30+
suspend fun importCsv(fileUri: Uri): ImportResult
31+
suspend fun importCsv(blob: String): ImportResult
32+
33+
sealed interface ImportResult {
34+
data class Success(val numberPasswordsInSource: Int, val passwordIdsImported: List<Long>) : ImportResult
35+
data object Error : ImportResult
36+
}
3137
}
3238

3339
@ContributesBinding(AppScope::class)
@@ -42,30 +48,31 @@ class GooglePasswordManagerCsvPasswordImporter @Inject constructor(
4248
private val blobDecoder: GooglePasswordBlobDecoder,
4349
) : CsvPasswordImporter {
4450

45-
override suspend fun importCsv(blob: String): List<Long> {
51+
override suspend fun importCsv(blob: String): ImportResult {
4652
return kotlin.runCatching {
4753
withContext(dispatchers.io()) {
4854
val csv = blobDecoder.decode(blob)
4955
importPasswords(csv)
5056
}
51-
}.getOrElse { emptyList() }
57+
}.getOrElse { ImportResult.Error }
5258
}
5359

54-
override suspend fun importCsv(fileUri: Uri): List<Long> {
60+
override suspend fun importCsv(fileUri: Uri): ImportResult {
5561
return kotlin.runCatching {
5662
withContext(dispatchers.io()) {
5763
val csv = fileReader.readCsvFile(fileUri)
5864
importPasswords(csv)
5965
}
60-
}.getOrElse { emptyList() }
66+
}.getOrElse { ImportResult.Error }
6167
}
6268

63-
private suspend fun importPasswords(csv: String): List<Long> {
69+
private suspend fun importPasswords(csv: String): ImportResult {
6470
val allPasswords = parser.parseCsv(csv)
6571
val dedupedPasswords = allPasswords.distinct()
6672
val validPasswords = filterValidPasswords(dedupedPasswords)
6773
val normalizedDomains = domainNameNormalizer.normalizeDomains(validPasswords)
68-
return savePasswords(normalizedDomains)
74+
val ids = savePasswords(normalizedDomains)
75+
return ImportResult.Success(allPasswords.size, ids)
6976
}
7077

7178
private suspend fun savePasswords(passwords: List<LoginCredentials>): List<Long> {

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowFragment.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import com.duckduckgo.autofill.api.domain.app.LoginTriggerType
4646
import com.duckduckgo.autofill.impl.R
4747
import com.duckduckgo.autofill.impl.databinding.FragmentImportGooglePasswordsWebflowBinding
4848
import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter
49+
import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter.ImportResult.Error
50+
import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter.ImportResult.Success
4951
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.NavigatingBack
5052
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.ShowingWebContent
5153
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.UserCancelledImportFlow
@@ -289,10 +291,15 @@ class ImportGooglePasswordsWebFlowFragment :
289291
override suspend fun onCsvAvailable(csv: String) {
290292
Timber.i("cdr CSV available %s", csv)
291293
val result = csvPasswordImporter.importCsv(csv)
292-
Timber.i("cdr Imported %d passwords", result.size)
293-
val resultBundle = Bundle().also {
294-
it.putParcelable(RESULT_KEY_DETAILS, Result.Success(result.size))
294+
val resultDetails = when (result) {
295+
is Success -> {
296+
Timber.i("cdr Found %d passwords; Imported %d passwords", result.numberPasswordsInSource, result.passwordIdsImported.size)
297+
Result.Success(foundInImport = result.numberPasswordsInSource, importedCount = result.passwordIdsImported.size)
298+
}
299+
300+
Error -> Result.Error
295301
}
302+
val resultBundle = Bundle().also { it.putParcelable(RESULT_KEY_DETAILS, resultDetails) }
296303
setFragmentResult(RESULT_KEY, resultBundle)
297304
}
298305

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ import android.view.ViewGroup
2525
import android.widget.CompoundButton
2626
import androidx.activity.result.contract.ActivityResultContracts
2727
import androidx.constraintlayout.widget.ConstraintLayout
28+
import androidx.core.os.BundleCompat
2829
import androidx.core.text.toSpanned
2930
import androidx.core.view.MenuProvider
3031
import androidx.core.view.children
3132
import androidx.core.view.updateLayoutParams
3233
import androidx.core.view.updateMargins
34+
import androidx.fragment.app.setFragmentResultListener
3335
import androidx.lifecycle.Lifecycle
3436
import androidx.lifecycle.Lifecycle.State
3537
import androidx.lifecycle.ViewModelProvider
@@ -68,6 +70,10 @@ import com.duckduckgo.autofill.impl.ui.credential.management.sorting.CredentialG
6870
import com.duckduckgo.autofill.impl.ui.credential.management.sorting.InitialExtractor
6971
import com.duckduckgo.autofill.impl.ui.credential.management.suggestion.SuggestionListBuilder
7072
import com.duckduckgo.autofill.impl.ui.credential.management.suggestion.SuggestionMatcher
73+
import com.duckduckgo.autofill.impl.ui.credential.management.viewing.SelectImportPasswordMethodDialog.Companion.Result
74+
import com.duckduckgo.autofill.impl.ui.credential.management.viewing.SelectImportPasswordMethodDialog.Companion.Result.UserChoseCsvImport
75+
import com.duckduckgo.autofill.impl.ui.credential.management.viewing.SelectImportPasswordMethodDialog.Companion.Result.UserChoseDesktopSyncImport
76+
import com.duckduckgo.autofill.impl.ui.credential.management.viewing.SelectImportPasswordMethodDialog.Companion.Result.UserChoseGcmImport
7177
import com.duckduckgo.browser.api.ui.BrowserScreens.WebViewActivityWithParams
7278
import com.duckduckgo.common.ui.DuckDuckGoFragment
7379
import com.duckduckgo.common.ui.view.SearchBar
@@ -243,6 +249,23 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
243249
viewModel.onImportPasswords()
244250
pixel.fire(AUTOFILL_IMPORT_PASSWORDS_CTA_BUTTON)
245251
}
252+
253+
setFragmentResultListener(SelectImportPasswordMethodDialog.RESULT_KEY) { _, result ->
254+
when (val importResult = BundleCompat.getParcelable(result, SelectImportPasswordMethodDialog.RESULT_KEY_DETAILS, Result::class.java)) {
255+
is UserChoseGcmImport -> userImportViaGcm(importResult.numberImported)
256+
is UserChoseCsvImport -> userImportedViaCsv(importResult.numberImported)
257+
is UserChoseDesktopSyncImport -> launchImportPasswordsFromDesktopSyncScreen()
258+
else -> {}
259+
}
260+
}
261+
}
262+
263+
private fun userImportedViaCsv(numberImported: Int) {
264+
Snackbar.make(binding.root, getString(R.string.autofillImportCsvPasswordsSuccessMessage, numberImported), Snackbar.LENGTH_LONG).show()
265+
}
266+
267+
private fun userImportViaGcm(numberImported: Int) {
268+
Snackbar.make(binding.root, getString(R.string.autofillImportGooglePasswordsSuccessMessage, numberImported), Snackbar.LENGTH_LONG).show()
246269
}
247270

248271
private fun configureToolbar() {
@@ -397,6 +420,13 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
397420
}
398421

399422
private fun launchImportPasswordsScreen() {
423+
context?.let {
424+
val dialog = SelectImportPasswordMethodDialog.instance()
425+
dialog.show(parentFragmentManager, "SelectImportPasswordMethodDialog")
426+
}
427+
}
428+
429+
private fun launchImportPasswordsFromDesktopSyncScreen() {
400430
context?.let {
401431
globalActivityStarter.start(it, ImportPasswordActivityParams)
402432
}
@@ -607,7 +637,11 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill
607637
}
608638

609639
companion object {
610-
fun instance(currentUrl: String? = null, privacyProtectionEnabled: Boolean?, source: AutofillSettingsLaunchSource? = null) =
640+
fun instance(
641+
currentUrl: String? = null,
642+
privacyProtectionEnabled: Boolean?,
643+
source: AutofillSettingsLaunchSource? = null,
644+
) =
611645
AutofillManagementListMode().apply {
612646
arguments = Bundle().apply {
613647
putString(ARG_CURRENT_URL, currentUrl)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/*
2+
* Copyright (c) 2022 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.autofill.impl.ui.credential.management.viewing
18+
19+
import android.app.Activity
20+
import android.content.Context
21+
import android.content.DialogInterface
22+
import android.content.Intent
23+
import android.os.Bundle
24+
import android.os.Parcelable
25+
import android.view.LayoutInflater
26+
import android.view.View
27+
import android.view.ViewGroup
28+
import androidx.activity.result.ActivityResult
29+
import androidx.activity.result.contract.ActivityResultContracts
30+
import androidx.core.content.IntentCompat
31+
import androidx.fragment.app.setFragmentResult
32+
import androidx.lifecycle.lifecycleScope
33+
import com.duckduckgo.anvil.annotations.InjectWith
34+
import com.duckduckgo.app.browser.favicon.FaviconManager
35+
import com.duckduckgo.app.statistics.pixels.Pixel
36+
import com.duckduckgo.autofill.api.AutofillScreens.ImportGooglePassword
37+
import com.duckduckgo.autofill.impl.R
38+
import com.duckduckgo.autofill.impl.databinding.ContentChooseImportPasswordMethodBinding
39+
import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter
40+
import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter.ImportResult.Error
41+
import com.duckduckgo.autofill.impl.importing.CsvPasswordImporter.ImportResult.Success
42+
import com.duckduckgo.autofill.impl.ui.credential.dialog.animateClosed
43+
import com.duckduckgo.di.scopes.FragmentScope
44+
import com.duckduckgo.navigation.api.GlobalActivityStarter
45+
import com.google.android.material.bottomsheet.BottomSheetBehavior
46+
import com.google.android.material.bottomsheet.BottomSheetDialog
47+
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
48+
import dagger.android.support.AndroidSupportInjection
49+
import javax.inject.Inject
50+
import kotlinx.coroutines.launch
51+
import kotlinx.parcelize.Parcelize
52+
import timber.log.Timber
53+
54+
@InjectWith(FragmentScope::class)
55+
class SelectImportPasswordMethodDialog : BottomSheetDialogFragment() {
56+
57+
@Inject
58+
lateinit var pixel: Pixel
59+
60+
/**
61+
* To capture all the ways the BottomSheet can be dismissed, we might end up with onCancel being called when we don't want it
62+
* This flag is set to true when taking an action which dismisses the dialog, but should not be treated as a cancellation.
63+
*/
64+
private var ignoreCancellationEvents = false
65+
66+
override fun getTheme(): Int = R.style.AutofillBottomSheetDialogTheme
67+
68+
@Inject
69+
lateinit var faviconManager: FaviconManager
70+
71+
@Inject
72+
lateinit var csvPasswordImporter: CsvPasswordImporter
73+
74+
@Inject
75+
lateinit var globalActivityStarter: GlobalActivityStarter
76+
77+
private val importGooglePasswordsFlowLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
78+
Timber.i("cdr onActivityResult for Google Password Manager import flow. resultCode=${result.resultCode}")
79+
80+
if (result.resultCode == Activity.RESULT_OK) {
81+
when (val resultDetails = parseGooglePasswordImportResultDetails(result)) {
82+
is ImportGooglePassword.Result.Success -> setResult(Result.UserChoseGcmImport(resultDetails.importedCount))
83+
is ImportGooglePassword.Result.Error -> setResult(Result.ErrorDuringImport)
84+
is ImportGooglePassword.Result.UserCancelled -> {}
85+
}
86+
}
87+
}
88+
89+
private val importCsvLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
90+
if (result.resultCode == Activity.RESULT_OK) {
91+
val data: Intent? = result.data
92+
val fileUrl = data?.data
93+
94+
Timber.i("cdr onActivityResult for CSV file request. resultCode=${result.resultCode}. uri=$fileUrl")
95+
96+
if (fileUrl != null) {
97+
lifecycleScope.launch {
98+
when (val importResult = csvPasswordImporter.importCsv(fileUrl)) {
99+
is Success -> {
100+
setResult(Result.UserChoseCsvImport(importResult.passwordIdsImported.size))
101+
}
102+
103+
Error -> setResult(Result.ErrorDuringImport)
104+
}
105+
}
106+
}
107+
}
108+
}
109+
110+
override fun onAttach(context: Context) {
111+
AndroidSupportInjection.inject(this)
112+
super.onAttach(context)
113+
}
114+
115+
override fun onCreate(savedInstanceState: Bundle?) {
116+
super.onCreate(savedInstanceState)
117+
118+
if (savedInstanceState != null) {
119+
// If being created after a configuration change, dismiss the dialog as the WebView will be re-created too
120+
dismiss()
121+
}
122+
}
123+
124+
override fun onCreateView(
125+
inflater: LayoutInflater,
126+
container: ViewGroup?,
127+
savedInstanceState: Bundle?,
128+
): View {
129+
val binding = ContentChooseImportPasswordMethodBinding.inflate(inflater, container, false)
130+
configureViews(binding)
131+
return binding.root
132+
}
133+
134+
private fun configureViews(binding: ContentChooseImportPasswordMethodBinding) {
135+
(dialog as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED
136+
configureCloseButton(binding)
137+
binding.importGcmButton.setOnClickListener { onImportGcmButtonClicked() }
138+
binding.csvButton.setOnClickListener { onImportCsvButtonClicked() }
139+
binding.desktopSyncButton.setOnClickListener { onImportDesktopSyncButtonClicked() }
140+
}
141+
142+
private fun onImportGcmButtonClicked() {
143+
launchImportGcmFlow()
144+
}
145+
146+
private fun launchImportGcmFlow() {
147+
val intent = globalActivityStarter.startIntent(requireContext(), ImportGooglePassword.AutofillImportViaGooglePasswordManagerScreen)
148+
importGooglePasswordsFlowLauncher.launch(intent)
149+
}
150+
151+
private fun onImportCsvButtonClicked() {
152+
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
153+
addCategory(Intent.CATEGORY_OPENABLE)
154+
type = "*/*"
155+
}
156+
importCsvLauncher.launch(intent)
157+
}
158+
159+
private fun onImportDesktopSyncButtonClicked() {
160+
setResult(Result.UserChoseDesktopSyncImport)
161+
}
162+
163+
private fun setResult(result: Result) {
164+
val resultBundle = Bundle().apply {
165+
putParcelable(RESULT_KEY_DETAILS, result)
166+
}
167+
setFragmentResult(RESULT_KEY, resultBundle)
168+
dismiss()
169+
}
170+
171+
override fun onCancel(dialog: DialogInterface) {
172+
if (ignoreCancellationEvents) {
173+
Timber.v("onCancel: Ignoring cancellation event")
174+
return
175+
}
176+
// parentFragment?.setFragmentResult(CredentialAutofillPickerDialog.resultKey(getTabId()), result)
177+
setResult(Result.UserCancelled)
178+
}
179+
180+
private fun configureCloseButton(binding: ContentChooseImportPasswordMethodBinding) {
181+
binding.closeButton.setOnClickListener { (dialog as BottomSheetDialog).animateClosed() }
182+
}
183+
184+
private fun parseGooglePasswordImportResultDetails(result: ActivityResult): ImportGooglePassword.Result {
185+
return IntentCompat.getParcelableExtra(result.data!!, ImportGooglePassword.Result.RESULT_KEY_DETAILS, ImportGooglePassword.Result::class.java)!!
186+
}
187+
188+
companion object {
189+
190+
fun instance(): SelectImportPasswordMethodDialog {
191+
val fragment = SelectImportPasswordMethodDialog()
192+
fragment.arguments = Bundle()
193+
return fragment
194+
}
195+
196+
const val RESULT_KEY = "SelectImportPasswordMethodDialogResult"
197+
const val RESULT_KEY_DETAILS = "SelectImportPasswordMethodDialogResultDetails"
198+
199+
sealed interface Result : Parcelable {
200+
@Parcelize
201+
data class UserChoseGcmImport(val numberImported: Int) : Result
202+
203+
@Parcelize
204+
data class UserChoseCsvImport(val numberImported: Int) : Result
205+
206+
@Parcelize
207+
data object UserChoseDesktopSyncImport : Result
208+
209+
@Parcelize
210+
data object UserCancelled : Result
211+
212+
@Parcelize
213+
data object ErrorDuringImport : Result
214+
}
215+
}
216+
}

0 commit comments

Comments
 (0)