Skip to content

Commit 402a647

Browse files
committed
Add webflow for importing bookmarks from Google
1 parent 3333dc7 commit 402a647

17 files changed

+1151
-1
lines changed

autofill/autofill-impl/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dependencies {
4747
testImplementation project(':feature-toggles-test')
4848
implementation project(path: ':settings-api') // temporary until we release new settings
4949
implementation project(':library-loader-api')
50+
implementation project(':saved-sites-api')
5051

5152
anvil project(path: ':anvil-compiler')
5253
implementation project(path: ':anvil-annotations')

autofill/autofill-impl/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
android:name=".importing.gpm.webflow.ImportGooglePasswordsWebFlowActivity"
2222
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|navigation|keyboard"
2323
android:exported="false" />
24+
<activity
25+
android:name=".importing.gpm.webflow.ImportGoogleBookmarksWebFlowActivity"
26+
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|navigation|keyboard"
27+
android:exported="false" />
2428
<activity
2529
android:name=".ui.credential.management.AutofillManagementActivity"
2630
android:configChanges="orientation|screenSize"
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright (c) 2024 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.importing.gpm.webflow
18+
19+
import android.webkit.CookieManager
20+
import com.duckduckgo.common.utils.DispatcherProvider
21+
import dagger.Lazy
22+
import java.io.IOException
23+
import javax.inject.Inject
24+
import javax.inject.Named
25+
import kotlinx.coroutines.withContext
26+
import logcat.logcat
27+
import okhttp3.OkHttpClient
28+
import okhttp3.Request
29+
30+
class BookmarkZipDownloader @Inject constructor(
31+
@Named("nonCaching") private val okHttpClient: Lazy<OkHttpClient>,
32+
private val dispatchers: DispatcherProvider,
33+
) {
34+
35+
suspend fun downloadZip(
36+
url: String,
37+
userAgent: String? = null,
38+
): ByteArray = downloadZipWithReferrer(url, userAgent, null)
39+
40+
suspend fun downloadZipWithReferrer(
41+
url: String,
42+
userAgent: String? = null,
43+
referrerUrl: String? = null,
44+
): ByteArray = withContext(dispatchers.io()) {
45+
logcat { "Starting bookmark zip download from: $url" }
46+
47+
val requestBuilder = Request.Builder().url(url)
48+
userAgent?.let { requestBuilder.addHeader("User-Agent", it) }
49+
50+
// Extract cookies from WebView's CookieManager for this URL
51+
val cookieManager = CookieManager.getInstance()
52+
val cookies = cookieManager.getCookie(url)
53+
if (cookies != null) {
54+
requestBuilder.addHeader("Cookie", cookies)
55+
logcat { "Added cookies to request: ${cookies.substring(0, minOf(100, cookies.length))}..." }
56+
} else {
57+
logcat { "No cookies found for URL: $url" }
58+
}
59+
60+
// Add referrer if available for proper authentication context
61+
referrerUrl?.let {
62+
requestBuilder.addHeader("Referer", it)
63+
logcat { "Added referrer: $it" }
64+
}
65+
66+
// Add common headers that might be expected for authenticated requests
67+
requestBuilder.addHeader("Accept", "application/zip,application/octet-stream,*/*")
68+
requestBuilder.addHeader("Accept-Language", "en-US,en;q=0.9")
69+
requestBuilder.addHeader("Accept-Encoding", "gzip, deflate, br")
70+
requestBuilder.addHeader("Sec-Fetch-Dest", "document")
71+
requestBuilder.addHeader("Sec-Fetch-Mode", "navigate")
72+
requestBuilder.addHeader("Sec-Fetch-Site", "same-origin")
73+
requestBuilder.addHeader("Upgrade-Insecure-Requests", "1")
74+
75+
val request = requestBuilder.build()
76+
77+
okHttpClient.get().newCall(request).execute().use { response ->
78+
if (response.isSuccessful) {
79+
val zipData = response.body?.bytes()
80+
if (zipData != null) {
81+
logcat { "Bookmark zip downloaded successfully: ${zipData.size} bytes" }
82+
zipData
83+
} else {
84+
logcat { "Bookmark zip download failed: empty response body" }
85+
throw IOException("Empty response body")
86+
}
87+
} else {
88+
logcat { "Bookmark zip download failed: HTTP ${response.code}" }
89+
logcat { "Response headers: ${response.headers}" }
90+
logcat { "Response body preview: ${response.body?.string()?.take(500)}" }
91+
throw IOException("HTTP ${response.code}: ${response.message}")
92+
}
93+
}
94+
}
95+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2024 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.importing.gpm.webflow
18+
19+
import android.os.Parcelable
20+
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGoogleBookmarksWebFlowViewModel.UserCannotImportReason
21+
import kotlinx.parcelize.Parcelize
22+
23+
sealed interface ImportGoogleBookmarkResult : Parcelable {
24+
25+
@Parcelize
26+
data object Success : ImportGoogleBookmarkResult
27+
28+
@Parcelize
29+
data class UserCancelled(val stage: String) : ImportGoogleBookmarkResult
30+
31+
@Parcelize
32+
data class Error(val reason: UserCannotImportReason) : ImportGoogleBookmarkResult
33+
34+
companion object {
35+
const val RESULT_KEY = "importBookmarkResult"
36+
const val RESULT_KEY_DETAILS = "importBookmarkResultDetails"
37+
}
38+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2024 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.importing.gpm.webflow
18+
19+
import javax.inject.Inject
20+
21+
class ImportGoogleBookmarkUrlToStageMapper @Inject constructor() {
22+
23+
fun getStage(url: String): String {
24+
return when {
25+
url.contains("takeout.google.com") -> {
26+
when {
27+
url.contains("archive") -> "download"
28+
url.contains("transfer") -> "transfer"
29+
url.contains("settings") -> "settings"
30+
else -> "takeout"
31+
}
32+
}
33+
url.contains("accounts.google.com") -> "authentication"
34+
else -> "unknown"
35+
}
36+
}
37+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) 2024 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.importing.gpm.webflow
18+
19+
import android.content.Intent
20+
import android.os.Bundle
21+
import androidx.fragment.app.commit
22+
import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
23+
import com.duckduckgo.anvil.annotations.InjectWith
24+
import com.duckduckgo.autofill.impl.R
25+
import com.duckduckgo.autofill.impl.databinding.ActivityImportGoogleBookmarksWebflowBinding
26+
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGoogleBookmark.AutofillImportViaGoogleTakeoutScreen
27+
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGoogleBookmarkResult.Companion.RESULT_KEY
28+
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGoogleBookmarkResult.Companion.RESULT_KEY_DETAILS
29+
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGoogleBookmarkResult.UserCancelled
30+
import com.duckduckgo.common.ui.DuckDuckGoActivity
31+
import com.duckduckgo.common.ui.viewbinding.viewBinding
32+
import com.duckduckgo.di.scopes.ActivityScope
33+
import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams
34+
35+
@InjectWith(ActivityScope::class)
36+
@ContributeToActivityStarter(AutofillImportViaGoogleTakeoutScreen::class)
37+
class ImportGoogleBookmarksWebFlowActivity : DuckDuckGoActivity() {
38+
39+
val binding: ActivityImportGoogleBookmarksWebflowBinding by viewBinding()
40+
41+
override fun onCreate(savedInstanceState: Bundle?) {
42+
super.onCreate(savedInstanceState)
43+
setContentView(binding.root)
44+
configureResultListeners()
45+
launchImportFragment()
46+
}
47+
48+
private fun launchImportFragment() {
49+
supportFragmentManager.commit {
50+
replace(R.id.fragment_container, ImportGoogleBookmarksWebFlowFragment())
51+
}
52+
}
53+
54+
private fun configureResultListeners() {
55+
supportFragmentManager.setFragmentResultListener(RESULT_KEY, this) { _, result ->
56+
exitWithResult(result)
57+
}
58+
}
59+
60+
private fun exitWithResult(resultBundle: Bundle) {
61+
setResult(RESULT_OK, Intent().putExtras(resultBundle))
62+
finish()
63+
}
64+
65+
fun exitUserCancelled(stage: String) {
66+
val result = Bundle().apply {
67+
putParcelable(RESULT_KEY_DETAILS, UserCancelled(stage))
68+
}
69+
exitWithResult(result)
70+
}
71+
}
72+
73+
object ImportGoogleBookmark {
74+
data object AutofillImportViaGoogleTakeoutScreen : ActivityParams {
75+
private fun readResolve(): Any = AutofillImportViaGoogleTakeoutScreen
76+
}
77+
}

0 commit comments

Comments
 (0)