/******************************************************************************* * Copyright 2012 Crazywater * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package de.knufficast.logic; import java.io.BufferedInputStream; 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 android.content.Context; import android.os.AsyncTask; import android.util.Pair; import de.knufficast.App; import de.knufficast.util.BooleanCallback; import de.knufficast.util.Callback; import de.knufficast.util.file.ExternalFileUtil; import de.knufficast.util.file.FileUtil; /** * An {@link AsyncTask} that downloads files to external storage and can report * its progress to callbacks. Takes two arguments in {@link #execute}: input URL * and output filename. The progress is reported in bytes downloaded and bytes * total. * * @author crazywater * */ public class DownloadTask extends AsyncTask<String, Long, String> { private final Callback<Pair<Long, Long>> progressCallback; private final BooleanCallback<Void, String> finishedCallback; private final FileUtil fileUtil; private String urlStr; private String filename; private long lastPublishTimestamp; public static final String SUCCESS = "Success"; public static final String ERROR_DATA_RANGE = "Invalid data range"; public static final String ERROR_RESPONSE_CODE = "Invalid response code"; public static final String ERROR_CONTENT_LENGTH = "Invalid content length"; public static final String ERROR_CONNECTION = "Connection error"; public static final String ERROR_OTHER = "Unknown error"; // How often progress is reported to the callback private static final long PUBLISH_INTERVAL = 250; // ms public DownloadTask(Context context, Callback<Pair<Long, Long>> progressCallback, BooleanCallback<Void, String> finishedCallback) { this.progressCallback = progressCallback; this.finishedCallback = finishedCallback; fileUtil = new ExternalFileUtil(context); } public DownloadTask(FileUtil fileUtil, Callback<Pair<Long, Long>> progressCallback, BooleanCallback<Void, String> finishedCallback) { this.progressCallback = progressCallback; this.finishedCallback = finishedCallback; this.fileUtil = fileUtil; } @Override protected String doInBackground(String... urlAndFilename) { App.get().getLockManager().lockWifi(urlAndFilename); try { if (urlAndFilename.length != 2) { throw new IllegalArgumentException("Wrong number of download arguments"); } urlStr = urlAndFilename[0]; filename = urlAndFilename[1]; // open input URL url = new URL(urlStr); HttpURLConnection.setFollowRedirects(true); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); File file = fileUtil.resolveFile(filename); long initiallyDownloaded = file.length(); boolean append = initiallyDownloaded > 0; // resume download if possible if (append) { connection.setRequestProperty("Range", "bytes=" + initiallyDownloaded + "-"); } if (!isCancelled()) { connection.connect(); if (connection.getResponseCode() / 100 == 3) { // redirect String location = connection.getHeaderField("Location"); connection = (HttpURLConnection) new URL(location).openConnection(); if (append) { connection.setRequestProperty("Range", "bytes=" + initiallyDownloaded + "-"); } connection.connect(); } if (connection.getResponseCode() == 416) { file.delete(); throw new RuntimeException(ERROR_DATA_RANGE); } // check responsecode 2xx if (connection.getResponseCode() / 100 != 2) { throw new RuntimeException(ERROR_RESPONSE_CODE); } if (!"bytes".equals(connection.getHeaderField("Accept-Ranges"))) { append = false; } // open output FileOutputStream output = new FileOutputStream(file, append); // open input InputStream input = new BufferedInputStream(connection.getInputStream()); // check content length int contentLength = connection.getContentLength(); if (!(contentLength > 0)) { input.close(); output.close(); throw new RuntimeException(ERROR_CONTENT_LENGTH); } byte data[] = new byte[1024]; int count = 0; long downloaded = initiallyDownloaded; while (!isCancelled() && (count = input.read(data)) != -1) { output.write(data, 0, count); downloaded += count; publishProgressRateLimited(downloaded, contentLength + initiallyDownloaded); } output.flush(); output.close(); input.close(); } return SUCCESS; } catch (IOException e) { return ERROR_CONNECTION; } catch (RuntimeException e) { return e.getMessage(); } finally { App.get().getLockManager().unlockWifi(urlAndFilename); } } /** * Rate-limits the progress such that it isn't reported more often than * {@link #PUBLISH_INTERVAL}. */ private void publishProgressRateLimited(long downloaded, long length) { long now = System.currentTimeMillis(); if (now - lastPublishTimestamp >= PUBLISH_INTERVAL) { lastPublishTimestamp = now; publishProgress(downloaded, length); } } @Override protected void onProgressUpdate(Long... progress) { if (progressCallback != null) { progressCallback.call(Pair.create(progress[0], progress[1])); } } @Override protected void onPostExecute(String result) { if (finishedCallback != null) { if (result == SUCCESS) { finishedCallback.success(null); } else { finishedCallback.fail(result); } } } }