1
0
Fork 0
mirror of https://github.com/geometer/FBReaderJ.git synced 2025-10-05 10:49:24 +02:00

OPDS catalogs cache

git-svn-id: https://only.mawhrin.net/repos/FBReaderJ/trunk@1675 6a642e6f-84f6-412e-ac94-c4a38d5a04b0
This commit is contained in:
Vasiliy Bout 2010-08-24 21:56:24 +00:00
parent d029660d2e
commit 105b518a46
22 changed files with 370 additions and 59 deletions

View file

@ -89,5 +89,6 @@
<service android:name="org.geometerplus.android.fbreader.network.ItemsLoadingService" android:process=":networkLibrary" />
<activity android:name="org.geometerplus.android.fbreader.network.NetworkBookInfoActivity" android:process=":networkLibrary" android:configChanges="orientation|keyboardHidden" />
<receiver android:name="org.geometerplus.android.fbreader.network.BookDownloaderCallback" android:process=":networkLibrary" />
<service android:name="org.geometerplus.android.fbreader.network.LibraryInitializationService" android:process=":networkLibrary" />
</application>
</manifest>

View file

@ -89,5 +89,6 @@
<service android:name="org.geometerplus.android.fbreader.network.ItemsLoadingService" android:process=":networkLibrary" />
<activity android:name="org.geometerplus.android.fbreader.network.NetworkBookInfoActivity" android:process=":networkLibrary" android:configChanges="orientation|keyboardHidden" />
<receiver android:name="org.geometerplus.android.fbreader.network.BookDownloaderCallback" android:process=":networkLibrary" />
<service android:name="org.geometerplus.android.fbreader.network.LibraryInitializationService" android:process=":networkLibrary" />
</application>
</manifest>

View file

@ -15,7 +15,8 @@ NP: оповещение об изменениях в namespace'ах проис
** Загрузка не из xml, а из OPDS-каталога с расширениями
DONE сделать DTD для xml-ей, как в OPDS, вместо существующего
DONE перенести xml на сервер
** кэширование каталога
DONE кэширование каталога
** реализовать обновление библиотеки так, чтобы не терялись загруженные элементы дерева
** сделать чтение информации о поиске напрямую из каталога
** Использовать default e-mail при регистрации новых пользователей в AuthenticationDialog (возможно только в Android 2.0+)

View file

@ -296,6 +296,7 @@
<node name="unsupportedOperation" value="Operation wird nicht unterstützt" />
<node name="notAnOPDS" toBeTranslated="true" value="This is not an OPDS catalog" />
<node name="noRequiredInformation" toBeTranslated="true" value="Required information is not specified in the catalog" />
<node name="cacheDirectoryError" toBeTranslated="true" value="Unable to create cache directory" />
</node>
<node name="emptyCatalogBox">
<node name="title" value="Information" />

View file

@ -295,6 +295,7 @@
<node name="unsupportedOperation" value="Unsupported operation" />
<node name="notAnOPDS" value="This is not an OPDS catalog" />
<node name="noRequiredInformation" value="Required information is not specified in the catalog" />
<node name="cacheDirectoryError" value="Unable to create cache directory" />
</node>
<node name="emptyCatalogBox">
<node name="title" value="Information" />

View file

@ -296,6 +296,7 @@
<node name="unsupportedOperation" value="Opération non supportée"/>
<node name="notAnOPDS" toBeTranslated="true" value="This is not an OPDS catalog" />
<node name="noRequiredInformation" toBeTranslated="true" value="Required information is not specified in the catalog" />
<node name="cacheDirectoryError" toBeTranslated="true" value="Unable to create cache directory" />
</node>
<node name="emptyCatalogBox">
<node name="title" value="Information"/>

View file

@ -296,6 +296,7 @@
<node name="unsupportedOperation" value="nem támogaott művelet" />
<node name="notAnOPDS" toBeTranslated="true" value="This is not an OPDS catalog" />
<node name="noRequiredInformation" toBeTranslated="true" value="Required information is not specified in the catalog" />
<node name="cacheDirectoryError" toBeTranslated="true" value="Unable to create cache directory" />
</node>
<node name="emptyCatalogBox">
<node name="title" value="Információ" />

