/* -*- 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; } } }