diff --git a/res/values/strings.xml b/res/values/strings.xml index ffe4af52c..26f5c031f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -152,4 +152,15 @@ 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; + } + }