Source: js/screenshot.js

/* global MozActivity, Notification, Service */

'use strict';

(function(exports) {

  /**
  * This system module takes a screenshot of the currently running app
  * or homescreen and stores it with DeviceStorage when the user
  * presses the home and sleep buttons at the same time. It communicates
  * with gecko code running in b2g/chrome/content/shell.js using a private
  * event-based API. It is the gecko code that creates the screenshot.
  *
  * This script must be used with the defer attribute.
  *
  * @class Screenshot
  *
  */
  function Screenshot() {
    this._started = false;
  }

  Screenshot.prototype = {
    /** @lends Screenshot */

    /**
     * Assumption for making sure we have enough space to save the image.
     *
     * Assume that the maximum screenshot size is 4 bytes per device pixel
     * plus a bit extra. In practice, with compression, our PNG files will be
     * much smaller than this.
     *
     * @type {Number}
     * @memberof Screenshot.prototype
     */
    MAX_SCREENSHOT_SIZE:
      window.innerWidth * window.devicePixelRatio *
      window.innerHeight * window.devicePixelRatio * 4 + 4096,

    /**
     * Start to handle screenshot events.
     * @memberof Screenshot.prototype
     */
    start: function() {
      if (this._started) {
        throw 'Instance should not be start()\'ed twice.';
      }
      this._started = true;

      Service.request('handleSystemMessageNotification', 'screenshot', this);
      window.addEventListener('volumedown+sleep', this);
      window.addEventListener('mozChromeEvent', this);
    },

    /**
     * Stop handling screenshot events.
     * @memberof Screenshot.prototype
     */
    stop: function() {
      if (!this._started) {
        throw 'Instance was never start()\'ed but stop() is called.';
      }
      this._started = false;

      Service.request('unhandleSystemMessageNotification', 'screenshot', this);
      window.removeEventListener('volumedown+sleep', this);
      window.removeEventListener('mozChromeEvent', this);
    },

    /**
     * Handle screenshot events.
     * @param  {DOMEvent} evt DOM Event to handle.
     * @memberof Screenshot.prototype
     */
    handleEvent: function(evt) {
      switch (evt.type) {
        case 'volumedown+sleep':
          this.takeScreenshot();
          break;

        case 'mozChromeEvent':
          if (evt.detail.type === 'take-screenshot-success') {
            this.handleTakeScreenshotSuccess(evt.detail.file);
          } else if (evt.detail.type === 'take-screenshot-error') {
            this._notify('screenshotFailed', evt.detail.error);
          }
          break;

        default:
          console.debug('Unhandled event: ' + evt.type);
          break;
      }
    },

    /**
     * Gets called when a system message notification for a screenshots is being
     * hit.
     **/
    handleSystemMessageNotification: function(message) {
      this.openImage(message.body);
      this.closeSystemMessageNotification(message);
    },

    closeSystemMessageNotification: function(msg) {
      Notification.get({ tag: msg.tag }).then(notifs => {
        notifs.forEach(notif => {
          if (notif.tag) {
            // Close notification with the matching tag
            if (notif.tag === msg.tag) {
              notif.close && notif.close();
            }
          } else {
            // If we have notification without a tag, check on the body
            if (notif.body === msg.body) {
              notif.close && notif.close();
            }
          }
        });
      });
    },

    /**
     * Actually take a screenshot (by do some check and send a mozContentEvent.)
     * @memberof Screenshot.prototype
     */
    takeScreenshot: function() {
      // Give feedback that the screenshot request was received
      navigator.vibrate(100);

      // We don't need device storage here, but check to see that
      // it is available before sending the screenshot request to chrome.
      // If device storage is available, the callback will be called.
      // Otherwise, an error message notification will be displayed.
      this._getDeviceStorage(function() {
        // Let chrome know we'd like a screenshot.
        // This is a completely non-standard undocumented API
        // for communicating with our chrome code.
        var screenshotProps = {
          detail: {
            type: 'take-screenshot'
          }
        };
        window.dispatchEvent(
          new CustomEvent('mozContentEvent', screenshotProps));
      });
    },

    openImage: function(filename) {
      this._getDeviceStorage(function(storage) {
        var request = storage.get(filename);
        request.onsuccess = function() {
          var imgblob = this.result;
          /*jshint nonew: false */
          new MozActivity({
            name: 'open',
            data: {
              type: imgblob.type,
              filename: filename,
              blob: imgblob,
              exitWhenHidden: true
            }
          });
        };
      });
    },

    /**
     * Handle the take-screenshot-success mozChromeEvent.
     * @param  {Blob} file Blob object received from the event.
     * @memberof Screenshot.prototype
     */
    handleTakeScreenshotSuccess: function(file) {
      try {
        this._getDeviceStorage(function(storage) {
          var d = new Date();
          d = new Date(d.getTime() - d.getTimezoneOffset() * 60000);
          var filename = 'screenshots/' +
            d.toISOString().slice(0, -5).replace(/[:T]/g, '-') + '.png';

          var saveRequest = storage.addNamed(file, filename);
          saveRequest.onsuccess = (function ss_onsuccess() {
            // Vibrate again when the screenshot is saved
            navigator.vibrate(100);

            // Display filename in a notification
            this._notify('screenshotSaved', filename, null,
                         this.openImage.bind(this, filename));
          }).bind(this);

          saveRequest.onerror = (function ss_onerror() {
            this._notify('screenshotFailed', saveRequest.error.name);
          }).bind(this);
        });
      } catch (e) {
        console.log('exception in screenshot handler', e);
        this._notify('screenshotFailed', e.toString());
      }
    },

    /**
     * Get a DeviceStorage object and pass it to the callback.
     * Or, if device storage is not available, display a notification.
     * @param {Function} callback Callback to run.
     * @memberof Screenshot.prototype
     */
    _getDeviceStorage: function(callback) {
      var storage = navigator.getDeviceStorage('pictures');
      var availreq = storage.available();

      availreq.onsuccess = (function() {
        var state = availreq.result;
        if (state === 'unavailable') {
          this._notify('screenshotFailed', null, 'screenshotNoSDCard');
        }
        else if (state === 'shared') {
          this._notify('screenshotFailed', null, 'screenshotSDCardInUse');
        }
        else if (state === 'available') {
          var freereq = storage.freeSpace();
          freereq.onsuccess = (function() {
            if (freereq.result < this.MAX_SCREENSHOT_SIZE) {
              this._notify('screenshotFailed', null, 'screenshotSDCardLow');
            } else {
              callback.call(this, storage);
            }
          }).bind(this);
          freereq.onerror = (function() {
            this._notify(
              'screenshotFailed', freereq.error && freereq.error.name);
          }).bind(this);
        }
      }).bind(this);

      availreq.onerror = (function() {
        this._notify(
          'screenshotFailed', availreq.error && availreq.error.name);
      }).bind(this);
    },

    /**
     * Display a screenshot success or failure notification.
     * Localize the first argument, and localize the third if the second is null
     * @param  {String} titleid  l10n ID of the string to show.
     * @param  {String} body     Label to show as body, or null.
     * @param  {String} bodyid   l10n ID of the label to show as body.
     * @param  {String} onClick  Optional handler if the notification is clicked
     * @memberof Screenshot.prototype
     */
    _notify: function notify(titleid, body, bodyid, onClick) {
      var title = navigator.mozL10n.get(titleid) || titleid;
      body = body || navigator.mozL10n.get(bodyid);
      var notification = new window.Notification(title, {
        body: body,
        icon: '/style/icons/Gallery.png',
        tag: 'screenshot:' + (new Date().getTime()),
        data: {
          systemMessageTarget: 'screenshot'
        }
      });

      notification.onclick = function() {
        notification.close();
        if (onClick) {
          onClick();
        }
      };
    }
  };

  exports.Screenshot = Screenshot;
}(window));