package net.hockeyapp.android.tasks; import android.graphics.Bitmap; import android.os.AsyncTask; import android.os.Build; import android.os.Handler; import android.os.Message; import net.hockeyapp.android.Constants; import net.hockeyapp.android.objects.FeedbackAttachment; import net.hockeyapp.android.utils.AsyncTaskUtils; import net.hockeyapp.android.utils.HockeyLog; import net.hockeyapp.android.utils.ImageUtils; import net.hockeyapp.android.views.AttachmentView; import java.io.BufferedInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.util.LinkedList; import java.util.Queue; /** * <h3>Description</h3> * * Singleton class to queue attachment downloads. * */ public class AttachmentDownloader { /** * AttachmentDownloaderHolder is loaded on the first execution of AttachmentDownloader.getInstance() * or the first access to FeedbackParserHolder.INSTANCE, not before. */ private static class AttachmentDownloaderHolder { public static final AttachmentDownloader INSTANCE = new AttachmentDownloader(); } public static AttachmentDownloader getInstance() { return AttachmentDownloaderHolder.INSTANCE; } private Queue<DownloadJob> queue; private boolean downloadRunning; private AttachmentDownloader() { this.queue = new LinkedList<DownloadJob>(); this.downloadRunning = false; } public void download(FeedbackAttachment feedbackAttachment, AttachmentView attachmentView) { queue.add(new DownloadJob(feedbackAttachment, attachmentView)); downloadNext(); } private void downloadNext() { if (downloadRunning) { return; } DownloadJob downloadJob = queue.peek(); if (downloadJob != null) { DownloadTask downloadTask = new DownloadTask(downloadJob, new Handler() { @Override public void handleMessage(Message msg) { final DownloadJob retryCandidate = queue.poll(); if (!retryCandidate.isSuccess() && retryCandidate.consumeRetry()) { this.postDelayed(new Runnable() { @Override public void run() { queue.add(retryCandidate); downloadNext(); } }, 3000); } downloadRunning = false; downloadNext(); } }); downloadRunning = true; AsyncTaskUtils.execute(downloadTask); } } /** * Holds everything needed for a download process. */ private static class DownloadJob { private final FeedbackAttachment feedbackAttachment; private final AttachmentView attachmentView; private boolean success; private int remainingRetries; private DownloadJob(FeedbackAttachment feedbackAttachment, AttachmentView attachmentView) { this.feedbackAttachment = feedbackAttachment; this.attachmentView = attachmentView; this.success = false; this.remainingRetries = 2; } public FeedbackAttachment getFeedbackAttachment() { return feedbackAttachment; } public AttachmentView getAttachmentView() { return attachmentView; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public boolean hasRetry() { return remainingRetries > 0; } public boolean consumeRetry() { return --remainingRetries < 0 ? false : true; } } /** * The AsyncTask that downloads the image and the updates the view. */ private static class DownloadTask extends AsyncTask<Void, Integer, Boolean> { private final DownloadJob downloadJob; private final Handler handler; private File dropFolder; private Bitmap bitmap; private int bitmapOrientation; public DownloadTask(DownloadJob downloadJob, Handler handler) { this.downloadJob = downloadJob; this.handler = handler; this.dropFolder = Constants.getHockeyAppStorageDir(); this.bitmap = null; this.bitmapOrientation = ImageUtils.ORIENTATION_PORTRAIT; // default } @Override protected void onPreExecute() { } @Override protected Boolean doInBackground(Void... args) { FeedbackAttachment attachment = downloadJob.getFeedbackAttachment(); if (attachment.isAvailableInCache()) { HockeyLog.error("Cached..."); loadImageThumbnail(); return true; } else { HockeyLog.error("Downloading..."); boolean success = downloadAttachment(attachment.getUrl(), attachment.getCacheId()); if (success) { loadImageThumbnail(); } return success; } } @Override protected void onProgressUpdate(Integer... values) { } @Override protected void onPostExecute(Boolean success) { AttachmentView attachmentView = downloadJob.getAttachmentView(); downloadJob.setSuccess(success); if (success) { attachmentView.setImage(bitmap, bitmapOrientation); } else { if (!downloadJob.hasRetry()) { attachmentView.signalImageLoadingError(); } } handler.sendEmptyMessage(0); } private void loadImageThumbnail() { try { String filename = downloadJob.getFeedbackAttachment().getCacheId(); AttachmentView attachmentView = downloadJob.getAttachmentView(); bitmapOrientation = ImageUtils.determineOrientation(new File(dropFolder, filename)); int width = bitmapOrientation == ImageUtils.ORIENTATION_LANDSCAPE ? attachmentView.getWidthLandscape() : attachmentView.getWidthPortrait(); int height = bitmapOrientation == ImageUtils.ORIENTATION_LANDSCAPE ? attachmentView.getMaxHeightLandscape() : attachmentView.getMaxHeightPortrait(); bitmap = ImageUtils.decodeSampledBitmap(new File(dropFolder, filename), width, height); } catch (IOException e) { e.printStackTrace(); bitmap = null; } } private boolean downloadAttachment(String urlString, String filename) { try { URL url = new URL(urlString); URLConnection connection = createConnection(url); connection.connect(); int lengthOfFile = connection.getContentLength(); String status = connection.getHeaderField("Status"); if (status != null) { if (!status.startsWith("200")) { return false; } } File file = new File(dropFolder, filename); InputStream input = new BufferedInputStream(connection.getInputStream()); OutputStream output = new FileOutputStream(file); byte data[] = new byte[1024]; int count = 0; long total = 0; while ((count = input.read(data)) != -1) { total += count; publishProgress((int) (total * 100 / lengthOfFile)); output.write(data, 0, count); } output.flush(); output.close(); input.close(); return (total > 0); } catch (IOException e) { e.printStackTrace(); return false; } } private URLConnection createConnection(URL url) throws IOException { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.addRequestProperty("User-Agent", Constants.SDK_USER_AGENT); connection.setInstanceFollowRedirects(true); /* connection bug workaround for SDK<=2.x */ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD) { connection.setRequestProperty("connection", "close"); } return connection; } } }