com.concentricsky.android.khanacademy.util.ThumbnailManager.java Source code

Java tutorial

Introduction

Here is the source code for com.concentricsky.android.khanacademy.util.ThumbnailManager.java

Source

/*
Viewer for Khan Academy
Copyright (C) 2012 Concentric Sky, Inc.
    
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 com.concentricsky.android.khanacademy.util;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.CacheResponse;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ResponseCache;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.Locale;

import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Environment;
import android.support.v4.util.LruCache;

import com.concentricsky.android.khanacademy.data.KADataService;
import com.concentricsky.android.khanacademy.data.db.Thumbnail;
import com.jakewharton.DiskLruCache;

/**
 * Handles downloading thumbnails, caching them, and returning them to the list fragment.
 * 
 * 
 * @author austinlally
 *
 */
public class ThumbnailManager {

    public static final String LOG_TAG = ThumbnailManager.class.getSimpleName();
    private static final int CONNECT_TIMEOUT = 3000;

    private static ThumbnailManager sharedInstance;

    /* ***********************   PRIVATE   ***************************/

    private final KADataService dataService;
    private final ConnectivityManager connectivityManager;
    private final LruCache<Thumbnail, Bitmap> cache;
    private final DiskLruCache diskCache;

    private boolean isDestroyed;

    public static ThumbnailManager getSharedInstance(KADataService dataService) {
        if (sharedInstance == null || sharedInstance.isDestroyed) {
            sharedInstance = new ThumbnailManager(dataService);
        }
        return sharedInstance;
    }

    private ThumbnailManager(final KADataService dataService) {
        this.dataService = dataService;
        connectivityManager = (ConnectivityManager) dataService.getSystemService(Context.CONNECTIVITY_SERVICE);

        cache = prepareCache();
        diskCache = prepareDiskCache();
    }

