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.graphics.Bitmap; import android.graphics.PointF; import android.graphics.drawable.BitmapDrawable; import android.os.AsyncTask; import android.support.v4.content.res.ResourcesCompat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.zip.ZipFile; import java.util.zip.ZipException; import java.util.zip.ZipInputStream; import java.io.FileInputStream; import java.io.IOException; import java.util.zip.ZipEntry; import java.util.Enumeration; import java.io.BufferedInputStream; import java.util.Iterator; import java.util.concurrent.ExecutorService; public class BrowserAdapter extends ArrayAdapter<ZFile> { public static final String TAG = BrowserAdapter.class.getName(); /** * Holder class for efficient recycling of adapter item views (avoids using findViewById repeatedly) */ private class Holder { public ImageView image; public TextView name; public TextView count; } /** A {@link Context} for access to resources. */ private Context mContext; /** The resource id of layout used for adapter view items. */ private int mResource; /** The list of files to be displayed by this adapter. */ private List<ZFile> mItems; /** The cache file for the directory being displayed. */ private ThumbnailCache mThumbsCache; /** The thumbnail images for each item in the adapter. */ private ArrayList<Bitmap> mThumbBitmaps; /** A list of the number of files under each directory item in the adapter. */ private ArrayList<Integer> mCountsCache; /** A reference to the inflater service. */ private LayoutInflater mInflater; /** The currently selected item in the adapter. */ private int mSelected; /** The size of the thumbnails. */ private final int mThumbSize; /** A random number generator for selecting random images. */ private final Random mRandom; /** The thumb loader task */ private final BrowserThumbLoaderTask mLoaderTask; /** * Constructor. * @param context A Context for resources. * @param resource The resource id of the adapter item layout. * @param items The list of files to be thumbnailed. * @param aRootFile The directory/zip file containing the files. * @param aSelected Index of currently selected item. */ public BrowserAdapter(Context context, int resource, List<ZFile> items, ZFile aRootFile, int aSelected) { super(context, resource, items); mSelected = aSelected; mContext = context; mResource = resource; mItems = items; ZFile[] mFiles = new ZFile[items.size()]; mThumbBitmaps = new ArrayList<>(items.size()); mCountsCache = new ArrayList<>(items.size()); mThumbSize = mContext.getResources().getDimensionPixelSize(R.dimen.thumbsize_small); mRandom = new Random(); ArrayList<String> mThumbNames = new ArrayList<>(items.size()); for (int i = 0; i < items.size(); i++) { mFiles[i] = items.get(i); mCountsCache.add(null); String name = mFiles[i].getName(); if (name.endsWith(File.separator)) { name = name.substring(0, name.length() - 1); } mThumbNames.add(name); } SlideShowActivity.debug(1, TAG, "<init> %s", aRootFile); mThumbsCache = null; mThumbsCache = new ThumbnailCache(ThumbnailCacheManager.getInstance(mContext), new ZFile(aRootFile, "[Browser]"), mThumbNames); for (int i = 0; i < items.size(); i++) { mThumbBitmaps.add(null); } mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mLoaderTask = new BrowserThumbLoaderTask(); mLoaderTask.executeOnExecutor(SlideShowActivity.THREAD_POOL_EXECUTOR, mFiles); } /** * {@inheritDoc} */ @Override public int getCount() { return mItems.size(); } /** * {@inheritDoc} */ @Override public ZFile getItem(int position) { return mItems.get(position); } /** * {@inheritDoc} */ @Override public long getItemId(int position) { return 0; } /** * {@inheritDoc} */ @Override public View getView(int position, View convertView, ViewGroup parent) { LinearLayout browserItemView; Holder holder; if (convertView == null) { browserItemView = new LinearLayout(mContext); mInflater.inflate(mResource, browserItemView, true); holder = new Holder(); holder.image = (ImageView) browserItemView.findViewById(R.id.rowImage); holder.name = (TextView) browserItemView.findViewById(R.id.rowFilename); holder.count = (TextView) browserItemView.findViewById(R.id.rowCount); browserItemView.setTag(holder); } else { browserItemView = (LinearLayout) convertView; holder = (Holder) browserItemView.getTag(); } final ImageView imageView = holder.image; final TextView textView = holder.name; final TextView countView = holder.count; ZFile item = getItem(position); String name = item.getName(); if (item.isDirectory() && item.getSubPath().length() == 0) { if (mThumbBitmaps.get(position) != null) imageView.setImageBitmap(mThumbBitmaps.get(position)); else imageView.setImageResource(R.drawable.folder); textView.setText(name.substring(0, name.length())); if (mCountsCache.get(position) != null) { countView.setText(String.format("%d", mCountsCache.get(position))); } else { countView.setText("\u2026"); } } else if (item.getSubPath().equals(mContext.getString(R.string.text_images_placeholder))) { imageView.setImageBitmap(mThumbBitmaps.get(position)); textView.setText(name.substring(0, name.length())); if (mCountsCache.get(position) != null) { countView.setText(String.format("%d", mCountsCache.get(position))); } else { countView.setText(""); } } else { if (mThumbBitmaps.get(position) != null) imageView.setImageBitmap(mThumbBitmaps.get(position)); else imageView.setImageResource(R.drawable.ic_launcher); if (mCountsCache.get(position) != null) { countView.setText(String.format("%d", mCountsCache.get(position))); } else { countView.setText("\u2026"); } textView.setText(name); } return browserItemView; } /** * Abort the thumb loader task and any worker threads */ public void abort() { if (mLoaderTask != null) { mLoaderTask.abort(); } } /** * {@link android.os.AsyncTask} to create thumbnails for zip files and count the images within them * for display by the adapter {@link BrowserAdapter#getView(int, View, ViewGroup)} method. */ private class BrowserThumbLoaderTask extends AsyncTask<ZFile, Void, Void> { /** Executor for worker threads. */ private ExecutorService mExecutor; /** CountDownLatch for synchronising completion of worker tasks */ private CountDownLatch mCountDownLatch; /** Flag to signal all tasks should shut themselves down. */ private boolean mAbort = false; /** * Constructor creates its own {@link Executor} to run up to 8 simultaneous decoder sub-tasks. */ public BrowserThumbLoaderTask() { mExecutor = Executors.newFixedThreadPool(8); } /** * Abort the thumb loader task and any worker threads. */ public void abort() { // Set a flag to signal the tasks to commit suicide mAbort = true; // stop any new tasks being scheduled mExecutor.shutdown(); } /** * Scan the specified directory tree to count all images and optionally select random files as thumbnail candidates. * @param aDirectory The directory or zip file to be scanned. * @param aFileList A list to hold prospective thumbnail candidates (or null if no selection is necessary). * @param aSelectImage Flag indicating if prospective images should be selected. * @return The total number of images in the directory tree, plus any candiate images in the provided list. */ private int getImageCountAndFiles(ZFile aDirectory, ArrayList<ZFile> aFileList, boolean aSelectImage) { int count = 0; ArrayList<ZFile> fileList = null; try { if (mAbort) { return 0; } if (aDirectory.isDirectory()) { fileList = aDirectory.listFiles(false, null); } else if (aDirectory.isZipFile() && aDirectory.getSubPath().length() == 0) { ZipFile zipFile = null; try { zipFile = new ZipFile(aDirectory.getRootFile()); count += zipFile.size(); if (aSelectImage && aFileList != null) { for (Enumeration<?> entries = zipFile.entries(); entries.hasMoreElements();) { ZipEntry entry = (ZipEntry) entries.nextElement(); try { if (aFileList != null && (aFileList.isEmpty() || mRandom.nextInt(30) == 0) && entry.getName().matches("(?i:.*\\.jpe*g)")) { aFileList.add(new ZFile(aDirectory, entry.getName())); } } catch (Exception e) { e.printStackTrace(); break; } } } return count; } catch (IOException e) { e.printStackTrace(); return 0; } finally { if (zipFile != null) { zipFile.close(); } } } if (fileList != null) { int selected = fileList.size() > 0 ? mRandom.nextInt(fileList.size()) : 0; for (int i = 0; i < fileList.size(); i++) { if (mAbort) { return 0; } ZFile f = fileList.get((i + selected) % fileList.size()); if (f.isDirectory()) { count += getImageCountAndFiles(f, aFileList, aSelectImage && aFileList != null && aFileList.size() == 0); } else if (f.isZipDirectory()) { count += getImageCountAndFiles(f, aFileList, aSelectImage && aFileList != null && aFileList.size() == 0); } else if (f.getName().matches("(?i:.*\\.jpe*g)")) { if (aFileList != null) { aFileList.add(f); } count++; } } } } catch (Exception e) { e.printStackTrace(); } return count; } /** * Generates a thumbnail for the given zip or directory file at the given position in the adapter. * @param aFile The full path/subpath to the zip directory. * @param aPosition The position of this file within the adapter. */ private void doDecode(final ZFile aFile, final int aPosition) { if (mAbort) { return; } Bitmap bmp = mThumbBitmaps.get(aPosition); ArrayList<String> names; int fileCount; try { if (aFile.isDirectory()) { ArrayList<ZFile> fileList = new ArrayList<>(); fileCount = getImageCountAndFiles(aFile, fileList, bmp == null); mCountsCache.set(aPosition, fileCount); if (bmp == null) { int count = fileList.size(); if (count > 0) { bmp = BitmapManager.getOptimisedBitmap(fileList.get(mRandom.nextInt(count)), new PointF(mThumbSize, mThumbSize), false); if (bmp != null && aPosition < mThumbBitmaps.size()) { mThumbBitmaps.set(aPosition, bmp); } } } publishProgress(); mCountDownLatch.countDown(); return; } else { Presentation presentation; if (aFile.getName().equals(mContext.getString(R.string.text_images_placeholder))) { // <Images> placeholder, so list all images in parent directory if (aFile.isZipFile()) { presentation = Presentation.getNewInstance(new ZFile(aFile.getParentPath()), Presentation.FILES_ONLY); } else { presentation = Presentation.getNewInstance(new ZFile(aFile.getRootPath()), Presentation.FILES_ONLY); } names = presentation.getIndex(); for (Iterator<String> iterator = names.iterator(); iterator.hasNext();) { if (!iterator.next().matches("(?i:.*\\.jpe*g)")) { iterator.remove(); } } fileCount = names.size(); // drop through to get a random bitmap } else if (aFile.isZipFile()) { // zip file, so list all images in the current subpath presentation = Presentation.getNewInstance(aFile, Presentation.FILES_ONLY); names = presentation.getIndex(); if (names.size() == 0) names = presentation.getSubdirIndex(aFile.getSubPath(), Presentation.FILES_ONLY | Presentation.INCLUDE_SUBDIRS); fileCount = presentation.getSubdirCount(aFile.getSubPath()); // drop through to get a random bitmap } else { // don't know what this is - some sort of error mCountsCache.set(aPosition, -1); publishProgress(); mCountDownLatch.countDown(); return; } mCountsCache.set(aPosition, fileCount); if (bmp == null) { // get a random bitmap int count = names.size(); int rnd = 0; if (count > 1) rnd = mRandom.nextInt(count); if (count != 0) { bmp = BitmapManager.getOptimisedBitmap(presentation, names.get(rnd), new PointF(mThumbSize, mThumbSize), false); } } presentation.close(); } } catch (Exception e) { e.printStackTrace(); // show an error icon instead of a thumbnail BitmapDrawable errorIcon = (BitmapDrawable) (ResourcesCompat.getDrawable(mContext.getResources(), R.drawable.ic_action_error, null)); bmp = errorIcon == null ? null : errorIcon.getBitmap(); mCountsCache.set(aPosition, 0); } // add the (possibly null) bitmap to the list if (mThumbBitmaps.size() > aPosition) { mThumbBitmaps.set(aPosition, bmp); } publishProgress(); mCountDownLatch.countDown(); } /** * Spawn a worker task to perform the decoding of each item in the adapter. * @param aFile The full path/subpath to the zip directory. * @param aPosition The position of this file within the adapter. */ private void decode(final ZFile aFile, final int aPosition) { if (!mAbort) { mExecutor.execute(new Runnable() { @Override public void run() { doDecode(aFile, aPosition); } }); } } /** * Decodes thumbnails from selected thumb in both directions simultaneously * @param arg0 List of files to decode * @return null */ @Override protected Void doInBackground(ZFile... arg0) { for (int i = 0; i < mThumbBitmaps.size(); i++) { mThumbBitmaps.set(i, mThumbsCache.getCachedThumb(i)); } publishProgress(); mCountDownLatch = new CountDownLatch(arg0.length); try { int count = arg0.length; int lower = mSelected - 1; int upper = mSelected; while (lower >= 0 || upper < count) { if (isCancelled() || mAbort) break; if (upper < arg0.length) { decode(arg0[upper], upper++); } if (lower >= 0) { decode(arg0[lower], lower--); } } if (!mAbort) { mCountDownLatch.await(); } } catch (Exception e) { e.printStackTrace(); } return null; } /** * Allow the adapter view to update its thumbnails * @param voids Nothing */ @Override public void onProgressUpdate(Void... voids) { notifyDataSetChanged(); } /** * All thumbs decoded, so save the cache file * @param aVoid Nothing */ @Override public void onPostExecute(Void aVoid) { if (mThumbsCache != null && !mAbort) { mThumbsCache.saveThumbCache(mThumbBitmaps); mThumbsCache.close(); } } } }