diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index 8550a42505e..9ddcb983863 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -490,31 +490,11 @@ pref("app.update.timerMinimumDelay", 30); // seconds #ifdef MOZ_UPDATER /* prefs used specifically for updating the app */ -pref("app.update.enabled", true); -pref("app.update.auto", false); +pref("app.update.enabled", false); pref("app.update.channel", "@MOZ_UPDATE_CHANNEL@"); -pref("app.update.mode", 1); -pref("app.update.silent", false); -#ifdef MOZ_PKG_SPECIAL -pref("app.update.url", "https://aus2.mozilla.org/update/4/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%-@MOZ_PKG_SPECIAL@/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PLATFORM_VERSION%/update.xml"); -#else -pref("app.update.url", "https://aus2.mozilla.org/update/4/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PLATFORM_VERSION%/update.xml"); -#endif -pref("app.update.promptWaitTime", 43200); -pref("app.update.idletime", 60); -pref("app.update.showInstalledUI", false); -pref("app.update.incompatible.mode", 0); -pref("app.update.download.backgroundInterval", 0); -#ifdef MOZ_OFFICIAL_BRANDING -pref("app.update.interval", 86400); -pref("app.update.url.manual", "http://www.mozilla.com/%LOCALE%/m/"); -pref("app.update.url.details", "http://www.mozilla.com/%LOCALE%/mobile/releases/"); -#else -pref("app.update.interval", 3600); -pref("app.update.url.manual", "http://www.mozilla.com/%LOCALE%/mobile/"); -pref("app.update.url.details", "http://www.mozilla.com/%LOCALE%/mobile/"); -#endif +// If you are looking for app.update.url, we no longer use it. +// See mobile/android/base/UpdateServiceHelper.java.in #endif // replace newlines with spaces on paste into single-line text boxes diff --git a/mobile/android/base/AndroidManifest.xml.in b/mobile/android/base/AndroidManifest.xml.in index dd2e0a843b8..7b756d9e171 100644 --- a/mobile/android/base/AndroidManifest.xml.in +++ b/mobile/android/base/AndroidManifest.xml.in @@ -148,6 +148,12 @@ + + + + + + @@ -224,6 +230,13 @@ android:authorities="@ANDROID_PACKAGE_NAME@.db.tabs" android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/> + + + + #include ../sync/manifests/SyncAndroidManifest_services.xml.in diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index ad10c243ce5..174527e4476 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -16,6 +16,8 @@ import org.mozilla.gecko.util.GeckoBackgroundThread; import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.GeckoEventResponder; import org.mozilla.gecko.GeckoAccessibility; +import org.mozilla.gecko.updater.UpdateServiceHelper; +import org.mozilla.gecko.updater.UpdateService; import org.json.JSONArray; import org.json.JSONException; @@ -127,7 +129,6 @@ abstract public class GeckoApp public static final String ACTION_DEBUG = "org.mozilla.gecko.DEBUG"; public static final String ACTION_BOOKMARK = "org.mozilla.gecko.BOOKMARK"; public static final String ACTION_LOAD = "org.mozilla.gecko.LOAD"; - public static final String ACTION_UPDATE = "org.mozilla.gecko.UPDATE"; public static final String ACTION_INIT_PW = "org.mozilla.gecko.INIT_PW"; public static final String SAVED_STATE_TITLE = "title"; public static final String SAVED_STATE_IN_BACKGROUND = "inBackground"; @@ -910,8 +911,6 @@ abstract public class GeckoApp String host = message.getString("host"); JSONArray permissions = message.getJSONArray("permissions"); showSiteSettingsDialog(host, permissions); - } else if (event.equals("Update:Restart")) { - doRestart("org.mozilla.gecko.restart_update"); } else if (event.equals("Tab:ViewportMetadata")) { int tabId = message.getInt("tabID"); Tab tab = Tabs.getInstance().getTab(tabId); @@ -1000,6 +999,8 @@ abstract public class GeckoApp GeckoAppShell.shareImage(src, type); } else if (event.equals("Sanitize:ClearHistory")) { handleClearHistory(); + } else if (event.equals("Update:Check")) { + startService(new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE, null, this, UpdateService.class)); } } catch (Exception e) { Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); @@ -1487,11 +1488,6 @@ abstract public class GeckoApp BrowserDB.initialize(getProfile().getName()); - if (ACTION_UPDATE.equals(action) || args != null && args.contains("-alert update-app")) { - Log.i(LOGTAG,"onCreate: Update request"); - checkAndLaunchUpdate(); - } - String passedUri = null; String uri = getURIFromIntent(intent); if (uri != null && uri.length() > 0) { @@ -1586,7 +1582,6 @@ abstract public class GeckoApp registerEventListener("ToggleChrome:Show"); registerEventListener("ToggleChrome:Focus"); registerEventListener("Permissions:Data"); - registerEventListener("Update:Restart"); registerEventListener("Tab:HasTouchListener"); registerEventListener("Tab:ViewportMetadata"); registerEventListener("Session:StatePurged"); @@ -1601,6 +1596,7 @@ abstract public class GeckoApp registerEventListener("Share:Text"); registerEventListener("Share:Image"); registerEventListener("Sanitize:ClearHistory"); + registerEventListener("Update:Check"); if (SmsManager.getInstance() != null) { SmsManager.getInstance().start(); @@ -1621,6 +1617,8 @@ abstract public class GeckoApp GeckoNetworkManager.getInstance().init(); GeckoNetworkManager.getInstance().start(); + UpdateServiceHelper.registerForUpdates(this); + GeckoScreenOrientationListener.getInstance().start(); final GeckoApp self = this; @@ -2029,7 +2027,6 @@ abstract public class GeckoApp unregisterEventListener("ToggleChrome:Show"); unregisterEventListener("ToggleChrome:Focus"); unregisterEventListener("Permissions:Data"); - unregisterEventListener("Update:Restart"); unregisterEventListener("Tab:HasTouchListener"); unregisterEventListener("Tab:ViewportMetadata"); unregisterEventListener("Session:StatePurged"); @@ -2044,6 +2041,7 @@ abstract public class GeckoApp unregisterEventListener("Share:Text"); unregisterEventListener("Share:Image"); unregisterEventListener("Sanitize:ClearHistory"); + unregisterEventListener("Update:Check"); deleteTempFiles(); @@ -2214,76 +2212,6 @@ abstract public class GeckoApp GeckoAppShell.handleNotification(action, alertName, alertCookie); } - private void checkAndLaunchUpdate() { - Log.i(LOGTAG, "Checking for an update"); - - int statusCode = 8; // UNEXPECTED_ERROR - File baseUpdateDir = null; - if (Build.VERSION.SDK_INT >= 8) - baseUpdateDir = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS); - else - baseUpdateDir = new File(Environment.getExternalStorageDirectory().getPath(), "download"); - - File updateDir = new File(new File(baseUpdateDir, "updates"),"0"); - - File updateFile = new File(updateDir, "update.apk"); - File statusFile = new File(updateDir, "update.status"); - - if (!statusFile.exists() || !readUpdateStatus(statusFile).equals("pending")) - return; - - if (!updateFile.exists()) - return; - - Log.i(LOGTAG, "Update is available!"); - - // Launch APK - File updateFileToRun = new File(updateDir, getPackageName() + "-update.apk"); - try { - if (updateFile.renameTo(updateFileToRun)) { - String amCmd = "/system/bin/am start -a android.intent.action.VIEW " + - "-n com.android.packageinstaller/.PackageInstallerActivity -d file://" + - updateFileToRun.getPath(); - Log.i(LOGTAG, amCmd); - Runtime.getRuntime().exec(amCmd); - statusCode = 0; // OK - } else { - Log.i(LOGTAG, "Cannot rename the update file!"); - statusCode = 7; // WRITE_ERROR - } - } catch (Exception e) { - Log.i(LOGTAG, "error launching installer to update", e); - } - - // Update the status file - String status = statusCode == 0 ? "succeeded\n" : "failed: "+ statusCode + "\n"; - - OutputStream outStream; - try { - byte[] buf = status.getBytes("UTF-8"); - outStream = new FileOutputStream(statusFile); - outStream.write(buf, 0, buf.length); - outStream.close(); - } catch (Exception e) { - Log.i(LOGTAG, "error writing status file", e); - } - - if (statusCode == 0) - System.exit(0); - } - - private String readUpdateStatus(File statusFile) { - String status = ""; - try { - BufferedReader reader = new BufferedReader(new FileReader(statusFile)); - status = reader.readLine(); - reader.close(); - } catch (Exception e) { - Log.i(LOGTAG, "error reading update status", e); - } - return status; - } - private void checkMigrateProfile() { final File profileDir = getProfile().getDir(); final long currentTime = SystemClock.uptimeMillis(); @@ -2566,6 +2494,10 @@ abstract public class GeckoApp } } + public void notifyCheckUpdateResult(boolean result) { + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Update:CheckResult", result ? "true" : "false")); + } + private void connectGeckoLayerClient() { mLayerView.getLayerClient().notifyGeckoReady(); diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java index d2da8ef30e0..1c55e7b9e4e 100644 --- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -2297,6 +2297,11 @@ public class GeckoAppShell public static void unregisterSurfaceTextureFrameListener(Object surfaceTexture) { ((SurfaceTexture)surfaceTexture).setOnFrameAvailableListener(null); } + + public static void notifyCheckUpdateResult(boolean result) { + if (GeckoApp.mAppContext != null) + GeckoApp.mAppContext.notifyCheckUpdateResult(result); + } } class ScreenshotHandler implements Runnable { diff --git a/mobile/android/base/GeckoUpdateReceiver.java b/mobile/android/base/GeckoUpdateReceiver.java new file mode 100644 index 00000000000..a232ebb8b0f --- /dev/null +++ b/mobile/android/base/GeckoUpdateReceiver.java @@ -0,0 +1,22 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko; + +import org.mozilla.gecko.updater.UpdateServiceHelper; + +import android.content.*; +import android.net.*; + +public class GeckoUpdateReceiver + extends BroadcastReceiver +{ + @Override + public void onReceive(Context context, Intent intent) { + if (UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT.equals(intent.getAction())) { + GeckoAppShell.notifyCheckUpdateResult(intent.getBooleanExtra("result", false)); + } + } +} diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index 6fd8ca39d86..3402492ba75 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -159,6 +159,8 @@ FENNEC_JAVA_FILES = \ ui/SubdocumentScrollHelper.java \ GeckoNetworkManager.java \ GeckoScreenOrientationListener.java \ + UpdateService.java \ + GeckoUpdateReceiver.java \ $(MOZGLUE_JAVA_FILES) \ $(UTIL_JAVA_FILES) \ $(NULL) @@ -183,6 +185,7 @@ FENNEC_PP_JAVA_FILES = \ db/TabsProvider.java \ db/GeckoProvider.java \ SmsManager.java \ + UpdateServiceHelper.java \ $(NULL) FENNEC_PP_XML_FILES = \ @@ -207,6 +210,8 @@ else MIN_CPU_VERSION=5 endif +MOZ_APP_BUILDID=$(shell cat $(DEPTH)/config/buildid) + ifeq (,$(ANDROID_VERSION_CODE)) ifeq ($(MIN_CPU_VERSION),7) ANDROID_VERSION_CODE=$(shell cat $(DEPTH)/config/buildid | cut -c1-10) @@ -231,6 +236,10 @@ DEFINES += \ -DANDROID_VERSION_CODE=$(ANDROID_VERSION_CODE) \ -DMOZILLA_OFFICIAL=$(MOZILLA_OFFICIAL) \ -DUA_BUILDID=$(UA_BUILDID) \ + -DMOZ_APP_BASENAME=$(MOZ_APP_BASENAME) \ + -DMOZ_APP_BUILDID=$(MOZ_APP_BUILDID) \ + -DMOZ_APP_ABI=$(TARGET_XPCOM_ABI) \ + -DMOZ_UPDATER=$(MOZ_UPDATER) \ $(NULL) ifdef MOZ_LINKER_EXTRACT diff --git a/mobile/android/base/Restarter.java.in b/mobile/android/base/Restarter.java.in index cb6c7d76479..ea14a59ad72 100644 --- a/mobile/android/base/Restarter.java.in +++ b/mobile/android/base/Restarter.java.in @@ -43,10 +43,7 @@ public class Restarter extends Activity { Log.i(LOGTAG, e.toString()); } try { - String action = "org.mozilla.gecko.restart_update".equals(getIntent().getAction()) ? - App.ACTION_UPDATE : Intent.ACTION_MAIN; - - Intent intent = new Intent(action); + Intent intent = new Intent(Intent.ACTION_MAIN); intent.setClassName("@ANDROID_PACKAGE_NAME@", "@ANDROID_PACKAGE_NAME@.App"); Bundle b = getIntent().getExtras(); diff --git a/mobile/android/base/UpdateService.java b/mobile/android/base/UpdateService.java new file mode 100644 index 00000000000..66d226b3989 --- /dev/null +++ b/mobile/android/base/UpdateService.java @@ -0,0 +1,638 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.updater; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.GeckoApp; + +import org.mozilla.apache.commons.codec.binary.Hex; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import android.app.AlarmManager; +import android.app.IntentService; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; + +import android.os.Bundle; +import android.os.Environment; +import android.os.IBinder; + +import android.util.Log; + +import android.widget.RemoteViews; + +import java.net.URL; +import java.net.URLConnection; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.OutputStream; + +import java.security.MessageDigest; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.Random; +import java.util.TimeZone; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + + +public class UpdateService extends IntentService { + private static final int BUFSIZE = 8192; + private static final int NOTIFICATION_ID = 0x3e40ddbd; + + private static final String LOGTAG = "UpdateService"; + + private static final int INTERVAL_LONG = 86400000; // in milliseconds + private static final int INTERVAL_SHORT = 14400000; // again, in milliseconds + private static final int INTERVAL_RETRY = 3600000; + + private static final String PREFS_NAME = "UpdateService"; + private static final String KEY_LAST_BUILDID = "UpdateService.lastBuildID"; + private static final String KEY_LAST_HASH_FUNCTION = "UpdateService.lastHashFunction"; + private static final String KEY_LAST_HASH_VALUE = "UpdateService.lastHashValue"; + private static final String KEY_LAST_ATTEMPT_DATE = "UpdateService.lastAttemptDate"; + + private SharedPreferences mPrefs; + + private NotificationManager mNotificationManager; + private ConnectivityManager mConnectivityManager; + + private boolean mDownloading; + private boolean mApplyImmediately; + + public UpdateService() { + super("updater"); + } + + @Override + public void onCreate () { + super.onCreate(); + + mPrefs = getSharedPreferences(PREFS_NAME, 0); + mNotificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); + mConnectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); + } + + @Override + public synchronized int onStartCommand (Intent intent, int flags, int startId) { + // If we are busy doing a download, the new Intent here would normally be queued for + // execution once that is done. In this case, however, we want to flip the boolean + // while that is running, so handle that now. + if (mDownloading && UpdateServiceHelper.ACTION_APPLY_UPDATE.equals(intent.getAction())) { + Log.i(LOGTAG, "will apply update when download finished"); + + mApplyImmediately = true; + showDownloadNotification(); + } else { + super.onStartCommand(intent, flags, startId); + } + + return Service.START_REDELIVER_INTENT; + } + + @Override + protected void onHandleIntent (Intent intent) { + if (UpdateServiceHelper.ACTION_REGISTER_FOR_UPDATES.equals(intent.getAction())) { + registerForUpdates(false); + } if (UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE.equals(intent.getAction())) { + startUpdate(intent.getIntExtra(UpdateServiceHelper.EXTRA_UPDATE_FLAGS_NAME, 0)); + } else if (UpdateServiceHelper.ACTION_APPLY_UPDATE.equals(intent.getAction())) { + applyUpdate(intent.getStringExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME)); + } + } + + private static boolean hasFlag(int flags, int flag) { + return (flags & flag) == flag; + } + + private void sendCheckUpdateResult(boolean result) { + Intent resultIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT); + resultIntent.putExtra("result", result); + sendBroadcast(resultIntent); + } + + private int getUpdateInterval(boolean isRetry) { + int interval; + if (isRetry) { + interval = INTERVAL_RETRY; + } else if (UpdateServiceHelper.UPDATE_CHANNEL.equals("nightly") || + UpdateServiceHelper.UPDATE_CHANNEL.equals("aurora")) { + interval = INTERVAL_SHORT; + } else { + interval = INTERVAL_LONG; + } + + return interval; + } + + private void registerForUpdates(boolean isRetry) { + Calendar lastAttempt = getLastAttemptDate(); + Calendar now = new GregorianCalendar(TimeZone.getTimeZone("GMT")); + + int interval = getUpdateInterval(isRetry); + + if (lastAttempt == null || (now.getTimeInMillis() - lastAttempt.getTimeInMillis()) > interval) { + // We've either never attempted an update, or we are passed the desired + // time. Start an update now. + Log.i(LOGTAG, "no update has ever been attempted, checking now"); + startUpdate(0); + return; + } + + AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + if (manager == null) + return; + + PendingIntent pending = PendingIntent.getService(this, 0, new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE, null, this, UpdateService.class), 0); + manager.cancel(pending); + + lastAttempt.setTimeInMillis(lastAttempt.getTimeInMillis() + interval); + Log.i(LOGTAG, "next update will be at: " + lastAttempt.getTime()); + + manager.set(AlarmManager.RTC_WAKEUP, lastAttempt.getTimeInMillis(), pending); + } + + private void startUpdate(int flags) { + setLastAttemptDate(); + + NetworkInfo netInfo = mConnectivityManager.getActiveNetworkInfo(); + if (netInfo == null || !netInfo.isConnected()) { + Log.i(LOGTAG, "not connected to the network"); + registerForUpdates(true); + sendCheckUpdateResult(false); + return; + } + + registerForUpdates(false); + + UpdateInfo info = findUpdate(hasFlag(flags, UpdateServiceHelper.FLAG_REINSTALL)); + boolean haveUpdate = (info != null); + sendCheckUpdateResult(haveUpdate); + + if (!haveUpdate) { + Log.i(LOGTAG, "no update available"); + return; + } + + Log.i(LOGTAG, "update available, buildID = " + info.buildID); + + int connectionType = netInfo.getType(); + if (!hasFlag(flags, UpdateServiceHelper.FLAG_FORCE_DOWNLOAD) && + connectionType != ConnectivityManager.TYPE_WIFI && + connectionType != ConnectivityManager.TYPE_ETHERNET) { + Log.i(LOGTAG, "not connected via wifi or ethernet"); + + // We aren't autodownloading here, so prompt to start the update + Notification notification = new Notification(R.drawable.icon, getResources().getString(R.string.updater_start_ticker), System.currentTimeMillis()); + + Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE); + notificationIntent.setClass(this, UpdateService.class); + notificationIntent.putExtra(UpdateServiceHelper.EXTRA_UPDATE_FLAGS_NAME, UpdateServiceHelper.FLAG_FORCE_DOWNLOAD); + + PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + notification.flags = Notification.FLAG_AUTO_CANCEL; + + notification.setLatestEventInfo(this, getResources().getString(R.string.updater_start_title), + getResources().getString(R.string.updater_start_select), + contentIntent); + + mNotificationManager.notify(NOTIFICATION_ID, notification); + + return; + } + + File pkg = downloadUpdatePackage(info, hasFlag(flags, UpdateServiceHelper.FLAG_OVERWRITE_EXISTING)); + if (pkg == null) + return; + + Log.i(LOGTAG, "have update package at " + pkg); + + saveUpdateInfo(info); + + // If we have root, we always apply the update immediately because it happens in the background + if (mApplyImmediately || checkRoot()) { + applyUpdate(pkg); + } else { + // Prompt to apply the update + Notification notification = new Notification(R.drawable.icon, getResources().getString(R.string.updater_apply_ticker), System.currentTimeMillis()); + + Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE); + notificationIntent.setClass(this, UpdateService.class); + notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, pkg.getAbsolutePath()); + + PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + notification.flags = Notification.FLAG_AUTO_CANCEL; + + notification.setLatestEventInfo(this, getResources().getString(R.string.updater_apply_title), + getResources().getString(R.string.updater_apply_select), + contentIntent); + + mNotificationManager.notify(NOTIFICATION_ID, notification); + } + } + + private UpdateInfo findUpdate(boolean force) { + try { + URL url = UpdateServiceHelper.getUpdateUrl(force); + + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document dom = builder.parse(url.openConnection().getInputStream()); + + NodeList nodes = dom.getElementsByTagName("update"); + if (nodes == null || nodes.getLength() == 0) + return null; + + Node updateNode = nodes.item(0); + Node buildIdNode = updateNode.getAttributes().getNamedItem("buildID"); + if (buildIdNode == null) + return null; + + nodes = dom.getElementsByTagName("patch"); + if (nodes == null || nodes.getLength() == 0) + return null; + + Node patchNode = nodes.item(0); + Node urlNode = patchNode.getAttributes().getNamedItem("URL"); + Node hashFunctionNode = patchNode.getAttributes().getNamedItem("hashFunction"); + Node hashValueNode = patchNode.getAttributes().getNamedItem("hashValue"); + Node sizeNode = patchNode.getAttributes().getNamedItem("size"); + + if (urlNode == null || hashFunctionNode == null || + hashValueNode == null || sizeNode == null) { + return null; + } + + // Fill in UpdateInfo from the XML data + UpdateInfo info = new UpdateInfo(); + info.url = new URL(urlNode.getTextContent()); + info.buildID = buildIdNode.getTextContent(); + info.hashFunction = hashFunctionNode.getTextContent(); + info.hashValue = hashValueNode.getTextContent(); + + try { + info.size = Integer.parseInt(sizeNode.getTextContent()); + } catch (NumberFormatException e) { + Log.e(LOGTAG, "Failed to find APK size: ", e); + return null; + } + + // Make sure we have all the stuff we need to apply the update + if (!info.isValid()) { + Log.e(LOGTAG, "missing some required update information, have: " + info); + return null; + } + + return info; + } catch (Exception e) { + Log.e(LOGTAG, "failed to check for update: ", e); + return null; + } + } + + private MessageDigest createMessageDigest(String hashFunction) { + String javaHashFunction = null; + + if ("sha512".equals(hashFunction)) { + javaHashFunction = "SHA-512"; + } else { + Log.e(LOGTAG, "Unhandled hash function: " + hashFunction); + return null; + } + + try { + return MessageDigest.getInstance(javaHashFunction); + } catch (java.security.NoSuchAlgorithmException e) { + Log.e(LOGTAG, "Couldn't find algorithm " + javaHashFunction, e); + return null; + } + } + + private void showDownloadNotification() { + showDownloadNotification(null); + } + + private void showDownloadNotification(File downloadFile) { + Notification notification = new Notification(android.R.drawable.stat_sys_download, getResources().getString(R.string.updater_downloading_ticker), System.currentTimeMillis()); + + Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE); + notificationIntent.setClass(this, UpdateService.class); + + if (downloadFile != null) + notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, downloadFile.getAbsolutePath()); + + PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + notification.setLatestEventInfo(this, getResources().getString(R.string.updater_downloading_title), + mApplyImmediately ? getResources().getString(R.string.updater_downloading_willapply) : + getResources().getString(R.string.updater_downloading_select), + contentIntent); + + mNotificationManager.notify(NOTIFICATION_ID, notification); + } + + private void showDownloadFailure() { + Notification notification = new Notification(android.R.drawable.stat_sys_warning, getResources().getString(R.string.updater_downloading_ticker_failed), System.currentTimeMillis()); + + Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE); + notificationIntent.setClass(this, UpdateService.class); + + PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + notification.setLatestEventInfo(this, getResources().getString(R.string.updater_downloading_title), + getResources().getString(R.string.updater_downloading_retry), + contentIntent); + + mNotificationManager.notify(NOTIFICATION_ID, notification); + } + + private File downloadUpdatePackage(UpdateInfo info, boolean overwriteExisting) { + File downloadFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), new File(info.url.getFile()).getName()); + + if (!overwriteExisting && info.buildID.equals(getLastBuildID()) && downloadFile.exists()) { + // The last saved buildID is the same as the one for the current update. We also have a file + // already downloaded, so it's probably the package we want. Verify it to be sure and just + // return that if it matches. + + if (verifyDownloadedPackage(downloadFile)) { + Log.i(LOGTAG, "using existing update package"); + return downloadFile; + } else { + // Didn't match, so we're going to download a new one. + downloadFile.delete(); + } + } + + Log.i(LOGTAG, "downloading update package"); + + OutputStream output = null; + InputStream input = null; + + mDownloading = true; + showDownloadNotification(downloadFile); + + try { + URLConnection conn = info.url.openConnection(); + int length = conn.getContentLength(); + + output = new BufferedOutputStream(new FileOutputStream(downloadFile)); + input = new BufferedInputStream(conn.getInputStream()); + + byte[] buf = new byte[BUFSIZE]; + int len = 0; + + int bytesRead = 0; + float lastPercent = 0.0f; + + while ((len = input.read(buf, 0, BUFSIZE)) > 0) { + output.write(buf, 0, len); + bytesRead += len; + } + + Log.i(LOGTAG, "completed update download!"); + + mNotificationManager.cancel(NOTIFICATION_ID); + + return downloadFile; + } catch (Exception e) { + downloadFile.delete(); + showDownloadFailure(); + + Log.e(LOGTAG, "failed to download update: ", e); + return null; + } finally { + try { + if (input != null) + input.close(); + } catch (java.io.IOException e) {} + + try { + if (output != null) + output.close(); + } catch (java.io.IOException e) {} + + mDownloading = false; + } + } + + private boolean verifyDownloadedPackage(File updateFile) { + MessageDigest digest = createMessageDigest(getLastHashFunction()); + if (digest == null) + return false; + + InputStream input = null; + + try { + input = new BufferedInputStream(new FileInputStream(updateFile)); + + byte[] buf = new byte[BUFSIZE]; + int len; + while ((len = input.read(buf, 0, BUFSIZE)) > 0) { + digest.update(buf, 0, len); + } + } catch (java.io.IOException e) { + Log.e(LOGTAG, "Failed to verify update package: ", e); + return false; + } finally { + try { + if (input != null) + input.close(); + } catch(java.io.IOException e) {} + } + + String hex = Hex.encodeHexString(digest.digest()); + if (!hex.equals(getLastHashValue())) { + Log.e(LOGTAG, "Package hash does not match"); + return false; + } + + return true; + } + + private void applyUpdate(String updatePath) { + applyUpdate(new File(updatePath)); + } + + private void applyUpdate(File updateFile) { + mApplyImmediately = false; + + if (!updateFile.exists()) + return; + + Log.i(LOGTAG, "Verifying package: " + updateFile); + + if (!verifyDownloadedPackage(updateFile)) { + Log.e(LOGTAG, "Not installing update, failed verification"); + return; + } + + if (checkRoot()) + applyUpdateWithRoot(updateFile); + else + applyUpdateWithActivity(updateFile); + } + + private void applyUpdateWithRoot(File updateFile) { + mNotificationManager.cancel(NOTIFICATION_ID); + + Notification notification = new Notification(R.drawable.icon, getResources().getString(R.string.updater_installing_ticker), System.currentTimeMillis()); + notification.flags = Notification.FLAG_NO_CLEAR; + + Intent notificationIntent = new Intent("org.mozilla.gecko.ACTION_NOOP"); + notificationIntent.setClass(this, UpdateService.class); + + PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, 0); + notification.flags = Notification.FLAG_NO_CLEAR; + + notification.setLatestEventInfo(this, getResources().getString(R.string.updater_installing_title), + getResources().getString(R.string.updater_installing_text), + contentIntent); + + mNotificationManager.notify(NOTIFICATION_ID, notification); + + int result = runAsRoot("pm install " + updateFile.getAbsolutePath()); + Log.i(LOGTAG, "install result = " + result); + + updateFile.delete(); + + int tickerText = result == 0 ? R.string.updater_installing_ticker_success : R.string.updater_installing_ticker_fail; + int contentText = result == 0 ? R.string.updater_installing_text_success : R.string.updater_installing_text_fail; + + mNotificationManager.cancel(NOTIFICATION_ID); + + notification = new Notification(R.drawable.icon, getResources().getString(tickerText), System.currentTimeMillis()); + notification.flags = Notification.FLAG_NO_CLEAR; + + notificationIntent = new Intent(Intent.ACTION_MAIN); + notificationIntent.setClassName(getPackageName(), getPackageName() + ".App"); + notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); + notification.flags = Notification.FLAG_NO_CLEAR; + + notification.setLatestEventInfo(this, getResources().getString(R.string.updater_installing_title), + getResources().getString(result == 0 ? R.string.updater_installing_text_success : R.string.updater_installing_text_fail), + contentIntent); + + mNotificationManager.notify(NOTIFICATION_ID, notification); + + + notification.setLatestEventInfo(this, getResources().getString(R.string.updater_installing_title), + getResources().getString(result == 0 ? R.string.updater_installing_text_success : R.string.updater_installing_text_fail), + contentIntent); + notification.flags = Notification.FLAG_AUTO_CANCEL; + mNotificationManager.notify(NOTIFICATION_ID, notification); + } + + private void applyUpdateWithActivity(File updateFile) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(updateFile), "application/vnd.android.package-archive"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + + private String getLastBuildID() { + return mPrefs.getString(KEY_LAST_BUILDID, null); + } + + private String getLastHashFunction() { + return mPrefs.getString(KEY_LAST_HASH_FUNCTION, null); + } + + private String getLastHashValue() { + return mPrefs.getString(KEY_LAST_HASH_VALUE, null); + } + + private Calendar getLastAttemptDate() { + long lastAttempt = mPrefs.getLong(KEY_LAST_ATTEMPT_DATE, -1); + if (lastAttempt < 0) + return null; + + GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); + cal.setTimeInMillis(lastAttempt); + return cal; + } + + private void setLastAttemptDate() { + SharedPreferences.Editor editor = mPrefs.edit(); + editor.putLong(KEY_LAST_ATTEMPT_DATE, System.currentTimeMillis()); + editor.commit(); + } + + private void saveUpdateInfo(UpdateInfo info) { + SharedPreferences.Editor editor = mPrefs.edit(); + editor.putString(KEY_LAST_BUILDID, info.buildID); + editor.putString(KEY_LAST_HASH_FUNCTION, info.hashFunction); + editor.putString(KEY_LAST_HASH_VALUE, info.hashValue); + editor.commit(); + } + + private int runAsRoot(String command) { + Process p = null; + try { + p = Runtime.getRuntime().exec("su"); + OutputStream output = p.getOutputStream(); + output.write(command.getBytes()); + output.write(new String("; exit\n").getBytes()); + output.flush(); + p.waitFor(); + + return p.exitValue(); + } catch (Exception e) { + return -1; + } finally { + if (p != null) + p.destroy(); + } + } + + private boolean checkRoot() { + return runAsRoot("echo woooooo") == 0; + } + + private class UpdateInfo { + public URL url; + public String buildID; + public String hashFunction; + public String hashValue; + public int size; + + private boolean isNonEmpty(String s) { + return s != null && s.length() > 0; + } + + public boolean isValid() { + return url != null && isNonEmpty(buildID) && + isNonEmpty(hashFunction) && isNonEmpty(hashValue) && size > 0; + } + + @Override + public String toString() { + return "url = " + url + ", buildID = " + buildID + ", hashFunction = " + hashFunction + ", hashValue = " + hashValue + ", size = " + size; + } + } +} diff --git a/mobile/android/base/UpdateServiceHelper.java.in b/mobile/android/base/UpdateServiceHelper.java.in new file mode 100644 index 00000000000..01018126bef --- /dev/null +++ b/mobile/android/base/UpdateServiceHelper.java.in @@ -0,0 +1,79 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +#filter substitution + +package org.mozilla.gecko.updater; + +import android.content.Context; +import android.content.Intent; + +import android.os.Build; + +import android.util.Log; + +import java.net.URL; + +import java.util.Locale; + +public class UpdateServiceHelper { + public static final String ACTION_REGISTER_FOR_UPDATES = "@ANDROID_PACKAGE_NAME@.REGISTER_FOR_UPDATES"; + public static final String ACTION_UNREGISTER_FOR_UPDATES = "@ANDROID_PACKAGE_NAME@.UNREGISTER_FOR_UPDATES"; + public static final String ACTION_CHECK_FOR_UPDATE = "@ANDROID_PACKAGE_NAME@.CHECK_FOR_UPDATE"; + public static final String ACTION_CHECK_UPDATE_RESULT = "@ANDROID_PACKAGE_NAME@.CHECK_UPDATE_RESULT"; + public static final String ACTION_APPLY_UPDATE = "@ANDROID_PACKAGE_NAME@.APPLY_UPDATE"; + + // Flags for ACTION_CHECK_FOR_UPDATE + public static final int FLAG_FORCE_DOWNLOAD = 1; + public static final int FLAG_OVERWRITE_EXISTING = 1 << 1; + public static final int FLAG_REINSTALL = 1 << 2; + public static final int FLAG_RETRY = 1 << 3; + + // Name of the Intent extra that holds the flags for ACTION_CHECK_FOR_UPDATE + public static final String EXTRA_UPDATE_FLAGS_NAME = "updateFlags"; + + // Name of the Intent extra that holds the APK path, used with ACTION_APPLY_UPDATE + public static final String EXTRA_PACKAGE_PATH_NAME = "packagePath"; + + public static final String UPDATE_CHANNEL = "@MOZ_UPDATE_CHANNEL@"; + + private static final String LOGTAG = "UpdateServiceHelper"; + private static final String BUILDID = "@MOZ_APP_BUILDID@"; + +#ifdef MOZ_PKG_SPECIAL + private static final String UPDATE_URL = "https://aus2.mozilla.org/update/4/@MOZ_APP_BASENAME@/@MOZ_APP_VERSION@/%BUILDID%/Android_@MOZ_APP_ABI@-@MOZ_PKG_SPECIAL@/%LOCALE%/@MOZ_UPDATE_CHANNEL@/%OS_VERSION%/default/default/@MOZ_APP_VERSION@/update.xml"; +#else + private static final String UPDATE_URL = "https://aus2.mozilla.org/update/4/@MOZ_APP_BASENAME@/@MOZ_APP_VERSION@/%BUILDID%/Android_@MOZ_APP_ABI@/%LOCALE%/@MOZ_UPDATE_CHANNEL@/%OS_VERSION%/default/default/@MOZ_APP_VERSION@/update.xml"; +#endif + + public static URL getUpdateUrl(boolean force) { + Locale locale = Locale.getDefault(); + String url = UPDATE_URL.replace("%LOCALE%", locale.getLanguage() + "-" + locale.getCountry()). + replace("%OS_VERSION%", Build.VERSION.RELEASE). + replace("%BUILDID%", force ? "0" : BUILDID); + + try { + return new URL(url); + } catch (java.net.MalformedURLException e) { + Log.e(LOGTAG, "Failed to create update url: ", e); + return null; + } + } + + public static boolean isUpdaterEnabled() { +#ifdef MOZ_UPDATER + return true; +#else + return false; +#endif + } + + public static void registerForUpdates(Context context) { + if (!isUpdaterEnabled()) + return; + + context.startService(new Intent(UpdateServiceHelper.ACTION_REGISTER_FOR_UPDATES, null, context, UpdateService.class)); + } +} diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd index 9c4267d9a54..c2a46cb4ee2 100644 --- a/mobile/android/base/locales/en-US/android_strings.dtd +++ b/mobile/android/base/locales/en-US/android_strings.dtd @@ -224,3 +224,27 @@ just addresses the organization to follow, e.g. "This site is run by " --> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/base/strings.xml.in b/mobile/android/base/strings.xml.in index 7b0151b14e8..54f038b0dd5 100644 --- a/mobile/android/base/strings.xml.in +++ b/mobile/android/base/strings.xml.in @@ -210,4 +210,29 @@ &bookmarkhistory_import_wait; &webapp_generic_name; + + + &updater_start_title; + &updater_start_ticker; + &updater_start_select; + + &updater_downloading_title; + &updater_downloading_ticker; + &updater_downloading_ticker_failed; + &updater_downloading_select; + &updater_downloading_retry; + &updater_downloading_willapply; + + &updater_apply_title; + &updater_apply_ticker; + &updater_apply_select; + + &updater_installing_title; + &updater_installing_ticker; + &updater_installing_ticker_success; + &updater_installing_ticker_fail; + &updater_installing_text; + &updater_installing_text_success; + &updater_installing_text_fail; + diff --git a/mobile/android/chrome/content/about.xhtml b/mobile/android/chrome/content/about.xhtml index f1955ec1b52..d84fac8c1b7 100644 --- a/mobile/android/chrome/content/about.xhtml +++ b/mobile/android/chrome/content/about.xhtml @@ -101,80 +101,29 @@ isChecking: false, update: null, - get updateEnabled() { - try { - return Services.prefs.getBoolPref("app.update.enabled"); - } - catch (e) { } - return true; // Mobile default is true + init: function() { + Services.obs.addObserver(this, "Update:CheckResult", false); }, - startUpdate: function() { - if (!this.update) - this.update = this.um.activeUpdate; - - this.aus.pauseDownload(); - - let updateTimerCallback = this.aus.QueryInterface(Ci.nsITimerCallback); - updateTimerCallback.notify(null); - } + observe: function(aSubject, aTopic, aData) { + if (aTopic == "Update:CheckResult") { + showUpdateMessage(aData == "true"); + } + }, }; - XPCOMUtils.defineLazyServiceGetter(Updater, "aus", - "@mozilla.org/updates/update-service;1", - "nsIApplicationUpdateService"); - XPCOMUtils.defineLazyServiceGetter(Updater, "checker", - "@mozilla.org/updates/update-checker;1", - "nsIUpdateChecker"); - XPCOMUtils.defineLazyServiceGetter(Updater, "um", - "@mozilla.org/updates/update-manager;1", - "nsIUpdateManager"); - - let UpdateCheckListener = { - onProgress: function(aRequest, aPosition, aTotalSize) { - }, - - onCheckComplete: function(aRequest, aUpdates, aUpdateCount) { - Updater.isChecking = false; - - Updater.update = Updater.aus.selectUpdate(aUpdates, aUpdates.length); - if (!Updater.update || !Updater.aus.canApplyUpdates) { - showUpdateMessage(false); - return; - } - - if (!Updater.update.appVersion || Services.vc.compare(Updater.update.appVersion, Services.appinfo.version) < 0) { - showUpdateMessage(false); - return; - } - - showUpdateMessage(true); - Updater.startUpdate(); - }, - - onError: function(aRequest, aUpdate) { - // Errors in the update check are treated as no updates found. If the - // update check fails repeatedly without a success the user will be - // notified with the normal app update user interface so this is safe. - Updater.isChecking = false; - showUpdateMessage(false); - }, - - QueryInterface: function(aIID) { - if (!aIID.equals(Ci.nsIUpdateCheckListener) && !aIID.equals(Ci.nsISupports)) - throw Cr.NS_ERROR_NO_INTERFACE; - return this; - } - }; - - if (!Updater.updateEnabled) - document.getElementById("updateBox").style.display = "none"; + Updater.init(); function checkForUpdates() { Updater.isChecking = true; showCheckingMessage(); - Updater.checker.checkForUpdates(UpdateCheckListener, true); + let bridge = Cc["@mozilla.org/android/bridge;1"].getService(Ci.nsIAndroidBridge); + bridge.handleGeckoMessage(JSON.stringify({ + gecko: { + type: "Update:Check", + } + })); } let updateLink = document.getElementById("updateLink"); diff --git a/mobile/android/components/Makefile.in b/mobile/android/components/Makefile.in index dcb91864c17..4b5155b3acc 100644 --- a/mobile/android/components/Makefile.in +++ b/mobile/android/components/Makefile.in @@ -43,8 +43,4 @@ ifdef MOZ_SAFE_BROWSING EXTRA_COMPONENTS += SafeBrowsing.js endif -ifdef MOZ_UPDATER -EXTRA_COMPONENTS += UpdatePrompt.js -endif - include $(topsrcdir)/config/rules.mk diff --git a/mobile/android/components/MobileComponents.manifest b/mobile/android/components/MobileComponents.manifest index 1f4260f382d..7318e385a67 100644 --- a/mobile/android/components/MobileComponents.manifest +++ b/mobile/android/components/MobileComponents.manifest @@ -96,9 +96,3 @@ contract @mozilla.org/safebrowsing/application;1 {aadaed90-6c03-42d0-924a-fc6119 category app-startup SafeBrowsing service,@mozilla.org/safebrowsing/application;1 #endif -#ifdef MOZ_UPDATER -# UpdatePrompt.js -component {88b3eb21-d072-4e3b-886d-f89d8c49fe59} UpdatePrompt.js -contract @mozilla.org/updates/update-prompt;1 {88b3eb21-d072-4e3b-886d-f89d8c49fe59} -#endif - diff --git a/mobile/android/components/UpdatePrompt.js b/mobile/android/components/UpdatePrompt.js deleted file mode 100644 index 12338794829..00000000000 --- a/mobile/android/components/UpdatePrompt.js +++ /dev/null @@ -1,316 +0,0 @@ -/* 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/. */ - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -const UPDATE_NOTIFICATION_NAME = "update-app"; -const UPDATE_NOTIFICATION_ICON = "drawable://alert_download"; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - - -XPCOMUtils.defineLazyGetter(this, "gUpdateBundle", function aus_gUpdateBundle() { - return Services.strings.createBundle("chrome://mozapps/locale/update/updates.properties"); -}); - -XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function aus_gBrandBundle() { - return Services.strings.createBundle("chrome://branding/locale/brand.properties"); -}); - -XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function aus_gBrowserBundle() { - return Services.strings.createBundle("chrome://browser/locale/browser.properties"); -}); - -XPCOMUtils.defineLazyGetter(this, "AddonManager", function() { - Cu.import("resource://gre/modules/AddonManager.jsm"); - return AddonManager; -}); - -XPCOMUtils.defineLazyGetter(this, "LocaleRepository", function() { - Cu.import("resource://gre/modules/LocaleRepository.jsm"); - return LocaleRepository; -}); - -function getPref(func, preference, defaultValue) { - try { - return Services.prefs[func](preference); - } catch (e) {} - return defaultValue; -} - -function sendMessageToJava(aMsg) { - let data = Cc["@mozilla.org/android/bridge;1"].getService(Ci.nsIAndroidBridge).handleGeckoMessage(JSON.stringify(aMsg)); - return JSON.parse(data); -} - -// ----------------------------------------------------------------------- -// Update Prompt -// ----------------------------------------------------------------------- - -function UpdatePrompt() { } - -UpdatePrompt.prototype = { - classID: Components.ID("{88b3eb21-d072-4e3b-886d-f89d8c49fe59}"), - QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdatePrompt, Ci.nsIRequestObserver, Ci.nsIProgressEventSink]), - - get _enabled() { - return !getPref("getBoolPref", "app.update.silent", false); - }, - - _showNotification: function UP__showNotif(aUpdate, aTitle, aText, aImageUrl, aMode) { - let observer = { - updatePrompt: this, - observe: function (aSubject, aTopic, aData) { - switch (aTopic) { - case "alertclickcallback": - this.updatePrompt._handleUpdate(aUpdate, aMode); - break; - } - } - }; - - let notifier = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService); - notifier.showAlertNotification(aImageUrl, aTitle, aText, true, "", observer, UPDATE_NOTIFICATION_NAME); - }, - - _handleUpdate: function UP__handleUpdate(aUpdate, aMode) { - if (aMode == "available") { - let window = Services.wm.getMostRecentWindow("navigator:browser"); - let title = gUpdateBundle.GetStringFromName("updatesfound_" + aUpdate.type + ".title"); - let brandName = gBrandBundle.GetStringFromName("brandShortName"); - - // Unconditionally use the "major" type here as for now it is always a new version - // without additional description required for a minor update message - let message = gUpdateBundle.formatStringFromName("intro_major", [brandName, aUpdate.displayVersion], 2); - let button0 = gUpdateBundle.GetStringFromName("okButton"); - let button1 = gUpdateBundle.GetStringFromName("askLaterButton"); - let prompt = Services.prompt; - let flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_IS_STRING + prompt.BUTTON_POS_1 * prompt.BUTTON_TITLE_IS_STRING; - - let download = (prompt.confirmEx(window, title, message, flags, button0, button1, null, null, {value: false}) == 0); - if (download) { - // Start downloading the update in the background - let aus = Cc["@mozilla.org/updates/update-service;1"].getService(Ci.nsIApplicationUpdateService); - if (aus.downloadUpdate(aUpdate, true) != "failed") { - let title = gBrowserBundle.formatStringFromName("alertDownloadsStart", [aUpdate.name], 1); - this._showNotification(aUpdate, title, "", UPDATE_NOTIFICATION_ICON, "download"); - - // Add this UI as a listener for active downloads - aus.addDownloadListener(this); - } - } - } else if(aMode == "downloaded") { - // Notify all windows that an application quit has been requested - let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); - Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); - - // If nothing aborted, restart the app - if (cancelQuit.data == false) { - sendMessageToJava({ - gecko: { - type: "Update:Restart" - } - }); - } - } - }, - - _updateDownloadProgress: function UP__updateDownloadProgress(aProgress, aTotal) { - let alertsService = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService); - let progressListener = alertsService.QueryInterface(Ci.nsIAlertsProgressListener); - if (progressListener) - progressListener.onProgress(UPDATE_NOTIFICATION_NAME, aProgress, aTotal); - }, - - // ------------------------- - // nsIUpdatePrompt interface - // ------------------------- - - // Right now this is used only to check for updates in progress - checkForUpdates: function UP_checkForUpdates() { - if (!this._enabled) - return; - - let aus = Cc["@mozilla.org/updates/update-service;1"].getService(Ci.nsIApplicationUpdateService); - if (!aus.isDownloading) - return; - - let updateManager = Cc["@mozilla.org/updates/update-manager;1"].getService(Ci.nsIUpdateManager); - - let updateName = updateManager.activeUpdate ? updateManager.activeUpdate.name : gBrandBundle.GetStringFromName("brandShortName"); - let title = gBrowserBundle.formatStringFromName("alertDownloadsStart", [updateName], 1); - - this._showNotification(updateManager.activeUpdate, title, "", UPDATE_NOTIFICATION_ICON, "downloading"); - - aus.removeDownloadListener(this); // just in case it's already added - aus.addDownloadListener(this); - }, - - showUpdateAvailable: function UP_showUpdateAvailable(aUpdate) { - if (!this._enabled) - return; - - const PREF_APP_UPDATE_SKIPNOTIFICATION = "app.update.skipNotification"; - - if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SKIPNOTIFICATION) && - Services.prefs.getBoolPref(PREF_APP_UPDATE_SKIPNOTIFICATION)) { - Services.prefs.setBoolPref(PREF_APP_UPDATE_SKIPNOTIFICATION, false); - - // Notification was already displayed and clicked, so jump to the next step: - // ask the user about downloading update - this._handleUpdate(aUpdate, "available"); - return; - } - - let stringsPrefix = "updateAvailable_" + aUpdate.type + "."; - let title = gUpdateBundle.formatStringFromName(stringsPrefix + "title", [aUpdate.name], 1); - let text = gUpdateBundle.GetStringFromName(stringsPrefix + "text"); - let imageUrl = ""; - this._showNotification(aUpdate, title, text, imageUrl, "available"); - }, - - showUpdateDownloaded: function UP_showUpdateDownloaded(aUpdate, aBackground) { - if (!this._enabled) - return; - - // uninstall all installed locales - AddonManager.getAddonsByTypes(["locale"], (function (aAddons) { - if (aAddons.length > 0) { - let listener = this.getAddonListener(aUpdate, this); - AddonManager.addAddonListener(listener); - aAddons.forEach(function(aAddon) { - listener._uninstalling.push(aAddon.id); - aAddon.uninstall(); - }, this); - } else { - this._showDownloadedNotification(aUpdate); - } - }).bind(this)); - }, - - _showDownloadedNotification: function UP_showDlNotification(aUpdate) { - let stringsPrefix = "updateDownloaded_" + aUpdate.type + "."; - let title = gUpdateBundle.formatStringFromName(stringsPrefix + "title", [aUpdate.name], 1); - let text = gUpdateBundle.GetStringFromName(stringsPrefix + "text"); - let imageUrl = ""; - this._showNotification(aUpdate, title, text, imageUrl, "downloaded"); - }, - - _uninstalling: [], - _installing: [], - _currentUpdate: null, - - _reinstallLocales: function UP_reinstallLocales(aUpdate, aListener, aPending) { - LocaleRepository.getLocales((function(aLocales) { - aLocales.forEach(function(aLocale, aIndex, aArray) { - let index = aPending.indexOf(aLocale.addon.id); - if (index > -1) { - aListener._installing.push(aLocale.addon.id); - aLocale.addon.install.install(); - } - }, this); - // store the buildid of these locales so that we can disable locales when the - // user updates through a non-updater channel - Services.prefs.setCharPref("extensions.compatability.locales.buildid", aUpdate.buildID); - }).bind(this), { buildID: aUpdate.buildID }); - }, - - showUpdateInstalled: function UP_showUpdateInstalled() { - if (!this._enabled || !getPref("getBoolPref", "app.update.showInstalledUI", false)) - return; - - let title = gBrandBundle.GetStringFromName("brandShortName"); - let text = gUpdateBundle.GetStringFromName("installSuccess"); - let imageUrl = ""; - this._showNotification(aUpdate, title, text, imageUrl, "installed"); - }, - - showUpdateError: function UP_showUpdateError(aUpdate) { - if (!this._enabled) - return; - - if (aUpdate.state == "failed") { - var title = gBrandBundle.GetStringFromName("brandShortName"); - let text = gUpdateBundle.GetStringFromName("updaterIOErrorTitle"); - let imageUrl = ""; - this._showNotification(aUpdate, title, text, imageUrl, "error"); - } - }, - - showUpdateHistory: function UP_showUpdateHistory(aParent) { - // NOT IMPL - }, - - // ---------------------------- - // nsIRequestObserver interface - // ---------------------------- - - // When the data transfer begins - onStartRequest: function(request, context) { - // NOT IMPL - }, - - // When the data transfer ends - onStopRequest: function(request, context, status) { - let alertsService = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService); - let progressListener = alertsService.QueryInterface(Ci.nsIAlertsProgressListener); - if (progressListener) - progressListener.onCancel(UPDATE_NOTIFICATION_NAME); - - - let aus = Cc["@mozilla.org/updates/update-service;1"].getService(Ci.nsIApplicationUpdateService); - aus.removeDownloadListener(this); - }, - - // ------------------------------ - // nsIProgressEventSink interface - // ------------------------------ - - // When new data has been downloaded - onProgress: function(request, context, progress, maxProgress) { - this._updateDownloadProgress(progress, maxProgress); - }, - - // When we have new status text - onStatus: function(request, context, status, statusText) { - // NOT IMPL - }, - - // ------------------------------- - // AddonListener - // ------------------------------- - getAddonListener: function(aUpdate, aUpdatePrompt) { - return { - _installing: [], - _uninstalling: [], - onInstalling: function(aAddon, aNeedsRestart) { - let index = this._installing.indexOf(aAddon.id); - if (index > -1) - this._installing.splice(index, 1); - - if (this._installing.length == 0) { - aUpdatePrompt._showDownloadedNotification(aUpdate); - AddonManager.removeAddonListener(this); - } - }, - - onUninstalling: function(aAddon, aNeedsRestart) { - let pending = []; - let index = this._uninstalling.indexOf(aAddon.id); - if (index > -1) { - pending.push(aAddon.id); - this._uninstalling.splice(index, 1); - } - if (this._uninstalling.length == 0) - aUpdatePrompt._reinstallLocales(aUpdate, this, pending); - } - } - } - -}; - -const NSGetFactory = XPCOMUtils.generateNSGetFactory([UpdatePrompt]); diff --git a/mobile/android/installer/package-manifest.in b/mobile/android/installer/package-manifest.in index d713dddadd0..aef140e31f9 100644 --- a/mobile/android/installer/package-manifest.in +++ b/mobile/android/installer/package-manifest.in @@ -516,9 +516,6 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DLL_SUFFIX@ #ifdef MOZ_SAFE_BROWSING @BINPATH@/components/SafeBrowsing.js #endif -#ifdef MOZ_UPDATER -@BINPATH@/components/UpdatePrompt.js -#endif @BINPATH@/components/XPIDialogService.js @BINPATH@/components/browsercomps.xpt @BINPATH@/extensions/feedback@mobile.mozilla.org.xpi