/* * 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 mParts = new ArrayList(); String dest; long getSize() { long result = 0; for(Part part : mParts) { if (part.size > 0) { result += part.size; } } return result; } } String version; ArrayList mFiles = new ArrayList(); } /** * * * * * ... * ... * * */ 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 keepSet = new HashSet(); keepSet.add(localConfigFile.getCanonicalPath()); HashMap fileMap = new HashMap(); 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 fileMap, HashSet 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 fileMap, HashSet 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); } } }; }