mobile/android/modules/Home.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/modules/Home.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,466 @@
     1.4 +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
     1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.7 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.8 +
     1.9 +"use strict";
    1.10 +
    1.11 +this.EXPORTED_SYMBOLS = ["Home"];
    1.12 +
    1.13 +const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
    1.14 +
    1.15 +Cu.import("resource://gre/modules/Services.jsm");
    1.16 +Cu.import("resource://gre/modules/SharedPreferences.jsm");
    1.17 +Cu.import("resource://gre/modules/Messaging.jsm");
    1.18 +
    1.19 +// Keep this in sync with the constant defined in PanelAuthCache.java
    1.20 +const PREFS_PANEL_AUTH_PREFIX = "home_panels_auth_";
    1.21 +
    1.22 +// See bug 915424
    1.23 +function resolveGeckoURI(aURI) {
    1.24 +  if (!aURI)
    1.25 +    throw "Can't resolve an empty uri";
    1.26 +
    1.27 +  if (aURI.startsWith("chrome://")) {
    1.28 +    let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
    1.29 +    return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
    1.30 +  } else if (aURI.startsWith("resource://")) {
    1.31 +    let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
    1.32 +    return handler.resolveURI(Services.io.newURI(aURI, null, null));
    1.33 +  }
    1.34 +  return aURI;
    1.35 +}
    1.36 +
    1.37 +function BannerMessage(options) {
    1.38 +  let uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
    1.39 +  this.id = uuidgen.generateUUID().toString();
    1.40 +
    1.41 +  if ("text" in options && options.text != null)
    1.42 +    this.text = options.text;
    1.43 +
    1.44 +  if ("icon" in options && options.icon != null)
    1.45 +    this.iconURI = resolveGeckoURI(options.icon);
    1.46 +
    1.47 +  if ("onshown" in options && typeof options.onshown === "function")
    1.48 +    this.onshown = options.onshown;
    1.49 +
    1.50 +  if ("onclick" in options && typeof options.onclick === "function")
    1.51 +    this.onclick = options.onclick;
    1.52 +
    1.53 +  if ("ondismiss" in options && typeof options.ondismiss === "function")
    1.54 +    this.ondismiss = options.ondismiss;
    1.55 +}
    1.56 +
    1.57 +// We need this object to have access to the HomeBanner
    1.58 +// private members without leaking it outside Home.jsm.
    1.59 +let HomeBannerMessageHandlers;
    1.60 +
    1.61 +let HomeBanner = (function () {
    1.62 +  // Whether there is a "HomeBanner:Get" request we couldn't fulfill.
    1.63 +  let _pendingRequest = false;
    1.64 +
    1.65 +  // Functions used to handle messages sent from Java.
    1.66 +  HomeBannerMessageHandlers = {
    1.67 +    "HomeBanner:Get": function handleBannerGet(data) {
    1.68 +      if (!_sendBannerData()) {
    1.69 +        _pendingRequest = true;
    1.70 +      }
    1.71 +    }
    1.72 +  };
    1.73 +
    1.74 +  // Holds the messages that will rotate through the banner.
    1.75 +  let _messages = {};
    1.76 +
    1.77 +  let _sendBannerData = function() {
    1.78 +    let keys = Object.keys(_messages);
    1.79 +    if (!keys.length) {
    1.80 +      return false;
    1.81 +    }
    1.82 +
    1.83 +    // Choose a message at random.
    1.84 +    let randomId = keys[Math.floor(Math.random() * keys.length)];
    1.85 +    let message = _messages[randomId];
    1.86 +
    1.87 +    sendMessageToJava({
    1.88 +      type: "HomeBanner:Data",
    1.89 +      id: message.id,
    1.90 +      text: message.text,
    1.91 +      iconURI: message.iconURI
    1.92 +    });
    1.93 +    return true;
    1.94 +  };
    1.95 +
    1.96 +  let _handleShown = function(id) {
    1.97 +    let message = _messages[id];
    1.98 +    if (message.onshown)
    1.99 +      message.onshown();
   1.100 +  };
   1.101 +
   1.102 +  let _handleClick = function(id) {
   1.103 +    let message = _messages[id];
   1.104 +    if (message.onclick)
   1.105 +      message.onclick();
   1.106 +  };
   1.107 +
   1.108 +  let _handleDismiss = function(id) {
   1.109 +    let message = _messages[id];
   1.110 +    if (message.ondismiss)
   1.111 +      message.ondismiss();
   1.112 +  };
   1.113 +
   1.114 +  return Object.freeze({
   1.115 +    observe: function(subject, topic, data) {
   1.116 +      switch(topic) {
   1.117 +        case "HomeBanner:Shown":
   1.118 +          _handleShown(data);
   1.119 +          break;
   1.120 +
   1.121 +        case "HomeBanner:Click":
   1.122 +          _handleClick(data);
   1.123 +          break;
   1.124 +
   1.125 +        case "HomeBanner:Dismiss":
   1.126 +          _handleDismiss(data);
   1.127 +          break;
   1.128 +      }
   1.129 +    },
   1.130 +
   1.131 +    /**
   1.132 +     * Adds a new banner message to the rotation.
   1.133 +     *
   1.134 +     * @return id Unique identifer for the message.
   1.135 +     */
   1.136 +    add: function(options) {
   1.137 +      let message = new BannerMessage(options);
   1.138 +      _messages[message.id] = message;
   1.139 +
   1.140 +      // If this is the first message we're adding, add
   1.141 +      // observers to listen for requests from the Java UI.
   1.142 +      if (Object.keys(_messages).length == 1) {
   1.143 +        Services.obs.addObserver(this, "HomeBanner:Shown", false);
   1.144 +        Services.obs.addObserver(this, "HomeBanner:Click", false);
   1.145 +        Services.obs.addObserver(this, "HomeBanner:Dismiss", false);
   1.146 +
   1.147 +        // Send a message to Java if there's a pending "HomeBanner:Get" request.
   1.148 +        if (_pendingRequest) {
   1.149 +          _pendingRequest = false;
   1.150 +          _sendBannerData();
   1.151 +        }
   1.152 +      }
   1.153 +
   1.154 +      return message.id;
   1.155 +    },
   1.156 +
   1.157 +    /**
   1.158 +     * Removes a banner message from the rotation.
   1.159 +     *
   1.160 +     * @param id The id of the message to remove.
   1.161 +     */
   1.162 +    remove: function(id) {
   1.163 +      if (!(id in _messages)) {
   1.164 +        throw "Home.banner: Can't remove message that doesn't exist: id = " + id;
   1.165 +      }
   1.166 +
   1.167 +      delete _messages[id];
   1.168 +
   1.169 +      // If there are no more messages, remove the observers.
   1.170 +      if (Object.keys(_messages).length == 0) {
   1.171 +        Services.obs.removeObserver(this, "HomeBanner:Shown");
   1.172 +        Services.obs.removeObserver(this, "HomeBanner:Click");
   1.173 +        Services.obs.removeObserver(this, "HomeBanner:Dismiss");
   1.174 +      }
   1.175 +    }
   1.176 +  });
   1.177 +})();
   1.178 +
   1.179 +// We need this object to have access to the HomePanels
   1.180 +// private members without leaking it outside Home.jsm.
   1.181 +let HomePanelsMessageHandlers;
   1.182 +
   1.183 +let HomePanels = (function () {
   1.184 +  // Functions used to handle messages sent from Java.
   1.185 +  HomePanelsMessageHandlers = {
   1.186 +
   1.187 +    "HomePanels:Get": function handlePanelsGet(data) {
   1.188 +      data = JSON.parse(data);
   1.189 +
   1.190 +      let requestId = data.requestId;
   1.191 +      let ids = data.ids || null;
   1.192 +
   1.193 +      let panels = [];
   1.194 +      for (let id in _registeredPanels) {
   1.195 +        // Null ids means we want to fetch all available panels
   1.196 +        if (ids == null || ids.indexOf(id) >= 0) {
   1.197 +          try {
   1.198 +            panels.push(_generatePanel(id));
   1.199 +          } catch(e) {
   1.200 +            Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e);
   1.201 +          }
   1.202 +        }
   1.203 +      }
   1.204 +
   1.205 +      sendMessageToJava({
   1.206 +        type: "HomePanels:Data",
   1.207 +        panels: panels,
   1.208 +        requestId: requestId
   1.209 +      });
   1.210 +    },
   1.211 +
   1.212 +    "HomePanels:Authenticate": function handlePanelsAuthenticate(id) {
   1.213 +      // Generate panel options to get auth handler.
   1.214 +      let options = _registeredPanels[id]();
   1.215 +      if (!options.auth) {
   1.216 +        throw "Home.panels: Invalid auth for panel.id = " + id;
   1.217 +      }
   1.218 +      if (!options.auth.authenticate || typeof options.auth.authenticate !== "function") {
   1.219 +        throw "Home.panels: Invalid auth authenticate function: panel.id = " + this.id;
   1.220 +      }
   1.221 +      options.auth.authenticate();
   1.222 +    },
   1.223 +
   1.224 +    "HomePanels:RefreshView": function handlePanelsRefreshView(data) {
   1.225 +      data = JSON.parse(data);
   1.226 +
   1.227 +      let options = _registeredPanels[data.panelId]();
   1.228 +      let view = options.views[data.viewIndex];
   1.229 +
   1.230 +      if (!view) {
   1.231 +        throw "Home.panels: Invalid view for panel.id = " + data.panelId
   1.232 +            + ", view.index = " + data.viewIndex;
   1.233 +      }
   1.234 +
   1.235 +      if (!view.onrefresh || typeof view.onrefresh !== "function") {
   1.236 +        throw "Home.panels: Invalid onrefresh for panel.id = " + data.panelId
   1.237 +            + ", view.index = " + data.viewIndex;
   1.238 +      }
   1.239 +
   1.240 +      view.onrefresh();
   1.241 +    },
   1.242 +
   1.243 +    "HomePanels:Installed": function handlePanelsInstalled(id) {
   1.244 +      _assertPanelExists(id);
   1.245 +
   1.246 +      let options = _registeredPanels[id]();
   1.247 +      if (!options.oninstall) {
   1.248 +        return;
   1.249 +      }
   1.250 +      if (typeof options.oninstall !== "function") {
   1.251 +        throw "Home.panels: Invalid oninstall function: panel.id = " + this.id;
   1.252 +      }
   1.253 +      options.oninstall();
   1.254 +    },
   1.255 +
   1.256 +    "HomePanels:Uninstalled": function handlePanelsUninstalled(id) {
   1.257 +      _assertPanelExists(id);
   1.258 +
   1.259 +      let options = _registeredPanels[id]();
   1.260 +      if (!options.onuninstall) {
   1.261 +        return;
   1.262 +      }
   1.263 +      if (typeof options.onuninstall !== "function") {
   1.264 +        throw "Home.panels: Invalid onuninstall function: panel.id = " + this.id;
   1.265 +      }
   1.266 +      options.onuninstall();
   1.267 +    }
   1.268 +  };
   1.269 +
   1.270 +  // Holds the current set of registered panels that can be
   1.271 +  // installed, updated, uninstalled, or unregistered. It maps
   1.272 +  // panel ids with the functions that dynamically generate
   1.273 +  // their respective panel options. This is used to retrieve
   1.274 +  // the current list of available panels in the system.
   1.275 +  // See HomePanels:Get handler.
   1.276 +  let _registeredPanels = {};
   1.277 +
   1.278 +  // Valid layouts for a panel.
   1.279 +  let Layout = Object.freeze({
   1.280 +    FRAME: "frame"
   1.281 +  });
   1.282 +
   1.283 +  // Valid types of views for a dataset.
   1.284 +  let View = Object.freeze({
   1.285 +    LIST: "list",
   1.286 +    GRID: "grid"
   1.287 +  });
   1.288 +
   1.289 +  // Valid item types for a panel view.
   1.290 +  let Item = Object.freeze({
   1.291 +    ARTICLE: "article",
   1.292 +    IMAGE: "image"
   1.293 +  });
   1.294 +
   1.295 +  // Valid item handlers for a panel view.
   1.296 +  let ItemHandler = Object.freeze({
   1.297 +    BROWSER: "browser",
   1.298 +    INTENT: "intent"
   1.299 +  });
   1.300 +
   1.301 +  function Panel(id, options) {
   1.302 +    this.id = id;
   1.303 +    this.title = options.title;
   1.304 +    this.layout = options.layout;
   1.305 +    this.views = options.views;
   1.306 +
   1.307 +    if (!this.id || !this.title) {
   1.308 +      throw "Home.panels: Can't create a home panel without an id and title!";
   1.309 +    }
   1.310 +
   1.311 +    if (!this.layout) {
   1.312 +      // Use FRAME layout by default
   1.313 +      this.layout = Layout.FRAME;
   1.314 +    } else if (!_valueExists(Layout, this.layout)) {
   1.315 +      throw "Home.panels: Invalid layout for panel: panel.id = " + this.id + ", panel.layout =" + this.layout;
   1.316 +    }
   1.317 +
   1.318 +    for (let view of this.views) {
   1.319 +      if (!_valueExists(View, view.type)) {
   1.320 +        throw "Home.panels: Invalid view type: panel.id = " + this.id + ", view.type = " + view.type;
   1.321 +      }
   1.322 +
   1.323 +      if (!view.itemType) {
   1.324 +        if (view.type == View.LIST) {
   1.325 +          // Use ARTICLE item type by default in LIST views
   1.326 +          view.itemType = Item.ARTICLE;
   1.327 +        } else if (view.type == View.GRID) {
   1.328 +          // Use IMAGE item type by default in GRID views
   1.329 +          view.itemType = Item.IMAGE;
   1.330 +        }
   1.331 +      } else if (!_valueExists(Item, view.itemType)) {
   1.332 +        throw "Home.panels: Invalid item type: panel.id = " + this.id + ", view.itemType = " + view.itemType;
   1.333 +      }
   1.334 +
   1.335 +      if (!view.itemHandler) {
   1.336 +        // Use BROWSER item handler by default
   1.337 +        view.itemHandler = ItemHandler.BROWSER;
   1.338 +      } else if (!_valueExists(ItemHandler, view.itemHandler)) {
   1.339 +        throw "Home.panels: Invalid item handler: panel.id = " + this.id + ", view.itemHandler = " + view.itemHandler;
   1.340 +      }
   1.341 +
   1.342 +      if (!view.dataset) {
   1.343 +        throw "Home.panels: No dataset provided for view: panel.id = " + this.id + ", view.type = " + view.type;
   1.344 +      }
   1.345 +
   1.346 +      if (view.onrefresh) {
   1.347 +        view.refreshEnabled = true;
   1.348 +      }
   1.349 +    }
   1.350 +
   1.351 +    if (options.auth) {
   1.352 +      if (!options.auth.messageText) {
   1.353 +        throw "Home.panels: Invalid auth messageText: panel.id = " + this.id;
   1.354 +      }
   1.355 +      if (!options.auth.buttonText) {
   1.356 +        throw "Home.panels: Invalid auth buttonText: panel.id = " + this.id;
   1.357 +      }
   1.358 +
   1.359 +      this.authConfig = {
   1.360 +        messageText: options.auth.messageText,
   1.361 +        buttonText: options.auth.buttonText
   1.362 +      };
   1.363 +
   1.364 +      // Include optional image URL if it is specified.
   1.365 +      if (options.auth.imageUrl) {
   1.366 +        this.authConfig.imageUrl = options.auth.imageUrl;
   1.367 +      }
   1.368 +    }
   1.369 +  }
   1.370 +
   1.371 +  let _generatePanel = function(id) {
   1.372 +    let options = _registeredPanels[id]();
   1.373 +    return new Panel(id, options);
   1.374 +  };
   1.375 +
   1.376 +  // Helper function used to see if a value is in an object.
   1.377 +  let _valueExists = function(obj, value) {
   1.378 +    for (let key in obj) {
   1.379 +      if (obj[key] == value) {
   1.380 +        return true;
   1.381 +      }
   1.382 +    }
   1.383 +    return false;
   1.384 +  };
   1.385 +
   1.386 +  let _assertPanelExists = function(id) {
   1.387 +    if (!(id in _registeredPanels)) {
   1.388 +      throw "Home.panels: Panel doesn't exist: id = " + id;
   1.389 +    }
   1.390 +  };
   1.391 +
   1.392 +  return Object.freeze({
   1.393 +    Layout: Layout,
   1.394 +    View: View,
   1.395 +    Item: Item,
   1.396 +    ItemHandler: ItemHandler,
   1.397 +
   1.398 +    register: function(id, optionsCallback) {
   1.399 +      // Bail if the panel already exists
   1.400 +      if (id in _registeredPanels) {
   1.401 +        throw "Home.panels: Panel already exists: id = " + id;
   1.402 +      }
   1.403 +
   1.404 +      if (!optionsCallback || typeof optionsCallback !== "function") {
   1.405 +        throw "Home.panels: Panel callback must be a function: id = " + id;
   1.406 +      }
   1.407 +
   1.408 +      _registeredPanels[id] = optionsCallback;
   1.409 +    },
   1.410 +
   1.411 +    unregister: function(id) {
   1.412 +      _assertPanelExists(id);
   1.413 +
   1.414 +      delete _registeredPanels[id];
   1.415 +    },
   1.416 +
   1.417 +    install: function(id) {
   1.418 +      _assertPanelExists(id);
   1.419 +
   1.420 +      sendMessageToJava({
   1.421 +        type: "HomePanels:Install",
   1.422 +        panel: _generatePanel(id)
   1.423 +      });
   1.424 +    },
   1.425 +
   1.426 +    uninstall: function(id) {
   1.427 +      _assertPanelExists(id);
   1.428 +
   1.429 +      sendMessageToJava({
   1.430 +        type: "HomePanels:Uninstall",
   1.431 +        id: id
   1.432 +      });
   1.433 +    },
   1.434 +
   1.435 +    update: function(id) {
   1.436 +      _assertPanelExists(id);
   1.437 +
   1.438 +      sendMessageToJava({
   1.439 +        type: "HomePanels:Update",
   1.440 +        panel: _generatePanel(id)
   1.441 +      });
   1.442 +    },
   1.443 +
   1.444 +    setAuthenticated: function(id, isAuthenticated) {
   1.445 +      _assertPanelExists(id);
   1.446 +
   1.447 +      let authKey = PREFS_PANEL_AUTH_PREFIX + id;
   1.448 +      let sharedPrefs = new SharedPreferences();
   1.449 +      sharedPrefs.setBoolPref(authKey, isAuthenticated);
   1.450 +    }
   1.451 +  });
   1.452 +})();
   1.453 +
   1.454 +// Public API
   1.455 +this.Home = Object.freeze({
   1.456 +  banner: HomeBanner,
   1.457 +  panels: HomePanels,
   1.458 +
   1.459 +  // Lazy notification observer registered in browser.js
   1.460 +  observe: function(subject, topic, data) {
   1.461 +    if (topic in HomeBannerMessageHandlers) {
   1.462 +      HomeBannerMessageHandlers[topic](data);
   1.463 +    } else if (topic in HomePanelsMessageHandlers) {
   1.464 +      HomePanelsMessageHandlers[topic](data);
   1.465 +    } else {
   1.466 +      Cu.reportError("Home.observe: message handler not found for topic: " + topic);
   1.467 +    }
   1.468 +  }
   1.469 +});

mercurial