package edu.vanderbilt.cs282.feisele; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.atomic.AtomicBoolean; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.AssetManager; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.Toast; /** * The Fragment is the android user interface component. Fragments can have a * lifetime which spans the demise of its parent activity. In this particular * case the fragment is attached to an effective clone of its original activity. * <p> * A fragment does not need to persist its view elements but in this * implementation it does. * <p> * There is a some concern of the bitmap being updated concurrently some there * is protection around the bitmap and its image view. * <p> * The following indicate the tolerated changes. * <dl> * <dt>orientation</dt> * <dt>startActivity</dt> * <dd>configuration change doesn't handle properly</dt> * <dt>keyboard</dt> * </dl> * <p> * * @author "Fred Eisele" <phreed@gmail.com> * */ public class DownloadFragment extends LifecycleLoggingFragment { static private final String TAG = "Threaded Download Fragment"; /** my oldest daughter */ static private final String DEFAULT_PORT_IMAGE = "raquel_eisele_port_2012.jpg"; static private final String DEFAULT_LAND_IMAGE = "raquel_eisele_land_2012.jpg"; private Bitmap bitmap = null; private ImageView bitmapImage = null; public static Handler msgHandler = null; public AtomicBoolean downloadPending = new AtomicBoolean(false); private BroadcastReceiver onEvent = null; private Context context = null; /** * In order for a fragment to be useful it must have a containing activity. * In many cases it is important for the fragment to communicate with that * activity. A common case is when the fragment generates an event in which * the other components of the UI may be interested. That is the case here, * when the fragment detects a failure related to the uri it received the * uri edit field should be marked in such a way that the operator is * notified. It would be presumptuous for the fragment to post the error * itself so it calls a method implemented by the controlling activity. In * keeping with the fragment being a UI component it relies on the * <p> * * @see http://developer.android.com/guide/components/fragments.html# * CommunicatingWithActivity */ public interface OnDownloadHandler { public void onFault(CharSequence msg); public void onComplete(); } private OnDownloadHandler eventHandler = null; /** * This ensures that the controlling activity implements the callback * interface. */ @Override public void onAttach(Activity activity) { super.onAttach(activity); this.context = activity.getApplicationContext(); /** for reporting back to the controlling activity */ try { this.eventHandler = (OnDownloadHandler) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement " + OnDownloadHandler.class.getName()); } /** for responding to messenger responses */ DownloadFragment.msgHandler = initMsgHandler(this); /** for the broadcast intent */ this.onEvent = new BroadcastReceiver() { final private DownloadFragment master = DownloadFragment.this; public void onReceive(Context ctxt, Intent intent) { Log.d(TAG, "received broadcast bitmap"); final String faultMsg = intent .getStringExtra(ThreadedDownloadService.RESULT_FAULT); if (faultMsg != null) { Toast.makeText(master.context, faultMsg, Toast.LENGTH_LONG).show(); master.reportDownloadFault(faultMsg); return; } final String bitmapFileString = intent .getStringExtra(ThreadedDownloadService.RESULT_BITMAP_FILE); master.loadBitmap(bitmapFileString); master.reportDownloadComplete(); } }; final IntentFilter filter = new IntentFilter( ThreadedDownloadService.BROADCAST_INTENT_ACTION); this.context.registerReceiver(this.onEvent, filter); } /** * Disable the ability to receiver download completion messages. */ @Override public void onDetach() { super.onDetach(); this.eventHandler = null; this.context.unregisterReceiver(this.onEvent); } /** * The bitmap field serves double duty. It serves to hold the downloaded * bitmap image and, when null, it acts as a flag to indicate that the * default image should be used. */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); this.setRetainInstance(true); final View result = inflater.inflate(R.layout.downloaded_image, container, false); this.bitmapImage = (ImageView) result.findViewById(R.id.current_image); synchronized (this.downloadPending) { if (this.bitmap == null) { this.resetImage(null); } else { this.bitmapImage.setImageBitmap(this.bitmap); } } return result; } /** * Load the default image from the assets. Just for fun a different asset is * loaded depending on the screen orientation. * * @param view */ public void resetImage(View view) { final AssetManager am = this.getActivity().getAssets(); final InputStream is; try { switch (this.getResources().getConfiguration().orientation) { case Configuration.ORIENTATION_LANDSCAPE: is = am.open(DEFAULT_LAND_IMAGE); break; default: is = am.open(DEFAULT_PORT_IMAGE); } } catch (IOException ex) { Toast.makeText(this.getActivity(), R.string.error_opening_default_image, Toast.LENGTH_LONG) .show(); return; } try { synchronized (this.downloadPending) { this.bitmap = null; final Bitmap bitmap = BitmapFactory.decodeStream(is); this.bitmapImage.setImageBitmap(bitmap); } } finally { try { is.close(); } catch (IOException ex) { Log.e(TAG, "cannot load a bitmap asset"); } } } /** * A new bitmap image has been generated. Update the * * @param result */ private void setBitmap(Bitmap result) { try { synchronized (this.downloadPending) { this.downloadPending.set(false); this.bitmap = result; if (this.bitmap != null) { this.bitmapImage.setImageBitmap(this.bitmap); } } } catch (IllegalArgumentException ex) { Log.e(TAG, "can not set bitmap image"); } } /** * Load the appropriate bitmap into the image view. * * @param bitmapFilePath */ public void loadBitmap(File bitmapFile) { if (bitmapFile == null) { Log.e(TAG, "null file"); return; } InputStream fileStream = null; try { fileStream = new FileInputStream(bitmapFile); final Bitmap bitmap = BitmapFactory.decodeStream(fileStream); this.setBitmap(bitmap); } catch (FileNotFoundException ex) { Log.e(TAG, "could not load file " + bitmapFile, ex); } finally { if (fileStream != null) try { fileStream.close(); } catch (IOException e) { Log.e(TAG, "could not close file " + bitmapFile); } } bitmapFile.delete(); } /** * The file path as a string. * * @param bitmapFilePath */ public void loadBitmap(String bitmapFilePath) { if (bitmapFilePath == null) { Log.e(TAG, "null file path"); return; } final File bitmapFile = new File(bitmapFilePath); this.loadBitmap(bitmapFile); } /** * Report problems with downloading the image back to the parent activity. * * @param errorMsg */ private void reportDownloadFault(CharSequence errorMsg) { if (this.eventHandler == null) return; this.eventHandler.onFault(errorMsg); } private void reportDownloadComplete() { if (this.eventHandler == null) return; this.eventHandler.onComplete(); } /** * The valid message types for the handler. */ protected static enum DownloadState { /** * indicate that the download is complete and so the bitmap should be * displayed */ SET_BITMAP, /** * indicate that the download has failed and so the default image should * be shown */ SET_ERROR; static DownloadState[] lookup = DownloadState.values(); } /** * Initialized the handler used by the run message method. * * @return */ private static Handler initMsgHandler(final DownloadFragment master) { return new Handler() { @Override public void handleMessage(Message msg) { switch (DownloadState.lookup[msg.what]) { case SET_BITMAP: { final Bundle bundle = msg.getData(); final String bitmapFileString = bundle .getString(ThreadedDownloadService.RESULT_BITMAP_FILE); master.loadBitmap(bitmapFileString); master.reportDownloadComplete(); } break; case SET_ERROR: { master.reportDownloadFault((CharSequence) msg.obj); break; } } } }; } }