From 4bcf4bf60de955f397b3e30583a47e1b7a062957 Mon Sep 17 00:00:00 2001 From: Peter Serwylo Date: Tue, 9 Apr 2013 18:31:33 +1000 Subject: [PATCH] Progress information during repo update. Polls the download server before download to see how big the file is so that we can figur eout our progress during download. Its a bit of a hit (about 1.5 seconds on my connection), but I think most people would be willing to take a small hit to get accurate percentage measurements. I also spend a small amount of time (~1.5 seconds) asking how big the file is before we download it, so that we can give an accurate progress measurement. The same can be said for peeking into the XML file before we pass it to the SAX parser, by just iterating over every line looking for "What\'s New Recently Updated + + Downloading %1$s / %2$s (%3$d%%) + Processing application %1$d of %2$d + Connecting to repository:\n%1$s + Checking apps compatibility with your device… + diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 5e4f75917..e3d86a440 100644 --- a/src/org/fdroid/fdroid/AppDetails.java +++ b/src/org/fdroid/fdroid/AppDetails.java @@ -127,7 +127,7 @@ public class AppDetails extends ListActivity { if (apk.detail_size == 0) { size.setText(""); } else { - size.setText(getFriendlySize(apk.detail_size)); + size.setText(Utils.getFriendlySize(apk.detail_size)); } TextView buildtype = (TextView) v.findViewById(R.id.buildtype); if (apk.srcname != null) { @@ -153,19 +153,6 @@ public class AppDetails extends ListActivity { } } - private static final String[] FRIENDLY_SIZE_FORMAT = { - "%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB" }; - - private static String getFriendlySize(int size) { - double s = size; - int i = 0; - while (i < FRIENDLY_SIZE_FORMAT.length - 1 && s >= 1024) { - s = (100 * s / 1024) / 100.0; - i++; - } - return String.format(FRIENDLY_SIZE_FORMAT[i], s); - } - private static final int INSTALL = Menu.FIRST; private static final int UNINSTALL = Menu.FIRST + 1; private static final int WEBSITE = Menu.FIRST + 2; diff --git a/src/org/fdroid/fdroid/FDroid.java b/src/org/fdroid/fdroid/FDroid.java index c14703eb5..ac73b9c76 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -384,13 +384,19 @@ public class FDroid extends FragmentActivity { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { - if (resultCode == 1) { - Toast.makeText(FDroid.this, resultData.getString("errmsg"), - Toast.LENGTH_LONG).show(); - } else { + String message = resultData.getString(UpdateService.RESULT_MESSAGE); + boolean finished = false; + if (resultCode == UpdateService.STATUS_ERROR) { + Toast.makeText(FDroid.this, message, Toast.LENGTH_LONG).show(); + finished = true; + } else if (resultCode == UpdateService.STATUS_COMPLETE) { repopulateViews(); + finished = true; + } else if (resultCode == UpdateService.STATUS_INFO) { + pd.setMessage(message); } - if (pd.isShowing()) + + if (finished && pd.isShowing()) pd.dismiss(); } } diff --git a/src/org/fdroid/fdroid/ProgressListener.java b/src/org/fdroid/fdroid/ProgressListener.java new file mode 100644 index 000000000..2ec97ed92 --- /dev/null +++ b/src/org/fdroid/fdroid/ProgressListener.java @@ -0,0 +1,7 @@ +package org.fdroid.fdroid; + +public interface ProgressListener { + + public void onProgress(int type, int progress, int total); + +} diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index 2ab7360a5..f8844dc9b 100644 --- a/src/org/fdroid/fdroid/RepoXMLHandler.java +++ b/src/org/fdroid/fdroid/RepoXMLHandler.java @@ -30,6 +30,7 @@ import java.io.Reader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLConnection; import java.security.cert.Certificate; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -66,13 +67,21 @@ public class RepoXMLHandler extends DefaultHandler { private String pubkey; private String hashType; + private int progressCounter = 0; + private ProgressListener progressListener; + + public static final int PROGRESS_TYPE_DOWNLOAD = 1; + public static final int PROGRESS_TYPE_PROCESS_XML = 2; + // The date format used in the repo XML file. private SimpleDateFormat mXMLDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + private int totalAppCount; - public RepoXMLHandler(int repo, Vector apps) { + public RepoXMLHandler(int repo, Vector apps, ProgressListener listener) { this.repo = repo; this.apps = apps; pubkey = null; + progressListener = listener; } @Override @@ -225,7 +234,6 @@ public class RepoXMLHandler extends DefaultHandler { @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - super.startElement(uri, localName, qName, attributes); if (localName == "repo") { String pk = attributes.getValue("", "pubkey"); @@ -234,6 +242,8 @@ public class RepoXMLHandler extends DefaultHandler { } else if (localName == "application" && curapp == null) { curapp = new DB.App(); curapp.detail_Populated = true; + progressCounter ++; + progressListener.onProgress(RepoXMLHandler.PROGRESS_TYPE_PROCESS_XML, progressCounter, totalAppCount); } else if (localName == "package" && curapp != null && curapk == null) { curapk = new DB.Apk(); curapk.id = curapp.id; @@ -252,29 +262,38 @@ public class RepoXMLHandler extends DefaultHandler { // empty) may contain an etag value for the response, or it may be left // empty if none was available. private static int getRemoteFile(Context ctx, String url, String dest, - String etag, StringBuilder retag) throws MalformedURLException, + String etag, StringBuilder retag, + ProgressListener progressListener ) throws MalformedURLException, IOException { long startTime = System.currentTimeMillis(); URL u = new URL(url); - HttpURLConnection uc = (HttpURLConnection) u.openConnection(); + HttpURLConnection connection = (HttpURLConnection) u.openConnection(); if (etag != null) - uc.setRequestProperty("If-None-Match", etag); + connection.setRequestProperty("If-None-Match", etag); int totalBytes = 0; - int code = uc.getResponseCode(); + int code = connection.getResponseCode(); if (code == 200) { + // Testing in the emulator for me, showed that figuring out the filesize took about 1 to 1.5 seconds. + // To put this in context, downloading a repo of: + // - 400k takes ~6 seconds + // - 5k takes ~3 seconds + // on my connection. I think the 1/1.5 seconds is worth it, because as the repo grows, the tradeoff will + // become more worth it. + int size = connection.getContentLength(); + Log.d("FDroid", "Downloading " + size + " bytes from " + url); InputStream input = null; OutputStream output = null; try { - input = new URL(url).openStream(); + input = connection.getInputStream(); output = ctx.openFileOutput(dest, Context.MODE_PRIVATE); - Utils.copy(input, output); + Utils.copy(input, output, size, progressListener, PROGRESS_TYPE_DOWNLOAD); } finally { Utils.closeQuietly(output); Utils.closeQuietly(input); } - String et = uc.getHeaderField("ETag"); + String et = connection.getHeaderField("ETag"); if (et != null) retag.append(et); } @@ -293,7 +312,8 @@ public class RepoXMLHandler extends DefaultHandler { // value for the index that was successfully processed, or it may contain // null if none was available. public static String doUpdate(Context ctx, DB.Repo repo, - Vector apps, StringBuilder newetag, Vector keeprepos) { + Vector apps, StringBuilder newetag, Vector keeprepos, + ProgressListener progressListener) { try { int code = 0; @@ -310,7 +330,7 @@ public class RepoXMLHandler extends DefaultHandler { } catch (Exception e) { } code = getRemoteFile(ctx, address, "tempindex.jar", - repo.lastetag, newetag); + repo.lastetag, newetag, progressListener); if (code == 200) { String jarpath = ctx.getFilesDir() + "/tempindex.jar"; JarFile jar = null; @@ -366,7 +386,7 @@ public class RepoXMLHandler extends DefaultHandler { // It's an old-fashioned unsigned repo... Log.d("FDroid", "Getting unsigned index from " + repo.address); code = getRemoteFile(ctx, repo.address + "/index.xml", - "tempindex.xml", repo.lastetag, newetag); + "tempindex.xml", repo.lastetag, newetag, progressListener); } if (code == 200) { @@ -374,11 +394,22 @@ public class RepoXMLHandler extends DefaultHandler { SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser sp = spf.newSAXParser(); XMLReader xr = sp.getXMLReader(); - RepoXMLHandler handler = new RepoXMLHandler(repo.id, apps); + RepoXMLHandler handler = new RepoXMLHandler(repo.id, apps, progressListener); xr.setContentHandler(handler); - Reader r = new BufferedReader(new FileReader(new File( - ctx.getFilesDir() + "/tempindex.xml"))); + File tempIndex = new File(ctx.getFilesDir() + "/tempindex.xml"); + BufferedReader r = new BufferedReader(new FileReader(tempIndex)); + + // A bit of a hack, this might return false positives if an apps description + // or some other part of the XML file contains this, but it is a pretty good + // estimate and makes the progress counter more informative. + // As with asking the server about the size of the index before downloading, + // this also has a time tradeoff. It takes about three seconds to iterate + // through the file and count 600 apps on a slow emulator (v17), but if it is + // taking two minutes to update, the three second wait may be worth it. + final String APPLICATION = " 0) + resultData.putString(RESULT_MESSAGE, message); + receiver.send( statusCode, resultData ); + } + } + + /** + * We might be doing a scheduled run, or we might have been launched by + * the app in response to a user's request. If we have a receiver, it's + * the latter... + */ + private boolean isScheduledRun() { + return receiver == null; + } + protected void onHandleIntent(Intent intent) { - // We might be doing a scheduled run, or we might have been launched by - // the app in response to a user's request. If we get this receiver, - // it's - // the latter... - ResultReceiver receiver = intent.getParcelableExtra("receiver"); + receiver = intent.getParcelableExtra("receiver"); long startTime = System.currentTimeMillis(); String errmsg = ""; - try { SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(getBaseContext()); // See if it's time to actually do anything yet... - if (receiver == null) { + if (isScheduledRun()) { long lastUpdate = prefs.getLong("lastUpdateCheck", 0); String sint = prefs.getString("updateInterval", "0"); int interval = Integer.parseInt(sint); @@ -124,9 +148,12 @@ public class UpdateService extends IntentService { boolean success = true; for (DB.Repo repo : repos) { if (repo.inuse) { + + sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address)); + StringBuilder newetag = new StringBuilder(); String err = RepoXMLHandler.doUpdate(getBaseContext(), - repo, apps, newetag, keeprepos); + repo, apps, newetag, keeprepos, this); if (err == null) { repo.lastetag = newetag.toString(); } else { @@ -142,6 +169,7 @@ public class UpdateService extends IntentService { } if (success) { + sendStatus(STATUS_INFO, getString(R.string.status_checking_compatibility)); Vector acceptedapps = new Vector(); Vector prevapps = ((FDroidApp) getApplication()) .getApps(); @@ -234,17 +262,12 @@ public class UpdateService extends IntentService { } } - if (receiver != null) { - Bundle resultData = new Bundle(); - if (!success) { - if (errmsg.length() == 0) - errmsg = "Unknown error"; - resultData.putString("errmsg", errmsg); - receiver.send(1, resultData); - } else { - receiver.send(0, resultData); - } - + if (!success) { + if (errmsg.length() == 0) + errmsg = "Unknown error"; + sendStatus(STATUS_ERROR, errmsg); + } else { + sendStatus(STATUS_COMPLETE); } if(success) { @@ -257,19 +280,15 @@ public class UpdateService extends IntentService { Log.e("FDroid", "Exception during update processing:\n" + Log.getStackTraceString(e)); - if (receiver != null) { - Bundle resultData = new Bundle(); - if (errmsg.length() == 0) - errmsg = "Unknown error"; - resultData.putString("errmsg", errmsg); - receiver.send(1, resultData); - } + if (errmsg.length() == 0) + errmsg = "Unknown error"; + sendStatus(STATUS_ERROR, errmsg); } finally { Log.d("FDroid", "Update took " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds."); + receiver = null; } - } private void getIcon(DB.App app, Vector repos) { @@ -306,4 +325,23 @@ public class UpdateService extends IntentService { } } + /** + * Received progress event from the RepoXMLHandler. + * It could be progress downloading from the repo, or perhaps processing the info from the repo. + */ + @Override + public void onProgress(int type, int progress, int total) { + + String message = ""; + if (type == RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD) { + String downloadedSize = Utils.getFriendlySize( progress ); + String totalSize = Utils.getFriendlySize( total ); + int percent = (int)((double)progress/total * 100); + message = getString(R.string.status_download, downloadedSize, totalSize, percent); + } else if (type == RepoXMLHandler.PROGRESS_TYPE_PROCESS_XML) { + message = getString(R.string.status_processing_xml, progress, total); + } + + sendStatus(STATUS_INFO, message); + } } diff --git a/src/org/fdroid/fdroid/Utils.java b/src/org/fdroid/fdroid/Utils.java index 14dad6be0..d0d5bfe2f 100644 --- a/src/org/fdroid/fdroid/Utils.java +++ b/src/org/fdroid/fdroid/Utils.java @@ -22,6 +22,10 @@ import java.io.Closeable; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; public final class Utils { private Utils() { @@ -29,18 +33,32 @@ public final class Utils { public static final int BUFFER_SIZE = 4096; - public static void copy(InputStream input, OutputStream output) - throws IOException { - byte[] buffer = new byte[BUFFER_SIZE]; - while (true) { - int count = input.read(buffer); - if (count == -1) { - break; - } - output.write(buffer, 0, count); - } - output.flush(); - } + private static final String[] FRIENDLY_SIZE_FORMAT = { + "%.0f B", "%.0f KiB", "%.1f MiB", "%.2f GiB" }; + + + public static void copy(InputStream input, OutputStream output) + throws IOException { + copy(input, output, -1, null, -1); + } + + public static void copy(InputStream input, OutputStream output, int totalSize, ProgressListener progressListener, int progressType) + throws IOException { + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead = 0; + while (true) { + int count = input.read(buffer); + if (count == -1) { + break; + } + if (progressListener != null) { + bytesRead += count; + progressListener.onProgress(progressType, bytesRead, totalSize); + } + output.write(buffer, 0, count); + } + output.flush(); + } public static void closeQuietly(Closeable closeable) { if (closeable == null) { @@ -52,4 +70,52 @@ public final class Utils { // ignore } } + + public static String getFriendlySize(int size) { + double s = size; + int i = 0; + while (i < FRIENDLY_SIZE_FORMAT.length - 1 && s >= 1024) { + s = (100 * s / 1024) / 100.0; + i++; + } + return String.format(FRIENDLY_SIZE_FORMAT[i], s); + } + + public static int countSubstringOccurrence(File file, String substring) throws IOException { + int count = 0; + BufferedReader reader = null; + try { + + reader = new BufferedReader(new FileReader(file)); + while(true) { + String line = reader.readLine(); + if (line == null) { + break; + } + count += countSubstringOccurrence(line, substring); + } + + } finally { + closeQuietly(reader); + } + return count; + } + + /** + * Thanks to http://stackoverflow.com/a/767910 + */ + public static int countSubstringOccurrence(String toSearch, String substring) { + int count = 0; + int index = 0; + while (true) { + index = toSearch.indexOf(substring, index); + if (index == -1){ + break; + } + count ++; + index += substring.length(); + } + return count; + } + }