Lab 8 : Files and Threads
We will develop an application that will download a large image file from the internet and display it in our app. To avoid blocking the UI we will perform the download action in a background thread (using AsyncTask). We will use a progress bar to provide visual feedback to the user while the image is being downloaded. Once downloaded, we will downsize the image to fit into an image view widget (Android has a maximum that can be loaded, and the downloaded file size far exceeds that size). The downsizing operation will also be performed on a background thread to avoid potential for blocking the UI.
- Create a new empty activity project named Lab8
- Add a url string value to the res/values/strings.xml file. This is the URL from which we will download the large image.
<string name="url">http://sptci.com/uploads/239H.jpg</string> - Add ImageView and ProgressBar (Large) widgets to the main activity layout file (note it may be easier to use LinearLayout with vertical orientation than the default RelativeLayout for this exercise).
- Set the style property for the ProgressBar to Widget.ProgressBar.Horizontal
style="@android:style/Widget.ProgressBar.Horizontal" - Set the ImageView widget to fill the rest of the parent layout
- Set the style property for the ProgressBar to Widget.ProgressBar.Horizontal
- Edit the MainActivity class
- Add fields that will retain references to the ImageView and ProgressBar widgets loaded from the layout file. Assign these in onCreate as usual using the id values assigned in the layout file.
private ImageView imageView;
private ProgressBar progressBar;
- Add an AtomicBoolean field which will act as a condition variable that we will used to abort any on-going background work when the activity is paused.
private AtomicBoolean keepDownloading = new AtomicBoolean( true );
-
- Add a Downloader inner class that extends AsyncTask.
private class Downloader extends AsyncTask<String,Integer,String>
{
private static final String FILE_NAME = "image.jpeg";
private int rotation;
- Declare a reference (do not create/initialize) to the Downloader class in the MainActivity.
private Downloader downloader;
- Within the Downloader inner class, add a Listener inner class that listens to notifications from the downloader utility we will use. We make the Listener an inner class of Downloader for the same reasons that we make the Downloader an inner class of MainActivity - gain access to the methods and fields of the enveloping class.
private class Listener extends LoggingHttpDownloadListener
- Over-ride the Listener.notifyProgress to use the publishProgress method to update our ProgressBar.
@Override
public void notifyProgress( long value )
{
publishProgress( Integer.valueOf( (int) value ) );
}
- Over-ride Listener.notifyHeader call-back method to retrieve the Content-Length HTTP header to set the maximum value of the ProgressBar. Note that we will use the View.post method to update the widget from a background thread.
@Override
public void notifyHeader( final String name, final String value )
{
if ( "Content-Length".equals( name ) )
{
progressBar.post( new Runnable() {
@Override
public void run()
{
progressBar.setMax( parseInt( value ) );
progressBar.setProgress( 0 );
}
} );
}
}
-
- In the onPreExecute callback we will store the value of the current device rotation in a field (we will use this in doInBackground to determine what type of file we will use as the target for the image).
@Override
protected void onPreExecute()
{
rotation = getWindowManager().getDefaultDisplay().getRotation();
}
- Add a method that will download the image to the application cache directory.
private File cacheFile( final String url )
{
final FileUtilities fileUtilities = FileUtilities.getInstance();
fileUtilities.registerListener( new Listener() );
fileUtilities.registerConditionVariable( keepDownloading );
return fileUtilities.fromUrl( url, getCacheDir().getAbsolutePath(), FILE_NAME );
}
- Add a method that will download the image to the application private directory.
private File privateFile( final String url ) throws FileNotFoundException
{
final IOUtilities ioUtilities = IOUtilities.getInstance();
ioUtilities.registerListener( new Listener() );
ioUtilities.registerConditionVariable( keepDownloading );
final FileOutputStream fos = openFileOutput( FILE_NAME, Context.MODE_PRIVATE );
ioUtilities.fromUrl( url, fos );
return new File( format( "%s/%s", getFilesDir(), FILE_NAME ) );
}
- In the doInBackground method, we will use two different ways to download the file - using a cache file as destination when device in in landscape orientation, and a private file when device is in portrait orientation (just for illustrating storage to both locations). Note that we are not reusing the downloaded files, which is what a typical app would do. However, we are just exercising background processing and storage, so will not go into that.
@Override
protected String doInBackground( String... params )
{
File file = null;
try
{
switch ( rotation )
{
case Surface.ROTATION_0:
case Surface.ROTATION_180:
file = cacheFile( params[0] );
break;
default:
file = privateFile( params[0] );
break;
}
}
catch ( final Throwable t )
{
Log.e( format( "%s.doInBackground", getClass().getName() ),
format( "Error retrieving content from url: %s%n%s", params[0], t ), t );
if ( file != null ) file.delete();
}
finally
{
FileUtilities.getInstance().deregisterListener();
IOUtilities.getInstance().deregisterListener();
}
return ( file != null ) ? file.getAbsolutePath() : "";
} - In the onProgressUpdate method we will update the value displayed in the progress bar to indicate the download progress.
@Override protected void onProgressUpdate( Integer... values ) { progressBar.setProgress( values[0] ); }
- In the onPostExecute method we will execute the AsyncTask that will resize the downloaded image.
@Override
protected void onPostExecute( String s )
{
resizeImage = new ResizeImage();
downloader = null;
resizeImage.execute( s );
}
- Declare a reference (do not create/initialize) to the Downloader class in the MainActivity.
- Add a Downloader inner class that extends AsyncTask.
- Add a ResizeImage inner class to MainActivity that extends AsyncTask.
private class ResizeImage extends AsyncTask<String,Void,Bitmap>
{
private int width;
private int height;
- Declare a reference (do not create/initialize) to the ResizeImage class in MainActivity.
private ResizeImage resizeImage;
- In the onPreExecute callback method we will set fields that store the dimensions of the ImageView.
@Override
protected void onPreExecute()
{
width = imageView.getWidth();
height = imageView.getHeight();
Toast.makeText( MainActivity.this, "Decoding downloaded image", Toast.LENGTH_SHORT ).show();
}
- In the doInBackground method we will use the BitmapFactory.Options class to retrieve the size of the downloaded image. We will then use this information along with the dimensions of the ImageView to scale the Bitmap that will be displayed in the widget.
@Override
protected Bitmap doInBackground( String... params )
{
final Options options = new Options();
options.inJustDecodeBounds = true;
decodeFile( params[0], options );
final int scaleFactor = min( options.outWidth / width, options.outHeight / height );
options.inJustDecodeBounds = false;
options.inSampleSize = scaleFactor;
return decodeFile( params[0], options );
}
- In the onPostExecute callback method, we will set the Bitmap returned by doInBackground as the image for the ImageView widget.
@Override
protected void onPostExecute( Bitmap bitmap )
{
if ( bitmap != null ) imageView.setImageBitmap( bitmap );
resizeImage = null;
}
- Declare a reference (do not create/initialize) to the ResizeImage class in MainActivity.
- In the onResume callback method, we will initialize and execute the Downloader task.
@Override
protected void onResume()
{
super.onResume();
downloader = new Downloader();
downloader.execute( getResources().getString( R.string.url ) );
}
- In the onPause callback method we will cancel the tasks if they have been initialized, as well as update our condition variable to false to indicate to any running background tasks that they should stop.
@Override
protected void onPause()
{
super.onPause();
keepDownloading.set( false );
if ( downloader != null ) downloader.cancel( true );
if ( resizeImage != null ) resizeImage.cancel( true );
}
- Add fields that will retain references to the ImageView and ProgressBar widgets loaded from the layout file. Assign these in onCreate as usual using the id values assigned in the layout file.
- Request the INTERNET permission for the app by declaring a uses-permission in the manifest file.
<uses-permission android:name="android.permission.INTERNET" />
- Run the app and test the interactions. While the file is being downloaded, change the device orientation and observe the device monitor. You will see a stack trace caused by the background task aborting the download using an Exception (which is just logged).