li.zeitgeist.android.worker.ThumbnailWorker.java Source code

Java tutorial

Introduction

Here is the source code for li.zeitgeist.android.worker.ThumbnailWorker.java

Source

/**
 * Zeitgeist for Android
 * Copyright (C) 2012  Matthias Hecker <http://apoc.cc/>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package li.zeitgeist.android.worker;

import li.zeitgeist.android.ZeitgeistApiFactory;
import li.zeitgeist.android.ZeitgeistApp;
import li.zeitgeist.android.worker.ItemWorker.UpdatedItemsListener;

import li.zeitgeist.android.R;

import li.zeitgeist.api.Item;
import li.zeitgeist.api.Item.Type;

import java.io.*;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.net.URL;

import android.content.Context;
import android.graphics.*;
import android.support.v4.util.LruCache;
import android.util.Log;

import li.zeitgeist.api.ZeitgeistApi;

/**
 * Concurrently loads thumbnail bitmaps and cache them on disk and memory.
 *
 * This class downloads and caches thumbnails from Zeitgeist. It uses
 * a fixed size ThreadPool (ExecutorService) for downloading and
 * reading from disk cache (sdcard). It maintains two caches to store 
 * the bitmaps: A LruCache with a fixed size and a disk cache on 
 * the sdcard.
 */
public class ThumbnailWorker implements UpdatedItemsListener {

    /**
     * Standard android logging tag.
     */
    private static final String TAG = ZeitgeistApp.TAG + ":ThumbnailWorker";

    /**
     * How many threads the ThreadPool (ExecutorService) should use.
     */
    private static final int THREADS = 8;

    /**
     * Interface for loaded thumbnail listeners.
     */
    public interface LoadedThumbnailListener {
        public void onLoadedThumbnail(final int id, final Bitmap bitmap);
    }

    /**
     * In-Memory cache, with a fixed size.
     */
    private LruCache<Integer, Bitmap> memCache = null;

    /**
     * Disk cache, points to sdcard application cache.
     */
    private File diskCache;

    /**
     * Thread Pool for downloading thumbnail images.
     */
    private ExecutorService pool;

    /**
     * The bitmap to overlay video thumbnails with.
     */
    private Bitmap videoOverlayBitmap;

    /**
     * Zeitgeist Java API instance.
     */
    private ZeitgeistApi api;

