gecko-dev/dom/activities/ActivitiesService.jsm

546 строки
17 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict"
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
Cu.import("resource://gre/modules/AppsUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DOMApplicationRegistry",
"resource://gre/modules/Webapps.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ActivitiesServiceFilter",
"resource://gre/modules/ActivitiesServiceFilter.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
"@mozilla.org/parentprocessmessagemanager;1",
"nsIMessageBroadcaster");
XPCOMUtils.defineLazyServiceGetter(this, "NetUtil",
"@mozilla.org/network/util;1",
"nsINetUtil");
this.EXPORTED_SYMBOLS = [];
function debug(aMsg) {
//dump("-- ActivitiesService.jsm " + Date.now() + " " + aMsg + "\n");
}
const DB_NAME = "activities";
const DB_VERSION = 2;
const STORE_NAME = "activities";
function ActivitiesDb() {
}
ActivitiesDb.prototype = {
__proto__: IndexedDBHelper.prototype,
init: function actdb_init() {
this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME]);
},
/**
* Create the initial database schema.
*
* The schema of records stored is as follows:
*
* {
* id: String
* manifest: String
* name: String
* icon: String
* description: jsval
* }
*/
upgradeSchema: function actdb_upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
debug("Upgrade schema " + aOldVersion + " -> " + aNewVersion);
let self = this;
function upgrade(currentVersion) {
let next = upgrade.bind(self, currentVersion + 1);
switch (currentVersion) {
case 0:
self.createSchema(aDb, next);
break;
case 1:
self.upgradeSchemaVersion2(aDb, aTransaction, next);
break;
}
}
upgrade(aOldVersion);
},
createSchema: function(aDb, aNext) {
let objectStore = aDb.createObjectStore(STORE_NAME, { keyPath: "id" });
// indexes
objectStore.createIndex("name", "name", { unique: false });
objectStore.createIndex("manifest", "manifest", { unique: false });
debug("Created object stores and indexes");
aNext();
},
upgradeSchemaVersion2: function(aDb, aTransaction, aNext) {
debug("Upgrading DB to version 2");
// In order to be able to have multiple activities with same name
// but different descriptions, we need to update the keypath from
// a hash made from {manifest, name} to a hash made from {manifest,
// name, description}.
//
// Unfortunately, updating the keypath is not allowed by IDB, so we
// need to remove and recreate the activities object store.
let activities = [];
let objectStore = aTransaction.objectStore(STORE_NAME);
objectStore.openCursor().onsuccess = (event) => {
let cursor = event.target.result;
if (!cursor) {
aDb.deleteObjectStore(STORE_NAME);
let objectStore = aDb.createObjectStore(STORE_NAME, { keyPath: "id" });
// indexes
objectStore.createIndex("name", "name", { unique: false });
objectStore.createIndex("manifest", "manifest", { unique: false });
this.add(activities, () => {
debug("DB upgraded to version 2");
aNext();
}, () => {
dump("Error upgrading DB to version 2 " + error + "\n");
});
return;
}
let activity = cursor.value;
debug("Upgrading activity " + JSON.stringify(activity));
activity.id = this.createId(activity);
activities.push(activity);
cursor.continue();
};
},
// unique ids made of (uri, action)
createId: function actdb_createId(aObject) {
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.SHA1);
// add uri and action to the hash
["manifest", "name", "description"].forEach(function(aProp) {
if (!aObject[aProp]) {
return;
}
let property = aObject[aProp];
if (aProp == "description") {
property = JSON.stringify(aObject[aProp]);
}
let data = converter.convertToByteArray(property, {});
hasher.update(data, data.length);
});
return hasher.finish(true);
},
// Add all the activities carried in the |aObjects| array.
add: function actdb_add(aObjects, aSuccess, aError) {
this.newTxn("readwrite", STORE_NAME, function (txn, store) {
aObjects.forEach(function (aObject) {
let object = {
manifest: aObject.manifest,
name: aObject.name,
icon: aObject.icon || "",
description: aObject.description
};
object.id = this.createId(object);
debug("Going to add " + JSON.stringify(object));
store.put(object);
}, this);
}.bind(this), aSuccess, aError);
},
// Remove all the activities carried in the |aObjects| array.
remove: function actdb_remove(aObjects) {
this.newTxn("readwrite", STORE_NAME, (txn, store) => {
aObjects.forEach((aObject) => {
let object = {
manifest: aObject.manifest,
name: aObject.name,
description: aObject.description
};
debug("Going to remove " + JSON.stringify(object));
store.delete(this.createId(object));
});
}, function() {}, function() {});
},
// Remove all activities associated with the given |aManifest| URL.
removeAll: function actdb_removeAll(aManifest) {
this.newTxn("readwrite", STORE_NAME, function (txn, store) {
let index = store.index("manifest");
let request = index.mozGetAll(aManifest);
request.onsuccess = function manifestActivities(aEvent) {
aEvent.target.result.forEach(function(result) {
debug('Removing activity: ' + JSON.stringify(result));
store.delete(result.id);
});
};
});
},
find: function actdb_find(aObject, aSuccess, aError, aMatch) {
debug("Looking for " + aObject.options.name);
this.newTxn("readonly", STORE_NAME, function (txn, store) {
let index = store.index("name");
let request = index.mozGetAll(aObject.options.name);
request.onsuccess = function findSuccess(aEvent) {
debug("Request successful. Record count: " + aEvent.target.result.length);
if (!txn.result) {
txn.result = {
name: aObject.options.name,
options: []
};
}
aEvent.target.result.forEach(function(result) {
if (!aMatch(result))
return;
txn.result.options.push({
manifest: result.manifest,
icon: result.icon,
description: result.description
});
});
}
}.bind(this), aSuccess, aError);
}
}
let Activities = {
messages: [
// ActivityProxy.js
"Activity:Start",
// ActivityWrapper.js
"Activity:Ready",
// ActivityRequestHandler.js
"Activity:PostResult",
"Activity:PostError",
"Activities:Register",
"Activities:Unregister",
"Activities:UnregisterAll",
"Activities:GetContentTypes",
"child-process-shutdown"
],
init: function activities_init() {
this.messages.forEach(function(msgName) {
ppmm.addMessageListener(msgName, this);
}, this);
Services.obs.addObserver(this, "xpcom-shutdown", false);
this.db = new ActivitiesDb();
this.db.init();
this.callers = {};
},
observe: function activities_observe(aSubject, aTopic, aData) {
this.messages.forEach(function(msgName) {
ppmm.removeMessageListener(msgName, this);
}, this);
ppmm = null;
if (this.db) {
this.db.close();
this.db = null;
}
Services.obs.removeObserver(this, "xpcom-shutdown");
},
/**
* Starts an activity by doing:
* - finds a list of matching activities.
* - calls the UI glue to get the user choice.
* - fire an system message of type "activity" to this app, sending the
* activity data as a payload.
*/
startActivity: function activities_startActivity(aMsg) {
debug("StartActivity: " + JSON.stringify(aMsg));
// The caller app will be killed by |assertAppHasStatus| if it doesn't
// fit our permission requirement.
let callerApp = this.callers[aMsg.id].mm;
if (aMsg.options.name === 'internal-system-engineering-mode' &&
!callerApp.assertAppHasStatus(Ci.nsIPrincipal.APP_STATUS_CERTIFIED)) {
return;
}
let self = this;
let successCb = function successCb(aResults) {
debug(JSON.stringify(aResults));
function getActivityChoice(aResultType, aResult) {
switch(aResultType) {
case Ci.nsIActivityUIGlueCallback.NATIVE_ACTIVITY: {
self.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireSuccess", {
"id": aMsg.id,
"result": aResult
});
break;
}
case Ci.nsIActivityUIGlueCallback.WEBAPPS_ACTIVITY: {
debug("Activity choice: " + aResult);
// We have no matching activity registered, let's fire an error.
// Don't do this check until we have passed to UIGlue so the glue
// can choose to launch its own activity if needed.
if (aResults.options.length === 0) {
self.trySendAndCleanup(aMsg.id, "Activity:FireError", {
"id": aMsg.id,
"error": "NO_PROVIDER"
});
return;
}
// The user has cancelled the choice, fire an error.
if (aResult === -1) {
self.trySendAndCleanup(aMsg.id, "Activity:FireError", {
"id": aMsg.id,
"error": "ActivityCanceled"
});
return;
}
let sysmm = Cc["@mozilla.org/system-message-internal;1"]
.getService(Ci.nsISystemMessagesInternal);
if (!sysmm) {
// System message is not present, what should we do?
self.removeCaller(aMsg.id);
return;
}
debug("Sending system message...");
let result = aResults.options[aResult];
sysmm.sendMessage("activity", {
"id": aMsg.id,
"payload": aMsg.options,
"target": result.description
},
Services.io.newURI(result.description.href, null, null),
Services.io.newURI(result.manifest, null, null),
{
"manifestURL": self.callers[aMsg.id].manifestURL,
"pageURL": self.callers[aMsg.id].pageURL
});
if (!result.description.returnValue) {
// No need to notify observers, since we don't want the caller
// to be raised on the foreground that quick.
self.trySendAndCleanup(aMsg.id, "Activity:FireSuccess", {
"id": aMsg.id,
"result": null
});
}
break;
}
}
};
let caller = Activities.callers[aMsg.id];
if (aMsg.getFilterResults === true &&
caller.mm.assertAppHasStatus(Ci.nsIPrincipal.APP_STATUS_CERTIFIED)) {
// Certified apps can ask to just get the picker data.
// We want to return the manifest url, icon url and app name.
// The app name needs to be picked up from the localized manifest.
let reg = DOMApplicationRegistry;
let ids = aResults.options.map((aItem) => {
return { id: reg._appIdForManifestURL(aItem.manifest) }
});
reg._readManifests(ids).then((aManifests) => {
let results = [];
aManifests.forEach((aManifest, i) => {
let manifestURL = aResults.options[i].manifest;
// Not passing the origin is fine here since we only need
// helper.name which doesn't rely on url resolution.
let helper =
new ManifestHelper(aManifest.manifest, manifestURL, manifestURL);
results.push({
manifestURL: manifestURL,
iconURL: aResults.options[i].icon,
appName: helper.name
});
});
// Now fire success with the array of choices.
caller.mm.sendAsyncMessage("Activity:FireSuccess",
{
"id": aMsg.id,
"result": results
});
self.removeCaller(aMsg.id);
});
} else {
let glue = Cc["@mozilla.org/dom/activities/ui-glue;1"]
.createInstance(Ci.nsIActivityUIGlue);
glue.chooseActivity(aMsg.options, aResults.options, getActivityChoice);
}
};
let errorCb = function errorCb(aError) {
// Something unexpected happened. Should we send an error back?
debug("Error in startActivity: " + aError + "\n");
};
let matchFunc = function matchFunc(aResult) {
let calleeApp = DOMApplicationRegistry.getAppByManifestURL(aResult.manifest);
// Only allow certified apps to handle this special activity
if (aMsg.options.name === 'internal-system-engineering-mode' &&
calleeApp.appStatus !== Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
return false;
}
// If the activity is in the developer mode activity list, only let the
// system app be a provider.
let isSystemApp = false;
let isDevModeActivity = false;
try {
isSystemApp =
aResult.manifest == Services.prefs.getCharPref("b2g.system_manifest_url");
isDevModeActivity =
Services.prefs.getCharPref("dom.activities.developer_mode_only")
.split(",").indexOf(aMsg.options.name) !== -1;
} catch(e) {}
if (isDevModeActivity && !isSystemApp) {
return false;
}
return ActivitiesServiceFilter.match(aMsg.options.data,
aResult.description.filters);
};
this.db.find(aMsg, successCb, errorCb, matchFunc);
},
trySendAndCleanup: function activities_trySendAndCleanup(aId, aName, aPayload) {
try {
this.callers[aId].mm.sendAsyncMessage(aName, aPayload);
} finally {
this.removeCaller(aId);
}
},
receiveMessage: function activities_receiveMessage(aMessage) {
let mm = aMessage.target;
let msg = aMessage.json;
let caller;
let obsData;
if (aMessage.name == "Activity:PostResult" ||
aMessage.name == "Activity:PostError" ||
aMessage.name == "Activity:Ready") {
caller = this.callers[msg.id];
if (!caller) {
debug("!! caller is null for msg.id=" + msg.id);
return;
}
obsData = JSON.stringify({ manifestURL: caller.manifestURL,
pageURL: caller.pageURL,
success: aMessage.name == "Activity:PostResult" });
}
switch(aMessage.name) {
case "Activity:Start":
Services.obs.notifyObservers(null, "activity-opened", msg.childID);
this.callers[msg.id] = { mm: mm,
manifestURL: msg.manifestURL,
childID: msg.childID,
pageURL: msg.pageURL };
this.startActivity(msg);
break;
case "Activity:Ready":
caller.childMM = mm;
break;
case "Activity:PostResult":
this.trySendAndCleanup(msg.id, "Activity:FireSuccess", msg);
break;
case "Activity:PostError":
this.trySendAndCleanup(msg.id, "Activity:FireError", msg);
break;
case "Activities:Register":
this.db.add(msg,
function onSuccess(aEvent) {
debug("Activities:Register:OK");
Services.obs.notifyObservers(null, "new-activity-registered-success", null);
mm.sendAsyncMessage("Activities:Register:OK", null);
},
function onError(aEvent) {
msg.error = "REGISTER_ERROR";
debug("Activities:Register:KO");
Services.obs.notifyObservers(null, "new-activity-registered-failure", null);
mm.sendAsyncMessage("Activities:Register:KO", msg);
});
break;
case "Activities:Unregister":
this.db.remove(msg);
break;
case "Activities:UnregisterAll":
this.db.removeAll(msg);
break;
case "child-process-shutdown":
for (let id in this.callers) {
if (this.callers[id].childMM == mm) {
this.trySendAndCleanup(id, "Activity:FireError", {
"id": id,
"error": "ActivityCanceled"
});
break;
}
}
break;
}
},
removeCaller: function activities_removeCaller(id) {
Services.obs.notifyObservers(null, "activity-closed",
this.callers[id].childID);
delete this.callers[id];
}
}
Activities.init();