dom/activities/src/ActivitiesService.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/dom/activities/src/ActivitiesService.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,414 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict"
     1.9 +
    1.10 +const Cu = Components.utils;
    1.11 +const Cc = Components.classes;
    1.12 +const Ci = Components.interfaces;
    1.13 +
    1.14 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.15 +Cu.import("resource://gre/modules/Services.jsm");
    1.16 +Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
    1.17 +
    1.18 +XPCOMUtils.defineLazyModuleGetter(this, "ActivitiesServiceFilter",
    1.19 +  "resource://gre/modules/ActivitiesServiceFilter.jsm");
    1.20 +
    1.21 +XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
    1.22 +                                   "@mozilla.org/parentprocessmessagemanager;1",
    1.23 +                                   "nsIMessageBroadcaster");
    1.24 +
    1.25 +XPCOMUtils.defineLazyServiceGetter(this, "NetUtil",
    1.26 +                                   "@mozilla.org/network/util;1",
    1.27 +                                   "nsINetUtil");
    1.28 +
    1.29 +this.EXPORTED_SYMBOLS = [];
    1.30 +
    1.31 +function debug(aMsg) {
    1.32 +  //dump("-- ActivitiesService.jsm " + Date.now() + " " + aMsg + "\n");
    1.33 +}
    1.34 +
    1.35 +const DB_NAME    = "activities";
    1.36 +const DB_VERSION = 1;
    1.37 +const STORE_NAME = "activities";
    1.38 +
    1.39 +function ActivitiesDb() {
    1.40 +
    1.41 +}
    1.42 +
    1.43 +ActivitiesDb.prototype = {
    1.44 +  __proto__: IndexedDBHelper.prototype,
    1.45 +
    1.46 +  init: function actdb_init() {
    1.47 +    this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME]);
    1.48 +  },
    1.49 +
    1.50 +  /**
    1.51 +   * Create the initial database schema.
    1.52 +   *
    1.53 +   * The schema of records stored is as follows:
    1.54 +   *
    1.55 +   * {
    1.56 +   *  id:                  String
    1.57 +   *  manifest:            String
    1.58 +   *  name:                String
    1.59 +   *  icon:                String
    1.60 +   *  description:         jsval
    1.61 +   * }
    1.62 +   */
    1.63 +  upgradeSchema: function actdb_upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
    1.64 +    debug("Upgrade schema " + aOldVersion + " -> " + aNewVersion);
    1.65 +    let objectStore = aDb.createObjectStore(STORE_NAME, { keyPath: "id" });
    1.66 +
    1.67 +    // indexes
    1.68 +    objectStore.createIndex("name", "name", { unique: false });
    1.69 +    objectStore.createIndex("manifest", "manifest", { unique: false });
    1.70 +
    1.71 +    debug("Created object stores and indexes");
    1.72 +  },
    1.73 +
    1.74 +  // unique ids made of (uri, action)
    1.75 +  createId: function actdb_createId(aObject) {
    1.76 +    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
    1.77 +                      .createInstance(Ci.nsIScriptableUnicodeConverter);
    1.78 +    converter.charset = "UTF-8";
    1.79 +
    1.80 +    let hasher = Cc["@mozilla.org/security/hash;1"]
    1.81 +                   .createInstance(Ci.nsICryptoHash);
    1.82 +    hasher.init(hasher.SHA1);
    1.83 +
    1.84 +    // add uri and action to the hash
    1.85 +    ["manifest", "name"].forEach(function(aProp) {
    1.86 +      let data = converter.convertToByteArray(aObject[aProp], {});
    1.87 +      hasher.update(data, data.length);
    1.88 +    });
    1.89 +
    1.90 +    return hasher.finish(true);
    1.91 +  },
    1.92 +
    1.93 +  // Add all the activities carried in the |aObjects| array.
    1.94 +  add: function actdb_add(aObjects, aSuccess, aError) {
    1.95 +    this.newTxn("readwrite", STORE_NAME, function (txn, store) {
    1.96 +      aObjects.forEach(function (aObject) {
    1.97 +        let object = {
    1.98 +          manifest: aObject.manifest,
    1.99 +          name: aObject.name,
   1.100 +          icon: aObject.icon || "",
   1.101 +          description: aObject.description
   1.102 +        };
   1.103 +        object.id = this.createId(object);
   1.104 +        debug("Going to add " + JSON.stringify(object));
   1.105 +        store.put(object);
   1.106 +      }, this);
   1.107 +    }.bind(this), aSuccess, aError);
   1.108 +  },
   1.109 +
   1.110 +  // Remove all the activities carried in the |aObjects| array.
   1.111 +  remove: function actdb_remove(aObjects) {
   1.112 +    this.newTxn("readwrite", STORE_NAME, function (txn, store) {
   1.113 +      aObjects.forEach(function (aObject) {
   1.114 +        let object = {
   1.115 +          manifest: aObject.manifest,
   1.116 +          name: aObject.name
   1.117 +        };
   1.118 +        debug("Going to remove " + JSON.stringify(object));
   1.119 +        store.delete(this.createId(object));
   1.120 +      }, this);
   1.121 +    }.bind(this), function() {}, function() {});
   1.122 +  },
   1.123 +
   1.124 +  find: function actdb_find(aObject, aSuccess, aError, aMatch) {
   1.125 +    debug("Looking for " + aObject.options.name);
   1.126 +
   1.127 +    this.newTxn("readonly", STORE_NAME, function (txn, store) {
   1.128 +      let index = store.index("name");
   1.129 +      let request = index.mozGetAll(aObject.options.name);
   1.130 +      request.onsuccess = function findSuccess(aEvent) {
   1.131 +        debug("Request successful. Record count: " + aEvent.target.result.length);
   1.132 +        if (!txn.result) {
   1.133 +          txn.result = {
   1.134 +            name: aObject.options.name,
   1.135 +            options: []
   1.136 +          };
   1.137 +        }
   1.138 +
   1.139 +        aEvent.target.result.forEach(function(result) {
   1.140 +          if (!aMatch(result))
   1.141 +            return;
   1.142 +
   1.143 +          txn.result.options.push({
   1.144 +            manifest: result.manifest,
   1.145 +            icon: result.icon,
   1.146 +            description: result.description
   1.147 +          });
   1.148 +        });
   1.149 +      }
   1.150 +    }.bind(this), aSuccess, aError);
   1.151 +  }
   1.152 +}
   1.153 +
   1.154 +let Activities = {
   1.155 +  messages: [
   1.156 +    // ActivityProxy.js
   1.157 +    "Activity:Start",
   1.158 +
   1.159 +    // ActivityWrapper.js
   1.160 +    "Activity:Ready",
   1.161 +
   1.162 +    // ActivityRequestHandler.js
   1.163 +    "Activity:PostResult",
   1.164 +    "Activity:PostError",
   1.165 +
   1.166 +    "Activities:Register",
   1.167 +    "Activities:Unregister",
   1.168 +    "Activities:GetContentTypes",
   1.169 +
   1.170 +    "child-process-shutdown"
   1.171 +  ],
   1.172 +
   1.173 +  init: function activities_init() {
   1.174 +    this.messages.forEach(function(msgName) {
   1.175 +      ppmm.addMessageListener(msgName, this);
   1.176 +    }, this);
   1.177 +
   1.178 +    Services.obs.addObserver(this, "xpcom-shutdown", false);
   1.179 +
   1.180 +    this.db = new ActivitiesDb();
   1.181 +    this.db.init();
   1.182 +    this.callers = {};
   1.183 +  },
   1.184 +
   1.185 +  observe: function activities_observe(aSubject, aTopic, aData) {
   1.186 +    this.messages.forEach(function(msgName) {
   1.187 +      ppmm.removeMessageListener(msgName, this);
   1.188 +    }, this);
   1.189 +    ppmm = null;
   1.190 +
   1.191 +    if (this.db) {
   1.192 +      this.db.close();
   1.193 +      this.db = null;
   1.194 +    }
   1.195 +
   1.196 +    Services.obs.removeObserver(this, "xpcom-shutdown");
   1.197 +  },
   1.198 +
   1.199 +  /**
   1.200 +    * Starts an activity by doing:
   1.201 +    * - finds a list of matching activities.
   1.202 +    * - calls the UI glue to get the user choice.
   1.203 +    * - fire an system message of type "activity" to this app, sending the
   1.204 +    *   activity data as a payload.
   1.205 +    */
   1.206 +  startActivity: function activities_startActivity(aMsg) {
   1.207 +    debug("StartActivity: " + JSON.stringify(aMsg));
   1.208 +
   1.209 +    let successCb = function successCb(aResults) {
   1.210 +      debug(JSON.stringify(aResults));
   1.211 +
   1.212 +      // We have no matching activity registered, let's fire an error.
   1.213 +      if (aResults.options.length === 0) {
   1.214 +        Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireError", {
   1.215 +          "id": aMsg.id,
   1.216 +          "error": "NO_PROVIDER"
   1.217 +        });
   1.218 +        delete Activities.callers[aMsg.id];
   1.219 +        return;
   1.220 +      }
   1.221 +
   1.222 +      function getActivityChoice(aChoice) {
   1.223 +        debug("Activity choice: " + aChoice);
   1.224 +
   1.225 +        // The user has cancelled the choice, fire an error.
   1.226 +        if (aChoice === -1) {
   1.227 +          Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireError", {
   1.228 +            "id": aMsg.id,
   1.229 +            "error": "ActivityCanceled"
   1.230 +          });
   1.231 +          delete Activities.callers[aMsg.id];
   1.232 +          return;
   1.233 +        }
   1.234 +
   1.235 +        let sysmm = Cc["@mozilla.org/system-message-internal;1"]
   1.236 +                      .getService(Ci.nsISystemMessagesInternal);
   1.237 +        if (!sysmm) {
   1.238 +          // System message is not present, what should we do?
   1.239 +          delete Activities.callers[aMsg.id];
   1.240 +          return;
   1.241 +        }
   1.242 +
   1.243 +        debug("Sending system message...");
   1.244 +        let result = aResults.options[aChoice];
   1.245 +        sysmm.sendMessage("activity", {
   1.246 +            "id": aMsg.id,
   1.247 +            "payload": aMsg.options,
   1.248 +            "target": result.description
   1.249 +          },
   1.250 +          Services.io.newURI(result.description.href, null, null),
   1.251 +          Services.io.newURI(result.manifest, null, null),
   1.252 +          {
   1.253 +            "manifestURL": Activities.callers[aMsg.id].manifestURL,
   1.254 +            "pageURL": Activities.callers[aMsg.id].pageURL
   1.255 +          });
   1.256 +
   1.257 +        if (!result.description.returnValue) {
   1.258 +          Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireSuccess", {
   1.259 +            "id": aMsg.id,
   1.260 +            "result": null
   1.261 +          });
   1.262 +          // No need to notify observers, since we don't want the caller
   1.263 +          // to be raised on the foreground that quick.
   1.264 +          delete Activities.callers[aMsg.id];
   1.265 +        }
   1.266 +      };
   1.267 +
   1.268 +      let glue = Cc["@mozilla.org/dom/activities/ui-glue;1"]
   1.269 +                   .createInstance(Ci.nsIActivityUIGlue);
   1.270 +      glue.chooseActivity(aResults.name, aResults.options, getActivityChoice);
   1.271 +    };
   1.272 +
   1.273 +    let errorCb = function errorCb(aError) {
   1.274 +      // Something unexpected happened. Should we send an error back?
   1.275 +      debug("Error in startActivity: " + aError + "\n");
   1.276 +    };
   1.277 +
   1.278 +    let matchFunc = function matchFunc(aResult) {
   1.279 +      return ActivitiesServiceFilter.match(aMsg.options.data,
   1.280 +                                           aResult.description.filters);
   1.281 +    };
   1.282 +
   1.283 +    this.db.find(aMsg, successCb, errorCb, matchFunc);
   1.284 +  },
   1.285 +
   1.286 +  receiveMessage: function activities_receiveMessage(aMessage) {
   1.287 +    let mm = aMessage.target;
   1.288 +    let msg = aMessage.json;
   1.289 +
   1.290 +    let caller;
   1.291 +    let obsData;
   1.292 +
   1.293 +    if (aMessage.name == "Activity:PostResult" ||
   1.294 +        aMessage.name == "Activity:PostError" ||
   1.295 +        aMessage.name == "Activity:Ready") {
   1.296 +      caller = this.callers[msg.id];
   1.297 +      if (!caller) {
   1.298 +        debug("!! caller is null for msg.id=" + msg.id);
   1.299 +        return;
   1.300 +      }
   1.301 +      obsData = JSON.stringify({ manifestURL: caller.manifestURL,
   1.302 +                                 pageURL: caller.pageURL,
   1.303 +                                 success: aMessage.name == "Activity:PostResult" });
   1.304 +    }
   1.305 +
   1.306 +    switch(aMessage.name) {
   1.307 +      case "Activity:Start":
   1.308 +        this.callers[msg.id] = { mm: mm,
   1.309 +                                 manifestURL: msg.manifestURL,
   1.310 +                                 pageURL: msg.pageURL };
   1.311 +        this.startActivity(msg);
   1.312 +        break;
   1.313 +
   1.314 +      case "Activity:Ready":
   1.315 +        caller.childMM = mm;
   1.316 +        break;
   1.317 +
   1.318 +      case "Activity:PostResult":
   1.319 +        caller.mm.sendAsyncMessage("Activity:FireSuccess", msg);
   1.320 +        delete this.callers[msg.id];
   1.321 +        break;
   1.322 +      case "Activity:PostError":
   1.323 +        caller.mm.sendAsyncMessage("Activity:FireError", msg);
   1.324 +        delete this.callers[msg.id];
   1.325 +        break;
   1.326 +
   1.327 +      case "Activities:Register":
   1.328 +        let self = this;
   1.329 +        this.db.add(msg,
   1.330 +          function onSuccess(aEvent) {
   1.331 +            mm.sendAsyncMessage("Activities:Register:OK", null);
   1.332 +            let res = [];
   1.333 +            msg.forEach(function(aActivity) {
   1.334 +              self.updateContentTypeList(aActivity, res);
   1.335 +            });
   1.336 +            if (res.length) {
   1.337 +              ppmm.broadcastAsyncMessage("Activities:RegisterContentTypes",
   1.338 +                                         { contentTypes: res });
   1.339 +            }
   1.340 +          },
   1.341 +          function onError(aEvent) {
   1.342 +            msg.error = "REGISTER_ERROR";
   1.343 +            mm.sendAsyncMessage("Activities:Register:KO", msg);
   1.344 +          });
   1.345 +        break;
   1.346 +      case "Activities:Unregister":
   1.347 +        this.db.remove(msg);
   1.348 +        let res = [];
   1.349 +        msg.forEach(function(aActivity) {
   1.350 +          this.updateContentTypeList(aActivity, res);
   1.351 +        }, this);
   1.352 +        if (res.length) {
   1.353 +          ppmm.broadcastAsyncMessage("Activities:UnregisterContentTypes",
   1.354 +                                     { contentTypes: res });
   1.355 +        }
   1.356 +        break;
   1.357 +      case "Activities:GetContentTypes":
   1.358 +        this.sendContentTypes(mm);
   1.359 +        break;
   1.360 +      case "child-process-shutdown":
   1.361 +        for (let id in this.callers) {
   1.362 +          if (this.callers[id].childMM == mm) {
   1.363 +            this.callers[id].mm.sendAsyncMessage("Activity:FireError", {
   1.364 +              "id": id,
   1.365 +              "error": "ActivityCanceled"
   1.366 +            });
   1.367 +            delete this.callers[id];
   1.368 +            break;
   1.369 +          }
   1.370 +        }
   1.371 +        break;
   1.372 +    }
   1.373 +  },
   1.374 +
   1.375 +  updateContentTypeList: function updateContentTypeList(aActivity, aResult) {
   1.376 +    // Bail out if this is not a "view" activity.
   1.377 +    if (aActivity.name != "view") {
   1.378 +      return;
   1.379 +    }
   1.380 +
   1.381 +    let types = aActivity.description.filters.type;
   1.382 +    if (typeof types == "string") {
   1.383 +      types = [types];
   1.384 +    }
   1.385 +
   1.386 +    // Check that this is a real content type and sanitize it.
   1.387 +    types.forEach(function(aContentType) {
   1.388 +      let hadCharset = { };
   1.389 +      let charset = { };
   1.390 +      let contentType =
   1.391 +        NetUtil.parseContentType(aContentType, charset, hadCharset);
   1.392 +      if (contentType) {
   1.393 +        aResult.push(contentType);
   1.394 +      }
   1.395 +    });
   1.396 +  },
   1.397 +
   1.398 +  sendContentTypes: function sendContentTypes(aMm) {
   1.399 +    let res = [];
   1.400 +    let self = this;
   1.401 +    this.db.find({ options: { name: "view" } },
   1.402 +      function() { // Success callback.
   1.403 +        if (res.length) {
   1.404 +          aMm.sendAsyncMessage("Activities:RegisterContentTypes",
   1.405 +                               { contentTypes: res });
   1.406 +        }
   1.407 +      },
   1.408 +      null, // Error callback.
   1.409 +      function(aActivity) { // Matching callback.
   1.410 +        self.updateContentTypeList(aActivity, res)
   1.411 +        return false;
   1.412 +      }
   1.413 +    );
   1.414 +  }
   1.415 +}
   1.416 +
   1.417 +Activities.init();

mercurial