    /**
     * Constructs the thumbnail loader.
     *
     * This starts the thread pool, initializes the memory cache,
     * and creates the directories for the disk-cache (if necessary).
     * 
     * @param context of the gallery service.
     */
    public ThumbnailWorker(Context context) {
        Log.v(TAG, "constructed");

        api = ZeitgeistApiFactory.createInstance(context);

        // start thread pool
        pool = Executors.newFixedThreadPool(THREADS);

        // initialize memory cache
        /*
            
         TODO: somehow that doesn't work at all, why?
            
        int memCacheSize = 2 * 1024 * 1024; // 2 MiB
        memCache = new LruCache<Integer, Bitmap>(memCacheSize) {
            protected int sizeOf(Integer key, Bitmap value) {
                return value.getRowBytes() * value.getHeight();
            }
        };
            
        */
        // 150 * ~16 KiB = 2250 KiB
        memCache = new LruCache<Integer, Bitmap>(150);

        // create disk cache directory
        File externalStorageDirectory = context.getExternalFilesDir(null);
        diskCache = new File(externalStorageDirectory, "cache");
        if (!diskCache.exists()) {
            diskCache.mkdirs();
        }
        Log.d(TAG, "disk cache: " + diskCache.getAbsolutePath());

        // load video overlay bitmap
        videoOverlayBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.video_overlay);
    }

    /**
     * Loads thumbnail bitmap from disk or web.
     *
     * @param item api item object
     * @param loadedListener called with the bitmap
     */
    public void loadThumbnail(final Item item, final LoadedThumbnailListener loadedListener) {
        pool.submit(new Runnable() {
            public void run() {
                Bitmap bitmap = getBitmapByItem(item);
                loadedListener.onLoadedThumbnail(item.getId(), bitmap);
            }
        });
    }

    /**
     * Retrieve thumbnail bitmap from cache or web return.
     * 
     * @param item
     * @return bitmap instance
     */
    public Bitmap getBitmapByItem(Item item) {
        if (item.getImage() == null) {
            Log.w(TAG, "tried to load item without image");
            return null;
        }

        Bitmap bitmap = null;
        if (isMemCached(item)) {
            bitmap = loadFromMemCache(item);
        } else {
            if (isDiskCached(item)) {
                bitmap = loadFromDiskCache(item);
            } else {
                bitmap = loadFromWeb(item);
                if (bitmap == null) {
                    Log.e(TAG, "bitmap from web is null!");
                    return null;
                }
                saveToDiskCache(item, bitmap);
            }

            // draws the video overlay into the bitmap (if the item is a video)
            if (item.getType() == Type.VIDEO) {
                bitmap = drawVideoOverlay(bitmap);
            }

            saveToMemCache(item, bitmap);
        }

        return bitmap;
    }

    /**
     * Draw the video overlay (play icon) on top of the bitmap.
     * 
     * @param bitmap to draw bitmap on.
     * @return bitmap instance with the overlay on it.
     */
    private Bitmap drawVideoOverlay(Bitmap bitmap) {
        return drawBitmapOverlay(bitmap, videoOverlayBitmap);
    }

    /**
     * Draw overlay on a bitmap.
     * 
     * @param bitmap
     * @param overlay
     * @return new bitmap
     */
    private Bitmap drawBitmapOverlay(Bitmap bitmap, Bitmap overlay) {
        // create plain empty bitmap
        Bitmap plainBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig());

        // canvas to draw on it
        Canvas canvas = new Canvas(plainBitmap);

        // draw the bitmap then the overlay
        canvas.drawBitmap(bitmap, new Matrix(), null);
        canvas.drawBitmap(overlay, new Matrix(), null);

        // recycle the original bitmap
        bitmap.recycle();

        return plainBitmap;
    }

    /**
     * True if the thumbnail bitmap exists on the sdcard cache.
     * 
     * @param item
     * @return boolean
     */
    private boolean isDiskCached(Item item) {
        return getDiskCacheFile(item).exists();
    }

    /**
     * Return disk cached thumbnail file.
     * 
     * @param item
     * @return file instance.
     */
    private File getDiskCacheFile(Item item) {
        return new File(diskCache, String.format("thumb_%d.jpg", item.getId()));
    }

    /**
     * Loads the bitmap from the sdcard into a Bitmap instance and return.
     * 
     * @param item
     * @return newly loaded Bitmap instance.
     */
    private Bitmap loadFromDiskCache(Item item) {
        Log.v(TAG, "load from disk cache");
        File file = getDiskCacheFile(item);
        return BitmapFactory.decodeFile(file.getAbsolutePath());
    }

    /**
     * Store item thumbnail bitmap instance to disk cache.
     * 
     * @param item
     * @param bitmap
     */
    private void saveToDiskCache(Item item, Bitmap bitmap) {
        Log.v(TAG, "save to disk cache");
        File file = getDiskCacheFile(item);

        // cache bitmap on disk
        try {
            FileOutputStream out = new FileOutputStream(file);
            try {
                bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
            } finally {
                out.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Return true if the thumbnail bitmap exists in memory.
     * 
     * @param item
     * @return boolean
     */
    public boolean isMemCached(Item item) {
        Bitmap bitmap = null;
        synchronized (memCache) {
            bitmap = memCache.get(item.getId());
        }
        return bitmap != null;
    }

    /**
     * Return thumbnail bitmap from memory cache.
     *
     * @param int unique item id
     * @return Bitmap or null
     */
    public Bitmap loadFromMemCache(Item item) {
        // Log.v(TAG, "load from memory cache");
        Bitmap bitmap = null;
        synchronized (memCache) {
            bitmap = memCache.get(item.getId());
        }
        return bitmap;
    }

    /**
     * Store bitmap instance in memory lru cache.
     * 
     * @param item
     * @param bitmap
     */
    private void saveToMemCache(Item item, Bitmap bitmap) {
        // Log.v(TAG, "save to memory cache");
        synchronized (memCache) {
            // put in in-memory cache
            memCache.put(item.getId(), bitmap);
        }
    }

    /**
     * Loads an thumbnail image of an item.
     *
     * @param item
     * @return bitmap
     */
    private Bitmap loadFromWeb(Item item) {
        String url = api.getBaseUrl() + item.getImage().getThumbnail();
        Log.d(TAG, "load from web " + url);
        Bitmap bitmap = null;
        InputStream in = null;
        try {
            try {
                in = (InputStream) (new URL(url).getContent());
                bitmap = BitmapFactory.decodeStream(in);
            } finally {
                if (in != null) {
                    in.close();
                }
            }

        } catch (Exception e) {
            Log.e(TAG, "unable to download thumbnail: " + url);
            e.printStackTrace();
        }
        return bitmap;
    }

    @Override
    public void onUpdatedItems(List<Item> newItemsList) {
        // load the thumbnails prematurely if new items are loaded.
        if (newItemsList != null) {
            for (Item item : newItemsList) {
                loadThumbnail(item, null);
            }
        }
    }

    @Override
    public void onError(String error) {
    }

    /**
     * Stops running downloads and shuts down the thread pool.
     */
    public void stopThreadPool() {
        Log.i(TAG, "stopping thread pool");
        pool.shutdownNow();
    }
}