1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.quake; 17 18 19 import java.io.File; 20 import java.io.FileInputStream; 21 import java.io.FileNotFoundException; 22 import java.io.FileOutputStream; 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.io.OutputStream; 26 import java.io.UnsupportedEncodingException; 27 import java.net.MalformedURLException; 28 import java.net.URL; 29 import java.nio.channels.FileLock; 30 31 import java.security.MessageDigest; 32 import java.security.NoSuchAlgorithmException; 33 import java.text.DecimalFormat; 34 import java.util.ArrayList; 35 import java.util.HashMap; 36 import java.util.HashSet; 37 38 import org.apache.http.Header; 39 import org.apache.http.HttpEntity; 40 import org.apache.http.HttpResponse; 41 import org.apache.http.HttpStatus; 42 import org.apache.http.client.ClientProtocolException; 43 import org.apache.http.client.methods.HttpGet; 44 import org.apache.http.client.methods.HttpHead; 45 import org.xml.sax.Attributes; 46 import org.xml.sax.SAXException; 47 import org.xml.sax.helpers.DefaultHandler; 48 49 import android.app.Activity; 50 import android.app.AlertDialog; 51 import android.app.AlertDialog.Builder; 52 import android.content.DialogInterface; 53 import android.content.Intent; 54 import android.os.Bundle; 55 import android.os.Handler; 56 import android.os.Message; 57 import android.os.SystemClock; 58 import android.net.http.AndroidHttpClient; 59 import android.util.Log; 60 import android.util.Xml; 61 import android.view.View; 62 import android.view.Window; 63 import android.view.WindowManager; 64 import android.widget.Button; 65 import android.widget.TextView; 66 67 public class DownloaderActivity extends Activity { 68 69 /** 70 * Checks if data has been downloaded. If so, returns true. If not, 71 * starts an activity to download the data and returns false. If this 72 * function returns false the caller should immediately return from its 73 * onCreate method. The calling activity will later be restarted 74 * (using a copy of its original intent) once the data download completes. 75 * @param activity The calling activity. 76 * @param customText A text string that is displayed in the downloader UI. 77 * @param fileConfigUrl The URL of the download configuration URL. 78 * @param configVersion The version of the configuration file. 79 * @param dataPath The directory on the device where we want to store the 80 * data. 81 * @param userAgent The user agent string to use when fetching URLs. 82 * @return true if the data has already been downloaded successfully, or 83 * false if the data needs to be downloaded. 84 */ ensureDownloaded(Activity activity, String customText, String fileConfigUrl, String configVersion, String dataPath, String userAgent)85 public static boolean ensureDownloaded(Activity activity, 86 String customText, String fileConfigUrl, 87 String configVersion, String dataPath, 88 String userAgent) { 89 File dest = new File(dataPath); 90 if (dest.exists()) { 91 // Check version 92 if (versionMatches(dest, configVersion)) { 93 Log.i(LOG_TAG, "Versions match, no need to download."); 94 return true; 95 } 96 } 97 Intent intent = PreconditionActivityHelper.createPreconditionIntent( 98 activity, DownloaderActivity.class); 99 intent.putExtra(EXTRA_CUSTOM_TEXT, customText); 100 intent.putExtra(EXTRA_FILE_CONFIG_URL, fileConfigUrl); 101 intent.putExtra(EXTRA_CONFIG_VERSION, configVersion); 102 intent.putExtra(EXTRA_DATA_PATH, dataPath); 103 intent.putExtra(EXTRA_USER_AGENT, userAgent); 104 PreconditionActivityHelper.startPreconditionActivityAndFinish( 105 activity, intent); 106 return false; 107 } 108 109 /** 110 * Delete a directory and all its descendants. 111 * @param directory The directory to delete 112 * @return true if the directory was deleted successfully. 113 */ deleteData(String directory)114 public static boolean deleteData(String directory) { 115 return deleteTree(new File(directory), true); 116 } 117 deleteTree(File base, boolean deleteBase)118 private static boolean deleteTree(File base, boolean deleteBase) { 119 boolean result = true; 120 if (base.isDirectory()) { 121 for (File child : base.listFiles()) { 122 result &= deleteTree(child, true); 123 } 124 } 125 if (deleteBase) { 126 result &= base.delete(); 127 } 128 return result; 129 } 130 versionMatches(File dest, String expectedVersion)131 private static boolean versionMatches(File dest, String expectedVersion) { 132 Config config = getLocalConfig(dest, LOCAL_CONFIG_FILE); 133 if (config != null) { 134 return config.version.equals(expectedVersion); 135 } 136 return false; 137 } 138 getLocalConfig(File destPath, String configFilename)139 private static Config getLocalConfig(File destPath, String configFilename) { 140 File configPath = new File(destPath, configFilename); 141 FileInputStream is; 142 try { 143 is = new FileInputStream(configPath); 144 } catch (FileNotFoundException e) { 145 return null; 146 } 147 try { 148 Config config = ConfigHandler.parse(is); 149 return config; 150 } catch (Exception e) { 151 Log.e(LOG_TAG, "Unable to read local config file", e); 152 return null; 153 } finally { 154 quietClose(is); 155 } 156 } 157 158 @Override onCreate(Bundle savedInstanceState)159 protected void onCreate(Bundle savedInstanceState) { 160 super.onCreate(savedInstanceState); 161 Intent intent = getIntent(); 162 requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); 163 setContentView(R.layout.downloader); 164 getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, 165 R.layout.downloader_title); 166 ((TextView) findViewById(R.id.customText)).setText( 167 intent.getStringExtra(EXTRA_CUSTOM_TEXT)); 168 mProgress = (TextView) findViewById(R.id.progress); 169 mTimeRemaining = (TextView) findViewById(R.id.time_remaining); 170 Button button = (Button) findViewById(R.id.cancel); 171 button.setOnClickListener(new Button.OnClickListener() { 172 public void onClick(View v) { 173 if (mDownloadThread != null) { 174 mSuppressErrorMessages = true; 175 mDownloadThread.interrupt(); 176 } 177 } 178 }); 179 startDownloadThread(); 180 } 181 startDownloadThread()182 private void startDownloadThread() { 183 mSuppressErrorMessages = false; 184 mProgress.setText(""); 185 mTimeRemaining.setText(""); 186 mDownloadThread = new Thread(new Downloader(), "Downloader"); 187 mDownloadThread.setPriority(Thread.NORM_PRIORITY - 1); 188 mDownloadThread.start(); 189 } 190 191 @Override onResume()192 protected void onResume() { 193 super.onResume(); 194 } 195 196 @Override onDestroy()197 protected void onDestroy() { 198 super.onDestroy(); 199 mSuppressErrorMessages = true; 200 mDownloadThread.interrupt(); 201 try { 202 mDownloadThread.join(); 203 } catch (InterruptedException e) { 204 // Don't care. 205 } 206 } 207 onDownloadSucceeded()208 private void onDownloadSucceeded() { 209 Log.i(LOG_TAG, "Download succeeded"); 210 PreconditionActivityHelper.startOriginalActivityAndFinish(this); 211 } 212 onDownloadFailed(String reason)213 private void onDownloadFailed(String reason) { 214 Log.e(LOG_TAG, "Download stopped: " + reason); 215 String shortReason; 216 int index = reason.indexOf('\n'); 217 if (index >= 0) { 218 shortReason = reason.substring(0, index); 219 } else { 220 shortReason = reason; 221 } 222 AlertDialog alert = new Builder(this).create(); 223 alert.setTitle(R.string.download_activity_download_stopped); 224 225 if (!mSuppressErrorMessages) { 226 alert.setMessage(shortReason); 227 } 228 229 alert.setButton(getString(R.string.download_activity_retry), 230 new DialogInterface.OnClickListener() { 231 public void onClick(DialogInterface dialog, int which) { 232 startDownloadThread(); 233 } 234 235 }); 236 alert.setButton2(getString(R.string.download_activity_quit), 237 new DialogInterface.OnClickListener() { 238 public void onClick(DialogInterface dialog, int which) { 239 finish(); 240 } 241 242 }); 243 try { 244 alert.show(); 245 } catch (WindowManager.BadTokenException e) { 246 // Happens when the Back button is used to exit the activity. 247 // ignore. 248 } 249 } 250 onReportProgress(int progress)251 private void onReportProgress(int progress) { 252 mProgress.setText(mPercentFormat.format(progress / 10000.0)); 253 long now = SystemClock.elapsedRealtime(); 254 if (mStartTime == 0) { 255 mStartTime = now; 256 } 257 long delta = now - mStartTime; 258 String timeRemaining = getString(R.string.download_activity_time_remaining_unknown); 259 if ((delta > 3 * MS_PER_SECOND) && (progress > 100)) { 260 long totalTime = 10000 * delta / progress; 261 long timeLeft = Math.max(0L, totalTime - delta); 262 if (timeLeft > MS_PER_DAY) { 263 timeRemaining = Long.toString( 264 (timeLeft + MS_PER_DAY - 1) / MS_PER_DAY) 265 + " " 266 + getString(R.string.download_activity_time_remaining_days); 267 } else if (timeLeft > MS_PER_HOUR) { 268 timeRemaining = Long.toString( 269 (timeLeft + MS_PER_HOUR - 1) / MS_PER_HOUR) 270 + " " 271 + getString(R.string.download_activity_time_remaining_hours); 272 } else if (timeLeft > MS_PER_MINUTE) { 273 timeRemaining = Long.toString( 274 (timeLeft + MS_PER_MINUTE - 1) / MS_PER_MINUTE) 275 + " " 276 + getString(R.string.download_activity_time_remaining_minutes); 277 } else { 278 timeRemaining = Long.toString( 279 (timeLeft + MS_PER_SECOND - 1) / MS_PER_SECOND) 280 + " " 281 + getString(R.string.download_activity_time_remaining_seconds); 282 } 283 } 284 mTimeRemaining.setText(timeRemaining); 285 } 286 quietClose(InputStream is)287 private static void quietClose(InputStream is) { 288 try { 289 if (is != null) { 290 is.close(); 291 } 292 } catch (IOException e) { 293 // Don't care. 294 } 295 } 296 quietClose(OutputStream os)297 private static void quietClose(OutputStream os) { 298 try { 299 if (os != null) { 300 os.close(); 301 } 302 } catch (IOException e) { 303 // Don't care. 304 } 305 } 306 307 private static class Config { getSize()308 long getSize() { 309 long result = 0; 310 for(File file : mFiles) { 311 result += file.getSize(); 312 } 313 return result; 314 } 315 static class File { File(String src, String dest, String md5, long size)316 public File(String src, String dest, String md5, long size) { 317 if (src != null) { 318 this.mParts.add(new Part(src, md5, size)); 319 } 320 this.dest = dest; 321 } 322 static class Part { Part(String src, String md5, long size)323 Part(String src, String md5, long size) { 324 this.src = src; 325 this.md5 = md5; 326 this.size = size; 327 } 328 String src; 329 String md5; 330 long size; 331 } 332 ArrayList<Part> mParts = new ArrayList<Part>(); 333 String dest; getSize()334 long getSize() { 335 long result = 0; 336 for(Part part : mParts) { 337 if (part.size > 0) { 338 result += part.size; 339 } 340 } 341 return result; 342 } 343 } 344 String version; 345 ArrayList<File> mFiles = new ArrayList<File>(); 346 } 347 348 /** 349 * <config version=""> 350 * <file src="http:..." dest ="b.x" /> 351 * <file dest="b.x"> 352 * <part src="http:..." /> 353 * ... 354 * ... 355 * </config> 356 * 357 */ 358 private static class ConfigHandler extends DefaultHandler { 359 parse(InputStream is)360 public static Config parse(InputStream is) throws SAXException, 361 UnsupportedEncodingException, IOException { 362 ConfigHandler handler = new ConfigHandler(); 363 Xml.parse(is, Xml.findEncodingByName("UTF-8"), handler); 364 return handler.mConfig; 365 } 366 ConfigHandler()367 private ConfigHandler() { 368 mConfig = new Config(); 369 } 370 371 @Override startElement(String uri, String localName, String qName, Attributes attributes)372 public void startElement(String uri, String localName, String qName, 373 Attributes attributes) throws SAXException { 374 if (localName.equals("config")) { 375 mConfig.version = getRequiredString(attributes, "version"); 376 } else if (localName.equals("file")) { 377 String src = attributes.getValue("", "src"); 378 String dest = getRequiredString(attributes, "dest"); 379 String md5 = attributes.getValue("", "md5"); 380 long size = getLong(attributes, "size", -1); 381 mConfig.mFiles.add(new Config.File(src, dest, md5, size)); 382 } else if (localName.equals("part")) { 383 String src = getRequiredString(attributes, "src"); 384 String md5 = attributes.getValue("", "md5"); 385 long size = getLong(attributes, "size", -1); 386 int length = mConfig.mFiles.size(); 387 if (length > 0) { 388 mConfig.mFiles.get(length-1).mParts.add( 389 new Config.File.Part(src, md5, size)); 390 } 391 } 392 } 393 getRequiredString(Attributes attributes, String localName)394 private static String getRequiredString(Attributes attributes, 395 String localName) throws SAXException { 396 String result = attributes.getValue("", localName); 397 if (result == null) { 398 throw new SAXException("Expected attribute " + localName); 399 } 400 return result; 401 } 402 getLong(Attributes attributes, String localName, long defaultValue)403 private static long getLong(Attributes attributes, String localName, 404 long defaultValue) { 405 String value = attributes.getValue("", localName); 406 if (value == null) { 407 return defaultValue; 408 } else { 409 return Long.parseLong(value); 410 } 411 } 412 413 public Config mConfig; 414 } 415 416 private class DownloaderException extends Exception { DownloaderException(String reason)417 public DownloaderException(String reason) { 418 super(reason); 419 } 420 } 421 422 private class Downloader implements Runnable { run()423 public void run() { 424 Intent intent = getIntent(); 425 mFileConfigUrl = intent.getStringExtra(EXTRA_FILE_CONFIG_URL); 426 mConfigVersion = intent.getStringExtra(EXTRA_CONFIG_VERSION); 427 mDataPath = intent.getStringExtra(EXTRA_DATA_PATH); 428 mUserAgent = intent.getStringExtra(EXTRA_USER_AGENT); 429 430 mDataDir = new File(mDataPath); 431 432 try { 433 // Download files. 434 mHttpClient = AndroidHttpClient.newInstance(mUserAgent); 435 try { 436 Config config = getConfig(); 437 filter(config); 438 persistantDownload(config); 439 verify(config); 440 cleanup(); 441 reportSuccess(); 442 } finally { 443 mHttpClient.close(); 444 } 445 } catch (Exception e) { 446 reportFailure(e.toString() + "\n" + Log.getStackTraceString(e)); 447 } 448 } 449 persistantDownload(Config config)450 private void persistantDownload(Config config) 451 throws ClientProtocolException, DownloaderException, IOException { 452 while(true) { 453 try { 454 download(config); 455 break; 456 } catch(java.net.SocketException e) { 457 if (mSuppressErrorMessages) { 458 throw e; 459 } 460 } catch(java.net.SocketTimeoutException e) { 461 if (mSuppressErrorMessages) { 462 throw e; 463 } 464 } 465 Log.i(LOG_TAG, "Network connectivity issue, retrying."); 466 } 467 } 468 filter(Config config)469 private void filter(Config config) 470 throws IOException, DownloaderException { 471 File filteredFile = new File(mDataDir, LOCAL_FILTERED_FILE); 472 if (filteredFile.exists()) { 473 return; 474 } 475 476 File localConfigFile = new File(mDataDir, LOCAL_CONFIG_FILE_TEMP); 477 HashSet<String> keepSet = new HashSet<String>(); 478 keepSet.add(localConfigFile.getCanonicalPath()); 479 480 HashMap<String, Config.File> fileMap = 481 new HashMap<String, Config.File>(); 482 for(Config.File file : config.mFiles) { 483 String canonicalPath = 484 new File(mDataDir, file.dest).getCanonicalPath(); 485 fileMap.put(canonicalPath, file); 486 } 487 recursiveFilter(mDataDir, fileMap, keepSet, false); 488 touch(filteredFile); 489 } 490 touch(File file)491 private void touch(File file) throws FileNotFoundException { 492 FileOutputStream os = new FileOutputStream(file); 493 quietClose(os); 494 } 495 recursiveFilter(File base, HashMap<String, Config.File> fileMap, HashSet<String> keepSet, boolean filterBase)496 private boolean recursiveFilter(File base, 497 HashMap<String, Config.File> fileMap, 498 HashSet<String> keepSet, boolean filterBase) 499 throws IOException, DownloaderException { 500 boolean result = true; 501 if (base.isDirectory()) { 502 for (File child : base.listFiles()) { 503 result &= recursiveFilter(child, fileMap, keepSet, true); 504 } 505 } 506 if (filterBase) { 507 if (base.isDirectory()) { 508 if (base.listFiles().length == 0) { 509 result &= base.delete(); 510 } 511 } else { 512 if (!shouldKeepFile(base, fileMap, keepSet)) { 513 result &= base.delete(); 514 } 515 } 516 } 517 return result; 518 } 519 shouldKeepFile(File file, HashMap<String, Config.File> fileMap, HashSet<String> keepSet)520 private boolean shouldKeepFile(File file, 521 HashMap<String, Config.File> fileMap, 522 HashSet<String> keepSet) 523 throws IOException, DownloaderException { 524 String canonicalPath = file.getCanonicalPath(); 525 if (keepSet.contains(canonicalPath)) { 526 return true; 527 } 528 Config.File configFile = fileMap.get(canonicalPath); 529 if (configFile == null) { 530 return false; 531 } 532 return verifyFile(configFile, false); 533 } 534 reportSuccess()535 private void reportSuccess() { 536 mHandler.sendMessage( 537 Message.obtain(mHandler, MSG_DOWNLOAD_SUCCEEDED)); 538 } 539 reportFailure(String reason)540 private void reportFailure(String reason) { 541 mHandler.sendMessage( 542 Message.obtain(mHandler, MSG_DOWNLOAD_FAILED, reason)); 543 } 544 reportProgress(int progress)545 private void reportProgress(int progress) { 546 mHandler.sendMessage( 547 Message.obtain(mHandler, MSG_REPORT_PROGRESS, progress, 0)); 548 } 549 getConfig()550 private Config getConfig() throws DownloaderException, 551 ClientProtocolException, IOException, SAXException { 552 Config config = null; 553 if (mDataDir.exists()) { 554 config = getLocalConfig(mDataDir, LOCAL_CONFIG_FILE_TEMP); 555 if ((config == null) 556 || !mConfigVersion.equals(config.version)) { 557 if (config == null) { 558 Log.i(LOG_TAG, "Couldn't find local config."); 559 } else { 560 Log.i(LOG_TAG, "Local version out of sync. Wanted " + 561 mConfigVersion + " but have " + config.version); 562 } 563 config = null; 564 } 565 } else { 566 Log.i(LOG_TAG, "Creating directory " + mDataPath); 567 mDataDir.mkdirs(); 568 mDataDir.mkdir(); 569 if (!mDataDir.exists()) { 570 throw new DownloaderException( 571 "Could not create the directory " + mDataPath); 572 } 573 } 574 if (config == null) { 575 File localConfig = download(mFileConfigUrl, 576 LOCAL_CONFIG_FILE_TEMP); 577 InputStream is = new FileInputStream(localConfig); 578 try { 579 config = ConfigHandler.parse(is); 580 } finally { 581 quietClose(is); 582 } 583 if (! config.version.equals(mConfigVersion)) { 584 throw new DownloaderException( 585 "Configuration file version mismatch. Expected " + 586 mConfigVersion + " received " + 587 config.version); 588 } 589 } 590 return config; 591 } 592 noisyDelete(File file)593 private void noisyDelete(File file) throws IOException { 594 if (! file.delete() ) { 595 throw new IOException("could not delete " + file); 596 } 597 } 598 download(Config config)599 private void download(Config config) throws DownloaderException, 600 ClientProtocolException, IOException { 601 mDownloadedSize = 0; 602 getSizes(config); 603 Log.i(LOG_TAG, "Total bytes to download: " 604 + mTotalExpectedSize); 605 for(Config.File file : config.mFiles) { 606 downloadFile(file); 607 } 608 } 609 downloadFile(Config.File file)610 private void downloadFile(Config.File file) throws DownloaderException, 611 FileNotFoundException, IOException, ClientProtocolException { 612 boolean append = false; 613 File dest = new File(mDataDir, file.dest); 614 long bytesToSkip = 0; 615 if (dest.exists() && dest.isFile()) { 616 append = true; 617 bytesToSkip = dest.length(); 618 mDownloadedSize += bytesToSkip; 619 } 620 FileOutputStream os = null; 621 long offsetOfCurrentPart = 0; 622 try { 623 for(Config.File.Part part : file.mParts) { 624 // The part.size==0 check below allows us to download 625 // zero-length files. 626 if ((part.size > bytesToSkip) || (part.size == 0)) { 627 MessageDigest digest = null; 628 if (part.md5 != null) { 629 digest = createDigest(); 630 if (bytesToSkip > 0) { 631 FileInputStream is = openInput(file.dest); 632 try { 633 is.skip(offsetOfCurrentPart); 634 readIntoDigest(is, bytesToSkip, digest); 635 } finally { 636 quietClose(is); 637 } 638 } 639 } 640 if (os == null) { 641 os = openOutput(file.dest, append); 642 } 643 downloadPart(part.src, os, bytesToSkip, 644 part.size, digest); 645 if (digest != null) { 646 String hash = getHash(digest); 647 if (!hash.equalsIgnoreCase(part.md5)) { 648 Log.e(LOG_TAG, "web MD5 checksums don't match. " 649 + part.src + "\nExpected " 650 + part.md5 + "\n got " + hash); 651 quietClose(os); 652 dest.delete(); 653 throw new DownloaderException( 654 "Received bad data from web server"); 655 } else { 656 Log.i(LOG_TAG, "web MD5 checksum matches."); 657 } 658 } 659 } 660 bytesToSkip -= Math.min(bytesToSkip, part.size); 661 offsetOfCurrentPart += part.size; 662 } 663 } finally { 664 quietClose(os); 665 } 666 } 667 cleanup()668 private void cleanup() throws IOException { 669 File filtered = new File(mDataDir, LOCAL_FILTERED_FILE); 670 noisyDelete(filtered); 671 File tempConfig = new File(mDataDir, LOCAL_CONFIG_FILE_TEMP); 672 File realConfig = new File(mDataDir, LOCAL_CONFIG_FILE); 673 tempConfig.renameTo(realConfig); 674 } 675 verify(Config config)676 private void verify(Config config) throws DownloaderException, 677 ClientProtocolException, IOException { 678 Log.i(LOG_TAG, "Verifying..."); 679 String failFiles = null; 680 for(Config.File file : config.mFiles) { 681 if (! verifyFile(file, true) ) { 682 if (failFiles == null) { 683 failFiles = file.dest; 684 } else { 685 failFiles += " " + file.dest; 686 } 687 } 688 } 689 if (failFiles != null) { 690 throw new DownloaderException( 691 "Possible bad SD-Card. MD5 sum incorrect for file(s) " 692 + failFiles); 693 } 694 } 695 verifyFile(Config.File file, boolean deleteInvalid)696 private boolean verifyFile(Config.File file, boolean deleteInvalid) 697 throws FileNotFoundException, DownloaderException, IOException { 698 Log.i(LOG_TAG, "verifying " + file.dest); 699 File dest = new File(mDataDir, file.dest); 700 if (! dest.exists()) { 701 Log.e(LOG_TAG, "File does not exist: " + dest.toString()); 702 return false; 703 } 704 long fileSize = file.getSize(); 705 long destLength = dest.length(); 706 if (fileSize != destLength) { 707 Log.e(LOG_TAG, "Length doesn't match. Expected " + fileSize 708 + " got " + destLength); 709 if (deleteInvalid) { 710 dest.delete(); 711 return false; 712 } 713 } 714 FileInputStream is = new FileInputStream(dest); 715 try { 716 for(Config.File.Part part : file.mParts) { 717 if (part.md5 == null) { 718 continue; 719 } 720 MessageDigest digest = createDigest(); 721 readIntoDigest(is, part.size, digest); 722 String hash = getHash(digest); 723 if (!hash.equalsIgnoreCase(part.md5)) { 724 Log.e(LOG_TAG, "MD5 checksums don't match. " + 725 part.src + " Expected " 726 + part.md5 + " got " + hash); 727 if (deleteInvalid) { 728 quietClose(is); 729 dest.delete(); 730 } 731 return false; 732 } 733 } 734 } finally { 735 quietClose(is); 736 } 737 return true; 738 } 739 readIntoDigest(FileInputStream is, long bytesToRead, MessageDigest digest)740 private void readIntoDigest(FileInputStream is, long bytesToRead, 741 MessageDigest digest) throws IOException { 742 while(bytesToRead > 0) { 743 int chunkSize = (int) Math.min(mFileIOBuffer.length, 744 bytesToRead); 745 int bytesRead = is.read(mFileIOBuffer, 0, chunkSize); 746 if (bytesRead < 0) { 747 break; 748 } 749 updateDigest(digest, bytesRead); 750 bytesToRead -= bytesRead; 751 } 752 } 753 createDigest()754 private MessageDigest createDigest() throws DownloaderException { 755 MessageDigest digest; 756 try { 757 digest = MessageDigest.getInstance("MD5"); 758 } catch (NoSuchAlgorithmException e) { 759 throw new DownloaderException("Couldn't create MD5 digest"); 760 } 761 return digest; 762 } 763 updateDigest(MessageDigest digest, int bytesRead)764 private void updateDigest(MessageDigest digest, int bytesRead) { 765 if (bytesRead == mFileIOBuffer.length) { 766 digest.update(mFileIOBuffer); 767 } else { 768 // Work around an awkward API: Create a 769 // new buffer with just the valid bytes 770 byte[] temp = new byte[bytesRead]; 771 System.arraycopy(mFileIOBuffer, 0, 772 temp, 0, bytesRead); 773 digest.update(temp); 774 } 775 } 776 getHash(MessageDigest digest)777 private String getHash(MessageDigest digest) { 778 StringBuilder builder = new StringBuilder(); 779 for(byte b : digest.digest()) { 780 builder.append(Integer.toHexString((b >> 4) & 0xf)); 781 builder.append(Integer.toHexString(b & 0xf)); 782 } 783 return builder.toString(); 784 } 785 786 787 /** 788 * Ensure we have sizes for all the items. 789 * @param config 790 * @throws ClientProtocolException 791 * @throws IOException 792 * @throws DownloaderException 793 */ getSizes(Config config)794 private void getSizes(Config config) 795 throws ClientProtocolException, IOException, DownloaderException { 796 for (Config.File file : config.mFiles) { 797 for(Config.File.Part part : file.mParts) { 798 if (part.size < 0) { 799 part.size = getSize(part.src); 800 } 801 } 802 } 803 mTotalExpectedSize = config.getSize(); 804 } 805 getSize(String url)806 private long getSize(String url) throws ClientProtocolException, 807 IOException { 808 url = normalizeUrl(url); 809 Log.i(LOG_TAG, "Head " + url); 810 HttpHead httpGet = new HttpHead(url); 811 HttpResponse response = mHttpClient.execute(httpGet); 812 if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { 813 throw new IOException("Unexpected Http status code " 814 + response.getStatusLine().getStatusCode()); 815 } 816 Header[] clHeaders = response.getHeaders("Content-Length"); 817 if (clHeaders.length > 0) { 818 Header header = clHeaders[0]; 819 return Long.parseLong(header.getValue()); 820 } 821 return -1; 822 } 823 normalizeUrl(String url)824 private String normalizeUrl(String url) throws MalformedURLException { 825 return (new URL(new URL(mFileConfigUrl), url)).toString(); 826 } 827 get(String url, long startOffset, long expectedLength)828 private InputStream get(String url, long startOffset, 829 long expectedLength) 830 throws ClientProtocolException, IOException { 831 url = normalizeUrl(url); 832 Log.i(LOG_TAG, "Get " + url); 833 834 mHttpGet = new HttpGet(url); 835 int expectedStatusCode = HttpStatus.SC_OK; 836 if (startOffset > 0) { 837 String range = "bytes=" + startOffset + "-"; 838 if (expectedLength >= 0) { 839 range += expectedLength-1; 840 } 841 Log.i(LOG_TAG, "requesting byte range " + range); 842 mHttpGet.addHeader("Range", range); 843 expectedStatusCode = HttpStatus.SC_PARTIAL_CONTENT; 844 } 845 HttpResponse response = mHttpClient.execute(mHttpGet); 846 long bytesToSkip = 0; 847 int statusCode = response.getStatusLine().getStatusCode(); 848 if (statusCode != expectedStatusCode) { 849 if ((statusCode == HttpStatus.SC_OK) 850 && (expectedStatusCode 851 == HttpStatus.SC_PARTIAL_CONTENT)) { 852 Log.i(LOG_TAG, "Byte range request ignored"); 853 bytesToSkip = startOffset; 854 } else { 855 throw new IOException("Unexpected Http status code " 856 + statusCode + " expected " 857 + expectedStatusCode); 858 } 859 } 860 HttpEntity entity = response.getEntity(); 861 InputStream is = entity.getContent(); 862 if (bytesToSkip > 0) { 863 is.skip(bytesToSkip); 864 } 865 return is; 866 } 867 download(String src, String dest)868 private File download(String src, String dest) 869 throws DownloaderException, ClientProtocolException, IOException { 870 File destFile = new File(mDataDir, dest); 871 FileOutputStream os = openOutput(dest, false); 872 try { 873 downloadPart(src, os, 0, -1, null); 874 } finally { 875 os.close(); 876 } 877 return destFile; 878 } 879 downloadPart(String src, FileOutputStream os, long startOffset, long expectedLength, MessageDigest digest)880 private void downloadPart(String src, FileOutputStream os, 881 long startOffset, long expectedLength, MessageDigest digest) 882 throws ClientProtocolException, IOException, DownloaderException { 883 boolean lengthIsKnown = expectedLength >= 0; 884 if (startOffset < 0) { 885 throw new IllegalArgumentException("Negative startOffset:" 886 + startOffset); 887 } 888 if (lengthIsKnown && (startOffset > expectedLength)) { 889 throw new IllegalArgumentException( 890 "startOffset > expectedLength" + startOffset + " " 891 + expectedLength); 892 } 893 InputStream is = get(src, startOffset, expectedLength); 894 try { 895 long bytesRead = downloadStream(is, os, digest); 896 if (lengthIsKnown) { 897 long expectedBytesRead = expectedLength - startOffset; 898 if (expectedBytesRead != bytesRead) { 899 Log.e(LOG_TAG, "Bad file transfer from server: " + src 900 + " Expected " + expectedBytesRead 901 + " Received " + bytesRead); 902 throw new DownloaderException( 903 "Incorrect number of bytes received from server"); 904 } 905 } 906 } finally { 907 is.close(); 908 mHttpGet = null; 909 } 910 } 911 openOutput(String dest, boolean append)912 private FileOutputStream openOutput(String dest, boolean append) 913 throws FileNotFoundException, DownloaderException { 914 File destFile = new File(mDataDir, dest); 915 File parent = destFile.getParentFile(); 916 if (! parent.exists()) { 917 parent.mkdirs(); 918 } 919 if (! parent.exists()) { 920 throw new DownloaderException("Could not create directory " 921 + parent.toString()); 922 } 923 FileOutputStream os = new FileOutputStream(destFile, append); 924 return os; 925 } 926 openInput(String src)927 private FileInputStream openInput(String src) 928 throws FileNotFoundException, DownloaderException { 929 File srcFile = new File(mDataDir, src); 930 File parent = srcFile.getParentFile(); 931 if (! parent.exists()) { 932 parent.mkdirs(); 933 } 934 if (! parent.exists()) { 935 throw new DownloaderException("Could not create directory " 936 + parent.toString()); 937 } 938 return new FileInputStream(srcFile); 939 } 940 downloadStream(InputStream is, FileOutputStream os, MessageDigest digest)941 private long downloadStream(InputStream is, FileOutputStream os, 942 MessageDigest digest) 943 throws DownloaderException, IOException { 944 long totalBytesRead = 0; 945 while(true){ 946 if (Thread.interrupted()) { 947 Log.i(LOG_TAG, "downloader thread interrupted."); 948 mHttpGet.abort(); 949 throw new DownloaderException("Thread interrupted"); 950 } 951 int bytesRead = is.read(mFileIOBuffer); 952 if (bytesRead < 0) { 953 break; 954 } 955 if (digest != null) { 956 updateDigest(digest, bytesRead); 957 } 958 totalBytesRead += bytesRead; 959 os.write(mFileIOBuffer, 0, bytesRead); 960 mDownloadedSize += bytesRead; 961 int progress = (int) (Math.min(mTotalExpectedSize, 962 mDownloadedSize * 10000 / 963 Math.max(1, mTotalExpectedSize))); 964 if (progress != mReportedProgress) { 965 mReportedProgress = progress; 966 reportProgress(progress); 967 } 968 } 969 return totalBytesRead; 970 } 971 972 private AndroidHttpClient mHttpClient; 973 private HttpGet mHttpGet; 974 private String mFileConfigUrl; 975 private String mConfigVersion; 976 private String mDataPath; 977 private File mDataDir; 978 private String mUserAgent; 979 private long mTotalExpectedSize; 980 private long mDownloadedSize; 981 private int mReportedProgress; 982 private final static int CHUNK_SIZE = 32 * 1024; 983 byte[] mFileIOBuffer = new byte[CHUNK_SIZE]; 984 } 985 986 private final static String LOG_TAG = "Downloader"; 987 private TextView mProgress; 988 private TextView mTimeRemaining; 989 private final DecimalFormat mPercentFormat = new DecimalFormat("0.00 %"); 990 private long mStartTime; 991 private Thread mDownloadThread; 992 private boolean mSuppressErrorMessages; 993 994 private final static long MS_PER_SECOND = 1000; 995 private final static long MS_PER_MINUTE = 60 * 1000; 996 private final static long MS_PER_HOUR = 60 * 60 * 1000; 997 private final static long MS_PER_DAY = 24 * 60 * 60 * 1000; 998 999 private final static String LOCAL_CONFIG_FILE = ".downloadConfig"; 1000 private final static String LOCAL_CONFIG_FILE_TEMP = ".downloadConfig_temp"; 1001 private final static String LOCAL_FILTERED_FILE = ".downloadConfig_filtered"; 1002 private final static String EXTRA_CUSTOM_TEXT = "DownloaderActivity_custom_text"; 1003 private final static String EXTRA_FILE_CONFIG_URL = "DownloaderActivity_config_url"; 1004 private final static String EXTRA_CONFIG_VERSION = "DownloaderActivity_config_version"; 1005 private final static String EXTRA_DATA_PATH = "DownloaderActivity_data_path"; 1006 private final static String EXTRA_USER_AGENT = "DownloaderActivity_user_agent"; 1007 1008 private final static int MSG_DOWNLOAD_SUCCEEDED = 0; 1009 private final static int MSG_DOWNLOAD_FAILED = 1; 1010 private final static int MSG_REPORT_PROGRESS = 2; 1011 1012 private final Handler mHandler = new Handler() { 1013 @Override 1014 public void handleMessage(Message msg) { 1015 switch (msg.what) { 1016 case MSG_DOWNLOAD_SUCCEEDED: 1017 onDownloadSucceeded(); 1018 break; 1019 case MSG_DOWNLOAD_FAILED: 1020 onDownloadFailed((String) msg.obj); 1021 break; 1022 case MSG_REPORT_PROGRESS: 1023 onReportProgress(msg.arg1); 1024 break; 1025 default: 1026 throw new IllegalArgumentException("Unknown message id " 1027 + msg.what); 1028 } 1029 } 1030 1031 }; 1032 1033 } 1034