• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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