--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -378,22 +378,54 @@
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<service
android:exported="false"
- android:name="org.mozilla.gecko.updater.UpdateService"
+ android:name="org.mozilla.gecko.updater.UpdatesApplyService"
+ android:permission="android.permission.BIND_JOB_SERVICE"
+ android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
+ </service>
+
+ <service
+ android:exported="false"
+ android:name="org.mozilla.gecko.updater.UpdatesCheckService"
+ android:permission="android.permission.BIND_JOB_SERVICE"
android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
</service>
<service
android:exported="false"
+ android:name="org.mozilla.gecko.updater.UpdatesDownloadService"
+ android:permission="android.permission.BIND_JOB_SERVICE"
+ android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
+ </service>
+
+ <service
+ android:exported="false"
+ android:name="org.mozilla.gecko.updater.UpdatesRegisterService"
+ android:permission="android.permission.BIND_JOB_SERVICE"
+ android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
+ </service>
+
+ <receiver
+ android:name="org.mozilla.gecko.updater.UpdateServiceReceiver"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="@ANDROID_PACKAGE_NAME@.CHECK_FOR_UPDATE"></action>
+ <action android:name="@ANDROID_PACKAGE_NAME@.APPLY_UPDATE"></action>
+ <action android:name="@ANDROID_PACKAGE_NAME@.DOWNLOAD_UPDATE"></action>
+ </intent-filter>
+ </receiver>
+
+ <service
+ android:exported="false"
android:name="org.mozilla.gecko.notifications.NotificationService">
</service>
<service
android:exported="false"
android:name="org.mozilla.gecko.dlc.DlcStudyService"
android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
--- a/mobile/android/base/java/org/mozilla/gecko/JobIdsConstants.java
+++ b/mobile/android/base/java/org/mozilla/gecko/JobIdsConstants.java
@@ -31,16 +31,21 @@ public class JobIdsConstants {
private static final int JOB_ID_PROFILE_FETCH = 1005;
private static final int JOB_ID_PROFILE_DELETE = 1006;
private static final int JOB_ID_TELEMETRY_UPLOAD = 1007;
private static final int JOB_ID_FILE_CLEANUP = 1008;
+ private static final int JOB_ID_UPDATES_REGISTER = 1009;
+ private static final int JOB_ID_UPDATES_CHECK_FOR = 1010;
+ private static final int JOB_ID_UPDATES_DOWNLOAD = 1011;
+ private static final int JOB_ID_UPDATES_APPLY = 1012;
+
public static int getIdForDlcStudyJob() {
return getIdWithOffset(JOB_ID_DLC_STUDY);
}
public static int getIdForDlcDownloadJob() {
return getIdWithOffset(JOB_ID_DLC_DOWNLOAD);
}
@@ -67,16 +72,32 @@ public class JobIdsConstants {
public static int getIdForTelemetryUploadJob() {
return getIdWithOffset(JOB_ID_TELEMETRY_UPLOAD);
}
public static int getIdForFileCleanupJob() {
return getIdWithOffset(JOB_ID_FILE_CLEANUP);
}
+ public static int getIdForUpdatesRegisterJob() {
+ return getIdWithOffset(JOB_ID_UPDATES_REGISTER);
+ }
+
+ public static int getIdForUpdatesCheckJob() {
+ return getIdWithOffset(JOB_ID_UPDATES_CHECK_FOR);
+ }
+
+ public static int getIdForUpdatesDownloadJob() {
+ return getIdWithOffset(JOB_ID_UPDATES_DOWNLOAD);
+ }
+
+ public static int getIdForUpdatesApplyJob() {
+ return getIdWithOffset(JOB_ID_UPDATES_APPLY);
+ }
+
private static boolean isReleaseBuild() {
return AppConstants.RELEASE_OR_BETA;
}
private static int getIdWithOffset(final int jobIdUsedInRelease) {
return isReleaseBuild() ? jobIdUsedInRelease : jobIdUsedInRelease + BETA_OFFSET;
}
}
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -70,18 +70,18 @@ import org.mozilla.gecko.TelemetryContra
import org.mozilla.gecko.TelemetryContract.Method;
import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
import org.mozilla.gecko.mma.MmaDelegate;
import org.mozilla.gecko.permissions.Permissions;
import org.mozilla.gecko.restrictions.Restrictable;
import org.mozilla.gecko.restrictions.Restrictions;
import org.mozilla.gecko.tabqueue.TabQueueHelper;
import org.mozilla.gecko.tabqueue.TabQueuePrompt;
-import org.mozilla.gecko.updater.UpdateService;
import org.mozilla.gecko.updater.UpdateServiceHelper;
+import org.mozilla.gecko.updater.UpdateServiceHelper.AutoDownloadPolicy;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.ContextUtils;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.InputOptionsUtils;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.ViewUtil;
@@ -1119,17 +1119,17 @@ public class GeckoPreferences
// Even though this is a list preference, we don't want to handle it
// below, so we return here.
return onLocaleSelected(Locales.getLanguageTag(lastLocale), (String) newValue);
}
if (PREFS_MENU_CHAR_ENCODING.equals(prefName)) {
setCharEncodingState(((String) newValue).equals("true"));
} else if (PREFS_UPDATER_AUTODOWNLOAD.equals(prefName)) {
- UpdateServiceHelper.setAutoDownloadPolicy(this, UpdateService.AutoDownloadPolicy.get((String) newValue));
+ UpdateServiceHelper.setAutoDownloadPolicy(this, AutoDownloadPolicy.get((String) newValue));
} else if (PREFS_UPDATER_URL.equals(prefName)) {
UpdateServiceHelper.setUpdateUrl(this, (String) newValue);
} else if (PREFS_HEALTHREPORT_UPLOAD_ENABLED.equals(prefName)) {
final Boolean newBooleanValue = (Boolean) newValue;
AdjustConstants.getAdjustHelper().setEnabled(newBooleanValue);
// If Health Report has been disabled Mma should also be stopped.
// If it was just enabled, we should also try to start Mma immediately
// provided that all the other requirements to start Mma are met.
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/PackageVerifier.java
@@ -0,0 +1,82 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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 android.util.Log;
+
+import org.mozilla.apache.commons.codec.binary.Hex;
+import org.mozilla.gecko.BuildConfig;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.MessageDigest;
+
+public class PackageVerifier {
+ private static final String LOGTAG = "GeckoPackageVerifier";
+ private static final boolean DEBUG = false;
+
+ static boolean verifyDownloadedPackage(final UpdatesPrefs prefs, final File updateFile, final int bufSize) {
+ MessageDigest digest = createMessageDigest(prefs.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) {
+ if (DEBUG) {
+ 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(prefs.getLastHashValue())) {
+ if (DEBUG) {
+ Log.e(LOGTAG, "Package hash does not match");
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ private static MessageDigest createMessageDigest(String hashFunction) {
+ String javaHashFunction;
+
+ if ("sha512".equalsIgnoreCase(hashFunction)) {
+ javaHashFunction = "SHA-512";
+ } else {
+ if (DEBUG) {
+ Log.e(LOGTAG, "Unhandled hash function: " + hashFunction);
+ }
+ return null;
+ }
+
+ try {
+ return MessageDigest.getInstance(javaHashFunction);
+ } catch (java.security.NoSuchAlgorithmException e) {
+ if (DEBUG) {
+ Log.e(LOGTAG, "Couldn't find algorithm " + javaHashFunction, e);
+ }
+ return null;
+ }
+ }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java
@@ -6,69 +6,81 @@
package org.mozilla.gecko.updater;
import android.content.res.AssetManager;
import android.content.SharedPreferences;
import android.util.Log;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.BuildConfig;
import org.mozilla.gecko.delegates.BrowserAppDelegateWithReference;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.util.IOUtils;
import org.mozilla.gecko.util.ThreadUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* Perform tasks in the background after the app has been installed/updated.
*/
public class PostUpdateHandler extends BrowserAppDelegateWithReference {
- private static final String LOGTAG = "PostUpdateHandler";
+ private static final String LOGTAG = "GeckoPostUpdateHandler";
+ private static final boolean DEBUG = false;
@Override
public void onStart(final BrowserApp browserApp) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final SharedPreferences prefs = GeckoSharedPrefs.forApp(browserApp);
// Check if this is a new installation or if the app has been updated since the last start.
if (!AppConstants.MOZ_APP_BUILDID.equals(prefs.getString(GeckoPreferences.PREFS_APP_UPDATE_LAST_BUILD_ID, null))) {
- Log.d(LOGTAG, "Build ID changed since last start: '" + AppConstants.MOZ_APP_BUILDID + "', '" + prefs.getString(GeckoPreferences.PREFS_APP_UPDATE_LAST_BUILD_ID, null) + "'");
+ if (DEBUG) {
+ Log.d(LOGTAG, "Build ID changed since last start: '" +
+ AppConstants.MOZ_APP_BUILDID +
+ "', '" +
+ prefs.getString(GeckoPreferences.PREFS_APP_UPDATE_LAST_BUILD_ID, null)
+ + "'");
+ }
// Copy the bundled system add-ons from the APK to the data directory.
copyFeaturesFromAPK(browserApp);
}
}
});
}
/**
* Copies the /assets/features folder out of the APK and into the app's data directory.
*/
private void copyFeaturesFromAPK(BrowserApp browserApp) {
- Log.d(LOGTAG, "Copying system add-ons from APK to dataDir");
+ if (DEBUG) {
+ Log.d(LOGTAG, "Copying system add-ons from APK to dataDir");
+ }
final String dataDir = browserApp.getApplicationInfo().dataDir;
final SharedPreferences prefs = GeckoSharedPrefs.forApp(browserApp);
final AssetManager assetManager = browserApp.getAssets();
try {
final String[] assetNames = assetManager.list("features");
for (int i = 0; i < assetNames.length; i++) {
final String assetPath = "features/" + assetNames[i];
- Log.d(LOGTAG, "Copying '" + assetPath + "' from APK to dataDir");
+ if (DEBUG) {
+ Log.d(LOGTAG, "Copying '" + assetPath + "' from APK to dataDir");
+ }
final InputStream assetStream = assetManager.open(assetPath);
final File outFile = getDataFile(dataDir, assetPath);
if (outFile == null) {
continue;
}
@@ -96,17 +108,19 @@ public class PostUpdateHandler extends B
*
* @return null if the parents could not be created.
*/
private File getDataFile(final String dataDir, final String name) {
File outFile = new File(dataDir, name);
File dir = outFile.getParentFile();
if (!dir.exists()) {
- Log.d(LOGTAG, "Creating " + dir.getAbsolutePath());
+ if (DEBUG) {
+ Log.d(LOGTAG, "Creating " + dir.getAbsolutePath());
+ }
if (!dir.mkdirs()) {
Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath());
return null;
}
}
return outFile;
}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java
+++ /dev/null
@@ -1,817 +0,0 @@
-/* -*- 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.AppConstants;
-import org.mozilla.gecko.CrashHandler;
-import org.mozilla.gecko.R;
-
-import org.mozilla.apache.commons.codec.binary.Hex;
-
-import org.mozilla.gecko.permissions.Permissions;
-import org.mozilla.gecko.util.IOUtils;
-import org.mozilla.gecko.util.ProxySelector;
-import org.w3c.dom.Document;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-
-import android.Manifest;
-import android.app.AlarmManager;
-import android.app.IntentService;
-import android.app.Notification;
-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.net.wifi.WifiManager;
-import android.net.wifi.WifiManager.WifiLock;
-import android.os.Environment;
-import android.provider.Settings;
-import android.support.v4.app.NotificationManagerCompat;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.content.FileProvider;
-import android.support.v4.net.ConnectivityManagerCompat;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.app.NotificationCompat.Builder;
-import android.util.Log;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URI;
-import java.net.URL;
-import java.net.URLConnection;
-import java.security.MessageDigest;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-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_FILE_NAME = "UpdateService.lastFileName";
- private static final String KEY_LAST_ATTEMPT_DATE = "UpdateService.lastAttemptDate";
- private static final String KEY_AUTODOWNLOAD_POLICY = "UpdateService.autoDownloadPolicy";
- private static final String KEY_UPDATE_URL = "UpdateService.updateUrl";
-
- private SharedPreferences mPrefs;
-
- private NotificationManagerCompat mNotificationManager;
- private ConnectivityManager mConnectivityManager;
- private Builder mBuilder;
-
- private volatile WifiLock mWifiLock;
-
- private boolean mDownloading;
- private boolean mCancelDownload;
- private boolean mApplyImmediately;
-
- private CrashHandler mCrashHandler;
-
- public enum AutoDownloadPolicy {
- NONE(-1),
- WIFI(0),
- DISABLED(1),
- ENABLED(2);
-
- public final int value;
-
- private AutoDownloadPolicy(int value) {
- this.value = value;
- }
-
- private final static AutoDownloadPolicy[] sValues = AutoDownloadPolicy.values();
-
- public static AutoDownloadPolicy get(int value) {
- for (AutoDownloadPolicy id: sValues) {
- if (id.value == value) {
- return id;
- }
- }
- return NONE;
- }
-
- public static AutoDownloadPolicy get(String name) {
- for (AutoDownloadPolicy id: sValues) {
- if (name.equalsIgnoreCase(id.toString())) {
- return id;
- }
- }
- return NONE;
- }
- }
-
- private enum CheckUpdateResult {
- // Keep these in sync with mobile/android/chrome/content/about.xhtml
- NOT_AVAILABLE,
- AVAILABLE,
- DOWNLOADING,
- DOWNLOADED
- }
-
-
- public UpdateService() {
- super("updater");
- }
-
- @Override
- public void onCreate () {
- mCrashHandler = CrashHandler.createDefaultCrashHandler(getApplicationContext());
-
- super.onCreate();
-
- mPrefs = getSharedPreferences(PREFS_NAME, 0);
- mNotificationManager = NotificationManagerCompat.from(this);
- mConnectivityManager = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
- mWifiLock = ((WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE))
- .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, PREFS_NAME);
- mCancelDownload = false;
- }
-
- @Override
- public void onDestroy() {
- mCrashHandler.unregister();
- mCrashHandler = null;
-
- if (mWifiLock.isHeld()) {
- mWifiLock.release();
- }
- }
-
- @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 if (UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD.equals(intent.getAction())) {
- mCancelDownload = true;
- } else {
- super.onStartCommand(intent, flags, startId);
- }
-
- return Service.START_REDELIVER_INTENT;
- }
-
- @Override
- protected void onHandleIntent (final Intent intent) {
- if (UpdateServiceHelper.ACTION_REGISTER_FOR_UPDATES.equals(intent.getAction())) {
- AutoDownloadPolicy policy = AutoDownloadPolicy.get(
- intent.getIntExtra(UpdateServiceHelper.EXTRA_AUTODOWNLOAD_NAME,
- AutoDownloadPolicy.NONE.value));
-
- if (policy != AutoDownloadPolicy.NONE) {
- setAutoDownloadPolicy(policy);
- }
-
- String url = intent.getStringExtra(UpdateServiceHelper.EXTRA_UPDATE_URL_NAME);
- if (url != null) {
- setUpdateUrl(url);
- }
-
- registerForUpdates(false);
- } else if (UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE.equals(intent.getAction())) {
- startUpdate(intent.getIntExtra(UpdateServiceHelper.EXTRA_UPDATE_FLAGS_NAME, 0));
- // Use this instead for forcing a download from about:fennec
- // startUpdate(UpdateServiceHelper.FLAG_FORCE_DOWNLOAD | UpdateServiceHelper.FLAG_REINSTALL);
- } else if (UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE.equals(intent.getAction())) {
- // We always want to do the download and apply it here
- mApplyImmediately = true;
- startUpdate(UpdateServiceHelper.FLAG_FORCE_DOWNLOAD);
- } 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(CheckUpdateResult result) {
- Intent resultIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT);
- resultIntent.putExtra("result", result.toString());
- sendBroadcast(resultIntent);
- }
-
- private int getUpdateInterval(boolean isRetry) {
- int interval;
- if (isRetry) {
- interval = INTERVAL_RETRY;
- } else if (!AppConstants.RELEASE_OR_BETA) {
- 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), PendingIntent.FLAG_UPDATE_CURRENT);
- 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(final int flags) {
- setLastAttemptDate();
-
- NetworkInfo netInfo = mConnectivityManager.getActiveNetworkInfo();
- if (netInfo == null || !netInfo.isConnected()) {
- Log.i(LOGTAG, "not connected to the network");
- registerForUpdates(true);
- sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
- return;
- }
-
- registerForUpdates(false);
-
- final UpdateInfo info = findUpdate(hasFlag(flags, UpdateServiceHelper.FLAG_REINSTALL));
- boolean haveUpdate = (info != null);
-
- if (!haveUpdate) {
- Log.i(LOGTAG, "no update available");
- sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
- return;
- }
-
- Log.i(LOGTAG, "update available, buildID = " + info.buildID);
-
- Permissions.from(this)
- .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- .doNotPrompt()
- .andFallback(new Runnable() {
- @Override
- public void run() {
- showPermissionNotification();
- sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
- }
- })
- .run(new Runnable() {
- @Override
- public void run() {
- startDownload(info, flags);
- }
- });
- }
-
- private void startDownload(UpdateInfo info, int flags) {
- AutoDownloadPolicy policy = getAutoDownloadPolicy();
-
- // We only start a download automatically if one of following criteria are met:
- //
- // - We have a FORCE_DOWNLOAD flag passed in
- // - The preference is set to 'always'
- // - The preference is set to 'wifi' and we are using a non-metered network (i.e. the user
- // is OK with large data transfers occurring)
- //
- boolean shouldStartDownload = hasFlag(flags, UpdateServiceHelper.FLAG_FORCE_DOWNLOAD) ||
- policy == AutoDownloadPolicy.ENABLED ||
- (policy == AutoDownloadPolicy.WIFI && !ConnectivityManagerCompat.isActiveNetworkMetered(mConnectivityManager));
-
- if (!shouldStartDownload) {
- Log.i(LOGTAG, "not initiating automatic update download due to policy " + policy.toString());
- sendCheckUpdateResult(CheckUpdateResult.AVAILABLE);
-
- // We aren't autodownloading here, so prompt to start the update
- Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE);
- notificationIntent.setClass(this, UpdateService.class);
- PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
- builder.setSmallIcon(R.drawable.ic_status_logo);
- builder.setWhen(System.currentTimeMillis());
- builder.setAutoCancel(true);
- builder.setContentTitle(getString(R.string.updater_start_title));
- builder.setContentText(getString(R.string.updater_start_select));
- builder.setContentIntent(contentIntent);
-
- mNotificationManager.notify(NOTIFICATION_ID, builder.build());
-
- return;
- }
-
- File pkg = downloadUpdatePackage(info, hasFlag(flags, UpdateServiceHelper.FLAG_OVERWRITE_EXISTING));
- if (pkg == null) {
- sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
- return;
- }
-
- Log.i(LOGTAG, "have update package at " + pkg);
-
- saveUpdateInfo(info, pkg);
- sendCheckUpdateResult(CheckUpdateResult.DOWNLOADED);
-
- if (mApplyImmediately) {
- applyUpdate(pkg);
- } else {
- // Prompt to apply the update
-
- 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);
-
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
- builder.setSmallIcon(R.drawable.ic_status_logo);
- builder.setWhen(System.currentTimeMillis());
- builder.setAutoCancel(true);
- builder.setContentTitle(getString(R.string.updater_apply_title));
- builder.setContentText(getString(R.string.updater_apply_select));
- builder.setContentIntent(contentIntent);
-
- mNotificationManager.notify(NOTIFICATION_ID, builder.build());
- }
- }
-
- private UpdateInfo findUpdate(boolean force) {
- URLConnection conn = null;
- try {
- URI uri = getUpdateURI(force);
-
- if (uri == null) {
- Log.e(LOGTAG, "failed to get update URI");
- return null;
- }
-
- DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
- conn = ProxySelector.openConnectionWithProxy(uri);
- Document dom = builder.parse(conn.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.uri = new URI(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;
- } finally {
- // conn isn't guaranteed to be an HttpURLConnection, hence we don't want to cast earlier
- // in this method. However in our current implementation it usually is, so we need to
- // make sure we close it in that case:
- final HttpURLConnection httpConn = (HttpURLConnection) conn;
- if (httpConn != null) {
- httpConn.disconnect();
- }
- }
- }
-
- private MessageDigest createMessageDigest(String hashFunction) {
- String javaHashFunction = null;
-
- if ("sha512".equalsIgnoreCase(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) {
-
- Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE);
- notificationIntent.setClass(this, UpdateService.class);
-
- Intent cancelIntent = new Intent(UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD);
- cancelIntent.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);
- PendingIntent deleteIntent = PendingIntent.getService(this, 0, cancelIntent, PendingIntent.FLAG_CANCEL_CURRENT);
-
- mBuilder = new NotificationCompat.Builder(this);
- mBuilder.setContentTitle(getResources().getString(R.string.updater_downloading_title))
- .setContentText(mApplyImmediately ? "" : getResources().getString(R.string.updater_downloading_select))
- .setSmallIcon(android.R.drawable.stat_sys_download)
- .setContentIntent(contentIntent)
- .setDeleteIntent(deleteIntent);
-
- mBuilder.setProgress(100, 0, true);
- mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
- }
-
- private void showDownloadFailure() {
- 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);
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
- builder.setSmallIcon(R.drawable.ic_status_logo);
- builder.setWhen(System.currentTimeMillis());
- builder.setContentTitle(getString(R.string.updater_downloading_title_failed));
- builder.setContentText(getString(R.string.updater_downloading_retry));
- builder.setContentIntent(contentIntent);
-
- mNotificationManager.notify(NOTIFICATION_ID, builder.build());
- }
-
- private boolean deleteUpdatePackage(String path) {
- if (path == null) {
- return false;
- }
-
- File pkg = new File(path);
- if (!pkg.exists()) {
- return false;
- }
-
- pkg.delete();
- Log.i(LOGTAG, "deleted update package: " + path);
-
- return true;
- }
-
- private File downloadUpdatePackage(UpdateInfo info, boolean overwriteExisting) {
- URL url = null;
- try {
- url = info.uri.toURL();
- } catch (java.net.MalformedURLException e) {
- Log.e(LOGTAG, "failed to read URL: ", e);
- return null;
- }
-
- File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
- path.mkdirs();
- String fileName = new File(url.getFile()).getName();
- File downloadFile = new File(path, fileName);
-
- 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();
- }
- }
-
- if (!info.buildID.equals(getLastBuildID())) {
- // Delete the previous package when a new version becomes available.
- deleteUpdatePackage(getLastFileName());
- }
-
- Log.i(LOGTAG, "downloading update package");
- sendCheckUpdateResult(CheckUpdateResult.DOWNLOADING);
-
- OutputStream output = null;
- InputStream input = null;
- URLConnection conn = null;
-
- mDownloading = true;
- mCancelDownload = false;
- showDownloadNotification(downloadFile);
-
- try {
- NetworkInfo netInfo = mConnectivityManager.getActiveNetworkInfo();
- if (netInfo != null && netInfo.isConnected() &&
- netInfo.getType() == ConnectivityManager.TYPE_WIFI) {
- mWifiLock.acquire();
- }
-
- conn = ProxySelector.openConnectionWithProxy(info.uri);
- 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;
- int lastNotify = 0;
-
- while ((len = input.read(buf, 0, BUFSIZE)) > 0 && !mCancelDownload) {
- output.write(buf, 0, len);
- bytesRead += len;
- // Updating the notification takes time so only do it every 1MB
- if (bytesRead - lastNotify > 1048576) {
- mBuilder.setProgress(length, bytesRead, false);
- mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
- lastNotify = bytesRead;
- }
- }
-
- mNotificationManager.cancel(NOTIFICATION_ID);
-
- // if the download was canceled by the user
- // delete the update package
- if (mCancelDownload) {
- Log.i(LOGTAG, "download canceled by user!");
- downloadFile.delete();
-
- return null;
- } else {
- Log.i(LOGTAG, "completed update download!");
- return downloadFile;
- }
- } catch (Exception e) {
- downloadFile.delete();
- showDownloadFailure();
-
- Log.e(LOGTAG, "failed to download update: ", e);
- return null;
- } finally {
- IOUtils.safeStreamClose(input);
- IOUtils.safeStreamClose(output);
-
- mDownloading = false;
-
- if (mWifiLock.isHeld()) {
- mWifiLock.release();
- }
-
- // conn isn't guaranteed to be an HttpURLConnection, hence we don't want to cast earlier
- // in this method. However in our current implementation it usually is, so we need to
- // make sure we close it in that case:
- final HttpURLConnection httpConn = (HttpURLConnection) conn;
- if (httpConn != null) {
- httpConn.disconnect();
- }
- }
- }
-
- 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) {
- if (updatePath == null) {
- updatePath = getLastFileName();
- }
-
- if (updatePath != null) {
- 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;
- }
-
- Intent intent = new Intent(Intent.ACTION_VIEW);
- if (AppConstants.Versions.preN) {
- intent.setDataAndType(Uri.fromFile(updateFile), "application/vnd.android.package-archive");
- } else {
- Uri apkUri = FileProvider.getUriForFile(this,
- AppConstants.MOZ_FILE_PROVIDER_AUTHORITY, updateFile);
- intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- }
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- }
-
- private void showPermissionNotification() {
- Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
- Uri.fromParts("package", getPackageName(), null));
-
- PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
-
- NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle()
- .bigText(getString(R.string.updater_permission_text));
-
- Notification notification = new NotificationCompat.Builder(this)
- .setContentTitle(getString(R.string.updater_permission_title))
- .setContentText(getString(R.string.updater_permission_text))
- .setStyle(bigTextStyle)
- .setAutoCancel(true)
- .setSmallIcon(R.drawable.ic_status_logo)
- .setColor(ContextCompat.getColor(this, R.color.rejection_red))
- .setContentIntent(pendingIntent)
- .build();
-
- NotificationManagerCompat.from(this)
- .notify(R.id.updateServicePermissionNotification, notification);
- }
-
- 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 String getLastFileName() {
- return mPrefs.getString(KEY_LAST_FILE_NAME, 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 AutoDownloadPolicy getAutoDownloadPolicy() {
- return AutoDownloadPolicy.get(mPrefs.getInt(KEY_AUTODOWNLOAD_POLICY, AutoDownloadPolicy.WIFI.value));
- }
-
- private void setAutoDownloadPolicy(AutoDownloadPolicy policy) {
- SharedPreferences.Editor editor = mPrefs.edit();
- editor.putInt(KEY_AUTODOWNLOAD_POLICY, policy.value);
- editor.commit();
- }
-
- private URI getUpdateURI(boolean force) {
- return UpdateServiceHelper.expandUpdateURI(this, mPrefs.getString(KEY_UPDATE_URL, null), force);
- }
-
- private void setUpdateUrl(String url) {
- SharedPreferences.Editor editor = mPrefs.edit();
- editor.putString(KEY_UPDATE_URL, url);
- editor.commit();
- }
-
- private void saveUpdateInfo(UpdateInfo info, File downloaded) {
- 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.putString(KEY_LAST_FILE_NAME, downloaded.toString());
- editor.commit();
- }
-
- private static final class UpdateInfo {
- public URI uri;
- 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 uri != null && isNonEmpty(buildID) &&
- isNonEmpty(hashFunction) && isNonEmpty(hashValue) && size > 0;
- }
-
- @Override
- public String toString() {
- return "uri = " + uri + ", buildID = " + buildID + ", hashFunction = " + hashFunction + ", hashValue = " + hashValue + ", size = " + size;
- }
- }
-}
--- a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java
@@ -1,76 +1,151 @@
/* -*- 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.annotation.RobocopTarget;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.v4.app.JobIntentService;
+import android.util.Log;
+
import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.JobIdsConstants;
import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.util.ContextUtils;
import org.mozilla.gecko.util.GeckoJarReader;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ApplicationInfo;
-import android.os.Build;
-import android.util.Log;
-
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
public class UpdateServiceHelper {
+ // Following can be used to start a specific UpdaterService
public static final String ACTION_REGISTER_FOR_UPDATES = AppConstants.ANDROID_PACKAGE_NAME + ".REGISTER_FOR_UPDATES";
- public static final String ACTION_UNREGISTER_FOR_UPDATES = AppConstants.ANDROID_PACKAGE_NAME + ".UNREGISTER_FOR_UPDATES";
public static final String ACTION_CHECK_FOR_UPDATE = AppConstants.ANDROID_PACKAGE_NAME + ".CHECK_FOR_UPDATE";
- public static final String ACTION_CHECK_UPDATE_RESULT = AppConstants.ANDROID_PACKAGE_NAME + ".CHECK_UPDATE_RESULT";
public static final String ACTION_DOWNLOAD_UPDATE = AppConstants.ANDROID_PACKAGE_NAME + ".DOWNLOAD_UPDATE";
public static final String ACTION_APPLY_UPDATE = AppConstants.ANDROID_PACKAGE_NAME + ".APPLY_UPDATE";
- public static final String ACTION_CANCEL_DOWNLOAD = AppConstants.ANDROID_PACKAGE_NAME + ".CANCEL_DOWNLOAD";
+
+ // Used to inform Gecko about current status
+ public static final String ACTION_CHECK_UPDATE_RESULT = AppConstants.ANDROID_PACKAGE_NAME + ".CHECK_UPDATE_RESULT"; //
+ // Used to change running service state
+ public static final String ACTION_CANCEL_DOWNLOAD = AppConstants.ANDROID_PACKAGE_NAME + ".CANCEL_DOWNLOAD"; //
- // Flags for ACTION_CHECK_FOR_UPDATE
- protected static final int FLAG_FORCE_DOWNLOAD = 1;
- protected static final int FLAG_OVERWRITE_EXISTING = 1 << 1;
- protected static final int FLAG_REINSTALL = 1 << 2;
- protected static final int FLAG_RETRY = 1 << 3;
+ // Update intervals
+ private static final int INTERVAL_LONG = 1000 * 60 * 60 * 24; // 1 day in milliseconds
+ private static final int INTERVAL_SHORT = 1000 * 60 * 60 * 4; // 4 hours in milliseconds
+ private static final int INTERVAL_RETRY = 1000 * 60 * 60 * 1; // 1 hour in milliseconds
+
+ // The number of bytes to read when working with the updated package
+ static final int BUFSIZE = 8192;
+
+ // All the notifications posted by the update services will use the same id
+ static final int NOTIFICATION_ID = 0x3e40ddbd;
// Name of the Intent extra for the autodownload policy, used with ACTION_REGISTER_FOR_UPDATES
protected static final String EXTRA_AUTODOWNLOAD_NAME = "autodownload";
// Name of the Intent extra that holds the flags for ACTION_CHECK_FOR_UPDATE
protected static final String EXTRA_UPDATE_FLAGS_NAME = "updateFlags";
// Name of the Intent extra that holds the APK path, used with ACTION_APPLY_UPDATE
protected static final String EXTRA_PACKAGE_PATH_NAME = "packagePath";
// Name of the Intent extra for the update URL, used with ACTION_REGISTER_FOR_UPDATES
protected static final String EXTRA_UPDATE_URL_NAME = "updateUrl";
- private static final String LOGTAG = "UpdateServiceHelper";
+ private static final String LOGTAG = "GeckoUpdatesHelper";
+ private static final boolean DEBUG = false;
private static final String DEFAULT_UPDATE_LOCALE = "en-US";
// So that updates can be disabled by tests.
private static volatile boolean isEnabled = true;
+ static final class UpdateInfo {
+ public URI uri;
+ 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 uri != null && isNonEmpty(buildID) &&
+ isNonEmpty(hashFunction) && isNonEmpty(hashValue) && size > 0;
+ }
+
+ @Override
+ public String toString() {
+ return "uri = " + uri + ", buildID = " + buildID + ", hashFunction = " + hashFunction + ", hashValue = " + hashValue + ", size = " + size;
+ }
+ }
+
+ public enum AutoDownloadPolicy {
+ NONE(-1),
+ WIFI(0),
+ DISABLED(1),
+ ENABLED(2);
+
+ public final int value;
+
+ AutoDownloadPolicy(int value) {
+ this.value = value;
+ }
+
+ private final static AutoDownloadPolicy[] sValues = AutoDownloadPolicy.values();
+
+ public static AutoDownloadPolicy get(int value) {
+ for (AutoDownloadPolicy id: sValues) {
+ if (id.value == value) {
+ return id;
+ }
+ }
+ return NONE;
+ }
+
+ public static AutoDownloadPolicy get(String name) {
+ for (AutoDownloadPolicy id: sValues) {
+ if (name.equalsIgnoreCase(id.toString())) {
+ return id;
+ }
+ }
+ return NONE;
+ }
+ }
+
+ enum CheckUpdateResult {
+ // Keep these in sync with mobile/android/chrome/content/about.xhtml
+ NOT_AVAILABLE,
+ AVAILABLE,
+ DOWNLOADING,
+ DOWNLOADED
+ }
+
private enum Pref {
AUTO_DOWNLOAD_POLICY("app.update.autodownload"),
UPDATE_URL("app.update.url.android");
- public final String name;
+ final String name;
- private Pref(String name) {
+ Pref(String name) {
this.name = name;
}
- public final static String[] names;
+ final static String[] names;
@Override
public String toString() {
return this.name;
}
static {
ArrayList<String> nameList = new ArrayList<String>();
@@ -105,17 +180,19 @@ public class UpdateServiceHelper {
String updateLocaleUrl = "jar:jar:file://" + info.sourceDir + "!/" + AppConstants.OMNIJAR_NAME + "!/update.locale";
final String jarLocale = GeckoJarReader.getText(context, updateLocaleUrl);
if (jarLocale != null) {
locale = jarLocale.trim();
}
} catch (android.content.pm.PackageManager.NameNotFoundException e) {
// Shouldn't really be possible, but fallback to default locale
- Log.i(LOGTAG, "Failed to read update locale file, falling back to " + locale);
+ if (DEBUG) {
+ Log.i(LOGTAG, "Failed to read update locale file, falling back to " + locale);
+ }
}
String url = updateUri.replace("%PRODUCT%", AppConstants.MOZ_APP_BASENAME)
.replace("%VERSION%", AppConstants.MOZ_APP_VERSION)
.replace("%BUILD_ID%", force ? "0" : AppConstants.MOZ_APP_BUILDID)
.replace("%BUILD_TARGET%", "Android_" + AppConstants.MOZ_APP_ABI + pkgSpecial)
.replace("%LOCALE%", locale)
.replace("%CHANNEL%", AppConstants.MOZ_UPDATE_CHANNEL)
@@ -135,79 +212,108 @@ public class UpdateServiceHelper {
public static boolean isUpdaterEnabled(final Context context) {
return AppConstants.MOZ_UPDATER && isEnabled && !ContextUtils.isInstalledFromGooglePlay(context);
}
public static void setUpdateUrl(Context context, String url) {
registerForUpdates(context, null, url);
}
- public static void setAutoDownloadPolicy(Context context, UpdateService.AutoDownloadPolicy policy) {
+ public static void setAutoDownloadPolicy(Context context, AutoDownloadPolicy policy) {
registerForUpdates(context, policy, null);
}
public static void checkForUpdate(Context context) {
if (context == null) {
return;
}
- context.startService(createIntent(context, ACTION_CHECK_FOR_UPDATE));
+ enqueueUpdateCheck(context, new Intent());
}
public static void downloadUpdate(Context context) {
if (context == null) {
return;
}
- context.startService(createIntent(context, ACTION_DOWNLOAD_UPDATE));
+ enqueueUpdateDownload(context, new Intent());
}
public static void applyUpdate(Context context) {
if (context == null) {
return;
}
- context.startService(createIntent(context, ACTION_APPLY_UPDATE));
+ enqueueUpdateApply(context, new Intent());
}
public static void registerForUpdates(final Context context) {
if (!isUpdaterEnabled(context)) {
return;
}
final HashMap<String, Object> prefs = new HashMap<String, Object>();
PrefsHelper.getPrefs(Pref.names, new PrefsHelper.PrefHandlerBase() {
@Override public void prefValue(String pref, String value) {
prefs.put(pref, value);
}
@Override public void finish() {
UpdateServiceHelper.registerForUpdates(context,
- UpdateService.AutoDownloadPolicy.get(
+ AutoDownloadPolicy.get(
(String) prefs.get(Pref.AUTO_DOWNLOAD_POLICY.toString())),
(String) prefs.get(Pref.UPDATE_URL.toString()));
}
});
}
- public static void registerForUpdates(Context context, UpdateService.AutoDownloadPolicy policy, String url) {
+ private static void registerForUpdates(Context context, AutoDownloadPolicy policy, String url) {
if (!isUpdaterEnabled(context)) {
return;
}
- Intent intent = createIntent(context, ACTION_REGISTER_FOR_UPDATES);
+ Intent intent = new Intent(ACTION_REGISTER_FOR_UPDATES);
if (policy != null) {
intent.putExtra(EXTRA_AUTODOWNLOAD_NAME, policy.value);
}
if (url != null) {
intent.putExtra(EXTRA_UPDATE_URL_NAME, url);
}
- context.startService(intent);
+ enqueueUpdateRegister(context, intent);
+ }
+
+ static int getUpdateInterval(boolean isRetry) {
+ int interval;
+ if (isRetry) {
+ interval = INTERVAL_RETRY;
+ } else if (!AppConstants.RELEASE_OR_BETA) {
+ interval = INTERVAL_SHORT;
+ } else {
+ interval = INTERVAL_LONG;
+ }
+
+ return interval;
}
- private static Intent createIntent(Context context, String action) {
- return new Intent(action, null, context, UpdateService.class);
+ public static void enqueueUpdateApply(@NonNull final Context context, @NonNull final Intent workIntent) {
+ JobIntentService.enqueueWork(context, UpdatesApplyService.class,
+ JobIdsConstants.getIdForUpdatesApplyJob(), workIntent);
+ }
+
+ public static void enqueueUpdateCheck(@NonNull final Context context, @NonNull final Intent workIntent) {
+ JobIntentService.enqueueWork(context, UpdatesCheckService.class,
+ JobIdsConstants.getIdForUpdatesCheckJob(), workIntent);
+ }
+
+ public static void enqueueUpdateDownload(@NonNull final Context context, @NonNull final Intent workIntent) {
+ JobIntentService.enqueueWork(context, UpdatesDownloadService.class,
+ JobIdsConstants.getIdForUpdatesDownloadJob(), workIntent);
+ }
+
+ public static void enqueueUpdateRegister(@NonNull final Context context, @NonNull final Intent workIntent) {
+ JobIntentService.enqueueWork(context, UpdatesRegisterService.class,
+ JobIdsConstants.getIdForUpdatesRegisterJob(), workIntent);
}
}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceReceiver.java
@@ -0,0 +1,46 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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 android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+/**
+ * Receiver only used in local notifications posted by an {@link UpdaterService} after which it finishes.<br>
+ * Used to start another variant of {@link UpdaterService}.
+ */
+public class UpdateServiceReceiver extends BroadcastReceiver {
+ private static final String LOGTAG = "GeckoUpdateServiceRcv"; // must be <= 23 characters
+ private static final boolean DEBUG = false;
+
+ public UpdateServiceReceiver() {
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+
+ if (DEBUG) {
+ Log.d(LOGTAG, String.format("Will enqueue \"%s\" work", action));
+ }
+
+ switch (action) {
+ case UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE:
+ UpdateServiceHelper.enqueueUpdateCheck(context, intent);
+ break;
+ case UpdateServiceHelper.ACTION_APPLY_UPDATE:
+ UpdateServiceHelper.enqueueUpdateApply(context, intent);
+ break;
+ case UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE:
+ UpdateServiceHelper.enqueueUpdateDownload(context, intent);
+ break;
+ default:
+ // no-op, we only listen for the above
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/Updater.java
@@ -0,0 +1,657 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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 android.Manifest;
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.WifiLock;
+import android.os.Environment;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationManagerCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.net.ConnectivityManagerCompat;
+import android.util.Log;
+
+import org.mozilla.gecko.GeckoUpdateReceiver;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.permissions.Permissions;
+import org.mozilla.gecko.updater.UpdateServiceHelper.AutoDownloadPolicy;
+import org.mozilla.gecko.updater.UpdateServiceHelper.CheckUpdateResult;
+import org.mozilla.gecko.updater.UpdateServiceHelper.UpdateInfo;
+import org.mozilla.gecko.util.IOUtils;
+import org.mozilla.gecko.util.ProxySelector;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+/**
+ * Helper class that handles everything needed to download and apply an updated package.<br>
+ * Only to be used on background threads because of the long time running operations involved.
+ */
+public class Updater {
+ // Because this class can be called by multiple parties the log tag
+ // will actually be the caller's name to easily identify whom requested the operations.
+ private String logtag;
+ private static final boolean DEBUG = false;
+
+ // Flags for ACTION_CHECK_FOR_UPDATE
+ static final int FLAG_FORCE_DOWNLOAD = 1;
+ static final int FLAG_OVERWRITE_EXISTING = 1 << 1;
+ static final int FLAG_REINSTALL = 1 << 2;
+
+ private final Context context;
+ private final ConnectivityManager connectivityManager;
+ private final NotificationManagerCompat notificationManager;
+ private final int bufferSize;
+ private final int notificationId;
+ private volatile WifiLock mWifiLock;
+
+ private NotificationCompat.Builder notifBuilder;
+ private BroadcastReceiver innerBroadcastReceiver;
+ private UpdatesPrefs prefs;
+
+ private boolean shouldCancelDownload;
+ private boolean shouldApplyImmediately;
+
+ Updater(final Context context, final UpdatesPrefs prefs, final int bufSize, final int notificationId) {
+ this.logtag = getLogtagFromParentName(context);
+ this.context = context;
+ this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ this.notificationManager = NotificationManagerCompat.from(context);
+ this.prefs = prefs;
+ this.mWifiLock = ((WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE))
+ .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, prefs.getPreferenceName());
+ this.bufferSize = bufSize;
+ this.notificationId = notificationId;
+
+ registerForLocalBroadcasts(context);
+
+ if (DEBUG) {
+ Log.d(logtag, "Updater created");
+ }
+ }
+
+ /**
+ * Must be called everytime the caller finished it's work to cleanup used resources.
+ */
+ void finish() {
+ unregisterFromLocalBroadcasts();
+
+ if (mWifiLock.isHeld()) {
+ mWifiLock.release();
+ }
+ }
+
+ void setIfShouldApplyUpdateImmediately(boolean shouldApplyImmediately) {
+ this.shouldApplyImmediately = shouldApplyImmediately;
+ }
+
+ void startUpdate(final int flags) {
+ prefs.setLastAttemptDate();
+
+ NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();
+ if (netInfo == null || !netInfo.isConnected()) {
+ if (DEBUG) {
+ Log.i(logtag, "not connected to the network");
+ }
+ registerForUpdates(true);
+ sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
+ return;
+ }
+
+ registerForUpdates(false);
+
+ final UpdateInfo info = findUpdate(hasFlag(flags, FLAG_REINSTALL));
+ boolean haveUpdate = (info != null);
+
+ if (!haveUpdate) {
+ if (DEBUG) {
+ Log.i(logtag, "no update available");
+ }
+ sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
+ return;
+ }
+
+ if (DEBUG) {
+ Log.i(logtag, "update available, buildID = " + info.buildID);
+ }
+
+ Permissions.from(context)
+ .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ .doNotPrompt()
+ .andFallback(new Runnable() {
+ @Override
+ public void run() {
+ showPermissionNotification();
+ sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
+ }
+ })
+ .run(new Runnable() {
+ @Override
+ public void run() {
+ startDownload(info, flags);
+ }
+ });
+
+ }
+
+ void registerForUpdates(boolean isRetry) {
+ Calendar lastAttempt = prefs.getLastAttemptDate();
+ Calendar now = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+
+ int interval = UpdateServiceHelper.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.
+ if (DEBUG) {
+ Log.i(logtag, "no update has ever been attempted, checking now");
+ }
+ startUpdate(0);
+ return;
+ }
+
+ AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ if (manager == null)
+ return;
+
+ PendingIntent pending = PendingIntent.getBroadcast(context, 0,
+ new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE, null, context, UpdateServiceReceiver.class),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ manager.cancel(pending);
+
+ lastAttempt.setTimeInMillis(lastAttempt.getTimeInMillis() + interval);
+ if (DEBUG) {
+ Log.i(logtag, "next update will be at: " + lastAttempt.getTime());
+ }
+
+ manager.set(AlarmManager.RTC_WAKEUP, lastAttempt.getTimeInMillis(), pending);
+ }
+
+ void applyUpdate(String updatePath) {
+ if (updatePath == null) {
+ updatePath = prefs.getLastFileName();
+ }
+
+ if (updatePath != null) {
+ applyUpdate(new File(updatePath));
+ }
+ }
+
+ private void showPermissionNotification() {
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ Uri.fromParts("package", context.getPackageName(), null));
+
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+
+ NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle()
+ .bigText(getString(R.string.updater_permission_text));
+
+ Notification notification = new NotificationCompat.Builder(context)
+ .setContentTitle(getString(R.string.updater_permission_title))
+ .setContentText(getString(R.string.updater_permission_text))
+ .setStyle(bigTextStyle)
+ .setAutoCancel(true)
+ .setSmallIcon(R.drawable.ic_status_logo)
+ .setColor(ContextCompat.getColor(context, R.color.rejection_red))
+ .setContentIntent(pendingIntent)
+ .build();
+
+ NotificationManagerCompat.from(context)
+ .notify(R.id.updateServicePermissionNotification, notification);
+ }
+
+ private void startDownload(UpdateInfo info, int flags) {
+ AutoDownloadPolicy policy = prefs.getAutoDownloadPolicy();
+
+ // We only start a download automatically if one of following criteria are met:
+ //
+ // - We have a FORCE_DOWNLOAD flag passed in
+ // - The preference is set to 'always'
+ // - The preference is set to 'wifi' and we are using a non-metered network (i.e. the user
+ // is OK with large data transfers occurring)
+ //
+ boolean shouldStartDownload = hasFlag(flags, FLAG_FORCE_DOWNLOAD) ||
+ policy == AutoDownloadPolicy.ENABLED ||
+ (policy == AutoDownloadPolicy.WIFI && !ConnectivityManagerCompat.isActiveNetworkMetered(connectivityManager));
+
+ if (!shouldStartDownload) {
+ if (DEBUG) {
+ Log.i(logtag, "not initiating automatic update download due to policy " + policy.toString());
+ }
+ sendCheckUpdateResult(CheckUpdateResult.AVAILABLE);
+
+ // We aren't autodownloading here, so prompt to start the update with a new JobIntentService
+ Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE);
+ notificationIntent.setClass(context, UpdateServiceReceiver.class);
+ PendingIntent contentIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
+ builder.setSmallIcon(R.drawable.ic_status_logo);
+ builder.setWhen(System.currentTimeMillis());
+ builder.setAutoCancel(true);
+ builder.setContentTitle(getString(R.string.updater_start_title));
+ builder.setContentText(getString(R.string.updater_start_select));
+ builder.setContentIntent(contentIntent);
+
+ notificationManager.notify(notificationId, builder.build());
+
+ return;
+ }
+
+ File pkg = downloadUpdatePackage(info, hasFlag(flags, FLAG_OVERWRITE_EXISTING));
+ if (pkg == null) {
+ sendCheckUpdateResult(CheckUpdateResult.NOT_AVAILABLE);
+ return;
+ }
+
+ if (DEBUG) {
+ Log.i(logtag, "have update package at " + pkg);
+ }
+
+ prefs.saveUpdateInfo(info, pkg);
+ sendCheckUpdateResult(CheckUpdateResult.DOWNLOADED);
+
+ if (shouldApplyImmediately) {
+ applyUpdate(pkg);
+ } else {
+ // Prompt to apply the update with a new JobIntentService
+
+ Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE);
+ notificationIntent.setClass(context, UpdateServiceReceiver.class);
+ notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, pkg.getAbsolutePath());
+ PendingIntent contentIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
+ builder.setSmallIcon(R.drawable.ic_status_logo);
+ builder.setWhen(System.currentTimeMillis());
+ builder.setAutoCancel(true);
+ builder.setContentTitle(getString(R.string.updater_apply_title));
+ builder.setContentText(getString(R.string.updater_apply_select));
+ builder.setContentIntent(contentIntent);
+
+ notificationManager.notify(notificationId, builder.build());
+ }
+ }
+
+ private File downloadUpdatePackage(UpdateInfo info, boolean overwriteExisting) {
+ URL url = null;
+ try {
+ url = info.uri.toURL();
+ } catch (java.net.MalformedURLException e) {
+ Log.e(logtag, "failed to read URL: ", e);
+ return null;
+ }
+
+ File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ path.mkdirs();
+ String fileName = new File(url.getFile()).getName();
+ File downloadFile = new File(path, fileName);
+
+
+ final String lastBuildId = prefs.getLastBuildID();
+ if (!overwriteExisting && info.buildID.equals(lastBuildId) && 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 (PackageVerifier.verifyDownloadedPackage(prefs, downloadFile, bufferSize)) {
+ if (DEBUG) {
+ Log.i(logtag, "using existing update package");
+ }
+ return downloadFile;
+ } else {
+ // Didn't match, so we're going to download a new one.
+ downloadFile.delete();
+ }
+ }
+
+ if (!info.buildID.equals(prefs.getLastBuildID())) {
+ // Delete the previous package when a new version becomes available.
+ deleteUpdatePackage(prefs.getLastFileName());
+ }
+
+ if (DEBUG) {
+ Log.i(logtag, "downloading update package");
+ }
+ sendCheckUpdateResult(CheckUpdateResult.DOWNLOADING);
+
+ OutputStream output = null;
+ InputStream input = null;
+ URLConnection conn = null;
+
+ shouldCancelDownload = false;
+ showDownloadNotification(downloadFile);
+
+ try {
+ NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();
+ if (netInfo != null && netInfo.isConnected() &&
+ netInfo.getType() == ConnectivityManager.TYPE_WIFI) {
+ mWifiLock.acquire();
+ }
+
+ conn = ProxySelector.openConnectionWithProxy(info.uri);
+ int length = conn.getContentLength();
+
+ output = new BufferedOutputStream(new FileOutputStream(downloadFile));
+ input = new BufferedInputStream(conn.getInputStream());
+
+ byte[] buf = new byte[bufferSize];
+ int len = 0;
+
+ int bytesRead = 0;
+ int lastNotify = 0;
+
+ while ((len = input.read(buf, 0, bufferSize)) > 0 && !shouldCancelDownload) {
+ output.write(buf, 0, len);
+ bytesRead += len;
+ // Updating the notification takes time so only do it every 1MB
+ if (bytesRead - lastNotify > 1048576) {
+ notifBuilder.setProgress(length, bytesRead, false);
+ notificationManager.notify(notificationId, notifBuilder.build());
+ lastNotify = bytesRead;
+ }
+ }
+
+ notificationManager.cancel(notificationId);
+
+ // if the download was canceled by the user
+ // delete the update package
+ if (shouldCancelDownload) {
+ if (DEBUG) {
+ Log.i(logtag, "download canceled by user!");
+ }
+ downloadFile.delete();
+
+ return null;
+ } else {
+ if (DEBUG) {
+ Log.i(logtag, "completed update download!");
+ }
+ return downloadFile;
+ }
+ } catch (Exception e) {
+ downloadFile.delete();
+ showDownloadFailure();
+
+ Log.e(logtag, "failed to download update: ", e);
+ return null;
+ } finally {
+ IOUtils.safeStreamClose(input);
+ IOUtils.safeStreamClose(output);
+
+ if (mWifiLock.isHeld()) {
+ mWifiLock.release();
+ }
+
+ // conn isn't guaranteed to be an HttpURLConnection, hence we don't want to cast earlier
+ // in this method. However in our current implementation it usually is, so we need to
+ // make sure we close it in that case:
+ final HttpURLConnection httpConn = (HttpURLConnection) conn;
+ if (httpConn != null) {
+ httpConn.disconnect();
+ }
+ }
+ }
+
+ private void showDownloadNotification() {
+ showDownloadNotification(null);
+ }
+
+ private void showDownloadNotification(File downloadFile) {
+
+ Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE);
+ notificationIntent.setClass(context, UserUpdatesReceiver.class);
+
+ Intent cancelIntent = new Intent(UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD);
+ cancelIntent.setClass(context, UserUpdatesReceiver.class);
+
+ if (downloadFile != null)
+ notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, downloadFile.getAbsolutePath());
+
+ PendingIntent contentIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent deleteIntent = PendingIntent.getBroadcast(context, 0, cancelIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ notifBuilder = new NotificationCompat.Builder(context);
+ notifBuilder.setContentTitle(getString(R.string.updater_downloading_title))
+ .setContentText(shouldApplyImmediately ? "" : getString(R.string.updater_downloading_select))
+ .setSmallIcon(android.R.drawable.stat_sys_download)
+ .setContentIntent(contentIntent)
+ .setDeleteIntent(deleteIntent);
+
+ notifBuilder.setProgress(100, 0, true);
+ notificationManager.notify(notificationId, notifBuilder.build());
+ }
+
+ private void showDownloadFailure() {
+ // Let the user restart the update process with a new JobIntentService
+ Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE);
+ notificationIntent.setClass(context, UpdateServiceReceiver.class);
+ PendingIntent contentIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
+ builder.setSmallIcon(R.drawable.ic_status_logo);
+ builder.setWhen(System.currentTimeMillis());
+ builder.setContentTitle(getString(R.string.updater_downloading_title_failed));
+ builder.setContentText(getString(R.string.updater_downloading_retry));
+ builder.setContentIntent(contentIntent);
+
+ notificationManager.notify(notificationId, builder.build());
+ }
+
+ private void applyUpdate(File updateFile) {
+ shouldApplyImmediately = false;
+
+ if (!updateFile.exists())
+ return;
+
+ if (DEBUG) {
+ Log.i(logtag, "Verifying package: " + updateFile);
+ }
+
+ if (!PackageVerifier.verifyDownloadedPackage(prefs, updateFile, bufferSize)) {
+ Log.e(logtag, "Not installing update, failed verification");
+ return;
+ }
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.fromFile(updateFile), "application/vnd.android.package-archive");
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+
+ private boolean deleteUpdatePackage(String path) {
+ if (path == null) {
+ return false;
+ }
+
+ File pkg = new File(path);
+ if (!pkg.exists()) {
+ return false;
+ }
+
+ pkg.delete();
+
+ if (DEBUG) {
+ Log.i(logtag, "deleted update package: " + path);
+ }
+
+ return true;
+ }
+
+ private void sendCheckUpdateResult(CheckUpdateResult result) {
+ Intent resultIntent = new Intent(context, GeckoUpdateReceiver.class);
+ resultIntent.setAction(UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT);
+ resultIntent.putExtra("result", result.toString());
+ context.sendBroadcast(resultIntent);
+ }
+
+ private UpdateInfo findUpdate(boolean force) {
+ URLConnection conn = null;
+ try {
+ URI uri = prefs.getUpdateURI(force);
+
+ if (uri == null) {
+ Log.e(logtag, "failed to get update URI");
+ return null;
+ }
+
+ DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+ conn = ProxySelector.openConnectionWithProxy(uri);
+ Document dom = builder.parse(conn.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.uri = new URI(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;
+ } finally {
+ // conn isn't guaranteed to be an HttpURLConnection, hence we don't want to cast earlier
+ // in this method. However in our current implementation it usually is, so we need to
+ // make sure we close it in that case:
+ final HttpURLConnection httpConn = (HttpURLConnection) conn;
+ if (httpConn != null) {
+ httpConn.disconnect();
+ }
+ }
+ }
+
+ private String getString(@StringRes final int stringResId) {
+ return context.getString(stringResId);
+ }
+
+ private static boolean hasFlag(int flags, int flag) {
+ return (flags & flag) == flag;
+ }
+
+ // The log tag can have at most 23 chars or Log will throw an IllegalArgumentException
+ private String getLogtagFromParentName(@NonNull final Context context) {
+ final String parentName = "Gecko" + context.getClass().getSimpleName();
+ if (parentName.length() > 23) {
+ return parentName.substring(0, 24);
+ } else {
+ return parentName;
+ }
+ }
+
+ /**
+ * Receiver only used in local notifications that allow for actions to modify current running state.
+ * As such it is an inner class of current JobIntentService.<br>
+ * For any notifications that could allow for actions after the service completed it's work
+ * use {@link UpdateServiceReceiver}
+ */
+ private class UserUpdatesReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+
+ switch (action) {
+ case UpdateServiceHelper.ACTION_APPLY_UPDATE:
+ // User can only press a notification button for this while a download is in progress.
+ // If download finished the notification will be update to allow starting
+ // a new JobIntentService to apply the update
+ if (DEBUG) {
+ Log.i(logtag, "will apply update when download finished");
+ }
+ shouldApplyImmediately = true;
+ showDownloadNotification();
+ break;
+ case UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD:
+ shouldCancelDownload = true;
+ break;
+ default:
+ // no-op, we only listen for the above
+ }
+ }
+ }
+
+ private void registerForLocalBroadcasts(@NonNull final Context context) {
+ innerBroadcastReceiver = new UserUpdatesReceiver();
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(UpdateServiceHelper.ACTION_APPLY_UPDATE);
+ intentFilter.addAction(UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD);
+
+ context.registerReceiver(innerBroadcastReceiver, intentFilter);
+ }
+
+ private void unregisterFromLocalBroadcasts() {
+ context.unregisterReceiver(innerBroadcastReceiver);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdaterService.java
@@ -0,0 +1,50 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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 android.content.Context;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.support.v4.app.JobIntentService;
+
+import org.mozilla.gecko.CrashHandler;
+
+public abstract class UpdaterService extends JobIntentService {
+ private static CrashHandler crashHandler;
+ protected Updater updater;
+ protected UpdatesPrefs prefs;
+
+ @Override
+ public void onCreate() {
+ initCrashHandler(getApplicationContext());
+
+ super.onCreate();
+
+ prefs = new UpdatesPrefs(this);
+ updater = new Updater(this, prefs, UpdateServiceHelper.BUFSIZE, UpdateServiceHelper.NOTIFICATION_ID);
+ }
+
+ @Override
+ protected abstract void onHandleWork(@NonNull Intent intent);
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ updater.finish();
+ crashHandler.unregister();
+ }
+
+ private void initCrashHandler(@NonNull final Context appContext) {
+ // Will only need one
+ if (crashHandler == null) {
+ synchronized (this) {
+ if (crashHandler == null) {
+ crashHandler = CrashHandler.createDefaultCrashHandler(appContext);
+ }
+ }
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdatesApplyService.java
@@ -0,0 +1,18 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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 android.content.Intent;
+import android.support.annotation.NonNull;
+
+public class UpdatesApplyService extends UpdaterService {
+ @Override
+ protected void onHandleWork(@NonNull Intent intent) {
+ // The apply action from the notification while the download is in progress is
+ // treated in the inner BroadcastReceiver of the service which posted the notification
+ updater.applyUpdate(intent.getStringExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME));
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdatesCheckService.java
@@ -0,0 +1,19 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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 android.content.Intent;
+import android.support.annotation.NonNull;
+
+public class UpdatesCheckService extends UpdaterService {
+ @Override
+ protected void onHandleWork(@NonNull Intent intent) {
+ updater.startUpdate(intent.getIntExtra(UpdateServiceHelper.EXTRA_UPDATE_FLAGS_NAME, 0));
+
+ // Use this instead for forcing a download from about:fennec
+ // startUpdate(UpdateServiceHelper.FLAG_FORCE_DOWNLOAD | UpdateServiceHelper.FLAG_REINSTALL);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdatesDownloadService.java
@@ -0,0 +1,17 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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 android.content.Intent;
+import android.support.annotation.NonNull;
+
+public class UpdatesDownloadService extends UpdaterService {
+ @Override
+ protected void onHandleWork(@NonNull Intent intent) {
+ updater.setIfShouldApplyUpdateImmediately(true);
+ updater.startUpdate(Updater.FLAG_FORCE_DOWNLOAD);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdatesPrefs.java
@@ -0,0 +1,103 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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 android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.updater.UpdateServiceHelper.AutoDownloadPolicy;
+import org.mozilla.gecko.updater.UpdateServiceHelper.UpdateInfo;
+
+import java.io.File;
+import java.net.URI;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+public final class UpdatesPrefs {
+ 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_FILE_NAME = "UpdateService.lastFileName";
+ private static final String KEY_LAST_ATTEMPT_DATE = "UpdateService.lastAttemptDate";
+ private static final String KEY_AUTODOWNLOAD_POLICY = "UpdateService.autoDownloadPolicy";
+ private static final String KEY_UPDATE_URL = "UpdateService.updateUrl";
+
+ private SharedPreferences sharedPrefs;
+
+ UpdatesPrefs(@NonNull final Context context) {
+ sharedPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ }
+
+ String getPreferenceName() {
+ return PREFS_NAME;
+ }
+
+ String getLastBuildID() {
+ return sharedPrefs.getString(KEY_LAST_BUILDID, null);
+ }
+
+ String getLastHashFunction() {
+ return sharedPrefs.getString(KEY_LAST_HASH_FUNCTION, null);
+ }
+
+ String getLastHashValue() {
+ return sharedPrefs.getString(KEY_LAST_HASH_VALUE, null);
+ }
+
+ String getLastFileName() {
+ return sharedPrefs.getString(KEY_LAST_FILE_NAME, null);
+ }
+
+ Calendar getLastAttemptDate() {
+ long lastAttempt = sharedPrefs.getLong(KEY_LAST_ATTEMPT_DATE, -1);
+ if (lastAttempt < 0)
+ return null;
+
+ GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+ cal.setTimeInMillis(lastAttempt);
+ return cal;
+ }
+
+ void setLastAttemptDate() {
+ SharedPreferences.Editor editor = sharedPrefs.edit();
+ editor.putLong(KEY_LAST_ATTEMPT_DATE, System.currentTimeMillis());
+ editor.commit();
+ }
+
+ AutoDownloadPolicy getAutoDownloadPolicy() {
+ return AutoDownloadPolicy.get(sharedPrefs.getInt(KEY_AUTODOWNLOAD_POLICY, AutoDownloadPolicy.WIFI.value));
+ }
+
+ void setAutoDownloadPolicy(AutoDownloadPolicy policy) {
+ SharedPreferences.Editor editor = sharedPrefs.edit();
+ editor.putInt(KEY_AUTODOWNLOAD_POLICY, policy.value);
+ editor.commit();
+ }
+
+ URI getUpdateURI(boolean force) {
+ return UpdateServiceHelper.expandUpdateURI(GeckoAppShell.getApplicationContext(),
+ sharedPrefs.getString(KEY_UPDATE_URL, null), force);
+ }
+
+ void setUpdateUrl(String url) {
+ SharedPreferences.Editor editor = sharedPrefs.edit();
+ editor.putString(KEY_UPDATE_URL, url);
+ editor.commit();
+ }
+
+ void saveUpdateInfo(UpdateInfo info, File downloaded) {
+ SharedPreferences.Editor editor = sharedPrefs.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.putString(KEY_LAST_FILE_NAME, downloaded.toString());
+ editor.commit();
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdatesRegisterService.java
@@ -0,0 +1,31 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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 android.content.Intent;
+import android.support.annotation.NonNull;
+
+import org.mozilla.gecko.updater.UpdateServiceHelper.AutoDownloadPolicy;
+
+public class UpdatesRegisterService extends UpdaterService {
+ @Override
+ protected void onHandleWork(@NonNull Intent intent) {
+ AutoDownloadPolicy policy = AutoDownloadPolicy.get(
+ intent.getIntExtra(UpdateServiceHelper.EXTRA_AUTODOWNLOAD_NAME,
+ AutoDownloadPolicy.NONE.value));
+
+ if (policy != AutoDownloadPolicy.NONE) {
+ prefs.setAutoDownloadPolicy(policy);
+ }
+
+ String url = intent.getStringExtra(UpdateServiceHelper.EXTRA_UPDATE_URL_NAME);
+ if (url != null) {
+ prefs.setUpdateUrl(url);
+ }
+
+ updater.registerForUpdates(false);
+ }
+}