package edu.vanderbilt.cs282.feisele.lab05;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
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 so 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 Logger logger = LoggerFactory
.getLogger("class.fragment.download");
/** 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 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;
/**
* An extension to the basic connection which holds information about the
* state of the connection and the service binding.
* <p>
* This cannot be implemented as a generic as there is no interface defining
* the asInterface() method on the stub. (or at least I don't know how)
*/
static abstract public class DownloadServiceConnection<T> implements
ServiceConnection {
protected T service;
protected boolean isBound;
public void onServiceConnected(ComponentName className, IBinder iservice) {
logger.debug("call service connected");
this.isBound = true;
}
public void onServiceDisconnected(ComponentName name) {
this.isBound = false;
}
};
/**
* provide implementation for the DownloadCall class.
*/
static private DownloadServiceConnection<DownloadCall> syncConnection = new DownloadServiceConnection<DownloadCall>() {
@Override
public void onServiceConnected(ComponentName className, IBinder iservice) {
super.onServiceConnected(className, iservice);
this.service = DownloadCall.Stub.asInterface(iservice);
}
};
/**
* provide implementation for the DownloadCall class.
*/
static private DownloadServiceConnection<DownloadRequest> asyncConnection = new DownloadServiceConnection<DownloadRequest>() {
@Override
public void onServiceConnected(ComponentName className, IBinder iservice) {
super.onServiceConnected(className, iservice);
this.service = DownloadRequest.Stub.asInterface(iservice);
}
};
/**
* This ensures that the controlling activity implements the callback
* interface.
* <p>
* The intents could be implicit... <code>
final Intent syncIntent = new Intent(DownloadCall.class.getName());
final Intent asyncIntent = new Intent(DownloadRequest.class.getName());
</code> This would also require changes
* to the AndroidManifest.xml
* <p>
* <code>
<intent-filter>
<action android:name="edu.vanderbilt.cs282.feisele.DownloadCall" />
</intent-filter>
</code> ...and... <code>
<intent-filter>
<action android:name="edu.vanderbilt.cs282.feisele.DownloadRequest" />
</intent-filter>
</code> ... but in this case we will be explicit.
*/
@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());
}
this.explicitBindService(DownloadFragment.syncConnection,
DownloadBoundServiceSync.class);
this.explicitBindService(DownloadFragment.asyncConnection,
DownloadBoundServiceAsync.class);
}
/**
* Generic helper method for binding to a service.
*
* @param <T>
*/
private void explicitBindService(ServiceConnection conn,
Class<? extends DownloadBoundService> clazz) {
logger.debug("bining to service explicitly {} {}", conn, clazz);
final Intent intent = new Intent(this.context, clazz);
this.context.bindService(intent, conn, Context.BIND_AUTO_CREATE);
}
/**
* Disable the ability to receiver download completion messages.
*/
@Override
public void onDetach() {
super.onDetach();
this.eventHandler = null;
if (DownloadFragment.syncConnection.isBound)
this.context.unbindService(DownloadFragment.syncConnection);
if (DownloadFragment.asyncConnection.isBound)
this.context.unbindService(DownloadFragment.asyncConnection);
}
/**
* 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, true);
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) {
logger.error("cannot load a bitmap asset");
}
}
}
/**
* A new bitmap image has been generated. Update the bitmap and the
* ImageView.
*
* @param result
*/
private void setBitmap(Bitmap result) {
try {
synchronized (this.downloadPending) {
this.downloadPending.set(false);
this.bitmap = result;
if (this.bitmap != null) {
this.getActivity().runOnUiThread(new Runnable() {
final DownloadFragment master = DownloadFragment.this;
public void run() {
master.bitmapImage.setImageBitmap(master.bitmap);
}
});
}
}
} catch (IllegalArgumentException ex) {
logger.error("can not set bitmap image");
}
}
/**
* Load the appropriate bitmap into the image view. Notice that the file is
* deleted after it is loaded. This is not an optimal solution but I could
* not get the ParcelFileDescriptor to operate as I wanted.
*
* @param bitmapFilePath
*/
public void loadBitmap(File bitmapFile) {
if (bitmapFile == null) {
logger.error("null file");
return;
}
InputStream fileStream = null;
try {
fileStream = new FileInputStream(bitmapFile);
final Bitmap bitmap = BitmapFactory.decodeStream(fileStream);
this.setBitmap(bitmap);
} catch (FileNotFoundException ex) {
logger.error("could not load file " + bitmapFile, ex);
} finally {
if (fileStream != null)
try {
fileStream.close();
} catch (IOException e) {
logger.error("could not close file " + bitmapFile);
}
}
bitmapFile.delete();
}
/**
* The file path as a string.
*
* @param imageFilePath
*/
public void loadBitmap(String imageFilePath) {
if (imageFilePath == null) {
logger.error("null file path");
return;
}
final File bitmapFile = new File(imageFilePath);
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 reportDownloadFault(int errorCode) {
if (this.eventHandler == null)
return;
this.reportDownloadFault(this.context.getResources().getString(
errorCode));
}
private void reportDownloadComplete() {
if (this.eventHandler == null)
return;
this.eventHandler.onComplete();
}
/**
* Sync AIDL model ("Run Sync AIDL").
* <p>
* In this model the DownloadActivity spawns a Thread, binds to a
* DownloadBoundServiceSync process, and uses a synchronous two-way AIDL
* method invocation to request that this service:
* <ol>
* <li>download a designated bitmap file</li>
* <li>store it in the</li> Android file system, and</li>
* <li>use the return value from the synchronous AIDL method invocation to
* return the filename back to thread, which opens the file and causes the
* bitmap to be displayed on the screen.
* </ol>
*
* @param view
*/
public void downloadSyncAidl(final Uri uri) {
if (!DownloadFragment.syncConnection.isBound) {
logger.warn("service not bound");
return;
}
final Runnable runner = new Runnable() {
final DownloadFragment master = DownloadFragment.this;
public void run() {
logger.debug("download thread");
try {
final String imageFilePath = DownloadFragment.syncConnection.service
.downloadImage(uri);
master.loadBitmap(imageFilePath);
master.reportDownloadComplete();
} catch (RemoteException ex) {
master.reportDownloadFault(R.string.error_downloading_url);
ex.printStackTrace();
}
}
};
new Thread(runner, "download sync thread").start();
}
/**
* Asynchronous AIDL model ("Run Async AIDL").
* <p>
* In this model the DownloadActivity binds to a DownloadBoundServiceAsync
* process and uses an asynchronous one-way AIDL method invocation to
* request that this service:
* <ol>
* <li>download a designated bitmap file,</li>
* <li>store it in the Android file system, and</li>
* <li>use another one-way AIDL interface passed as a parameter to the
* original one-way AIDL method invocation to return the filename back to
* DownloadActivity as a callback, which opens the file and causes the
* bitmap to be displayed on the screen.
* </ol>
*
* @param view
*/
public void downloadAsyncAidl(Uri uri) {
if (!DownloadFragment.asyncConnection.isBound) {
logger.warn("async service not bound");
return;
}
logger.debug("download async aidl");
try {
DownloadFragment.asyncConnection.service.downloadImage(uri,
callback);
} catch (RemoteException ex) {
logger.error("download async aidl", ex);
}
}
private final DownloadCallback.Stub callback = new DownloadCallback.Stub() {
private DownloadFragment master = DownloadFragment.this;
public void sendPath(String imageFilePath) throws RemoteException {
master.loadBitmap(imageFilePath);
master.reportDownloadComplete();
}
public void sendFault(String msg) throws RemoteException {
master.reportDownloadFault(msg);
}
};
}