bug 934760 - implement synthetic APK update flow; r=wesj

This commit is contained in:
Myk Melez 2014-02-07 23:50:13 -08:00
parent df7c3c109f
commit e3e1f9ec39
18 changed files with 527 additions and 48 deletions

View File

@ -824,6 +824,16 @@ pref("browser.snippets.syncPromo.enabled", false);
// The URL of the APK factory from which we obtain APKs for webapps.
// This currently points to the development server.
pref("browser.webapps.apkFactoryUrl", "http://dapk.net/application.apk");
// How frequently to check for webapp updates, in seconds (86400 is daily).
pref("browser.webapps.updateInterval", 86400);
// The URL of the service that checks for updates.
// This currently points to the development server.
// To test updates, set this to http://apk-update-checker.paas.allizom.org,
// which is a test server that always reports all apps as having updates.
pref("browser.webapps.updateCheckUrl", "http://dapk.net/app_updates");
#endif
// Whether or not to only sync home provider data when the user is on wifi.

View File

@ -639,13 +639,6 @@ public abstract class GeckoApp
final String title = message.getString("title");
final String type = message.getString("shortcutType");
GeckoAppShell.removeShortcut(title, url, origin, type);
} else if (!AppConstants.MOZ_ANDROID_SYNTHAPKS && event.equals("WebApps:PreInstall")) {
String name = message.getString("name");
String manifestURL = message.getString("manifestURL");
String origin = message.getString("origin");
// preInstallWebapp will return a File object pointing to the profile directory of the webapp
mCurrentResponse = EventListener.preInstallWebApp(name, manifestURL, origin).toString();
} else if (event.equals("Share:Text")) {
String text = message.getString("text");
GeckoAppShell.openUriExternal(text, "text/plain", "", "", Intent.ACTION_SEND, "");
@ -1554,7 +1547,6 @@ public abstract class GeckoApp
registerEventListener("Locale:Set");
registerEventListener("NativeApp:IsDebuggable");
registerEventListener("SystemUI:Visibility");
registerEventListener("WebApps:PreInstall");
EventListener.registerEvents();
@ -2080,7 +2072,6 @@ public abstract class GeckoApp
unregisterEventListener("Locale:Set");
unregisterEventListener("NativeApp:IsDebuggable");
unregisterEventListener("SystemUI:Visibility");
unregisterEventListener("WebApps:PreInstall");
EventListener.unregisterEvents();

View File

@ -42,6 +42,7 @@ public final class NotificationHelper implements GeckoEventListener {
// Attributes that can be used while sending a notification from js.
private static final String PROGRESS_VALUE_ATTR = "progress_value";
private static final String PROGRESS_MAX_ATTR = "progress_max";
private static final String PROGRESS_INDETERMINATE_ATTR = "progress_indeterminate";
private static final String LIGHT_ATTR = "light";
private static final String ONGOING_ATTR = "ongoing";
private static final String WHEN_ATTR = "when";
@ -253,11 +254,13 @@ public final class NotificationHelper implements GeckoEventListener {
}
if (message.has(PROGRESS_VALUE_ATTR) &&
message.has(PROGRESS_MAX_ATTR)) {
message.has(PROGRESS_MAX_ATTR) &&
message.has(PROGRESS_INDETERMINATE_ATTR)) {
try {
final int progress = message.getInt(PROGRESS_VALUE_ATTR);
final int progressMax = message.getInt(PROGRESS_MAX_ATTR);
builder.setProgress(progressMax, progress, false);
final boolean progressIndeterminate = message.getBoolean(PROGRESS_INDETERMINATE_ATTR);
builder.setProgress(progressMax, progress, progressIndeterminate);
} catch (JSONException ex) {
Log.i(LOGTAG, "Error parsing", ex);
}

View File

@ -13,6 +13,7 @@ import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.util.ActivityResultHandler;
import org.mozilla.gecko.util.EventDispatcher;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.GeckoEventResponder;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.WebAppAllocator;
@ -21,24 +22,30 @@ import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Bitmap;
import android.net.Uri;
import android.util.Log;
import java.io.File;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class EventListener implements GeckoEventListener {
public class EventListener implements GeckoEventListener, GeckoEventResponder {
private static final String LOGTAG = "GeckoWebAppEventListener";
private EventListener() { }
private static EventListener mEventListener;
private String mCurrentResponse = "";
private static EventListener getEventListener() {
if (mEventListener == null) {
@ -61,6 +68,7 @@ public class EventListener implements GeckoEventListener {
registerEventListener("WebApps:PostInstall");
registerEventListener("WebApps:Open");
registerEventListener("WebApps:Uninstall");
registerEventListener("WebApps:GetApkVersions");
}
public static void unregisterEvents() {
@ -69,6 +77,7 @@ public class EventListener implements GeckoEventListener {
unregisterEventListener("WebApps:PostInstall");
unregisterEventListener("WebApps:Open");
unregisterEventListener("WebApps:Uninstall");
unregisterEventListener("WebApps:GetApkVersions");
}
@Override
@ -96,12 +105,28 @@ public class EventListener implements GeckoEventListener {
GeckoAppShell.getGeckoInterface().getActivity().startActivity(intent);
} else if (!AppConstants.MOZ_ANDROID_SYNTHAPKS && event.equals("WebApps:Uninstall")) {
uninstallWebApp(message.getString("origin"));
} else if (!AppConstants.MOZ_ANDROID_SYNTHAPKS && event.equals("WebApps:PreInstall")) {
String name = message.getString("name");
String manifestURL = message.getString("manifestURL");
String origin = message.getString("origin");
// preInstallWebapp will return a File object pointing to the profile directory of the webapp
mCurrentResponse = preInstallWebApp(name, manifestURL, origin).toString();
} else if (event.equals("WebApps:GetApkVersions")) {
mCurrentResponse = getApkVersions(GeckoAppShell.getGeckoInterface().getActivity(),
message.getJSONArray("packageNames")).toString();
}
} catch (Exception e) {
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
}
}
public String getResponse(JSONObject origMessage) {
String res = mCurrentResponse;
mCurrentResponse = "";
return res;
}
// Not used by MOZ_ANDROID_SYNTHAPKS.
public static File preInstallWebApp(String aTitle, String aURI, String aOrigin) {
int index = WebAppAllocator.getInstance(GeckoAppShell.getContext()).findAndAllocateIndex(aOrigin, aTitle, (String) null);
@ -192,11 +217,17 @@ public class EventListener implements GeckoEventListener {
filter.addDataScheme("package");
context.registerReceiver(receiver, filter);
// Now call the package installer.
File file = new File(filePath);
if (!file.exists()) {
Log.wtf(LOGTAG, "APK file doesn't exist at path " + filePath);
// TODO: propagate the error back to the mozApps.install caller.
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
// Now call the package installer.
GeckoAppShell.sActivityHelper.startIntentForActivity(context, intent, new ActivityResultHandler() {
@Override
public void onActivityResult(int resultCode, Intent data) {
@ -217,4 +248,40 @@ public class EventListener implements GeckoEventListener {
}
});
}
private static final int DEFAULT_VERSION_CODE = -1;
public static JSONObject getApkVersions(Activity context, JSONArray packageNames) {
Set<String> packageNameSet = new HashSet<String>();
for (int i = 0; i < packageNames.length(); i++) {
try {
packageNameSet.add(packageNames.getString(i));
} catch (JSONException e) {
Log.w(LOGTAG, "exception populating settings item", e);
}
}
final PackageManager pm = context.getPackageManager();
List<ApplicationInfo> apps = pm.getInstalledApplications(0);
JSONObject jsonMessage = new JSONObject();
for (ApplicationInfo app : apps) {
if (packageNameSet.contains(app.packageName)) {
int versionCode = DEFAULT_VERSION_CODE;
try {
versionCode = pm.getPackageInfo(app.packageName, 0).versionCode;
} catch (PackageManager.NameNotFoundException e) {
Log.e(LOGTAG, "couldn't get version for app " + app.packageName, e);
}
try {
jsonMessage.put(app.packageName, versionCode);
} catch (JSONException e) {
Log.e(LOGTAG, "unable to store version code field for app " + app.packageName, e);
}
}
}
return jsonMessage;
}
}

View File

@ -56,8 +56,11 @@ public class InstallListener extends BroadcastReceiver {
Log.i(LOGTAG, "No manifest URL present in metadata");
return;
} else if (!isCorrectManifest(manifestUrl)) {
Log.i(LOGTAG, "Waiting to finish installing " + mManifestUrl + " but this is " +manifestUrl);
//return;
// This happens when the updater triggers installation of multiple
// APK updates simultaneously. If we're the receiver for another
// update, then simply ignore this intent by returning early.
Log.i(LOGTAG, "Manifest URL is for a different install; ignoring");
return;
}
if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {

View File

@ -32,6 +32,11 @@ public class UninstallListener extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
Log.i(LOGTAG, "Package is being replaced; ignoring removal intent");
return;
}
String packageName = intent.getData().getSchemeSpecificPart();
if (TextUtils.isEmpty(packageName)) {

View File

@ -12,6 +12,8 @@ Cu.import("resource://gre/modules/Services.jsm")
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/AppsUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebappManager", "resource://gre/modules/WebappManager.jsm");
const DEFAULT_ICON = "chrome://browser/skin/images/default-app-icon.png";
let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutApps.properties");
@ -41,6 +43,10 @@ function openLink(aEvent) {
} catch (ex) {}
}
function checkForUpdates(aEvent) {
WebappManager.checkForUpdates(true);
}
#ifndef MOZ_ANDROID_SYNTHAPKS
var ContextMenus = {
target: null,
@ -87,6 +93,8 @@ function onLoad(aEvent) {
elmts[i].addEventListener("click", openLink, false);
}
document.getElementById("update-item").addEventListener("click", checkForUpdates, false);
navigator.mozApps.mgmt.oninstall = onInstall;
navigator.mozApps.mgmt.onuninstall = onUninstall;
updateList();

View File

@ -55,5 +55,12 @@
<div id="browse-title" class="title">&aboutApps.browseMarketplace;</div>
</div>
</div>
<div class="list-item" id="update-item" role="button">
<img class="icon" src="chrome://browser/skin/images/update.png" />
<div class="inner">
<div id="browse-title" class="title">&aboutApps.checkForUpdates;</div>
</div>
</div>
</body>
</html>

View File

@ -106,6 +106,11 @@ contract @mozilla.org/snippets;1 {a78d7e59-b558-4321-a3d6-dffe2f1e76dd}
category profile-after-change Snippets @mozilla.org/snippets;1
category update-timer Snippets @mozilla.org/snippets;1,getService,snippets-update-timer,browser.snippets.updateInterval,86400
# WebappsUpdateTimer.js
component {8f7002cb-e959-4f0a-a2e8-563232564385} WebappsUpdateTimer.js
contract @mozilla.org/b2g/webapps-update-timer;1 {8f7002cb-e959-4f0a-a2e8-563232564385}
category update-timer WebappsUpdateTimer @mozilla.org/b2g/webapps-update-timer;1,getService,background-update-timer,browser.webapps.updateInterval,86400
# ColorPicker.js
component {430b987f-bb9f-46a3-99a5-241749220b29} ColorPicker.js
contract @mozilla.org/colorpicker;1 {430b987f-bb9f-46a3-99a5-241749220b29}

View File

@ -0,0 +1,59 @@
/* 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/. */
/**
* This component triggers a periodic webapp update check.
*/
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/WebappManager.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
function log(message) {
// We use *dump* instead of Services.console.logStringMessage so the messages
// have the INFO level of severity instead of the ERROR level. And we don't
// append a newline character to the end of the message because *dump* spills
// into the Android native logging system, which strips newlines from messages
// and breaks messages into lines automatically at display time (i.e. logcat).
dump(message);
}
function WebappsUpdateTimer() {}
WebappsUpdateTimer.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback,
Ci.nsISupportsWeakReference]),
classID: Components.ID("{8f7002cb-e959-4f0a-a2e8-563232564385}"),
notify: function(aTimer) {
// If we are offline, wait to be online to start the update check.
if (Services.io.offline) {
log("network offline for webapp update check; waiting");
Services.obs.addObserver(this, "network:offline-status-changed", true);
return;
}
log("periodic check for webapp updates");
WebappManager.checkForUpdates();
},
observe: function(aSubject, aTopic, aData) {
if (aTopic !== "network:offline-status-changed" || aData !== "online") {
return;
}
log("network back online for webapp update check; commencing");
// TODO: observe pref to do this only on wifi.
Services.obs.removeObserver(this, "network:offline-status-changed");
WebappManager.checkForUpdates();
}
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebappsUpdateTimer]);

View File

@ -24,6 +24,7 @@ EXTRA_COMPONENTS += [
'PromptService.js',
'SiteSpecificUserAgent.js',
'Snippets.js',
'WebappsUpdateTimer.js',
'XPIDialogService.js',
]

View File

@ -8,3 +8,4 @@
<!ENTITY aboutApps.browseMarketplace "Browse the Firefox Marketplace">
<!ENTITY aboutApps.uninstall "Uninstall">
<!ENTITY aboutApps.addToHomescreen "Add to Home Screen">
<!ENTITY aboutApps.checkForUpdates "Check for Updates">

View File

@ -34,6 +34,9 @@
locale/@AB_CD@/browser/phishing.dtd (%chrome/phishing.dtd)
locale/@AB_CD@/browser/payments.properties (%chrome/payments.properties)
locale/@AB_CD@/browser/handling.properties (%chrome/handling.properties)
#ifdef MOZ_ANDROID_SYNTHAPKS
locale/@AB_CD@/browser/webapp.properties (%chrome/webapp.properties)
#endif
# overrides for toolkit l10n, also for en-US
relativesrcdir toolkit/locales:

View File

@ -103,6 +103,11 @@ Notification.prototype = {
if (this._progress) {
msg.progress_value = this._progress;
msg.progress_max = 100;
msg.progress_indeterminate = false;
} else if (Number.isNaN(this._progress)) {
msg.progress_value = 0;
msg.progress_max = 0;
msg.progress_indeterminate = true;
}
if (this._priority)

View File

@ -8,6 +8,8 @@ this.EXPORTED_SYMBOLS = ["WebappManager"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
const UPDATE_URL_PREF = "browser.webapps.updateCheckUrl";
Cu.import("resource://gre/modules/AppsUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
@ -16,9 +18,23 @@ Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
Cu.import("resource://gre/modules/Webapps.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/PluralForm.jsm");
function dump(a) {
Services.console.logStringMessage("* * WebappManager.jsm: " + a);
XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
XPCOMUtils.defineLazyGetter(this, "Strings", function() {
return Services.strings.createBundle("chrome://browser/locale/webapp.properties");
});
function log(message) {
// We use *dump* instead of Services.console.logStringMessage so the messages
// have the INFO level of severity instead of the ERROR level. And we don't
// append a newline character to the end of the message because *dump* spills
// into the Android native logging system, which strips newlines from messages
// and breaks messages into lines automatically at display time (i.e. logcat).
dump(message);
}
function sendMessageToJava(aMessage) {
@ -43,7 +59,7 @@ this.WebappManager = {
return;
}
this._downloadApk(aMessage, aMessageManager);
this._installApk(aMessage, aMessageManager);
},
installPackage: function(aMessage, aMessageManager) {
@ -53,12 +69,31 @@ this.WebappManager = {
return;
}
this._downloadApk(aMessage, aMessageManager);
this._installApk(aMessage, aMessageManager);
},
_downloadApk: function(aMsg, aMessageManager) {
let manifestUrl = aMsg.app.manifestURL;
dump("_downloadApk for " + manifestUrl);
_installApk: function(aMessage, aMessageManager) { return Task.spawn((function*() {
let filePath;
try {
filePath = yield this._downloadApk(aMessage.app.manifestURL);
} catch(ex) {
aMessage.error = ex;
aMessageManager.sendAsyncMessage("Webapps:Install:Return:KO", aMessage);
log("error downloading APK: " + ex);
return;
}
sendMessageToJava({
type: "WebApps:InstallApk",
filePath: filePath,
data: JSON.stringify(aMessage),
});
}).bind(this)); },
_downloadApk: function(aManifestUrl) {
log("_downloadApk for " + aManifestUrl);
let deferred = Promise.defer();
// Get the endpoint URL and convert it to an nsIURI/nsIURL object.
const GENERATOR_URL_PREF = "browser.webapps.apkFactoryUrl";
@ -67,19 +102,19 @@ this.WebappManager = {
// Populate the query part of the URL with the manifest URL parameter.
let params = {
manifestUrl: manifestUrl,
manifestUrl: aManifestUrl,
};
generatorUrl.query =
[p + "=" + encodeURIComponent(params[p]) for (p in params)].join("&");
dump("downloading APK from " + generatorUrl.spec);
log("downloading APK from " + generatorUrl.spec);
let file = Cc["@mozilla.org/download-manager;1"].
getService(Ci.nsIDownloadManager).
defaultDownloadsDirectory.
clone();
file.append(manifestUrl.replace(/[^a-zA-Z0-9]/gi, "") + ".apk");
file.append(aManifestUrl.replace(/[^a-zA-Z0-9]/gi, "") + ".apk");
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
dump("downloading APK to " + file.path);
log("downloading APK to " + file.path);
let worker = new ChromeWorker("resource://gre/modules/WebappManagerWorker.js");
worker.onmessage = function(event) {
@ -88,20 +123,17 @@ this.WebappManager = {
worker.terminate();
if (type == "success") {
sendMessageToJava({
type: "WebApps:InstallApk",
filePath: file.path,
data: JSON.stringify(aMsg),
});
deferred.resolve(file.path);
} else { // type == "failure"
aMsg.error = message;
aMessageManager.sendAsyncMessage("Webapps:Install:Return:KO", aMsg);
dump("error downloading APK: " + message);
log("error downloading APK: " + message);
deferred.reject(message);
}
}
// Trigger the download.
worker.postMessage({ url: generatorUrl.spec, path: file.path });
return deferred.promise;
},
askInstall: function(aData) {
@ -115,7 +147,7 @@ this.WebappManager = {
// when we trigger the native install dialog and doesn't re-init itself
// afterward (TODO: file bug about this behavior).
if ("appcache_path" in aData.app.manifest) {
dump("deleting appcache_path from manifest: " + aData.app.manifest.appcache_path);
log("deleting appcache_path from manifest: " + aData.app.manifest.appcache_path);
delete aData.app.manifest.appcache_path;
}
@ -136,7 +168,7 @@ this.WebappManager = {
},
launch: function({ manifestURL, origin }) {
dump("launchWebapp: " + manifestURL);
log("launchWebapp: " + manifestURL);
sendMessageToJava({
type: "WebApps:Open",
@ -146,7 +178,7 @@ this.WebappManager = {
},
uninstall: function(aData) {
dump("uninstall: " + aData.manifestURL);
log("uninstall: " + aData.manifestURL);
if (this._testing) {
// We don't have to do anything, as the registry does all the work.
@ -157,10 +189,17 @@ this.WebappManager = {
},
autoInstall: function(aData) {
let oldApp = DOMApplicationRegistry.getAppByManifestURL(aData.manifestUrl);
if (oldApp) {
// If the app is already installed, update the existing installation.
this._autoUpdate(aData, oldApp);
return;
}
let mm = {
sendAsyncMessage: function (aMessageName, aData) {
// TODO hook this back to Java to report errors.
dump("sendAsyncMessage " + aMessageName + ": " + JSON.stringify(aData));
log("sendAsyncMessage " + aMessageName + ": " + JSON.stringify(aData));
}
};
@ -204,19 +243,282 @@ this.WebappManager = {
});
},
_autoUpdate: function(aData, aOldApp) { return Task.spawn((function*() {
log("_autoUpdate app of type " + aData.type);
// The data object has a manifestUrl property for the manifest URL,
// but updateHostedApp expects it to be called manifestURL, and we pass
// the data object to it, so we need to change the name.
// TODO: rename this to manifestURL upstream, so the data object always has
// a consistent name for the property (even if we name it differently
// internally).
aData.manifestURL = aData.manifestUrl;
delete aData.manifestUrl;
if (aData.type == "hosted") {
let oldManifest = yield DOMApplicationRegistry.getManifestFor(aData.manifestURL);
DOMApplicationRegistry.updateHostedApp(aData, aOldApp.id, aOldApp, oldManifest, aData.manifest);
} else {
DOMApplicationRegistry.updatePackagedApp(aData, aOldApp.id, aOldApp, aData.manifest);
}
}).bind(this)); },
_checkingForUpdates: false,
checkForUpdates: function(userInitiated) { return Task.spawn((function*() {
log("checkForUpdates");
// Don't start checking for updates if we're already doing so.
// TODO: Consider cancelling the old one and starting a new one anyway
// if the user requested this one.
if (this._checkingForUpdates) {
log("already checking for updates");
return;
}
this._checkingForUpdates = true;
try {
let installedApps = yield this._getInstalledApps();
if (installedApps.length === 0) {
return;
}
// Map APK names to APK versions.
let apkNameToVersion = JSON.parse(sendMessageToJava({
type: "WebApps:GetApkVersions",
packageNames: installedApps.map(app => app.packageName).filter(packageName => !!packageName)
}));
// Map manifest URLs to APK versions, which is what the service needs
// in order to tell us which apps are outdated; and also map them to app
// objects, which the downloader/installer uses to download/install APKs.
// XXX Will this cause us to update apps without packages, and if so,
// does that satisfy the legacy migration story?
let manifestUrlToApkVersion = {};
let manifestUrlToApp = {};
for (let app of installedApps) {
manifestUrlToApkVersion[app.manifestURL] = apkNameToVersion[app.packageName] || 0;
manifestUrlToApp[app.manifestURL] = app;
}
let outdatedApps = yield this._getOutdatedApps(manifestUrlToApkVersion, userInitiated);
if (outdatedApps.length === 0) {
// If the user asked us to check for updates, tell 'em we came up empty.
if (userInitiated) {
this._notify({
title: Strings.GetStringFromName("noUpdatesTitle"),
message: Strings.GetStringFromName("noUpdatesMessage"),
icon: "drawable://alert_app",
});
}
return;
}
let names = [manifestUrlToApp[url].name for (url of outdatedApps)].join(", ");
let accepted = yield this._notify({
title: PluralForm.get(outdatedApps.length, Strings.GetStringFromName("downloadUpdateTitle")).
replace("#1", outdatedApps.length),
message: Strings.formatStringFromName("downloadUpdateMessage", [names], 1),
icon: "drawable://alert_download",
}).dismissed;
if (accepted) {
yield this._updateApks([manifestUrlToApp[url] for (url of outdatedApps)]);
}
}
// There isn't a catch block because we want the error to propagate through
// the promise chain, so callers can receive it and choose to respond to it.
finally {
// Ensure we update the _checkingForUpdates flag even if there's an error;
// otherwise the process will get stuck and never check for updates again.
this._checkingForUpdates = false;
}
}).bind(this)); },
_getInstalledApps: function() {
let deferred = Promise.defer();
DOMApplicationRegistry.getAll(apps => deferred.resolve(apps));
return deferred.promise;
},
_getOutdatedApps: function(installedApps, userInitiated) {
let deferred = Promise.defer();
let data = JSON.stringify({ installed: installedApps });
let notification;
if (userInitiated) {
notification = this._notify({
title: Strings.GetStringFromName("checkingForUpdatesTitle"),
message: Strings.GetStringFromName("checkingForUpdatesMessage"),
// TODO: replace this with an animated icon.
icon: "drawable://alert_app",
progress: NaN,
});
}
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest).
QueryInterface(Ci.nsIXMLHttpRequestEventTarget);
request.mozBackgroundRequest = true;
request.open("POST", Services.prefs.getCharPref(UPDATE_URL_PREF), true);
request.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS |
Ci.nsIChannel.LOAD_BYPASS_CACHE |
Ci.nsIChannel.INHIBIT_CACHING;
request.onload = function() {
notification.cancel();
deferred.resolve(JSON.parse(this.response).outdated);
};
request.onerror = function() {
if (userInitiated) {
notification.cancel();
}
deferred.reject(this.status || this.statusText);
};
request.setRequestHeader("Content-Type", "application/json");
request.setRequestHeader("Content-Length", data.length);
request.send(data);
return deferred.promise;
},
_updateApks: function(aApps) { return Task.spawn((function*() {
// Notify the user that we're in the progress of downloading updates.
let downloadingNames = [app.name for (app of aApps)].join(", ");
let notification = this._notify({
title: PluralForm.get(aApps.length, Strings.GetStringFromName("downloadingUpdateTitle")).
replace("#1", aApps.length),
message: Strings.formatStringFromName("downloadingUpdateMessage", [downloadingNames], 1),
// TODO: replace this with an animated icon. UpdateService uses
// android.R.drawable.stat_sys_download, but I don't think we can reference
// a system icon with a drawable: URL here, so we'll have to craft our own.
icon: "drawable://alert_download",
// TODO: make this a determinate progress indicator once we can determine
// the sizes of the APKs and observe their progress.
progress: NaN,
});
// Download the APKs for the given apps. We do this serially to avoid
// saturating the user's network connection.
// TODO: download APKs in parallel (or at least more than one at a time)
// if it seems reasonable.
let downloadedApks = [];
let downloadFailedApps = [];
for (let app of aApps) {
try {
let filePath = yield this._downloadApk(app.manifestURL);
downloadedApks.push({ app: app, filePath: filePath });
} catch(ex) {
downloadFailedApps.push(app);
}
}
notification.cancel();
// Notify the user if any downloads failed, but don't do anything
// when the user accepts/cancels the notification.
// In the future, we might prompt the user to retry the download.
if (downloadFailedApps.length > 0) {
let downloadFailedNames = [app.name for (app of downloadFailedApps)].join(", ");
this._notify({
title: PluralForm.get(downloadFailedApps.length, Strings.GetStringFromName("downloadFailedTitle")).
replace("#1", downloadFailedApps.length),
message: Strings.formatStringFromName("downloadFailedMessage", [downloadFailedNames], 1),
icon: "drawable://alert_app",
});
}
// If we weren't able to download any APKs, then there's nothing more to do.
if (downloadedApks.length === 0) {
return;
}
// Prompt the user to update the apps for which we downloaded APKs, and wait
// until they accept/cancel the notification.
let downloadedNames = [apk.app.name for (apk of downloadedApks)].join(", ");
let accepted = yield this._notify({
title: PluralForm.get(downloadedApks.length, Strings.GetStringFromName("installUpdateTitle")).
replace("#1", downloadedApks.length),
message: Strings.formatStringFromName("installUpdateMessage", [downloadedNames], 1),
icon: "drawable://alert_app",
}).dismissed;
if (accepted) {
// The user accepted the notification, so install the downloaded APKs.
for (let apk of downloadedApks) {
let msg = {
app: apk.app,
// TODO: figure out why WebApps:InstallApk needs the "from" property.
from: apk.app.installOrigin,
};
sendMessageToJava({
type: "WebApps:InstallApk",
filePath: apk.filePath,
data: JSON.stringify(msg),
});
}
} else {
// The user cancelled the notification, so remove the downloaded APKs.
for (let apk of downloadedApks) {
try {
yield OS.file.remove(apk.filePath);
} catch(ex) {
log("error removing " + apk.filePath + " for cancelled update: " + ex);
}
}
}
}).bind(this)); },
_notify: function(aOptions) {
dump("_notify: " + aOptions.title);
// Resolves to true if the notification is "clicked" (i.e. touched)
// and false if the notification is "cancelled" by swiping it away.
let dismissed = Promise.defer();
// TODO: make notifications expandable so users can expand them to read text
// that gets cut off in standard notifications.
let id = Notifications.create({
title: aOptions.title,
message: aOptions.message,
icon: aOptions.icon,
progress: aOptions.progress,
onClick: function(aId, aCookie) {
dismissed.resolve(true);
},
onCancel: function(aId, aCookie) {
dismissed.resolve(false);
},
});
// Return an object with a promise that resolves when the notification
// is dismissed by the user along with a method for cancelling it,
// so callers who want to wait for user action can do so, while those
// who want to control the notification's lifecycle can do that instead.
return {
dismissed: dismissed.promise,
cancel: function() {
Notifications.cancel(id);
},
};
},
autoUninstall: function(aData) {
DOMApplicationRegistry.registryReady.then(() => {
for (let id in DOMApplicationRegistry.webapps) {
let app = DOMApplicationRegistry.webapps[id];
if (aData.apkPackageNames.indexOf(app.apkPackageName) > -1) {
dump("attempting to uninstall " + app.name);
log("attempting to uninstall " + app.name);
DOMApplicationRegistry.uninstall(
app.manifestURL,
function() {
dump("success uninstalling " + app.name);
log("success uninstalling " + app.name);
},
function(error) {
dump("error uninstalling " + app.name + ": " + error);
log("error uninstalling " + app.name + ": " + error);
}
);
}
@ -241,7 +543,7 @@ this.WebappManager = {
if (aPrefs.length > 0) {
let array = new TextEncoder().encode(JSON.stringify(aPrefs));
OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) {
dump("Error writing default prefs: " + reason);
log("Error writing default prefs: " + reason);
});
}
},

View File

@ -26,14 +26,22 @@ onmessage = function(event) {
request.onreadystatechange = function(event) {
log("onreadystatechange: " + request.readyState);
if (request.readyState == 4) {
file.close();
if (request.readyState !== 4) {
return;
}
if (request.status == 200 || request.status == 0) {
postMessage({ type: "success" });
} else {
postMessage({ type: "failure", message: request.statusText });
file.close();
if (request.status === 200) {
postMessage({ type: "success" });
} else {
try {
OS.File.remove(path);
} catch(ex) {
log("error removing " + path + ": " + ex);
}
let statusMessage = request.status + " - " + request.statusText;
postMessage({ type: "failure", message: statusMessage });
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -96,3 +96,4 @@ chrome.jar:
skin/images/reader-style-icon-hdpi.png (images/reader-style-icon-hdpi.png)
skin/images/reader-style-icon-xhdpi.png (images/reader-style-icon-xhdpi.png)
skin/images/privatebrowsing-mask.png (images/privatebrowsing-mask.png)
skin/images/update.png (images/update.png)