diff --git a/res/values/strings.xml b/res/values/strings.xml index ffe4af52c..3a064405c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -152,4 +152,16 @@ What\'s New Recently Updated + + Downloading\n%2$s / %3$s (%4$d%%) from\n%1$s + Processing application\n%2$d of %3$d from\n%1$s + Connecting to\n%1$s + Checking all apps compatibility with your device… + diff --git a/src/org/fdroid/fdroid/AppDetails.java b/src/org/fdroid/fdroid/AppDetails.java index 3fdd92aae..624a32cc6 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 c68e9b662..88c32b654 100644 --- a/src/org/fdroid/fdroid/FDroid.java +++ b/src/org/fdroid/fdroid/FDroid.java @@ -263,13 +263,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..d38e342cd --- /dev/null +++ b/src/org/fdroid/fdroid/ProgressListener.java @@ -0,0 +1,54 @@ +package org.fdroid.fdroid; + +import android.os.Bundle; + +public interface ProgressListener { + + public void onProgress(Event event); + + // I went a bit overboard with the overloaded constructors, but they all + // seemed potentially useful and unambiguous, so I just put them in there + // while I'm here. + public static class Event { + + public static final int NO_VALUE = Integer.MIN_VALUE; + + public final int type; + public final Bundle data; + + // These two are not final, so that you can create a template Event, + // pass it into a function which performs something over time, and + // that function can initialize "total" and progressively + // update "progress" + public int progress; + public int total; + + public Event(int type) { + this(type, NO_VALUE, NO_VALUE, null); + } + + public Event(int type, Bundle data) { + this(type, NO_VALUE, NO_VALUE, data); + } + + public Event(int type, int progress) { + this(type, progress, NO_VALUE, null); + } + + public Event(int type, int progress, Bundle data) { + this(type, NO_VALUE, NO_VALUE, data); + } + + public Event(int type, int progress, int total) { + this(type, progress, total, null); + } + + public Event(int type, int progress, int total, Bundle data) { + this.type = type; + this.progress = progress; + this.total = total; + this.data = data == null ? new Bundle() : data; + } + } + +} diff --git a/src/org/fdroid/fdroid/RepoXMLHandler.java b/src/org/fdroid/fdroid/RepoXMLHandler.java index ad19265f1..27e6300fb 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; @@ -41,6 +42,7 @@ import javax.net.ssl.SSLHandshakeException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; +import android.os.Bundle; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -54,8 +56,8 @@ import android.util.Log; public class RepoXMLHandler extends DefaultHandler { - // The ID of the repo we're processing. - private int repo; + // The repo we're processing. + private DB.Repo repo; private List apps; @@ -66,13 +68,23 @@ 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; + + public static final String PROGRESS_DATA_REPO = "repo"; + // The date format used in the repo XML file. private SimpleDateFormat mXMLDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + private int totalAppCount; - public RepoXMLHandler(int repo, List apps) { + public RepoXMLHandler(DB.Repo repo, List apps, ProgressListener listener) { this.repo = repo; this.apps = apps; pubkey = null; + progressListener = listener; } @Override @@ -219,27 +231,37 @@ public class RepoXMLHandler extends DefaultHandler { curapp.requirements = DB.CommaSeparatedList.make(str); } } - } + private static Bundle createProgressData(String repoAddress) { + Bundle data = new Bundle(); + data.putString(PROGRESS_DATA_REPO, repoAddress); + return data; + } + @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - super.startElement(uri, localName, qName, attributes); - if (localName == "repo") { + if (localName.equals("repo")) { String pk = attributes.getValue("", "pubkey"); if (pk != null) pubkey = pk; - } else if (localName == "application" && curapp == null) { + } else if (localName.equals("application") && curapp == null) { curapp = new DB.App(); curapp.detail_Populated = true; - } else if (localName == "package" && curapp != null && curapk == null) { + Bundle progressData = createProgressData(repo.address); + progressCounter ++; + progressListener.onProgress( + new ProgressListener.Event( + RepoXMLHandler.PROGRESS_TYPE_PROCESS_XML, progressCounter, + totalAppCount, progressData)); + } else if (localName.equals("package") && curapp != null && curapk == null) { curapk = new DB.Apk(); curapk.id = curapp.id; - curapk.repo = repo; + curapk.repo = repo.id; hashType = null; - } else if (localName == "hash" && curapk != null) { + } else if (localName.equals("hash") && curapk != null) { hashType = attributes.getValue("", "type"); } curchars.setLength(0); @@ -252,29 +274,39 @@ 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, + ProgressListener.Event progressEvent) 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. + progressEvent.total = connection.getContentLength(); + Log.d("FDroid", "Downloading " + progressEvent.total + " 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, progressListener, progressEvent); } 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 +325,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, - List apps, StringBuilder newetag, List keeprepos) { + List apps, StringBuilder newetag, List keeprepos, + ProgressListener progressListener) { try { int code = 0; @@ -309,8 +342,11 @@ public class RepoXMLHandler extends DefaultHandler { address += "?" + pi.versionName; } catch (Exception e) { } + Bundle progressData = createProgressData(repo.address); + ProgressListener.Event event = new ProgressListener.Event( + RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD, progressData); code = getRemoteFile(ctx, address, "tempindex.jar", - repo.lastetag, newetag); + repo.lastetag, newetag, progressListener, event ); if (code == 200) { String jarpath = ctx.getFilesDir() + "/tempindex.jar"; JarFile jar = null; @@ -365,8 +401,12 @@ public class RepoXMLHandler extends DefaultHandler { // It's an old-fashioned unsigned repo... Log.d("FDroid", "Getting unsigned index from " + repo.address); + Bundle eventData = createProgressData(repo.address); + ProgressListener.Event event = new ProgressListener.Event( + RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD, eventData); code = getRemoteFile(ctx, repo.address + "/index.xml", - "tempindex.xml", repo.lastetag, newetag); + "tempindex.xml", repo.lastetag, newetag, + progressListener, event); } if (code == 200) { @@ -374,11 +414,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, 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); @@ -125,9 +149,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 { @@ -143,9 +170,9 @@ public class UpdateService extends IntentService { } if (success) { + sendStatus(STATUS_INFO, getString(R.string.status_checking_compatibility)); List acceptedapps = new ArrayList(); - List prevapps = ((FDroidApp) getApplication()) - .getApps(); + List prevapps = ((FDroidApp) getApplication()).getApps(); DB db = DB.getDB(); try { @@ -235,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) { @@ -258,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, List repos) { @@ -307,4 +325,25 @@ 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(ProgressListener.Event event) { + + String message = ""; + if (event.type == RepoXMLHandler.PROGRESS_TYPE_DOWNLOAD) { + String repoAddress = event.data.getString(RepoXMLHandler.PROGRESS_DATA_REPO); + String downloadedSize = Utils.getFriendlySize( event.progress ); + String totalSize = Utils.getFriendlySize( event.total ); + int percent = (int)((double)event.progress/event.total * 100); + message = getString(R.string.status_download, repoAddress, downloadedSize, totalSize, percent); + } else if (event.type == RepoXMLHandler.PROGRESS_TYPE_PROCESS_XML) { + String repoAddress = event.data.getString(RepoXMLHandler.PROGRESS_DATA_REPO); + message = getString(R.string.status_processing_xml, repoAddress, event.progress, event.total); + } + + sendStatus(STATUS_INFO, message); + } } diff --git a/src/org/fdroid/fdroid/Utils.java b/src/org/fdroid/fdroid/Utils.java index 529dc9182..d9d926a45 100644 --- a/src/org/fdroid/fdroid/Utils.java +++ b/src/org/fdroid/fdroid/Utils.java @@ -26,19 +26,34 @@ import java.io.IOException; import java.io.OutputStream; public final class Utils { - private Utils() { - } public static final int BUFFER_SIZE = 4096; + 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, null, null); + } + + public static void copy(InputStream input, OutputStream output, + ProgressListener progressListener, + ProgressListener.Event templateProgressEvent) + 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; + templateProgressEvent.progress = bytesRead; + progressListener.onProgress(templateProgressEvent); + } output.write(buffer, 0, count); } output.flush(); @@ -62,4 +77,52 @@ public final class Utils { public static int getApi() { return Build.VERSION.SDK_INT; } + + 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; + } + }