View file

@ -296,6 +296,7 @@
<node name="unsupportedOperation" value="Operazione non supportata" />
<node name="notAnOPDS" toBeTranslated="true" value="This is not an OPDS catalog" />
<node name="noRequiredInformation" toBeTranslated="true" value="Required information is not specified in the catalog" />
<node name="cacheDirectoryError" toBeTranslated="true" value="Unable to create cache directory" />
</node>
<node name="emptyCatalogBox">
<node name="title" value="Informazioni" />

View file

@ -295,6 +295,7 @@
<node name="unsupportedOperation" value="Неподдерживаемая операция" />
<node name="notAnOPDS" value="Это не OPDS каталог" />
<node name="noRequiredInformation" value="Требуемая информация не найдена в каталоге" />
<node name="cacheDirectoryError" value="Не удается создать каталог для кэша" />
</node>
<node name="emptyCatalogBox">
<node name="title" value="Информация" />

View file

@ -296,6 +296,7 @@
<node name="unsupportedOperation" value="Thao tác không được hỗ trợ" />
<node name="notAnOPDS" toBeTranslated="true" value="This is not an OPDS catalog" />
<node name="noRequiredInformation" toBeTranslated="true" value="Required information is not specified in the catalog" />
<node name="cacheDirectoryError" toBeTranslated="true" value="Unable to create cache directory" />
</node>
<node name="emptyCatalogBox">
<node name="title" value="Thông tin" />

View file

@ -296,6 +296,7 @@
<node name="unsupportedOperation" value="非法操作" />
<node name="notAnOPDS" toBeTranslated="true" value="This is not an OPDS catalog" />
<node name="noRequiredInformation" toBeTranslated="true" value="Required information is not specified in the catalog" />
<node name="cacheDirectoryError" toBeTranslated="true" value="Unable to create cache directory" />
</node>
<node name="emptyCatalogBox">
<node name="title" value="信息" />

View file

