import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_shared_style.css.js';
import 'chrome://resources/ash/common/cr_elements/cros_color_overrides.css.js';
import 'chrome://resources/ash/common/cr_elements/icons.html.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
import '/strings.m.js';
import {isRTL} from '//resources/js/util.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {AuthResult} from '../mojom/graduation_ui.mojom-webui.js';
import {ScreenSwitchedEvent, ScreenSwitchEvents} from './graduation_app.js';
import {getTemplate} from './graduation_takeout_ui.html.js';
import {getGraduationUiHandler} from './graduation_ui_handler.js';
declare global {
interface HTMLElementEventMap {
'newwindow': chrome.webviewTag.NewWindowEvent;
}
}
enum AuthStatus {
IN_PROGRESS = 0,
SUCCESS = 1,
ERROR = 2,
}
* The base URL of the banner shown in Takeout indicating that the user has
* completed the final step of the flow.
* May be suffixed by a year, for example: "-2024.png".
*/
const TAKEOUT_COMPLETED_BANNER_BASE_URL: string =
'https://www.gstatic.com/ac/takeout/migration/migration-banner';
* There are some cases wherein the Takeout tool fires a loadabort event
* that is benign and does not indicate a fatal error (i.e. when double-clicking
* the `Start Transfer` button).
*
* Therefore, on loadabort, the webview is reloaded until the limit on
* consecutive failed reload attempts is reached. If that limit is reached, the
* terminal error screen is triggered.
*
* Reloads are attempted with an exponential backoff that starts at 500ms and
* plateaus at 2000ms to limit perceived latency. The first reload occurs after
* the first backoff.
* Backoff pattern: 500ms, 1000ms, 2000ms, 2000ms, ...
*/
export class WebviewReloadHelper {
static readonly MAX_RELOAD_ATTEMPTS: number = 3;
private static readonly INITIAL_RELOAD_DELAY_IN_MS: number = 500;
private static readonly MAXIMUM_RELOAD_DELAY_IN_MS: number = 2000;
private static readonly BACKOFF_FACTOR: number = 2;
private reloadCount: number = 0;
private reloadDelay: number = WebviewReloadHelper.INITIAL_RELOAD_DELAY_IN_MS;
private reloadTimer: number = 0;
reset(): void {
this.reloadCount = 0;
this.reloadDelay = WebviewReloadHelper.INITIAL_RELOAD_DELAY_IN_MS;
window.clearTimeout(this.reloadTimer);
}
isReloadCountLimitReached(): boolean {
return this.reloadCount === WebviewReloadHelper.MAX_RELOAD_ATTEMPTS;
}
private updateReloadDelay(): void {
const multipliedReloadDelay =
this.reloadDelay * WebviewReloadHelper.BACKOFF_FACTOR;
this.reloadDelay = Math.min(
multipliedReloadDelay, WebviewReloadHelper.MAXIMUM_RELOAD_DELAY_IN_MS);
}
scheduleReload(webview: chrome.webviewTag.WebView): void {
if (this.isReloadCountLimitReached()) {
return;
}
this.reloadCount++;
window.clearTimeout(this.reloadTimer);
this.reloadTimer =
window.setTimeout(webview.reload.bind(this), this.reloadDelay);
this.updateReloadDelay();
}
}
export class GraduationTakeoutUi extends PolymerElement {
static get is() {
return 'graduation-takeout-ui' as const;
}
static get template() {
return getTemplate();
}
static get properties() {
return {
showLoadingScreen: {
type: Boolean,
computed: 'computeShowLoadingScreen(authStatus, isWebviewLoading)',
},
* Whether the webview content has indicated that the user has completed
* the Takeout flow.
*/
takeoutFlowCompleted: {
type: Boolean,
value: false,
},
};
}
private showLoadingScreen: boolean;
authStatus: AuthStatus = AuthStatus.IN_PROGRESS;
isWebviewLoading: boolean = false;
takeoutFlowCompleted: boolean;
private webview: chrome.webviewTag.WebView;
private webviewReloadHelper: WebviewReloadHelper;
private startTransferUrl: string;
override ready() {
super.ready();
this.webview =
this.shadowRoot!.querySelector<chrome.webviewTag.WebView>('webview')!;
if (loadTimeData.getBoolean('isEmbeddedEndpointEnabled')) {
const userAgent = this.webview.getUserAgent();
const webviewUserAgent = loadTimeData.getString('userAgentString');
this.webview.setUserAgentOverride(`${userAgent} ${webviewUserAgent}`);
}
this.configureWebviewListeners();
this.addEventListener(ScreenSwitchedEvent, () => {
this.shadowRoot!.querySelector<HTMLElement>('#backButton')!.focus();
});
this.webviewReloadHelper = new WebviewReloadHelper();
this.startTransferUrl =
loadTimeData.getString('startTransferUrl').toString();
}
onAuthComplete(result: AuthResult): void {
switch (result) {
case (AuthResult.kSuccess):
this.setIsWebviewLoading(true);
this.authStatus = AuthStatus.SUCCESS;
this.loadStartTransferPage();
break;
case (AuthResult.kError):
this.authStatus = AuthStatus.ERROR;
this.triggerErrorScreen();
}
}
private computeShowLoadingScreen(
authStatus: AuthStatus, isWebviewLoading: boolean) {
return authStatus === AuthStatus.IN_PROGRESS || isWebviewLoading === true;
}
private configureWebviewListeners(): void {
this.webview.addEventListener('contentload', () => {
this.webviewReloadHelper.reset();
this.setIsWebviewLoading(false);
});
this.webview.addEventListener('loadabort', () => {
this.onLoadAbort();
});
this.webview.addEventListener(
'newwindow', (e: chrome.webviewTag.NewWindowEvent) => {
window.open(e.targetUrl);
});
this.webview.request.onCompleted.addListener((details: any) => {
if (details.statusCode === 200 &&
details.url.startsWith(TAKEOUT_COMPLETED_BANNER_BASE_URL)) {
getGraduationUiHandler().onTransferComplete();
this.takeoutFlowCompleted = true;
}
}, {urls: ['<all_urls>']});
}
private onLoadAbort(): void {
if (this.authStatus !== AuthStatus.SUCCESS) {
return;
}
if (!navigator.onLine) {
return;
}
if (this.webviewReloadHelper.isReloadCountLimitReached()) {
this.webviewReloadHelper.reset();
this.setIsWebviewLoading(false);
this.triggerErrorScreen();
return;
}
this.setIsWebviewLoading(true);
this.webviewReloadHelper.scheduleReload(this.webview);
}
private setIsWebviewLoading(isWebviewLoading: boolean): void {
this.isWebviewLoading = isWebviewLoading;
}
private triggerErrorScreen(): void {
this.dispatchEvent(new CustomEvent(ScreenSwitchEvents.SHOW_ERROR, {
bubbles: true,
composed: true,
}));
}
private triggerWelcomeScreen(): void {
this.dispatchEvent(new CustomEvent(ScreenSwitchEvents.SHOW_WELCOME, {
bubbles: true,
composed: true,
}));
}
private loadStartTransferPage(): void {
if (this.webview.src !== this.startTransferUrl) {
this.webview.src = this.startTransferUrl;
return;
}
this.webview.reload();
}
private getBackButtonIcon(): string {
return isRTL() ? 'cr:chevron-right' : 'cr:chevron-left';
}
private onBackClicked(): void {
this.triggerWelcomeScreen();
if (this.authStatus === AuthStatus.SUCCESS) {
this.webview.stop();
this.webviewReloadHelper.reset();
this.setIsWebviewLoading(true);
this.loadStartTransferPage();
}
}
private onDoneClicked(): void {
window.close();
}
}
declare global {
interface HTMLElementTagNameMap {
[GraduationTakeoutUi.is]: GraduationTakeoutUi;
}
}
customElements.define(GraduationTakeoutUi.is, GraduationTakeoutUi);