Java tutorial
/** Copyright 2014-2016 Alan G. Downie Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package uk.org.downiesoft.slideshow; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.PointF; import android.graphics.drawable.BitmapDrawable; import android.os.AsyncTask; import android.os.AsyncTask.Status; import android.preference.PreferenceManager; import android.support.v4.content.res.ResourcesCompat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.BaseAdapter; import android.widget.GridView; import android.widget.ImageView; import android.widget.ListView; import java.io.File; import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Executors; /** * class to load thumbnail images using a background task */ public class ThumbnailAdapter extends BaseAdapter { public static final String TAG = ThumbnailAdapter.class.getName(); /** * Callbacks to the holder of the GridView */ public interface ThumbnailAdapterListener { /** * Allows caller to turn on or off a progress bar or dialog * @param aVisibility Either View.VISIBLE or View.GONE. */ void showProgress(int aVisibility); /** * Report back to caller that the thumbnails are complete */ void onThumbnailsReady(); } /** A context */ protected Context mContext; /** The path containing the images to be thumbnailed */ protected ZFile mFile; /** The size of the thumbnails in pixels */ protected int mThumbSize; /** The currently selected image in the gridview */ protected int mSelectedItem; /** Cache for generated thumbnails (@see @link{ThumbnailManager}) */ protected ThumbnailCache mThumbsCache; /** Presentation containing references to be thumbnails */ protected Presentation mPresentation; /** Generated thumbnail bitmaps*/ protected ArrayList<Bitmap> mThumbBitmaps; /** Names of the images to be thumbnailed*/ protected ArrayList<String> mThumbNames; /** AsyncTask used to generate the thumbnails in the background */ protected ThumbLoaderTask mThumbLoader; /** Reference to the calling class */ protected ThumbnailAdapterListener mListener; /** Working variables * Saves repeatedly requesting a LayoutInflater and LayoutParams in getView() */ protected LayoutInflater mLayoutInflater; protected GridView.LayoutParams mLayoutParams; /** * Constructor. * @param c Context to be used for services and resources. * @param aFile Path to container of images (directory, zip file or list). * @param aThumbSize Size of thumbnail images in pixels. * @param aListener Reference to listener class to all. */ public ThumbnailAdapter(Context c, ZFile aFile, int aSelected, int aThumbSize, ThumbnailAdapterListener aListener) { mContext = c; mFile = aFile; mThumbSize = aThumbSize; mSelectedItem = aSelected; mListener = aListener; mLayoutInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mLayoutParams = new GridView.LayoutParams(mThumbSize, mThumbSize); // special case for images not in a directory leaf node if (mFile.getName().equals(c.getString(R.string.text_images_placeholder))) { mFile = new ZFile(mFile.getParentPath()); } mPresentation = Presentation.getNewInstance(mFile); mThumbNames = mPresentation.getSubdirIndex(mFile.getSubPath(), Presentation.FILES_ONLY | Presentation.SORTED); mThumbBitmaps = new ArrayList<>(mThumbNames.size()); // strip out directories and add thumbnail placeholders (null) for (int i = mThumbNames.size() - 1; i >= 0; i--) { if (mThumbNames.get(i).endsWith(File.separator)) mThumbNames.remove(i); else { mThumbBitmaps.add(0, null); } } // create a thumbnail cache if required SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); boolean cache = prefs.getBoolean(mContext.getString(R.string.PREFS_CACHE_THUMBS), false); if (cache) { mThumbsCache = new ThumbnailCache(ThumbnailCacheManager.getInstance(mContext), mFile, mThumbNames); } } /* * Set the thumbnail size in pixels */ public void setThumbSize(int aThumbSize) { mThumbSize = aThumbSize; } /* * Returns the Presentation created */ public Presentation getPresentation() { return mPresentation; } /* * Cancels the running background task using stop and cancel (belt and braces) */ public void cancel() { if (mThumbLoader != null) { mThumbLoader.cancel(false); } } /* * Starts a new instance of the ThumbLoaderTask to pick up from where it left off */ public void resume() { if (mThumbLoader == null || mThumbLoader.getStatus() == Status.FINISHED) { mThumbLoader = new ThumbLoaderTask(mPresentation); mThumbLoader.executeOnExecutor(SlideShowActivity.THREAD_POOL_EXECUTOR); } } /* * Tidy up all resources used */ public void close() { cancel(); mPresentation.close(); for (int i = 0; i < mThumbBitmaps.size(); i++) { Bitmap b = mThumbBitmaps.get(i); if (b != null) { mThumbBitmaps.set(i, null); BitmapManager.recycleBitmap(b); } } mThumbBitmaps.clear(); mThumbNames.clear(); } /* * Returns the actual number of image thumbnails * @return The number of thumbnail images */ @Override public int getCount() { return mThumbNames.size(); } /* * Returns the current thumbnail at the specified position (may be placeholder still) * @param position - index of the thumbnail * @return the thumbnail bitmap */ @Override public Object getItem(int position) { return mThumbBitmaps.get(position); } /* * Null implementation of method from ListAdapter interface */ @Override public long getItemId(int position) { return 0; } /* * Get a view that displays the image thumbnail at the specified position * @see @link{android.widget.Adapter.getView()} */ @Override public View getView(int position, View convertView, ViewGroup parent) { ImageView imageView; if (convertView == null) { imageView = (ImageView) mLayoutInflater.inflate(R.layout.gridview_item, parent, false); imageView.setLayoutParams(mLayoutParams); } else { imageView = (ImageView) convertView; // hack to reset selection state of re-used views after action mode ended AbsListView list = (AbsListView) parent; if (list != null && list.getChoiceMode() == ListView.CHOICE_MODE_NONE) { imageView.setActivated(false); } } if (mThumbBitmaps.size() > position && mThumbBitmaps.get(position) != null) { Bitmap bmp = mThumbBitmaps.get(position); imageView.setImageBitmap(bmp); if (bmp.getWidth() >= mThumbSize || bmp.getHeight() >= mThumbSize) { imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); } else { imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); } } else { imageView.setImageResource(R.drawable.ic_launcher); } return imageView; } /* * Get name of image at specified position. * @param position Index of thumbnail we want. * @return The name of the image, not including path. */ public String getName(int position) { return mPresentation.getName(position); } /* * Get full path of image at specified position. * @param position Index of thumbnail we want. * @return The path of the image, including name. */ public String getPath(int position) { return mPresentation.getFullPath(position).toString(); } /* * Remove an item from the adapter. * @param position Index of thumbnail we want to remove */ public void remove(int position) { mThumbBitmaps.remove(position); mPresentation.remove(position); notifyDataSetChanged(); } /** * Regenerate the thumbs. */ public void refreshThumbs() { mThumbsCache.invalidate(); mThumbsCache.close(); for (int i = 0; i < mThumbBitmaps.size(); i++) { Bitmap b = mThumbBitmaps.get(i); mThumbBitmaps.set(i, null); if (b != null) { BitmapManager.recycleBitmap(b); } } notifyDataSetChanged(); mThumbLoader = new ThumbLoaderTask(mPresentation); mThumbLoader.executeOnExecutor(SlideShowActivity.THREAD_POOL_EXECUTOR); } /** * Background task to load thumbs from cache (if present) or by decoding the files. */ public class ThumbLoaderTask extends AsyncTask<Void, Void, Void> { /** The maximum number of worker threads that can be run at once. */ private static final int MAX_THREADS = 4; /** The presentation defining the images to be thumbnailed. */ private Presentation mPresentation; /** Latch to ensure all bitmaps are ready before saving the cache. */ private CountDownLatch mCountDownLatch; /** A fixed pool executor for worker threads */ private final Executor mExecutor; /** * Constructor */ public ThumbLoaderTask(Presentation aPresentation) { mPresentation = aPresentation; mExecutor = Executors.newFixedThreadPool(MAX_THREADS); } /** * Tell listener to show the progress dialog */ @Override protected void onPreExecute() { SlideShowActivity.debug(3, TAG, "onPreExecute"); if (mListener != null) { mListener.showProgress(View.VISIBLE); } BitmapManager.resetDecodeTime(); } /** * Worker to generate a thumbnail from a full image file at the given position in the adapter. */ private class DecodeRunnable implements Runnable { /** The position of this file within the adapter. */ private int mPosition; /** * Constructor. * @param aPosition The position of this file within the adapter. */ public DecodeRunnable(final int aPosition) { this.mPosition = aPosition; } /** * Decode a full image bitmap, making sure that the {@link java.util.concurrent.CountDownLatch} is decremented even if there's a problem. */ @Override public void run() { Bitmap bmp; try { bmp = BitmapManager.getOptimisedBitmap(mPresentation, mPresentation.getSubPath(mPosition), new PointF(mThumbSize, mThumbSize), false); if (bmp != null) { if (mThumbBitmaps.size() > mPosition) { mThumbBitmaps.set(mPosition, bmp); publishProgress(); } mCountDownLatch.countDown(); return; } } catch (Exception e) { e.printStackTrace(); bmp = null; } // something went wrong, so load the error icon instead try { BitmapDrawable drawable = (BitmapDrawable) ResourcesCompat.getDrawable(mContext.getResources(), R.drawable.ic_action_error, null); if (drawable != null) { bmp = drawable.getBitmap(); } if (mThumbBitmaps.size() > mPosition) { mThumbBitmaps.set(mPosition, bmp); publishProgress(); } } catch (Exception e) { // even that didn't work (out of memory?) e.printStackTrace(); } mCountDownLatch.countDown(); } } /** * Get thumb from cache, otherwise spawn a worker thread to decode the full image. * @param aPosition The position of the thumb in the adapter. */ private void decode(int aPosition) { if (mThumbBitmaps.get(aPosition) == null) { if (!mPresentation.isDirectory(aPosition)) { Bitmap bmp = null; if (mThumbsCache != null) { bmp = mThumbsCache.getCachedThumb(aPosition); } if (bmp == null) { // spawn a new worker thread mExecutor.execute(new DecodeRunnable(aPosition)); } else { // update the thumbnail and notify progress if (mThumbBitmaps.size() > aPosition) { mThumbBitmaps.set(aPosition, bmp); publishProgress(); } // this task complete so decrement latch mCountDownLatch.countDown(); } } } else { mCountDownLatch.countDown(); } } /** * Load the thumbs, starting at the currently selected thumb and working in both directions simultaneously. */ @Override protected Void doInBackground(Void... arg0) { if (isCancelled()) return null; int lower = mSelectedItem - 1; int upper = mSelectedItem; int count = mThumbNames.size(); mCountDownLatch = new CountDownLatch(mPresentation.getCount()); while (lower >= 0 || upper < count) { if (isCancelled()) { return null; } if (upper < mThumbNames.size()) { decode(upper++); } if (lower >= 0) { decode(lower--); } Thread.yield(); } // wait for all worker threads to complete try { mCountDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } if (mThumbsCache != null) { mThumbsCache.saveThumbCache(mThumbBitmaps); } return null; } /** * Allow the grid view to update the thumbs progressively */ @Override public void onProgressUpdate(Void... arg) { notifyDataSetChanged(); } /** * Take down the progess dialog and notify listener that the task is complete */ @Override public void onPostExecute(Void aVoid) { SlideShowActivity.debug(3, TAG, "onPostExecute"); SlideShowActivity.debug(1, TAG, "Average decode time: %s ms", BitmapManager.averageDecodeTime()); notifyDataSetChanged(); if (mListener != null) { mListener.showProgress(View.GONE); mListener.onThumbnailsReady(); } } } }