Skip to content

Commit 8e0a9fc

Browse files
authored
[Google Takeout Automation] Bookmark import on Android (#1935)
* separate password and bookmark flow * checkpoint automation changes * wip: complete flow * [PoC] try DBP click for bookmark import (#1965) * Use DBP for running execution * fix: use adsjs entrypoint for messaging * fix: sleep a second before downloading * fix: add custom context for autofill import * refactor: extend actionexecutor * fix: simplify config reading * feat: patch .notify to send a simple payload * fix: use actionCompleted for errors * fix: check settings before execution * chore: don't need separate context for now * fix: retryconfigfor must be implemented per class * test: config path * test: config path
1 parent 9774c5a commit 8e0a9fc

File tree

17 files changed

+236
-94
lines changed

17 files changed

+236
-94
lines changed

CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ injected/src/element-hiding.js @duckduckgo/content-scope-scripts-owners @jonatha
1010
injected/src/features/click-to-load.js @duckduckgo/content-scope-scripts-owners @kzar @ladamski @franfaccin @jonathanKingston @shakyShane
1111
injected/src/features/click-to-load/ @duckduckgo/content-scope-scripts-owners @kzar @ladamski @franfaccin @jonathanKingston @shakyShane
1212
injected/src/locales/click-to-load/ @duckduckgo/content-scope-scripts-owners @kzar @ladamski @franfaccin @jonathanKingston @shakyShane
13-
injected/src/features/autofill-password-import.js @duckduckgo/content-scope-scripts-owners @dbajpeyi
13+
injected/src/features/autofill-import.js @duckduckgo/content-scope-scripts-owners @dbajpeyi
1414

1515
# Broker protection
1616
injected/src/features/broker-protection.js @duckduckgo/content-scope-scripts-owners @duckduckgo/injected-broker-protection

injected/integration-test/autofill-password-import.spec.js renamed to injected/integration-test/autofill-import.spec.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { test, expect } from '@playwright/test';
2-
import { OVERLAY_ID } from '../src/features/autofill-password-import';
2+
import { OVERLAY_ID } from '../src/features/autofill-import';
33
import { ResultsCollector } from './page-objects/results-collector.js';
44

5-
const HTML = '/autofill-password-import/index.html';
6-
const CONFIG = './integration-test/test-pages/autofill-password-import/config/config.json';
5+
const HTML = '/autofill-import/index.html';
6+
const CONFIG = './integration-test/test-pages/autofill-import/config/config.json';
77

88
test('Password import feature', async ({ page }, testInfo) => {
99
const collector = ResultsCollector.create(page, testInfo.project.use);
1010
await collector.load(HTML, CONFIG);
1111

12-
const passwordImportFeature = new AutofillPasswordImportSpec(page);
12+
const passwordImportFeature = new AutofillImportSpec(page);
1313
await passwordImportFeature.clickOnElement('Home page');
1414
await passwordImportFeature.waitForAnimation();
1515

@@ -25,7 +25,7 @@ test('Password import feature', async ({ page }, testInfo) => {
2525
await expect(overlay).not.toBeVisible();
2626
});
2727

28-
class AutofillPasswordImportSpec {
28+
class AutofillImportSpec {
2929
/**
3030
* @param {import("@playwright/test").Page} page
3131
*/

injected/integration-test/page-objects/results-collector.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export class ResultsCollector {
163163
android: async () => {
164164
// noop
165165
},
166-
'android-autofill-password-import': async () => {
166+
'android-autofill-import': async () => {
167167
// noop
168168
},
169169
});
@@ -173,7 +173,7 @@ export class ResultsCollector {
173173
'apple-isolated': () => mockWebkitMessaging,
174174
windows: () => mockWindowsMessaging,
175175
android: () => mockAndroidMessaging,
176-
'android-autofill-password-import': () => mockAndroidMessaging,
176+
'android-autofill-import': () => mockAndroidMessaging,
177177
});
178178

179179
await this.page.addInitScript(messagingMock, {
@@ -187,7 +187,7 @@ export class ResultsCollector {
187187
'apple-isolated': () => wrapWebkitScripts,
188188
apple: () => wrapWebkitScripts,
189189
android: () => wrapWebkitScripts,
190-
'android-autofill-password-import': () => wrapWebkitScripts,
190+
'android-autofill-import': () => wrapWebkitScripts,
191191
windows: () => wrapWindowsScripts,
192192
});
193193

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"readme": "This config is used to test the autofill password import feature.",
33
"version": 1,
44
"features": {
5-
"autofillPasswordImport": {
5+
"autofillImport": {
66
"state": "enabled",
77
"exceptions": [],
88
"settings": {
File renamed without changes.

injected/integration-test/type-helpers.mjs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export class Build {
6161
android: () => '../build/android/contentScope.js',
6262
apple: () => '../build/apple/contentScope.js',
6363
'apple-isolated': () => '../build/apple/contentScopeIsolated.js',
64-
'android-autofill-password-import': () => '../build/android/autofillPasswordImport.js',
64+
'android-autofill-import': () => '../build/android/autofillImport.js',
6565
'android-broker-protection': () => '../build/android/brokerProtection.js',
6666
});
6767
return readFileSync(path, 'utf8');
@@ -73,16 +73,7 @@ export class Build {
7373
*/
7474
static supported(name) {
7575
/** @type {ImportMeta['injectName'][]} */
76-
const items = [
77-
'apple',
78-
'apple-isolated',
79-
'windows',
80-
'integration',
81-
'android',
82-
'android-autofill-password-import',
83-
'chrome-mv3',
84-
'firefox',
85-
];
76+
const items = ['apple', 'apple-isolated', 'windows', 'integration', 'android', 'android-autofill-import', 'chrome-mv3', 'firefox'];
8677
if (items.includes(name)) {
8778
return name;
8879
}

injected/playwright.config.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ export default defineConfig({
5555
use: { injectName: 'android', platform: 'android', ...devices['Galaxy S5'] },
5656
},
5757
{
58-
name: 'android-autofill-password-import',
59-
testMatch: ['integration-test/autofill-password-import.spec.js'],
60-
use: { injectName: 'android-autofill-password-import', platform: 'android', ...devices['Galaxy S5'] },
58+
name: 'android-autofill-import',
59+
testMatch: ['integration-test/autofill-import.spec.js'],
60+
use: { injectName: 'android-autofill-import', platform: 'android', ...devices['Galaxy S5'] },
6161
},
6262
{
6363
name: 'chrome-mv3',

injected/scripts/entry-points.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ const builds = {
3131
input: 'entry-points/android.js',
3232
output: ['../build/android/brokerProtection.js'],
3333
},
34-
'android-autofill-password-import': {
35-
input: 'entry-points/android.js',
36-
output: ['../build/android/autofillPasswordImport.js'],
34+
'android-autofill-import': {
35+
input: 'entry-points/android-adsjs.js',
36+
output: ['../build/android/autofillImport.js'],
3737
},
3838
'android-adsjs': {
3939
input: 'entry-points/android-adsjs.js',

injected/src/features.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const otherFeatures = /** @type {const} */ ([
2828
'brokerProtection',
2929
'performanceMetrics',
3030
'breakageReporting',
31-
'autofillPasswordImport',
31+
'autofillImport',
3232
'favicon',
3333
'webTelemetry',
3434
'pageContext',
@@ -49,7 +49,7 @@ export const platformSupport = {
4949
],
5050
android: [...baseFeatures, 'webCompat', 'breakageReporting', 'duckPlayer', 'messageBridge'],
5151
'android-broker-protection': ['brokerProtection'],
52-
'android-autofill-password-import': ['autofillPasswordImport'],
52+
'android-autofill-import': ['autofillImport'],
5353
'android-adsjs': [
5454
'apiManipulation',
5555
'webCompat',

injected/src/features/autofill-password-import.js renamed to injected/src/features/autofill-import.js

Lines changed: 130 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import ContentFeature from '../content-feature';
2-
import { isBeingFramed, withExponentialBackoff } from '../utils';
1+
import { isBeingFramed, withRetry } from '../utils';
2+
import { ActionExecutorBase } from './broker-protection';
3+
import { ErrorResponse } from './broker-protection/types';
34

45
export const ANIMATION_DURATION_MS = 1000;
56
export const ANIMATION_ITERATIONS = Infinity;
67
export const BACKGROUND_COLOR_START = 'rgba(85, 127, 243, 0.10)';
78
export const BACKGROUND_COLOR_END = 'rgba(85, 127, 243, 0.25)';
89
export const OVERLAY_ID = 'ddg-password-import-overlay';
910
export const DELAY_BEFORE_ANIMATION = 300;
11+
const TAKEOUT_DOWNLOAD_URL_BASE = '/takeout/download';
1012

1113
/**
1214
* @typedef ButtonAnimationStyle
@@ -33,7 +35,7 @@ export const DELAY_BEFORE_ANIMATION = 300;
3335
* 2. Find the element to animate based on the path - using structural selectors first and then fallback to label texts),
3436
* 3. Animate the element, or tap it if it should be autotapped.
3537
*/
36-
export default class AutofillPasswordImport extends ContentFeature {
38+
export default class AutofillImport extends ActionExecutorBase {
3739
#exportButtonSettings;
3840

3941
#settingsButtonSettings;
@@ -53,6 +55,12 @@ export default class AutofillPasswordImport extends ContentFeature {
5355

5456
#domLoaded;
5557

58+
#exportId;
59+
60+
#processingBookmark;
61+
62+
#isBookmarkModalVisible = false;
63+
5664
/** @type {WeakSet<Element>} */
5765
#tappedElements = new WeakSet();
5866

@@ -135,10 +143,10 @@ export default class AutofillPasswordImport extends ContentFeature {
135143
/**
136144
* @returns {Promise<Element|HTMLElement|null>}
137145
*/
138-
async runWithRetry(fn) {
146+
async runWithRetry(fn, maxAttempts = 4, delay = 500, strategy = 'exponential') {
139147
try {
140-
return await withExponentialBackoff(fn);
141-
} catch {
148+
return await withRetry(fn, maxAttempts, delay, strategy);
149+
} catch (error) {
142150
return null;
143151
}
144152
}
@@ -457,20 +465,53 @@ export default class AutofillPasswordImport extends ContentFeature {
457465
].includes(path);
458466
}
459467

460-
async handlePath(path) {
468+
async handlePasswordManagerPath(pathname) {
461469
this.removeOverlayIfNeeded();
462-
if (this.isSupportedPath(path)) {
470+
if (this.isSupportedPath(pathname)) {
463471
try {
464-
this.setCurrentElementConfig(await this.getElementAndStyleFromPath(path));
472+
this.setCurrentElementConfig(await this.getElementAndStyleFromPath(pathname));
465473
if (this.currentElementConfig?.element && !this.#tappedElements.has(this.currentElementConfig?.element)) {
466474
await this.animateOrTapElement();
467475
if (this.currentElementConfig?.shouldTap && this.currentElementConfig?.tapOnce) {
468476
this.#tappedElements.add(this.currentElementConfig.element);
469477
}
470478
}
471479
} catch {
472-
console.error('password-import: failed for path:', path);
480+
console.error('password-import: failed for path:', pathname);
481+
}
482+
}
483+
}
484+
485+
/**
486+
* @returns {Array<Record<string, any>>}
487+
*/
488+
get bookmarkImportActionSettings() {
489+
return this.getFeatureSetting('actions') || [];
490+
}
491+
492+
/**
493+
* @returns {Record<string, string>}
494+
*/
495+
get bookmarkImportSelectorSettings() {
496+
return this.getFeatureSetting('selectors');
497+
}
498+
499+
/**
500+
* @param {Location} location
501+
*
502+
*/
503+
async handleLocation(location) {
504+
const { pathname } = location;
505+
if (this.bookmarkImportActionSettings.length > 0) {
506+
if (this.#processingBookmark) {
507+
return;
473508
}
509+
this.#processingBookmark = true;
510+
await this.handleBookmarkImportPath(pathname);
511+
} else if (this.getFeatureSetting('settingsButton')) {
512+
await this.handlePasswordManagerPath(pathname);
513+
} else {
514+
// Unknown feature, we bail out
474515
}
475516
}
476517

@@ -547,24 +588,98 @@ export default class AutofillPasswordImport extends ContentFeature {
547588
return `${this.#settingsButtonSettings?.selectors?.join(',')}, ${this.settingsLabelTextSelector}`;
548589
}
549590

550-
setButtonSettings() {
591+
/** Bookmark import code */
592+
async downloadData() {
593+
// sleep for a second, sometimes download link is not yet available
594+
await new Promise((resolve) => setTimeout(resolve, 1000));
595+
596+
const userId = document.querySelector(this.bookmarkImportSelectorSettings.userIdLink)?.getAttribute('href')?.split('&user=')[1];
597+
await this.runWithRetry(() => document.querySelector(`a[href="./manage/archive/${this.#exportId}"]`), 15, 2000, 'linear');
598+
if (userId != null && this.#exportId != null) {
599+
const downloadURL = `${TAKEOUT_DOWNLOAD_URL_BASE}?j=${this.#exportId}&i=0&user=${userId}`;
600+
window.location.href = downloadURL;
601+
} else {
602+
// If there's no user id or export id, we post an action failed message
603+
this.postBookmarkImportMessage('actionCompleted', {
604+
result: new ErrorResponse({
605+
actionID: 'download-data',
606+
message: 'No user id or export id found',
607+
}),
608+
});
609+
}
610+
}
611+
612+
/**
613+
* Here we ignore the action and return a default retry config
614+
* as for now the retry doesn't need to be per action.
615+
*/
616+
retryConfigFor(_) {
617+
return {
618+
interval: { ms: 1000 },
619+
maxAttempts: 30,
620+
};
621+
}
622+
623+
postBookmarkImportMessage(name, data) {
624+
globalThis.ddgBookmarkImport?.postMessage(
625+
JSON.stringify({
626+
name,
627+
data,
628+
}),
629+
);
630+
}
631+
632+
patchMessagingAndProcessAction(action) {
633+
// Ideally we should be usuing standard messaging in Android, but we are not ready yet
634+
// So just patching the notify method to post a message to the Android side
635+
this.messaging.notify = this.postBookmarkImportMessage.bind(this);
636+
return this.processActionAndNotify(action, {});
637+
}
638+
639+
async handleBookmarkImportPath(pathname) {
640+
if (pathname === '/' && !this.#isBookmarkModalVisible) {
641+
for (const action of this.bookmarkImportActionSettings) {
642+
// Before clicking on the manage button, we need to store the export id
643+
if (action.id === 'manage-button-click') {
644+
await this.storeExportId();
645+
}
646+
647+
await this.patchMessagingAndProcessAction(action);
648+
}
649+
await this.downloadData();
650+
}
651+
}
652+
653+
setPasswordImportSettings() {
551654
this.#exportButtonSettings = this.getFeatureSetting('exportButton');
552655
this.#signInButtonSettings = this.getFeatureSetting('signInButton');
553656
this.#settingsButtonSettings = this.getFeatureSetting('settingsButton');
554657
this.#exportConfirmButtonSettings = this.getFeatureSetting('exportConfirmButton');
555658
}
556659

660+
findExportId() {
661+
const panels = document.querySelectorAll(this.bookmarkImportSelectorSettings.tabPanel);
662+
const exportPanel = panels[panels.length - 1];
663+
return exportPanel.querySelector('div[data-archive-id]')?.getAttribute('data-archive-id');
664+
}
665+
666+
async storeExportId() {
667+
this.#exportId = await this.runWithRetry(() => this.findExportId(), 30, 1000, 'linear');
668+
}
669+
557670
urlChanged() {
558-
this.handlePath(window.location.pathname);
671+
this.handleLocation(window.location);
559672
}
560673

561674
init() {
562675
if (isBeingFramed()) {
563676
return;
564677
}
565-
this.setButtonSettings();
566678

567-
const handlePath = this.handlePath.bind(this);
679+
if (this.getFeatureSetting('settingsButton')) {
680+
this.setPasswordImportSettings();
681+
}
682+
const handleLocation = this.handleLocation.bind(this);
568683

569684
this.#domLoaded = new Promise((resolve) => {
570685
if (document.readyState !== 'loading') {
@@ -578,8 +693,7 @@ export default class AutofillPasswordImport extends ContentFeature {
578693
async () => {
579694
// @ts-expect-error - caller doesn't expect a value here
580695
resolve();
581-
const path = window.location.pathname;
582-
await handlePath(path);
696+
await handleLocation(window.location);
583697
},
584698
{ once: true },
585699
);

0 commit comments

Comments
 (0)