mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-13 23:17:57 +00:00
555 lines
20 KiB
Java
555 lines
20 KiB
Java
/* -*- 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);
|
|
} else 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), 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(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.ic_status_logo, null, 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 (mApplyImmediately) {
|
|
applyUpdate(pkg);
|
|
} else {
|
|
// Prompt to apply the update
|
|
Notification notification = new Notification(R.drawable.ic_status_logo, null, 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(this, 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, null, 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_select),
|
|
contentIntent);
|
|
|
|
mNotificationManager.notify(NOTIFICATION_ID, notification);
|
|
}
|
|
|
|
private void showDownloadFailure() {
|
|
Notification notification = new Notification(R.drawable.ic_status_logo, null, 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_failed),
|
|
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;
|
|
}
|
|
|
|
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 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;
|
|
}
|
|
}
|
|
}
|