chromium/chrome/browser/resources/chromeos/desk_api/service_worker.ts

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

/**
 * @fileoverview This file provides the implementation for the service worker
 * class as declared in types.d.ts
 */

import {DeskApiBridgeRequest, DeskApiBridgeResponse, MessageSender, ServiceWorker} from './desk_api_types.js';
import {EventType, RequestType, ResponseType} from './message_type.js';
import {Desk, DeskApi, GetDeskByIdOperands, LaunchOptions, NotificationApi, NotificationOptions, RemoveDeskOperands, SwitchDeskOperands, WindowProperties} from './types.js';


/**
 * Actual implementation for the service worker.
 */
class ServiceWorkerImpl implements ServiceWorker {
  /**
   * This constructor sets three fields: the deskApi and notification field
   * exists in order to facilitate dependency injection for this class, the
   * isTest boolean field exists in order to short circuit reads to
   * chrome.runtime.LastError during tests.  This is done because said variable
   * is not available in the test environment and there is no way to inject a
   * reference to that error here.
   */
  constructor(
      public deskApi: DeskApi, public notificationApi: NotificationApi,
      public isTest: boolean) {}

  launchDeskPromise(options: LaunchOptions) {
    return new Promise<DeskApiBridgeResponse>((resolve, reject) => {
      try {
        this.deskApi.launchDesk(options, (deskId: string) => {
          if (!this.isTest && chrome.runtime.lastError) {
            reject(chrome.runtime.lastError);
            return;
          }
          resolve({
            messageType: ResponseType.OPERATION_SUCCESS,
            operands: {'deskUuid': deskId},
          });
        });
      } catch (error: unknown) {
        reject(error);
      }
    });
  }

  removeDeskPromise(operands: RemoveDeskOperands) {
    if (!operands.deskId) {
      return Promise.reject({message: 'The desk can not be found.'});
    }

    if (operands.options) {
      if (operands.options.allowUndo && operands.options.combineDesks) {
        return Promise.reject({
          message:
              'Unsupported behavior: allowUndo and combineDesks are both true.',
        });
      }
    }

    if (operands.skipConfirmation) {
      return this.removeDeskInternalPromise(operands);
    }

    const notificationId = 'deskAPI';
    this.notificationApi.clear(notificationId);
    const defaultNotificationOptions: NotificationOptions = {
      title: 'Close all windows?',
      message: 'This interaction has ended. ' +
          'This will close all windows on this desk.',
      iconUrl: 'templates_icon.png',
      buttons: [{title: 'Close windows'}, {title: 'Keep windows'}],
    };
    const notificationOptions =
        !operands.confirmationSetting ? defaultNotificationOptions : {
          title: operands.confirmationSetting.title,
          message: operands.confirmationSetting.message,
          iconUrl: operands.confirmationSetting.iconUrl,
          buttons: [
            {title: operands.confirmationSetting.acceptMessage},
            {title: operands.confirmationSetting.rejectMessage},
          ],
        };

    return new Promise<DeskApiBridgeResponse>((resolve, reject) => {
      // First, check if the desk is able to be obtained.
      this.getDeskByIdPromise({deskId: operands.deskId})
          .then(() => {
            resolve(this.removeDeskNotificationPromise(
                operands, notificationOptions, notificationId));
          })
          .catch(error => {
            reject(error);
          });
    });
  }

  removeDeskNotificationPromise(
      operands: RemoveDeskOperands, notificationOptions: NotificationOptions,
      notificationId: string) {
    return new Promise<DeskApiBridgeResponse>((resolve, reject) => {
      try {
        //  Create a pop up window
        //  Set notificationId so that the same notification needs to be
        //  cancelled before the new notification can be created
        this.notificationApi.create(notificationId, notificationOptions, () => {
          resolve(this.removeDeskAddNotificationListenerPromise(operands));
        });
      } catch (error: unknown) {
        this.notificationApi.clear(notificationId);
        reject(error);
      }
    });
  }

  removeDeskAddNotificationListenerPromise(operands: RemoveDeskOperands) {
    return new Promise<DeskApiBridgeResponse>((resolve, reject) => {
      this.notificationApi.addClickEventListener(
          (notificationId, buttonIndex) => {
            if (buttonIndex === 0) {
              this.removeDeskInternalPromise(operands)
                  .then((result) => {
                    resolve(result);
                  })
                  .catch(error => {
                    reject(error);
                  });
            } else {
              reject(new Error('User cancelled desk removal operation'));
            }
            this.notificationApi.clear(notificationId);
          });
    });
  }

  removeDeskInternalPromise(operands: RemoveDeskOperands) {
    return new Promise<DeskApiBridgeResponse>((resolve, reject) => {
      try {
        this.deskApi.removeDesk(operands.deskId, operands.options, () => {
          if (!this.isTest && chrome.runtime.lastError) {
            reject(chrome.runtime.lastError);
            return;
          }
          resolve({
            messageType: ResponseType.OPERATION_SUCCESS,
            operands: [],
          });
        });
      } catch (error: unknown) {
        reject(error);
      }
    });
  }

