Java tutorial
/* * Copyright 2015 OpenMarket Ltd * * 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 org.matrix.matrixandroidsdk.db; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.AsyncTask; import android.support.v4.util.LruCache; import android.util.Log; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; import org.matrix.androidsdk.rest.model.ContentResponse; import org.matrix.androidsdk.util.ContentManager; import org.matrix.matrixandroidsdk.Matrix; import org.matrix.matrixandroidsdk.R; import org.matrix.matrixandroidsdk.view.PieFractionView; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.HashMap; public class ConsoleMediasCache { /** * Interface to implement to get the mxc URI of downloaded content. */ public static interface DownloadCallback { /** * Warn of the progress download * @param downloadId the download Identifier * @param percentageProgress the progress value */ public void onDownloadProgress(String downloadId, int percentageProgress); /** * Called when the upload is complete or has failed. * @param downloadId the download Identifier */ public void onDownloadComplete(String downloadId); } private static final String LOG_TAG = "ConsoleMediasCache"; /** * Compute the filesystem cache size * @param context * @return the medias cache size in bytes */ public static long cacheSize(Activity context) { long size = 0; String[] filesList = context.fileList(); for (String filename : filesList) { try { File file = new File(context.getFilesDir(), filename); size += file.length(); } catch (Exception e) { } } return size; } /** * Clear the medias caches. * @param context The application context to use. */ public static void clearCache(Activity context) { String[] filesList = context.fileList(); for (String file : filesList) { try { context.deleteFile(file); } catch (Exception e) { } } BitmapWorkerTask.clearBitmapsCache(); } /** * Convert matrix url into http one. * @param context the context * @param url the matrix url * @param width the expected image width * @param height the expected image height * @return the URL to access the described resource. */ private static String downloadableUrl(Context context, String url, int width, int height) { // check if the Url is a matrix one if (url.startsWith(ContentManager.MATRIX_CONTENT_URI_SCHEME)) { ContentManager contentManager = Matrix.getInstance(context).getDefaultSession().getContentManager(); if ((width > 0) && (height > 0)) { return contentManager.getDownloadableThumbnailUrl(url, width, height, ContentManager.METHOD_SCALE); } else { return contentManager.getDownloadableUrl(url); } } else { return url; } } /** * Return the cache file name for a media * @param context the context * @param url the media url * @return the cache file name (private directory) */ public static String mediaCacheFilename(Context context, String url) { return mediaCacheFilename(context, url, -1, -1); } public static String mediaCacheFilename(Context context, String url, int width, int height) { // sanity check if (null == url) { return null; } String filename = BitmapWorkerTask.buildFileName(downloadableUrl(context, url, width, height)); try { // already a local file if (filename.startsWith("file:")) { Uri uri = Uri.parse(filename); filename = uri.getLastPathSegment(); } File file = new File(context.getApplicationContext().getFilesDir(), filename); if (!file.exists()) { filename = null; } } catch (Exception e) { filename = null; } return filename; } /** * Save a bitmap to the local cache * it could be used for unsent media to allow them to be resent. * @param bitmap the bitmap to save * @param defaultFileName the filename is provided, if null, a filename will be generated * @return the media cache URL */ public static String saveBitmap(Bitmap bitmap, Context context, String defaultFileName) { String filename = "file" + System.currentTimeMillis(); String cacheURL = null; try { if (null != defaultFileName) { File file = new File(defaultFileName); file.delete(); filename = Uri.fromFile(file).getLastPathSegment(); } FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos); fos.flush(); fos.close(); cacheURL = Uri.fromFile(context.getFileStreamPath(filename)).toString(); } catch (Exception e) { } return cacheURL; } /** * Save a media to the local cache * it could be used for unsent media to allow them to be resent. * @param stream the file stream to save * @param defaultFileName the filename is provided, if null, a filename will be generated * @return the media cache URL */ public static String saveMedia(InputStream stream, Context context, String defaultFileName) { String filename = (defaultFileName == null) ? ("file" + System.currentTimeMillis()) : defaultFileName; String cacheURL = null; try { FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE); try { byte[] buf = new byte[1024 * 32]; int len; while ((len = stream.read(buf)) != -1) { fos.write(buf, 0, len); } } catch (Exception e) { } fos.flush(); fos.close(); stream.close(); cacheURL = Uri.fromFile(context.getFileStreamPath(filename)).toString(); } catch (Exception e) { } return cacheURL; } /** * Returns the cached bitmap with the expected rotation angle. * @param context the context * @param url the bitmap url * @param rotationAngle the rotation angle (degrees) * @return the bitmap or null if it does not exist */ public static Bitmap bitmapForUrl(Context context, String url, int rotationAngle) { return BitmapWorkerTask.bitmapForURL(context, url, rotationAngle); } /** * Replace a media cache by a file content. * @param context the context * @param mediaUrl the mediaUrl * @param fileUrl the file which replaces the cached media. */ public static void saveFileMediaForUrl(Context context, String mediaUrl, String fileUrl) { saveFileMediaForUrl(context, mediaUrl, fileUrl, -1, -1); } /** * Replace a media cache by a file content. * MediaUrl is the same model as the one used in loadBitmap. * @param context the context * @param mediaUrl the mediaUrl * @param fileUrl the file which replaces the cached media. * @param width the expected image width * @param height the expected image height */ public static void saveFileMediaForUrl(Context context, String mediaUrl, String fileUrl, int width, int height) { String downloadableUrl = downloadableUrl(context, mediaUrl, width, height); String filename = BitmapWorkerTask.buildFileName(downloadableUrl); try { // delete the current content File destFile = new File(context.getFilesDir(), filename); if (destFile.exists()) { try { destFile.delete(); } catch (Exception e) { } } Uri uri = Uri.parse(fileUrl); File srcFile = new File(uri.getPath()); srcFile.renameTo(destFile); } catch (Exception e) { } } /** * Load an avatar thumbnail. * The imageView image is updated when the bitmap is loaded or downloaded. * @param imageView Ihe imageView to update with the image. * @param url the image url * @param side the avatar thumbnail side * @return a download identifier if the image is not cached. */ public static String loadAvatarThumbnail(ImageView imageView, String url, int side) { return loadBitmap(imageView, url, side, side, 0); } /** * Load a bitmap from the url. * The imageView image is updated when the bitmap is loaded or downloaded. * @param imageView Ihe imageView to update with the image. * @param url the image url * @param rotationAngle the rotation angle (degrees) * @return a download identifier if the image is not cached. */ public static String loadBitmap(ImageView imageView, String url, int rotationAngle) { return loadBitmap(imageView, url, -1, -1, rotationAngle); } /** * Load a bitmap from the url. * The imageView image is updated when the bitmap is loaded or downloaded. * @param context The context * @param url the image url * @param rotationAngle the rotation angle (degrees) * @return a download identifier if the image is not cached. */ public static String loadBitmap(Context context, String url, int rotationAngle) { return loadBitmap(context, null, url, -1, -1, rotationAngle); } /** * Load a bitmap from an url. * The imageView image is updated when the bitmap is loaded or downloaded. * The width/height parameters are optional. If they are > 0, download a thumbnail. * @param imageView the imageView to fill when the image is downloaded * @param url the image url * @param width the expected image width * @param height the expected image height * @param rotationAngle the rotation angle (degrees) * @return a download identifier if the image is not cached */ public static String loadBitmap(ImageView imageView, String url, int width, int height, int rotationAngle) { return loadBitmap(imageView.getContext(), imageView, url, width, height, rotationAngle); } /** * Load a bitmap from an url. * The imageView image is updated when the bitmap is loaded or downloaded. * The width/height parameters are optional. If they are > 0, download a thumbnail. * @param context the context * @param imageView the imageView to fill when the image is downloaded * @param url the image url * @param width the expected image width * @param height the expected image height * @param rotationAngle the rotation angle (degrees) * @return a download identifier if the image is not cached */ public static String loadBitmap(Context context, ImageView imageView, String url, int width, int height, int rotationAngle) { if (null == url) { return null; } // request invalid bitmap size if ((0 == width) || (0 == height)) { return null; } String downloadableUrl = downloadableUrl(context, url, width, height); if (null != imageView) { imageView.setTag(downloadableUrl); } // check if the bitmap is already cached Bitmap bitmap = BitmapWorkerTask.bitmapForURL(context.getApplicationContext(), downloadableUrl, rotationAngle); if (null != bitmap) { if (null != imageView) { // display it imageView.setImageBitmap(bitmap); } downloadableUrl = null; } else { BitmapWorkerTask currentTask = BitmapWorkerTask.bitmapWorkerTaskForUrl(downloadableUrl); if (null != currentTask) { if (null != imageView) { currentTask.addImageView(imageView); } } else { // download it in background BitmapWorkerTask task = new BitmapWorkerTask(context, downloadableUrl, rotationAngle); if (null != imageView) { task.addImageView(imageView); } task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null); } } return downloadableUrl; } /** * Returns the download progress (percentage). * @param downloadId the downloadId provided by loadBitmap; * @return the download progress */ public static int progressValueForDownloadId(String downloadId) { BitmapWorkerTask currentTask = BitmapWorkerTask.bitmapWorkerTaskForUrl(downloadId); if (null != currentTask) { return currentTask.getProgress(); } return -1; } /** * Add a download listener for an downloadId. * @param downloadId The uploadId. * @param callback the async callback */ public static void addDownloadListener(String downloadId, DownloadCallback callback) { BitmapWorkerTask currentTask = BitmapWorkerTask.bitmapWorkerTaskForUrl(downloadId); if (null != currentTask) { currentTask.addCallback(callback); } } static class BitmapWorkerTask extends AsyncTask<Integer, Integer, Bitmap> { private static final int MEMORY_CACHE_MB = 16; private static HashMap<String, BitmapWorkerTask> mPendingDownloadByUrl = new HashMap<String, BitmapWorkerTask>(); private static LruCache<String, Bitmap> sMemoryCache = new LruCache<String, Bitmap>( 1024 * 1024 * MEMORY_CACHE_MB) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes() * bitmap.getHeight(); // size in bytes } }; private ArrayList<DownloadCallback> mCallbacks = new ArrayList<DownloadCallback>(); private final ArrayList<WeakReference<ImageView>> mImageViewReferences; private String mUrl; private Context mApplicationContext; private int mRotation = 0; private int mProgress = 0; public static void clearBitmapsCache() { sMemoryCache.evictAll(); } /** * Check if there is a pending download for the url. * @param url The url to check the existence * @return the dedicated BitmapWorkerTask if it exists. */ public static BitmapWorkerTask bitmapWorkerTaskForUrl(String url) { if ((url != null) && mPendingDownloadByUrl.containsKey(url)) { return mPendingDownloadByUrl.get(url); } else { return null; } } /** * Build a filename from an url * @param Url the media url * @return the cache filename */ public static String buildFileName(String Url) { return "file" + Url.hashCode(); } /** * Search a cached bitmap from an url. * @param appContext the context * @param url the media url * @param rotation the bitmap rotation * @return the cached bitmap or null it does not exist */ public static Bitmap bitmapForURL(Context appContext, String url, int rotation) { Bitmap bitmap = null; // sanity check if (null != url) { synchronized (sMemoryCache) { bitmap = sMemoryCache.get(url); } // check if the image has not been saved in file system if ((null == bitmap) && (null != appContext)) { String filename = null; // the url is a file one if (url.startsWith("file:")) { // try to parse it try { Uri uri = Uri.parse(url); filename = uri.getPath(); } catch (Exception e) { } // cannot extract the filename -> sorry if (null == filename) { return null; } } // not a valid file name if (null == filename) { filename = buildFileName(url); } try { FileInputStream fis; if (filename.startsWith(File.separator)) { fis = new FileInputStream(new File(filename)); } else { fis = appContext.getApplicationContext().openFileInput(filename); } if (null != fis) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.ARGB_8888; try { bitmap = BitmapFactory.decodeStream(fis, null, options); } catch (OutOfMemoryError error) { System.gc(); Log.e(LOG_TAG, "bitmapForURL() : Out of memory 1 " + error); } // try again if (null == bitmap) { try { bitmap = BitmapFactory.decodeStream(fis, null, options); } catch (OutOfMemoryError error) { Log.e(LOG_TAG, "bitmapForURL() Out of memory 2" + error); } } if (null != bitmap) { synchronized (sMemoryCache) { if (0 != rotation) { try { android.graphics.Matrix bitmapMatrix = new android.graphics.Matrix(); bitmapMatrix.postRotate(rotation); Bitmap transformedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), bitmapMatrix, false); bitmap.recycle(); bitmap = transformedBitmap; } catch (OutOfMemoryError ex) { } } sMemoryCache.put(url, bitmap); } } fis.close(); } } catch (FileNotFoundException e) { Log.e(LOG_TAG, "bitmapForURL() : " + filename + " does not exist"); } catch (Exception e) { Log.e(LOG_TAG, "bitmapForURL() " + e); } } } return bitmap; } /** * BitmapWorkerTask creator * @param appContext the context * @param url the media url * @param rotation the rotation */ public BitmapWorkerTask(Context appContext, String url, int rotation) { mApplicationContext = appContext; mUrl = url; mRotation = rotation; mPendingDownloadByUrl.put(url, this); mImageViewReferences = new ArrayList<WeakReference<ImageView>>(); } /** * Add an imageView to the list to refresh when the bitmap is downloaded. * @param imageView an image view instance to refresh. */ public void addImageView(ImageView imageView) { mImageViewReferences.add(new WeakReference<ImageView>(imageView)); } /** * Add a download callback. * @param callback the download callback to add */ public void addCallback(DownloadCallback callback) { mCallbacks.add(callback); } /** * Returns the download progress. * @return the download progress */ public int getProgress() { return mProgress; } // Decode image in background. @Override protected Bitmap doInBackground(Integer... params) { try { // check the in-memory cache String key = mUrl; URL url = new URL(mUrl); Log.d(LOG_TAG, "BitmapWorkerTask open >>>>> " + mUrl); InputStream stream = null; Bitmap bitmap = null; long filelen = -1; try { URLConnection connection = url.openConnection(); filelen = connection.getContentLength(); stream = connection.getInputStream(); } catch (FileNotFoundException e) { Log.d(LOG_TAG, "BitmapWorkerTask " + mUrl + " does not exist"); bitmap = BitmapFactory.decodeResource(mApplicationContext.getResources(), R.drawable.ic_menu_gallery); } String filename = BitmapWorkerTask.buildFileName(mUrl); FileOutputStream fos = mApplicationContext.openFileOutput(filename, Context.MODE_PRIVATE); // a bitmap has been provided if (null != bitmap) { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos); } else { try { int totalDownloaded = 0; byte[] buf = new byte[1024 * 32]; int len; while ((len = stream.read(buf)) != -1) { fos.write(buf, 0, len); totalDownloaded += len; int progress = 0; if (filelen > 0) { if (totalDownloaded >= filelen) { progress = 99; } else { progress = (int) (totalDownloaded * 100 / filelen); } } else { progress = -1; } Log.d(LOG_TAG, "download " + progress + " (" + mUrl + ")"); publishProgress(mProgress = progress); } } catch (OutOfMemoryError outOfMemoryError) { outOfMemoryError = outOfMemoryError; } catch (Exception e) { e = e; } close(stream); } fos.flush(); fos.close(); Log.d(LOG_TAG, "download is done (" + mUrl + ")"); // get the bitmap from the filesytem if (null == bitmap) { bitmap = BitmapWorkerTask.bitmapForURL(mApplicationContext, key, mRotation); } synchronized (sMemoryCache) { cacheBitmap(key, bitmap); } return bitmap; } catch (Exception e) { Log.e(LOG_TAG, "Unable to load bitmap: " + e); return null; } } /** * Dispatch progress update to the callbacks. * @param progress the new progress value */ private void sendProgress(int progress) { for (DownloadCallback callback : mCallbacks) { try { callback.onDownloadProgress(mUrl, progress); } catch (Exception e) { } } } /** * Dispatch end of download */ private void sendDownloadComplete() { for (DownloadCallback callback : mCallbacks) { try { callback.onDownloadComplete(mUrl); } catch (Exception e) { } } } @Override protected void onProgressUpdate(Integer... progress) { super.onProgressUpdate(progress); sendProgress(progress[0]); } // Once complete, see if ImageView is still around and set bitmap. @Override protected void onPostExecute(Bitmap bitmap) { sendDownloadComplete(); // update the imageView image if (bitmap != null) { for (WeakReference<ImageView> weakRef : mImageViewReferences) { final ImageView imageView = weakRef.get(); if (imageView != null && mUrl.equals(imageView.getTag())) { imageView.setImageBitmap(bitmap); } } } mPendingDownloadByUrl.remove(mUrl); } private void cacheBitmap(String key, Bitmap bitmap) { // for now we'll just in-memory cache this. In future, they should be written to the // cache directory as well. sMemoryCache.put(key, bitmap); } private void close(InputStream stream) { try { stream.close(); } catch (Exception e) { } // don't care, it's being closed! } } }