Source: panels/sound/tone_manager.js

/* global URL, MozActivity */
/**
 * Handle tone functionality in sound panel
 * @module ToneManager
 */
define(function(require) {
  'use strict';

  var SettingsListener = require('shared/settings_listener');
  var SettingsCache = require('modules/settings_cache');
  var ForwardLock = require('shared/omadrm/fl');

  var ToneManager = function() {
    this._elements = null;
    this._tones = null;
  };

  ToneManager.prototype = {
    /**
     * initialization.
     *
     * @access public
     * @memberOf ToneManager.prototype
     */
    init: function tm_init(elements) {
      this._elements = elements;
      this._configureTones();
      this._handleTones();

      this._elements.manageTones.addEventListener('click',
        this._manageTonesClickHandler);
    },

    /**
     * Initialize the ring tone and alert tone menus.
     *
     * @access private
     * @memberOf ToneManager.prototype
     */
    _configureTones: function tm_configureTones() {
      // This array has one element for each selectable tone that
      // appears in the "Tones" section of ../elements/sound.html.
      this._tones = [{
        pickType: 'alerttone',
        settingsKey: 'notification.ringtone',
        allowNone: true, // Allow "None" as a choice for alert tones.
        button: this._elements.alertToneSelection,
        desc: this._elements.alertToneSelectionDesc
      }];

      // If we're a telephone, then show the section for ringtones, too.
      if (navigator.mozTelephony) {
        this._tones.push({
          pickType: 'ringtone',
          settingsKey: 'dialer.ringtone',
          allowNone: false, // The ringer must always have an actual sound.
          button: this._elements.ringToneSelection,
          desc: this._elements.ringToneSelectionDesc
        });
        this._elements.ringer.hidden = false;
      }
    },

    /**
     * The button looks like a select element and holds the name of the
     * currently-selected tone. Sometimes the name is an l10n ID
     * and sometimes it is just text.
     *
     * @access private
     * @memberOf ToneManager.prototype
     */
    _renderToneName: function tm_renderToneName(tone, tonename) {
      var l10nID = tonename && tonename.l10nID;

      if (l10nID) {
        tone.desc.setAttribute('data-l10n-id', l10nID);
      } else {
        tone.desc.removeAttribute('data-l10n-id');
        tone.desc.textContent = tonename;
      }
    },

    /**
     * render tone content
     *
     * @access private
     * @memberOf ToneManager.prototype
     * @param  {Object} tone     tone element
     * @param  {Object} result     web activity result
     * @param  {String} secret   sound is playable
     */
    _renderTone: function tm_renderTone(tone, result, secret) {
      var oldRingtoneName = null;

      var l10nId = tone.desc.getAttribute('data-l10n-id');
      if (!l10nId) {
        oldRingtoneName = tone.desc.textContent;
      }
      tone.desc.setAttribute('data-l10n-id', 'saving-tone');
      var promise;
      var self = this;
      // If we got a locked ringtone, we have to unlock it first
      if (result.blob.type.split('/')[1] === ForwardLock.mimeSubtype) {
        ForwardLock.unlockBlob(secret, result.blob, function(unlocked) {
          promise = self._isPlayableTone(unlocked);
        });
      } else {  // Otherwise we can just use the blob directly.
        promise = self._isPlayableTone(result.blob);
      }
      promise.then(function(isPlayable) {
        if (isPlayable) {
          self._setRingtone(tone.settingsKey, result.l10nID,
            result.name, result.blob, result.id);
        } else {
          if (l10nId) {
            tone.desc.setAttribute('data-l10n-id', l10nId);
          } else {
             // remove temp 'saving-tone' l10nId
            tone.desc.removeAttribute('data-l10n-id');
            tone.desc.textContent = oldRingtoneName;
          }
          alert(navigator.mozL10n.get('unplayable-ringtone'));
        }
      });
    },

    /**
     * Call web activity to pick a tone
     *
     * @access private
     * @memberOf ToneManager.prototype
     * @param  {Object} tone          tone element
     * @param  {Number} currentToneID tone id
     * @param  {String} secret        forwardlock secret key
     */
    _pickTone: function tm_pickRingTone(tone, currentToneID, secret) {
      var self = this;
      var activity = new MozActivity({
        name: 'pick',
        data: {
          type: tone.pickType,
          allowNone: tone.allowNone,
          currentToneID: currentToneID,
          // If we have a secret then there is locked content on the
          // phone so include it as a choice for the user
          includeLocked: (secret !== null)
        }
      });

      activity.onsuccess = function() {
        var result = activity.result;
        if (!result.blob) {
          if (tone.allowNone) {
            // If we allow a null blob, then everything is okay
            self._setRingtone(tone.settingsKey, result.l10nID, result.name,
              result.blob, result.id);
          } else {
            // Otherwise this is an error and we should not change the
            // current setting. (The ringtones app should never return
            // a null blob if allowNone is false, but other apps might.)
            alert(navigator.mozL10n.get('unplayable-ringtone'));
          }
          return;
        }

        self._renderTone(tone, result, secret);
      };
    },

    /**
     * Update Ringtones list.
     *
     * @access private
     * @memberOf ToneManager.prototype
     */
    _handleTones: function tm_handleTones() {
      // For each kind of tone, hook up the button that will allow the user
      // to select a sound for that kind of tone.
      this._tones.forEach(function(tone) {
        var nameKey = tone.settingsKey + '.name';
        var idKey = tone.settingsKey + '.id';

        SettingsListener.observe(nameKey, '',
          this._renderToneName.bind(this, tone));

        var self = this;
        // When the user clicks the button, we launch an activity that lets
        // the user select new ringtone.
        tone.button.addEventListener('click', function() {
          // First, get the ID of the currently-selected tone.
          SettingsCache.getSettings(function(results) {
            var currentToneID = results[idKey];

            // Before we can start the Pick activity, we need to know if there
            // is locked content on the phone because we don't want the user to
            // see "Purchased Media" as a choice if there isn't any purchased
            // media on the phone. The ForwardLock secret key is not generated
            // until it is needed, so we can use its existance to
            // determine whether to show the Purchased Media app.
            ForwardLock.getKey(function(secret) {
              self._pickTone(tone, currentToneID, secret);
            });
          });
        });
      }.bind(this));
    },

    /**
     * Save the sound blob in the settings so that other apps can use it.
     * Also save the sound name in the db so we can display it in the
     * future.  And update the button text to the new name now.
     *
     * @access private
     * @memberOf ToneManager.prototype
     * @param  {String} settingsKey   key string
     * @param  {String} l10nID        element l10nID id
     * @param  {String} toneName      element name
     * @param  {Blob} blob            tone blob
     * @param  {String} id            tone id
     */
    _setRingtone: function tm_setRingtone(settingsKey, l10nID, toneName,
      blob, id) {
      var blobKey = settingsKey;
      var nameKey = settingsKey + '.name';
      var idKey = settingsKey + '.id';

      // Update the settings database. This will cause the button
      // text to change as well because of the SettingsListener above.
      var values = {};
      var name = l10nID ? {l10nID : l10nID} : toneName;

      values[blobKey] = blob;
      values[nameKey] = name || '';
      values[idKey] = id;
      navigator.mozSettings.createLock().set(values);
    },

    /**
     * Make sure that the blob we got from the activity is actually
     * a playable audio file. It would be very bad to set an corrupt
     * blob as a ringtone because then the phone wouldn't ring!
     *
     * @access private
     * @memberOf ToneManager.prototype
     * @param  {Blob} blob            tone blob
     * @return {Promise} A promise that resolves to the media playable stat
     */
    _isPlayableTone: function tm_isPlayableTone(blob) {
      return new Promise(function(resolve, reject) {
        var player = new Audio();
        player.preload = 'metadata';
        player.src = URL.createObjectURL(blob);
        player.oncanplay = function() {
          release();
          resolve(true);
        };
        player.onerror = function() {
          release();
          resolve(false);
        };

        function release() {
          URL.revokeObjectURL(player.src);
          player.removeAttribute('src');
          player.load();
        }
      });
    },

    /**
     * Call ringtone activity to manage tones.
     *
     * @access private
     * @memberOf ToneManager.prototype
     */
    _manageTonesClickHandler: function tm_manageTonesClickHandler() {
      var activity = new MozActivity({
        name: 'configure',
        data: {
          target: 'ringtone'
        }
      });

      // We should hopefully never encounter this error, but if we do, it means
      // we couldn't find the ringtone app. It also has the happy side effect of
      // quieting jshint about not using the `activity` variable.
      activity.onerror = function() {
        console.log(this.error);
        if (this.error.name === 'NO_PROVIDER') {
          alert(navigator.mozL10n.get('no-ringtone-app'));
        }
      };
    }
  };

  return function ctor_toneManager() {
    return new ToneManager();
  };
});