chromium/ash/webui/projector_app/resources/app/untrusted/untrusted_app_comm_factory.js

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {COLOR_PROVIDER_CHANGED, ColorChangeUpdater} from '//resources/cr_components/color_change_listener/colors_css_updater.js';
import {PromiseResolver} from '//resources/js/promise_resolver.js';
import {ProjectorError} from 'chrome-untrusted://projector/common/message_types.js';

import {installLaunchHandler} from './launch.js';
import {browserProxy} from './untrusted_projector_browser_proxy.js';

// Maps video file id to promises of video files.
const loadingFiles = new Map /*<string, PromiseResolver>*/ ();

function getOrCreateLoadFilePromise(fileId) {
  if (loadingFiles.has(fileId)) {
    return loadingFiles.get(fileId);
  }
  const promise = new PromiseResolver();
  loadingFiles.set(fileId, promise);
  return promise;
}

/**
 * Returns the projector app element inside this current DOM.
 * @return {projectorApp.AppApi}
 */
function getAppElement() {
  return /** @type {projectorApp.AppApi} */ (
      document.querySelector('projector-app'));
}

/**
 * Waits for the projector-app element to exist in the DOM.
 */
function waitForAppElement() {
  return new Promise(resolve => {
    if (getAppElement()) {
      return resolve();
    }

    const observer = new MutationObserver(mutations => {
      if (getAppElement()) {
        resolve();
        observer.disconnect();
      }
    });

    observer.observe(document.body, {childList: true, subtree: true});
  });
}

/**
 * Implements and supports the methods defined by the
 * projectorApp.ClientDelegate interface defined in
 * //ash/webui/projector_app/resources/communication/projector_app.externs.js.
 */
const CLIENT_DELEGATE = {
  /**
   * Gets the list of primary and secondary accounts currently available on the
   * device.
   * @return {Promise<Array<!projectorApp.Account>>}
   */
  getAccounts() {
    return browserProxy.getAccounts();
  },

  /**
   * Checks whether the SWA can trigger a new Projector session.
   * @return {!Promise<!projectorApp.NewScreencastPreconditionState>}
   */
  getNewScreencastPreconditionState() {
    return browserProxy.getNewScreencastPreconditionState();
  },

  /**
   * Starts the Projector session if it is possible. Provides the storage dir
   * where  projector output artifacts will be saved in.
   * @param {string} storageDir
   * @return {Promise<boolean>}
   */
  startProjectorSession(storageDir) {
    return browserProxy.startProjectorSession(storageDir);
  },

  /**
   * Gets the oauth token with the required scopes for the specified account.
   * @param {string} account user's email
   * @return {!Promise<!projectorApp.OAuthToken>}
   */
  getOAuthTokenForAccount(account) {
    return Promise.reject('Unsupported method getOauthTokenForAccount');
  },

  /**
   * Sends 'error' message to handler.
   * The Handler will log the message. If the error is not a recoverable error,
   * the handler closes the corresponding WebUI.
   * @param {!Array<ProjectorError>} msg Error messages.
   */
  onError(msg) {
    console.error('Received error messages:', msg);
  },

  /**
   * Gets the list of pending screencasts that are uploading to drive on current
   * device.
   * @return {Promise<Array<projectorApp.PendingScreencast>>}
   */
  getPendingScreencasts() {
    return browserProxy.getPendingScreencasts();
  },

  /*
   * Send XHR request.
   * @param {string} url the request URL.
   * @param {string} method the request method.
   * @param {string=} requestBody the request body data.
   * @param {boolean=} useCredentials authorize the request with end user
   *     credentials. Used for getting streaming URL.
   * @param {boolean=} useApiKey authorize the request with API key. Used for
   *     translaton requests.
   * @param {object=} additional headers.
   * @param {string=} account email.
   * @return {!Promise<!projectorApp.XhrResponse>}
   */
  sendXhr(
      url, method, requestBody, useCredentials, useApiKey, headers,
      accountEmail) {
    return browserProxy.sendXhr(
        url, method, requestBody, !!useCredentials, !!useApiKey, headers,
        accountEmail);
  },

  /**
   * Returns true if the device supports on device speech recognition.
   * @return {!Promise<boolean>}
   */
  shouldDownloadSoda() {
    return browserProxy.shouldDownloadSoda();
  },

  /**
   * Triggers the installation of on device speech recognition binary and
   * language packs for the user's locale. Returns true if download and
   * installation started.
   * @return {!Promise<boolean>}
   */
  installSoda() {
    return browserProxy.installSoda();
  },

  /**
   * Checks if the user has given consent for the creation flow during
   * onboarding. If the `userPref` is not supported the returned promise will be
   * rejected.
   * @param {string} userPref
   * @return {!Promise<Object>}
   */
  getUserPref(userPref) {
    return browserProxy.getUserPref(userPref);
  },

  /**
   * Returns consent given by the user to enable creation flow during
   * onboarding.
   * @param {string} userPref
   * @param {Object} value A preference can store multiple types (dictionaries,
   *     lists, Boolean, etc..); therefore, accept a generic Object value.
   * @return {!Promise} Promise resolved when the request was handled.
   */
  setUserPref(userPref, value) {
    return browserProxy.setUserPref(userPref, value);
  },

  /**
   * Triggers the opening of the Chrome feedback dialog.
   * @return {!Promise}
   */
  openFeedbackDialog() {
    return browserProxy.openFeedbackDialog();
  },

  /**
   * Gets information about the specified video from DriveFS.
   * @param {string} videoFileId The Drive item id of the video file.
   * @param {string|undefined} resourceKey The Drive item resource key.
   * TODO(b/237089852): Wire up the resource key once DriveFS has support.
   * @return {!Promise<!projectorApp.Video>}
   */
  async getVideo(videoFileId, resourceKey) {
    try {
      const video = await browserProxy.getVideo(videoFileId, resourceKey);
      const videoFile = await getOrCreateLoadFilePromise(videoFileId).promise;
      return {
        fileId: videoFileId,
        durationMillis: video.durationMillis.toString(),
        // The streaming url must be generated in the untrusted context.
        // The corresponding cleanup call to URL.revokeObjectURL() happens in
        // ProjectorViewer::maybeResetUI() in Google3.
        srcUrl: URL.createObjectURL(videoFile),
      };
    } catch (e) {
      return Promise.reject(e);
    } finally {
      // Do not cache video files in the map because it's unclear when to
      // invalidate entries. Delete the key once we are finally done with it.
      loadingFiles.delete(videoFileId);
    }
  },
};