  setWindowPropertiesPromise(
      operands: WindowProperties, sender: MessageSender) {
    return new Promise<DeskApiBridgeResponse>((resolve, reject) => {
      try {
        this.deskApi.setWindowProperties(sender.tab.windowId, operands, () => {
          if (!this.isTest && chrome.runtime.lastError) {
            reject(chrome.runtime.lastError);
            return;
          }
          resolve({messageType: ResponseType.OPERATION_SUCCESS, operands: []});
        });

      } catch (error: unknown) {
        reject(error);
      }
    });
  }

  getActiveDeskPromise() {
    return new Promise<DeskApiBridgeResponse>((resolve, reject) => {
      try {
        this.deskApi.getActiveDesk((deskId: string) => {
          if (!this.isTest && chrome.runtime.lastError) {
            reject(chrome.runtime.lastError);
            return;
          }
          resolve({
            messageType: ResponseType.OPERATION_SUCCESS,
            operands: {'deskUuid': deskId},
          });
        });

      } catch (error: unknown) {
        reject(error);
      }
    });
  }

  switchDeskPromise(operands: SwitchDeskOperands) {
    return new Promise<DeskApiBridgeResponse>((resolve, reject) => {
      try {
        this.deskApi.switchDesk(operands.deskId, () => {
          if (!this.isTest && chrome.runtime.lastError) {
            reject(chrome.runtime.lastError);
            return;
          }
          resolve({messageType: ResponseType.OPERATION_SUCCESS, operands: []});
        });

      } catch (error: unknown) {
        reject(error);
      }
    });
  }

  getDeskByIdPromise(operands: GetDeskByIdOperands) {
    return new Promise<DeskApiBridgeResponse>((resolve, reject) => {
      try {
        this.deskApi.getDeskById(operands.deskId, (desk: Desk) => {
          if (!this.isTest && chrome.runtime.lastError) {
            reject(chrome.runtime.lastError);
            return;
          }
          resolve({
            messageType: ResponseType.OPERATION_SUCCESS,
            operands: {'deskUuid': desk.deskUuid, 'deskName': desk.deskName},
          });
        });
      } catch (error: unknown) {
        reject(error);
      }
    });
  }
  /**
   * This function handles a message and returns a promise containing
   * the result of the operation called for by the RequestType field.
   */
  handleMessage(message: DeskApiBridgeRequest, sender: MessageSender):
      Promise<DeskApiBridgeResponse> {
    switch (message.messageType) {
      case RequestType.LAUNCH_DESK:
        return this.launchDeskPromise(message.operands as LaunchOptions);
      case RequestType.REMOVE_DESK:
        return this.removeDeskPromise(message.operands as RemoveDeskOperands);
      case RequestType.SET_WINDOW_PROPERTIES:
        return this.setWindowPropertiesPromise(
            message.operands as WindowProperties, sender);
      case RequestType.GET_ACTIVE_DESK:
        return this.getActiveDeskPromise();
      case RequestType.SWITCH_DESK:
        return this.switchDeskPromise(message.operands as SwitchDeskOperands);
      case RequestType.GET_DESK_BY_ID:
        return this.getDeskByIdPromise(message.operands as GetDeskByIdOperands);
      default:
        throw new Error(`message of unknown type: ${message.messageType}!`);
    }
  }

  /**
   * This function register listener for desk events.
   * @param port port for communicating with web page
   */
  //@ts-ignore
  registerEventsListener(port: chrome.runtime.Port): void {
    this.deskApi.addDeskAddedListener((id: string, fromUndo = false) => {
      const eventType = fromUndo ? EventType.DESK_UNDONE : EventType.DESK_ADDED;
      port.postMessage({'eventName': eventType, 'data': {'deskId': id}});
    });
    this.deskApi.addDeskRemovedListener((id: string) => {
      port.postMessage(
          {'eventName': EventType.DESK_REMOVED, 'data': {'deskId': id}});
    });
    this.deskApi.addDeskSwitchedListener((a: string, d: string) => {
      port.postMessage({
        'eventName': EventType.DESK_SWITCHED,
        'data': {'activated': a, 'deactivated': d},
      });
    });
  }
}

/**
 * This class manages the single service worker instance and is the point of
 * injections for its dependencies.
 */
export class ServiceWorkerFactory {
  // global managed instance.
  private static serviceWorkerInstance: ServiceWorker;

  constructor(
      deskApi: DeskApi, notificationApi: NotificationApi, isTest: boolean) {
    ServiceWorkerFactory.serviceWorkerInstance =
        new ServiceWorkerImpl(deskApi, notificationApi, isTest);
  }

  get(): Promise<ServiceWorker> {
    return new Promise<ServiceWorker>((resolve, reject) => {
      if (ServiceWorkerFactory.serviceWorkerInstance === null) {
        reject(new Error('Service worker factory not constructed!'));
        return;
      }

      resolve(ServiceWorkerFactory.serviceWorkerInstance);
    });
  }
}