chromium/ui/accessibility/extensions/highcontrast/storage.js

// 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.

/** @enum {number} */
const SchemeType = {
  NORMAL: 0,
  INCREASED_CONTRAST: 1,
  GRAYSCALE: 2,
  INVERTED_COLOR: 3,
  INVERTED_GRAYSCALE: 4,
  YELLOW_ON_BLACK: 5,
};

/**
 * Class to handle interactions with the chrome.storage API and values that are
 * stored there.
 */
class Storage {
  /**
   * @param {function()=} opt_callbackForTesting
   * @private
   */
  constructor(opt_callbackForTesting) {
    /** @private {boolean} */
    this.enabled_ = Storage.ENABLED.defaultValue;
    /** @private {!SchemeType} */
    this.baseScheme_ = Storage.SCHEME.defaultValue;
    /** @private {!Object<string, !SchemeType>} */
    this.siteSchemes_ = Storage.SITE_SCHEMES.defaultValue;

    this.init_();
  }

  // ======= Public Methods =======

  static initialize() {
    if (!Storage.instance) {
      Storage.instance = new Storage();
    }
  }

  /** @return {boolean} */
  static get enabled() { return Storage.instance.enabled_; }
  /** @return {!SchemeType} */
  static get baseScheme() { return Storage.instance.baseScheme_; }

  /**
   * @param {string} site
   * @return {!SchemeType}
   */
  static getSiteScheme(site) {
    const scheme = Storage.instance.siteSchemes_[site];
    if (Storage.SCHEME.validate(scheme)) {
      return scheme;
    }
    return Storage.baseScheme;
  }

  /** @param {boolean} newValue */
  static set enabled(newValue) {
    Storage.instance.setOrResetValue_(Storage.ENABLED, newValue);
    Storage.instance.store_(Storage.ENABLED);
  }

  /** @param {!SchemeType} newScheme */
  static set baseScheme(newScheme) {
    Storage.instance.setOrResetValue_(Storage.SCHEME, newScheme);
    Storage.instance.store_(Storage.SCHEME);
  }

  /**
   * @param {string} site
   * @param {!SchemeType} scheme
   */
  static setSiteScheme(site, scheme) {
    if (Storage.SCHEME.validate(scheme)) {
      Storage.instance.siteSchemes_[site] = scheme;
    } else {
      Storage.instance.siteSchemes_[site] = Storage.baseScheme;
    }
    Storage.instance.store_(Storage.SITE_SCHEMES);
  }

  static resetSiteSchemes() {
    Storage.instance.siteSchemes_ = Storage.SITE_SCHEMES.defaultValue;
    Storage.instance.store_(Storage.SITE_SCHEMES);
  }

  // ======= Private Methods =======

  /**
   * @param {!Storage.Value} container
   * @param {*} newValue
   * @private
   */
  setOrResetValue_(container, newValue) {
    if (newValue === container.get()) {
      return;
    }

    if (container.validate(newValue)) {
      container.set(newValue);
    } else {
      container.reset();
    }

    container.listeners.forEach(listener => listener(newValue));
  }

  /**
   * @param {function()=} opt_callback
   * @private
   */
  init_(opt_callback) {
    chrome.storage.onChanged.addListener(this.onChange_);
    chrome.storage.local.get(null /* all values */, (results) => {
      const storedData = Storage.ALL_VALUES.filter(v => results[v.key]);
      for (const data of storedData) {
        this.setOrResetValue_(data, results[data.key]);
      }
      opt_callback ? opt_callback() : undefined;
    });
  }

  /**
   * @param {!Object<string, chrome.storage.StorageChange>} changes
   * @private
   */
  onChange_(changes) {
    const changedData = Storage.ALL_VALUES.filter(v => changes[v.key]);
    for (const data of changedData) {
      Storage.instance.setOrResetValue_(data, changes[data.key].newValue);
    }
  }

  /**
   * @param {!Storage.Value} value
   * @private
   */
  store_(value) {
    chrome.storage.local.set({ [value.key]: value.get() });
  }

  // ======= Stored Values =======

  /**
   * @typedef {{
   *     key: string,
   *     defaultValue: *,
   *     validate: function(*): boolean,
   *     get: function: *,
   *     set: function(*),
   *     reset: function(),
   *     listeners: !Array<function(*)>
   * }}
   */
  static Value;

  /** @const {!Storage.Value} */
  static ENABLED = {
    key: 'enabled',
    defaultValue: true,
    validate: (enabled) => enabled === true || enabled === false,
    get: () => Storage.instance.enabled_,
    set: (enabled) => Storage.instance.enabled_ = enabled,
    reset: () => Storage.instance.enabled_ = Storage.ENABLED.defaultValue,
    listeners: [],
  };

  /** @const {!Storage.Value} */
  static SCHEME = {
    key: 'scheme',
    defaultValue: SchemeType.INVERTED_COLOR,
    validate: (scheme) => Object.values(SchemeType).includes(scheme),
    get: () => Storage.instance.baseScheme_,
    set: (scheme) => Storage.instance.baseScheme_ = scheme,
    reset: () => Storage.instance.baseScheme_ = Storage.SCHEME.defaultValue,
    listeners: [],
  };

  /** @const {!Storage.Value} */
  static SITE_SCHEMES = {
    key: 'siteschemes',
    defaultValue: {},
    validate: (siteSchemes) => typeof (siteSchemes) === 'object',
    get: () => Storage.instance.siteSchemes_,
    set: (siteSchemes) => {
      for (const site of Object.keys(siteSchemes)) {
        if (Storage.SCHEME.validate(siteSchemes[site])) {
          Storage.instance.siteSchemes_[site] = siteSchemes[site];
        }
      }
    },
    reset: () => {} /** Do nothing */,
    listeners: [],
  };

  /** @const {!Array<!Storage.Value>} */
  static ALL_VALUES = [
      Storage.ENABLED, Storage.SCHEME, Storage.SITE_SCHEMES
  ];
}