package edu.vanderbilt.cs282.feisele; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.UnknownHostException; import java.util.HashMap; import java.util.Map; import android.annotation.TargetApi; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.app.Service; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Message; import android.os.Messenger; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.RemoteException; import android.util.Log; import android.util.SparseArray; public class ThreadedDownloadService extends LifecycleLoggingService { static private final String TAG = "Threaded Download Service"; static public final String DOWNLOAD_METHOD = "edu.vanderbilt.cs282.feisele.DOWNLOAD_METHOD"; static public final String PENDING_INTENT_KEY = "edu.vanderbilt.cs282.feisele.pending_intent"; static public final String BROADCAST_INTENT_ACTION = "edu.vanderbilt.cs282.feisele.DOWNLOAD_COMPLETE_ACTION"; static public final String MESSENGER_KEY = "edu.vanderbilt.cs282.feisele.broadcast_intent"; static public final int RESULT_BITMAP_ID = 12; static public final String RESULT_BITMAP_FILE = "edu.vanderbilt.cs282.feisele.bitmap_file_descriptor"; static public final String RESULT_FAULT = "edu.vanderbilt.cs282.feisele.download_fault"; static public final int MAXIMUM_SIZE = 100; static public enum DownloadMethod implements Parcelable { /** Thread and Messenger model */ THREAD_MESSENGER(0x01, "edu.vanderbilt.cs282.feisele.THREAD_MESSENGER"), /** Thread and PendingIntent model */ THREAD_PENDING_INTENT(0x02, "edu.vanderbilt.cs282.feisele.THREAD_PENDING_INTENT"), /** AsyncTask with BroadcastReceiver */ ASYNC_TASK_BROADCAST(0x03, "edu.vanderbilt.cs282.feisele.ASYNC_TASK_BROADCAST"); final public int nominal; final public String key; private DownloadMethod(int nominal, String key) { this.nominal = nominal; this.key = key; } static final public Map<String, DownloadMethod> lookup = new HashMap<String, DownloadMethod>( 3); static final public SparseArray<DownloadMethod> lookupByNominal = new SparseArray<DownloadMethod>( 3); static { for (DownloadMethod action : DownloadMethod.values()) { DownloadMethod.lookupByNominal.put(action.nominal, action); DownloadMethod.lookup.put(action.key, action); } } public int describeContents() { return 0; } public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.nominal); } public static final Creator<DownloadMethod> CREATOR = new Creator<DownloadMethod>() { public DownloadMethod createFromParcel(Parcel source) { return DownloadMethod.lookupByNominal.get(source.readInt()); } public DownloadMethod[] newArray(int size) { return new DownloadMethod[size]; } }; public Parcelable asParcelable() { return (Parcelable) this; } } /** * The workhorse for the class. Download the provided image url. If there is * a problem an exception is raised and the calling method is expected to * handle it in an appropriate manner. * * @param uri * the thing to download */ private Bitmap downloadBitmap(Uri uri) throws FailedDownload, FileNotFoundException, IOException { Log.d(TAG, "downloadBitmap:"); final Bitmap bitmap; try { final String scheme = uri.getScheme(); if ("http".equals(scheme)) { final URL url = new URL(uri.toString()); final InputStream is = url.openConnection().getInputStream(); bitmap = BitmapFactory.decodeStream(is); } else if ("content".equals(scheme)) { final InputStream detection = this.getContentResolver() .openInputStream(uri); final BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options(); onlyBoundsOptions.inJustDecodeBounds = true; onlyBoundsOptions.inDither = true; onlyBoundsOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; BitmapFactory.decodeStream(detection, null, onlyBoundsOptions); detection.close(); Log.d(TAG, "bitmap size:" + Integer.toString(onlyBoundsOptions.outWidth) + " : " + Integer.toString(onlyBoundsOptions.outWidth)); if (onlyBoundsOptions.outWidth == -1) return null; if (onlyBoundsOptions.outHeight == -1) return null; final int majorSize = Math.max(onlyBoundsOptions.outHeight, onlyBoundsOptions.outWidth); final int ratio = (majorSize < MAXIMUM_SIZE) ? 1 : (int) Math .floor(majorSize / MAXIMUM_SIZE); final BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); final int base2Scale = Integer.highestOneBit(ratio); bitmapOptions.inSampleSize = (base2Scale < 1) ? 1 : base2Scale; bitmapOptions.inDither = true;// optional bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; final InputStream input = this.getContentResolver() .openInputStream(uri); bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions); input.close(); } else { return null; } Log.d(TAG, "bitmap size [" + Integer.toString(bitmap.getWidth()) + " : " + Integer.toString(bitmap.getHeight()) + "]"); return bitmap; } catch (UnknownHostException ex) { Log.w(TAG, "download failed bad host", ex); throw new FailedDownload(this.getResources().getText( R.string.error_downloading_url)); } catch (IOException ex) { Log.w(TAG, "download failed ?", ex); throw new FailedDownload(this.getResources().getText( R.string.error_downloading_url)); } } /** * An exception class used when there is a problem with the download. */ /* package */static class FailedDownload extends Exception { private static final long serialVersionUID = 6673968049922918951L; final public CharSequence msg; @Override public String getMessage() { return new StringBuilder().append(this.msg).toString(); } public FailedDownload(CharSequence msg) { super(); this.msg = msg; } } /** * <p> * AsyncTask with BroadcastReceiver model ("Run Async Receiver"). In this * model the ThreadedDownloadService process executes an AsyncTask that * downloads the designated bitmap file, stores it in the Android file * system, and use sendBroadcast() to send the filename back to a * BroadcastReceiver in the DownloadActivity process, which opens the file * and displays the bitmap on the screen. * * @param intent */ public int downloadWithAsyncTaskViaBroadcastIntent(final Uri uri, final Bundle extras) { Log.d(TAG, "downloadWithAsyncTaskViaBroadcastIntent"); (new AsyncTask<Uri, Void, Bitmap>() { private final ThreadedDownloadService master = ThreadedDownloadService.this; private volatile String faultMessage = null; @Override protected Bitmap doInBackground(Uri... params) { if (params.length < 1) return null; final Uri uri = params[0]; try { return master.downloadBitmap(uri); } catch (FailedDownload ex) { this.faultMessage = ex.getMessage(); } catch (FileNotFoundException ex) { this.faultMessage = ex.getLocalizedMessage(); } catch (IOException ex) { this.faultMessage = ex.getLocalizedMessage(); } return null; } /** * Provide the down loaded image as a file descriptor. */ @Override protected void onPostExecute(Bitmap result) { final Intent send = new Intent(BROADCAST_INTENT_ACTION); if (result == null) { send.putExtra(RESULT_FAULT, this.faultMessage); master.sendBroadcast(send); return; } final File bitmapFile = master.storeBitmap(result); try { final String bitmapFilePath = bitmapFile.getCanonicalPath(); Log.d(TAG, "bitmap file name " + bitmapFilePath); send.putExtra(RESULT_BITMAP_FILE, bitmapFilePath); } catch (IOException ex) { Log.w(TAG, "could not find file " + bitmapFile.toString(), ex); } master.sendBroadcast(send); } }).execute(uri); return Service.START_NOT_STICKY; } /** * Start a new thread to perform the download. Once the download is * completed the UI thread is notified via a post to on the handler. * <p> * Thread and Messenger model ("Run Thread Messenger"). In this model the * ThreadedDownloadService process starts a thread that downloads the * designated bitmap file, stores it in the Android file system, and use a * Messenger to send the filename back to the DownloadActivity process, * which opens the file and displays the bitmap on the screen. * * @param uri * @return */ public int downloadWithThreadViaMessage(final Uri uri, final Bundle extras) { Log.d(TAG, "downloadWithThreadViaMessage"); if (extras == null) return Service.START_NOT_STICKY; (new Thread(null, new Runnable() { private final ThreadedDownloadService master = ThreadedDownloadService.this; final Uri uri_ = uri; final Messenger messenger = extras.getParcelable(MESSENGER_KEY); public void run() { final Message msg = Message.obtain(); final Bundle bundle = new Bundle(); try { final Bitmap bitmap = master.downloadBitmap(uri_); if (bitmap == null) { msg.what = DownloadFragment.DownloadState.SET_ERROR .ordinal(); bundle.putString(RESULT_FAULT, "bitmap null"); return; } final File bitmapFile = master.storeBitmap(bitmap); final String bitmapFilePath = bitmapFile.getCanonicalPath(); Log.d(TAG, "bitmap file name " + bitmapFilePath); bundle.putString(RESULT_BITMAP_FILE, bitmapFilePath); msg.setData(bundle); msg.what = DownloadFragment.DownloadState.SET_BITMAP .ordinal(); return; } catch (FailedDownload ex) { msg.what = DownloadFragment.DownloadState.SET_ERROR .ordinal(); bundle.putString(RESULT_FAULT, ex.getMessage()); } catch (FileNotFoundException ex) { msg.what = DownloadFragment.DownloadState.SET_ERROR .ordinal(); bundle.putString(RESULT_FAULT, ex.getMessage()); } catch (IOException ex) { msg.what = DownloadFragment.DownloadState.SET_ERROR .ordinal(); bundle.putString(RESULT_FAULT, ex.getMessage()); } finally { msg.setData(bundle); try { messenger.send(msg); } catch (RemoteException ex) { Log.e(TAG, "could not send fault", ex); } } } })).start(); return Service.START_NOT_STICKY; } /** * Make use of the message handler to keep the UI updated. Thread and * PendingIntent model ("Run Thread Pending Intent"). In this model the * ThreadedDownloadService process starts a thread that downloads the * designated bitmap file, stores it in the Android file system, and use a * PendingIntent to send the filename back to the DownloadActivity process * via it's onActivityResult() method, which opens the file and displays the * bitmap on the screen. * * @return * */ public int downloadWithThreadViaPendingIntent(final Uri uri, final Bundle extras) { Log.d(TAG, "downloadWithThreadViaPendingIntent"); if (extras == null) return Service.START_NOT_STICKY; final Object piobj = extras.get(PENDING_INTENT_KEY); if (!(piobj instanceof PendingIntent)) { return Service.START_NOT_STICKY; } final PendingIntent pendingIntent = (PendingIntent) piobj; (new Thread(null, new Runnable() { private final ThreadedDownloadService master = ThreadedDownloadService.this; private final Uri uri_ = uri; public void run() { try { final Bitmap bitmap = master.downloadBitmap(uri_); final File bitmapFile = master.storeBitmap(bitmap); final String bitmapFilePath = bitmapFile.getCanonicalPath(); pendingIntent.send(master, RESULT_BITMAP_ID, new Intent() .putExtra(RESULT_BITMAP_FILE, bitmapFilePath)); } catch (FileNotFoundException ex) { ex.printStackTrace(); } catch (FailedDownload ex) { try { pendingIntent.send(master, RESULT_BITMAP_ID, null); } catch (CanceledException ex1) { Log.e(TAG, "download failed and report failed", ex1); } } catch (IOException ex) { Log.e(TAG, "download io exception", ex); } catch (CanceledException ex) { // TODO Auto-generated catch block ex.printStackTrace(); } } })).start(); return Service.START_NOT_STICKY; } /** * In order to preserve security for this object only a file descriptor is * provided. The file is immediately deleted. * * @param bitmap * @return */ @TargetApi(9) protected File storeBitmap(Bitmap bitmap) { final File cacheDir = this.getCacheDir(); File tempFile = null; try { tempFile = File.createTempFile("download", "tmp", cacheDir); final ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 40, outBytes); final FileOutputStream fo = new FileOutputStream(tempFile); fo.write(outBytes.toByteArray()); fo.close(); outBytes.close(); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO) { tempFile.setReadable(true, false); tempFile.setWritable(true, false); } return tempFile; } catch (IOException ex) { Log.e(TAG, "could not write bitmap file " + tempFile); } return null; } /** * In order to preserve security for this object only a file descriptor is * provided. The file is immediately deleted. * * @param bitmap * @return */ protected ParcelFileDescriptor storeBitmapGetPFD(Bitmap bitmap) { final File tempFile = this.storeBitmap(bitmap); try { final ParcelFileDescriptor pdf = ParcelFileDescriptor.open( tempFile, ParcelFileDescriptor.MODE_READ_ONLY); // tempFile.delete(); return pdf; } catch (IOException ex) { } return null; } /** * This method * <ol> * <li>receives the request from the application/fragment, * <li>determines which type of processing is desired * <li>calls the appropriate method with the intent. * </ol> * * @param intent * @param flags * @param startId * @return */ @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "onStartCommand: Received start id " + startId + ": " + intent); final DownloadMethod method = intent .getParcelableExtra(DOWNLOAD_METHOD); if (method == null) { Log.e(TAG, "invalid action supplied"); return Service.START_NOT_STICKY; } final Uri uri = intent.getData(); if (uri == null) { Log.e(TAG, "null uri provided"); return Service.START_NOT_STICKY; } Log.d(TAG, "process " + method + " : " + uri.toString()); final Bundle extras = intent.getExtras(); switch (method) { case THREAD_MESSENGER: return this.downloadWithThreadViaMessage(uri, extras); case THREAD_PENDING_INTENT: return this.downloadWithThreadViaPendingIntent(uri, extras); case ASYNC_TASK_BROADCAST: return this.downloadWithAsyncTaskViaBroadcastIntent(uri, extras); } return Service.START_NOT_STICKY; } }