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 "<application" and counting that. It
is not perfect, and it takes about 3 seconds for 600 apps on my
crappy emulator, but the progress makes much more sense.

Refactored helper loops as per Andrew's suggestions.

Close file reader correctly.
This commit is contained in:
Peter Serwylo 2013-04-09 18:31:33 +10:00 committed by Peter Serwylo
parent 68edafc48d
commit 4bcf4bf60d
7 changed files with 223 additions and 74 deletions

View File

@ -152,4 +152,15 @@
<string name="category_whatsnew">What\'s New</string>
<string name="category_recentlyupdated">Recently Updated</string>
<!--
status_download takes three parameters:
- Downloaded size (human readable)
- Total size (human readable)
- Percentage complete (int between 0-100)
-->
<string name="status_download">Downloading %1$s / %2$s (%3$d%%)</string>
<string name="status_processing_xml">Processing application %1$d of %2$d</string>
<string name="status_connecting_to_repo">Connecting to repository:\n%1$s</string>
<string name="status_checking_compatibility">Checking apps compatibility with your device…</string>
</resources>

View File

@ -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;

View File

@ -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();
}
}

View File

@ -0,0 +1,7 @@
package org.fdroid.fdroid;
public interface ProgressListener {
public void onProgress(int type, int progress, int total);
}

View File

@ -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<DB.App> apps) {
public RepoXMLHandler(int repo, Vector<DB.App> 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<DB.App> apps, StringBuilder newetag, Vector<Integer> keeprepos) {
Vector<DB.App> apps, StringBuilder newetag, Vector<Integer> 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 = "<application";
handler.setTotalAppCount(Utils.countSubstringOccurrence(tempIndex, APPLICATION));
InputSource is = new InputSource(r);
xr.parse(is);
@ -427,4 +458,7 @@ public class RepoXMLHandler extends DefaultHandler {
return null;
}
public void setTotalAppCount(int totalAppCount) {
this.totalAppCount = totalAppCount;
}
}

View File

@ -41,7 +41,14 @@ import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.util.Log;
public class UpdateService extends IntentService {
public class UpdateService extends IntentService implements ProgressListener {
public static final String RESULT_MESSAGE = "msg";
public static final int STATUS_COMPLETE = 0;
public static final int STATUS_ERROR = 1;
public static final int STATUS_INFO = 2;
private ResultReceiver receiver = null;
public UpdateService() {
super("UpdateService");
@ -71,24 +78,41 @@ public class UpdateService extends IntentService {
}
}
protected void sendStatus(int statusCode ) {
sendStatus(statusCode, null);
}
protected void sendStatus(int statusCode, String message ) {
if (receiver != null) {
Bundle resultData = new Bundle();
if (message != null && message.length() > 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<DB.App> acceptedapps = new Vector<DB.App>();
Vector<DB.App> 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);
sendStatus(STATUS_ERROR, errmsg);
} else {
receiver.send(0, resultData);
}
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);
}
sendStatus(STATUS_ERROR, errmsg);
} finally {
Log.d("FDroid", "Update took "
+ ((System.currentTimeMillis() - startTime) / 1000)
+ " seconds.");
receiver = null;
}
}
private void getIcon(DB.App app, Vector<DB.Repo> 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);
}
}

View File

@ -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,14 +33,28 @@ public final class 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, -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();
@ -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;
}
}