Merge commit 'refs/merge-requests/27' of git://gitorious.org/f-droid/fdroidclient into merge-requests/27

Conflicts:
	src/org/fdroid/fdroid/RepoXMLHandler.java
	src/org/fdroid/fdroid/UpdateService.java
	src/org/fdroid/fdroid/Utils.java
This commit is contained in:
Ciaran Gultnieks 2013-04-16 09:48:10 +01:00
commit bbd9223ced
7 changed files with 289 additions and 74 deletions

View File

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

View File

@ -127,7 +127,7 @@ public class AppDetails extends ListActivity {
if (apk.detail_size == 0) { if (apk.detail_size == 0) {
size.setText(""); size.setText("");
} else { } else {
size.setText(getFriendlySize(apk.detail_size)); size.setText(Utils.getFriendlySize(apk.detail_size));
} }
TextView buildtype = (TextView) v.findViewById(R.id.buildtype); TextView buildtype = (TextView) v.findViewById(R.id.buildtype);
if (apk.srcname != null) { 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 INSTALL = Menu.FIRST;
private static final int UNINSTALL = Menu.FIRST + 1; private static final int UNINSTALL = Menu.FIRST + 1;
private static final int WEBSITE = Menu.FIRST + 2; private static final int WEBSITE = Menu.FIRST + 2;

View File

@ -263,13 +263,19 @@ public class FDroid extends FragmentActivity {
@Override @Override
protected void onReceiveResult(int resultCode, Bundle resultData) { protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultCode == 1) { String message = resultData.getString(UpdateService.RESULT_MESSAGE);
Toast.makeText(FDroid.this, resultData.getString("errmsg"), boolean finished = false;
Toast.LENGTH_LONG).show(); if (resultCode == UpdateService.STATUS_ERROR) {
} else { Toast.makeText(FDroid.this, message, Toast.LENGTH_LONG).show();
finished = true;
} else if (resultCode == UpdateService.STATUS_COMPLETE) {
repopulateViews(); repopulateViews();
finished = true;
} else if (resultCode == UpdateService.STATUS_INFO) {
pd.setMessage(message);
} }
if (pd.isShowing())
if (finished && pd.isShowing())
pd.dismiss(); pd.dismiss();
} }
} }

View File

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

View File

