package net.osmand.plus.download;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import net.osmand.IProgress;
import net.osmand.IndexConstants;
import net.osmand.PlatformUtil;
import net.osmand.osm.io.NetworkUtils;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.R;
import net.osmand.plus.Version;
import net.osmand.plus.helpers.FileNameTranslationHelper;
import net.osmand.util.Algorithms;
import org.apache.commons.logging.Log;
public class DownloadFileHelper {
private final static Log log = PlatformUtil.getLog(DownloadFileHelper.class);
private static final int BUFFER_SIZE = 32256;
protected static final int TRIES_TO_DOWNLOAD = 15;
protected static final long TIMEOUT_BETWEEN_DOWNLOADS = 8000;
private final OsmandApplication ctx;
private boolean interruptDownloading = false;
public DownloadFileHelper(OsmandApplication ctx){
this.ctx = ctx;
}
public interface DownloadFileShowWarning {
public void showWarning(String warning);
}
public static boolean isInterruptedException(IOException e) {
return e != null && e.getMessage().equals("Interrupted");
}
public InputStream getInputStreamToDownload(final URL url, final boolean forceWifi) throws IOException {
InputStream cis = new InputStream() {
byte[] buffer = new byte[BUFFER_SIZE];
int bufLen = 0;
int bufRead = 0;
int length = 0;
int fileread = 0;
int triesDownload = TRIES_TO_DOWNLOAD;
boolean notFound = false;
boolean first = true;
private InputStream is;
private void reconnect() throws IOException {
while (triesDownload > 0) {
try {
if (!first) {
log.info("Reconnecting"); //$NON-NLS-1$
try {
Thread.sleep(TIMEOUT_BETWEEN_DOWNLOADS);
} catch (InterruptedException e) {
}
}
HttpURLConnection conn = NetworkUtils.getHttpURLConnection(url);
conn.setRequestProperty("User-Agent", Version.getFullVersion(ctx)); //$NON-NLS-1$
conn.setReadTimeout(30000);
if (fileread > 0) {
String range = "bytes="+fileread + "-" + (length -1); //$NON-NLS-1$ //$NON-NLS-2$
conn.setRequestProperty("Range", range); //$NON-NLS-1$
}
conn.setConnectTimeout(30000);
log.info(conn.getResponseMessage() + " " + conn.getResponseCode()); //$NON-NLS-1$
boolean wifiConnectionBroken = forceWifi && !isWifiConnected();
if(conn.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND){
notFound = true;
break;
}
if ((conn.getResponseCode() != HttpURLConnection.HTTP_PARTIAL &&
conn.getResponseCode() != HttpURLConnection.HTTP_OK ) || wifiConnectionBroken) {
conn.disconnect();
triesDownload--;
continue;
}
is = conn.getInputStream();
if (first) {
length = conn.getContentLength();
}
first = false;
return;
} catch (IOException e) {
log.error("IOException", e); //$NON-NLS-1$
triesDownload--;
}
}
if(notFound) {
throw new IOException("File not found "); //$NON-NLS-1$
} else if(length == 0){
throw new IOException("File was not fully read"); //$NON-NLS-1$
} else if(triesDownload == 0 && length != fileread) {
throw new IOException("File was not fully read"); //$NON-NLS-1$
}
}
// use as prepare
@Override
public synchronized void reset() throws IOException {
reconnect();
}
@Override
public int read(byte[] buffer, int offset, int len) throws IOException {
if (bufLen == -1) {
return -1;
}
if (bufRead >= bufLen) {
refillBuffer();
}
if (bufLen == -1) {
return -1;
}
int av = bufLen - bufRead;
int min = Math.min(len, av);
System.arraycopy(this.buffer, bufRead, buffer, offset, min);
bufRead += min;
return min;
}
@Override
public int read() throws IOException {
int r = -1;
if(bufLen == -1) {
return -1;
}
refillBuffer();
if(bufRead < bufLen) {
byte b = buffer[bufRead++];
return b >= 0 ? b : b + 256;
}
if (length <= fileread) {
throw new IOException("File was not fully read"); //$NON-NLS-1$
}
return r;
}
private void refillBuffer() throws IOException {
boolean readAgain = bufRead >= bufLen;
while (readAgain) {
if (is == null) {
reconnect();
}
try {
readAgain = false;
bufRead = 0;
if ((bufLen = is.read(buffer)) != -1) {
fileread += bufLen;
if (interruptDownloading) {
break;
}
}
} catch (IOException e) {
if(interruptDownloading)
log.error("IOException", e); //$NON-NLS-1$
triesDownload--;
reconnect();
readAgain = true;
}
}
if (interruptDownloading) {
throw new IOException("Interrupted");
}
}
@Override
public void close() throws IOException {
if (is != null) {
is.close();
}
}
@Override
public int available() throws IOException {
if (is == null) {
reconnect();
}
return length - fileread;
}
};
cis.reset();
return cis;
}
public boolean isWifiConnected(){
return ctx.getSettings().isWifiConnected();
}
public boolean downloadFile(IndexItem.DownloadEntry de, IProgress progress,
List<File> toReIndex, DownloadFileShowWarning showWarningCallback, boolean forceWifi) throws InterruptedException {
try {
final List<InputStream> downloadInputStreams = new ArrayList<InputStream>();
URL url = new URL(de.urlToDownload); //$NON-NLS-1$
log.debug("Url downloading " + de.urlToDownload);
downloadInputStreams.add(getInputStreamToDownload(url, forceWifi));
de.fileToDownload = de.targetFile;
if(!de.unzipFolder) {
de.fileToDownload = new File(de.targetFile.getParentFile(), de.targetFile.getName() +".download");
}
unzipFile(de, progress, downloadInputStreams);
if(!de.targetFile.getAbsolutePath().equals(de.fileToDownload.getAbsolutePath())){
Algorithms.removeAllFiles(de.targetFile);
boolean renamed = de.fileToDownload.renameTo(de.targetFile);
if(!renamed) {
showWarningCallback.showWarning(ctx.getString(R.string.shared_string_io_error) + ": old file can't be deleted");
return false;
}
}
if (de.type == DownloadActivityType.VOICE_FILE){
copyVoiceConfig(de);
}
toReIndex.add(de.targetFile);
showWarningCallback.showWarning(ctx.getString(R.string.shared_string_download_successful));
return true;
} catch (IOException e) {
log.error("Exception ocurred", e); //$NON-NLS-1$
showWarningCallback.showWarning(ctx.getString(R.string.shared_string_io_error) + ": " + e.getMessage());
// Possibly file is corrupted
Algorithms.removeAllFiles(de.fileToDownload);
return false;
}
}
private void copyVoiceConfig(IndexItem.DownloadEntry de) {
File f = ctx.getAppPath("/voice/" + de.baseName + "/_config.p");
if (f.exists()) try {
InputStream is = ctx.getAssets().open("voice/" + de.baseName + "/config.p");
int size = is.available();
byte[] buffer = new byte[size];
is.read(buffer);
is.close();
FileOutputStream fos = new FileOutputStream(f);
fos.write(buffer);
fos.close();
} catch (Exception ex){
log.debug(ex.getMessage());
}
}
private void unzipFile(IndexItem.DownloadEntry de, IProgress progress, List<InputStream> is) throws IOException {
CountingMultiInputStream fin = new CountingMultiInputStream(is);
int len = (int) fin.available();
int mb = (int) (len / (1024f*1024f));
if(mb == 0) {
mb = 1;
}
String taskName = ctx.getString(R.string.shared_string_downloading) + " " +
//+ de.baseName /*+ " " + mb + " MB"*/;
FileNameTranslationHelper.getFileName(ctx, ctx.getRegions(), de.baseName);
progress.startTask(taskName, len / 1024);
if (!de.zipStream) {
copyFile(de, progress, fin, len, fin, de.fileToDownload);
} else if(de.urlToDownload.contains(".gz")) {
GZIPInputStream zipIn = new GZIPInputStream(fin);
copyFile(de, progress, fin, len, zipIn, de.fileToDownload);
} else {
if (de.unzipFolder) {
de.fileToDownload.mkdirs();
}
ZipInputStream zipIn = new ZipInputStream(fin);
ZipEntry entry = null;
boolean first = true;
while ((entry = zipIn.getNextEntry()) != null) {
if (entry.isDirectory() || entry.getName().endsWith(IndexConstants.GEN_LOG_EXT)) {
continue;
}
File fs;
if (!de.unzipFolder) {
if (first) {
fs = de.fileToDownload;
first = false;
} else {
String name = entry.getName();
// small simplification
int ind = name.lastIndexOf('_');
if (ind > 0) {
// cut version
int i = name.indexOf('.', ind);
if (i > 0) {
name = name.substring(0, ind) + name.substring(i, name.length());
}
}
fs = new File(de.fileToDownload.getParent(), name);
}
} else {
fs = new File(de.fileToDownload, entry.getName());
}
copyFile(de, progress, fin, len, zipIn, fs);
}
zipIn.close();
}
fin.close();
}
private void copyFile(IndexItem.DownloadEntry de, IProgress progress,
CountingMultiInputStream countIS, int length, InputStream toRead, File targetFile)
throws IOException {
targetFile.getParentFile().mkdirs();
FileOutputStream out = new FileOutputStream(targetFile);
try {
int read;
byte[] buffer = new byte[BUFFER_SIZE];
int remaining = length;
while ((read = toRead.read(buffer)) != -1) {
out.write(buffer, 0, read);
remaining -= countIS.getAndClearReadCount();
progress.remaining(remaining / 1024);
}
} finally {
out.close();
}
targetFile.setLastModified(de.dateModified);
}
public void setInterruptDownloading(boolean interruptDownloading) {
this.interruptDownloading = interruptDownloading;
}
public boolean isInterruptDownloading() {
return interruptDownloading;
}
private static class CountingMultiInputStream extends InputStream {
private final InputStream[] delegate;
private int count;
private int currentRead = 0;
public CountingMultiInputStream(List<InputStream> streams) {
this.delegate = streams.toArray(new InputStream[streams.size()]);
}
@Override
public int read(byte[] buffer, int offset, int length)
throws IOException {
int r = -1;
while (r == -1 && currentRead < delegate.length) {
r = delegate[currentRead].read(buffer, offset, length);
if (r == -1) {
delegate[currentRead].close();
currentRead++;
}
}
if (r > 0) {
this.count += r;
}
return r;
}
@Override
public int read() throws IOException {
if (currentRead >= delegate.length) {
return -1;
}
int r = -1;
while (r == -1 && currentRead < delegate.length) {
r = delegate[currentRead].read();
if (r == -1) {
delegate[currentRead].close();
currentRead++;
} else {
this.count++;
}
}
return r;
}
@Override
public int available() throws IOException {
int av = 0;
for(int i = currentRead; i < delegate.length; i++) {
av += delegate[i].available();
}
return av;
}
public int getAndClearReadCount() {
int last = count;
count = 0;
return last;
}
}
}