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_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>

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

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

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.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<DB.App> 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<DB.App> apps) {
public RepoXMLHandler(DB.Repo repo, List<DB.App> 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<DB.App> apps, StringBuilder newetag, List<Integer> keeprepos) {
List<DB.App> apps, StringBuilder newetag, List<Integer> 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 = "<application";
handler.setTotalAppCount(Utils.countSubstringOccurrence(tempIndex, APPLICATION));
InputSource is = new InputSource(r);
xr.parse(is);
@ -427,4 +478,7 @@ public class RepoXMLHandler extends DefaultHandler {
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.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");
@ -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) {
// 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<DB.App> acceptedapps = new ArrayList<DB.App>();
List<DB.App> prevapps = ((FDroidApp) getApplication())
.getApps();
List<DB.App> 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<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;
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;
}
}