@ -30,6 +30,7 @@ import java.io.Reader;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLConnection;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@ -41,6 +42,7 @@ import javax.net.ssl.SSLHandshakeException;
import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory; import javax.xml.parsers.SAXParserFactory;
import android.os.Bundle;
import org.xml.sax.Attributes; import org.xml.sax.Attributes;
import org.xml.sax.InputSource; import org.xml.sax.InputSource;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
@ -54,8 +56,8 @@ import android.util.Log;
public class RepoXMLHandler extends DefaultHandler { public class RepoXMLHandler extends DefaultHandler {
// The ID of the repo we're processing. // The repo we're processing.
private int repo; private DB.Repo repo;
private List<DB.App> apps; private List<DB.App> apps;
@ -66,13 +68,23 @@ public class RepoXMLHandler extends DefaultHandler {
private String pubkey; private String pubkey;
private String hashType; 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. // The date format used in the repo XML file.
private SimpleDateFormat mXMLDateFormat = new SimpleDateFormat("yyyy-MM-dd"); private SimpleDateFormat mXMLDateFormat = new SimpleDateFormat("yyyy-MM-dd");
private int totalAppCount;
public RepoXMLHandler(int repo, List<DB.App> apps) { public RepoXMLHandler(DB.Repo repo, List<DB.App> apps, ProgressListener listener) {
this.repo = repo; this.repo = repo;
this.apps = apps; this.apps = apps;
pubkey = null; pubkey = null;
progressListener = listener;
} }
@Override @Override
@ -219,27 +231,37 @@ public class RepoXMLHandler extends DefaultHandler {
curapp.requirements = DB.CommaSeparatedList.make(str); 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 @Override
public void startElement(String uri, String localName, String qName, public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException { Attributes attributes) throws SAXException {
super.startElement(uri, localName, qName, attributes); super.startElement(uri, localName, qName, attributes);
if (localName == "repo") { if (localName.equals("repo")) {
String pk = attributes.getValue("", "pubkey"); String pk = attributes.getValue("", "pubkey");
if (pk != null) if (pk != null)
pubkey = pk; pubkey = pk;
} else if (localName == "application" && curapp == null) { } else if (localName.equals("application") && curapp == null) {
curapp = new DB.App(); curapp = new DB.App();
curapp.detail_Populated = true; 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 = new DB.Apk();
curapk.id = curapp.id; curapk.id = curapp.id;
curapk.repo = repo; curapk.repo = repo.id;
hashType = null; hashType = null;
} else if (localName == "hash" && curapk != null) { } else if (localName.equals("hash") && curapk != null) {
hashType = attributes.getValue("", "type"); hashType = attributes.getValue("", "type");
} }
curchars.setLength(0); 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) may contain an etag value for the response, or it may be left
// empty if none was available. // empty if none was available.
private static int getRemoteFile(Context ctx, String url, String dest, 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 { IOException {
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
URL u = new URL(url); URL u = new URL(url);
HttpURLConnection uc = (HttpURLConnection) u.openConnection(); HttpURLConnection connection = (HttpURLConnection) u.openConnection();
if (etag != null) if (etag != null)
uc.setRequestProperty("If-None-Match", etag); connection.setRequestProperty("If-None-Match", etag);
int totalBytes = 0; int totalBytes = 0;
int code = uc.getResponseCode(); int code = connection.getResponseCode();
if (code == 200) { 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; InputStream input = null;
OutputStream output = null; OutputStream output = null;
try { try {
input = new URL(url).openStream(); input = connection.getInputStream();
output = ctx.openFileOutput(dest, Context.MODE_PRIVATE); output = ctx.openFileOutput(dest, Context.MODE_PRIVATE);
Utils.copy(input, output); Utils.copy(input, output, progressListener, progressEvent);
} finally { } finally {
Utils.closeQuietly(output); Utils.closeQuietly(output);
Utils.closeQuietly(input); Utils.closeQuietly(input);
} }
String et = uc.getHeaderField("ETag"); String et = connection.getHeaderField("ETag");
if (et != null) if (et != null)
retag.append(et); retag.append(et);
} }
@ -293,7 +325,8 @@ public class RepoXMLHandler extends DefaultHandler {
// value for the index that was successfully processed, or it may contain // value for the index that was successfully processed, or it may contain
// null if none was available. // null if none was available.
public static String doUpdate(Context ctx, DB.Repo repo, public static String doUpdate(Context ctx, DB.Repo repo,
List<DB.App> apps, StringBuilder newetag, List<Integer> keeprepos) { List<DB.App> apps, StringBuilder newetag, List<Integer> keeprepos,
ProgressListener progressListener) {
try { try {
int code = 0; int code = 0;
@ -309,8 +342,11 @@ public class RepoXMLHandler extends DefaultHandler {
address += "?" + pi.versionName; address += "?" + pi.versionName;
} catch (Exception e) { } 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", code = getRemoteFile(ctx, address, "tempindex.jar",
repo.lastetag, newetag); repo.lastetag, newetag, progressListener, event );
if (code == 200) { if (code == 200) {
String jarpath = ctx.getFilesDir() + "/tempindex.jar"; String jarpath = ctx.getFilesDir() + "/tempindex.jar";
JarFile jar = null; JarFile jar = null;
@ -365,8 +401,12 @@ public class RepoXMLHandler extends DefaultHandler {
// It's an old-fashioned unsigned repo... // It's an old-fashioned unsigned repo...
Log.d("FDroid", "Getting unsigned index from " + repo.address); 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", code = getRemoteFile(ctx, repo.address + "/index.xml",
"tempindex.xml", repo.lastetag, newetag); "tempindex.xml", repo.lastetag, newetag,
progressListener, event);
} }
if (code == 200) { if (code == 200) {
@ -374,11 +414,22 @@ public class RepoXMLHandler extends DefaultHandler {
SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = spf.newSAXParser(); SAXParser sp = spf.newSAXParser();
XMLReader xr = sp.getXMLReader(); XMLReader xr = sp.getXMLReader();
RepoXMLHandler handler = new RepoXMLHandler(repo.id, apps); RepoXMLHandler handler = new RepoXMLHandler(repo, apps, progressListener);
xr.setContentHandler(handler); xr.setContentHandler(handler);
Reader r = new BufferedReader(new FileReader(new File( File tempIndex = new File(ctx.getFilesDir() + "/tempindex.xml");
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); InputSource is = new InputSource(r);
xr.parse(is); xr.parse(is);
@ -427,4 +478,7 @@ public class RepoXMLHandler extends DefaultHandler {
return null; return null;
} }
public void setTotalAppCount(int totalAppCount) {
this.totalAppCount = totalAppCount;
}
} }

View File

@ -42,7 +42,14 @@ import android.os.SystemClock;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.util.Log; 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() { public UpdateService() {
super("UpdateService"); super("UpdateService");
@ -72,24 +79,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) { protected void onHandleIntent(Intent intent) {
// We might be doing a scheduled run, or we might have been launched by receiver = intent.getParcelableExtra("receiver");
// the app in response to a user's request. If we get this receiver,
// it's
// the latter...
ResultReceiver receiver = intent.getParcelableExtra("receiver");
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
String errmsg = ""; String errmsg = "";
try { try {
SharedPreferences prefs = PreferenceManager SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(getBaseContext()); .getDefaultSharedPreferences(getBaseContext());
// See if it's time to actually do anything yet... // See if it's time to actually do anything yet...
if (receiver == null) { if (isScheduledRun()) {
long lastUpdate = prefs.getLong("lastUpdateCheck", 0); long lastUpdate = prefs.getLong("lastUpdateCheck", 0);
String sint = prefs.getString("updateInterval", "0"); String sint = prefs.getString("updateInterval", "0");
int interval = Integer.parseInt(sint); int interval = Integer.parseInt(sint);
@ -125,9 +149,12 @@ public class UpdateService extends IntentService {
boolean success = true; boolean success = true;
for (DB.Repo repo : repos) { for (DB.Repo repo : repos) {
if (repo.inuse) { if (repo.inuse) {
sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address));
StringBuilder newetag = new StringBuilder(); StringBuilder newetag = new StringBuilder();
String err = RepoXMLHandler.doUpdate(getBaseContext(), String err = RepoXMLHandler.doUpdate(getBaseContext(),
repo, apps, newetag, keeprepos); repo, apps, newetag, keeprepos, this);
if (err == null) { if (err == null) {
repo.lastetag = newetag.toString(); repo.lastetag = newetag.toString();
} else { } else {
@ -143,9 +170,9 @@ public class UpdateService extends IntentService {
} }
if (success) { if (success) {
sendStatus(STATUS_INFO, getString(R.string.status_checking_compatibility));
List<DB.App> acceptedapps = new ArrayList<DB.App>(); List<DB.App> acceptedapps = new ArrayList<DB.App>();
List<DB.App> prevapps = ((FDroidApp) getApplication()) List<DB.App> prevapps = ((FDroidApp) getApplication()).getApps();
.getApps();
DB db = DB.getDB(); DB db = DB.getDB();
try { try {
@ -235,17 +262,12 @@ public class UpdateService extends IntentService {
} }
} }
if (receiver != null) { if (!success) {
Bundle resultData = new Bundle(); if (errmsg.length() == 0)
if (!success) { errmsg = "Unknown error";
if (errmsg.length() == 0) sendStatus(STATUS_ERROR, errmsg);
errmsg = "Unknown error"; } else {
resultData.putString("errmsg", errmsg); sendStatus(STATUS_COMPLETE);
receiver.send(1, resultData);
} else {
receiver.send(0, resultData);
}
} }
if(success) { if(success) {
@ -258,19 +280,15 @@ public class UpdateService extends IntentService {
Log.e("FDroid", Log.e("FDroid",
"Exception during update processing:\n" "Exception during update processing:\n"
+ Log.getStackTraceString(e)); + Log.getStackTraceString(e));
if (receiver != null) { if (errmsg.length() == 0)
Bundle resultData = new Bundle(); errmsg = "Unknown error";
if (errmsg.length() == 0) sendStatus(STATUS_ERROR, errmsg);
errmsg = "Unknown error";
resultData.putString("errmsg", errmsg);
receiver.send(1, resultData);
}
} finally { } finally {
Log.d("FDroid", "Update took " Log.d("FDroid", "Update took "
+ ((System.currentTimeMillis() - startTime) / 1000) + ((System.currentTimeMillis() - startTime) / 1000)
+ " seconds."); + " seconds.");
receiver = null;
} }
} }
private void getIcon(DB.App app, List<DB.Repo> repos) { private void getIcon(DB.App app, List<DB.Repo> 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);
}
} }

View File

@ -26,19 +26,34 @@ import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
public final class Utils { public final class Utils {
private Utils() {
}
public static final int BUFFER_SIZE = 4096; 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) public static void copy(InputStream input, OutputStream output)
throws IOException { 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]; byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead = 0;
while (true) { while (true) {
int count = input.read(buffer); int count = input.read(buffer);
if (count == -1) { if (count == -1) {
break; break;
} }
if (progressListener != null) {
bytesRead += count;
templateProgressEvent.progress = bytesRead;
progressListener.onProgress(templateProgressEvent);
}
output.write(buffer, 0, count); output.write(buffer, 0, count);
} }
output.flush(); output.flush();
@ -62,4 +77,52 @@ public final class Utils {
public static int getApi() { public static int getApi() {
return Build.VERSION.SDK_INT; 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;
}
} }