@ -0,0 +1,69 @@
/*
* Copyright (C) 2010 Geometer Plus <contact@geometerplus.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
package org.geometerplus.android.fbreader.network;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.app.Service;
import android.content.Intent;
public class LibraryInitializationService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
final NetworkView view = NetworkView.Instance();
if (!view.isInitialized()) {
stopSelf();
return;
}
final Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what > 0) {
view.finishBackgroundUpdate();
}
stopSelf();
}
};
final Thread thread = new Thread(new Runnable() {
public void run() {
boolean result = false;
try {
result = view.runBackgroundUpdate();
} finally {
handler.sendEmptyMessage(result ? 1 : 0);
}
}
});
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
}
}

View file

@ -21,6 +21,7 @@ package org.geometerplus.android.fbreader.network;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
@ -59,10 +60,6 @@ public class NetworkLibraryActivity extends NetworkBaseActivity {
@Override
public void onResume() {
super.onResume();
tryResume();
}
private void tryResume() {
if (!NetworkView.Instance().isInitialized()) {
new Initializator().start();
} else {
@ -114,6 +111,7 @@ public class NetworkLibraryActivity extends NetworkBaseActivity {
if (message.what == 0) {
runInitialization(); // run initialization process
} else if (message.obj == null) {
startService(new Intent(getApplicationContext(), LibraryInitializationService.class));
prepareView(); // initialization is complete successfully
} else {
processResults((String) message.obj); // handle initialization error

View file

@ -79,6 +79,19 @@ class NetworkView {
return null;
}
// This method must be called from background thread
public boolean runBackgroundUpdate() {
return NetworkLibrary.Instance().runBackgroundUpdate();
}
// This method MUST be called from main thread
// This method can be called only after runBackgroundUpdate method has returned true
public void finishBackgroundUpdate() {
NetworkLibrary library = NetworkLibrary.Instance();
library.finishBackgroundUpdate();
library.synchronize();
fireModelChangedInternal();
}
/*
* NetworkLibraryItem's actions

View file

@ -29,4 +29,8 @@ public abstract class Paths {
public static String cacheDirectory() {
return BooksDirectoryOption.getValue() + "/.FBReader";
}
public static String networkCacheDirectory() {
return cacheDirectory() + "/cache";
}
}

View file

@ -23,6 +23,8 @@ import java.io.*;
import org.geometerplus.zlibrary.core.image.ZLBase64EncodedImage;
import org.geometerplus.fbreader.Paths;
final class Base64EncodedImage extends ZLBase64EncodedImage {
private static final String ENCODED_SUFFIX = ".base64";
@ -36,7 +38,7 @@ final class Base64EncodedImage extends ZLBase64EncodedImage {
}
public static String makeImagesDir() {
return NetworkImage.makeImagesDir() + File.separator + "base64";
return Paths.networkCacheDirectory() + "/base64";
}
public void setData(String data) {

View file

@ -23,7 +23,7 @@ import java.io.*;
import java.net.*;
import org.geometerplus.zlibrary.core.image.ZLSingleImage;
import org.geometerplus.zlibrary.core.network.*;
import org.geometerplus.zlibrary.core.network.ZLNetworkManager;
import org.geometerplus.fbreader.Paths;
@ -40,15 +40,11 @@ public final class NetworkImage extends ZLSingleImage {
public NetworkImage(String url, String mimeType) {
super(mimeType);
Url = url;
new File(makeImagesDir()).mkdirs();
new File(Paths.networkCacheDirectory()).mkdirs();
}
private static final String TOESCAPE = "<>:\"|?*\\";
public static String makeImagesDir() {
return Paths.cacheDirectory() + File.separator + "cache";
}
// mimeType string MUST be interned
public static String makeImageFileName(String url, String mimeType) {
URI uri;
@ -65,7 +61,7 @@ public final class NetworkImage extends ZLSingleImage {
path.delete(0, 4);
}
path.insert(0, File.separator);
path.insert(0, makeImagesDir());
path.insert(0, Paths.networkCacheDirectory());
int index = path.length();
@ -191,24 +187,7 @@ public final class NetworkImage extends ZLSingleImage {
return;
}
ZLNetworkManager.Instance().perform(new ZLNetworkRequest(Url) {
public String handleStream(URLConnection connection, InputStream inputStream) throws IOException {
OutputStream outStream = new FileOutputStream(imageFile);
try {
final byte[] buffer = new byte[8192];
while (true) {
final int size = inputStream.read(buffer);
if (size <= 0) {
break;
}
outStream.write(buffer, 0, size);
}
} finally {
outStream.close();
}
return null;
}
});
ZLNetworkManager.Instance().downloadToFile(Url, imageFile);
} finally {
mySynchronized = true;
}

View file

@ -232,15 +232,11 @@ public class NetworkLibrary {
public String initialize() {
final LinksComparator comparator = new LinksComparator();
final String url = "http://data.fbreader.org/catalogs/generic-1.0.xml";
final String error = ZLNetworkManager.Instance().perform(
OPDSLinkReader.loadOPDSLinksRequest(url, new OnNewLinkListener() {
final String error = OPDSLinkReader.loadOPDSLinks(false, new OnNewLinkListener() {
public void onNewLink(INetworkLink link) {
addLinkInternal(myLoadedLinks, link, comparator);
}
})
);
});
if (error != null) {
synchronized (myLinks) {
@ -262,9 +258,84 @@ public class NetworkLibrary {
}
);
/*testDate(new ATOMUpdated(2010, 1, 1, 1, 0, 0, 0, 2, 0),
new ATOMUpdated(2009, 12, 31, 23, 0, 0, 0, 0, 0));
testDate(new ATOMUpdated(2010, 12, 31, 23, 40, 0, 0, -1, -30),
new ATOMUpdated(2011, 1, 1, 1, 10, 0, 0, 0, 0));
testDate(new ATOMUpdated(2010, 1, 31, 23, 40, 0, 0, -1, -30),
new ATOMUpdated(2010, 2, 1, 1, 10, 0, 0, 0, 0));
testDate(new ATOMUpdated(2010, 2, 28, 23, 40, 0, 0, -1, -30),
new ATOMUpdated(2010, 3, 1, 1, 10, 0, 0, 0, 0));
testDate(new ATOMUpdated(2012, 2, 28, 23, 40, 0, 0, -1, -30),
new ATOMUpdated(2012, 2, 29, 1, 10, 0, 0, 0, 0));
testDate(new ATOMUpdated(2012, 2, 15, 23, 40, 0, 0, -1, -30),
new ATOMUpdated(2012, 2, 16, 1, 10, 0, 0, 0, 0));
testDate(new ATOMUpdated(2012, 2, 15, 23, 40, 1, 0, 3, 30),
new ATOMUpdated(2012, 2, 15, 23, 40, 0, 0, 3, 30));
testDate(new ATOMUpdated(2012, 2, 15, 23, 40, 0, 0, 3, 30),
new ATOMUpdated(2012, 2, 15, 23, 40, 1, 0, 3, 30));
testDate(new ATOMUpdated(2012, 2, 15, 23, 40, 0, 0.001f, 3, 30),
new ATOMUpdated(2012, 2, 15, 23, 40, 0, 0, 3, 30));*/
return null;
}
/*private void testDate(ATOMDateConstruct date1, ATOMDateConstruct date2) {
String sign = " == ";
final int diff = date1.compareTo(date2);
if (diff > 0) {
sign = " > ";
} else if (diff < 0) {
sign = " < ";
}
Log.w("FBREADER", "" + date1 + sign + date2);
}*/
private ArrayList<INetworkLink> myBackgroundLinks;
private Object myBackgroundLock = new Object();
// This method must be called from background thread
public boolean runBackgroundUpdate() {
synchronized (myBackgroundLock) {
myBackgroundLinks = new ArrayList<INetworkLink>();
final String error = OPDSLinkReader.loadOPDSLinks(true, new OnNewLinkListener() {
public void onNewLink(INetworkLink link) {
myBackgroundLinks.add(link);
}
});
if (error != null || myBackgroundLinks.isEmpty()) {
myBackgroundLinks = null;
}
if (myBackgroundLinks == null) {
return false;
} else {
Collections.sort(myBackgroundLinks, new LinksComparator());
return true;
}
}
}
// This method MUST be called from main thread
// This method can be called only after runBackgroundUpdate method has returned true
//
// synchronize() method MUST be called after this method
public void finishBackgroundUpdate() {
synchronized (myBackgroundLock) {
if (myBackgroundLinks == null) {
throw new RuntimeException("Invalid state: that's impossible!!!");
}
synchronized (myLinks) {
myLoadedLinks.clear();
myLoadedLinks.addAll(myBackgroundLinks);
invalidate();
}
}
}
public String rewriteUrl(String url, boolean externalUrl) {
final String host = ZLNetworkUtil.hostFromUrl(url).toLowerCase();
synchronized (myLinks) {

View file

@ -19,7 +19,7 @@
package org.geometerplus.fbreader.network.atom;
public abstract class ATOMDateConstruct extends ATOMCommonAttributes {
public abstract class ATOMDateConstruct extends ATOMCommonAttributes implements Comparable<ATOMDateConstruct> {
public int Year;
public int Month;
@ -278,5 +278,51 @@ public abstract class ATOMDateConstruct extends ATOMCommonAttributes {
public String toString() {
return getDateTime(false);
}
private static final int[] DAYS_IN_MONTHS = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
private int daysInMonth(int month, int year) {
--month;
while (month > 11) month -= 12;
while (month < 0) month += 12;
if (month == 1 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)) {
return DAYS_IN_MONTHS[1] + 1;
}
return DAYS_IN_MONTHS[month];
}
public int compareTo(ATOMDateConstruct date) {
int dateYear = date.Year;
int dateMonth = date.Month;
int dateDay = date.Day;
int dateHour = date.Hour;
int dateMinutes = date.Minutes;
if (TZHour != date.TZHour || TZMinutes != date.TZMinutes) {
dateMinutes += TZMinutes - date.TZMinutes;
while (dateMinutes < 0) { dateMinutes += 60; --dateHour; }
while (dateMinutes > 59) { dateMinutes -= 60; ++dateHour; }
dateHour += TZHour - date.TZHour;
while (dateHour < 0) { dateHour += 24; --dateDay; }
while (dateHour > 23) { dateHour -= 24; ++dateDay; }
while (dateDay < 1) dateDay += daysInMonth(--dateMonth, dateYear);
while (dateDay > daysInMonth(dateMonth, dateYear)) dateDay -= daysInMonth(dateMonth++, dateYear);
while (dateMonth < 1) { dateMonth += 12; --dateYear; }
while (dateMonth > 12) { dateMonth -= 12; ++dateYear; }
}
if (Year != dateYear) return Year - dateYear;
if (Month != dateMonth) return Month - dateMonth;
if (Day != dateDay) return Day - dateDay;
if (Hour != dateHour) return Hour - dateHour;
if (Minutes != dateMinutes) return Minutes - dateMinutes;
if (Seconds != date.Seconds) return Seconds - date.Seconds;
return Math.round(100 * SecondFraction) - Math.round(100 * date.SecondFraction);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ATOMDateConstruct)) {
return false;
}
return compareTo((ATOMDateConstruct) obj) == 0;
}
};

View file

@ -19,18 +19,23 @@
package org.geometerplus.fbreader.network.opds;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLConnection;
import java.util.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.Map;
import org.geometerplus.zlibrary.core.network.ZLNetworkRequest;
import org.geometerplus.zlibrary.core.network.ZLNetworkManager;
import org.geometerplus.fbreader.Paths;
import org.geometerplus.fbreader.network.*;
import org.geometerplus.fbreader.network.atom.ATOMUpdated;
public class OPDSLinkReader {
static final String CATALOGS_URL = "http://data.fbreader.org/catalogs/generic-1.0.xml";
public static ICustomNetworkLink createCustomLink(int id, String siteName, String title, String summary, String icon, Map<String, String> links) {
if (siteName == null || title == null || links.get(INetworkLink.URL_MAIN) == null) {
return null;
@ -44,13 +49,71 @@ public class OPDSLinkReader {
return new OPDSCustomLink(ICustomNetworkLink.INVALID_ID, siteName, null, null, null, links);
}
public static ZLNetworkRequest loadOPDSLinksRequest(String url, final NetworkLibrary.OnNewLinkListener listener) {
return new ZLNetworkRequest(url) {
@Override
public String handleStream(URLConnection connection, InputStream inputStream) throws IOException {
new OPDSLinkXMLReader(listener).read(inputStream);
public static String loadOPDSLinks(boolean updateNotLoad, final NetworkLibrary.OnNewLinkListener listener) {
final File dirFile = new File(Paths.networkCacheDirectory());
if (!dirFile.exists() && !dirFile.mkdirs()) {
return NetworkErrors.errorMessage("cacheDirectoryError");
}
final String fileName = "fbreader_catalogs-"
+ CATALOGS_URL.substring(CATALOGS_URL.lastIndexOf(File.separator) + 1);
boolean goodCache = false;
File oldCache = null;
ATOMUpdated cacheUpdatedTime = null;
final File catalogsFile = new File(dirFile, fileName);
if (catalogsFile.exists()) {
if (updateNotLoad) {
try {
final OPDSLinkXMLReader reader = new OPDSLinkXMLReader();
reader.read(new FileInputStream(catalogsFile));
cacheUpdatedTime = reader.getUpdatedTime();
} catch (FileNotFoundException e) {
throw new RuntimeException("That's impossible!!!", e);
}
final long diff = System.currentTimeMillis() - catalogsFile.lastModified();
final long valid = 7 * 24 * 60 * 60 * 1000; // one week in milliseconds; FIXME: hardcoded const
if (diff >= 0 && diff <= valid) {
goodCache = true;
} else {
oldCache = new File(dirFile, "_" + fileName);
oldCache.delete();
if (!catalogsFile.renameTo(oldCache)) {
catalogsFile.delete();
oldCache = null;
}
}
} else {
goodCache = true;
}
}
String error = null;
if (!goodCache) {
error = ZLNetworkManager.Instance().downloadToFile(CATALOGS_URL, catalogsFile);
}
if (error != null) {
if (oldCache == null) {
return error;
}
catalogsFile.delete();
if (!oldCache.renameTo(catalogsFile)) {
oldCache.delete();
return error;
}
} else if (oldCache != null) {
oldCache.delete();
oldCache = null;
}
try {
new OPDSLinkXMLReader(listener, cacheUpdatedTime).read(new FileInputStream(catalogsFile));
} catch (FileNotFoundException e) {
throw new RuntimeException("That's impossible!!!", e);
}
return null;
}
};
}
}

View file

@ -31,6 +31,7 @@ import org.geometerplus.fbreader.network.INetworkLink;
import org.geometerplus.fbreader.network.NetworkImage;
import org.geometerplus.fbreader.network.NetworkLibrary;
import org.geometerplus.fbreader.network.atom.ATOMLink;
import org.geometerplus.fbreader.network.atom.ATOMUpdated;
import org.geometerplus.fbreader.network.authentication.NetworkAuthenticationManager;
import org.geometerplus.fbreader.network.authentication.litres.LitResAuthenticationManager;
@ -45,8 +46,12 @@ public class OPDSLinkXMLReader extends OPDSXMLReader {
private final LinkedList<URLRewritingRule> myUrlRewritingRules = new LinkedList<URLRewritingRule>();
private HashMap<RelationAlias, String> myRelationAliases = new HashMap<RelationAlias, String>();
public LinkReader(NetworkLibrary.OnNewLinkListener listener) {
private ATOMUpdated myUpdatedTime;
private ATOMUpdated myReadAfterTime;
public LinkReader(NetworkLibrary.OnNewLinkListener listener, ATOMUpdated readAfter) {
myListener = listener;
myReadAfterTime = readAfter;
}
public void setAuthenticationType(String type) {
@ -72,6 +77,10 @@ public class OPDSLinkXMLReader extends OPDSXMLReader {
myRelationAliases.clear();
}
public ATOMUpdated getUpdatedTime() {
return myUpdatedTime;
}
private static final String ENTRY_ID_PREFIX = "urn:fbreader-org-catalog:";
public boolean processFeedEntry(OPDSEntry entry) {
@ -185,13 +194,33 @@ public class OPDSLinkXMLReader extends OPDSXMLReader {
return opdsLink;
}
public boolean processFeedMetadata(OPDSFeedMetadata feed, boolean beforeEntries) { return false; }
public void processFeedStart() {}
public void processFeedEnd() {}
public boolean processFeedMetadata(OPDSFeedMetadata feed, boolean beforeEntries) {
myUpdatedTime = feed.Updated;
if (myUpdatedTime != null && myReadAfterTime != null
&& myUpdatedTime.compareTo(myReadAfterTime) <= 0) {
return true;
}
return myListener == null; // no listener -- no need to proceed
}
public OPDSLinkXMLReader(NetworkLibrary.OnNewLinkListener listener) {
super(new LinkReader(listener));
public void processFeedStart() {
myUpdatedTime = null;
}
public void processFeedEnd() {
}
}
public OPDSLinkXMLReader() {
super(new LinkReader(null, null));
}
public OPDSLinkXMLReader(NetworkLibrary.OnNewLinkListener listener, ATOMUpdated readAfter) {
super(new LinkReader(listener, readAfter));
}
public ATOMUpdated getUpdatedTime() {
return ((LinkReader) myFeedReader).getUpdatedTime();
}
private String myFBReaderNamespaceId;

View file

@ -196,4 +196,30 @@ public class ZLNetworkManager {
}
return message.toString();
}
public final String downloadToFile(String url, final File outFile) {
return downloadToFile(url, outFile, 8192);
}
public final String downloadToFile(String url, final File outFile, final int bufferSize) {
return perform(new ZLNetworkRequest(url) {
public String handleStream(URLConnection connection, InputStream inputStream) throws IOException {
OutputStream outStream = new FileOutputStream(outFile);
try {
final byte[] buffer = new byte[bufferSize];
while (true) {
final int size = inputStream.read(buffer);
if (size <= 0) {
break;
}
outStream.write(buffer, 0, size);
}
} finally {
outStream.close();
}
return null;
}
});
}
}