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.androidsdk.db; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.net.Uri; import android.os.AsyncTask; import android.support.v4.util.LruCache; import android.text.TextUtils; import android.util.Log; import android.webkit.MimeTypeMap; import android.widget.ImageView; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import org.matrix.androidsdk.HomeserverConnectionConfig; import org.matrix.androidsdk.ssl.CertUtil; import org.matrix.androidsdk.util.ImageUtils; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.ref.WeakReference; import java.net.URL; import java.net.URLConnection; import java.security.MessageDigest; import java.util.ArrayList; import java.util.HashMap; import javax.net.ssl.HttpsURLConnection; class MXMediaWorkerTask extends AsyncTask<Integer, Integer, Bitmap> { private static final String LOG_TAG = "MediaWorkerTask"; private static HashMap<String, MXMediaWorkerTask> mPendingDownloadByUrl = new HashMap<String, MXMediaWorkerTask>(); private static ArrayList<String> mFileNotFoundUrlsList = new ArrayList<String>(); private static LruCache<String, Bitmap> sMemoryCache = null; private ArrayList<MXMediasCache.DownloadCallback> mCallbacks = new ArrayList<MXMediasCache.DownloadCallback>(); private final ArrayList<WeakReference<ImageView>> mImageViewReferences; private String mUrl; private String mMimeType; private Context mApplicationContext; private File mDirectoryFile = null; private int mRotation = 0; private int mProgress = 0; private JsonElement mErrorAsJsonElement; private final HomeserverConnectionConfig mHsConfig; public static void clearBitmapsCache() { // sMemoryCache can be null if no bitmap have been downloaded. if (null != sMemoryCache) { sMemoryCache.evictAll(); } } public String getUrl() { return mUrl; } /** * 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 MXMediaWorkerTask mediaWorkerTaskForUrl(String url) { if ((url != null) && mPendingDownloadByUrl.containsKey(url)) { MXMediaWorkerTask task; synchronized (mPendingDownloadByUrl) { task = mPendingDownloadByUrl.get(url); } return task; } else { return null; } } /** * Generate an unique ID for a string * @param input the string * @return the unique ID */ private static String uniqueId(String input) { String uniqueId = null; try { MessageDigest mDigest = MessageDigest.getInstance("SHA1"); byte[] result = mDigest.digest(input.getBytes()); StringBuffer sb = new StringBuffer(); for (int i = 0; i < result.length; i++) { sb.append(Integer.toString((result[i] & 0xff) + 0x100, 16).substring(1)); } uniqueId = sb.toString(); } catch (Exception e) { Log.e(LOG_TAG, "uniqueId failed " + e.getLocalizedMessage()); } if (null == uniqueId) { uniqueId = "" + Math.abs(input.hashCode() + (System.currentTimeMillis() + "").hashCode()); } return uniqueId; } /** * Build a filename from an url * @param Url the media url * @param mimeType the mime type; * @return the cache filename */ public static String buildFileName(String Url, String mimeType) { String name = "file_" + MXMediaWorkerTask.uniqueId(Url); if (null == mimeType) { mimeType = "image/jpeg"; } String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); // some devices don't support .jpeg files if ("jpeg".equals(fileExtension)) { fileExtension = "jpg"; } if (null != fileExtension) { name += "." + fileExtension; } return name; } /** * Tell if the media is cached * @param url the media url * @return true if the media is cached */ public static boolean isUrlCached(String url) { boolean res = false; if ((null != sMemoryCache) && (null != url)) { synchronized (sMemoryCache) { res = (null != sMemoryCache.get(url)); } } return res; } /** * Search a cached bitmap from an url. * rotationAngle is set to Integer.MAX_VALUE when undefined : the EXIF metadata must be checked. * * @param baseFile the base file * @param url the media url * @param rotation the bitmap rotation * @param mimeType the mime type * @return the cached bitmap or null it does not exist */ public static Bitmap bitmapForURL(Context context, File baseFile, String url, int rotation, String mimeType) { Bitmap bitmap = null; // sanity check if (null != url) { if (null == sMemoryCache) { int lruSize = Math.min(20 * 1024 * 1024, (int) Runtime.getRuntime().maxMemory() / 8); Log.d(LOG_TAG, "bitmapForURL lruSize : " + lruSize); sMemoryCache = new LruCache<String, Bitmap>(lruSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes() * bitmap.getHeight(); // size in bytes } }; } // the image is downloading in background if (null != mediaWorkerTaskForUrl(url)) { return null; } synchronized (sMemoryCache) { bitmap = sMemoryCache.get(url); } if (null == bitmap) { // if some medias are not found // do not try to reload them until the next application launch. synchronized (mFileNotFoundUrlsList) { if (mFileNotFoundUrlsList.indexOf(url) >= 0) { bitmap = BitmapFactory.decodeResource(context.getResources(), android.R.drawable.ic_menu_gallery); } } } // check if the image has not been saved in file system if ((null == bitmap) && (null != baseFile)) { 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, mimeType); } try { File file = filename.startsWith(File.separator) ? new File(filename) : new File(baseFile, filename); if (!file.exists()) { Log.d(LOG_TAG, "bitmapForURL() : " + filename + " does not exist"); return null; } FileInputStream fis = new FileInputStream(file); // read the metadata if (Integer.MAX_VALUE == rotation) { rotation = ImageUtils.getRotationAngleForBitmap(context, Uri.fromFile(file)); } 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) { } } // cache only small images // caching large images does not make sense // it would replace small ones. // let assume that the application must be faster when showing the chat history. if ((bitmap.getWidth() < 1000) && (bitmap.getHeight() < 1000)) { sMemoryCache.put(url, bitmap); } } } fis.close(); } } catch (FileNotFoundException e) { Log.d(LOG_TAG, "bitmapForURL() : " + filename + " does not exist"); } catch (Exception e) { Log.e(LOG_TAG, "bitmapForURL() " + e); } } } return bitmap; } private void commonInit(Context appContext, String url, String mimeType) { mApplicationContext = appContext; mUrl = url; synchronized (mPendingDownloadByUrl) { mPendingDownloadByUrl.put(url, this); } mMimeType = mimeType; mRotation = 0; } /** * BitmapWorkerTask creator * @param appContext the context * @param hsConfig * @param directoryFile the directry in which the media must be stored * @param url the media url * @param mimeType the mime type. */ public MXMediaWorkerTask(Context appContext, HomeserverConnectionConfig hsConfig, File directoryFile, String url, String mimeType) { commonInit(appContext, url, mimeType); mDirectoryFile = directoryFile; mImageViewReferences = new ArrayList<WeakReference<ImageView>>(); mHsConfig = hsConfig; } /** * BitmapWorkerTask creator * @param appContext the context * @param hsConfig * @param directoryFile the directry in which the media must be stored * @param url the media url * @param rotation the rotation * @param mimeType the mime type. */ public MXMediaWorkerTask(Context appContext, HomeserverConnectionConfig hsConfig, File directoryFile, String url, int rotation, String mimeType) { commonInit(appContext, url, mimeType); mImageViewReferences = new ArrayList<WeakReference<ImageView>>(); mDirectoryFile = directoryFile; mRotation = rotation; mHsConfig = hsConfig; } /** * BitmapWorkerTask creator * @param task another bitmap task */ public MXMediaWorkerTask(MXMediaWorkerTask task) { mApplicationContext = task.mApplicationContext; mUrl = task.mUrl; mRotation = task.mRotation; synchronized (mPendingDownloadByUrl) { mPendingDownloadByUrl.put(mUrl, this); } mMimeType = task.mMimeType; mImageViewReferences = task.mImageViewReferences; mHsConfig = task.mHsConfig; } /** * 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(MXMediasCache.DownloadCallback callback) { mCallbacks.add(callback); } /** * Returns the download progress. * @return the download progress */ public int getProgress() { return mProgress; } private boolean isBitmapDownload() { return (null == mMimeType) || mMimeType.startsWith("image/"); } // 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; URLConnection connection = null; try { connection = url.openConnection(); if (mHsConfig != null && connection instanceof HttpsURLConnection) { // Add SSL Socket factory. HttpsURLConnection sslConn = (HttpsURLConnection) connection; try { sslConn.setSSLSocketFactory(CertUtil.newPinnedSSLSocketFactory(mHsConfig)); sslConn.setHostnameVerifier(CertUtil.newHostnameVerifier(mHsConfig)); } catch (Exception e) { Log.e(LOG_TAG, "doInBackground SSL exception " + e.getLocalizedMessage()); } } // add a timeout to avoid infinite loading display. connection.setReadTimeout(10 * 1000); filelen = connection.getContentLength(); stream = connection.getInputStream(); } catch (FileNotFoundException e) { InputStream errorStream = ((HttpsURLConnection) connection).getErrorStream(); if (null != errorStream) { try { BufferedReader streamReader = new BufferedReader( new InputStreamReader(errorStream, "UTF-8")); StringBuilder responseStrBuilder = new StringBuilder(); String inputStr; while ((inputStr = streamReader.readLine()) != null) { responseStrBuilder.append(inputStr); } mErrorAsJsonElement = new JsonParser().parse(responseStrBuilder.toString()); } catch (Exception ee) { } } Log.d(LOG_TAG, "MediaWorkerTask " + mUrl + " does not exist"); if (isBitmapDownload()) { bitmap = BitmapFactory.decodeResource(mApplicationContext.getResources(), android.R.drawable.ic_menu_gallery); // if some medias are not found // do not try to reload them until the next application launch. synchronized (mFileNotFoundUrlsList) { mFileNotFoundUrlsList.add(mUrl); } } } sendStart(); String filename = MXMediaWorkerTask.buildFileName(mUrl, mMimeType) + ".tmp"; FileOutputStream fos = new FileOutputStream(new File(mDirectoryFile, filename)); // 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); } mProgress = 100; } catch (OutOfMemoryError outOfMemoryError) { Log.e(LOG_TAG, "MediaWorkerTask : out of memory"); } catch (Exception e) { Log.e(LOG_TAG, "MediaWorkerTask fail to read image " + e.getMessage()); } close(stream); } fos.flush(); fos.close(); // the file has been successfully downloaded if (mProgress == 100) { try { File originalFile = new File(mDirectoryFile, filename); String newFileName = MXMediaWorkerTask.buildFileName(mUrl, mMimeType); File newFile = new File(mDirectoryFile, newFileName); if (newFile.exists()) { // Or you could throw here. mApplicationContext.deleteFile(newFileName); } originalFile.renameTo(newFile); } catch (Exception e) { } } Log.d(LOG_TAG, "download is done (" + mUrl + ")"); synchronized (mPendingDownloadByUrl) { mPendingDownloadByUrl.remove(mUrl); } // load the bitmap from the cache if (isBitmapDownload()) { // get the bitmap from the filesytem if (null == bitmap) { bitmap = MXMediaWorkerTask.bitmapForURL(mApplicationContext, mDirectoryFile, key, mRotation, mMimeType); } } return bitmap; } catch (Exception e) { // remove the image from the loading one // else the loading will be stucked (and never be tried again). synchronized (mPendingDownloadByUrl) { mPendingDownloadByUrl.remove(mUrl); } Log.e(LOG_TAG, "Unable to load bitmap: " + e); return null; } } /** * Dispatch start event to the callbacks. */ private void sendStart() { for (MXMediasCache.DownloadCallback callback : mCallbacks) { try { callback.onDownloadStart(mUrl); } catch (Exception e) { } } } /** * Dispatch progress update to the callbacks. * @param progress the new progress value */ private void sendProgress(int progress) { for (MXMediasCache.DownloadCallback callback : mCallbacks) { try { callback.onDownloadProgress(mUrl, progress); } catch (Exception e) { } } } /** * Dispatch error message. * @param jsonElement the Json error */ private void sendError(JsonElement jsonElement) { for (MXMediasCache.DownloadCallback callback : mCallbacks) { try { callback.onError(mUrl, jsonElement); } catch (Exception e) { } } } /** * Dispatch end of download */ private void sendDownloadComplete() { for (MXMediasCache.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) { if (null != mErrorAsJsonElement) { sendError(mErrorAsJsonElement); } sendDownloadComplete(); // update the imageView image if (bitmap != null) { for (WeakReference<ImageView> weakRef : mImageViewReferences) { final ImageView imageView = weakRef.get(); if (imageView != null && TextUtils.equals(mUrl, (String) imageView.getTag())) { imageView.setBackgroundColor(Color.TRANSPARENT); imageView.setImageBitmap(bitmap); } } } } private void close(InputStream stream) { try { stream.close(); } catch (Exception e) { } // don't care, it's being closed! } }