Sans Pareil Technologies, Inc.

Key To Your Business

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
  • 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 );
            }
      • 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;
          }
      • 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 );
        }
    • 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).