Skip to content

Commit 482ba59

Browse files
committed
Add web message listener for js call from bookmark import
1 parent 889c892 commit 482ba59

24 files changed

+717
-44
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ interface AutofillFeature {
122122
@Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
123123
fun canImportBookmarksFromGoogleTakeout(): Toggle
124124

125+
/**
126+
* Remote Flag that enables the ability to use web message listener during bookmark import flow from Google Takeout
127+
*/
128+
@Toggle.DefaultValue(DefaultFeatureValue.TRUE)
129+
fun canUseWebMessageListenerDuringBookmarkImport(): Toggle
130+
125131
/**
126132
* Remote flag that enables the ability to support partial form saves. A partial form save is common with scenarios like:
127133
* - a multi-step login form where username and password are entered on separate pages
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.duckduckgo.autofill.impl.importing.takeout.webflow
2+
3+
import androidx.core.net.toUri
4+
import com.duckduckgo.autofill.impl.importing.takeout.processor.BookmarkImportProcessor.ImportResult
5+
import com.duckduckgo.autofill.impl.importing.takeout.processor.BookmarkImportProcessor.ImportResult.Error
6+
import com.duckduckgo.autofill.impl.importing.takeout.processor.BookmarkImportProcessor.ImportResult.Success
7+
import com.duckduckgo.di.scopes.FragmentScope
8+
import com.squareup.anvil.annotations.ContributesBinding
9+
import logcat.logcat
10+
import javax.inject.Inject
11+
12+
interface BookmarkImportWebFlowStepTracker {
13+
fun getCurrentStep(): String
14+
fun startFlow()
15+
fun updateStepFromUrl(url: String?)
16+
fun updateLatestStepSpecificStage(step: String)
17+
fun updateStepToDownloadDetected()
18+
fun updateStepFromImportResult(importResult: ImportResult)
19+
}
20+
21+
@ContributesBinding(FragmentScope::class)
22+
class BookmarkImportWebFlowStepTrackerImpl @Inject constructor() : BookmarkImportWebFlowStepTracker {
23+
24+
private var latestStepInWebFlow: String = STEP_UNINITIALIZED
25+
private var hasVisitedLogin: Boolean = false
26+
private var hasVisitedTakeout: Boolean = false
27+
28+
override fun getCurrentStep(): String = latestStepInWebFlow
29+
30+
override fun startFlow() {
31+
latestStepInWebFlow = STEP_UNINITIALIZED
32+
hasVisitedLogin = false
33+
hasVisitedTakeout = false
34+
logcat { "Bookmark-import: flow started, flags reset" }
35+
}
36+
37+
override fun updateLatestStepSpecificStage(step: String) {
38+
latestStepInWebFlow = step
39+
logcat { "Bookmark-import: latest step is: $step" }
40+
}
41+
42+
override fun updateStepFromUrl(url: String?) {
43+
val host = url?.toUri()?.host ?: return
44+
45+
when {
46+
host.contains(TAKEOUT_ADDRESS, ignoreCase = true) -> updateLatestStepTakeoutReached()
47+
host.contains(ACCOUNTS_ADDRESS, ignoreCase = true) -> updateLatestStepLoginPage()
48+
else -> updateLatestStepSpecificStage(STEP_UNKNOWN_URL)
49+
}
50+
}
51+
52+
override fun updateStepToDownloadDetected() {
53+
updateLatestStepSpecificStage(STEP_DOWNLOAD_DETECTED)
54+
}
55+
56+
override fun updateStepFromImportResult(importResult: ImportResult) {
57+
val step = when (importResult) {
58+
is Success -> STEP_IMPORT_SUCCESS
59+
is Error.DownloadError -> STEP_IMPORT_ERROR_DOWNLOAD
60+
is Error.ParseError -> STEP_IMPORT_ERROR_PARSE
61+
is Error.ImportError -> STEP_IMPORT_ERROR_WHILE_IMPORTING
62+
}
63+
updateLatestStepSpecificStage(step)
64+
}
65+
66+
private fun updateLatestStepTakeoutReached() {
67+
if (latestStepInWebFlow == STEP_GOOGLE_TAKEOUT_PAGE_FIRST || latestStepInWebFlow == STEP_GOOGLE_TAKEOUT_PAGE_REPEATED) {
68+
return
69+
}
70+
71+
if (!hasVisitedTakeout) {
72+
hasVisitedTakeout = true
73+
updateLatestStepSpecificStage(STEP_GOOGLE_TAKEOUT_PAGE_FIRST)
74+
} else {
75+
updateLatestStepSpecificStage(STEP_GOOGLE_TAKEOUT_PAGE_REPEATED)
76+
}
77+
}
78+
79+
private fun updateLatestStepLoginPage() {
80+
if (latestStepInWebFlow == STEP_GOOGLE_ACCOUNTS_PAGE_FIRST || latestStepInWebFlow == STEP_GOOGLE_ACCOUNTS_REPEATED) {
81+
return
82+
}
83+
84+
if (!hasVisitedLogin) {
85+
hasVisitedLogin = true
86+
updateLatestStepSpecificStage(STEP_GOOGLE_ACCOUNTS_PAGE_FIRST)
87+
} else {
88+
updateLatestStepSpecificStage(STEP_GOOGLE_ACCOUNTS_REPEATED)
89+
}
90+
}
91+
92+
companion object {
93+
private const val STEP_GOOGLE_TAKEOUT_PAGE_FIRST = "takeout-first"
94+
private const val STEP_GOOGLE_TAKEOUT_PAGE_REPEATED = "takeout-repeat"
95+
private const val STEP_GOOGLE_ACCOUNTS_PAGE_FIRST = "login-first"
96+
private const val STEP_GOOGLE_ACCOUNTS_REPEATED = "login-repeat"
97+
private const val STEP_UNINITIALIZED = "uninitialized"
98+
private const val STEP_IMPORT_SUCCESS = "completed-successful"
99+
private const val STEP_IMPORT_ERROR_PARSE = "completed-failure-parse"
100+
private const val STEP_IMPORT_ERROR_DOWNLOAD = "completed-failure-download"
101+
private const val STEP_IMPORT_ERROR_WHILE_IMPORTING = "completed-failure-import"
102+
private const val STEP_DOWNLOAD_DETECTED = "download-detected"
103+
private const val STEP_UNKNOWN_URL = "unknown-url"
104+
private const val TAKEOUT_ADDRESS = "takeout.google.com"
105+
private const val ACCOUNTS_ADDRESS = "accounts.google.com"
106+
}
107+
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarkResult.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ sealed interface UserCannotImportReason : Parcelable {
4848
@Parcelize
4949
data object DownloadError : UserCannotImportReason
5050

51+
@Parcelize
52+
data class WebAutomationError(val step: String) : UserCannotImportReason
53+
5154
@Parcelize
5255
data object Unknown : UserCannotImportReason
5356

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/takeout/webflow/ImportGoogleBookmarksWebFlowFragment.kt

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import androidx.lifecycle.lifecycleScope
3333
import androidx.lifecycle.repeatOnLifecycle
3434
import androidx.webkit.WebViewCompat
3535
import com.duckduckgo.anvil.annotations.InjectWith
36+
import com.duckduckgo.autofill.api.AutofillFeature
3637
import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin
3738
import com.duckduckgo.autofill.api.BrowserAutofill
3839
import com.duckduckgo.autofill.api.CredentialAutofillDialogFactory
@@ -66,6 +67,7 @@ import com.duckduckgo.autofill.impl.importing.takeout.webflow.ImportGoogleBookma
6667
import com.duckduckgo.autofill.impl.importing.takeout.webflow.UserCannotImportReason.DownloadError
6768
import com.duckduckgo.autofill.impl.importing.takeout.webflow.UserCannotImportReason.ErrorParsingBookmarks
6869
import com.duckduckgo.autofill.impl.importing.takeout.webflow.UserCannotImportReason.Unknown
70+
import com.duckduckgo.autofill.impl.importing.takeout.webflow.UserCannotImportReason.WebAutomationError
6971
import com.duckduckgo.autofill.impl.importing.takeout.webflow.UserCannotImportReason.WebViewError
7072
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType
7173
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.PASSWORD
@@ -123,8 +125,12 @@ class ImportGoogleBookmarksWebFlowFragment :
123125
@Inject
124126
lateinit var browserAutofillConfigurator: InternalBrowserAutofillConfigurator
125127

128+
@Inject
129+
lateinit var autofillFeature: AutofillFeature
130+
126131
private var binding: FragmentImportGoogleBookmarksWebflowBinding? = null
127132
private var cancellationDialog: DaxAlertDialog? = null
133+
private var webFlowIsEnding = false
128134

129135
private val viewModel by lazy {
130136
ViewModelProvider(requireActivity(), viewModelFactory)[ImportGoogleBookmarksWebFlowViewModel::class.java]
@@ -195,16 +201,12 @@ class ImportGoogleBookmarksWebFlowFragment :
195201
// Inject null to indicate no credentials available
196202
browserAutofill.injectCredentials(null)
197203
}
198-
is PromptUserToSelectFromStoredCredentials ->
199-
showCredentialChooserDialog(
200-
command.originalUrl,
201-
command.credentials,
202-
command.triggerType,
203-
)
204-
is ExitFlowWithSuccess -> {
205-
logcat { "Bookmark-import: ExitFlowWithSuccess received with count: ${command.importedCount}" }
206-
exitFlowAsSuccess(command.importedCount)
207-
}
204+
is PromptUserToSelectFromStoredCredentials -> showCredentialChooserDialog(
205+
command.originalUrl,
206+
command.credentials,
207+
command.triggerType,
208+
)
209+
is ExitFlowWithSuccess -> exitFlowAsSuccess(command.importedCount)
208210
is ExitFlowAsFailure -> exitFlowAsError(command.reason)
209211
is PromptUserToConfirmFlowCancellation -> askUserToConfirmCancellation()
210212
}
@@ -223,12 +225,11 @@ class ImportGoogleBookmarksWebFlowFragment :
223225
return@withContext
224226
}
225227

226-
val credentials =
227-
LoginCredentials(
228-
domain = url,
229-
username = username,
230-
password = password,
231-
)
228+
val credentials = LoginCredentials(
229+
domain = url,
230+
username = username,
231+
password = password,
232+
)
232233

233234
logcat { "Injecting re-authentication credentials" }
234235
browserAutofill.injectCredentials(credentials)
@@ -319,6 +320,24 @@ class ImportGoogleBookmarksWebFlowFragment :
319320
} else {
320321
logcat(WARN) { "Bookmark-import: Not able to inject bookmark import JavaScript" }
321322
}
323+
324+
val canAddMessageListener = withContext(dispatchers.io()) {
325+
autofillFeature.canUseWebMessageListenerDuringBookmarkImport().isEnabled()
326+
}
327+
328+
if (canAddMessageListener) {
329+
WebViewCompat.addWebMessageListener(webView, "ddgBookmarkImport", setOf("*")) { _, message, sourceOrigin, _, _ ->
330+
if (webFlowIsEnding) {
331+
logcat(WARN) { "Bookmark-import: web flow is ending, ignoring message" }
332+
return@addWebMessageListener
333+
}
334+
335+
val data = message.data ?: return@addWebMessageListener
336+
viewModel.onWebMessageReceived(data)
337+
}
338+
} else {
339+
logcat(WARN) { "Bookmark-import: Not able to add WebMessage listener for bookmark import" }
340+
}
322341
}
323342

324343
private fun initialiseToolbar() {
@@ -357,6 +376,10 @@ class ImportGoogleBookmarksWebFlowFragment :
357376
private fun getToolbar() = (activity as ImportGoogleBookmarksWebFlowActivity).binding.includeToolbar.toolbar
358377

359378
override fun onPageStarted(url: String?) {
379+
if (webFlowIsEnding) {
380+
return
381+
}
382+
360383
viewModel.onPageStarted(url)
361384
lifecycleScope.launch(dispatchers.main()) {
362385
binding?.let {
@@ -384,6 +407,8 @@ class ImportGoogleBookmarksWebFlowFragment :
384407

385408
private fun exitFlowAsSuccess(bookmarkCount: Int) {
386409
logcat { "Bookmark-import: Reporting import success with bookmarkCount: $bookmarkCount" }
410+
onWebFlowEnding()
411+
387412
lifecycleScope.launch {
388413
repeatOnLifecycle(Lifecycle.State.STARTED) {
389414
dismissCancellationDialog()
@@ -397,6 +422,7 @@ class ImportGoogleBookmarksWebFlowFragment :
397422

398423
private fun exitFlowAsCancellation(stage: String) {
399424
logcat { "Bookmark-import: Flow cancelled at stage: $stage" }
425+
onWebFlowEnding()
400426

401427
lifecycleScope.launch {
402428
repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -413,6 +439,7 @@ class ImportGoogleBookmarksWebFlowFragment :
413439

414440
private fun exitFlowAsError(reason: UserCannotImportReason) {
415441
logcat { "Bookmark-import: Flow error at stage: ${reason.mapToStage()}" }
442+
onWebFlowEnding()
416443

417444
lifecycleScope.launch {
418445
repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -426,6 +453,17 @@ class ImportGoogleBookmarksWebFlowFragment :
426453
}
427454
}
428455

456+
/**
457+
* Does a best-effort to attempt to stop the web flow from any further processing.
458+
*/
459+
private fun onWebFlowEnding() {
460+
webFlowIsEnding = true
461+
binding?.webView?.run {
462+
stopLoading()
463+
loadUrl("about:blank")
464+
}
465+
}
466+
429467
private suspend fun showCredentialChooserDialog(
430468
originalUrl: String,
431469
credentials: List<LoginCredentials>,
@@ -438,13 +476,12 @@ class ImportGoogleBookmarksWebFlowFragment :
438476
return@withContext
439477
}
440478

441-
val dialog =
442-
credentialAutofillDialogFactory.autofillSelectCredentialsDialog(
443-
url,
444-
credentials,
445-
triggerType,
446-
CUSTOM_FLOW_TAB_ID,
447-
)
479+
val dialog = credentialAutofillDialogFactory.autofillSelectCredentialsDialog(
480+
url,
481+
credentials,
482+
triggerType,
483+
CUSTOM_FLOW_TAB_ID,
484+
)
448485
dialog.show(childFragmentManager, SELECT_CREDENTIALS_FRAGMENT_TAG)
449486
}
450487
}
@@ -548,8 +585,9 @@ class ImportGoogleBookmarksWebFlowFragment :
548585

549586
private fun UserCannotImportReason.mapToStage(): String =
550587
when (this) {
551-
DownloadError -> "zip-download-error"
552-
ErrorParsingBookmarks -> "zip-parse-error"
553-
Unknown -> "import-error-unknown"
554-
WebViewError -> "webview-error"
588+
is DownloadError -> "zip-download-error"
589+
is ErrorParsingBookmarks -> "zip-parse-error"
590+
is Unknown -> "import-error-unknown"
591+
is WebViewError -> "webview-error"
592+
is WebAutomationError -> "web-automation-step-failure-${this.step}"
555593
}

0 commit comments

Comments
 (0)