    private DiskLruCache prepareDiskCache() {
        int v = 0;
        try {
            v = dataService.getPackageManager().getPackageInfo(dataService.getPackageName(), 0).versionCode;
        } catch (NameNotFoundException e) {
            // Huh? Really?
        }

        // TODO : allow user to configure this.
        long maxSize = 1024 * 1024 * 1024;

        File cacheDir = new File(dataService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
                "thumbnail_cache");
        int valueCount = 8;

        try {
            // TODO : This is slow on first run. Look into improving that.
            return DiskLruCache.open(cacheDir, v, valueCount, maxSize);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private LruCache<Thumbnail, Bitmap> prepareCache() {
        // Total available heap size. This changes based on manifest android:largeHeap="true". (Fire HD, transformer both go from 48MB to 256MB)
        Runtime rt = Runtime.getRuntime();
        long maxMemory = rt.maxMemory();
        Log.v(LOG_TAG, "maxMemory:" + Long.toString(maxMemory));

        // Want to use at most about 1/2 of available memory for thumbs.
        // In SAT Math category (116 videos), with a heap size of 48MB, this setting
        // allows 109 thumbs to be cached resulting in total heap usage around 34MB.
        long usableMemory = maxMemory / 2;

        return new LruCache<Thumbnail, Bitmap>((int) usableMemory) {
            @Override
            protected int sizeOf(Thumbnail key, Bitmap value) {
                return value.getByteCount();
            }

            @Override
            protected void entryRemoved(boolean evicted, Thumbnail key, Bitmap oldValue, Bitmap newValue) {
                if (oldValue != newValue) {
                    oldValue.recycle();
                }
            }
        };

    }

    /* ***********************   PUBLIC   ***************************/

    public void destroy() {
        cache.evictAll();
        if (diskCache != null) {
            try {
                diskCache.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        isDestroyed = true;
    }

    /**
     * Cache of thumbnails by youtube id.
     * @return
     */
    public LruCache<Thumbnail, Bitmap> getCache() {
        return cache;
    }

    /**
     * Get the thumbnail for the video with the given youtube id, or start a download if it isn't yet stored locally.
     * 
     * @param y_id The youtube id of the video whose thumbnail we need.
     * @return The thumbnail as a {@link Bitmap}, or {@code null} if none exists.
     */

    public Bitmap getThumbnail(String y_id, byte quality, boolean useCache) {

        Bitmap result = null;

        if (useCache) {
            Thumbnail thumbnail = new Thumbnail(y_id, quality);
            result = cache.get(thumbnail);
        }

        if (result == null) {
            result = getThumbnail(y_id, quality);
        }
        return result;
    }

    private int indexForAvailability(byte q) {
        switch (q) {
        case Thumbnail.QUALITY_LOW:
            return 4;
        case Thumbnail.QUALITY_MEDIUM:
            return 5;
        case Thumbnail.QUALITY_HIGH:
            return 6;
        case Thumbnail.QUALITY_SD:
            return 7;
        default:
            throw new IllegalArgumentException("invalid thumb quality");
        }
    }

    private int indexForQuality(byte q) {
        switch (q) {
        case Thumbnail.QUALITY_LOW:
            return 0;
        case Thumbnail.QUALITY_MEDIUM:
            return 1;
        case Thumbnail.QUALITY_HIGH:
            return 2;
        case Thumbnail.QUALITY_SD:
            return 3;
        default:
            throw new IllegalArgumentException("invalid thumb quality");
        }
    }

    public Bitmap getThumbnailFromDiskCache(String youtubeId, byte quality) {
        String key = youtubeId.toLowerCase(Locale.US);
        Bitmap result = null;
        DiskLruCache.Snapshot snap = null;
        DiskLruCache.Editor editor = null;

        // Ensure we have a cache entry for this youtube id.
        try {
            // null while another editor is open
            while ((editor = diskCache.edit(key)) == null) {
            }

            if (editor.getString(indexForAvailability(Thumbnail.QUALITY_HIGH)) == null) {
                // values only null if they've never been set, so this must be a new entry
                editor.set(indexForQuality(Thumbnail.QUALITY_HIGH), "");
                editor.set(indexForAvailability(Thumbnail.QUALITY_HIGH),
                        String.valueOf(Thumbnail.AVAILABILITY_UNKNOWN));
                editor.set(indexForQuality(Thumbnail.QUALITY_MEDIUM), "");
                editor.set(indexForAvailability(Thumbnail.QUALITY_MEDIUM),
                        String.valueOf(Thumbnail.AVAILABILITY_UNKNOWN));
                editor.set(indexForQuality(Thumbnail.QUALITY_LOW), "");
                editor.set(indexForAvailability(Thumbnail.QUALITY_LOW),
                        String.valueOf(Thumbnail.AVAILABILITY_UNKNOWN));
                editor.set(indexForQuality(Thumbnail.QUALITY_SD), "");
                editor.set(indexForAvailability(Thumbnail.QUALITY_SD),
                        String.valueOf(Thumbnail.AVAILABILITY_UNKNOWN));
                editor.commit();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (editor != null)
                editor.abortUnlessCommitted();
        }

        while (quality >= Thumbnail.QUALITY_LOW && result == null) {

            try {
                // Try getting a bitmap for this quality from disk.
                snap = diskCache.get(key);
                InputStream is = snap.getInputStream(indexForQuality(quality));
                try {
                    result = BitmapFactory.decodeStream(is);
                } finally {
                    if (is != null) {
                        try {
                            is.close();
                        } catch (IOException ex) {
                        }
                    }
                }
                if (result != null) {
                    return result;
                }

                // If none exists, try fetching it if we haven't before.
                int availability = Integer.parseInt(snap.getString(indexForAvailability(quality)));
                if (availability != Thumbnail.AVAILABILITY_UNAVAILABLE) {
                    NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo();
                    if (activeNetwork != null && activeNetwork.isConnected()) {
                        try {
                            result = bitmap_from_url(Thumbnail.getDownloadUrl(dataService, quality, youtubeId));
                        } catch (MalformedURLException e) {
                            e.printStackTrace();
                        } catch (IOException e) {
                            // FileNotFoundException on 404. Mark as unavailable.
                            if (e instanceof FileNotFoundException) {
                                try {
                                    while ((editor = diskCache.edit(key)) == null) {
                                    }
                                    editor.set(indexForAvailability(quality),
                                            String.valueOf(Thumbnail.AVAILABILITY_UNAVAILABLE));
                                    editor.commit();
                                } catch (IOException ex) {
                                    ex.printStackTrace();
                                } finally {
                                    if (editor != null)
                                        editor.abortUnlessCommitted();
                                }
                            } else {
                                e.printStackTrace();
                            }
                        }
                    }

                    // If we receive a thumbnail response, store it in the cache and return it.
                    if (result != null) {
                        try {
                            while ((editor = diskCache.edit(key)) == null) {
                            }
                            OutputStream os = editor.newOutputStream(indexForQuality(quality));
                            try {
                                result.compress(Bitmap.CompressFormat.PNG, 100, os);
                            } finally {
                                if (os != null)
                                    os.close();
                            }
                            editor.set(indexForAvailability(quality),
                                    String.valueOf(Thumbnail.AVAILABILITY_AVAILABLE));
                            editor.commit();
                        } catch (IOException e) {
                            e.printStackTrace();
                        } finally {
                            if (editor != null)
                                editor.abortUnlessCommitted();
                        }
                        return result;
                    }
                }

            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (snap != null)
                    snap.close();
            }

            quality--;
        }

        return result;
    }

    public Bitmap getThumbnail(String y_id, byte quality) {
        Log.v(LOG_TAG, ".getThumbnailForYoutubeId");
        return getThumbnailFromDiskCache(y_id, quality);
    }

    /**
     * Attempt to download a bitmap from the given url.
     * 
     * @param url The url of the thumbnail to download.
     * @return A {@link Bitmap} of the thumbnail, or {@code null} if it cannot be downloaded and is not cached locally.
     * @throws java.net.MalformedURLException if the url is malformed!
     * @throws java.io.IOException if a connection cannot be opened to the given url, or if an IOException is thrown by {@link ResponseCache} or by {@link CacheResponse}.
     */
    public static Bitmap bitmap_from_url(String url) throws java.net.MalformedURLException, IOException {
        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
        connection.setConnectTimeout(CONNECT_TIMEOUT);
        connection.setUseCaches(true);

        InputStream input = null;
        try {
            connection.connect();
            input = connection.getInputStream();
            return BitmapFactory.decodeStream(input);
        } catch (SocketTimeoutException e) {
            e.printStackTrace();
            return null;
        } finally {
            if (input != null)
                input.close();
        }
    }

    /*
     "media$thumbnail": [
       {
     "url": "http://i.ytimg.com/vi/l-QFT7XNeb4/default.jpg",
     "height": 90,
     "width": 120,
     "time": "00:08:31",
     "yt$name": "default"
       },
       {
     "url": "http://i.ytimg.com/vi/l-QFT7XNeb4/mqdefault.jpg",
     "height": 180,
     "width": 320,
     "yt$name": "mqdefault"
       },
       {
     "url": "http://i.ytimg.com/vi/l-QFT7XNeb4/hqdefault.jpg",
     "height": 360,
     "width": 480,
     "yt$name": "hqdefault"
       },
       {
     "url": "http://i.ytimg.com/vi/l-QFT7XNeb4/sddefault.jpg",
     "height": 480,
     "width": 640,
     "yt$name": "sddefault"
       },
       {
     "url": "http://i.ytimg.com/vi/l-QFT7XNeb4/1.jpg",
     "height": 90,
     "width": 120,
     "time": "00:04:15.500",
     "yt$name": "start"
       },
       {
     "url": "http://i.ytimg.com/vi/l-QFT7XNeb4/2.jpg",
     "height": 90,
     "width": 120,
     "time": "00:08:31",
     "yt$name": "middle"
       },
       {
     "url": "http://i.ytimg.com/vi/l-QFT7XNeb4/3.jpg",
     "height": 90,
     "width": 120,
     "time": "00:12:46.500",
     "yt$name": "end"
       }
    ],
    */

}