/*
* Created by Angel Leon (@gubatron), Alden Torres (aldenml)
* Copyright (c) 2011, 2012, FrostWire(TM). All rights reserved.
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
package com.bt.download.android.gui.transfers;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import com.bt.download.android.core.SystemPaths;
import com.frostwire.transfers.TransferItem;
import org.apache.commons.io.FilenameUtils;
import android.os.SystemClock;
import android.util.Log;
import com.bt.download.android.R;
import com.bt.download.android.core.Constants;
import com.bt.download.android.core.HttpFetcher;
import com.bt.download.android.core.HttpFetcherListener;
import com.bt.download.android.gui.Librarian;
import com.bt.download.android.gui.services.Engine;
import com.frostwire.util.ZipUtils;
/**
* @author gubatron
* @author aldenml
*
*/
public final class HttpDownload implements DownloadTransfer {
private static final String TAG = "FW.HttpDownload";
static final int STATUS_DOWNLOADING = 1;
static final int STATUS_COMPLETE = 2;
static final int STATUS_ERROR = 3;
static final int STATUS_CANCELLED = 4;
static final int STATUS_WAITING = 5;
static final int STATUS_UNCOMPRESSING = 6;
static final int STATUS_SAVE_DIR_ERROR = 7;
private static final int SPEED_AVERAGE_CALCULATION_INTERVAL_MILLISECONDS = 1000;
private final TransferManager manager;
private final HttpDownloadLink link;
private final Date dateCreated;
private final File savePath;
private int status;
private long bytesReceived;
private long averageSpeed; // in bytes
// variables to keep the download rate of file transfer
private long speedMarkTimestamp;
private long totalReceivedSinceLastSpeedStamp;
private HttpDownloadListener listener;
HttpDownload(TransferManager manager, File savePath, HttpDownloadLink link) {
this.manager = manager;
this.link = link;
this.dateCreated = new Date();
this.savePath = new File(savePath, link.getFileName());
this.status = STATUS_DOWNLOADING;
if (savePath == null) {
this.status = STATUS_SAVE_DIR_ERROR;
}
if (!savePath.isDirectory() && !savePath.mkdirs()) {
this.status = STATUS_SAVE_DIR_ERROR;
}
}
HttpDownload(TransferManager manager, HttpDownloadLink link) {
this(manager, SystemPaths.getTorrentData(), link);
}
public HttpDownloadListener getListener() {
return listener;
}
public void setListener(HttpDownloadListener listener) {
this.listener = listener;
}
public String getDisplayName() {
return link.getDisplayName();
}
public String getStatus() {
return getStatusString(status);
}
public int getProgress() {
if (link.getSize() > 0) {
return isComplete() ? 100 : (int) ((bytesReceived * 100) / link.getSize());
} else {
return 0;
}
}
public long getSize() {
return link.getSize();
}
public Date getDateCreated() {
return dateCreated;
}
public long getBytesReceived() {
return bytesReceived;
}
public long getBytesSent() {
return 0;
}
public long getDownloadSpeed() {
return (!isDownloading()) ? 0 : averageSpeed;
}
public long getUploadSpeed() {
return 0;
}
public long getETA() {
if (link.getSize() > 0) {
long speed = getDownloadSpeed();
return speed > 0 ? (link.getSize() - getBytesReceived()) / speed : Long.MAX_VALUE;
} else {
return 0;
}
}
public boolean isComplete() {
if (bytesReceived > 0) {
return (bytesReceived == link.getSize() && status == STATUS_COMPLETE) || status == STATUS_ERROR;
} else {
return false;
}
}
public boolean isDownloading() {
return status == STATUS_DOWNLOADING;
}
public List<TransferItem> getItems() {
return Collections.emptyList();
}
public File getSavePath() {
return savePath;
}
public void cancel() {
cancel(false);
}
public void cancel(boolean deleteData) {
if (status != STATUS_COMPLETE) {
status = STATUS_CANCELLED;
}
if (status != STATUS_COMPLETE || deleteData) {
cleanup();
}
manager.remove(this);
}
public void start() {
start(0, 0);
}
int getStatusCode() {
return status;
}
/**
*
* @param delay in seconds.
* @param retry
*/
private void start(final int delay, final int retry) {
if (status == STATUS_SAVE_DIR_ERROR) {
return;
}
Engine.instance().getThreadPool().execute(new Thread(getDisplayName()) {
public void run() {
try {
status = STATUS_WAITING;
SystemClock.sleep(delay * 1000);
status = STATUS_DOWNLOADING;
String uri = link.getUrl();
new HttpFetcher(uri, 10000).save(savePath, new DownloadListener(retry));
Librarian.instance().scan(savePath);
} catch (Throwable e) {
error(e);
}
}
});
}
private String getStatusString(int status) {
int resId;
switch (status) {
case STATUS_DOWNLOADING:
resId = R.string.peer_http_download_status_downloading;
break;
case STATUS_COMPLETE:
resId = R.string.peer_http_download_status_complete;
break;
case STATUS_ERROR:
resId = R.string.peer_http_download_status_error;
break;
case STATUS_SAVE_DIR_ERROR:
resId = R.string.http_download_status_save_dir_error;
break;
case STATUS_CANCELLED:
resId = R.string.peer_http_download_status_cancelled;
break;
case STATUS_WAITING:
resId = R.string.peer_http_download_status_waiting;
break;
case STATUS_UNCOMPRESSING:
resId = R.string.http_download_status_uncompressing;
break;
default:
resId = R.string.peer_http_download_status_unknown;
break;
}
return String.valueOf(resId);
}
private void updateAverageDownloadSpeed() {
long now = System.currentTimeMillis();
if (isComplete()) {
averageSpeed = 0;
speedMarkTimestamp = now;
totalReceivedSinceLastSpeedStamp = 0;
} else if (now - speedMarkTimestamp > SPEED_AVERAGE_CALCULATION_INTERVAL_MILLISECONDS) {
averageSpeed = ((bytesReceived - totalReceivedSinceLastSpeedStamp) * 1000) / (now - speedMarkTimestamp);
speedMarkTimestamp = now;
totalReceivedSinceLastSpeedStamp = bytesReceived;
}
}
private void complete() {
boolean success = true;
String location = null;
if (link.isCompressed()) {
status = STATUS_UNCOMPRESSING;
location = FilenameUtils.removeExtension(savePath.getAbsolutePath());
success = ZipUtils.unzip(savePath, new File(location));
}
if (success) {
if (listener != null) {
listener.onComplete(this);
}
status = STATUS_COMPLETE;
manager.incrementDownloadsToReview();
Engine.instance().notifyDownloadFinished(getDisplayName(), getSavePath());
if (savePath.getAbsoluteFile().exists()) {
Librarian.instance().scan(link.isCompressed() ? new File(location) : getSavePath().getAbsoluteFile());
}
} else {
error(new Exception("Error"));
}
}
private void error(Throwable e) {
if (status != STATUS_CANCELLED) {
Log.e(TAG, String.format("Error downloading url: %s", link.getUrl()), e);
status = STATUS_ERROR;
cleanup();
}
}
private void cleanup() {
try {
savePath.delete();
} catch (Throwable tr) {
// ignore
}
}
private final class DownloadListener implements HttpFetcherListener {
private final int retry;
public DownloadListener(int retry) {
this.retry = retry;
}
public void onData(byte[] data, int length) {
bytesReceived += length;
updateAverageDownloadSpeed();
if (status == STATUS_CANCELLED) {
// ok, this is not the most elegant solution but it effectively breaks the
// download logic flow.
throw new RuntimeException("Invalid status, transfer cancelled");
}
}
public void onSuccess(byte[] body) {
complete();
}
public void onError(Throwable e, int statusCode, Map<String, String> headers) {
try {
if (statusCode == 503 && headers.containsKey("Retry-After") && retry < Constants.MAX_PEER_HTTP_DOWNLOAD_RETRIES) {
int delay = Integer.parseInt(headers.get("Retry-After"));
if (delay > 0) {
start(delay, retry + 1);
} else {
error(e);
}
} else {
error(e);
}
} catch (Throwable tr) {
error(tr);
}
}
}
static void simpleHTTP(String url, OutputStream out) throws Throwable {
simpleHTTP(url, out, 1000);
}
static void simpleHTTP(String url, OutputStream out, int timeout) throws Throwable {
URL u = new URL(url);
URLConnection con = u.openConnection();
con.setConnectTimeout(timeout);
con.setReadTimeout(timeout);
InputStream in = con.getInputStream();
try {
byte[] b = new byte[1024];
int n = 0;
while ((n = in.read(b, 0, b.length)) != -1) {
out.write(b, 0, n);
}
} finally {
try {
out.close();
} catch (Throwable e) {
// ignore
}
try {
in.close();
} catch (Throwable e) {
// ignore
}
}
}
@Override
public String getDetailsUrl() {
return link.getUrl();
}
}