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;
+ }
+
}