diff --git a/.gitignore b/.gitignore index 22273c3f4..9708725fd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,6 @@ /build.xml *~ /.idea/ -/*.iml +*.iml out /.settings/ diff --git a/src/org/fdroid/fdroid/ApkDownloader.java b/src/org/fdroid/fdroid/ApkDownloader.java index 0e99249b6..bef04e314 100644 --- a/src/org/fdroid/fdroid/ApkDownloader.java +++ b/src/org/fdroid/fdroid/ApkDownloader.java @@ -20,37 +20,29 @@ package org.fdroid.fdroid; -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; +import java.io.*; import android.util.Log; import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.net.HttpDownloader; public class ApkDownloader extends Thread { + public static final int EVENT_APK_DOWNLOAD_COMPLETE = 100; + public static final int EVENT_ERROR_HASH_MISMATCH = 101; + public static final int EVENT_ERROR_DOWNLOAD_FAILED = 102; + public static final int EVENT_ERROR_UNKNOWN = 103; private Apk curapk; private String repoaddress; - private String filename; private File destdir; private File localfile; - public static enum Status { - STARTING, RUNNING, ERROR, DONE, CANCELLED - } + private ProgressListener listener; - public static enum Error { - CORRUPT, UNKNOWN + public void setProgressListener(ProgressListener listener) { + this.listener = listener; } - private Status status = Status.STARTING; - private Error error; - private int progress; - private int max; - private String errorMessage; - // Constructor - creates a Downloader to download the given Apk, // which must have its detail populated. ApkDownloader(Apk apk, String repoaddress, File destdir) { @@ -59,50 +51,19 @@ public class ApkDownloader extends Thread { this.destdir = destdir; } - public synchronized Status getStatus() { - return status; - } - - // Current progress and maximum value for progress dialog - public synchronized int getProgress() { - return progress; - } - - public synchronized int getMax() { - return max; - } - - // Error code and error message, only valid if status is ERROR - public synchronized Error getErrorType() { - return error; - } - - public synchronized String getErrorMessage() { - return errorMessage; - } - - // The URL being downloaded or path to a cached file - public synchronized String remoteFile() { - return filename; - } - // The downloaded APK. Valid only when getStatus() has returned STATUS.DONE. public File localFile() { return localfile; } - // The APK being downloaded - public synchronized Apk getApk() { - return curapk; + public String remoteFile() { + return repoaddress + "/" + curapk.apkName.replace(" ", "%20"); } @Override public void run() { + localfile = new File(destdir, curapk.apkName); - InputStream input = null; - OutputStream output = null; - String apkname = curapk.apkName; - localfile = new File(destdir, apkname); try { // See if we already have this apk cached... @@ -111,12 +72,7 @@ public class ApkDownloader extends Thread { Hasher hash = new Hasher(curapk.hashType, localfile); if (hash.match(curapk.hash)) { Log.d("FDroid", "Using cached apk at " + localfile); - synchronized (this) { - progress = 1; - max = 1; - status = Status.DONE; - return; - } + return; } else { Log.d("FDroid", "Not using cached apk at " + localfile); localfile.delete(); @@ -124,72 +80,45 @@ public class ApkDownloader extends Thread { } // If we haven't got the apk locally, we'll have to download it... - String remotefile; - remotefile = repoaddress + "/" + apkname.replace(" ", "%20"); - Log.d("FDroid", "Downloading apk from " + remotefile); - synchronized (this) { - filename = remotefile; - progress = 0; - max = curapk.size; - status = Status.RUNNING; - } - input = new URL(remotefile).openStream(); - output = new FileOutputStream(localfile); - byte data[] = new byte[Utils.BUFFER_SIZE]; - while (true) { - if (isInterrupted()) { - Log.d("FDroid", "Download cancelled!"); - break; - } - int count = input.read(data); - if (count == -1) { - break; - } - output.write(data, 0, count); - synchronized (this) { - progress += count; - } - } + HttpDownloader downloader = new HttpDownloader(remoteFile(), localfile); + downloader.setProgressListener(listener); - if (isInterrupted()) { - localfile.delete(); - synchronized (this) { - status = Status.CANCELLED; - } + Log.d("FDroid", "Downloading apk from " + remoteFile()); + int httpStatus = downloader.downloadHttpFile(); + + if (httpStatus != 200 || !localfile.exists()) { + sendProgress(EVENT_ERROR_DOWNLOAD_FAILED); return; } + Hasher hash = new Hasher(curapk.hashType, localfile); if (!hash.match(curapk.hash)) { - synchronized (this) { - Log.d("FDroid", "Downloaded file hash of " + hash.getHash() - + " did not match repo's " + curapk.hash); - // No point keeping a bad file, whether we're - // caching or not. - localfile.delete(); - error = Error.CORRUPT; - errorMessage = null; - status = Status.ERROR; - return; - } + Log.d("FDroid", "Downloaded file hash of " + hash.getHash() + + " did not match repo's " + curapk.hash); + // No point keeping a bad file, whether we're + // caching or not. + localfile.delete(); + sendProgress(EVENT_ERROR_HASH_MISMATCH); + return; } } catch (Exception e) { Log.e("FDroid", "Download failed:\n" + Log.getStackTraceString(e)); - synchronized (this) { + if (localfile.exists()) { localfile.delete(); - error = Error.UNKNOWN; - errorMessage = e.toString(); - status = Status.ERROR; - return; } - } finally { - Utils.closeQuietly(output); - Utils.closeQuietly(input); + sendProgress(EVENT_ERROR_UNKNOWN); + return; } Log.d("FDroid", "Download finished: " + localfile); - synchronized (this) { - status = Status.DONE; + sendProgress(EVENT_APK_DOWNLOAD_COMPLETE); + } + + private void sendProgress(int type) { + if (listener != null) { + listener.onProgress(new ProgressListener.Event(type)); } } + } diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 7638242b5..63af2b9d7 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -26,6 +26,7 @@ import org.fdroid.fdroid.data.*; import org.fdroid.fdroid.installer.Installer; import org.fdroid.fdroid.installer.Installer.AndroidNotCompatibleException; import org.fdroid.fdroid.installer.Installer.InstallerCallback; +import org.fdroid.fdroid.net.Downloader; import org.xml.sax.XMLReader; import android.app.AlertDialog; @@ -394,10 +395,6 @@ public class AppDetails extends ListActivity { updateViews(); MenuManager.create(this).invalidateOptionsMenu(); - - if (downloadHandler != null) { - downloadHandler.startUpdates(); - } } @Override @@ -405,9 +402,6 @@ public class AppDetails extends ListActivity { if (myAppObserver != null) { getContentResolver().unregisterContentObserver(myAppObserver); } - if (downloadHandler != null) { - downloadHandler.stopUpdates(); - } if (app != null && (app.ignoreAllUpdates != startingIgnoreAll || app.ignoreThisUpdate != startingIgnoreThis)) { setIgnoreUpdates(app.id, app.ignoreAllUpdates, app.ignoreThisUpdate); @@ -1039,7 +1033,7 @@ public class AppDetails extends ListActivity { shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_SUBJECT, app.name); - shareIntent.putExtra(Intent.EXTRA_TEXT, app.name+" ("+app.summary+") - https://f-droid.org/app/"+app.id); + shareIntent.putExtra(Intent.EXTRA_TEXT, app.name + " (" + app.summary + ") - https://f-droid.org/app/" + app.id); startActivity(Intent.createChooser(shareIntent, getString(R.string.menu_share))); } @@ -1071,81 +1065,61 @@ public class AppDetails extends ListActivity { } // Handler used to update the progress dialog while downloading. - private class DownloadHandler extends Handler { + private class DownloadHandler extends Handler implements ProgressListener { private ApkDownloader download; private ProgressDialog pd; - private boolean updating; private String id; public DownloadHandler(Apk apk, String repoaddress, File destdir) { id = apk.id; download = new ApkDownloader(apk, repoaddress, destdir); download.start(); - startUpdates(); } public DownloadHandler(DownloadHandler oldHandler) { if (oldHandler != null) { download = oldHandler.download; } - startUpdates(); } - public boolean updateProgress() { + public void onProgress(ProgressListener.Event event) { boolean finished = false; - switch (download.getStatus()) { - case RUNNING: - if (pd == null) { - pd = createProgressDialog(download.remoteFile(), - download.getProgress(), download.getMax()); - } else { - pd.setProgress(download.getProgress()); - } - break; - case ERROR: - if (pd != null) - pd.dismiss(); - String text; - if (download.getErrorType() == ApkDownloader.Error.CORRUPT) - text = getString(R.string.corrupt_download); - else - text = download.getErrorMessage(); - Toast.makeText(AppDetails.this, text, Toast.LENGTH_LONG).show(); - finished = true; - break; - case DONE: - if (pd != null) - pd.dismiss(); - installApk(download.localFile(), id); - finished = true; - break; - case CANCELLED: - Toast.makeText(AppDetails.this, - getString(R.string.download_cancelled), - Toast.LENGTH_SHORT).show(); - finished = true; - break; - default: - break; - } - return finished; - } + switch (event.type) { + case Downloader.EVENT_PROGRESS: + if (pd == null) { + pd = createProgressDialog(download.remoteFile(), + event.progress, event.total); + } else { + pd.setProgress(event.progress); + } + break; - public void startUpdates() { - if (!updating) { - updating = true; - sendEmptyMessage(0); - } - } + case ApkDownloader.EVENT_ERROR_DOWNLOAD_FAILED: + case ApkDownloader.EVENT_ERROR_HASH_MISMATCH: + case ApkDownloader.EVENT_ERROR_UNKNOWN: + String text; + if (event.type == ApkDownloader.EVENT_ERROR_HASH_MISMATCH) + text = getString(R.string.corrupt_download); + else + text = getString(R.string.details_notinstalled); + Toast.makeText(AppDetails.this, text, Toast.LENGTH_LONG).show(); + finished = true; + break; - public void stopUpdates() { - updating = false; - removeMessages(0); + case ApkDownloader.EVENT_APK_DOWNLOAD_COMPLETE: + installApk(download.localFile(), id); + finished = true; + break; + + } + + if (finished) { + destroy(); + } } public void cancel() { - if (download != null) - download.interrupt(); + // TODO: Re-implement... } public void destroy() { @@ -1155,24 +1129,10 @@ public class AppDetails extends ListActivity { pd.dismiss(); pd = null; } - // Cancel any scheduled updates so that we don't - // accidentally recreate the progress dialog. - stopUpdates(); - } - - // Repeatedly run updateProgress() until it's finished. - @Override - public void handleMessage(Message msg) { - if (download == null) - return; - boolean finished = updateProgress(); - if (finished) - download = null; - else - sendMessageDelayed(obtainMessage(), 50); } } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // handle cases for install manager first diff --git a/src/org/fdroid/fdroid/net/Downloader.java b/src/org/fdroid/fdroid/net/Downloader.java index 922256c3f..304e3640b 100644 --- a/src/org/fdroid/fdroid/net/Downloader.java +++ b/src/org/fdroid/fdroid/net/Downloader.java @@ -10,15 +10,28 @@ public abstract class Downloader { private OutputStream outputStream; private ProgressListener progressListener = null; private ProgressListener.Event progressEvent = null; - private final File outputFile; + private File outputFile; + + public static final int EVENT_PROGRESS = 1; public abstract InputStream inputStream() throws IOException; // The context is required for opening the file to write to. public Downloader(String destFile, Context ctx) throws FileNotFoundException, MalformedURLException { - outputStream = ctx.openFileOutput(destFile, Context.MODE_PRIVATE); - outputFile = new File(ctx.getFilesDir() + File.separator + destFile); + this(new File(ctx.getFilesDir() + File.separator + destFile)); + } + + // The context is required for opening the file to write to. + public Downloader(Context ctx) throws IOException { + this(File.createTempFile("dl-", "", ctx.getCacheDir())); + } + + public Downloader(File destFile) + throws FileNotFoundException, MalformedURLException { + // http://developer.android.com/guide/topics/data/data-storage.html#InternalCache + outputFile = destFile; + outputStream = new FileOutputStream(outputFile); } /** @@ -26,10 +39,7 @@ public abstract class Downloader { * you are done*. * @see org.fdroid.fdroid.net.Downloader#getFile() */ - public Downloader(Context ctx) throws IOException { - // http://developer.android.com/guide/topics/data/data-storage.html#InternalCache - outputFile = File.createTempFile("dl-", "", ctx.getCacheDir()); - outputStream = new FileOutputStream(outputFile); + public Downloader(File destFile, Context ctx) throws IOException { } public Downloader(OutputStream output) @@ -38,6 +48,11 @@ public abstract class Downloader { outputFile = null; } + public void setProgressListener(ProgressListener listener) { + this.progressListener = listener; + this.progressEvent = new ProgressListener.Event(EVENT_PROGRESS, totalDownloadSize()); + } + public void setProgressListener(ProgressListener progressListener, ProgressListener.Event progressEvent) { this.progressListener = progressListener; @@ -61,7 +76,7 @@ public abstract class Downloader { protected abstract int totalDownloadSize(); - public void download() throws IOException { + protected void download() throws IOException { setupProgressListener(); InputStream input = null; try { diff --git a/src/org/fdroid/fdroid/net/HttpDownloader.java b/src/org/fdroid/fdroid/net/HttpDownloader.java index 363ff5f49..74347ae0b 100644 --- a/src/org/fdroid/fdroid/net/HttpDownloader.java +++ b/src/org/fdroid/fdroid/net/HttpDownloader.java @@ -24,6 +24,13 @@ public class HttpDownloader extends Downloader { sourceUrl = new URL(source); } + // The context is required for opening the file to write to. + public HttpDownloader(String source, File destFile) + throws FileNotFoundException, MalformedURLException { + super(destFile); + sourceUrl = new URL(source); + } + /** * Downloads to a temporary file, which *you must delete yourself when * you are done*.