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

This commit is contained in:
Myk Melez 2014-02-07 23:50:13 -08:00
Родитель df7c3c109f
Коммит e3e1f9ec39
18 изменённых файлов: 527 добавлений и 48 удалений

Просмотреть файл

@ -824,6 +824,16 @@ pref("browser.snippets.syncPromo.enabled", false);
// The URL of the APK factory from which we obtain APKs for webapps. // The URL of the APK factory from which we obtain APKs for webapps.
// This currently points to the development server. // This currently points to the development server.
pref("browser.webapps.apkFactoryUrl", "http://dapk.net/application.apk"); 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 #endif
// Whether or not to only sync home provider data when the user is on wifi. // 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 title = message.getString("title");
final String type = message.getString("shortcutType"); final String type = message.getString("shortcutType");
GeckoAppShell.removeShortcut(title, url, origin, type); 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")) { } else if (event.equals("Share:Text")) {
String text = message.getString("text"); String text = message.getString("text");
GeckoAppShell.openUriExternal(text, "text/plain", "", "", Intent.ACTION_SEND, ""); GeckoAppShell.openUriExternal(text, "text/plain", "", "", Intent.ACTION_SEND, "");
@ -1554,7 +1547,6 @@ public abstract class GeckoApp
registerEventListener("Locale:Set"); registerEventListener("Locale:Set");
registerEventListener("NativeApp:IsDebuggable"); registerEventListener("NativeApp:IsDebuggable");
registerEventListener("SystemUI:Visibility"); registerEventListener("SystemUI:Visibility");
registerEventListener("WebApps:PreInstall");
EventListener.registerEvents(); EventListener.registerEvents();
@ -2080,7 +2072,6 @@ public abstract class GeckoApp
unregisterEventListener("Locale:Set"); unregisterEventListener("Locale:Set");
unregisterEventListener("NativeApp:IsDebuggable"); unregisterEventListener("NativeApp:IsDebuggable");
unregisterEventListener("SystemUI:Visibility"); unregisterEventListener("SystemUI:Visibility");
unregisterEventListener("WebApps:PreInstall");
EventListener.unregisterEvents(); EventListener.unregisterEvents();

Просмотреть файл

