зеркало из https://github.com/mozilla/gecko-dev.git
bug 934760 - implement synthetic APK update flow; r=wesj
This commit is contained in:
Родитель
df7c3c109f
Коммит
e3e1f9ec39
|
@ -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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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)
|
||||||
|
|
Загрузка…
Ссылка в новой задаче