diff --git a/art/play_gif.svg b/art/play_gif_black.svg
similarity index 97%
rename from art/play_gif.svg
rename to art/play_gif_black.svg
index 47f5cc24d..a2b426a24 100644
--- a/art/play_gif.svg
+++ b/art/play_gif_black.svg
@@ -64,5 +64,5 @@
d="M11.5 9H13v6h-1.5zM9 9H6c-.6 0-1 .5-1 1v4c0 .5.4 1 1 1h3c.6 0 1-.5 1-1v-2H8.5v1.5h-2v-3H10V10c0-.5-.4-1-1-1zm10 1.5V9h-4.5v6H16v-2h2v-1.5h-2v-1z"
clip-path="url(#b)"
id="path10"
- style="fill:#ffffff;fill-opacity:0.7019608" />
+ style="fill:#000000;fill-opacity:0.54" />
diff --git a/art/play_gif_white.svg b/art/play_gif_white.svg
new file mode 100644
index 000000000..f8ec27426
--- /dev/null
+++ b/art/play_gif_white.svg
@@ -0,0 +1,68 @@
+
+
diff --git a/art/play_video.svg b/art/play_video_black.svg
similarity index 94%
rename from art/play_video.svg
rename to art/play_video_black.svg
index 083e7cfad..72d6e756f 100644
--- a/art/play_video.svg
+++ b/art/play_video_black.svg
@@ -55,5 +55,5 @@
+ style="fill:#000000;fill-opacity:0.54;opacity:1;stroke:none;stroke-opacity:0.38039216" />
diff --git a/art/play_video_white.svg b/art/play_video_white.svg
new file mode 100644
index 000000000..c8a1558ba
--- /dev/null
+++ b/art/play_video_white.svg
@@ -0,0 +1,59 @@
+
+
diff --git a/art/render.rb b/art/render.rb
index ad3a40e81..ba50be73b 100755
--- a/art/render.rb
+++ b/art/render.rb
@@ -18,8 +18,10 @@ images = {
'ic_search_white.svg' => ['ic_search_background_white', 144],
'ic_no_results_white.svg' => ['ic_no_results_background_white', 144],
'ic_no_results_black.svg' => ['ic_no_results_background_black', 144],
- 'play_video.svg' => ['play_video', 128],
- 'play_gif.svg' => ['play_gif', 128],
+ 'play_video_white.svg' => ['play_video_white', 128],
+ 'play_gif_white.svg' => ['play_gif_white', 128],
+ 'play_video_black.svg' => ['play_video_black', 128],
+ 'play_gif_black.svg' => ['play_gif_black', 128],
'conversations_mono.svg' => ['ic_notification', 24],
'ic_received_indicator.svg' => ['ic_received_indicator', 12],
'ic_send_text_offline.svg' => ['ic_send_text_offline', 36],
diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
index 8adfc2c9e..510b50225 100644
--- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -26,7 +26,6 @@ import android.util.Base64;
import android.util.Base64OutputStream;
import android.util.Log;
import android.util.LruCache;
-import android.webkit.MimeTypeMap;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
@@ -44,7 +43,6 @@ import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
-import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
@@ -65,1071 +63,1089 @@ import eu.siacs.conversations.xmpp.pep.Avatar;
public class FileBackend {
- private static final Object THUMBNAIL_LOCK = new Object();
-
- private static final SimpleDateFormat IMAGE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
-
- private static final String FILE_PROVIDER = ".files";
-
- private XmppConnectionService mXmppConnectionService;
-
- public FileBackend(XmppConnectionService service) {
- this.mXmppConnectionService = service;
- }
-
- private static boolean isInDirectoryThatShouldNotBeScanned(Context context, File file) {
- return isInDirectoryThatShouldNotBeScanned(context, file.getAbsolutePath());
- }
-
- public static boolean isInDirectoryThatShouldNotBeScanned(Context context, String path) {
- for (String type : new String[]{RecordingActivity.STORAGE_DIRECTORY_TYPE_NAME, "Files"}) {
- if (path.startsWith(getConversationsDirectory(context, type))) {
- return true;
- }
- }
- return false;
- }
-
- public static long getFileSize(Context context, Uri uri) {
- try {
- final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
- if (cursor != null && cursor.moveToFirst()) {
- long size = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE));
- cursor.close();
- return size;
- } else {
- return -1;
- }
- } catch (Exception e) {
- return -1;
- }
- }
-
- public static boolean allFilesUnderSize(Context context, List uris, long max) {
- if (max <= 0) {
- Log.d(Config.LOGTAG, "server did not report max file size for http upload");
- return true; //exception to be compatible with HTTP Upload < v0.2
- }
- for (Uri uri : uris) {
- String mime = context.getContentResolver().getType(uri);
- if (mime != null && mime.startsWith("video/")) {
- try {
- Dimensions dimensions = FileBackend.getVideoDimensions(context, uri);
- if (dimensions.getMin() > 720) {
- Log.d(Config.LOGTAG, "do not consider video file with min width larger than 720 for size check");
- continue;
- }
- } catch (NotAVideoFile notAVideoFile) {
- //ignore and fall through
- }
- }
- if (FileBackend.getFileSize(context, uri) > max) {
- Log.d(Config.LOGTAG, "not all files are under " + max + " bytes. suggesting falling back to jingle");
- return false;
- }
- }
- return true;
- }
-
- public static String getConversationsDirectory(Context context, final String type) {
- if (Config.ONLY_INTERNAL_STORAGE) {
- return context.getFilesDir().getAbsolutePath() + "/" + type + "/";
- } else {
- return getAppMediaDirectory(context) + context.getString(R.string.app_name) + " " + type + "/";
- }
- }
-
- public static String getAppMediaDirectory(Context context) {
- return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + context.getString(R.string.app_name) + "/Media/";
- }
-
- public static String getConversationsLogsDirectory() {
- return Environment.getExternalStorageDirectory().getAbsolutePath() + "/Conversations/";
- }
-
- private static Bitmap rotate(Bitmap bitmap, int degree) {
- if (degree == 0) {
- return bitmap;
- }
- int w = bitmap.getWidth();
- int h = bitmap.getHeight();
- Matrix mtx = new Matrix();
- mtx.postRotate(degree);
- Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
- if (bitmap != null && !bitmap.isRecycled()) {
- bitmap.recycle();
- }
- return result;
- }
-
- public static boolean isPathBlacklisted(String path) {
- final String androidDataPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/";
- return path.startsWith(androidDataPath);
- }
-
- private static Paint createAntiAliasingPaint() {
- Paint paint = new Paint();
- paint.setAntiAlias(true);
- paint.setFilterBitmap(true);
- paint.setDither(true);
- return paint;
- }
-
- private static String getTakePhotoPath() {
- return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/Camera/";
- }
-
- public static Uri getUriForFile(Context context, File file) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || Config.ONLY_INTERNAL_STORAGE) {
- try {
- return FileProvider.getUriForFile(context, getAuthority(context), file);
- } catch (IllegalArgumentException e) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- throw new SecurityException(e);
- } else {
- return Uri.fromFile(file);
- }
- }
- } else {
- return Uri.fromFile(file);
- }
- }
-
- public static String getAuthority(Context context) {
- return context.getPackageName() + FILE_PROVIDER;
- }
-
- private static boolean hasAlpha(final Bitmap bitmap) {
- for (int x = 0; x < bitmap.getWidth(); ++x) {
- for (int y = 0; y < bitmap.getWidth(); ++y) {
- if (Color.alpha(bitmap.getPixel(x, y)) < 255) {
- return true;
- }
- }
- }
- return false;
- }
-
- private static int calcSampleSize(File image, int size) {
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(image.getAbsolutePath(), options);
- return calcSampleSize(options, size);
- }
-
- private static int calcSampleSize(BitmapFactory.Options options, int size) {
- int height = options.outHeight;
- int width = options.outWidth;
- int inSampleSize = 1;
-
- if (height > size || width > size) {
- int halfHeight = height / 2;
- int halfWidth = width / 2;
-
- while ((halfHeight / inSampleSize) > size
- && (halfWidth / inSampleSize) > size) {
- inSampleSize *= 2;
- }
- }
- return inSampleSize;
- }
-
- private static Dimensions getVideoDimensions(Context context, Uri uri) throws NotAVideoFile {
- MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
- try {
- mediaMetadataRetriever.setDataSource(context, uri);
- } catch (RuntimeException e) {
- throw new NotAVideoFile(e);
- }
- return getVideoDimensions(mediaMetadataRetriever);
- }
-
- private static Dimensions getVideoDimensionsOfFrame(MediaMetadataRetriever mediaMetadataRetriever) {
- Bitmap bitmap = null;
- try {
- bitmap = mediaMetadataRetriever.getFrameAtTime();
- return new Dimensions(bitmap.getHeight(), bitmap.getWidth());
- } catch (Exception e) {
- return null;
- } finally {
- if (bitmap != null) {
- bitmap.recycle();
- ;
- }
- }
- }
-
- private static Dimensions getVideoDimensions(MediaMetadataRetriever metadataRetriever) throws NotAVideoFile {
- String hasVideo = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
- if (hasVideo == null) {
- throw new NotAVideoFile();
- }
- Dimensions dimensions = getVideoDimensionsOfFrame(metadataRetriever);
- if (dimensions != null) {
- return dimensions;
- }
- int rotation = extractRotationFromMediaRetriever(metadataRetriever);
- boolean rotated = rotation == 90 || rotation == 270;
- int height;
- try {
- String h = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
- height = Integer.parseInt(h);
- } catch (Exception e) {
- height = -1;
- }
- int width;
- try {
- String w = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
- width = Integer.parseInt(w);
- } catch (Exception e) {
- width = -1;
- }
- metadataRetriever.release();
- Log.d(Config.LOGTAG, "extracted video dims " + width + "x" + height);
- return rotated ? new Dimensions(width, height) : new Dimensions(height, width);
- }
-
- private static int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) {
- String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
- try {
- return Integer.parseInt(r);
- } catch (Exception e) {
- return 0;
- }
- }
-
- public static void close(Closeable stream) {
- if (stream != null) {
- try {
- stream.close();
- } catch (IOException e) {
- }
- }
- }
-
- public static void close(Socket socket) {
- if (socket != null) {
- try {
- socket.close();
- } catch (IOException e) {
- }
- }
- }
-
- public static boolean weOwnFile(Context context, Uri uri) {
- if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
- return false;
- } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
- return fileIsInFilesDir(context, uri);
- } else {
- return weOwnFileLollipop(uri);
- }
- }
-
- /**
- * This is more than hacky but probably way better than doing nothing
- * Further 'optimizations' might contain to get the parents of CacheDir and NoBackupDir
- * and check against those as well
- */
- private static boolean fileIsInFilesDir(Context context, Uri uri) {
- try {
- final String haystack = context.getFilesDir().getParentFile().getCanonicalPath();
- final String needle = new File(uri.getPath()).getCanonicalPath();
- return needle.startsWith(haystack);
- } catch (IOException e) {
- return false;
- }
- }
-
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
- private static boolean weOwnFileLollipop(Uri uri) {
- try {
- File file = new File(uri.getPath());
- FileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).getFileDescriptor();
- StructStat st = Os.fstat(fd);
- return st.st_uid == android.os.Process.myUid();
- } catch (FileNotFoundException e) {
- return false;
- } catch (Exception e) {
- return true;
- }
- }
-
- private void createNoMedia(File diretory) {
- final File noMedia = new File(diretory, ".nomedia");
- if (!noMedia.exists()) {
- try {
- if (!noMedia.createNewFile()) {
- Log.d(Config.LOGTAG, "created nomedia file " + noMedia.getAbsolutePath());
- }
- } catch (Exception e) {
- Log.d(Config.LOGTAG, "could not create nomedia file");
- }
- }
- }
-
- public void updateMediaScanner(File file) {
- if (!isInDirectoryThatShouldNotBeScanned(mXmppConnectionService, file)) {
- Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
- intent.setData(Uri.fromFile(file));
- mXmppConnectionService.sendBroadcast(intent);
- } else if (file.getAbsolutePath().startsWith(getAppMediaDirectory(mXmppConnectionService))) {
- createNoMedia(file.getParentFile());
- }
- }
-
- public boolean deleteFile(Message message) {
- File file = getFile(message);
- if (file.delete()) {
- updateMediaScanner(file);
- return true;
- } else {
- return false;
- }
- }
-
- public DownloadableFile getFile(Message message) {
- return getFile(message, true);
- }
-
- public DownloadableFile getFileForPath(String path, String mime) {
- final DownloadableFile file;
- if (path.startsWith("/")) {
- file = new DownloadableFile(path);
- } else {
- if (mime != null && mime.startsWith("image/")) {
- file = new DownloadableFile(getConversationsDirectory("Images") + path);
- } else if (mime != null && mime.startsWith("video/")) {
- file = new DownloadableFile(getConversationsDirectory("Videos") + path);
- } else {
- file = new DownloadableFile(getConversationsDirectory("Files") + path);
- }
- }
- return file;
- }
-
- public DownloadableFile getFile(Message message, boolean decrypted) {
- final boolean encrypted = !decrypted
- && (message.getEncryption() == Message.ENCRYPTION_PGP
- || message.getEncryption() == Message.ENCRYPTION_DECRYPTED);
- String path = message.getRelativeFilePath();
- if (path == null) {
- path = message.getUuid();
- }
- final DownloadableFile file = getFileForPath(path, message.getMimeType());
- if (encrypted) {
- return new DownloadableFile(getConversationsDirectory("Files") + file.getName() + ".pgp");
- } else {
- return file;
- }
- }
-
- public String getConversationsDirectory(final String type) {
- return getConversationsDirectory(mXmppConnectionService, type);
- }
-
- private Bitmap resize(final Bitmap originalBitmap, int size) throws IOException {
- int w = originalBitmap.getWidth();
- int h = originalBitmap.getHeight();
- if (w <= 0 || h <= 0) {
- throw new IOException("Decoded bitmap reported bounds smaller 0");
- } else if (Math.max(w, h) > size) {
- int scalledW;
- int scalledH;
- if (w <= h) {
- scalledW = Math.max((int) (w / ((double) h / size)), 1);
- scalledH = size;
- } else {
- scalledW = size;
- scalledH = Math.max((int) (h / ((double) w / size)), 1);
- }
- final Bitmap result = Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true);
- if (!originalBitmap.isRecycled()) {
- originalBitmap.recycle();
- }
- return result;
- } else {
- return originalBitmap;
- }
- }
-
- public boolean useImageAsIs(Uri uri) {
- String path = getOriginalPath(uri);
- if (path == null || isPathBlacklisted(path)) {
- return false;
- }
- File file = new File(path);
- long size = file.length();
- if (size == 0 || size >= mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize)) {
- return false;
- }
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- try {
- BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri), null, options);
- if (options.outMimeType == null || options.outHeight <= 0 || options.outWidth <= 0) {
- return false;
- }
- return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase()));
- } catch (FileNotFoundException e) {
- return false;
- }
- }
-
- public String getOriginalPath(Uri uri) {
- return FileUtils.getPath(mXmppConnectionService, uri);
- }
-
- private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
- Log.d(Config.LOGTAG, "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath());
- file.getParentFile().mkdirs();
- OutputStream os = null;
- InputStream is = null;
- try {
- file.createNewFile();
- os = new FileOutputStream(file);
- is = mXmppConnectionService.getContentResolver().openInputStream(uri);
- byte[] buffer = new byte[1024];
- int length;
- while ((length = is.read(buffer)) > 0) {
- try {
- os.write(buffer, 0, length);
- } catch (IOException e) {
- throw new FileWriterException();
- }
- }
- try {
- os.flush();
- } catch (IOException e) {
- throw new FileWriterException();
- }
- } catch (FileNotFoundException e) {
- throw new FileCopyException(R.string.error_file_not_found);
- } catch (FileWriterException e) {
- throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
- } catch (IOException e) {
- e.printStackTrace();
- throw new FileCopyException(R.string.error_io_exception);
- } finally {
- close(os);
- close(is);
- }
- }
-
- public void copyFileToPrivateStorage(Message message, Uri uri, String type) throws FileCopyException {
- String mime = type != null ? type : MimeUtils.guessMimeTypeFromUri(mXmppConnectionService, uri);
- Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime=" + mime + ")");
- String extension = MimeUtils.guessExtensionFromMimeType(mime);
- if (extension == null) {
- Log.d(Config.LOGTAG, "extension from mime type was null");
- extension = getExtensionFromUri(uri);
- }
- if ("ogg".equals(extension) && type != null && type.startsWith("audio/")) {
- extension = "oga";
- }
- message.setRelativeFilePath(message.getUuid() + "." + extension);
- copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri);
- }
-
- private String getExtensionFromUri(Uri uri) {
- String[] projection = {MediaStore.MediaColumns.DATA};
- String filename = null;
- Cursor cursor = mXmppConnectionService.getContentResolver().query(uri, projection, null, null, null);
- if (cursor != null) {
- try {
- if (cursor.moveToFirst()) {
- filename = cursor.getString(0);
- }
- } catch (Exception e) {
- filename = null;
- } finally {
- cursor.close();
- }
- }
- if (filename == null) {
- final List segments = uri.getPathSegments();
- if (segments.size() > 0) {
- filename = segments.get(segments.size() - 1);
- }
- }
- int pos = filename == null ? -1 : filename.lastIndexOf('.');
- return pos > 0 ? filename.substring(pos + 1) : null;
- }
-
- private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException {
- file.getParentFile().mkdirs();
- InputStream is = null;
- OutputStream os = null;
- try {
- if (!file.exists() && !file.createNewFile()) {
- throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
- }
- is = mXmppConnectionService.getContentResolver().openInputStream(image);
- if (is == null) {
- throw new FileCopyException(R.string.error_not_an_image_file);
- }
- Bitmap originalBitmap;
- BitmapFactory.Options options = new BitmapFactory.Options();
- int inSampleSize = (int) Math.pow(2, sampleSize);
- Log.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize);
- options.inSampleSize = inSampleSize;
- originalBitmap = BitmapFactory.decodeStream(is, null, options);
- is.close();
- if (originalBitmap == null) {
- throw new FileCopyException(R.string.error_not_an_image_file);
- }
- Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE);
- int rotation = getRotation(image);
- scaledBitmap = rotate(scaledBitmap, rotation);
- boolean targetSizeReached = false;
- int quality = Config.IMAGE_QUALITY;
- final int imageMaxSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
- while (!targetSizeReached) {
- os = new FileOutputStream(file);
- boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, quality, os);
- if (!success) {
- throw new FileCopyException(R.string.error_compressing_image);
- }
- os.flush();
- targetSizeReached = file.length() <= imageMaxSize || quality <= 50;
- quality -= 5;
- }
- scaledBitmap.recycle();
- } catch (FileNotFoundException e) {
- throw new FileCopyException(R.string.error_file_not_found);
- } catch (IOException e) {
- e.printStackTrace();
- throw new FileCopyException(R.string.error_io_exception);
- } catch (SecurityException e) {
- throw new FileCopyException(R.string.error_security_exception_during_image_copy);
- } catch (OutOfMemoryError e) {
- ++sampleSize;
- if (sampleSize <= 3) {
- copyImageToPrivateStorage(file, image, sampleSize);
- } else {
- throw new FileCopyException(R.string.error_out_of_memory);
- }
- } finally {
- close(os);
- close(is);
- }
- }
-
- public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException {
- Log.d(Config.LOGTAG, "copy image (" + image.toString() + ") to private storage " + file.getAbsolutePath());
- copyImageToPrivateStorage(file, image, 0);
- }
-
- public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException {
- switch (Config.IMAGE_FORMAT) {
- case JPEG:
- message.setRelativeFilePath(message.getUuid() + ".jpg");
- break;
- case PNG:
- message.setRelativeFilePath(message.getUuid() + ".png");
- break;
- case WEBP:
- message.setRelativeFilePath(message.getUuid() + ".webp");
- break;
- }
- copyImageToPrivateStorage(getFile(message), image);
- updateFileParams(message);
- }
-
- private int getRotation(File file) {
- return getRotation(Uri.parse("file://" + file.getAbsolutePath()));
- }
-
- private int getRotation(Uri image) {
- InputStream is = null;
- try {
- is = mXmppConnectionService.getContentResolver().openInputStream(image);
- return ExifHelper.getOrientation(is);
- } catch (FileNotFoundException e) {
- return 0;
- } finally {
- close(is);
- }
- }
-
- public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws IOException {
- final String uuid = message.getUuid();
- final LruCache cache = mXmppConnectionService.getBitmapCache();
- Bitmap thumbnail = cache.get(uuid);
- if ((thumbnail == null) && (!cacheOnly)) {
- synchronized (THUMBNAIL_LOCK) {
- thumbnail = cache.get(uuid);
- if (thumbnail != null) {
- return thumbnail;
- }
- DownloadableFile file = getFile(message);
- final String mime = file.getMimeType();
- if (mime.startsWith("video/")) {
- thumbnail = getVideoPreview(file, size);
- } else {
- Bitmap fullsize = getFullsizeImagePreview(file, size);
- if (fullsize == null) {
- throw new FileNotFoundException();
- }
- thumbnail = resize(fullsize, size);
- thumbnail = rotate(thumbnail, getRotation(file));
- if (mime.equals("image/gif")) {
- Bitmap withGifOverlay = thumbnail.copy(Bitmap.Config.ARGB_8888, true);
- drawOverlay(withGifOverlay, R.drawable.play_gif, 1.0f);
- thumbnail.recycle();
- thumbnail = withGifOverlay;
- }
- }
- this.mXmppConnectionService.getBitmapCache().put(uuid, thumbnail);
- }
- }
- return thumbnail;
- }
-
- private Bitmap getFullsizeImagePreview(File file, int size) {
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = calcSampleSize(file, size);
- try {
- return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
- } catch (OutOfMemoryError e) {
- options.inSampleSize *= 2;
- return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
- }
- }
-
- private void drawOverlay(Bitmap bitmap, int resource, float factor) {
- Bitmap overlay = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource);
- Canvas canvas = new Canvas(bitmap);
- float targetSize = Math.min(canvas.getWidth(), canvas.getHeight()) * factor;
- Log.d(Config.LOGTAG, "target size overlay: " + targetSize + " overlay bitmap size was " + overlay.getHeight());
- float left = (canvas.getWidth() - targetSize) / 2.0f;
- float top = (canvas.getHeight() - targetSize) / 2.0f;
- RectF dst = new RectF(left, top, left + targetSize - 1, top + targetSize - 1);
- canvas.drawBitmap(overlay, null, dst, createAntiAliasingPaint());
- }
-
- private Bitmap getVideoPreview(File file, int size) {
- MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
- Bitmap frame;
- try {
- metadataRetriever.setDataSource(file.getAbsolutePath());
- frame = metadataRetriever.getFrameAtTime(0);
- metadataRetriever.release();
- frame = resize(frame, size);
- } catch (IOException | RuntimeException e) {
- frame = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
- frame.eraseColor(0xff000000);
- }
- drawOverlay(frame, R.drawable.play_video, 0.75f);
- return frame;
- }
-
- public Uri getTakePhotoUri() {
- File file;
- if (Config.ONLY_INTERNAL_STORAGE) {
- file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath(), "Camera/IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
- } else {
- file = new File(getTakePhotoPath() + "IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
- }
- file.getParentFile().mkdirs();
- return getUriForFile(mXmppConnectionService, file);
- }
-
- public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
-
- final Avatar uncompressAvatar = getUncompressedAvatar(image);
- if (uncompressAvatar != null && uncompressAvatar.image.length() <= Config.AVATAR_CHAR_LIMIT) {
- return uncompressAvatar;
- }
- if (uncompressAvatar != null) {
- Log.d(Config.LOGTAG,"uncompressed avatar exceeded char limit by "+(uncompressAvatar.image.length() - Config.AVATAR_CHAR_LIMIT));
- }
-
- Bitmap bm = cropCenterSquare(image, size);
- if (bm == null) {
- return null;
- }
- if (hasAlpha(bm)) {
- Log.d(Config.LOGTAG, "alpha in avatar detected; uploading as PNG");
- bm.recycle();
- bm = cropCenterSquare(image, 96);
- return getPepAvatar(bm, Bitmap.CompressFormat.PNG, 100);
- }
- return getPepAvatar(bm, format, 100);
- }
-
- private Avatar getUncompressedAvatar(Uri uri) {
- Bitmap bitmap = null;
- try {
- bitmap = BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri));
- return getPepAvatar(bitmap, Bitmap.CompressFormat.PNG, 100);
- } catch (Exception e) {
- if (bitmap != null) {
- bitmap.recycle();
- }
- }
- return null;
- }
-
- private Avatar getPepAvatar(Bitmap bitmap, Bitmap.CompressFormat format, int quality) {
- try {
- ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
- Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
- MessageDigest digest = MessageDigest.getInstance("SHA-1");
- DigestOutputStream mDigestOutputStream = new DigestOutputStream(mBase64OutputStream, digest);
- if (!bitmap.compress(format, quality, mDigestOutputStream)) {
- return null;
- }
- mDigestOutputStream.flush();
- mDigestOutputStream.close();
- long chars = mByteArrayOutputStream.size();
- if (format != Bitmap.CompressFormat.PNG && quality >= 50 && chars >= Config.AVATAR_CHAR_LIMIT) {
- int q = quality - 2;
- Log.d(Config.LOGTAG, "avatar char length was " + chars + " reducing quality to " + q);
- return getPepAvatar(bitmap, format, q);
- }
- Log.d(Config.LOGTAG, "settled on char length " + chars + " with quality=" + quality);
- final Avatar avatar = new Avatar();
- avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
- avatar.image = new String(mByteArrayOutputStream.toByteArray());
- if (format.equals(Bitmap.CompressFormat.WEBP)) {
- avatar.type = "image/webp";
- } else if (format.equals(Bitmap.CompressFormat.JPEG)) {
- avatar.type = "image/jpeg";
- } else if (format.equals(Bitmap.CompressFormat.PNG)) {
- avatar.type = "image/png";
- }
- avatar.width = bitmap.getWidth();
- avatar.height = bitmap.getHeight();
- return avatar;
- } catch (Exception e) {
- return null;
- }
- }
-
- public Avatar getStoredPepAvatar(String hash) {
- if (hash == null) {
- return null;
- }
- Avatar avatar = new Avatar();
- File file = new File(getAvatarPath(hash));
- FileInputStream is = null;
- try {
- avatar.size = file.length();
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(file.getAbsolutePath(), options);
- is = new FileInputStream(file);
- ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
- Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
- MessageDigest digest = MessageDigest.getInstance("SHA-1");
- DigestOutputStream os = new DigestOutputStream(mBase64OutputStream, digest);
- byte[] buffer = new byte[4096];
- int length;
- while ((length = is.read(buffer)) > 0) {
- os.write(buffer, 0, length);
- }
- os.flush();
- os.close();
- avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
- avatar.image = new String(mByteArrayOutputStream.toByteArray());
- avatar.height = options.outHeight;
- avatar.width = options.outWidth;
- avatar.type = options.outMimeType;
- return avatar;
- } catch (NoSuchAlgorithmException | IOException e) {
- return null;
- } finally {
- close(is);
- }
- }
-
- public boolean isAvatarCached(Avatar avatar) {
- File file = new File(getAvatarPath(avatar.getFilename()));
- return file.exists();
- }
-
- public boolean save(final Avatar avatar) {
- File file;
- if (isAvatarCached(avatar)) {
- file = new File(getAvatarPath(avatar.getFilename()));
- avatar.size = file.length();
- } else {
- file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + UUID.randomUUID().toString());
- if (file.getParentFile().mkdirs()) {
- Log.d(Config.LOGTAG, "created cache directory");
- }
- OutputStream os = null;
- try {
- if (!file.createNewFile()) {
- Log.d(Config.LOGTAG, "unable to create temporary file " + file.getAbsolutePath());
- }
- os = new FileOutputStream(file);
- MessageDigest digest = MessageDigest.getInstance("SHA-1");
- digest.reset();
- DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest);
- final byte[] bytes = avatar.getImageAsBytes();
- mDigestOutputStream.write(bytes);
- mDigestOutputStream.flush();
- mDigestOutputStream.close();
- String sha1sum = CryptoHelper.bytesToHex(digest.digest());
- if (sha1sum.equals(avatar.sha1sum)) {
- File outputFile = new File(getAvatarPath(avatar.getFilename()));
- if (outputFile.getParentFile().mkdirs()) {
- Log.d(Config.LOGTAG, "created avatar directory");
- }
- String filename = getAvatarPath(avatar.getFilename());
- if (!file.renameTo(new File(filename))) {
- Log.d(Config.LOGTAG, "unable to rename " + file.getAbsolutePath() + " to " + outputFile);
- return false;
- }
- } else {
- Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner);
- if (!file.delete()) {
- Log.d(Config.LOGTAG, "unable to delete temporary file");
- }
- return false;
- }
- avatar.size = bytes.length;
- } catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) {
- return false;
- } finally {
- close(os);
- }
- }
- return true;
- }
-
- private String getAvatarPath(String avatar) {
- return mXmppConnectionService.getFilesDir().getAbsolutePath() + "/avatars/" + avatar;
- }
-
- public Uri getAvatarUri(String avatar) {
- return Uri.parse("file:" + getAvatarPath(avatar));
- }
-
- public Bitmap cropCenterSquare(Uri image, int size) {
- if (image == null) {
- return null;
- }
- InputStream is = null;
- try {
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = calcSampleSize(image, size);
- is = mXmppConnectionService.getContentResolver().openInputStream(image);
- if (is == null) {
- return null;
- }
- Bitmap input = BitmapFactory.decodeStream(is, null, options);
- if (input == null) {
- return null;
- } else {
- input = rotate(input, getRotation(image));
- return cropCenterSquare(input, size);
- }
- } catch (FileNotFoundException | SecurityException e) {
- return null;
- } finally {
- close(is);
- }
- }
-
- public Bitmap cropCenter(Uri image, int newHeight, int newWidth) {
- if (image == null) {
- return null;
- }
- InputStream is = null;
- try {
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = calcSampleSize(image, Math.max(newHeight, newWidth));
- is = mXmppConnectionService.getContentResolver().openInputStream(image);
- if (is == null) {
- return null;
- }
- Bitmap source = BitmapFactory.decodeStream(is, null, options);
- if (source == null) {
- return null;
- }
- int sourceWidth = source.getWidth();
- int sourceHeight = source.getHeight();
- float xScale = (float) newWidth / sourceWidth;
- float yScale = (float) newHeight / sourceHeight;
- float scale = Math.max(xScale, yScale);
- float scaledWidth = scale * sourceWidth;
- float scaledHeight = scale * sourceHeight;
- float left = (newWidth - scaledWidth) / 2;
- float top = (newHeight - scaledHeight) / 2;
-
- RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);
- Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(dest);
- canvas.drawBitmap(source, null, targetRect, createAntiAliasingPaint());
- if (source.isRecycled()) {
- source.recycle();
- }
- return dest;
- } catch (SecurityException e) {
- return null; //android 6.0 with revoked permissions for example
- } catch (FileNotFoundException e) {
- return null;
- } finally {
- close(is);
- }
- }
-
- public Bitmap cropCenterSquare(Bitmap input, int size) {
- int w = input.getWidth();
- int h = input.getHeight();
-
- float scale = Math.max((float) size / h, (float) size / w);
-
- float outWidth = scale * w;
- float outHeight = scale * h;
- float left = (size - outWidth) / 2;
- float top = (size - outHeight) / 2;
- RectF target = new RectF(left, top, left + outWidth, top + outHeight);
-
- Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(output);
- canvas.drawBitmap(input, null, target, createAntiAliasingPaint());
- if (!input.isRecycled()) {
- input.recycle();
- }
- return output;
- }
-
- private int calcSampleSize(Uri image, int size) throws FileNotFoundException, SecurityException {
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(image), null, options);
- return calcSampleSize(options, size);
- }
-
- public void updateFileParams(Message message) {
- updateFileParams(message, null);
- }
-
- public void updateFileParams(Message message, URL url) {
- DownloadableFile file = getFile(message);
- final String mime = file.getMimeType();
- boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/"));
- boolean video = mime != null && mime.startsWith("video/");
- boolean audio = mime != null && mime.startsWith("audio/");
- final StringBuilder body = new StringBuilder();
- if (url != null) {
- body.append(url.toString());
- }
- body.append('|').append(file.getSize());
- if (image || video) {
- try {
- Dimensions dimensions = image ? getImageDimensions(file) : getVideoDimensions(file);
- if (dimensions.valid()) {
- body.append('|').append(dimensions.width).append('|').append(dimensions.height);
- }
- } catch (NotAVideoFile notAVideoFile) {
- Log.d(Config.LOGTAG, "file with mime type " + file.getMimeType() + " was not a video file");
- //fall threw
- }
- } else if (audio) {
- body.append("|0|0|").append(getMediaRuntime(file));
- }
- message.setBody(body.toString());
- }
-
- public int getMediaRuntime(Uri uri) {
- try {
- MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
- mediaMetadataRetriever.setDataSource(mXmppConnectionService, uri);
- return Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
- } catch (RuntimeException e) {
- return 0;
- }
- }
-
- private int getMediaRuntime(File file) {
- try {
- MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
- mediaMetadataRetriever.setDataSource(file.toString());
- return Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
- } catch (RuntimeException e) {
- return 0;
- }
- }
-
- private Dimensions getImageDimensions(File file) {
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(file.getAbsolutePath(), options);
- int rotation = getRotation(file);
- boolean rotated = rotation == 90 || rotation == 270;
- int imageHeight = rotated ? options.outWidth : options.outHeight;
- int imageWidth = rotated ? options.outHeight : options.outWidth;
- return new Dimensions(imageHeight, imageWidth);
- }
-
- private Dimensions getVideoDimensions(File file) throws NotAVideoFile {
- MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
- try {
- metadataRetriever.setDataSource(file.getAbsolutePath());
- } catch (RuntimeException e) {
- throw new NotAVideoFile(e);
- }
- return getVideoDimensions(metadataRetriever);
- }
-
- public Bitmap getAvatar(String avatar, int size) {
- if (avatar == null) {
- return null;
- }
- Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
- if (bm == null) {
- return null;
- }
- return bm;
- }
-
- public boolean isFileAvailable(Message message) {
- return getFile(message).exists();
- }
-
- private static class Dimensions {
- public final int width;
- public final int height;
-
- Dimensions(int height, int width) {
- this.width = width;
- this.height = height;
- }
-
- public int getMin() {
- return Math.min(width, height);
- }
-
- public boolean valid() {
- return width > 0 && height > 0;
- }
- }
-
- private static class NotAVideoFile extends Exception {
- public NotAVideoFile(Throwable t) {
- super(t);
- }
-
- public NotAVideoFile() {
- super();
- }
- }
-
- public class FileCopyException extends Exception {
- private static final long serialVersionUID = -1010013599132881427L;
- private int resId;
-
- public FileCopyException(int resId) {
- this.resId = resId;
- }
-
- public int getResId() {
- return resId;
- }
- }
+ private static final Object THUMBNAIL_LOCK = new Object();
+
+ private static final SimpleDateFormat IMAGE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
+
+ private static final String FILE_PROVIDER = ".files";
+
+ private XmppConnectionService mXmppConnectionService;
+
+ public FileBackend(XmppConnectionService service) {
+ this.mXmppConnectionService = service;
+ }
+
+ private static boolean isInDirectoryThatShouldNotBeScanned(Context context, File file) {
+ return isInDirectoryThatShouldNotBeScanned(context, file.getAbsolutePath());
+ }
+
+ public static boolean isInDirectoryThatShouldNotBeScanned(Context context, String path) {
+ for (String type : new String[]{RecordingActivity.STORAGE_DIRECTORY_TYPE_NAME, "Files"}) {
+ if (path.startsWith(getConversationsDirectory(context, type))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static long getFileSize(Context context, Uri uri) {
+ try {
+ final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ long size = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE));
+ cursor.close();
+ return size;
+ } else {
+ return -1;
+ }
+ } catch (Exception e) {
+ return -1;
+ }
+ }
+
+ public static boolean allFilesUnderSize(Context context, List uris, long max) {
+ if (max <= 0) {
+ Log.d(Config.LOGTAG, "server did not report max file size for http upload");
+ return true; //exception to be compatible with HTTP Upload < v0.2
+ }
+ for (Uri uri : uris) {
+ String mime = context.getContentResolver().getType(uri);
+ if (mime != null && mime.startsWith("video/")) {
+ try {
+ Dimensions dimensions = FileBackend.getVideoDimensions(context, uri);
+ if (dimensions.getMin() > 720) {
+ Log.d(Config.LOGTAG, "do not consider video file with min width larger than 720 for size check");
+ continue;
+ }
+ } catch (NotAVideoFile notAVideoFile) {
+ //ignore and fall through
+ }
+ }
+ if (FileBackend.getFileSize(context, uri) > max) {
+ Log.d(Config.LOGTAG, "not all files are under " + max + " bytes. suggesting falling back to jingle");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static String getConversationsDirectory(Context context, final String type) {
+ if (Config.ONLY_INTERNAL_STORAGE) {
+ return context.getFilesDir().getAbsolutePath() + "/" + type + "/";
+ } else {
+ return getAppMediaDirectory(context) + context.getString(R.string.app_name) + " " + type + "/";
+ }
+ }
+
+ public static String getAppMediaDirectory(Context context) {
+ return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + context.getString(R.string.app_name) + "/Media/";
+ }
+
+ public static String getConversationsLogsDirectory() {
+ return Environment.getExternalStorageDirectory().getAbsolutePath() + "/Conversations/";
+ }
+
+ private static Bitmap rotate(Bitmap bitmap, int degree) {
+ if (degree == 0) {
+ return bitmap;
+ }
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ Matrix mtx = new Matrix();
+ mtx.postRotate(degree);
+ Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
+ if (bitmap != null && !bitmap.isRecycled()) {
+ bitmap.recycle();
+ }
+ return result;
+ }
+
+ public static boolean isPathBlacklisted(String path) {
+ final String androidDataPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/";
+ return path.startsWith(androidDataPath);
+ }
+
+ private static Paint createAntiAliasingPaint() {
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setFilterBitmap(true);
+ paint.setDither(true);
+ return paint;
+ }
+
+ private static String getTakePhotoPath() {
+ return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/Camera/";
+ }
+
+ public static Uri getUriForFile(Context context, File file) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || Config.ONLY_INTERNAL_STORAGE) {
+ try {
+ return FileProvider.getUriForFile(context, getAuthority(context), file);
+ } catch (IllegalArgumentException e) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ throw new SecurityException(e);
+ } else {
+ return Uri.fromFile(file);
+ }
+ }
+ } else {
+ return Uri.fromFile(file);
+ }
+ }
+
+ public static String getAuthority(Context context) {
+ return context.getPackageName() + FILE_PROVIDER;
+ }
+
+ private static boolean hasAlpha(final Bitmap bitmap) {
+ for (int x = 0; x < bitmap.getWidth(); ++x) {
+ for (int y = 0; y < bitmap.getWidth(); ++y) {
+ if (Color.alpha(bitmap.getPixel(x, y)) < 255) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static int calcSampleSize(File image, int size) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(image.getAbsolutePath(), options);
+ return calcSampleSize(options, size);
+ }
+
+ private static int calcSampleSize(BitmapFactory.Options options, int size) {
+ int height = options.outHeight;
+ int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > size || width > size) {
+ int halfHeight = height / 2;
+ int halfWidth = width / 2;
+
+ while ((halfHeight / inSampleSize) > size
+ && (halfWidth / inSampleSize) > size) {
+ inSampleSize *= 2;
+ }
+ }
+ return inSampleSize;
+ }
+
+ private static Dimensions getVideoDimensions(Context context, Uri uri) throws NotAVideoFile {
+ MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
+ try {
+ mediaMetadataRetriever.setDataSource(context, uri);
+ } catch (RuntimeException e) {
+ throw new NotAVideoFile(e);
+ }
+ return getVideoDimensions(mediaMetadataRetriever);
+ }
+
+ private static Dimensions getVideoDimensionsOfFrame(MediaMetadataRetriever mediaMetadataRetriever) {
+ Bitmap bitmap = null;
+ try {
+ bitmap = mediaMetadataRetriever.getFrameAtTime();
+ return new Dimensions(bitmap.getHeight(), bitmap.getWidth());
+ } catch (Exception e) {
+ return null;
+ } finally {
+ if (bitmap != null) {
+ bitmap.recycle();
+ ;
+ }
+ }
+ }
+
+ private static Dimensions getVideoDimensions(MediaMetadataRetriever metadataRetriever) throws NotAVideoFile {
+ String hasVideo = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
+ if (hasVideo == null) {
+ throw new NotAVideoFile();
+ }
+ Dimensions dimensions = getVideoDimensionsOfFrame(metadataRetriever);
+ if (dimensions != null) {
+ return dimensions;
+ }
+ int rotation = extractRotationFromMediaRetriever(metadataRetriever);
+ boolean rotated = rotation == 90 || rotation == 270;
+ int height;
+ try {
+ String h = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
+ height = Integer.parseInt(h);
+ } catch (Exception e) {
+ height = -1;
+ }
+ int width;
+ try {
+ String w = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
+ width = Integer.parseInt(w);
+ } catch (Exception e) {
+ width = -1;
+ }
+ metadataRetriever.release();
+ Log.d(Config.LOGTAG, "extracted video dims " + width + "x" + height);
+ return rotated ? new Dimensions(width, height) : new Dimensions(height, width);
+ }
+
+ private static int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) {
+ String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+ try {
+ return Integer.parseInt(r);
+ } catch (Exception e) {
+ return 0;
+ }
+ }
+
+ public static void close(Closeable stream) {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ public static void close(Socket socket) {
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ public static boolean weOwnFile(Context context, Uri uri) {
+ if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+ return false;
+ } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return fileIsInFilesDir(context, uri);
+ } else {
+ return weOwnFileLollipop(uri);
+ }
+ }
+
+ /**
+ * This is more than hacky but probably way better than doing nothing
+ * Further 'optimizations' might contain to get the parents of CacheDir and NoBackupDir
+ * and check against those as well
+ */
+ private static boolean fileIsInFilesDir(Context context, Uri uri) {
+ try {
+ final String haystack = context.getFilesDir().getParentFile().getCanonicalPath();
+ final String needle = new File(uri.getPath()).getCanonicalPath();
+ return needle.startsWith(haystack);
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private static boolean weOwnFileLollipop(Uri uri) {
+ try {
+ File file = new File(uri.getPath());
+ FileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).getFileDescriptor();
+ StructStat st = Os.fstat(fd);
+ return st.st_uid == android.os.Process.myUid();
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (Exception e) {
+ return true;
+ }
+ }
+
+ private void createNoMedia(File diretory) {
+ final File noMedia = new File(diretory, ".nomedia");
+ if (!noMedia.exists()) {
+ try {
+ if (!noMedia.createNewFile()) {
+ Log.d(Config.LOGTAG, "created nomedia file " + noMedia.getAbsolutePath());
+ }
+ } catch (Exception e) {
+ Log.d(Config.LOGTAG, "could not create nomedia file");
+ }
+ }
+ }
+
+ public void updateMediaScanner(File file) {
+ if (!isInDirectoryThatShouldNotBeScanned(mXmppConnectionService, file)) {
+ Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+ intent.setData(Uri.fromFile(file));
+ mXmppConnectionService.sendBroadcast(intent);
+ } else if (file.getAbsolutePath().startsWith(getAppMediaDirectory(mXmppConnectionService))) {
+ createNoMedia(file.getParentFile());
+ }
+ }
+
+ public boolean deleteFile(Message message) {
+ File file = getFile(message);
+ if (file.delete()) {
+ updateMediaScanner(file);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public DownloadableFile getFile(Message message) {
+ return getFile(message, true);
+ }
+
+ public DownloadableFile getFileForPath(String path, String mime) {
+ final DownloadableFile file;
+ if (path.startsWith("/")) {
+ file = new DownloadableFile(path);
+ } else {
+ if (mime != null && mime.startsWith("image/")) {
+ file = new DownloadableFile(getConversationsDirectory("Images") + path);
+ } else if (mime != null && mime.startsWith("video/")) {
+ file = new DownloadableFile(getConversationsDirectory("Videos") + path);
+ } else {
+ file = new DownloadableFile(getConversationsDirectory("Files") + path);
+ }
+ }
+ return file;
+ }
+
+ public DownloadableFile getFile(Message message, boolean decrypted) {
+ final boolean encrypted = !decrypted
+ && (message.getEncryption() == Message.ENCRYPTION_PGP
+ || message.getEncryption() == Message.ENCRYPTION_DECRYPTED);
+ String path = message.getRelativeFilePath();
+ if (path == null) {
+ path = message.getUuid();
+ }
+ final DownloadableFile file = getFileForPath(path, message.getMimeType());
+ if (encrypted) {
+ return new DownloadableFile(getConversationsDirectory("Files") + file.getName() + ".pgp");
+ } else {
+ return file;
+ }
+ }
+
+ public String getConversationsDirectory(final String type) {
+ return getConversationsDirectory(mXmppConnectionService, type);
+ }
+
+ private Bitmap resize(final Bitmap originalBitmap, int size) throws IOException {
+ int w = originalBitmap.getWidth();
+ int h = originalBitmap.getHeight();
+ if (w <= 0 || h <= 0) {
+ throw new IOException("Decoded bitmap reported bounds smaller 0");
+ } else if (Math.max(w, h) > size) {
+ int scalledW;
+ int scalledH;
+ if (w <= h) {
+ scalledW = Math.max((int) (w / ((double) h / size)), 1);
+ scalledH = size;
+ } else {
+ scalledW = size;
+ scalledH = Math.max((int) (h / ((double) w / size)), 1);
+ }
+ final Bitmap result = Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true);
+ if (!originalBitmap.isRecycled()) {
+ originalBitmap.recycle();
+ }
+ return result;
+ } else {
+ return originalBitmap;
+ }
+ }
+
+ public boolean useImageAsIs(Uri uri) {
+ String path = getOriginalPath(uri);
+ if (path == null || isPathBlacklisted(path)) {
+ return false;
+ }
+ File file = new File(path);
+ long size = file.length();
+ if (size == 0 || size >= mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize)) {
+ return false;
+ }
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ try {
+ BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri), null, options);
+ if (options.outMimeType == null || options.outHeight <= 0 || options.outWidth <= 0) {
+ return false;
+ }
+ return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase()));
+ } catch (FileNotFoundException e) {
+ return false;
+ }
+ }
+
+ public String getOriginalPath(Uri uri) {
+ return FileUtils.getPath(mXmppConnectionService, uri);
+ }
+
+ private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
+ Log.d(Config.LOGTAG, "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath());
+ file.getParentFile().mkdirs();
+ OutputStream os = null;
+ InputStream is = null;
+ try {
+ file.createNewFile();
+ os = new FileOutputStream(file);
+ is = mXmppConnectionService.getContentResolver().openInputStream(uri);
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = is.read(buffer)) > 0) {
+ try {
+ os.write(buffer, 0, length);
+ } catch (IOException e) {
+ throw new FileWriterException();
+ }
+ }
+ try {
+ os.flush();
+ } catch (IOException e) {
+ throw new FileWriterException();
+ }
+ } catch (FileNotFoundException e) {
+ throw new FileCopyException(R.string.error_file_not_found);
+ } catch (FileWriterException e) {
+ throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new FileCopyException(R.string.error_io_exception);
+ } finally {
+ close(os);
+ close(is);
+ }
+ }
+
+ public void copyFileToPrivateStorage(Message message, Uri uri, String type) throws FileCopyException {
+ String mime = type != null ? type : MimeUtils.guessMimeTypeFromUri(mXmppConnectionService, uri);
+ Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime=" + mime + ")");
+ String extension = MimeUtils.guessExtensionFromMimeType(mime);
+ if (extension == null) {
+ Log.d(Config.LOGTAG, "extension from mime type was null");
+ extension = getExtensionFromUri(uri);
+ }
+ if ("ogg".equals(extension) && type != null && type.startsWith("audio/")) {
+ extension = "oga";
+ }
+ message.setRelativeFilePath(message.getUuid() + "." + extension);
+ copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri);
+ }
+
+ private String getExtensionFromUri(Uri uri) {
+ String[] projection = {MediaStore.MediaColumns.DATA};
+ String filename = null;
+ Cursor cursor = mXmppConnectionService.getContentResolver().query(uri, projection, null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ filename = cursor.getString(0);
+ }
+ } catch (Exception e) {
+ filename = null;
+ } finally {
+ cursor.close();
+ }
+ }
+ if (filename == null) {
+ final List segments = uri.getPathSegments();
+ if (segments.size() > 0) {
+ filename = segments.get(segments.size() - 1);
+ }
+ }
+ int pos = filename == null ? -1 : filename.lastIndexOf('.');
+ return pos > 0 ? filename.substring(pos + 1) : null;
+ }
+
+ private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException {
+ file.getParentFile().mkdirs();
+ InputStream is = null;
+ OutputStream os = null;
+ try {
+ if (!file.exists() && !file.createNewFile()) {
+ throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
+ }
+ is = mXmppConnectionService.getContentResolver().openInputStream(image);
+ if (is == null) {
+ throw new FileCopyException(R.string.error_not_an_image_file);
+ }
+ Bitmap originalBitmap;
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ int inSampleSize = (int) Math.pow(2, sampleSize);
+ Log.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize);
+ options.inSampleSize = inSampleSize;
+ originalBitmap = BitmapFactory.decodeStream(is, null, options);
+ is.close();
+ if (originalBitmap == null) {
+ throw new FileCopyException(R.string.error_not_an_image_file);
+ }
+ Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE);
+ int rotation = getRotation(image);
+ scaledBitmap = rotate(scaledBitmap, rotation);
+ boolean targetSizeReached = false;
+ int quality = Config.IMAGE_QUALITY;
+ final int imageMaxSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
+ while (!targetSizeReached) {
+ os = new FileOutputStream(file);
+ boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, quality, os);
+ if (!success) {
+ throw new FileCopyException(R.string.error_compressing_image);
+ }
+ os.flush();
+ targetSizeReached = file.length() <= imageMaxSize || quality <= 50;
+ quality -= 5;
+ }
+ scaledBitmap.recycle();
+ } catch (FileNotFoundException e) {
+ throw new FileCopyException(R.string.error_file_not_found);
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new FileCopyException(R.string.error_io_exception);
+ } catch (SecurityException e) {
+ throw new FileCopyException(R.string.error_security_exception_during_image_copy);
+ } catch (OutOfMemoryError e) {
+ ++sampleSize;
+ if (sampleSize <= 3) {
+ copyImageToPrivateStorage(file, image, sampleSize);
+ } else {
+ throw new FileCopyException(R.string.error_out_of_memory);
+ }
+ } finally {
+ close(os);
+ close(is);
+ }
+ }
+
+ public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException {
+ Log.d(Config.LOGTAG, "copy image (" + image.toString() + ") to private storage " + file.getAbsolutePath());
+ copyImageToPrivateStorage(file, image, 0);
+ }
+
+ public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException {
+ switch (Config.IMAGE_FORMAT) {
+ case JPEG:
+ message.setRelativeFilePath(message.getUuid() + ".jpg");
+ break;
+ case PNG:
+ message.setRelativeFilePath(message.getUuid() + ".png");
+ break;
+ case WEBP:
+ message.setRelativeFilePath(message.getUuid() + ".webp");
+ break;
+ }
+ copyImageToPrivateStorage(getFile(message), image);
+ updateFileParams(message);
+ }
+
+ private int getRotation(File file) {
+ return getRotation(Uri.parse("file://" + file.getAbsolutePath()));
+ }
+
+ private int getRotation(Uri image) {
+ InputStream is = null;
+ try {
+ is = mXmppConnectionService.getContentResolver().openInputStream(image);
+ return ExifHelper.getOrientation(is);
+ } catch (FileNotFoundException e) {
+ return 0;
+ } finally {
+ close(is);
+ }
+ }
+
+ public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws IOException {
+ final String uuid = message.getUuid();
+ final LruCache cache = mXmppConnectionService.getBitmapCache();
+ Bitmap thumbnail = cache.get(uuid);
+ if ((thumbnail == null) && (!cacheOnly)) {
+ synchronized (THUMBNAIL_LOCK) {
+ thumbnail = cache.get(uuid);
+ if (thumbnail != null) {
+ return thumbnail;
+ }
+ DownloadableFile file = getFile(message);
+ final String mime = file.getMimeType();
+ if (mime.startsWith("video/")) {
+ thumbnail = getVideoPreview(file, size);
+ } else {
+ Bitmap fullsize = getFullsizeImagePreview(file, size);
+ if (fullsize == null) {
+ throw new FileNotFoundException();
+ }
+ thumbnail = resize(fullsize, size);
+ thumbnail = rotate(thumbnail, getRotation(file));
+ if (mime.equals("image/gif")) {
+ Bitmap withGifOverlay = thumbnail.copy(Bitmap.Config.ARGB_8888, true);
+ drawOverlay(withGifOverlay, paintOverlayBlack(withGifOverlay) ? R.drawable.play_gif_black : R.drawable.play_gif_white, 1.0f);
+ thumbnail.recycle();
+ thumbnail = withGifOverlay;
+ }
+ }
+ this.mXmppConnectionService.getBitmapCache().put(uuid, thumbnail);
+ }
+ }
+ return thumbnail;
+ }
+
+ private Bitmap getFullsizeImagePreview(File file, int size) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = calcSampleSize(file, size);
+ try {
+ return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+ } catch (OutOfMemoryError e) {
+ options.inSampleSize *= 2;
+ return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+ }
+ }
+
+ private void drawOverlay(Bitmap bitmap, int resource, float factor) {
+ Bitmap overlay = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource);
+ Canvas canvas = new Canvas(bitmap);
+ float targetSize = Math.min(canvas.getWidth(), canvas.getHeight()) * factor;
+ Log.d(Config.LOGTAG, "target size overlay: " + targetSize + " overlay bitmap size was " + overlay.getHeight());
+ float left = (canvas.getWidth() - targetSize) / 2.0f;
+ float top = (canvas.getHeight() - targetSize) / 2.0f;
+ RectF dst = new RectF(left, top, left + targetSize - 1, top + targetSize - 1);
+ canvas.drawBitmap(overlay, null, dst, createAntiAliasingPaint());
+ }
+
+ /**
+ * https://stackoverflow.com/a/3943023/210897
+ */
+ private boolean paintOverlayBlack(final Bitmap bitmap) {
+ int record = 0;
+ for (int y = 0; y < bitmap.getHeight(); ++y) {
+ for (int x = 0; x < bitmap.getWidth(); ++x) {
+ int pixel = bitmap.getPixel(x, y);
+ if ((Color.red(pixel) * 0.299 + Color.green(pixel) * 0.587 + Color.blue(pixel) * 0.114) > 186) {
+ --record;
+ } else {
+ ++record;
+ }
+ }
+ }
+ return record < 0;
+ }
+
+ private Bitmap getVideoPreview(File file, int size) {
+ MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
+ Bitmap frame;
+ try {
+ metadataRetriever.setDataSource(file.getAbsolutePath());
+ frame = metadataRetriever.getFrameAtTime(0);
+ metadataRetriever.release();
+ frame = resize(frame, size);
+ } catch (IOException | RuntimeException e) {
+ frame = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ frame.eraseColor(0xff000000);
+ }
+ drawOverlay(frame, paintOverlayBlack(frame) ? R.drawable.play_video_black : R.drawable.play_video_white, 0.75f);
+ return frame;
+ }
+
+ public Uri getTakePhotoUri() {
+ File file;
+ if (Config.ONLY_INTERNAL_STORAGE) {
+ file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath(), "Camera/IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
+ } else {
+ file = new File(getTakePhotoPath() + "IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
+ }
+ file.getParentFile().mkdirs();
+ return getUriForFile(mXmppConnectionService, file);
+ }
+
+ public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
+
+ final Avatar uncompressAvatar = getUncompressedAvatar(image);
+ if (uncompressAvatar != null && uncompressAvatar.image.length() <= Config.AVATAR_CHAR_LIMIT) {
+ return uncompressAvatar;
+ }
+ if (uncompressAvatar != null) {
+ Log.d(Config.LOGTAG, "uncompressed avatar exceeded char limit by " + (uncompressAvatar.image.length() - Config.AVATAR_CHAR_LIMIT));
+ }
+
+ Bitmap bm = cropCenterSquare(image, size);
+ if (bm == null) {
+ return null;
+ }
+ if (hasAlpha(bm)) {
+ Log.d(Config.LOGTAG, "alpha in avatar detected; uploading as PNG");
+ bm.recycle();
+ bm = cropCenterSquare(image, 96);
+ return getPepAvatar(bm, Bitmap.CompressFormat.PNG, 100);
+ }
+ return getPepAvatar(bm, format, 100);
+ }
+
+ private Avatar getUncompressedAvatar(Uri uri) {
+ Bitmap bitmap = null;
+ try {
+ bitmap = BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri));
+ return getPepAvatar(bitmap, Bitmap.CompressFormat.PNG, 100);
+ } catch (Exception e) {
+ if (bitmap != null) {
+ bitmap.recycle();
+ }
+ }
+ return null;
+ }
+
+ private Avatar getPepAvatar(Bitmap bitmap, Bitmap.CompressFormat format, int quality) {
+ try {
+ ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
+ Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ DigestOutputStream mDigestOutputStream = new DigestOutputStream(mBase64OutputStream, digest);
+ if (!bitmap.compress(format, quality, mDigestOutputStream)) {
+ return null;
+ }
+ mDigestOutputStream.flush();
+ mDigestOutputStream.close();
+ long chars = mByteArrayOutputStream.size();
+ if (format != Bitmap.CompressFormat.PNG && quality >= 50 && chars >= Config.AVATAR_CHAR_LIMIT) {
+ int q = quality - 2;
+ Log.d(Config.LOGTAG, "avatar char length was " + chars + " reducing quality to " + q);
+ return getPepAvatar(bitmap, format, q);
+ }
+ Log.d(Config.LOGTAG, "settled on char length " + chars + " with quality=" + quality);
+ final Avatar avatar = new Avatar();
+ avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
+ avatar.image = new String(mByteArrayOutputStream.toByteArray());
+ if (format.equals(Bitmap.CompressFormat.WEBP)) {
+ avatar.type = "image/webp";
+ } else if (format.equals(Bitmap.CompressFormat.JPEG)) {
+ avatar.type = "image/jpeg";
+ } else if (format.equals(Bitmap.CompressFormat.PNG)) {
+ avatar.type = "image/png";
+ }
+ avatar.width = bitmap.getWidth();
+ avatar.height = bitmap.getHeight();
+ return avatar;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public Avatar getStoredPepAvatar(String hash) {
+ if (hash == null) {
+ return null;
+ }
+ Avatar avatar = new Avatar();
+ File file = new File(getAvatarPath(hash));
+ FileInputStream is = null;
+ try {
+ avatar.size = file.length();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+ is = new FileInputStream(file);
+ ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
+ Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ DigestOutputStream os = new DigestOutputStream(mBase64OutputStream, digest);
+ byte[] buffer = new byte[4096];
+ int length;
+ while ((length = is.read(buffer)) > 0) {
+ os.write(buffer, 0, length);
+ }
+ os.flush();
+ os.close();
+ avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
+ avatar.image = new String(mByteArrayOutputStream.toByteArray());
+ avatar.height = options.outHeight;
+ avatar.width = options.outWidth;
+ avatar.type = options.outMimeType;
+ return avatar;
+ } catch (NoSuchAlgorithmException | IOException e) {
+ return null;
+ } finally {
+ close(is);
+ }
+ }
+
+ public boolean isAvatarCached(Avatar avatar) {
+ File file = new File(getAvatarPath(avatar.getFilename()));
+ return file.exists();
+ }
+
+ public boolean save(final Avatar avatar) {
+ File file;
+ if (isAvatarCached(avatar)) {
+ file = new File(getAvatarPath(avatar.getFilename()));
+ avatar.size = file.length();
+ } else {
+ file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + UUID.randomUUID().toString());
+ if (file.getParentFile().mkdirs()) {
+ Log.d(Config.LOGTAG, "created cache directory");
+ }
+ OutputStream os = null;
+ try {
+ if (!file.createNewFile()) {
+ Log.d(Config.LOGTAG, "unable to create temporary file " + file.getAbsolutePath());
+ }
+ os = new FileOutputStream(file);
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest);
+ final byte[] bytes = avatar.getImageAsBytes();
+ mDigestOutputStream.write(bytes);
+ mDigestOutputStream.flush();
+ mDigestOutputStream.close();
+ String sha1sum = CryptoHelper.bytesToHex(digest.digest());
+ if (sha1sum.equals(avatar.sha1sum)) {
+ File outputFile = new File(getAvatarPath(avatar.getFilename()));
+ if (outputFile.getParentFile().mkdirs()) {
+ Log.d(Config.LOGTAG, "created avatar directory");
+ }
+ String filename = getAvatarPath(avatar.getFilename());
+ if (!file.renameTo(new File(filename))) {
+ Log.d(Config.LOGTAG, "unable to rename " + file.getAbsolutePath() + " to " + outputFile);
+ return false;
+ }
+ } else {
+ Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner);
+ if (!file.delete()) {
+ Log.d(Config.LOGTAG, "unable to delete temporary file");
+ }
+ return false;
+ }
+ avatar.size = bytes.length;
+ } catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) {
+ return false;
+ } finally {
+ close(os);
+ }
+ }
+ return true;
+ }
+
+ private String getAvatarPath(String avatar) {
+ return mXmppConnectionService.getFilesDir().getAbsolutePath() + "/avatars/" + avatar;
+ }
+
+ public Uri getAvatarUri(String avatar) {
+ return Uri.parse("file:" + getAvatarPath(avatar));
+ }
+
+ public Bitmap cropCenterSquare(Uri image, int size) {
+ if (image == null) {
+ return null;
+ }
+ InputStream is = null;
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = calcSampleSize(image, size);
+ is = mXmppConnectionService.getContentResolver().openInputStream(image);
+ if (is == null) {
+ return null;
+ }
+ Bitmap input = BitmapFactory.decodeStream(is, null, options);
+ if (input == null) {
+ return null;
+ } else {
+ input = rotate(input, getRotation(image));
+ return cropCenterSquare(input, size);
+ }
+ } catch (FileNotFoundException | SecurityException e) {
+ return null;
+ } finally {
+ close(is);
+ }
+ }
+
+ public Bitmap cropCenter(Uri image, int newHeight, int newWidth) {
+ if (image == null) {
+ return null;
+ }
+ InputStream is = null;
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = calcSampleSize(image, Math.max(newHeight, newWidth));
+ is = mXmppConnectionService.getContentResolver().openInputStream(image);
+ if (is == null) {
+ return null;
+ }
+ Bitmap source = BitmapFactory.decodeStream(is, null, options);
+ if (source == null) {
+ return null;
+ }
+ int sourceWidth = source.getWidth();
+ int sourceHeight = source.getHeight();
+ float xScale = (float) newWidth / sourceWidth;
+ float yScale = (float) newHeight / sourceHeight;
+ float scale = Math.max(xScale, yScale);
+ float scaledWidth = scale * sourceWidth;
+ float scaledHeight = scale * sourceHeight;
+ float left = (newWidth - scaledWidth) / 2;
+ float top = (newHeight - scaledHeight) / 2;
+
+ RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);
+ Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(dest);
+ canvas.drawBitmap(source, null, targetRect, createAntiAliasingPaint());
+ if (source.isRecycled()) {
+ source.recycle();
+ }
+ return dest;
+ } catch (SecurityException e) {
+ return null; //android 6.0 with revoked permissions for example
+ } catch (FileNotFoundException e) {
+ return null;
+ } finally {
+ close(is);
+ }
+ }
+
+ public Bitmap cropCenterSquare(Bitmap input, int size) {
+ int w = input.getWidth();
+ int h = input.getHeight();
+
+ float scale = Math.max((float) size / h, (float) size / w);
+
+ float outWidth = scale * w;
+ float outHeight = scale * h;
+ float left = (size - outWidth) / 2;
+ float top = (size - outHeight) / 2;
+ RectF target = new RectF(left, top, left + outWidth, top + outHeight);
+
+ Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(output);
+ canvas.drawBitmap(input, null, target, createAntiAliasingPaint());
+ if (!input.isRecycled()) {
+ input.recycle();
+ }
+ return output;
+ }
+
+ private int calcSampleSize(Uri image, int size) throws FileNotFoundException, SecurityException {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(image), null, options);
+ return calcSampleSize(options, size);
+ }
+
+ public void updateFileParams(Message message) {
+ updateFileParams(message, null);
+ }
+
+ public void updateFileParams(Message message, URL url) {
+ DownloadableFile file = getFile(message);
+ final String mime = file.getMimeType();
+ boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/"));
+ boolean video = mime != null && mime.startsWith("video/");
+ boolean audio = mime != null && mime.startsWith("audio/");
+ final StringBuilder body = new StringBuilder();
+ if (url != null) {
+ body.append(url.toString());
+ }
+ body.append('|').append(file.getSize());
+ if (image || video) {
+ try {
+ Dimensions dimensions = image ? getImageDimensions(file) : getVideoDimensions(file);
+ if (dimensions.valid()) {
+ body.append('|').append(dimensions.width).append('|').append(dimensions.height);
+ }
+ } catch (NotAVideoFile notAVideoFile) {
+ Log.d(Config.LOGTAG, "file with mime type " + file.getMimeType() + " was not a video file");
+ //fall threw
+ }
+ } else if (audio) {
+ body.append("|0|0|").append(getMediaRuntime(file));
+ }
+ message.setBody(body.toString());
+ }
+
+ public int getMediaRuntime(Uri uri) {
+ try {
+ MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
+ mediaMetadataRetriever.setDataSource(mXmppConnectionService, uri);
+ return Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
+ } catch (RuntimeException e) {
+ return 0;
+ }
+ }
+
+ private int getMediaRuntime(File file) {
+ try {
+ MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
+ mediaMetadataRetriever.setDataSource(file.toString());
+ return Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
+ } catch (RuntimeException e) {
+ return 0;
+ }
+ }
+
+ private Dimensions getImageDimensions(File file) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+ int rotation = getRotation(file);
+ boolean rotated = rotation == 90 || rotation == 270;
+ int imageHeight = rotated ? options.outWidth : options.outHeight;
+ int imageWidth = rotated ? options.outHeight : options.outWidth;
+ return new Dimensions(imageHeight, imageWidth);
+ }
+
+ private Dimensions getVideoDimensions(File file) throws NotAVideoFile {
+ MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
+ try {
+ metadataRetriever.setDataSource(file.getAbsolutePath());
+ } catch (RuntimeException e) {
+ throw new NotAVideoFile(e);
+ }
+ return getVideoDimensions(metadataRetriever);
+ }
+
+ public Bitmap getAvatar(String avatar, int size) {
+ if (avatar == null) {
+ return null;
+ }
+ Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
+ if (bm == null) {
+ return null;
+ }
+ return bm;
+ }
+
+ public boolean isFileAvailable(Message message) {
+ return getFile(message).exists();
+ }
+
+ private static class Dimensions {
+ public final int width;
+ public final int height;
+
+ Dimensions(int height, int width) {
+ this.width = width;
+ this.height = height;
+ }
+
+ public int getMin() {
+ return Math.min(width, height);
+ }
+
+ public boolean valid() {
+ return width > 0 && height > 0;
+ }
+ }
+
+ private static class NotAVideoFile extends Exception {
+ public NotAVideoFile(Throwable t) {
+ super(t);
+ }
+
+ public NotAVideoFile() {
+ super();
+ }
+ }
+
+ public class FileCopyException extends Exception {
+ private static final long serialVersionUID = -1010013599132881427L;
+ private int resId;
+
+ public FileCopyException(int resId) {
+ this.resId = resId;
+ }
+
+ public int getResId() {
+ return resId;
+ }
+ }
}
diff --git a/src/main/res/drawable-hdpi/date_bubble_grey.9.png b/src/main/res/drawable-hdpi/date_bubble_grey.9.png
index 39a3c42d9..1b8d67520 100644
Binary files a/src/main/res/drawable-hdpi/date_bubble_grey.9.png and b/src/main/res/drawable-hdpi/date_bubble_grey.9.png differ
diff --git a/src/main/res/drawable-hdpi/date_bubble_white.9.png b/src/main/res/drawable-hdpi/date_bubble_white.9.png
index 2d3ef050d..090af33a9 100644
Binary files a/src/main/res/drawable-hdpi/date_bubble_white.9.png and b/src/main/res/drawable-hdpi/date_bubble_white.9.png differ
diff --git a/src/main/res/drawable-hdpi/message_bubble_received.9.png b/src/main/res/drawable-hdpi/message_bubble_received.9.png
index 7216e4ac1..4e4329f91 100644
Binary files a/src/main/res/drawable-hdpi/message_bubble_received.9.png and b/src/main/res/drawable-hdpi/message_bubble_received.9.png differ
diff --git a/src/main/res/drawable-hdpi/message_bubble_received_dark.9.png b/src/main/res/drawable-hdpi/message_bubble_received_dark.9.png
index 398787321..714568183 100644
Binary files a/src/main/res/drawable-hdpi/message_bubble_received_dark.9.png and b/src/main/res/drawable-hdpi/message_bubble_received_dark.9.png differ
diff --git a/src/main/res/drawable-hdpi/message_bubble_received_grey.9.png b/src/main/res/drawable-hdpi/message_bubble_received_grey.9.png
index a1b190287..f54f43235 100644
Binary files a/src/main/res/drawable-hdpi/message_bubble_received_grey.9.png and b/src/main/res/drawable-hdpi/message_bubble_received_grey.9.png differ
diff --git a/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png
index eefb7ef04..69d08da2b 100644
Binary files a/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png and b/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png differ
diff --git a/src/main/res/drawable-hdpi/message_bubble_received_white.9.png b/src/main/res/drawable-hdpi/message_bubble_received_white.9.png
index 1759963ca..4c1196b9f 100644
Binary files a/src/main/res/drawable-hdpi/message_bubble_received_white.9.png and b/src/main/res/drawable-hdpi/message_bubble_received_white.9.png differ
diff --git a/src/main/res/drawable-hdpi/message_bubble_sent.9.png b/src/main/res/drawable-hdpi/message_bubble_sent.9.png
index 2fd82ffe1..4179c9d79 100644
Binary files a/src/main/res/drawable-hdpi/message_bubble_sent.9.png and b/src/main/res/drawable-hdpi/message_bubble_sent.9.png differ
diff --git a/src/main/res/drawable-hdpi/message_bubble_sent_grey.9.png b/src/main/res/drawable-hdpi/message_bubble_sent_grey.9.png
index 1633b2fa6..d9450969d 100644
Binary files a/src/main/res/drawable-hdpi/message_bubble_sent_grey.9.png and b/src/main/res/drawable-hdpi/message_bubble_sent_grey.9.png differ
diff --git a/src/main/res/drawable-hdpi/play_gif_black.png b/src/main/res/drawable-hdpi/play_gif_black.png
new file mode 100644
index 000000000..054018ad3
Binary files /dev/null and b/src/main/res/drawable-hdpi/play_gif_black.png differ
diff --git a/src/main/res/drawable-hdpi/play_gif.png b/src/main/res/drawable-hdpi/play_gif_white.png
similarity index 100%
rename from src/main/res/drawable-hdpi/play_gif.png
rename to src/main/res/drawable-hdpi/play_gif_white.png
diff --git a/src/main/res/drawable-hdpi/play_video_black.png b/src/main/res/drawable-hdpi/play_video_black.png
new file mode 100644
index 000000000..533437fb8
Binary files /dev/null and b/src/main/res/drawable-hdpi/play_video_black.png differ
diff --git a/src/main/res/drawable-hdpi/play_video.png b/src/main/res/drawable-hdpi/play_video_white.png
similarity index 100%
rename from src/main/res/drawable-hdpi/play_video.png
rename to src/main/res/drawable-hdpi/play_video_white.png
diff --git a/src/main/res/drawable-mdpi/date_bubble_grey.9.png b/src/main/res/drawable-mdpi/date_bubble_grey.9.png
index bb12d5d0b..eb36a1c03 100644
Binary files a/src/main/res/drawable-mdpi/date_bubble_grey.9.png and b/src/main/res/drawable-mdpi/date_bubble_grey.9.png differ
diff --git a/src/main/res/drawable-mdpi/date_bubble_white.9.png b/src/main/res/drawable-mdpi/date_bubble_white.9.png
index af3c2d491..71d7293b0 100644
Binary files a/src/main/res/drawable-mdpi/date_bubble_white.9.png and b/src/main/res/drawable-mdpi/date_bubble_white.9.png differ
diff --git a/src/main/res/drawable-mdpi/message_bubble_received.9.png b/src/main/res/drawable-mdpi/message_bubble_received.9.png
index 7406ccc0e..b619b017d 100644
Binary files a/src/main/res/drawable-mdpi/message_bubble_received.9.png and b/src/main/res/drawable-mdpi/message_bubble_received.9.png differ
diff --git a/src/main/res/drawable-mdpi/message_bubble_received_dark.9.png b/src/main/res/drawable-mdpi/message_bubble_received_dark.9.png
index 387925323..4b2f47c9f 100644
Binary files a/src/main/res/drawable-mdpi/message_bubble_received_dark.9.png and b/src/main/res/drawable-mdpi/message_bubble_received_dark.9.png differ
diff --git a/src/main/res/drawable-mdpi/message_bubble_received_grey.9.png b/src/main/res/drawable-mdpi/message_bubble_received_grey.9.png
index 9fea89221..1c1f98c0d 100644
Binary files a/src/main/res/drawable-mdpi/message_bubble_received_grey.9.png and b/src/main/res/drawable-mdpi/message_bubble_received_grey.9.png differ
diff --git a/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png
index 272da4127..3e6c5f620 100644
Binary files a/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png and b/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png differ
diff --git a/src/main/res/drawable-mdpi/message_bubble_received_white.9.png b/src/main/res/drawable-mdpi/message_bubble_received_white.9.png
index 2013c6e07..981dbd2cc 100644
Binary files a/src/main/res/drawable-mdpi/message_bubble_received_white.9.png and b/src/main/res/drawable-mdpi/message_bubble_received_white.9.png differ
diff --git a/src/main/res/drawable-mdpi/message_bubble_sent.9.png b/src/main/res/drawable-mdpi/message_bubble_sent.9.png
index eb8992e81..dc946156c 100644
Binary files a/src/main/res/drawable-mdpi/message_bubble_sent.9.png and b/src/main/res/drawable-mdpi/message_bubble_sent.9.png differ
diff --git a/src/main/res/drawable-mdpi/message_bubble_sent_grey.9.png b/src/main/res/drawable-mdpi/message_bubble_sent_grey.9.png
index d1c94a7a0..bcb340f84 100644
Binary files a/src/main/res/drawable-mdpi/message_bubble_sent_grey.9.png and b/src/main/res/drawable-mdpi/message_bubble_sent_grey.9.png differ
diff --git a/src/main/res/drawable-mdpi/play_gif_black.png b/src/main/res/drawable-mdpi/play_gif_black.png
new file mode 100644
index 000000000..d0f0aae47
Binary files /dev/null and b/src/main/res/drawable-mdpi/play_gif_black.png differ
diff --git a/src/main/res/drawable-mdpi/play_gif.png b/src/main/res/drawable-mdpi/play_gif_white.png
similarity index 100%
rename from src/main/res/drawable-mdpi/play_gif.png
rename to src/main/res/drawable-mdpi/play_gif_white.png
diff --git a/src/main/res/drawable-mdpi/play_video_black.png b/src/main/res/drawable-mdpi/play_video_black.png
new file mode 100644
index 000000000..6c25cda8e
Binary files /dev/null and b/src/main/res/drawable-mdpi/play_video_black.png differ
diff --git a/src/main/res/drawable-mdpi/play_video.png b/src/main/res/drawable-mdpi/play_video_white.png
similarity index 100%
rename from src/main/res/drawable-mdpi/play_video.png
rename to src/main/res/drawable-mdpi/play_video_white.png
diff --git a/src/main/res/drawable-xhdpi/date_bubble_grey.9.png b/src/main/res/drawable-xhdpi/date_bubble_grey.9.png
index d86b8c68c..f55428d98 100644
Binary files a/src/main/res/drawable-xhdpi/date_bubble_grey.9.png and b/src/main/res/drawable-xhdpi/date_bubble_grey.9.png differ
diff --git a/src/main/res/drawable-xhdpi/date_bubble_white.9.png b/src/main/res/drawable-xhdpi/date_bubble_white.9.png
index e72b81c84..c62901af5 100644
Binary files a/src/main/res/drawable-xhdpi/date_bubble_white.9.png and b/src/main/res/drawable-xhdpi/date_bubble_white.9.png differ
diff --git a/src/main/res/drawable-xhdpi/message_bubble_received.9.png b/src/main/res/drawable-xhdpi/message_bubble_received.9.png
index e91de7120..742194f17 100644
Binary files a/src/main/res/drawable-xhdpi/message_bubble_received.9.png and b/src/main/res/drawable-xhdpi/message_bubble_received.9.png differ
diff --git a/src/main/res/drawable-xhdpi/message_bubble_received_dark.9.png b/src/main/res/drawable-xhdpi/message_bubble_received_dark.9.png
index d53c545d4..d3f5f7e58 100644
Binary files a/src/main/res/drawable-xhdpi/message_bubble_received_dark.9.png and b/src/main/res/drawable-xhdpi/message_bubble_received_dark.9.png differ
diff --git a/src/main/res/drawable-xhdpi/message_bubble_received_grey.9.png b/src/main/res/drawable-xhdpi/message_bubble_received_grey.9.png
index a3ad4bde2..2d4e6af33 100644
Binary files a/src/main/res/drawable-xhdpi/message_bubble_received_grey.9.png and b/src/main/res/drawable-xhdpi/message_bubble_received_grey.9.png differ
diff --git a/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png
index 784911b93..50e522032 100644
Binary files a/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png and b/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png differ
diff --git a/src/main/res/drawable-xhdpi/message_bubble_received_white.9.png b/src/main/res/drawable-xhdpi/message_bubble_received_white.9.png
index 29dd812cb..50584eaac 100644
Binary files a/src/main/res/drawable-xhdpi/message_bubble_received_white.9.png and b/src/main/res/drawable-xhdpi/message_bubble_received_white.9.png differ
diff --git a/src/main/res/drawable-xhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xhdpi/message_bubble_sent.9.png
index 2c569cd19..0f0c0c579 100644
Binary files a/src/main/res/drawable-xhdpi/message_bubble_sent.9.png and b/src/main/res/drawable-xhdpi/message_bubble_sent.9.png differ
diff --git a/src/main/res/drawable-xhdpi/message_bubble_sent_grey.9.png b/src/main/res/drawable-xhdpi/message_bubble_sent_grey.9.png
index 2eef1578b..ede14e1c5 100644
Binary files a/src/main/res/drawable-xhdpi/message_bubble_sent_grey.9.png and b/src/main/res/drawable-xhdpi/message_bubble_sent_grey.9.png differ
diff --git a/src/main/res/drawable-xhdpi/play_gif_black.png b/src/main/res/drawable-xhdpi/play_gif_black.png
new file mode 100644
index 000000000..e621c14b8
Binary files /dev/null and b/src/main/res/drawable-xhdpi/play_gif_black.png differ
diff --git a/src/main/res/drawable-xhdpi/play_gif.png b/src/main/res/drawable-xhdpi/play_gif_white.png
similarity index 100%
rename from src/main/res/drawable-xhdpi/play_gif.png
rename to src/main/res/drawable-xhdpi/play_gif_white.png
diff --git a/src/main/res/drawable-xhdpi/play_video_black.png b/src/main/res/drawable-xhdpi/play_video_black.png
new file mode 100644
index 000000000..79a4d382d
Binary files /dev/null and b/src/main/res/drawable-xhdpi/play_video_black.png differ
diff --git a/src/main/res/drawable-xhdpi/play_video.png b/src/main/res/drawable-xhdpi/play_video_white.png
similarity index 100%
rename from src/main/res/drawable-xhdpi/play_video.png
rename to src/main/res/drawable-xhdpi/play_video_white.png
diff --git a/src/main/res/drawable-xxhdpi/date_bubble_grey.9.png b/src/main/res/drawable-xxhdpi/date_bubble_grey.9.png
index 7a8087744..a434778b8 100644
Binary files a/src/main/res/drawable-xxhdpi/date_bubble_grey.9.png and b/src/main/res/drawable-xxhdpi/date_bubble_grey.9.png differ
diff --git a/src/main/res/drawable-xxhdpi/date_bubble_white.9.png b/src/main/res/drawable-xxhdpi/date_bubble_white.9.png
index 7d43e8177..9b5646040 100644
Binary files a/src/main/res/drawable-xxhdpi/date_bubble_white.9.png and b/src/main/res/drawable-xxhdpi/date_bubble_white.9.png differ
diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received.9.png
index 0b192f10d..fda98dcec 100644
Binary files a/src/main/res/drawable-xxhdpi/message_bubble_received.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_received.9.png differ
diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received_dark.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received_dark.9.png
index 83da1c8cc..5a88c0d91 100644
Binary files a/src/main/res/drawable-xxhdpi/message_bubble_received_dark.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_received_dark.9.png differ
diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received_grey.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received_grey.9.png
index 5dfaf22a1..6ecb82886 100644
Binary files a/src/main/res/drawable-xxhdpi/message_bubble_received_grey.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_received_grey.9.png differ
diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png
index ad8eae094..acc18615c 100644
Binary files a/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png differ
diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received_white.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received_white.9.png
index 24bcaa98f..bfd2b9319 100644
Binary files a/src/main/res/drawable-xxhdpi/message_bubble_received_white.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_received_white.9.png differ
diff --git a/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png
index 5c61e28d5..5fde2041a 100644
Binary files a/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png differ
diff --git a/src/main/res/drawable-xxhdpi/message_bubble_sent_grey.9.png b/src/main/res/drawable-xxhdpi/message_bubble_sent_grey.9.png
index 960758ae3..8a30c3a08 100644
Binary files a/src/main/res/drawable-xxhdpi/message_bubble_sent_grey.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_sent_grey.9.png differ
diff --git a/src/main/res/drawable-xxhdpi/play_gif_black.png b/src/main/res/drawable-xxhdpi/play_gif_black.png
new file mode 100644
index 000000000..f49471c5d
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/play_gif_black.png differ
diff --git a/src/main/res/drawable-xxhdpi/play_gif.png b/src/main/res/drawable-xxhdpi/play_gif_white.png
similarity index 100%
rename from src/main/res/drawable-xxhdpi/play_gif.png
rename to src/main/res/drawable-xxhdpi/play_gif_white.png
diff --git a/src/main/res/drawable-xxhdpi/play_video_black.png b/src/main/res/drawable-xxhdpi/play_video_black.png
new file mode 100644
index 000000000..5c3c48027
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/play_video_black.png differ
diff --git a/src/main/res/drawable-xxhdpi/play_video.png b/src/main/res/drawable-xxhdpi/play_video_white.png
similarity index 100%
rename from src/main/res/drawable-xxhdpi/play_video.png
rename to src/main/res/drawable-xxhdpi/play_video_white.png
diff --git a/src/main/res/drawable-xxxhdpi/date_bubble_grey.9.png b/src/main/res/drawable-xxxhdpi/date_bubble_grey.9.png
index 0232b31ed..988c84bd3 100644
Binary files a/src/main/res/drawable-xxxhdpi/date_bubble_grey.9.png and b/src/main/res/drawable-xxxhdpi/date_bubble_grey.9.png differ
diff --git a/src/main/res/drawable-xxxhdpi/date_bubble_white.9.png b/src/main/res/drawable-xxxhdpi/date_bubble_white.9.png
index bb9790601..5c8e380d8 100644
Binary files a/src/main/res/drawable-xxxhdpi/date_bubble_white.9.png and b/src/main/res/drawable-xxxhdpi/date_bubble_white.9.png differ
diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png
index 657fc59fe..63c8b041c 100644
Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png differ
diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received_dark.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received_dark.9.png
index 6cd3bd199..f7ac36723 100644
Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_received_dark.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_received_dark.9.png differ
diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received_grey.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received_grey.9.png
index 810c46a70..9980ba6c4 100644
Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_received_grey.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_received_grey.9.png differ
diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png
index 0420c44ac..7aa8cb13c 100644
Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png differ
diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received_white.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received_white.9.png
index 620f8aef5..aa7348d84 100644
Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_received_white.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_received_white.9.png differ
diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png
index 51429d3d3..3caa0af43 100644
Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png differ
diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_sent_grey.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_sent_grey.9.png
index 7ffa35811..2a8d950d8 100644
Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_sent_grey.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_sent_grey.9.png differ
diff --git a/src/main/res/drawable-xxxhdpi/play_gif_black.png b/src/main/res/drawable-xxxhdpi/play_gif_black.png
new file mode 100644
index 000000000..bec4804d5
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/play_gif_black.png differ
diff --git a/src/main/res/drawable-xxxhdpi/play_gif.png b/src/main/res/drawable-xxxhdpi/play_gif_white.png
similarity index 100%
rename from src/main/res/drawable-xxxhdpi/play_gif.png
rename to src/main/res/drawable-xxxhdpi/play_gif_white.png
diff --git a/src/main/res/drawable-xxxhdpi/play_video_black.png b/src/main/res/drawable-xxxhdpi/play_video_black.png
new file mode 100644
index 000000000..ecb0a2bd0
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/play_video_black.png differ
diff --git a/src/main/res/drawable-xxxhdpi/play_video.png b/src/main/res/drawable-xxxhdpi/play_video_white.png
similarity index 100%
rename from src/main/res/drawable-xxxhdpi/play_video.png
rename to src/main/res/drawable-xxxhdpi/play_video_white.png