blob: 4b321b3ffc5149389debe1337dba95aeb85225d7 [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quake;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.channels.FileLock;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.net.http.AndroidHttpClient;
import android.util.Log;
import android.util.Xml;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
public class DownloaderActivity extends Activity {
/**
* Checks if data has been downloaded. If so, returns true. If not,
* starts an activity to download the data and returns false. If this
* function returns false the caller should immediately return from its
* onCreate method. The calling activity will later be restarted
* (using a copy of its original intent) once the data download completes.
* @param activity The calling activity.
* @param customText A text string that is displayed in the downloader UI.
* @param fileConfigUrl The URL of the download configuration URL.
* @param configVersion The version of the configuration file.
* @param dataPath The directory on the device where we want to store the
* data.
* @param userAgent The user agent string to use when fetching URLs.
* @return true if the data has already been downloaded successfully, or
* false if the data needs to be downloaded.
*/
public static boolean ensureDownloaded(Activity activity,
String customText, String fileConfigUrl,
String configVersion, String dataPath,
String userAgent) {
File dest = new File(dataPath);
if (dest.exists()) {
// Check version
if (versionMatches(dest, configVersion)) {
Log.i(LOG_TAG, "Versions match, no need to download.");
return true;
}
}
Intent intent = PreconditionActivityHelper.createPreconditionIntent(
activity, DownloaderActivity.class);
intent.putExtra(EXTRA_CUSTOM_TEXT, customText);
intent.putExtra(EXTRA_FILE_CONFIG_URL, fileConfigUrl);
intent.putExtra(EXTRA_CONFIG_VERSION, configVersion);
intent.putExtra(EXTRA_DATA_PATH, dataPath);
intent.putExtra(EXTRA_USER_AGENT, userAgent);
PreconditionActivityHelper.startPreconditionActivityAndFinish(
activity, intent);
return false;
}
/**
* Delete a directory and all its descendants.
* @param directory The directory to delete
* @return true if the directory was deleted successfully.
*/
public static boolean deleteData(String directory) {
return deleteTree(new File(directory), true);
}
private static boolean deleteTree(File base, boolean deleteBase) {
boolean result = true;
if (base.isDirectory()) {
for (File child : base.listFiles()) {
result &= deleteTree(child, true);
}
}
if (deleteBase) {
result &= base.delete();
}
return result;
}
private static boolean versionMatches(File dest, String expectedVersion) {
Config config = getLocalConfig(dest, LOCAL_CONFIG_FILE);
if (config != null) {
return config.version.equals(expectedVersion);
}
return false;
}
private static Config getLocalConfig(File destPath, String configFilename) {
File configPath = new File(destPath, configFilename);
FileInputStream is;
try {
is = new FileInputStream(configPath);
} catch (FileNotFoundException e) {
return null;
}
try {
Config config = ConfigHandler.parse(is);
return config;
} catch (Exception e) {
Log.e(LOG_TAG, "Unable to read local config file", e);
return null;
} finally {
quietClose(is);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);
setContentView(R.layout.downloader);
getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,
R.layout.downloader_title);
((TextView) findViewById(R.id.customText)).setText(
intent.getStringExtra(EXTRA_CUSTOM_TEXT));
mProgress = (TextView) findViewById(R.id.progress);
mTimeRemaining = (TextView) findViewById(R.id.time_remaining);
Button button = (Button) findViewById(R.id.cancel);
button.setOnClickListener(new Button.OnClickListener() {
public void onClick(View v) {
if (mDownloadThread != null) {
mSuppressErrorMessages = true;
mDownloadThread.interrupt();
}
}
});
startDownloadThread();
}
private void startDownloadThread() {
mSuppressErrorMessages = false;
mProgress.setText("");
mTimeRemaining.setText("");
mDownloadThread = new Thread(new Downloader(), "Downloader");
mDownloadThread.setPriority(Thread.NORM_PRIORITY - 1);
mDownloadThread.start();
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onDestroy() {
super.onDestroy();
mSuppressErrorMessages = true;
mDownloadThread.interrupt();
try {
mDownloadThread.join();
} catch (InterruptedException e) {
// Don't care.
}
}
private void onDownloadSucceeded() {
Log.i(LOG_TAG, "Download succeeded");
PreconditionActivityHelper.startOriginalActivityAndFinish(this);
}
private void onDownloadFailed(String reason) {
Log.e(LOG_TAG, "Download stopped: " + reason);
String shortReason;
int index = reason.indexOf('\n');
if (index >= 0) {
shortReason = reason.substring(0, index);
} else {
shortReason = reason;
}
AlertDialog alert = new Builder(this).create();
alert.setTitle(R.string.download_activity_download_stopped);
if (!mSuppressErrorMessages) {
alert.setMessage(shortReason);
}
alert.setButton(getString(R.string.download_activity_retry),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
startDownloadThread();
}
});
alert.setButton2(getString(R.string.download_activity_quit),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finish();
}
});
try {
alert.show();
} catch (WindowManager.BadTokenException e) {
// Happens when the Back button is used to exit the activity.
// ignore.
}
}
private void onReportProgress(int progress) {
mProgress.setText(mPercentFormat.format(progress / 10000.0));
long now = SystemClock.elapsedRealtime();
if (mStartTime == 0) {
mStartTime = now;
}
long delta = now - mStartTime;
String timeRemaining = getString(R.string.download_activity_time_remaining_unknown);
if ((delta > 3 * MS_PER_SECOND) && (progress > 100)) {
long totalTime = 10000 * delta / progress;
long timeLeft = Math.max(0L, totalTime - delta);
if (timeLeft > MS_PER_DAY) {
timeRemaining = Long.toString(
(timeLeft + MS_PER_DAY - 1) / MS_PER_DAY)
+ " "
+ getString(R.string.download_activity_time_remaining_days);
} else if (timeLeft > MS_PER_HOUR) {
timeRemaining = Long.toString(
(timeLeft + MS_PER_HOUR - 1) / MS_PER_HOUR)
+ " "
+ getString(R.string.download_activity_time_remaining_hours);
} else if (timeLeft > MS_PER_MINUTE) {
timeRemaining = Long.toString(
(timeLeft + MS_PER_MINUTE - 1) / MS_PER_MINUTE)
+ " "
+ getString(R.string.download_activity_time_remaining_minutes);
} else {
timeRemaining = Long.toString(
(timeLeft + MS_PER_SECOND - 1) / MS_PER_SECOND)
+ " "
+ getString(R.string.download_activity_time_remaining_seconds);
}
}
mTimeRemaining.setText(timeRemaining);
}
private static void quietClose(InputStream is) {
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
// Don't care.
}
}
private static void quietClose(OutputStream os) {
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
// Don't care.
}
}
private static class Config {
long getSize() {
long result = 0;
for(File file : mFiles) {
result += file.getSize();
}
return result;
}
static class File {
public File(String src, String dest, String md5, long size) {
if (src != null) {
this.mParts.add(new Part(src, md5, size));
}
this.dest = dest;
}
static class Part {
Part(String src, String md5, long size) {
this.src = src;
this.md5 = md5;
this.size = size;
}
String src;
String md5;
long size;
}
ArrayList<Part> mParts = new ArrayList<Part>();
String dest;
long getSize() {
long result = 0;
for(Part part : mParts) {
if (part.size > 0) {
result += part.size;
}
}
return result;
}
}
String version;
ArrayList<File> mFiles = new ArrayList<File>();
}
/**
* <config version="">
* <file src="http:..." dest ="b.x" />
* <file dest="b.x">
* <part src="http:..." />
* ...
* ...
* </config>
*
*/
private static class ConfigHandler extends DefaultHandler {
public static Config parse(InputStream is) throws SAXException,
UnsupportedEncodingException, IOException {
ConfigHandler handler = new ConfigHandler();
Xml.parse(is, Xml.findEncodingByName("UTF-8"), handler);
return handler.mConfig;
}
private ConfigHandler() {
mConfig = new Config();
}
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException {
if (localName.equals("config")) {
mConfig.version = getRequiredString(attributes, "version");
} else if (localName.equals("file")) {
String src = attributes.getValue("", "src");
String dest = getRequiredString(attributes, "dest");
String md5 = attributes.getValue("", "md5");
long size = getLong(attributes, "size", -1);
mConfig.mFiles.add(new Config.File(src, dest, md5, size));
} else if (localName.equals("part")) {
String src = getRequiredString(attributes, "src");
String md5 = attributes.getValue("", "md5");
long size = getLong(attributes, "size", -1);
int length = mConfig.mFiles.size();
if (length > 0) {
mConfig.mFiles.get(length-1).mParts.add(
new Config.File.Part(src, md5, size));
}
}
}
private static String getRequiredString(Attributes attributes,
String localName) throws SAXException {
String result = attributes.getValue("", localName);
if (result == null) {
throw new SAXException("Expected attribute " + localName);
}
return result;
}
private static long getLong(Attributes attributes, String localName,
long defaultValue) {
String value = attributes.getValue("", localName);
if (value == null) {
return defaultValue;
} else {
return Long.parseLong(value);
}
}
public Config mConfig;
}
private class DownloaderException extends Exception {
public DownloaderException(String reason) {
super(reason);
}
}
private class Downloader implements Runnable {
public void run() {
Intent intent = getIntent();
mFileConfigUrl = intent.getStringExtra(EXTRA_FILE_CONFIG_URL);
mConfigVersion = intent.getStringExtra(EXTRA_CONFIG_VERSION);
mDataPath = intent.getStringExtra(EXTRA_DATA_PATH);
mUserAgent = intent.getStringExtra(EXTRA_USER_AGENT);
mDataDir = new File(mDataPath);
try {
// Download files.
mHttpClient = AndroidHttpClient.newInstance(mUserAgent);
try {
Config config = getConfig();
filter(config);
persistantDownload(config);
verify(config);
cleanup();
reportSuccess();
} finally {
mHttpClient.close();
}
} catch (Exception e) {
reportFailure(e.toString() + "\n" + Log.getStackTraceString(e));
}
}
private void persistantDownload(Config config)
throws ClientProtocolException, DownloaderException, IOException {
while(true) {
try {
download(config);
break;
} catch(java.net.SocketException e) {
if (mSuppressErrorMessages) {
throw e;
}
} catch(java.net.SocketTimeoutException e) {
if (mSuppressErrorMessages) {
throw e;
}
}
Log.i(LOG_TAG, "Network connectivity issue, retrying.");
}
}
private void filter(Config config)
throws IOException, DownloaderException {
File filteredFile = new File(mDataDir, LOCAL_FILTERED_FILE);
if (filteredFile.exists()) {
return;
}
File localConfigFile = new File(mDataDir, LOCAL_CONFIG_FILE_TEMP);
HashSet<String> keepSet = new HashSet<String>();
keepSet.add(localConfigFile.getCanonicalPath());
HashMap<String, Config.File> fileMap =
new HashMap<String, Config.File>();
for(Config.File file : config.mFiles) {
String canonicalPath =
new File(mDataDir, file.dest).getCanonicalPath();
fileMap.put(canonicalPath, file);
}
recursiveFilter(mDataDir, fileMap, keepSet, false);
touch(filteredFile);
}
private void touch(File file) throws FileNotFoundException {
FileOutputStream os = new FileOutputStream(file);
quietClose(os);
}
private boolean recursiveFilter(File base,
HashMap<String, Config.File> fileMap,
HashSet<String> keepSet, boolean filterBase)
throws IOException, DownloaderException {
boolean result = true;
if (base.isDirectory()) {
for (File child : base.listFiles()) {
result &= recursiveFilter(child, fileMap, keepSet, true);
}
}
if (filterBase) {
if (base.isDirectory()) {
if (base.listFiles().length == 0) {
result &= base.delete();
}
} else {
if (!shouldKeepFile(base, fileMap, keepSet)) {
result &= base.delete();
}
}
}
return result;
}
private boolean shouldKeepFile(File file,
HashMap<String, Config.File> fileMap,
HashSet<String> keepSet)
throws IOException, DownloaderException {
String canonicalPath = file.getCanonicalPath();
if (keepSet.contains(canonicalPath)) {
return true;
}
Config.File configFile = fileMap.get(canonicalPath);
if (configFile == null) {
return false;
}
return verifyFile(configFile, false);
}
private void reportSuccess() {
mHandler.sendMessage(
Message.obtain(mHandler, MSG_DOWNLOAD_SUCCEEDED));
}
private void reportFailure(String reason) {
mHandler.sendMessage(
Message.obtain(mHandler, MSG_DOWNLOAD_FAILED, reason));
}
private void reportProgress(int progress) {
mHandler.sendMessage(
Message.obtain(mHandler, MSG_REPORT_PROGRESS, progress, 0));
}
private Config getConfig() throws DownloaderException,
ClientProtocolException, IOException, SAXException {
Config config = null;
if (mDataDir.exists()) {
config = getLocalConfig(mDataDir, LOCAL_CONFIG_FILE_TEMP);
if ((config == null)
|| !mConfigVersion.equals(config.version)) {
if (config == null) {
Log.i(LOG_TAG, "Couldn't find local config.");
} else {
Log.i(LOG_TAG, "Local version out of sync. Wanted " +
mConfigVersion + " but have " + config.version);
}
config = null;
}
} else {
Log.i(LOG_TAG, "Creating directory " + mDataPath);
mDataDir.mkdirs();
mDataDir.mkdir();
if (!mDataDir.exists()) {
throw new DownloaderException(
"Could not create the directory " + mDataPath);
}
}
if (config == null) {
File localConfig = download(mFileConfigUrl,
LOCAL_CONFIG_FILE_TEMP);
InputStream is = new FileInputStream(localConfig);
try {
config = ConfigHandler.parse(is);
} finally {
quietClose(is);
}
if (! config.version.equals(mConfigVersion)) {
throw new DownloaderException(
"Configuration file version mismatch. Expected " +
mConfigVersion + " received " +
config.version);
}
}
return config;
}
private void noisyDelete(File file) throws IOException {
if (! file.delete() ) {
throw new IOException("could not delete " + file);
}
}
private void download(Config config) throws DownloaderException,
ClientProtocolException, IOException {
mDownloadedSize = 0;
getSizes(config);
Log.i(LOG_TAG, "Total bytes to download: "
+ mTotalExpectedSize);
for(Config.File file : config.mFiles) {
downloadFile(file);
}
}
private void downloadFile(Config.File file) throws DownloaderException,
FileNotFoundException, IOException, ClientProtocolException {
boolean append = false;
File dest = new File(mDataDir, file.dest);
long bytesToSkip = 0;
if (dest.exists() && dest.isFile()) {
append = true;
bytesToSkip = dest.length();
mDownloadedSize += bytesToSkip;
}
FileOutputStream os = null;
long offsetOfCurrentPart = 0;
try {
for(Config.File.Part part : file.mParts) {
// The part.size==0 check below allows us to download
// zero-length files.
if ((part.size > bytesToSkip) || (part.size == 0)) {
MessageDigest digest = null;
if (part.md5 != null) {
digest = createDigest();
if (bytesToSkip > 0) {
FileInputStream is = openInput(file.dest);
try {
is.skip(offsetOfCurrentPart);
readIntoDigest(is, bytesToSkip, digest);
} finally {
quietClose(is);
}
}
}
if (os == null) {
os = openOutput(file.dest, append);
}
downloadPart(part.src, os, bytesToSkip,
part.size, digest);
if (digest != null) {
String hash = getHash(digest);
if (!hash.equalsIgnoreCase(part.md5)) {
Log.e(LOG_TAG, "web MD5 checksums don't match. "
+ part.src + "\nExpected "
+ part.md5 + "\n got " + hash);
quietClose(os);
dest.delete();
throw new DownloaderException(
"Received bad data from web server");
} else {
Log.i(LOG_TAG, "web MD5 checksum matches.");
}
}
}
bytesToSkip -= Math.min(bytesToSkip, part.size);
offsetOfCurrentPart += part.size;
}
} finally {
quietClose(os);
}
}
private void cleanup() throws IOException {
File filtered = new File(mDataDir, LOCAL_FILTERED_FILE);
noisyDelete(filtered);
File tempConfig = new File(mDataDir, LOCAL_CONFIG_FILE_TEMP);
File realConfig = new File(mDataDir, LOCAL_CONFIG_FILE);
tempConfig.renameTo(realConfig);
}
private void verify(Config config) throws DownloaderException,
ClientProtocolException, IOException {
Log.i(LOG_TAG, "Verifying...");
String failFiles = null;
for(Config.File file : config.mFiles) {
if (! verifyFile(file, true) ) {
if (failFiles == null) {
failFiles = file.dest;
} else {
failFiles += " " + file.dest;
}
}
}
if (failFiles != null) {
throw new DownloaderException(
"Possible bad SD-Card. MD5 sum incorrect for file(s) "
+ failFiles);
}
}
private boolean verifyFile(Config.File file, boolean deleteInvalid)
throws FileNotFoundException, DownloaderException, IOException {
Log.i(LOG_TAG, "verifying " + file.dest);
File dest = new File(mDataDir, file.dest);
if (! dest.exists()) {
Log.e(LOG_TAG, "File does not exist: " + dest.toString());
return false;
}
long fileSize = file.getSize();
long destLength = dest.length();
if (fileSize != destLength) {
Log.e(LOG_TAG, "Length doesn't match. Expected " + fileSize
+ " got " + destLength);
if (deleteInvalid) {
dest.delete();
return false;
}
}
FileInputStream is = new FileInputStream(dest);
try {
for(Config.File.Part part : file.mParts) {
if (part.md5 == null) {
continue;
}
MessageDigest digest = createDigest();
readIntoDigest(is, part.size, digest);
String hash = getHash(digest);
if (!hash.equalsIgnoreCase(part.md5)) {
Log.e(LOG_TAG, "MD5 checksums don't match. " +
part.src + " Expected "
+ part.md5 + " got " + hash);
if (deleteInvalid) {
quietClose(is);
dest.delete();
}
return false;
}
}
} finally {
quietClose(is);
}
return true;
}
private void readIntoDigest(FileInputStream is, long bytesToRead,
MessageDigest digest) throws IOException {
while(bytesToRead > 0) {
int chunkSize = (int) Math.min(mFileIOBuffer.length,
bytesToRead);
int bytesRead = is.read(mFileIOBuffer, 0, chunkSize);
if (bytesRead < 0) {
break;
}
updateDigest(digest, bytesRead);
bytesToRead -= bytesRead;
}
}
private MessageDigest createDigest() throws DownloaderException {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new DownloaderException("Couldn't create MD5 digest");
}
return digest;
}
private void updateDigest(MessageDigest digest, int bytesRead) {
if (bytesRead == mFileIOBuffer.length) {
digest.update(mFileIOBuffer);
} else {
// Work around an awkward API: Create a
// new buffer with just the valid bytes
byte[] temp = new byte[bytesRead];
System.arraycopy(mFileIOBuffer, 0,
temp, 0, bytesRead);
digest.update(temp);
}
}
private String getHash(MessageDigest digest) {
StringBuilder builder = new StringBuilder();
for(byte b : digest.digest()) {
builder.append(Integer.toHexString((b >> 4) & 0xf));
builder.append(Integer.toHexString(b & 0xf));
}
return builder.toString();
}
/**
* Ensure we have sizes for all the items.
* @param config
* @throws ClientProtocolException
* @throws IOException
* @throws DownloaderException
*/
private void getSizes(Config config)
throws ClientProtocolException, IOException, DownloaderException {
for (Config.File file : config.mFiles) {
for(Config.File.Part part : file.mParts) {
if (part.size < 0) {
part.size = getSize(part.src);
}
}
}
mTotalExpectedSize = config.getSize();
}
private long getSize(String url) throws ClientProtocolException,
IOException {
url = normalizeUrl(url);
Log.i(LOG_TAG, "Head " + url);
HttpHead httpGet = new HttpHead(url);
HttpResponse response = mHttpClient.execute(httpGet);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
throw new IOException("Unexpected Http status code "
+ response.getStatusLine().getStatusCode());
}
Header[] clHeaders = response.getHeaders("Content-Length");
if (clHeaders.length > 0) {
Header header = clHeaders[0];
return Long.parseLong(header.getValue());
}
return -1;
}
private String normalizeUrl(String url) throws MalformedURLException {
return (new URL(new URL(mFileConfigUrl), url)).toString();
}
private InputStream get(String url, long startOffset,
long expectedLength)
throws ClientProtocolException, IOException {
url = normalizeUrl(url);
Log.i(LOG_TAG, "Get " + url);
mHttpGet = new HttpGet(url);
int expectedStatusCode = HttpStatus.SC_OK;
if (startOffset > 0) {
String range = "bytes=" + startOffset + "-";
if (expectedLength >= 0) {
range += expectedLength-1;
}
Log.i(LOG_TAG, "requesting byte range " + range);
mHttpGet.addHeader("Range", range);
expectedStatusCode = HttpStatus.SC_PARTIAL_CONTENT;
}
HttpResponse response = mHttpClient.execute(mHttpGet);
long bytesToSkip = 0;
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != expectedStatusCode) {
if ((statusCode == HttpStatus.SC_OK)
&& (expectedStatusCode
== HttpStatus.SC_PARTIAL_CONTENT)) {
Log.i(LOG_TAG, "Byte range request ignored");
bytesToSkip = startOffset;
} else {
throw new IOException("Unexpected Http status code "
+ statusCode + " expected "
+ expectedStatusCode);
}
}
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
if (bytesToSkip > 0) {
is.skip(bytesToSkip);
}
return is;
}
private File download(String src, String dest)
throws DownloaderException, ClientProtocolException, IOException {
File destFile = new File(mDataDir, dest);
FileOutputStream os = openOutput(dest, false);
try {
downloadPart(src, os, 0, -1, null);
} finally {
os.close();
}
return destFile;
}
private void downloadPart(String src, FileOutputStream os,
long startOffset, long expectedLength, MessageDigest digest)
throws ClientProtocolException, IOException, DownloaderException {
boolean lengthIsKnown = expectedLength >= 0;
if (startOffset < 0) {
throw new IllegalArgumentException("Negative startOffset:"
+ startOffset);
}
if (lengthIsKnown && (startOffset > expectedLength)) {
throw new IllegalArgumentException(
"startOffset > expectedLength" + startOffset + " "
+ expectedLength);
}
InputStream is = get(src, startOffset, expectedLength);
try {
long bytesRead = downloadStream(is, os, digest);
if (lengthIsKnown) {
long expectedBytesRead = expectedLength - startOffset;
if (expectedBytesRead != bytesRead) {
Log.e(LOG_TAG, "Bad file transfer from server: " + src
+ " Expected " + expectedBytesRead
+ " Received " + bytesRead);
throw new DownloaderException(
"Incorrect number of bytes received from server");
}
}
} finally {
is.close();
mHttpGet = null;
}
}
private FileOutputStream openOutput(String dest, boolean append)
throws FileNotFoundException, DownloaderException {
File destFile = new File(mDataDir, dest);
File parent = destFile.getParentFile();
if (! parent.exists()) {
parent.mkdirs();
}
if (! parent.exists()) {
throw new DownloaderException("Could not create directory "
+ parent.toString());
}
FileOutputStream os = new FileOutputStream(destFile, append);
return os;
}
private FileInputStream openInput(String src)
throws FileNotFoundException, DownloaderException {
File srcFile = new File(mDataDir, src);
File parent = srcFile.getParentFile();
if (! parent.exists()) {
parent.mkdirs();
}
if (! parent.exists()) {
throw new DownloaderException("Could not create directory "
+ parent.toString());
}
return new FileInputStream(srcFile);
}
private long downloadStream(InputStream is, FileOutputStream os,
MessageDigest digest)
throws DownloaderException, IOException {
long totalBytesRead = 0;
while(true){
if (Thread.interrupted()) {
Log.i(LOG_TAG, "downloader thread interrupted.");
mHttpGet.abort();
throw new DownloaderException("Thread interrupted");
}
int bytesRead = is.read(mFileIOBuffer);
if (bytesRead < 0) {
break;
}
if (digest != null) {
updateDigest(digest, bytesRead);
}
totalBytesRead += bytesRead;
os.write(mFileIOBuffer, 0, bytesRead);
mDownloadedSize += bytesRead;
int progress = (int) (Math.min(mTotalExpectedSize,
mDownloadedSize * 10000 /
Math.max(1, mTotalExpectedSize)));
if (progress != mReportedProgress) {
mReportedProgress = progress;
reportProgress(progress);
}
}
return totalBytesRead;
}
private AndroidHttpClient mHttpClient;
private HttpGet mHttpGet;
private String mFileConfigUrl;
private String mConfigVersion;
private String mDataPath;
private File mDataDir;
private String mUserAgent;
private long mTotalExpectedSize;
private long mDownloadedSize;
private int mReportedProgress;
private final static int CHUNK_SIZE = 32 * 1024;
byte[] mFileIOBuffer = new byte[CHUNK_SIZE];
}
private final static String LOG_TAG = "Downloader";
private TextView mProgress;
private TextView mTimeRemaining;
private final DecimalFormat mPercentFormat = new DecimalFormat("0.00 %");
private long mStartTime;
private Thread mDownloadThread;
private boolean mSuppressErrorMessages;
private final static long MS_PER_SECOND = 1000;
private final static long MS_PER_MINUTE = 60 * 1000;
private final static long MS_PER_HOUR = 60 * 60 * 1000;
private final static long MS_PER_DAY = 24 * 60 * 60 * 1000;
private final static String LOCAL_CONFIG_FILE = ".downloadConfig";
private final static String LOCAL_CONFIG_FILE_TEMP = ".downloadConfig_temp";
private final static String LOCAL_FILTERED_FILE = ".downloadConfig_filtered";
private final static String EXTRA_CUSTOM_TEXT = "DownloaderActivity_custom_text";
private final static String EXTRA_FILE_CONFIG_URL = "DownloaderActivity_config_url";
private final static String EXTRA_CONFIG_VERSION = "DownloaderActivity_config_version";
private final static String EXTRA_DATA_PATH = "DownloaderActivity_data_path";
private final static String EXTRA_USER_AGENT = "DownloaderActivity_user_agent";
private final static int MSG_DOWNLOAD_SUCCEEDED = 0;
private final static int MSG_DOWNLOAD_FAILED = 1;
private final static int MSG_REPORT_PROGRESS = 2;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DOWNLOAD_SUCCEEDED:
onDownloadSucceeded();
break;
case MSG_DOWNLOAD_FAILED:
onDownloadFailed((String) msg.obj);
break;
case MSG_REPORT_PROGRESS:
onReportProgress(msg.arg1);
break;
default:
throw new IllegalArgumentException("Unknown message id "
+ msg.what);
}
}
};
}