Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
michael@0 | 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
michael@0 | 2 | * This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | package org.mozilla.gecko.updater; |
michael@0 | 7 | |
michael@0 | 8 | import org.mozilla.gecko.AppConstants; |
michael@0 | 9 | import org.mozilla.gecko.R; |
michael@0 | 10 | |
michael@0 | 11 | import org.mozilla.apache.commons.codec.binary.Hex; |
michael@0 | 12 | |
michael@0 | 13 | import org.w3c.dom.Document; |
michael@0 | 14 | import org.w3c.dom.Node; |
michael@0 | 15 | import org.w3c.dom.NodeList; |
michael@0 | 16 | |
michael@0 | 17 | import android.app.AlarmManager; |
michael@0 | 18 | import android.app.IntentService; |
michael@0 | 19 | import android.app.Notification; |
michael@0 | 20 | import android.app.NotificationManager; |
michael@0 | 21 | import android.app.PendingIntent; |
michael@0 | 22 | import android.app.Service; |
michael@0 | 23 | import android.content.Context; |
michael@0 | 24 | import android.content.Intent; |
michael@0 | 25 | import android.content.SharedPreferences; |
michael@0 | 26 | import android.net.ConnectivityManager; |
michael@0 | 27 | import android.net.NetworkInfo; |
michael@0 | 28 | import android.net.Uri; |
michael@0 | 29 | import android.os.Environment; |
michael@0 | 30 | import android.support.v4.app.NotificationCompat; |
michael@0 | 31 | import android.support.v4.app.NotificationCompat.Builder; |
michael@0 | 32 | import android.util.Log; |
michael@0 | 33 | |
michael@0 | 34 | import java.io.BufferedInputStream; |
michael@0 | 35 | import java.io.BufferedOutputStream; |
michael@0 | 36 | import java.io.File; |
michael@0 | 37 | import java.io.FileInputStream; |
michael@0 | 38 | import java.io.FileOutputStream; |
michael@0 | 39 | import java.io.InputStream; |
michael@0 | 40 | import java.io.OutputStream; |
michael@0 | 41 | import java.net.Proxy; |
michael@0 | 42 | import java.net.ProxySelector; |
michael@0 | 43 | import java.net.URL; |
michael@0 | 44 | import java.net.URLConnection; |
michael@0 | 45 | import java.security.MessageDigest; |
michael@0 | 46 | import java.util.Calendar; |
michael@0 | 47 | import java.util.GregorianCalendar; |
michael@0 | 48 | import java.util.List; |
michael@0 | 49 | import java.util.TimeZone; |
michael@0 | 50 | |
michael@0 | 51 | import javax.xml.parsers.DocumentBuilder; |
michael@0 | 52 | import javax.xml.parsers.DocumentBuilderFactory; |
michael@0 | 53 | |
michael@0 | 54 | public class UpdateService extends IntentService { |
michael@0 | 55 | private static final int BUFSIZE = 8192; |
michael@0 | 56 | private static final int NOTIFICATION_ID = 0x3e40ddbd; |
michael@0 | 57 | |
michael@0 | 58 | private static final String LOGTAG = "UpdateService"; |
michael@0 | 59 | |
michael@0 | 60 | private static final int INTERVAL_LONG = 86400000; // in milliseconds |
michael@0 | 61 | private static final int INTERVAL_SHORT = 14400000; // again, in milliseconds |
michael@0 | 62 | private static final int INTERVAL_RETRY = 3600000; |
michael@0 | 63 | |
michael@0 | 64 | private static final String PREFS_NAME = "UpdateService"; |
michael@0 | 65 | private static final String KEY_LAST_BUILDID = "UpdateService.lastBuildID"; |
michael@0 | 66 | private static final String KEY_LAST_HASH_FUNCTION = "UpdateService.lastHashFunction"; |
michael@0 | 67 | private static final String KEY_LAST_HASH_VALUE = "UpdateService.lastHashValue"; |
michael@0 | 68 | private static final String KEY_LAST_FILE_NAME = "UpdateService.lastFileName"; |
michael@0 | 69 | private static final String KEY_LAST_ATTEMPT_DATE = "UpdateService.lastAttemptDate"; |
michael@0 | 70 | private static final String KEY_AUTODOWNLOAD_POLICY = "UpdateService.autoDownloadPolicy"; |
michael@0 | 71 | |
michael@0 | 72 | private SharedPreferences mPrefs; |
michael@0 | 73 | |
michael@0 | 74 | private NotificationManager mNotificationManager; |
michael@0 | 75 | private ConnectivityManager mConnectivityManager; |
michael@0 | 76 | private Builder mBuilder; |
michael@0 | 77 | |
michael@0 | 78 | private boolean mDownloading; |
michael@0 | 79 | private boolean mCancelDownload; |
michael@0 | 80 | private boolean mApplyImmediately; |
michael@0 | 81 | |
michael@0 | 82 | public UpdateService() { |
michael@0 | 83 | super("updater"); |
michael@0 | 84 | } |
michael@0 | 85 | |
michael@0 | 86 | @Override |
michael@0 | 87 | public void onCreate () { |
michael@0 | 88 | super.onCreate(); |
michael@0 | 89 | |
michael@0 | 90 | mPrefs = getSharedPreferences(PREFS_NAME, 0); |
michael@0 | 91 | mNotificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); |
michael@0 | 92 | mConnectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); |
michael@0 | 93 | mCancelDownload = false; |
michael@0 | 94 | } |
michael@0 | 95 | |
michael@0 | 96 | @Override |
michael@0 | 97 | public synchronized int onStartCommand (Intent intent, int flags, int startId) { |
michael@0 | 98 | // If we are busy doing a download, the new Intent here would normally be queued for |
michael@0 | 99 | // execution once that is done. In this case, however, we want to flip the boolean |
michael@0 | 100 | // while that is running, so handle that now. |
michael@0 | 101 | if (mDownloading && UpdateServiceHelper.ACTION_APPLY_UPDATE.equals(intent.getAction())) { |
michael@0 | 102 | Log.i(LOGTAG, "will apply update when download finished"); |
michael@0 | 103 | |
michael@0 | 104 | mApplyImmediately = true; |
michael@0 | 105 | showDownloadNotification(); |
michael@0 | 106 | } else if (UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD.equals(intent.getAction())) { |
michael@0 | 107 | mCancelDownload = true; |
michael@0 | 108 | } else { |
michael@0 | 109 | super.onStartCommand(intent, flags, startId); |
michael@0 | 110 | } |
michael@0 | 111 | |
michael@0 | 112 | return Service.START_REDELIVER_INTENT; |
michael@0 | 113 | } |
michael@0 | 114 | |
michael@0 | 115 | @Override |
michael@0 | 116 | protected void onHandleIntent (Intent intent) { |
michael@0 | 117 | if (UpdateServiceHelper.ACTION_REGISTER_FOR_UPDATES.equals(intent.getAction())) { |
michael@0 | 118 | int policy = intent.getIntExtra(UpdateServiceHelper.EXTRA_AUTODOWNLOAD_NAME, -1); |
michael@0 | 119 | if (policy >= 0) { |
michael@0 | 120 | setAutoDownloadPolicy(policy); |
michael@0 | 121 | } |
michael@0 | 122 | |
michael@0 | 123 | registerForUpdates(false); |
michael@0 | 124 | } else if (UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE.equals(intent.getAction())) { |
michael@0 | 125 | startUpdate(intent.getIntExtra(UpdateServiceHelper.EXTRA_UPDATE_FLAGS_NAME, 0)); |
michael@0 | 126 | // Use this instead for forcing a download from about:fennec |
michael@0 | 127 | // startUpdate(UpdateServiceHelper.FLAG_FORCE_DOWNLOAD | UpdateServiceHelper.FLAG_REINSTALL); |
michael@0 | 128 | } else if (UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE.equals(intent.getAction())) { |
michael@0 | 129 | // We always want to do the download and apply it here |
michael@0 | 130 | mApplyImmediately = true; |
michael@0 | 131 | startUpdate(UpdateServiceHelper.FLAG_FORCE_DOWNLOAD); |
michael@0 | 132 | } else if (UpdateServiceHelper.ACTION_APPLY_UPDATE.equals(intent.getAction())) { |
michael@0 | 133 | applyUpdate(intent.getStringExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME)); |
michael@0 | 134 | } |
michael@0 | 135 | } |
michael@0 | 136 | |
michael@0 | 137 | private static boolean hasFlag(int flags, int flag) { |
michael@0 | 138 | return (flags & flag) == flag; |
michael@0 | 139 | } |
michael@0 | 140 | |
michael@0 | 141 | private void sendCheckUpdateResult(UpdateServiceHelper.CheckUpdateResult result) { |
michael@0 | 142 | Intent resultIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_UPDATE_RESULT); |
michael@0 | 143 | resultIntent.putExtra("result", result.toString()); |
michael@0 | 144 | sendBroadcast(resultIntent); |
michael@0 | 145 | } |
michael@0 | 146 | |
michael@0 | 147 | private int getUpdateInterval(boolean isRetry) { |
michael@0 | 148 | int interval; |
michael@0 | 149 | if (isRetry) { |
michael@0 | 150 | interval = INTERVAL_RETRY; |
michael@0 | 151 | } else if (!AppConstants.RELEASE_BUILD) { |
michael@0 | 152 | interval = INTERVAL_SHORT; |
michael@0 | 153 | } else { |
michael@0 | 154 | interval = INTERVAL_LONG; |
michael@0 | 155 | } |
michael@0 | 156 | |
michael@0 | 157 | return interval; |
michael@0 | 158 | } |
michael@0 | 159 | |
michael@0 | 160 | private void registerForUpdates(boolean isRetry) { |
michael@0 | 161 | Calendar lastAttempt = getLastAttemptDate(); |
michael@0 | 162 | Calendar now = new GregorianCalendar(TimeZone.getTimeZone("GMT")); |
michael@0 | 163 | |
michael@0 | 164 | int interval = getUpdateInterval(isRetry); |
michael@0 | 165 | |
michael@0 | 166 | if (lastAttempt == null || (now.getTimeInMillis() - lastAttempt.getTimeInMillis()) > interval) { |
michael@0 | 167 | // We've either never attempted an update, or we are passed the desired |
michael@0 | 168 | // time. Start an update now. |
michael@0 | 169 | Log.i(LOGTAG, "no update has ever been attempted, checking now"); |
michael@0 | 170 | startUpdate(0); |
michael@0 | 171 | return; |
michael@0 | 172 | } |
michael@0 | 173 | |
michael@0 | 174 | AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); |
michael@0 | 175 | if (manager == null) |
michael@0 | 176 | return; |
michael@0 | 177 | |
michael@0 | 178 | PendingIntent pending = PendingIntent.getService(this, 0, new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE, null, this, UpdateService.class), PendingIntent.FLAG_UPDATE_CURRENT); |
michael@0 | 179 | manager.cancel(pending); |
michael@0 | 180 | |
michael@0 | 181 | lastAttempt.setTimeInMillis(lastAttempt.getTimeInMillis() + interval); |
michael@0 | 182 | Log.i(LOGTAG, "next update will be at: " + lastAttempt.getTime()); |
michael@0 | 183 | |
michael@0 | 184 | manager.set(AlarmManager.RTC_WAKEUP, lastAttempt.getTimeInMillis(), pending); |
michael@0 | 185 | } |
michael@0 | 186 | |
michael@0 | 187 | private void startUpdate(int flags) { |
michael@0 | 188 | setLastAttemptDate(); |
michael@0 | 189 | |
michael@0 | 190 | NetworkInfo netInfo = mConnectivityManager.getActiveNetworkInfo(); |
michael@0 | 191 | if (netInfo == null || !netInfo.isConnected()) { |
michael@0 | 192 | Log.i(LOGTAG, "not connected to the network"); |
michael@0 | 193 | registerForUpdates(true); |
michael@0 | 194 | sendCheckUpdateResult(UpdateServiceHelper.CheckUpdateResult.NOT_AVAILABLE); |
michael@0 | 195 | return; |
michael@0 | 196 | } |
michael@0 | 197 | |
michael@0 | 198 | registerForUpdates(false); |
michael@0 | 199 | |
michael@0 | 200 | UpdateInfo info = findUpdate(hasFlag(flags, UpdateServiceHelper.FLAG_REINSTALL)); |
michael@0 | 201 | boolean haveUpdate = (info != null); |
michael@0 | 202 | |
michael@0 | 203 | if (!haveUpdate) { |
michael@0 | 204 | Log.i(LOGTAG, "no update available"); |
michael@0 | 205 | sendCheckUpdateResult(UpdateServiceHelper.CheckUpdateResult.NOT_AVAILABLE); |
michael@0 | 206 | return; |
michael@0 | 207 | } |
michael@0 | 208 | |
michael@0 | 209 | Log.i(LOGTAG, "update available, buildID = " + info.buildID); |
michael@0 | 210 | |
michael@0 | 211 | int connectionType = netInfo.getType(); |
michael@0 | 212 | int autoDownloadPolicy = getAutoDownloadPolicy(); |
michael@0 | 213 | |
michael@0 | 214 | |
michael@0 | 215 | /** |
michael@0 | 216 | * We only start a download automatically if one of following criteria are met: |
michael@0 | 217 | * |
michael@0 | 218 | * - We have a FORCE_DOWNLOAD flag passed in |
michael@0 | 219 | * - The preference is set to 'always' |
michael@0 | 220 | * - The preference is set to 'wifi' and we are actually using wifi (or regular ethernet) |
michael@0 | 221 | */ |
michael@0 | 222 | boolean shouldStartDownload = hasFlag(flags, UpdateServiceHelper.FLAG_FORCE_DOWNLOAD) || |
michael@0 | 223 | autoDownloadPolicy == UpdateServiceHelper.AUTODOWNLOAD_ENABLED || |
michael@0 | 224 | (autoDownloadPolicy == UpdateServiceHelper.AUTODOWNLOAD_WIFI && |
michael@0 | 225 | (connectionType == ConnectivityManager.TYPE_WIFI || connectionType == ConnectivityManager.TYPE_ETHERNET)); |
michael@0 | 226 | |
michael@0 | 227 | if (!shouldStartDownload) { |
michael@0 | 228 | Log.i(LOGTAG, "not initiating automatic update download due to policy " + autoDownloadPolicy); |
michael@0 | 229 | sendCheckUpdateResult(UpdateServiceHelper.CheckUpdateResult.AVAILABLE); |
michael@0 | 230 | |
michael@0 | 231 | // We aren't autodownloading here, so prompt to start the update |
michael@0 | 232 | Notification notification = new Notification(R.drawable.ic_status_logo, null, System.currentTimeMillis()); |
michael@0 | 233 | |
michael@0 | 234 | Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE); |
michael@0 | 235 | notificationIntent.setClass(this, UpdateService.class); |
michael@0 | 236 | |
michael@0 | 237 | PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); |
michael@0 | 238 | notification.flags = Notification.FLAG_AUTO_CANCEL; |
michael@0 | 239 | |
michael@0 | 240 | notification.setLatestEventInfo(this, getResources().getString(R.string.updater_start_title), |
michael@0 | 241 | getResources().getString(R.string.updater_start_select), |
michael@0 | 242 | contentIntent); |
michael@0 | 243 | |
michael@0 | 244 | mNotificationManager.notify(NOTIFICATION_ID, notification); |
michael@0 | 245 | |
michael@0 | 246 | return; |
michael@0 | 247 | } |
michael@0 | 248 | |
michael@0 | 249 | File pkg = downloadUpdatePackage(info, hasFlag(flags, UpdateServiceHelper.FLAG_OVERWRITE_EXISTING)); |
michael@0 | 250 | if (pkg == null) { |
michael@0 | 251 | sendCheckUpdateResult(UpdateServiceHelper.CheckUpdateResult.NOT_AVAILABLE); |
michael@0 | 252 | return; |
michael@0 | 253 | } |
michael@0 | 254 | |
michael@0 | 255 | Log.i(LOGTAG, "have update package at " + pkg); |
michael@0 | 256 | |
michael@0 | 257 | saveUpdateInfo(info, pkg); |
michael@0 | 258 | sendCheckUpdateResult(UpdateServiceHelper.CheckUpdateResult.DOWNLOADED); |
michael@0 | 259 | |
michael@0 | 260 | if (mApplyImmediately) { |
michael@0 | 261 | applyUpdate(pkg); |
michael@0 | 262 | } else { |
michael@0 | 263 | // Prompt to apply the update |
michael@0 | 264 | Notification notification = new Notification(R.drawable.ic_status_logo, null, System.currentTimeMillis()); |
michael@0 | 265 | |
michael@0 | 266 | Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE); |
michael@0 | 267 | notificationIntent.setClass(this, UpdateService.class); |
michael@0 | 268 | notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, pkg.getAbsolutePath()); |
michael@0 | 269 | |
michael@0 | 270 | PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); |
michael@0 | 271 | notification.flags = Notification.FLAG_AUTO_CANCEL; |
michael@0 | 272 | |
michael@0 | 273 | notification.setLatestEventInfo(this, getResources().getString(R.string.updater_apply_title), |
michael@0 | 274 | getResources().getString(R.string.updater_apply_select), |
michael@0 | 275 | contentIntent); |
michael@0 | 276 | |
michael@0 | 277 | mNotificationManager.notify(NOTIFICATION_ID, notification); |
michael@0 | 278 | } |
michael@0 | 279 | } |
michael@0 | 280 | |
michael@0 | 281 | private URLConnection openConnectionWithProxy(URL url) throws java.net.URISyntaxException, java.io.IOException { |
michael@0 | 282 | Log.i(LOGTAG, "opening connection with url: " + url); |
michael@0 | 283 | |
michael@0 | 284 | ProxySelector ps = ProxySelector.getDefault(); |
michael@0 | 285 | Proxy proxy = Proxy.NO_PROXY; |
michael@0 | 286 | if (ps != null) { |
michael@0 | 287 | List<Proxy> proxies = ps.select(url.toURI()); |
michael@0 | 288 | if (proxies != null && !proxies.isEmpty()) { |
michael@0 | 289 | proxy = proxies.get(0); |
michael@0 | 290 | } |
michael@0 | 291 | } |
michael@0 | 292 | |
michael@0 | 293 | return url.openConnection(proxy); |
michael@0 | 294 | } |
michael@0 | 295 | |
michael@0 | 296 | private UpdateInfo findUpdate(boolean force) { |
michael@0 | 297 | try { |
michael@0 | 298 | URL url = UpdateServiceHelper.getUpdateUrl(this, force); |
michael@0 | 299 | |
michael@0 | 300 | DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); |
michael@0 | 301 | Document dom = builder.parse(openConnectionWithProxy(url).getInputStream()); |
michael@0 | 302 | |
michael@0 | 303 | NodeList nodes = dom.getElementsByTagName("update"); |
michael@0 | 304 | if (nodes == null || nodes.getLength() == 0) |
michael@0 | 305 | return null; |
michael@0 | 306 | |
michael@0 | 307 | Node updateNode = nodes.item(0); |
michael@0 | 308 | Node buildIdNode = updateNode.getAttributes().getNamedItem("buildID"); |
michael@0 | 309 | if (buildIdNode == null) |
michael@0 | 310 | return null; |
michael@0 | 311 | |
michael@0 | 312 | nodes = dom.getElementsByTagName("patch"); |
michael@0 | 313 | if (nodes == null || nodes.getLength() == 0) |
michael@0 | 314 | return null; |
michael@0 | 315 | |
michael@0 | 316 | Node patchNode = nodes.item(0); |
michael@0 | 317 | Node urlNode = patchNode.getAttributes().getNamedItem("URL"); |
michael@0 | 318 | Node hashFunctionNode = patchNode.getAttributes().getNamedItem("hashFunction"); |
michael@0 | 319 | Node hashValueNode = patchNode.getAttributes().getNamedItem("hashValue"); |
michael@0 | 320 | Node sizeNode = patchNode.getAttributes().getNamedItem("size"); |
michael@0 | 321 | |
michael@0 | 322 | if (urlNode == null || hashFunctionNode == null || |
michael@0 | 323 | hashValueNode == null || sizeNode == null) { |
michael@0 | 324 | return null; |
michael@0 | 325 | } |
michael@0 | 326 | |
michael@0 | 327 | // Fill in UpdateInfo from the XML data |
michael@0 | 328 | UpdateInfo info = new UpdateInfo(); |
michael@0 | 329 | info.url = new URL(urlNode.getTextContent()); |
michael@0 | 330 | info.buildID = buildIdNode.getTextContent(); |
michael@0 | 331 | info.hashFunction = hashFunctionNode.getTextContent(); |
michael@0 | 332 | info.hashValue = hashValueNode.getTextContent(); |
michael@0 | 333 | |
michael@0 | 334 | try { |
michael@0 | 335 | info.size = Integer.parseInt(sizeNode.getTextContent()); |
michael@0 | 336 | } catch (NumberFormatException e) { |
michael@0 | 337 | Log.e(LOGTAG, "Failed to find APK size: ", e); |
michael@0 | 338 | return null; |
michael@0 | 339 | } |
michael@0 | 340 | |
michael@0 | 341 | // Make sure we have all the stuff we need to apply the update |
michael@0 | 342 | if (!info.isValid()) { |
michael@0 | 343 | Log.e(LOGTAG, "missing some required update information, have: " + info); |
michael@0 | 344 | return null; |
michael@0 | 345 | } |
michael@0 | 346 | |
michael@0 | 347 | return info; |
michael@0 | 348 | } catch (Exception e) { |
michael@0 | 349 | Log.e(LOGTAG, "failed to check for update: ", e); |
michael@0 | 350 | return null; |
michael@0 | 351 | } |
michael@0 | 352 | } |
michael@0 | 353 | |
michael@0 | 354 | private MessageDigest createMessageDigest(String hashFunction) { |
michael@0 | 355 | String javaHashFunction = null; |
michael@0 | 356 | |
michael@0 | 357 | if ("sha512".equalsIgnoreCase(hashFunction)) { |
michael@0 | 358 | javaHashFunction = "SHA-512"; |
michael@0 | 359 | } else { |
michael@0 | 360 | Log.e(LOGTAG, "Unhandled hash function: " + hashFunction); |
michael@0 | 361 | return null; |
michael@0 | 362 | } |
michael@0 | 363 | |
michael@0 | 364 | try { |
michael@0 | 365 | return MessageDigest.getInstance(javaHashFunction); |
michael@0 | 366 | } catch (java.security.NoSuchAlgorithmException e) { |
michael@0 | 367 | Log.e(LOGTAG, "Couldn't find algorithm " + javaHashFunction, e); |
michael@0 | 368 | return null; |
michael@0 | 369 | } |
michael@0 | 370 | } |
michael@0 | 371 | |
michael@0 | 372 | private void showDownloadNotification() { |
michael@0 | 373 | showDownloadNotification(null); |
michael@0 | 374 | } |
michael@0 | 375 | |
michael@0 | 376 | private void showDownloadNotification(File downloadFile) { |
michael@0 | 377 | |
michael@0 | 378 | Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE); |
michael@0 | 379 | notificationIntent.setClass(this, UpdateService.class); |
michael@0 | 380 | |
michael@0 | 381 | Intent cancelIntent = new Intent(UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD); |
michael@0 | 382 | cancelIntent.setClass(this, UpdateService.class); |
michael@0 | 383 | |
michael@0 | 384 | if (downloadFile != null) |
michael@0 | 385 | notificationIntent.putExtra(UpdateServiceHelper.EXTRA_PACKAGE_PATH_NAME, downloadFile.getAbsolutePath()); |
michael@0 | 386 | |
michael@0 | 387 | PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); |
michael@0 | 388 | PendingIntent deleteIntent = PendingIntent.getService(this, 0, cancelIntent, PendingIntent.FLAG_CANCEL_CURRENT); |
michael@0 | 389 | |
michael@0 | 390 | mBuilder = new NotificationCompat.Builder(this); |
michael@0 | 391 | mBuilder.setContentTitle(getResources().getString(R.string.updater_downloading_title)) |
michael@0 | 392 | .setContentText(mApplyImmediately ? "" : getResources().getString(R.string.updater_downloading_select)) |
michael@0 | 393 | .setSmallIcon(android.R.drawable.stat_sys_download) |
michael@0 | 394 | .setContentIntent(contentIntent) |
michael@0 | 395 | .setDeleteIntent(deleteIntent); |
michael@0 | 396 | |
michael@0 | 397 | mBuilder.setProgress(100, 0, true); |
michael@0 | 398 | mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build()); |
michael@0 | 399 | } |
michael@0 | 400 | |
michael@0 | 401 | private void showDownloadFailure() { |
michael@0 | 402 | Notification notification = new Notification(R.drawable.ic_status_logo, null, System.currentTimeMillis()); |
michael@0 | 403 | |
michael@0 | 404 | Intent notificationIntent = new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE); |
michael@0 | 405 | notificationIntent.setClass(this, UpdateService.class); |
michael@0 | 406 | |
michael@0 | 407 | PendingIntent contentIntent = PendingIntent.getService(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); |
michael@0 | 408 | |
michael@0 | 409 | notification.setLatestEventInfo(this, getResources().getString(R.string.updater_downloading_title_failed), |
michael@0 | 410 | getResources().getString(R.string.updater_downloading_retry), |
michael@0 | 411 | contentIntent); |
michael@0 | 412 | |
michael@0 | 413 | mNotificationManager.notify(NOTIFICATION_ID, notification); |
michael@0 | 414 | } |
michael@0 | 415 | |
michael@0 | 416 | private File downloadUpdatePackage(UpdateInfo info, boolean overwriteExisting) { |
michael@0 | 417 | File downloadFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), new File(info.url.getFile()).getName()); |
michael@0 | 418 | |
michael@0 | 419 | if (!overwriteExisting && info.buildID.equals(getLastBuildID()) && downloadFile.exists()) { |
michael@0 | 420 | // The last saved buildID is the same as the one for the current update. We also have a file |
michael@0 | 421 | // already downloaded, so it's probably the package we want. Verify it to be sure and just |
michael@0 | 422 | // return that if it matches. |
michael@0 | 423 | |
michael@0 | 424 | if (verifyDownloadedPackage(downloadFile)) { |
michael@0 | 425 | Log.i(LOGTAG, "using existing update package"); |
michael@0 | 426 | return downloadFile; |
michael@0 | 427 | } else { |
michael@0 | 428 | // Didn't match, so we're going to download a new one. |
michael@0 | 429 | downloadFile.delete(); |
michael@0 | 430 | } |
michael@0 | 431 | } |
michael@0 | 432 | |
michael@0 | 433 | Log.i(LOGTAG, "downloading update package"); |
michael@0 | 434 | sendCheckUpdateResult(UpdateServiceHelper.CheckUpdateResult.DOWNLOADING); |
michael@0 | 435 | |
michael@0 | 436 | OutputStream output = null; |
michael@0 | 437 | InputStream input = null; |
michael@0 | 438 | |
michael@0 | 439 | mDownloading = true; |
michael@0 | 440 | mCancelDownload = false; |
michael@0 | 441 | showDownloadNotification(downloadFile); |
michael@0 | 442 | |
michael@0 | 443 | try { |
michael@0 | 444 | URLConnection conn = openConnectionWithProxy(info.url); |
michael@0 | 445 | int length = conn.getContentLength(); |
michael@0 | 446 | |
michael@0 | 447 | output = new BufferedOutputStream(new FileOutputStream(downloadFile)); |
michael@0 | 448 | input = new BufferedInputStream(conn.getInputStream()); |
michael@0 | 449 | |
michael@0 | 450 | byte[] buf = new byte[BUFSIZE]; |
michael@0 | 451 | int len = 0; |
michael@0 | 452 | |
michael@0 | 453 | int bytesRead = 0; |
michael@0 | 454 | int lastNotify = 0; |
michael@0 | 455 | |
michael@0 | 456 | while ((len = input.read(buf, 0, BUFSIZE)) > 0 && !mCancelDownload) { |
michael@0 | 457 | output.write(buf, 0, len); |
michael@0 | 458 | bytesRead += len; |
michael@0 | 459 | // Updating the notification takes time so only do it every 1MB |
michael@0 | 460 | if(bytesRead - lastNotify > 1048576) { |
michael@0 | 461 | mBuilder.setProgress(length, bytesRead, false); |
michael@0 | 462 | mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build()); |
michael@0 | 463 | lastNotify = bytesRead; |
michael@0 | 464 | } |
michael@0 | 465 | } |
michael@0 | 466 | |
michael@0 | 467 | mNotificationManager.cancel(NOTIFICATION_ID); |
michael@0 | 468 | |
michael@0 | 469 | // if the download was canceled by the user |
michael@0 | 470 | // delete the update package |
michael@0 | 471 | if (mCancelDownload) { |
michael@0 | 472 | Log.i(LOGTAG, "download canceled by user!"); |
michael@0 | 473 | downloadFile.delete(); |
michael@0 | 474 | |
michael@0 | 475 | return null; |
michael@0 | 476 | } else { |
michael@0 | 477 | Log.i(LOGTAG, "completed update download!"); |
michael@0 | 478 | return downloadFile; |
michael@0 | 479 | } |
michael@0 | 480 | } catch (Exception e) { |
michael@0 | 481 | downloadFile.delete(); |
michael@0 | 482 | showDownloadFailure(); |
michael@0 | 483 | |
michael@0 | 484 | Log.e(LOGTAG, "failed to download update: ", e); |
michael@0 | 485 | return null; |
michael@0 | 486 | } finally { |
michael@0 | 487 | try { |
michael@0 | 488 | if (input != null) |
michael@0 | 489 | input.close(); |
michael@0 | 490 | } catch (java.io.IOException e) {} |
michael@0 | 491 | |
michael@0 | 492 | try { |
michael@0 | 493 | if (output != null) |
michael@0 | 494 | output.close(); |
michael@0 | 495 | } catch (java.io.IOException e) {} |
michael@0 | 496 | |
michael@0 | 497 | mDownloading = false; |
michael@0 | 498 | } |
michael@0 | 499 | } |
michael@0 | 500 | |
michael@0 | 501 | private boolean verifyDownloadedPackage(File updateFile) { |
michael@0 | 502 | MessageDigest digest = createMessageDigest(getLastHashFunction()); |
michael@0 | 503 | if (digest == null) |
michael@0 | 504 | return false; |
michael@0 | 505 | |
michael@0 | 506 | InputStream input = null; |
michael@0 | 507 | |
michael@0 | 508 | try { |
michael@0 | 509 | input = new BufferedInputStream(new FileInputStream(updateFile)); |
michael@0 | 510 | |
michael@0 | 511 | byte[] buf = new byte[BUFSIZE]; |
michael@0 | 512 | int len; |
michael@0 | 513 | while ((len = input.read(buf, 0, BUFSIZE)) > 0) { |
michael@0 | 514 | digest.update(buf, 0, len); |
michael@0 | 515 | } |
michael@0 | 516 | } catch (java.io.IOException e) { |
michael@0 | 517 | Log.e(LOGTAG, "Failed to verify update package: ", e); |
michael@0 | 518 | return false; |
michael@0 | 519 | } finally { |
michael@0 | 520 | try { |
michael@0 | 521 | if (input != null) |
michael@0 | 522 | input.close(); |
michael@0 | 523 | } catch(java.io.IOException e) {} |
michael@0 | 524 | } |
michael@0 | 525 | |
michael@0 | 526 | String hex = Hex.encodeHexString(digest.digest()); |
michael@0 | 527 | if (!hex.equals(getLastHashValue())) { |
michael@0 | 528 | Log.e(LOGTAG, "Package hash does not match"); |
michael@0 | 529 | return false; |
michael@0 | 530 | } |
michael@0 | 531 | |
michael@0 | 532 | return true; |
michael@0 | 533 | } |
michael@0 | 534 | |
michael@0 | 535 | private void applyUpdate(String updatePath) { |
michael@0 | 536 | if (updatePath == null) { |
michael@0 | 537 | updatePath = mPrefs.getString(KEY_LAST_FILE_NAME, null); |
michael@0 | 538 | } |
michael@0 | 539 | applyUpdate(new File(updatePath)); |
michael@0 | 540 | } |
michael@0 | 541 | |
michael@0 | 542 | private void applyUpdate(File updateFile) { |
michael@0 | 543 | mApplyImmediately = false; |
michael@0 | 544 | |
michael@0 | 545 | if (!updateFile.exists()) |
michael@0 | 546 | return; |
michael@0 | 547 | |
michael@0 | 548 | Log.i(LOGTAG, "Verifying package: " + updateFile); |
michael@0 | 549 | |
michael@0 | 550 | if (!verifyDownloadedPackage(updateFile)) { |
michael@0 | 551 | Log.e(LOGTAG, "Not installing update, failed verification"); |
michael@0 | 552 | return; |
michael@0 | 553 | } |
michael@0 | 554 | |
michael@0 | 555 | Intent intent = new Intent(Intent.ACTION_VIEW); |
michael@0 | 556 | intent.setDataAndType(Uri.fromFile(updateFile), "application/vnd.android.package-archive"); |
michael@0 | 557 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
michael@0 | 558 | startActivity(intent); |
michael@0 | 559 | } |
michael@0 | 560 | |
michael@0 | 561 | private String getLastBuildID() { |
michael@0 | 562 | return mPrefs.getString(KEY_LAST_BUILDID, null); |
michael@0 | 563 | } |
michael@0 | 564 | |
michael@0 | 565 | private String getLastHashFunction() { |
michael@0 | 566 | return mPrefs.getString(KEY_LAST_HASH_FUNCTION, null); |
michael@0 | 567 | } |
michael@0 | 568 | |
michael@0 | 569 | private String getLastHashValue() { |
michael@0 | 570 | return mPrefs.getString(KEY_LAST_HASH_VALUE, null); |
michael@0 | 571 | } |
michael@0 | 572 | |
michael@0 | 573 | private Calendar getLastAttemptDate() { |
michael@0 | 574 | long lastAttempt = mPrefs.getLong(KEY_LAST_ATTEMPT_DATE, -1); |
michael@0 | 575 | if (lastAttempt < 0) |
michael@0 | 576 | return null; |
michael@0 | 577 | |
michael@0 | 578 | GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); |
michael@0 | 579 | cal.setTimeInMillis(lastAttempt); |
michael@0 | 580 | return cal; |
michael@0 | 581 | } |
michael@0 | 582 | |
michael@0 | 583 | private void setLastAttemptDate() { |
michael@0 | 584 | SharedPreferences.Editor editor = mPrefs.edit(); |
michael@0 | 585 | editor.putLong(KEY_LAST_ATTEMPT_DATE, System.currentTimeMillis()); |
michael@0 | 586 | editor.commit(); |
michael@0 | 587 | } |
michael@0 | 588 | |
michael@0 | 589 | private int getAutoDownloadPolicy() { |
michael@0 | 590 | return mPrefs.getInt(KEY_AUTODOWNLOAD_POLICY, UpdateServiceHelper.AUTODOWNLOAD_WIFI); |
michael@0 | 591 | } |
michael@0 | 592 | |
michael@0 | 593 | private void setAutoDownloadPolicy(int policy) { |
michael@0 | 594 | SharedPreferences.Editor editor = mPrefs.edit(); |
michael@0 | 595 | editor.putInt(KEY_AUTODOWNLOAD_POLICY, policy); |
michael@0 | 596 | editor.commit(); |
michael@0 | 597 | } |
michael@0 | 598 | |
michael@0 | 599 | private void saveUpdateInfo(UpdateInfo info, File downloaded) { |
michael@0 | 600 | SharedPreferences.Editor editor = mPrefs.edit(); |
michael@0 | 601 | editor.putString(KEY_LAST_BUILDID, info.buildID); |
michael@0 | 602 | editor.putString(KEY_LAST_HASH_FUNCTION, info.hashFunction); |
michael@0 | 603 | editor.putString(KEY_LAST_HASH_VALUE, info.hashValue); |
michael@0 | 604 | editor.putString(KEY_LAST_FILE_NAME, downloaded.toString()); |
michael@0 | 605 | editor.commit(); |
michael@0 | 606 | } |
michael@0 | 607 | |
michael@0 | 608 | private class UpdateInfo { |
michael@0 | 609 | public URL url; |
michael@0 | 610 | public String buildID; |
michael@0 | 611 | public String hashFunction; |
michael@0 | 612 | public String hashValue; |
michael@0 | 613 | public int size; |
michael@0 | 614 | |
michael@0 | 615 | private boolean isNonEmpty(String s) { |
michael@0 | 616 | return s != null && s.length() > 0; |
michael@0 | 617 | } |
michael@0 | 618 | |
michael@0 | 619 | public boolean isValid() { |
michael@0 | 620 | return url != null && isNonEmpty(buildID) && |
michael@0 | 621 | isNonEmpty(hashFunction) && isNonEmpty(hashValue) && size > 0; |
michael@0 | 622 | } |
michael@0 | 623 | |
michael@0 | 624 | @Override |
michael@0 | 625 | public String toString() { |
michael@0 | 626 | return "url = " + url + ", buildID = " + buildID + ", hashFunction = " + hashFunction + ", hashValue = " + hashValue + ", size = " + size; |
michael@0 | 627 | } |
michael@0 | 628 | } |
michael@0 | 629 | } |