diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /mobile/android/base/java/org/mozilla/gecko/updater | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/updater')
3 files changed, 1128 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java b/mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java new file mode 100644 index 000000000..f0ad78e77 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java @@ -0,0 +1,120 @@ +/* -*- 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.res.AssetManager; +import android.content.SharedPreferences; +import android.util.Log; + +import com.keepsafe.switchboard.SwitchBoard; + +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.BrowserApp; +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.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Perform tasks in the background after the app has been installed/updated. + */ +public class PostUpdateHandler extends BrowserAppDelegateWithReference { + private static final String LOGTAG = "PostUpdateHandler"; + + @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) + "'"); + + // 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"); + + final String dataDir = browserApp.getApplicationInfo().dataDir; + final SharedPreferences prefs = GeckoSharedPrefs.forApp(browserApp); + final AssetManager assetManager = browserApp.getContext().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"); + + final InputStream assetStream = assetManager.open(assetPath); + final File outFile = getDataFile(dataDir, assetPath); + + if (outFile == null) { + continue; + } + + final OutputStream outStream = new FileOutputStream(outFile); + + try { + IOUtils.copy(assetStream, outStream); + } catch (IOException e) { + Log.e(LOGTAG, "Error copying '" + assetPath + "' from APK to dataDir"); + } finally { + outStream.close(); + } + } + } catch (IOException e) { + Log.e(LOGTAG, "Error retrieving packaged system add-ons from APK", e); + } + + // Save the Build ID so we don't perform post-update operations again until the app is updated. + prefs.edit().putString(GeckoPreferences.PREFS_APP_UPDATE_LAST_BUILD_ID, AppConstants.MOZ_APP_BUILDID).apply(); + } + + /** + * Return a File instance in the data directory, ensuring + * that the parent exists. + * + * @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 (!dir.mkdirs()) { + Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath()); + return null; + } + } + + return outFile; + } +} diff --git a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java new file mode 100644 index 000000000..7ccc43e28 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java @@ -0,0 +1,795 @@ +/* -*- 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.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.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.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.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.URI; +import java.net.URL; +import java.net.URLConnection; +import java.security.MessageDigest; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; +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)getSystemService(Context.CONNECTIVITY_SERVICE); + mWifiLock = ((WifiManager)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) { + try { + URI uri = getUpdateURI(force); + + if (uri == null) { + Log.e(LOGTAG, "failed to get update URI"); + return null; + } + + DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document dom = builder.parse(ProxySelector.openConnectionWithProxy(uri).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; + } + } + + 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; + + mDownloading = true; + mCancelDownload = false; + showDownloadNotification(downloadFile); + + try { + NetworkInfo netInfo = mConnectivityManager.getActiveNetworkInfo(); + if (netInfo != null && netInfo.isConnected() && + netInfo.getType() == ConnectivityManager.TYPE_WIFI) { + mWifiLock.acquire(); + } + + URLConnection 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 { + try { + if (input != null) + input.close(); + } catch (java.io.IOException e) { } + + try { + if (output != null) + output.close(); + } catch (java.io.IOException e) { } + + mDownloading = false; + + if (mWifiLock.isHeld()) { + mWifiLock.release(); + } + } + } + + 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); + intent.setDataAndType(Uri.fromFile(updateFile), "application/vnd.android.package-archive"); + intent.setFlags(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 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; + } + } +} diff --git a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java new file mode 100644 index 000000000..c4d198ae7 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateServiceHelper.java @@ -0,0 +1,213 @@ +/* -*- 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 org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.PrefsHelper; +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 { + 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"; + + // 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; + + // 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 DEFAULT_UPDATE_LOCALE = "en-US"; + + // So that updates can be disabled by tests. + private static volatile boolean isEnabled = true; + + private enum Pref { + AUTO_DOWNLOAD_POLICY("app.update.autodownload"), + UPDATE_URL("app.update.url.android"); + + public final String name; + + private Pref(String name) { + this.name = name; + } + + public final static String[] names; + + @Override + public String toString() { + return this.name; + } + + static { + ArrayList<String> nameList = new ArrayList<String>(); + + for (Pref id: Pref.values()) { + nameList.add(id.toString()); + } + + names = nameList.toArray(new String[0]); + } + } + + @RobocopTarget + public static void setEnabled(final boolean enabled) { + isEnabled = enabled; + } + + public static URI expandUpdateURI(Context context, String updateUri, boolean force) { + if (updateUri == null) { + return null; + } + + PackageManager pm = context.getPackageManager(); + + String pkgSpecial = AppConstants.MOZ_PKG_SPECIAL != null ? + "-" + AppConstants.MOZ_PKG_SPECIAL : + ""; + String locale = DEFAULT_UPDATE_LOCALE; + + try { + ApplicationInfo info = pm.getApplicationInfo(AppConstants.ANDROID_PACKAGE_NAME, 0); + 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); + } + + 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) + .replace("%OS_VERSION%", Build.VERSION.RELEASE) + .replace("%DISTRIBUTION%", "default") + .replace("%DISTRIBUTION_VERSION%", "default") + .replace("%MOZ_VERSION%", AppConstants.MOZILLA_VERSION); + + try { + return new URI(url); + } catch (java.net.URISyntaxException e) { + Log.e(LOGTAG, "Failed to create update url: ", e); + return null; + } + } + + 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) { + registerForUpdates(context, policy, null); + } + + public static void checkForUpdate(Context context) { + if (context == null) { + return; + } + + context.startService(createIntent(context, ACTION_CHECK_FOR_UPDATE)); + } + + public static void downloadUpdate(Context context) { + if (context == null) { + return; + } + + context.startService(createIntent(context, ACTION_DOWNLOAD_UPDATE)); + } + + public static void applyUpdate(Context context) { + if (context == null) { + return; + } + + context.startService(createIntent(context, ACTION_APPLY_UPDATE)); + } + + 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( + (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) { + if (!isUpdaterEnabled(context)) { + return; + } + + Intent intent = createIntent(context, 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); + } + + private static Intent createIntent(Context context, String action) { + return new Intent(action, null, context, UpdateService.class); + } +} |