chromium/chrome/browser/resources/chromeos/accessibility/chromevox/background/braille/braille_captions_background.ts

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

/**
 * @fileoverview If the braille captions feature is enabled, sends
 * braille content to the Panel on Chrome OS, or a content script on
 * other platforms.
 */
import {LocalStorage} from '/common/local_storage.js';
import {TestImportManager} from '/common/testing/test_import_manager.js';

import {BrailleDisplayState} from '../../common/braille/braille_key_types.js';
import {NavBraille} from '../../common/braille/nav_braille.js';
import {Msgs} from '../../common/msgs.js';
import {PanelCommand, PanelCommandType} from '../../common/panel_command.js';
import {SettingsManager} from '../../common/settings_manager.js';
import {QueueMode} from '../../common/tts_types.js';
import {ChromeVox} from '../chromevox.js';
import {ChromeVoxPrefs} from '../prefs.js';

interface Offsets {
  brailleOffset: number;
  textOffset: number;
}

type TextToDisplay = [text: string, braille: string];

/**
 * Interface that allows clients to listen for changes to the braille captions.
 */
export interface BrailleCaptionsListener {
  /** Called when the braille captions state changes. */
  onBrailleCaptionsStateChanged(): void;
}

export class BrailleCaptionsBackground {
  static instance: BrailleCaptionsBackground;

  private listener_: BrailleCaptionsListener;

  constructor(listener: BrailleCaptionsListener) {
    this.listener_ = listener;
  }

  /** Called once to initialize the class. */
  static init(listener: BrailleCaptionsListener): void {
    BrailleCaptionsBackground.instance =
        new BrailleCaptionsBackground(listener);
  }

  /** Returns whether the braille captions feature is enabled. */
  static isEnabled(): boolean {
    return Boolean(LocalStorage.get(PREF_KEY));
  }

  /**
   * @param text Text of the shown braille.
   * @param cells Braille cells shown on the display.
   * @param brailleToText Map of Braille letters to the first
   *     index of corresponding text letter.
   * @param offsetsForSlices
   *    Offsets to use for calculating indices. The element is the braille
   * offset, the second is the text offset.
   * @param rows Number of rows to display.
   * @param columns Number of columns to display.
   */
  static setContent(
      text: string, cells: ArrayBuffer, brailleToText: number[],
      offsetsForSlices: Offsets, rows: number, columns: number): void {
    // Convert the cells to Unicode braille pattern characters.
    const byteBuf = new Uint8Array(cells);
    let brailleChars = '';

    for (const byteVal of byteBuf) {
      brailleChars +=
          String.fromCharCode(BRAILLE_UNICODE_BLOCK_START | byteVal);
    }
    const groups = BrailleCaptionsBackground.groupBrailleAndText(
        brailleChars, text, brailleToText, offsetsForSlices);
    const data = {groups, rows, cols: columns};
    (new PanelCommand(PanelCommandType.UPDATE_BRAILLE, data)).send();
  }

  /**
   * @param cells Braille cells shown on the display.
   * @param rows Number of rows to display.
   * @param columns Number of columns to display.
   */
  static setImageContent(
      cells: ArrayBuffer, rows: number, columns: number): void {
    // Convert the cells to Unicode braille pattern characters.
    const byteBuf = new Uint8Array(cells);
    let brailleChars = '';

    for (const byteVal of byteBuf) {
      brailleChars +=
          String.fromCharCode(BRAILLE_UNICODE_BLOCK_START | byteVal);
    }

    const groups = [['Image', brailleChars]];
    const data = {groups, rows, cols: columns};
    (new PanelCommand(PanelCommandType.UPDATE_BRAILLE, data)).send();
  }

  /**
   * @param brailleChars Braille characters shown on the display.
   * @param text Text of the shown braille.
   * @param brailleToText Map of Braille cells to the first index of
   *     corresponding text letter.
   * @param offsets Offsets to use for calculating indices. The element is the
   *     braille offset, the second is the text offset.
   * @return The groups of braille and texts to be displayed.
   */
  static groupBrailleAndText(
      brailleChars: string, text: string, brailleToText: number[],
      offsets: Offsets): TextToDisplay[] {
    let brailleBuf = '';
    const groups: TextToDisplay[] = [];
    let textIndex = 0;
    const brailleOffset = offsets.brailleOffset;
    const textOffset = offsets.textOffset;
    const calculateTextIndex = (index: number): number =>
        brailleToText[index + brailleOffset] - textOffset;

    for (let i = 0; i < brailleChars.length; ++i) {
      const textSliceIndex = calculateTextIndex(i);
      if (i !== 0 && textSliceIndex !== textIndex) {
        groups.push(
            [text.substr(textIndex, textSliceIndex - textIndex), brailleBuf]);
        brailleBuf = '';
        textIndex = textSliceIndex;
      }
      brailleBuf += brailleChars.charAt(i);
    }

    // Puts the rest of the text into the last group.
    if (brailleBuf.length > 0 && text.charAt(textIndex) !== ' ') {
      groups.push([text.substr(textIndex), brailleBuf]);
    }
    return groups;
  }

  /**
   * Sets whether the overlay should be active.
   * @param newValue The new value of the active flag.
   */
  static setActive(newValue: boolean): void {
    const oldValue = BrailleCaptionsBackground.isEnabled();
    // TODO(b/314203187): Not null asserted, check that this is correct.
    ChromeVoxPrefs.instance!.setPref(PREF_KEY, newValue);
    if (oldValue !== newValue) {
      BrailleCaptionsBackground.instance.listener_
          .onBrailleCaptionsStateChanged();
      const msg = newValue ? Msgs.getMsg('braille_captions_enabled') :
                             Msgs.getMsg('braille_captions_disabled');
      ChromeVox.tts.speak(msg, QueueMode.QUEUE);
      ChromeVox.braille.write(NavBraille.fromText(msg));
    }
  }

  /**
   * Asynchronously returns a display state representing the state of the
   * captions feature. This is used when no actual hardware display is
   * connected.
   */
  static getVirtualDisplayState(): BrailleDisplayState {
    if (BrailleCaptionsBackground.isEnabled()) {
      const rows = SettingsManager.getNumber('virtualBrailleRows');
      const columns = SettingsManager.getNumber('virtualBrailleColumns');
      // TODO(accessibility) make `cellSize` customizable.
      return {
        available: true,
        textRowCount: rows,
        textColumnCount: columns,
        cellSize: 8,
      };
    } else {
      return {
        available: false,
        textRowCount: 0,
        textColumnCount: 0,
        cellSize: 0,
      };
    }
  }
}

// Local to module.

/** Key set in local storage when this feature is enabled. */
const PREF_KEY = 'brailleCaptions';

/**
 * Unicode block of braille pattern characters.  A braille pattern is formed
 * from this value with the low order 8 bits set to the bits representing
 * the dots as per the ISO 11548-1 standard.
 */
const BRAILLE_UNICODE_BLOCK_START = 0x2800;

TestImportManager.exportForTesting(BrailleCaptionsBackground);