forked from GitHub-Mirror/riotX-android
1158 lines
41 KiB
Java
1158 lines
41 KiB
Java
/*
|
|
* Copyright 2015 OpenMarket Ltd
|
|
* Copyright 2018 New Vector 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 im.vector.matrix.android.internal.legacy.db;
|
|
|
|
import android.content.Context;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.BitmapFactory;
|
|
import android.net.Uri;
|
|
import android.os.AsyncTask;
|
|
import android.support.annotation.Nullable;
|
|
import android.support.v4.util.LruCache;
|
|
import android.text.TextUtils;
|
|
import android.util.Pair;
|
|
import android.webkit.MimeTypeMap;
|
|
import android.widget.ImageView;
|
|
|
|
import com.google.gson.JsonElement;
|
|
import com.google.gson.JsonParser;
|
|
|
|
import im.vector.matrix.android.internal.legacy.HomeServerConnectionConfig;
|
|
import im.vector.matrix.android.internal.legacy.RestClient;
|
|
import im.vector.matrix.android.internal.legacy.crypto.MXEncryptedAttachments;
|
|
import im.vector.matrix.android.internal.legacy.listeners.IMXMediaDownloadListener;
|
|
import im.vector.matrix.android.internal.legacy.network.NetworkConnectivityReceiver;
|
|
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
|
|
import im.vector.matrix.android.internal.legacy.rest.callback.SimpleApiCallback;
|
|
import im.vector.matrix.android.internal.legacy.rest.client.MediaScanRestClient;
|
|
import im.vector.matrix.android.internal.legacy.rest.model.EncryptedMediaScanBody;
|
|
import im.vector.matrix.android.internal.legacy.rest.model.EncryptedMediaScanEncryptedBody;
|
|
import im.vector.matrix.android.internal.legacy.rest.model.MatrixError;
|
|
import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedBodyFileInfo;
|
|
import im.vector.matrix.android.internal.legacy.rest.model.crypto.EncryptedFileInfo;
|
|
import im.vector.matrix.android.internal.legacy.ssl.CertUtil;
|
|
import im.vector.matrix.android.internal.legacy.util.ImageUtils;
|
|
import im.vector.matrix.android.internal.legacy.util.JsonUtils;
|
|
import im.vector.matrix.android.internal.legacy.util.Log;
|
|
import org.matrix.olm.OlmPkEncryption;
|
|
import org.matrix.olm.OlmPkMessage;
|
|
|
|
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.io.OutputStream;
|
|
import java.lang.ref.WeakReference;
|
|
import java.net.HttpURLConnection;
|
|
import java.net.URL;
|
|
import java.security.MessageDigest;
|
|
import java.util.ArrayList;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Timer;
|
|
import java.util.TimerTask;
|
|
import java.util.concurrent.CountDownLatch;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import javax.net.ssl.HttpsURLConnection;
|
|
import javax.net.ssl.SSLSocketFactory;
|
|
import javax.net.ssl.X509TrustManager;
|
|
|
|
/**
|
|
* This class manages the media downloading in background.
|
|
* <p>
|
|
* JsonElement: Error message if not null.
|
|
*/
|
|
class MXMediaDownloadWorkerTask extends AsyncTask<Void, Void, JsonElement> {
|
|
|
|
private static final String LOG_TAG = MXMediaDownloadWorkerTask.class.getSimpleName();
|
|
|
|
/**
|
|
* Pending media URLs
|
|
*/
|
|
private static final Map<String, MXMediaDownloadWorkerTask> sPendingDownloadById = new HashMap<>();
|
|
|
|
/**
|
|
* List of unreachable media urls.
|
|
*/
|
|
private static final List<String> sUnreachableUrls = new ArrayList<>();
|
|
|
|
// avoid sync on "this" because it might differ if there is a timer.
|
|
private static final Object sSyncObject = new Object();
|
|
|
|
/**
|
|
* The medias cache
|
|
*/
|
|
private static LruCache<String, Bitmap> sBitmapByDownloadIdCache = null;
|
|
|
|
/**
|
|
* The downloaded media callbacks.
|
|
*/
|
|
private final List<IMXMediaDownloadListener> mDownloadListeners = new ArrayList<>();
|
|
|
|
/**
|
|
* The ImageView list to refresh when the media is downloaded.
|
|
*/
|
|
private final List<WeakReference<ImageView>> mImageViewReferences;
|
|
|
|
/**
|
|
* The media URL.
|
|
*/
|
|
private String mUrl;
|
|
|
|
/**
|
|
* The download identifier based on the original matrix content url for this media.
|
|
*/
|
|
private String mDownloadId;
|
|
|
|
/**
|
|
* Tells if the anti-virus scanner is enabled.
|
|
*/
|
|
private boolean mIsAvScannerEnabled;
|
|
|
|
/**
|
|
* The media mime type
|
|
*/
|
|
private String mMimeType;
|
|
|
|
/**
|
|
* The application context
|
|
*/
|
|
private Context mApplicationContext;
|
|
|
|
/**
|
|
* The directory in which the media must be stored.
|
|
*/
|
|
private File mDirectoryFile;
|
|
|
|
/**
|
|
* The rotation to apply.
|
|
*/
|
|
private int mRotation;
|
|
|
|
/**
|
|
* The download stats.
|
|
*/
|
|
private IMXMediaDownloadListener.DownloadStats mDownloadStats;
|
|
|
|
/**
|
|
* Tells the download has been cancelled.
|
|
*/
|
|
private boolean mIsDownloadCancelled;
|
|
|
|
/**
|
|
* Tells if the download has been completed
|
|
*/
|
|
private boolean mIsDone;
|
|
|
|
/**
|
|
* The home server config.
|
|
*/
|
|
private final HomeServerConnectionConfig mHsConfig;
|
|
|
|
/**
|
|
* The bitmap to use when the URL is unreachable.
|
|
*/
|
|
private Bitmap mDefaultBitmap;
|
|
|
|
/**
|
|
* the encrypted file information
|
|
*/
|
|
private final EncryptedFileInfo mEncryptedFileInfo;
|
|
|
|
/**
|
|
* Network updates tracker
|
|
*/
|
|
private final NetworkConnectivityReceiver mNetworkConnectivityReceiver;
|
|
|
|
/**
|
|
* Rest client to retrieve public antivirus server key
|
|
*/
|
|
@Nullable
|
|
private MediaScanRestClient mMediaScanRestClient;
|
|
|
|
/**
|
|
* Download constants
|
|
*/
|
|
private static final int DOWNLOAD_TIME_OUT = 10 * 1000;
|
|
private static final int DOWNLOAD_BUFFER_READ_SIZE = 1024 * 32;
|
|
|
|
//==============================================================================================================
|
|
// static methods
|
|
//==============================================================================================================
|
|
|
|
/**
|
|
* Clear the internal cache.
|
|
*/
|
|
public static void clearBitmapsCache() {
|
|
if (null != sBitmapByDownloadIdCache) {
|
|
sBitmapByDownloadIdCache.evictAll();
|
|
}
|
|
|
|
// Clear the list of unreachable Urls, to retry to download it on next access
|
|
synchronized (sUnreachableUrls) {
|
|
sUnreachableUrls.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if there is a pending download with the provided id.
|
|
*
|
|
* @param downloadId The identifier to check
|
|
* @return the dedicated MXMediaDownloadWorkerTask if it exists.
|
|
*/
|
|
public static MXMediaDownloadWorkerTask getMediaDownloadWorkerTask(String downloadId) {
|
|
if (sPendingDownloadById.containsKey(downloadId)) {
|
|
MXMediaDownloadWorkerTask task;
|
|
synchronized (sPendingDownloadById) {
|
|
task = sPendingDownloadById.get(downloadId);
|
|
}
|
|
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.getMessage(), e);
|
|
}
|
|
|
|
if (null == uniqueId) {
|
|
uniqueId = "" + Math.abs(input.hashCode() + (System.currentTimeMillis() + "").hashCode());
|
|
}
|
|
|
|
return uniqueId;
|
|
}
|
|
|
|
/**
|
|
* Build a filename from a download Id
|
|
*
|
|
* @param downloadId the media url
|
|
* @param mimeType the mime type;
|
|
* @return the cache filename
|
|
*/
|
|
static String buildFileName(String downloadId, String mimeType) {
|
|
String name = "file_" + uniqueId(downloadId);
|
|
|
|
if (!TextUtils.isEmpty(mimeType)) {
|
|
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 with the provided cache identifier
|
|
*
|
|
* @param mediaCacheId
|
|
* @return true if a media is cached with this identifier
|
|
*/
|
|
public static boolean isMediaCached(String mediaCacheId) {
|
|
boolean res = false;
|
|
|
|
if ((null != sBitmapByDownloadIdCache)) {
|
|
synchronized (sSyncObject) {
|
|
res = (null != sBitmapByDownloadIdCache.get(mediaCacheId));
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Tells if the media URL is unreachable.
|
|
*
|
|
* @param url the url to test.
|
|
* @return true if the media URL is unreachable.
|
|
*/
|
|
public static boolean isMediaUrlUnreachable(String url) {
|
|
boolean res = true;
|
|
|
|
if (!TextUtils.isEmpty(url)) {
|
|
synchronized (sUnreachableUrls) {
|
|
res = sUnreachableUrls.contains(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 actual media url
|
|
* @param downloadId the predefined id of the download task for this content
|
|
* @param aRotation the bitmap rotation
|
|
* @param mimeType the mime type
|
|
* @param encryptionInfo the encryption information
|
|
* @return true if the bitmap is cached
|
|
*/
|
|
static boolean bitmapForURL(final Context context,
|
|
final File baseFile,
|
|
final String url,
|
|
final String downloadId,
|
|
final int aRotation,
|
|
final String mimeType,
|
|
final EncryptedFileInfo encryptionInfo,
|
|
final ApiCallback<Bitmap> callback) {
|
|
if (TextUtils.isEmpty(url)) {
|
|
Log.d(LOG_TAG, "bitmapForURL : null url");
|
|
return false;
|
|
}
|
|
|
|
if (null == sBitmapByDownloadIdCache) {
|
|
int lruSize = Math.min(20 * 1024 * 1024, (int) Runtime.getRuntime().maxMemory() / 8);
|
|
|
|
Log.d(LOG_TAG, "bitmapForURL lruSize : " + lruSize);
|
|
|
|
sBitmapByDownloadIdCache = 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 != getMediaDownloadWorkerTask(downloadId)) {
|
|
return false;
|
|
}
|
|
|
|
// the url is invalid
|
|
if (isMediaUrlUnreachable(url)) {
|
|
return false;
|
|
}
|
|
|
|
final Bitmap cachedBitmap;
|
|
|
|
synchronized (sSyncObject) {
|
|
cachedBitmap = sBitmapByDownloadIdCache.get(downloadId);
|
|
}
|
|
|
|
if (null != cachedBitmap) {
|
|
MXMediasCache.mUIHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
callback.onSuccess(cachedBitmap);
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// invalid basefile
|
|
if (null == baseFile) {
|
|
return false;
|
|
}
|
|
|
|
// check if the image has not been saved in file system
|
|
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) {
|
|
Log.e(LOG_TAG, "bitmapForURL #1 : " + e.getMessage(), e);
|
|
}
|
|
|
|
// cannot extract the filename -> sorry
|
|
if (null == filename) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// not a valid file name
|
|
if (null == filename) {
|
|
filename = buildFileName(downloadId, mimeType);
|
|
}
|
|
|
|
final String fFilename = filename;
|
|
final File file = filename.startsWith(File.separator) ? new File(filename) : new File(baseFile, filename);
|
|
|
|
if (!file.exists()) {
|
|
return false;
|
|
}
|
|
|
|
MXMediasCache.mDecryptingHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
Bitmap bitmap = null;
|
|
int rotation = aRotation;
|
|
|
|
try {
|
|
|
|
InputStream fis = new FileInputStream(file);
|
|
|
|
if (null != encryptionInfo) {
|
|
InputStream decryptedIs = MXEncryptedAttachments.decryptAttachment(fis, encryptionInfo);
|
|
fis.close();
|
|
fis = decryptedIs;
|
|
}
|
|
|
|
// 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, 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, error);
|
|
}
|
|
}
|
|
|
|
if (null != bitmap) {
|
|
synchronized (sSyncObject) {
|
|
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.createBitmap() can return the same bitmap, so do not recycle it if it is the case
|
|
if (transformedBitmap != bitmap) {
|
|
bitmap.recycle();
|
|
}
|
|
|
|
bitmap = transformedBitmap;
|
|
} catch (OutOfMemoryError ex) {
|
|
Log.e(LOG_TAG, "bitmapForURL rotation error : " + ex.getMessage(), 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)) {
|
|
sBitmapByDownloadIdCache.put(downloadId, bitmap);
|
|
}
|
|
}
|
|
}
|
|
|
|
fis.close();
|
|
}
|
|
|
|
} catch (FileNotFoundException e) {
|
|
Log.d(LOG_TAG, "bitmapForURL() : " + fFilename + " does not exist");
|
|
} catch (Exception e) {
|
|
Log.e(LOG_TAG, "bitmapForURL() " + e, e);
|
|
|
|
}
|
|
|
|
final Bitmap fBitmap = bitmap;
|
|
MXMediasCache.mUIHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
callback.onSuccess(fBitmap);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
//==============================================================================================================
|
|
// class methods
|
|
//==============================================================================================================
|
|
|
|
/**
|
|
* MXMediaDownloadWorkerTask creator
|
|
*
|
|
* @param appContext the context
|
|
* @param hsConfig the home server config
|
|
* @param networkConnectivityReceiver the network connectivity receiver
|
|
* @param directoryFile the directory in which the media must be stored
|
|
* @param url the media url
|
|
* @param downloadId the predefined id of the download task for this content
|
|
* @param rotation the rotation angle (degrees), use 0 by default
|
|
* @param mimeType the mime type.
|
|
* @param encryptedFileInfo the encryption information
|
|
* @param mediaScanRestClient the media scan rest client
|
|
* @param isAvScannerEnabled tell whether an anti-virus scanner is enabled
|
|
*/
|
|
public MXMediaDownloadWorkerTask(Context appContext,
|
|
HomeServerConnectionConfig hsConfig,
|
|
NetworkConnectivityReceiver networkConnectivityReceiver,
|
|
File directoryFile,
|
|
String url,
|
|
String downloadId,
|
|
int rotation,
|
|
String mimeType,
|
|
EncryptedFileInfo encryptedFileInfo,
|
|
@Nullable MediaScanRestClient mediaScanRestClient,
|
|
boolean isAvScannerEnabled) {
|
|
mApplicationContext = appContext;
|
|
mHsConfig = hsConfig;
|
|
mNetworkConnectivityReceiver = networkConnectivityReceiver;
|
|
mDirectoryFile = directoryFile;
|
|
mUrl = url;
|
|
mDownloadId = downloadId;
|
|
mRotation = rotation;
|
|
mMimeType = mimeType;
|
|
mEncryptedFileInfo = encryptedFileInfo;
|
|
mMediaScanRestClient = mediaScanRestClient;
|
|
mIsAvScannerEnabled = isAvScannerEnabled;
|
|
|
|
mImageViewReferences = new ArrayList<>();
|
|
|
|
synchronized (sPendingDownloadById) {
|
|
sPendingDownloadById.put(downloadId, this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* MXMediaDownloadWorkerTask creator
|
|
*
|
|
* @param task another bitmap task
|
|
*/
|
|
public MXMediaDownloadWorkerTask(MXMediaDownloadWorkerTask task) {
|
|
mApplicationContext = task.mApplicationContext;
|
|
mHsConfig = task.mHsConfig;
|
|
mNetworkConnectivityReceiver = task.mNetworkConnectivityReceiver;
|
|
mDirectoryFile = task.mDirectoryFile;
|
|
mUrl = task.mUrl;
|
|
mDownloadId = task.mDownloadId;
|
|
mRotation = task.mRotation;
|
|
mMimeType = task.mMimeType;
|
|
mEncryptedFileInfo = task.mEncryptedFileInfo;
|
|
mIsAvScannerEnabled = task.mIsAvScannerEnabled;
|
|
mMediaScanRestClient = task.mMediaScanRestClient;
|
|
|
|
mImageViewReferences = task.mImageViewReferences;
|
|
|
|
synchronized (sPendingDownloadById) {
|
|
sPendingDownloadById.put(mDownloadId, this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancels the current download.
|
|
*/
|
|
public synchronized void cancelDownload() {
|
|
mIsDownloadCancelled = true;
|
|
}
|
|
|
|
/**
|
|
* @return tells if the current download has been cancelled.
|
|
*/
|
|
public synchronized boolean isDownloadCancelled() {
|
|
return mIsDownloadCancelled;
|
|
}
|
|
|
|
/**
|
|
* @return the media URL.
|
|
*/
|
|
public String getUrl() {
|
|
return mUrl;
|
|
}
|
|
|
|
/**
|
|
* 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));
|
|
}
|
|
|
|
/**
|
|
* Set the default bitmap to use when the Url is unreachable.
|
|
*
|
|
* @param aBitmap the bitmap.
|
|
*/
|
|
public void setDefaultBitmap(Bitmap aBitmap) {
|
|
mDefaultBitmap = aBitmap;
|
|
}
|
|
|
|
/**
|
|
* Add a download listener.
|
|
*
|
|
* @param listener the listener to add.
|
|
*/
|
|
public void addDownloadListener(IMXMediaDownloadListener listener) {
|
|
if (null != listener) {
|
|
mDownloadListeners.add(listener);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the download progress.
|
|
*
|
|
* @return the download progress
|
|
*/
|
|
public int getProgress() {
|
|
if (null != mDownloadStats) {
|
|
return mDownloadStats.mProgress;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* @return the download stats
|
|
*/
|
|
public IMXMediaDownloadListener.DownloadStats getDownloadStats() {
|
|
return mDownloadStats;
|
|
}
|
|
|
|
/**
|
|
* @return true if the current task is an image one.
|
|
*/
|
|
private boolean isBitmapDownloadTask() {
|
|
return null != mMimeType && mMimeType.startsWith("image/");
|
|
}
|
|
|
|
/**
|
|
* Push the download progress.
|
|
*
|
|
* @param startDownloadTime the start download time.
|
|
*/
|
|
private void updateAndPublishProgress(long startDownloadTime) {
|
|
mDownloadStats.mElapsedTime = (int) ((System.currentTimeMillis() - startDownloadTime) / 1000);
|
|
|
|
if (mDownloadStats.mFileSize > 0) {
|
|
if (mDownloadStats.mDownloadedSize >= mDownloadStats.mFileSize) {
|
|
mDownloadStats.mProgress = 99;
|
|
} else {
|
|
mDownloadStats.mProgress = (int) (mDownloadStats.mDownloadedSize * 100L / mDownloadStats.mFileSize);
|
|
}
|
|
} else {
|
|
mDownloadStats.mProgress = -1;
|
|
}
|
|
|
|
// avoid zero div
|
|
if (System.currentTimeMillis() != startDownloadTime) {
|
|
mDownloadStats.mBitRate = (int) (mDownloadStats.mDownloadedSize * 1000L / (System.currentTimeMillis() - startDownloadTime) / 1024);
|
|
} else {
|
|
mDownloadStats.mBitRate = -1;
|
|
}
|
|
|
|
if ((0 != mDownloadStats.mBitRate) && (mDownloadStats.mFileSize > 0) && (mDownloadStats.mFileSize > mDownloadStats.mDownloadedSize)) {
|
|
mDownloadStats.mEstimatedRemainingTime = (mDownloadStats.mFileSize - mDownloadStats.mDownloadedSize) / 1024 / mDownloadStats.mBitRate;
|
|
} else {
|
|
mDownloadStats.mEstimatedRemainingTime = -1;
|
|
}
|
|
|
|
Log.d(LOG_TAG, "updateAndPublishProgress " + this + " : " + mDownloadStats.mProgress);
|
|
|
|
publishProgress();
|
|
}
|
|
|
|
/**
|
|
* Download and decode media in background.
|
|
*
|
|
* @param params
|
|
* @return JsonElement if an error occurs
|
|
*/
|
|
@Override
|
|
protected JsonElement doInBackground(Void... params) {
|
|
JsonElement jsonElementResult = null;
|
|
|
|
MatrixError defaultError = new MatrixError();
|
|
defaultError.errcode = MatrixError.UNKNOWN;
|
|
|
|
// Note: No need for access token here
|
|
|
|
try {
|
|
URL url = new URL(mUrl);
|
|
Log.d(LOG_TAG, "MXMediaDownloadWorkerTask " + this + " starts");
|
|
|
|
mDownloadStats = new IMXMediaDownloadListener.DownloadStats();
|
|
// don't known yet
|
|
mDownloadStats.mEstimatedRemainingTime = -1;
|
|
|
|
InputStream stream = null;
|
|
|
|
int filelen = -1;
|
|
HttpURLConnection connection = null;
|
|
|
|
try {
|
|
connection = (HttpURLConnection) url.openConnection();
|
|
|
|
if (RestClient.getUserAgent() != null) {
|
|
connection.setRequestProperty("User-Agent", RestClient.getUserAgent());
|
|
}
|
|
|
|
if (mHsConfig != null && connection instanceof HttpsURLConnection) {
|
|
// Add SSL Socket factory.
|
|
HttpsURLConnection sslConn = (HttpsURLConnection) connection;
|
|
try {
|
|
Pair<SSLSocketFactory, X509TrustManager> pair = CertUtil.newPinnedSSLSocketFactory(mHsConfig);
|
|
sslConn.setSSLSocketFactory(pair.first);
|
|
sslConn.setHostnameVerifier(CertUtil.newHostnameVerifier(mHsConfig));
|
|
} catch (Exception e) {
|
|
Log.e(LOG_TAG, "doInBackground SSL exception " + e.getMessage(), e);
|
|
}
|
|
}
|
|
|
|
// add a timeout to avoid infinite loading display.
|
|
float scale = (null != mNetworkConnectivityReceiver) ? mNetworkConnectivityReceiver.getTimeoutScale() : 1.0f;
|
|
connection.setReadTimeout((int) (DOWNLOAD_TIME_OUT * scale));
|
|
|
|
if (mIsAvScannerEnabled && null != mEncryptedFileInfo) {
|
|
// POST the encryption info to let the av scanner decrypt and scan the content.
|
|
connection.setRequestMethod("POST");
|
|
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
|
connection.setDoOutput(true);
|
|
connection.setUseCaches(false);
|
|
|
|
EncryptedMediaScanBody encryptedMediaScanBody = new EncryptedMediaScanBody();
|
|
encryptedMediaScanBody.encryptedFileInfo = mEncryptedFileInfo;
|
|
|
|
String data = JsonUtils.getCanonicalizedJsonString(encryptedMediaScanBody);
|
|
|
|
// Encrypt the data, if antivirus server supports it
|
|
String publicServerKey = getAntivirusServerPublicKey();
|
|
|
|
if (publicServerKey == null) {
|
|
// Error
|
|
throw new Exception("Unable to get public key");
|
|
} else if (!TextUtils.isEmpty(publicServerKey)) {
|
|
OlmPkEncryption olmPkEncryption = new OlmPkEncryption();
|
|
|
|
olmPkEncryption.setRecipientKey(publicServerKey);
|
|
|
|
OlmPkMessage message = olmPkEncryption.encrypt(data);
|
|
|
|
EncryptedMediaScanEncryptedBody encryptedMediaScanEncryptedBody = new EncryptedMediaScanEncryptedBody();
|
|
encryptedMediaScanEncryptedBody.encryptedBodyFileInfo = new EncryptedBodyFileInfo(message);
|
|
|
|
data = JsonUtils.getCanonicalizedJsonString(encryptedMediaScanEncryptedBody);
|
|
}
|
|
// Else: no public key on this server, do not encrypt data
|
|
|
|
OutputStream outputStream = connection.getOutputStream();
|
|
try {
|
|
outputStream.write(data.getBytes("UTF-8"));
|
|
} catch (Exception e) {
|
|
Log.e(LOG_TAG, "doInBackground Failed to serialize encryption info " + e.getMessage(), e);
|
|
} finally {
|
|
outputStream.close();
|
|
}
|
|
}
|
|
|
|
filelen = connection.getContentLength();
|
|
stream = connection.getInputStream();
|
|
} catch (Exception e) {
|
|
Log.e(LOG_TAG, "bitmapForURL : fail to open the connection " + e.getMessage(), e);
|
|
defaultError.error = e.getLocalizedMessage();
|
|
|
|
// In case of 403, revert the key
|
|
if (connection.getResponseCode() == 403 && mMediaScanRestClient != null) {
|
|
mMediaScanRestClient.resetServerPublicKey();
|
|
}
|
|
|
|
InputStream errorStream = 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);
|
|
}
|
|
|
|
jsonElementResult = new JsonParser().parse(responseStrBuilder.toString());
|
|
} catch (Exception ee) {
|
|
Log.e(LOG_TAG, "bitmapForURL : Error parsing error " + ee.getMessage(), ee);
|
|
}
|
|
}
|
|
|
|
// privacy
|
|
//Log.d(LOG_TAG, "MediaWorkerTask " + mUrl + " does not exist");
|
|
Log.d(LOG_TAG, "MediaWorkerTask an url does not exist");
|
|
|
|
// If some medias are not found,
|
|
// do not try to reload them until the next application launch.
|
|
// We mark this url as unreachable.
|
|
// We can do this only if the av scanner is disabled or if the media is unencrypted,
|
|
// (because the same url is used for all encrypted media when the av scanner is enabled).
|
|
if (!mIsAvScannerEnabled || null == mEncryptedFileInfo) {
|
|
synchronized (sUnreachableUrls) {
|
|
sUnreachableUrls.add(mUrl);
|
|
}
|
|
}
|
|
}
|
|
|
|
dispatchDownloadStart();
|
|
|
|
// failed to open the remote stream without having exception
|
|
if ((null == stream) && (null == jsonElementResult)) {
|
|
jsonElementResult = new JsonParser().parse("Cannot open " + mUrl);
|
|
|
|
// if some medias are not found
|
|
// do not try to reload them until the next application launch.
|
|
// We mark this url as unreachable.
|
|
// We can do this only if the av scanner is disabled or if the media is unencrypted,
|
|
// (because the same url is used for all encrypted media when the av scanner is enabled).
|
|
if (!mIsAvScannerEnabled || null == mEncryptedFileInfo) {
|
|
synchronized (sUnreachableUrls) {
|
|
sUnreachableUrls.add(mUrl);
|
|
}
|
|
}
|
|
}
|
|
|
|
// test if the download has not been cancelled
|
|
if (!isDownloadCancelled() && (null == jsonElementResult)) {
|
|
final long startDownloadTime = System.currentTimeMillis();
|
|
|
|
String filename = buildFileName(mDownloadId, mMimeType) + ".tmp";
|
|
FileOutputStream fos = new FileOutputStream(new File(mDirectoryFile, filename));
|
|
|
|
mDownloadStats.mDownloadId = mDownloadId;
|
|
mDownloadStats.mProgress = 0;
|
|
mDownloadStats.mDownloadedSize = 0;
|
|
mDownloadStats.mFileSize = filelen;
|
|
mDownloadStats.mElapsedTime = 0;
|
|
mDownloadStats.mEstimatedRemainingTime = -1;
|
|
mDownloadStats.mBitRate = 0;
|
|
|
|
// Publish progress every 100ms
|
|
final Timer refreshTimer = new Timer();
|
|
|
|
refreshTimer.scheduleAtFixedRate(new TimerTask() {
|
|
@Override
|
|
public void run() {
|
|
if (!mIsDone) {
|
|
updateAndPublishProgress(startDownloadTime);
|
|
}
|
|
}
|
|
}, new Date(), 100);
|
|
|
|
try {
|
|
byte[] buf = new byte[DOWNLOAD_BUFFER_READ_SIZE];
|
|
int len;
|
|
while (!isDownloadCancelled() && (len = stream.read(buf)) != -1) {
|
|
fos.write(buf, 0, len);
|
|
mDownloadStats.mDownloadedSize += len;
|
|
}
|
|
|
|
if (!isDownloadCancelled()) {
|
|
mDownloadStats.mProgress = 100;
|
|
}
|
|
} catch (OutOfMemoryError outOfMemoryError) {
|
|
Log.e(LOG_TAG, "doInBackground: out of memory", outOfMemoryError);
|
|
defaultError.error = outOfMemoryError.getLocalizedMessage();
|
|
} catch (Exception e) {
|
|
Log.e(LOG_TAG, "doInBackground fail to read image " + e.getMessage(), e);
|
|
defaultError.error = e.getLocalizedMessage();
|
|
}
|
|
|
|
mIsDone = true;
|
|
|
|
close(stream);
|
|
fos.flush();
|
|
fos.close();
|
|
|
|
refreshTimer.cancel();
|
|
|
|
if ((null != connection) && (connection instanceof HttpsURLConnection)) {
|
|
connection.disconnect();
|
|
}
|
|
|
|
// the file has been successfully downloaded
|
|
if (mDownloadStats.mProgress == 100) {
|
|
try {
|
|
File originalFile = new File(mDirectoryFile, filename);
|
|
String newFileName = buildFileName(mDownloadId, 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.e(LOG_TAG, "doInBackground : renaming error " + e.getMessage(), e);
|
|
defaultError.error = e.getLocalizedMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mDownloadStats.mProgress == 100) {
|
|
Log.d(LOG_TAG, "The download " + this + " is done.");
|
|
} else {
|
|
if (null != jsonElementResult) {
|
|
Log.d(LOG_TAG, "The download " + this + " failed : mErrorAsJsonElement " + jsonElementResult.toString());
|
|
} else {
|
|
Log.d(LOG_TAG, "The download " + this + " failed.");
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
Log.e(LOG_TAG, "Unable to download media " + this, e);
|
|
defaultError.error = e.getMessage();
|
|
}
|
|
|
|
// build a JSON from the error
|
|
if (!TextUtils.isEmpty(defaultError.error)) {
|
|
jsonElementResult = JsonUtils.getGson(false).toJsonTree(defaultError);
|
|
}
|
|
|
|
// remove the task from the loading one
|
|
synchronized (sPendingDownloadById) {
|
|
sPendingDownloadById.remove(mDownloadId);
|
|
}
|
|
|
|
return jsonElementResult;
|
|
}
|
|
|
|
/**
|
|
* Get the public key of the antivirus server
|
|
*
|
|
* @return either empty string if server does not provide the public key, null in case of error, or the public server key
|
|
*/
|
|
@Nullable
|
|
private String getAntivirusServerPublicKey() {
|
|
if (mMediaScanRestClient == null) {
|
|
// Error
|
|
Log.e(LOG_TAG, "Mandatory mMediaScanRestClient is null");
|
|
return null;
|
|
}
|
|
|
|
// Make async request sync with a CountDownLatch
|
|
// It is easier than adding a method to get the server public key synchronously with Call<T>.execute()
|
|
final CountDownLatch latch = new CountDownLatch(1);
|
|
final String[] publicServerKey = new String[1];
|
|
|
|
mMediaScanRestClient.getServerPublicKey(new ApiCallback<String>() {
|
|
@Override
|
|
public void onSuccess(String serverPublicKey) {
|
|
publicServerKey[0] = serverPublicKey;
|
|
latch.countDown();
|
|
}
|
|
|
|
@Override
|
|
public void onNetworkError(Exception e) {
|
|
latch.countDown();
|
|
}
|
|
|
|
@Override
|
|
public void onMatrixError(MatrixError e) {
|
|
latch.countDown();
|
|
}
|
|
|
|
@Override
|
|
public void onUnexpectedError(Exception e) {
|
|
latch.countDown();
|
|
}
|
|
});
|
|
|
|
try {
|
|
latch.await(30, TimeUnit.SECONDS);
|
|
} catch (InterruptedException ie) {
|
|
|
|
}
|
|
|
|
return publicServerKey[0];
|
|
}
|
|
|
|
/**
|
|
* Close the stream.
|
|
*
|
|
* @param stream the stream to close.
|
|
*/
|
|
private void close(InputStream stream) {
|
|
try {
|
|
stream.close();
|
|
} catch (Exception e) {
|
|
Log.e(LOG_TAG, "close error " + e.getMessage(), e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onProgressUpdate(Void... aVoid) {
|
|
super.onProgressUpdate();
|
|
dispatchOnDownloadProgress(mDownloadStats);
|
|
}
|
|
|
|
// Once complete, see if ImageView is still around and set bitmap.
|
|
@Override
|
|
protected void onPostExecute(JsonElement jsonElementError) {
|
|
if (null != jsonElementError) {
|
|
dispatchOnDownloadError(jsonElementError);
|
|
} else if (isDownloadCancelled()) {
|
|
dispatchDownloadCancel();
|
|
} else {
|
|
dispatchOnDownloadComplete();
|
|
|
|
// image download
|
|
// update the linked ImageViews.
|
|
if (isBitmapDownloadTask()) {
|
|
// retrieve the bitmap from the file s
|
|
if (!bitmapForURL(mApplicationContext, mDirectoryFile, mUrl, mDownloadId, mRotation, mMimeType, mEncryptedFileInfo,
|
|
new SimpleApiCallback<Bitmap>() {
|
|
@Override
|
|
public void onSuccess(Bitmap bitmap) {
|
|
setBitmap((null == bitmap) ? mDefaultBitmap : bitmap);
|
|
}
|
|
})) {
|
|
setBitmap(mDefaultBitmap);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the bitmap in a referenced imageview
|
|
*
|
|
* @param bitmap the bitmap
|
|
*/
|
|
private void setBitmap(Bitmap bitmap) {
|
|
// update the imageViews image
|
|
if (bitmap != null) {
|
|
for (WeakReference<ImageView> weakRef : mImageViewReferences) {
|
|
final ImageView imageView = weakRef.get();
|
|
|
|
if (imageView != null && TextUtils.equals(mDownloadId, (String) imageView.getTag())) {
|
|
imageView.setImageBitmap(bitmap);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//==============================================================================================================
|
|
// Dispatchers
|
|
//==============================================================================================================
|
|
|
|
/**
|
|
* Dispatch start event to the callbacks.
|
|
*/
|
|
private void dispatchDownloadStart() {
|
|
for (IMXMediaDownloadListener callback : mDownloadListeners) {
|
|
try {
|
|
callback.onDownloadStart(mDownloadId);
|
|
} catch (Exception e) {
|
|
Log.e(LOG_TAG, "dispatchDownloadStart error " + e.getMessage(), e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dispatch stats update to the callbacks.
|
|
*
|
|
* @param stats the new stats value
|
|
*/
|
|
private void dispatchOnDownloadProgress(IMXMediaDownloadListener.DownloadStats stats) {
|
|
for (IMXMediaDownloadListener callback : mDownloadListeners) {
|
|
try {
|
|
callback.onDownloadProgress(mDownloadId, stats);
|
|
} catch (Exception e) {
|
|
Log.e(LOG_TAG, "dispatchOnDownloadProgress error " + e.getMessage(), e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dispatch error message.
|
|
*
|
|
* @param jsonElement the Json error
|
|
*/
|
|
private void dispatchOnDownloadError(JsonElement jsonElement) {
|
|
for (IMXMediaDownloadListener callback : mDownloadListeners) {
|
|
try {
|
|
callback.onDownloadError(mDownloadId, jsonElement);
|
|
} catch (Exception e) {
|
|
Log.e(LOG_TAG, "dispatchOnDownloadError error " + e.getMessage(), e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dispatch end of download
|
|
*/
|
|
private void dispatchOnDownloadComplete() {
|
|
for (IMXMediaDownloadListener callback : mDownloadListeners) {
|
|
try {
|
|
callback.onDownloadComplete(mDownloadId);
|
|
} catch (Exception e) {
|
|
Log.e(LOG_TAG, "dispatchOnDownloadComplete error " + e.getMessage(), e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dispatch download cancel
|
|
*/
|
|
private void dispatchDownloadCancel() {
|
|
for (IMXMediaDownloadListener callback : mDownloadListeners) {
|
|
try {
|
|
callback.onDownloadCancel(mDownloadId);
|
|
} catch (Exception e) {
|
|
Log.e(LOG_TAG, "dispatchDownloadCancel error " + e.getMessage(), e);
|
|
}
|
|
}
|
|
}
|
|
}
|