mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-25 22:01:30 +00:00
bug 934760 - implement synthetic APK update flow; r=wesj
This commit is contained in:
parent
df7c3c109f
commit
e3e1f9ec39
@ -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.
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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)) {
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
59
mobile/android/components/WebappsUpdateTimer.js
Normal file
59
mobile/android/components/WebappsUpdateTimer.js
Normal 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]);
|
@ -24,6 +24,7 @@ EXTRA_COMPONENTS += [
|
||||
'PromptService.js',
|
||||
'SiteSpecificUserAgent.js',
|
||||
'Snippets.js',
|
||||
'WebappsUpdateTimer.js',
|
||||
'XPIDialogService.js',
|
||||
]
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
BIN
mobile/android/themes/core/images/update.png
Normal file
BIN
mobile/android/themes/core/images/update.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user