let initialized = false;
function initCommunication() {
  if (initialized) {
    return;
  }
  initialized = true;

  // Set the client delegate to the app element.
  getAppElement().setClientDelegate(CLIENT_DELEGATE);

  // Install launch handler to observe files sent from
  // Drivefs.
  installLaunchHandler((fileId, file, error) => {
    const resolver = getOrCreateLoadFilePromise(fileId);
    if (!file || error) {
      resolver.reject(error);
      return;
    }
    resolver.resolve(file);
  });

  const callbackRouter = browserProxy.getProjectorCallbackRouter();
  // Setup the callback routers to handle requests from browser process.
  callbackRouter.onNewScreencastPreconditionChanged.addListener(
      (precondition) => {
        try {
          getAppElement().onNewScreencastPreconditionChanged(precondition);
        } catch (error) {
          console.error(
              'Unable to notify onNewScreencastPreconditionChanged method',
              error);
        }
      });
  callbackRouter.onSodaInstallProgressUpdated.addListener((progress) => {
    getAppElement().onSodaInstallProgressUpdated(progress);
  });
  callbackRouter.onSodaInstallError.addListener(() => {
    getAppElement().onSodaInstallError();
  });
  callbackRouter.onSodaInstalled.addListener(() => {
    getAppElement().onSodaInstalled();
  });
  callbackRouter.onScreencastsStateChange.addListener((pendingScreencasts) => {
    getAppElement().onScreencastsStateChange(pendingScreencasts);
  });
}

/** @suppress {missingProperties} */
function initColorUpdater() {
  window.addEventListener('DOMContentLoaded', () => {
    // Start listening to color change events. These events get picked up by
    // logic in ts_helpers.ts on the google3 side.
    ColorChangeUpdater.forDocument().start();
  });

  // Expose functions to bind to color change events to window so they can be
  // automatically picked up by installColors(). See ts_helpers.ts in google3.
  window.addColorChangeListener = function(listener) {
    ColorChangeUpdater.forDocument().eventTarget.addEventListener(
        COLOR_PROVIDER_CHANGED, listener);
  };
  window.removeColorChangeListener = function(listener) {
    ColorChangeUpdater.forDocument().eventTarget.removeEventListener(
        COLOR_PROVIDER_CHANGED, listener);
  };
}

waitForAppElement().then(() => {
  // Create instances of the singletons (PostMessageAPIClient and
  // RequestHandler) when the document has finished loading.
  initCommunication();
  initColorUpdater();
});