@ -42,6 +42,7 @@ public final class NotificationHelper implements GeckoEventListener {
// Attributes that can be used while sending a notification from js. // 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_VALUE_ATTR = "progress_value";
private static final String PROGRESS_MAX_ATTR = "progress_max"; 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 LIGHT_ATTR = "light";
private static final String ONGOING_ATTR = "ongoing"; private static final String ONGOING_ATTR = "ongoing";
private static final String WHEN_ATTR = "when"; private static final String WHEN_ATTR = "when";
@ -253,11 +254,13 @@ public final class NotificationHelper implements GeckoEventListener {
} }
if (message.has(PROGRESS_VALUE_ATTR) && if (message.has(PROGRESS_VALUE_ATTR) &&
message.has(PROGRESS_MAX_ATTR)) { message.has(PROGRESS_MAX_ATTR) &&
message.has(PROGRESS_INDETERMINATE_ATTR)) {
try { try {
final int progress = message.getInt(PROGRESS_VALUE_ATTR); final int progress = message.getInt(PROGRESS_VALUE_ATTR);
final int progressMax = message.getInt(PROGRESS_MAX_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) { } catch (JSONException ex) {
Log.i(LOGTAG, "Error parsing", 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.ActivityResultHandler;
import org.mozilla.gecko.util.EventDispatcher; import org.mozilla.gecko.util.EventDispatcher;
import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.GeckoEventResponder;
import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.WebAppAllocator; import org.mozilla.gecko.WebAppAllocator;
@ -21,24 +22,30 @@ import android.app.ActivityManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; 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.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import java.io.File; import java.io.File;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
public class EventListener implements GeckoEventListener { public class EventListener implements GeckoEventListener, GeckoEventResponder {
private static final String LOGTAG = "GeckoWebAppEventListener"; private static final String LOGTAG = "GeckoWebAppEventListener";
private EventListener() { } private EventListener() { }
private static EventListener mEventListener; private static EventListener mEventListener;
private String mCurrentResponse = "";
private static EventListener getEventListener() { private static EventListener getEventListener() {
if (mEventListener == null) { if (mEventListener == null) {
@ -61,6 +68,7 @@ public class EventListener implements GeckoEventListener {
registerEventListener("WebApps:PostInstall"); registerEventListener("WebApps:PostInstall");
registerEventListener("WebApps:Open"); registerEventListener("WebApps:Open");
registerEventListener("WebApps:Uninstall"); registerEventListener("WebApps:Uninstall");
registerEventListener("WebApps:GetApkVersions");
} }
public static void unregisterEvents() { public static void unregisterEvents() {
@ -69,6 +77,7 @@ public class EventListener implements GeckoEventListener {
unregisterEventListener("WebApps:PostInstall"); unregisterEventListener("WebApps:PostInstall");
unregisterEventListener("WebApps:Open"); unregisterEventListener("WebApps:Open");
unregisterEventListener("WebApps:Uninstall"); unregisterEventListener("WebApps:Uninstall");
unregisterEventListener("WebApps:GetApkVersions");
} }
@Override @Override
@ -96,12 +105,28 @@ public class EventListener implements GeckoEventListener {
GeckoAppShell.getGeckoInterface().getActivity().startActivity(intent); GeckoAppShell.getGeckoInterface().getActivity().startActivity(intent);
} else if (!AppConstants.MOZ_ANDROID_SYNTHAPKS && event.equals("WebApps:Uninstall")) { } else if (!AppConstants.MOZ_ANDROID_SYNTHAPKS && event.equals("WebApps:Uninstall")) {
uninstallWebApp(message.getString("origin")); 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) { } catch (Exception e) {
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", 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. // Not used by MOZ_ANDROID_SYNTHAPKS.
public static File preInstallWebApp(String aTitle, String aURI, String aOrigin) { public static File preInstallWebApp(String aTitle, String aURI, String aOrigin) {
int index = WebAppAllocator.getInstance(GeckoAppShell.getContext()).findAndAllocateIndex(aOrigin, aTitle, (String) null); int index = WebAppAllocator.getInstance(GeckoAppShell.getContext()).findAndAllocateIndex(aOrigin, aTitle, (String) null);
@ -192,11 +217,17 @@ public class EventListener implements GeckoEventListener {
filter.addDataScheme("package"); filter.addDataScheme("package");
context.registerReceiver(receiver, filter); context.registerReceiver(receiver, filter);
// Now call the package installer.
File file = new File(filePath); 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 intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive"); intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
// Now call the package installer.
GeckoAppShell.sActivityHelper.startIntentForActivity(context, intent, new ActivityResultHandler() { GeckoAppShell.sActivityHelper.startIntentForActivity(context, intent, new ActivityResultHandler() {
@Override @Override
public void onActivityResult(int resultCode, Intent data) { 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"); Log.i(LOGTAG, "No manifest URL present in metadata");
return; return;
} else if (!isCorrectManifest(manifestUrl)) { } else if (!isCorrectManifest(manifestUrl)) {
Log.i(LOGTAG, "Waiting to finish installing " + mManifestUrl + " but this is " +manifestUrl); // This happens when the updater triggers installation of multiple
//return; // 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)) { if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {

Просмотреть файл

@ -32,6 +32,11 @@ public class UninstallListener extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { 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(); String packageName = intent.getData().getSchemeSpecificPart();
if (TextUtils.isEmpty(packageName)) { 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/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/AppsUtils.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"; const DEFAULT_ICON = "chrome://browser/skin/images/default-app-icon.png";
let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutApps.properties"); let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutApps.properties");
@ -41,6 +43,10 @@ function openLink(aEvent) {
} catch (ex) {} } catch (ex) {}
} }
function checkForUpdates(aEvent) {
WebappManager.checkForUpdates(true);
}
#ifndef MOZ_ANDROID_SYNTHAPKS #ifndef MOZ_ANDROID_SYNTHAPKS
var ContextMenus = { var ContextMenus = {
target: null, target: null,
@ -87,6 +93,8 @@ function onLoad(aEvent) {
elmts[i].addEventListener("click", openLink, false); elmts[i].addEventListener("click", openLink, false);
} }
document.getElementById("update-item").addEventListener("click", checkForUpdates, false);
navigator.mozApps.mgmt.oninstall = onInstall; navigator.mozApps.mgmt.oninstall = onInstall;
navigator.mozApps.mgmt.onuninstall = onUninstall; navigator.mozApps.mgmt.onuninstall = onUninstall;
updateList(); updateList();

Просмотреть файл

@ -55,5 +55,12 @@
<div id="browse-title" class="title">&aboutApps.browseMarketplace;</div> <div id="browse-title" class="title">&aboutApps.browseMarketplace;</div>
</div> </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> </body>
</html> </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 profile-after-change Snippets @mozilla.org/snippets;1
category update-timer Snippets @mozilla.org/snippets;1,getService,snippets-update-timer,browser.snippets.updateInterval,86400 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 # ColorPicker.js
component {430b987f-bb9f-46a3-99a5-241749220b29} ColorPicker.js component {430b987f-bb9f-46a3-99a5-241749220b29} ColorPicker.js
contract @mozilla.org/colorpicker;1 {430b987f-bb9f-46a3-99a5-241749220b29} contract @mozilla.org/colorpicker;1 {430b987f-bb9f-46a3-99a5-241749220b29}

Просмотреть файл

@ -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', 'PromptService.js',
'SiteSpecificUserAgent.js', 'SiteSpecificUserAgent.js',
'Snippets.js', 'Snippets.js',
'WebappsUpdateTimer.js',
'XPIDialogService.js', 'XPIDialogService.js',
] ]

Просмотреть файл

@ -8,3 +8,4 @@
<!ENTITY aboutApps.browseMarketplace "Browse the Firefox Marketplace"> <!ENTITY aboutApps.browseMarketplace "Browse the Firefox Marketplace">
<!ENTITY aboutApps.uninstall "Uninstall"> <!ENTITY aboutApps.uninstall "Uninstall">
<!ENTITY aboutApps.addToHomescreen "Add to Home Screen"> <!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/phishing.dtd (%chrome/phishing.dtd)
locale/@AB_CD@/browser/payments.properties (%chrome/payments.properties) locale/@AB_CD@/browser/payments.properties (%chrome/payments.properties)
locale/@AB_CD@/browser/handling.properties (%chrome/handling.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 # overrides for toolkit l10n, also for en-US
relativesrcdir toolkit/locales: relativesrcdir toolkit/locales:

Просмотреть файл

@ -103,6 +103,11 @@ Notification.prototype = {
if (this._progress) { if (this._progress) {
msg.progress_value = this._progress; msg.progress_value = this._progress;
msg.progress_max = 100; 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) if (this._priority)

Просмотреть файл

@ -8,6 +8,8 @@ this.EXPORTED_SYMBOLS = ["WebappManager"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components; 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/AppsUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.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/DOMRequestHelper.jsm");
Cu.import("resource://gre/modules/Webapps.jsm"); Cu.import("resource://gre/modules/Webapps.jsm");
Cu.import("resource://gre/modules/osfile.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) { XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
Services.console.logStringMessage("* * WebappManager.jsm: " + a);
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) { function sendMessageToJava(aMessage) {
@ -43,7 +59,7 @@ this.WebappManager = {
return; return;
} }
this._downloadApk(aMessage, aMessageManager); this._installApk(aMessage, aMessageManager);
}, },
installPackage: function(aMessage, aMessageManager) { installPackage: function(aMessage, aMessageManager) {
@ -53,12 +69,31 @@ this.WebappManager = {
return; return;
} }
this._downloadApk(aMessage, aMessageManager); this._installApk(aMessage, aMessageManager);
}, },
_downloadApk: function(aMsg, aMessageManager) { _installApk: function(aMessage, aMessageManager) { return Task.spawn((function*() {
let manifestUrl = aMsg.app.manifestURL; let filePath;
dump("_downloadApk for " + manifestUrl);
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. // Get the endpoint URL and convert it to an nsIURI/nsIURL object.
const GENERATOR_URL_PREF = "browser.webapps.apkFactoryUrl"; 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. // Populate the query part of the URL with the manifest URL parameter.
let params = { let params = {
manifestUrl: manifestUrl, manifestUrl: aManifestUrl,
}; };
generatorUrl.query = generatorUrl.query =
[p + "=" + encodeURIComponent(params[p]) for (p in params)].join("&"); [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"]. let file = Cc["@mozilla.org/download-manager;1"].
getService(Ci.nsIDownloadManager). getService(Ci.nsIDownloadManager).
defaultDownloadsDirectory. defaultDownloadsDirectory.
clone(); 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); 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"); let worker = new ChromeWorker("resource://gre/modules/WebappManagerWorker.js");
worker.onmessage = function(event) { worker.onmessage = function(event) {
@ -88,20 +123,17 @@ this.WebappManager = {
worker.terminate(); worker.terminate();
if (type == "success") { if (type == "success") {
sendMessageToJava({ deferred.resolve(file.path);
type: "WebApps:InstallApk",
filePath: file.path,
data: JSON.stringify(aMsg),
});
} else { // type == "failure" } else { // type == "failure"
aMsg.error = message; log("error downloading APK: " + message);
aMessageManager.sendAsyncMessage("Webapps:Install:Return:KO", aMsg); deferred.reject(message);
dump("error downloading APK: " + message);
} }
} }
// Trigger the download. // Trigger the download.
worker.postMessage({ url: generatorUrl.spec, path: file.path }); worker.postMessage({ url: generatorUrl.spec, path: file.path });
return deferred.promise;
}, },
askInstall: function(aData) { askInstall: function(aData) {
@ -115,7 +147,7 @@ this.WebappManager = {
// when we trigger the native install dialog and doesn't re-init itself // when we trigger the native install dialog and doesn't re-init itself
// afterward (TODO: file bug about this behavior). // afterward (TODO: file bug about this behavior).
if ("appcache_path" in aData.app.manifest) { 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; delete aData.app.manifest.appcache_path;
} }
@ -136,7 +168,7 @@ this.WebappManager = {
}, },
launch: function({ manifestURL, origin }) { launch: function({ manifestURL, origin }) {
dump("launchWebapp: " + manifestURL); log("launchWebapp: " + manifestURL);
sendMessageToJava({ sendMessageToJava({
type: "WebApps:Open", type: "WebApps:Open",
@ -146,7 +178,7 @@ this.WebappManager = {
}, },
uninstall: function(aData) { uninstall: function(aData) {
dump("uninstall: " + aData.manifestURL); log("uninstall: " + aData.manifestURL);
if (this._testing) { if (this._testing) {
// We don't have to do anything, as the registry does all the work. // We don't have to do anything, as the registry does all the work.
@ -157,10 +189,17 @@ this.WebappManager = {
}, },
autoInstall: function(aData) { 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 = { let mm = {
sendAsyncMessage: function (aMessageName, aData) { sendAsyncMessage: function (aMessageName, aData) {
// TODO hook this back to Java to report errors. // 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) { autoUninstall: function(aData) {
DOMApplicationRegistry.registryReady.then(() => { DOMApplicationRegistry.registryReady.then(() => {
for (let id in DOMApplicationRegistry.webapps) { for (let id in DOMApplicationRegistry.webapps) {
let app = DOMApplicationRegistry.webapps[id]; let app = DOMApplicationRegistry.webapps[id];
if (aData.apkPackageNames.indexOf(app.apkPackageName) > -1) { if (aData.apkPackageNames.indexOf(app.apkPackageName) > -1) {
dump("attempting to uninstall " + app.name); log("attempting to uninstall " + app.name);
DOMApplicationRegistry.uninstall( DOMApplicationRegistry.uninstall(
app.manifestURL, app.manifestURL,
function() { function() {
dump("success uninstalling " + app.name); log("success uninstalling " + app.name);
}, },
function(error) { function(error) {
dump("error uninstalling " + app.name + ": " + error); log("error uninstalling " + app.name + ": " + error);
} }
); );
} }
@ -241,7 +543,7 @@ this.WebappManager = {
if (aPrefs.length > 0) { if (aPrefs.length > 0) {
let array = new TextEncoder().encode(JSON.stringify(aPrefs)); let array = new TextEncoder().encode(JSON.stringify(aPrefs));
OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) { 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) { request.onreadystatechange = function(event) {
log("onreadystatechange: " + request.readyState); log("onreadystatechange: " + request.readyState);
if (request.readyState == 4) { if (request.readyState !== 4) {
return;
}
file.close(); file.close();
if (request.status == 200 || request.status == 0) { if (request.status === 200) {
postMessage({ type: "success" }); postMessage({ type: "success" });
} else { } else {
postMessage({ type: "failure", message: request.statusText }); try {
OS.File.remove(path);
} catch(ex) {
log("error removing " + path + ": " + ex);
} }
let statusMessage = request.status + " - " + request.statusText;
postMessage({ type: "failure", message: statusMessage });
} }
}; };

Двоичные данные
mobile/android/themes/core/images/update.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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-hdpi.png (images/reader-style-icon-hdpi.png)
skin/images/reader-style-icon-xhdpi.png (images/reader-style-icon-xhdpi.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/privatebrowsing-mask.png (images/privatebrowsing-mask.png)
skin/images/update.png (images/update.png)