From 55fcda3cee3c68b179ad37be7f54b7046dab5818 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 30 Jun 2018 17:52:34 +0200 Subject: [PATCH] use dark play/gif button as overlay when image is mostly light --- art/{play_gif.svg => play_gif_black.svg} | 2 +- art/play_gif_white.svg | 68 + art/{play_video.svg => play_video_black.svg} | 2 +- art/play_video_white.svg | 59 + art/render.rb | 6 +- .../persistance/FileBackend.java | 2154 +++++++++-------- .../res/drawable-hdpi/date_bubble_grey.9.png | Bin 657 -> 657 bytes .../res/drawable-hdpi/date_bubble_white.9.png | Bin 689 -> 689 bytes .../message_bubble_received.9.png | Bin 772 -> 772 bytes .../message_bubble_received_dark.9.png | Bin 773 -> 773 bytes .../message_bubble_received_grey.9.png | Bin 750 -> 750 bytes .../message_bubble_received_warning.9.png | Bin 776 -> 776 bytes .../message_bubble_received_white.9.png | Bin 779 -> 779 bytes .../drawable-hdpi/message_bubble_sent.9.png | Bin 687 -> 687 bytes .../message_bubble_sent_grey.9.png | Bin 707 -> 707 bytes src/main/res/drawable-hdpi/play_gif_black.png | Bin 0 -> 761 bytes .../{play_gif.png => play_gif_white.png} | Bin .../res/drawable-hdpi/play_video_black.png | Bin 0 -> 4799 bytes .../{play_video.png => play_video_white.png} | Bin .../res/drawable-mdpi/date_bubble_grey.9.png | Bin 514 -> 514 bytes .../res/drawable-mdpi/date_bubble_white.9.png | Bin 525 -> 525 bytes .../message_bubble_received.9.png | Bin 596 -> 596 bytes .../message_bubble_received_dark.9.png | Bin 617 -> 617 bytes .../message_bubble_received_grey.9.png | Bin 595 -> 595 bytes .../message_bubble_received_warning.9.png | Bin 599 -> 599 bytes .../message_bubble_received_white.9.png | Bin 610 -> 610 bytes .../drawable-mdpi/message_bubble_sent.9.png | Bin 558 -> 558 bytes .../message_bubble_sent_grey.9.png | Bin 568 -> 568 bytes src/main/res/drawable-mdpi/play_gif_black.png | Bin 0 -> 584 bytes .../{play_gif.png => play_gif_white.png} | Bin .../res/drawable-mdpi/play_video_black.png | Bin 0 -> 3204 bytes .../{play_video.png => play_video_white.png} | Bin .../res/drawable-xhdpi/date_bubble_grey.9.png | Bin 739 -> 739 bytes .../drawable-xhdpi/date_bubble_white.9.png | Bin 769 -> 769 bytes .../message_bubble_received.9.png | Bin 936 -> 936 bytes .../message_bubble_received_dark.9.png | Bin 926 -> 926 bytes .../message_bubble_received_grey.9.png | Bin 915 -> 915 bytes .../message_bubble_received_warning.9.png | Bin 916 -> 916 bytes .../message_bubble_received_white.9.png | Bin 935 -> 935 bytes .../drawable-xhdpi/message_bubble_sent.9.png | Bin 857 -> 857 bytes .../message_bubble_sent_grey.9.png | Bin 842 -> 842 bytes .../res/drawable-xhdpi/play_gif_black.png | Bin 0 -> 1052 bytes .../{play_gif.png => play_gif_white.png} | Bin .../res/drawable-xhdpi/play_video_black.png | Bin 0 -> 6572 bytes .../{play_video.png => play_video_white.png} | Bin .../drawable-xxhdpi/date_bubble_grey.9.png | Bin 1072 -> 1072 bytes .../drawable-xxhdpi/date_bubble_white.9.png | Bin 1127 -> 1127 bytes .../message_bubble_received.9.png | Bin 1319 -> 1319 bytes .../message_bubble_received_dark.9.png | Bin 1319 -> 1319 bytes .../message_bubble_received_grey.9.png | Bin 1301 -> 1301 bytes .../message_bubble_received_warning.9.png | Bin 1332 -> 1332 bytes .../message_bubble_received_white.9.png | Bin 1344 -> 1344 bytes .../drawable-xxhdpi/message_bubble_sent.9.png | Bin 1190 -> 1190 bytes .../message_bubble_sent_grey.9.png | Bin 1173 -> 1173 bytes .../res/drawable-xxhdpi/play_gif_black.png | Bin 0 -> 1580 bytes .../{play_gif.png => play_gif_white.png} | Bin .../res/drawable-xxhdpi/play_video_black.png | Bin 0 -> 10044 bytes .../{play_video.png => play_video_white.png} | Bin .../drawable-xxxhdpi/date_bubble_grey.9.png | Bin 1392 -> 1392 bytes .../drawable-xxxhdpi/date_bubble_white.9.png | Bin 1430 -> 1430 bytes .../message_bubble_received.9.png | Bin 1713 -> 1713 bytes .../message_bubble_received_dark.9.png | Bin 1691 -> 1691 bytes .../message_bubble_received_grey.9.png | Bin 1670 -> 1670 bytes .../message_bubble_received_warning.9.png | Bin 1696 -> 1696 bytes .../message_bubble_received_white.9.png | Bin 1705 -> 1705 bytes .../message_bubble_sent.9.png | Bin 1499 -> 1499 bytes .../message_bubble_sent_grey.9.png | Bin 1468 -> 1468 bytes .../res/drawable-xxxhdpi/play_gif_black.png | Bin 0 -> 2334 bytes .../{play_gif.png => play_gif_white.png} | Bin .../res/drawable-xxxhdpi/play_video_black.png | Bin 0 -> 13926 bytes .../{play_video.png => play_video_white.png} | Bin 71 files changed, 1218 insertions(+), 1073 deletions(-) rename art/{play_gif.svg => play_gif_black.svg} (97%) create mode 100644 art/play_gif_white.svg rename art/{play_video.svg => play_video_black.svg} (94%) create mode 100644 art/play_video_white.svg create mode 100644 src/main/res/drawable-hdpi/play_gif_black.png rename src/main/res/drawable-hdpi/{play_gif.png => play_gif_white.png} (100%) create mode 100644 src/main/res/drawable-hdpi/play_video_black.png rename src/main/res/drawable-hdpi/{play_video.png => play_video_white.png} (100%) create mode 100644 src/main/res/drawable-mdpi/play_gif_black.png rename src/main/res/drawable-mdpi/{play_gif.png => play_gif_white.png} (100%) create mode 100644 src/main/res/drawable-mdpi/play_video_black.png rename src/main/res/drawable-mdpi/{play_video.png => play_video_white.png} (100%) create mode 100644 src/main/res/drawable-xhdpi/play_gif_black.png rename src/main/res/drawable-xhdpi/{play_gif.png => play_gif_white.png} (100%) create mode 100644 src/main/res/drawable-xhdpi/play_video_black.png rename src/main/res/drawable-xhdpi/{play_video.png => play_video_white.png} (100%) create mode 100644 src/main/res/drawable-xxhdpi/play_gif_black.png rename src/main/res/drawable-xxhdpi/{play_gif.png => play_gif_white.png} (100%) create mode 100644 src/main/res/drawable-xxhdpi/play_video_black.png rename src/main/res/drawable-xxhdpi/{play_video.png => play_video_white.png} (100%) create mode 100644 src/main/res/drawable-xxxhdpi/play_gif_black.png rename src/main/res/drawable-xxxhdpi/{play_gif.png => play_gif_white.png} (100%) create mode 100644 src/main/res/drawable-xxxhdpi/play_video_black.png rename src/main/res/drawable-xxxhdpi/{play_video.png => play_video_white.png} (100%) 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 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + + 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 39a3c42d9210cc827239d9aa7ef95e7ccf24c0b3..1b8d675204cba3e3ccc2ed98550432349ff84c5e 100644 GIT binary patch delta 71 zcmbQpI+1lkKZl&KzEHUIM9q!Un;Auo4MGgftxU|V3@x+`jI0a{UU@3~pRCU$gDEpx LWQOzP1SUfOvi}n1 delta 71 zcmbQpI+1lkKZm5Ku%#eh=-iFdn;AuoOhXJUt$@hXT-(6N%D~{mnHxVQ>odt<$}~<) KaG9LIWC#G*)f7np 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 2d3ef050d2b52361eb61d65c3a804cf353129fb2..090af33a94ba43d0a577cfe5a968ad76267f5735 100644 GIT binary patch delta 71 zcmdnUx{-B4KZl&KzTi2b1CuvSU(6_KY!G5-Ze?O_WoWK#U}R-rAj23SHrby^22&Ry*3=FCzt`Q|E ii6yC4x%nxXX_X8{28I^824=b#8hou2ZcV<-WC#G6O&pd0 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 398787321620fce34ff774afa7c458af0bb014de..7145681839cb7e6b74b0f120911ebe76fb08c9b8 100644 GIT binary patch delta 94 zcmZo=Yh|0z&mkwQ&#|g9BY5NV?~HE71|f##Rwm|F1{T@|Mpgy}YL68X85kH;OI#yL jQW8s2t#b2IGSeytLJE&mkwQ&*f6iS-)}mFGe?GgAhY=D-&}oLj!FCBP#=go6GWMFfcHvmbgZg jq$HN4TIJ@aWTsUz7#SE^=o*;mVrW?L`r93K60{rI delta 94 zcmeBR>tLJE&mk!)Z2a-|p5~3ye=)ilnT8lzS^<%%p|*jMm4U%znYV`+7#LJbTq8t>tK&mkwQ&$Cu!jn~HMe;GxM4MGgftxU|V42`r6jI0a{{4DD?PF~3*gDGR5 La-4VaEha+%;e!+v delta 71 zcmeBX>t>tK&mk!)Y$~xsa@)q~e;GxMOhXJUt$@hXSlhtJ%D{l7`OoXgE16_4WlHb4 KgipT3WC#HHs1->7 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 2fd82ffe1e47587233cc7fffbb486d32f2e759ab..4179c9d791b3699769fcfe3a3c16f6dd4791ff41 100644 GIT binary patch delta 71 zcmZ3_x}J4HKZl&KKA*GH*|d$*7cz<(8-y5|TbY4nJ zl*xcF9gP2NHH)lm3g{2 zhE&XXd)L=5I8f%;!*GK$hEk93>|%c8ey72(Q25X$Lo{Ib?JA0lYWcKpPwY7_H)9M zH&W&X6XTd}_J^y?*NTK`rg?Zgu03U$39CEHGgVn8l#s3zidRSjyRODeqA! zgPHNS{EprG`X0a(oGoss_x)R6Jws3(tcb-SlL?{snH9s*^~n~DP;+*7o@adaO)m1x z4B>`LV3k|sm@IOEPK;m6Zs5nyIA@F8hC_3b%rRi&c&b-ZgrWPqoupUXO@geCxm+82)i literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..533437fb8f747f14cc71d70ce7e32dfa21cf0b4b GIT binary patch literal 4799 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 JCf{K+1OUGs6tMsR 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 7406ccc0eb2995983fdcd3adaed1ce3bfecbdedd..b619b017d7bdc13fa0aa69bb52c412139eba5d07 100644 GIT binary patch delta 94 zcmcb@a)o6=KZl&KKI@eeBJVa%cVl!jHV82^w=yxeGBDLPFtRc*a5r&W$iTp$TH+c} jl9E`GYL%Oxl9^V?U}Ruup=)5Ki=n|R^QY`&WhO%aHi;Tk delta 94 zcmcb@a)o6=KZm5Ku>R9q56*0y?#Ae5WEx^-ZE7 delta 71 zcmcc2a+zg9KZm5KutCP9x0V~HyE2LznT8lzS^<%Xg|>l_m4U%2y%qhFKQhW-%Iq!{ KFrKW$WC#EvzZA3p 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 272da41275fe45be9a53bf1cbbdf2505cfec5a01..3e6c5f62098aad86fb7bcfe16a767729bc65f8e1 100644 GIT binary patch delta 94 zcmcc4a-C&DKZl&KK9@^5XZ^+_hfW4G7T}bv;rbiLu~^iD+7bcGH(wtFfgc=xJHzu iB$lLF<>sekrd2W+85mmV8kp%~Xn5opS~ppZ$q)dY!W=39 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 2013c6e076f8b74d6a68cd8fd2542f15defb84ba..981dbd2cc8418a0411b0654f7cc933359d9fee8a 100644 GIT binary patch delta 70 zcmaFF@`z|F0b@| JJlT-R5CHGp6$$_V 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 eb8992e817fb0de679b05fb7a0755fafca80ee09..dc946156c7df0e6b98a9d270d891b5640e5466a4 100644 GIT binary patch delta 71 zcmZ3-vW{g!KZl&KKA*GH*|d$*6&OX04MGgftxU|V3{A8RjI0a{bYFUPPd>vagDDfU L<&*j3zl??e%wZIh delta 71 zcmZ3-vW{g!KZm5Kuvz0q51x(F6&OX0OhXJUt$@hXMBBi~%D~{D%AwTBXBcHLWj0h+ K-JJZF(GURD;1vh} 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 d1c94a7a0fca606d3ebb62b5015d9d16aff9d339..bcb340f846c76e97687b7d1a728596aa674b7b75 100644 GIT binary patch delta 94 zcmdnNvV&zpKZl&KzQ9h=53C!fYcaYR8-y5|TbY?(_1#K6FyTH+c} jl9E`GYL%Oxl9^V?U}Ruup=)5Ki=pAP_G7up>`aCL2(TI| delta 94 zcmdnNvV&zpKZm5Ku=%%b6|*)@*J5-tG7T}bv;rbiQ*8qyD+7b{;FA{_7#LJbTq8GvP=1yj3G6Vo!!5VJ> 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 0000000000000000000000000000000000000000..d0f0aae47e98cb7cf684085d118b5087a702cc82 GIT binary patch literal 584 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 L$PDMno0tp%`(qS< delta 71 zcmaFN`j~Y>KZm5KuvG^;i~7dtR~bc(OhXJUt$@hXLfgQ|%D{lFWrg76MkX0dnev+g KYbS4FG6Vp|)f1fn 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 e72b81c84a38659b35ac49410632da52d113700e..c62901af5344db0fb0c0a297911faf6d9a2435be 100644 GIT binary patch delta 71 zcmZo~#9%Dr*=XGT#Y(-1>TD5@&GB60+ec5O7d?p!88TDt` KA0}U5G6Vp^wG^BH 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 e91de712074a92103938201a5326c10473f75307..742194f17f235993008a121789520ad6867090f6 100644 GIT binary patch delta 94 zcmZ3%zJh&1KZl&KKI@eeBJVa%pT*>6Y!G5-Ze?O_WnijpU}R-r;BMl$kb!|gwZt`| jBqgyV)hahXB{Qv(!N|bSLf61d7ej+t=1R9q56*0yK8wlC$TY;z(h7)7%(V@StPBin(r%sekrd2W+85mmV8kp%~Xz;a8xHY+)*$@DiyBsP2 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 d53c545d4edb0bf4240792675cb35a5c7d842b90..d3f5f7e58939f3f0f301e54c19e572865966fff4 100644 GIT binary patch delta 71 zcmbQoK97AuKZl&KKIh%Kh@%^)Phb)?HV82^w=yxeGO*M(FtRc*XqcjXb+Qe!45rM- Mlan(i=Q0}t015XMUjP6A delta 71 zcmbQoK97AuKZm5Kuum4Sh2_Or6dHq0`ZGA?Fu KrzYnz8v+34ZxhM@ 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 a3ad4bde217ffaed97b91ab0ab71b1f3209eb729..2d4e6af33c18863c09f14225c9077f4718f86c2c 100644 GIT binary patch delta 71 zcmbQtKAC+&KZl&KKD*Vi(nA}kw=#(u8-y5|TbY!^4Vh&yWn@{T K+b1V68v+379TMjN 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 784911b93976c08c827a6bb95440f374db3266b9..50e52203223d880cc808f7ea7c837811b7117502 100644 GIT binary patch delta 94 zcmbQjK81ZkKZl&KKDTj!<;#uJ+nC&p4MGgftxU|V3=OpnjI0a{>My41FfcHvmbgZg jq$HN4TIJ@aWTsUz7#SE^=o*;mVrby&xjSKUGP5B7_-Pth delta 94 zcmbQjK81ZkKZm5Kuu1-YO@)oq+nC&pOhXJUt$@hXNZY{3%D`aJm+$cm3=FCzt`Q|E ii6yC4x%nxXX_X8{28I^824=b#8ZNK&e>^#v*$@CGyc^p9 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 29dd812cb11742d3709de6779cea14f9f424af49..50584eaac25b4b84b6821db9e3b9e371858c9d05 100644 GIT binary patch delta 94 zcmZ3^zMOqRKZl&KKJP8zId?WrpULEAY!G5-Ze?O_WoWEzU}R-r@WQWzm4SglwZt`| kBqgyV)hahXB{Qv(!N|bSLf61d7em8L;WIlYmoXax060P$LjV8( delta 94 zcmZ3^zMOqRKZm5Ku&Klj$!!~_&t!5lG7T}bv;rbiV{HQ?D+2?T=0C3)7#LJbTq8Oxv 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 2c569cd198fd0afa690c33ee332aeeea5062a316..0f0c0c579325de967a5caea009c89fbbba724c33 100644 GIT binary patch delta 94 zcmcb~c9U&FKZl&KKEH|V-wzw7do#Hi8-y5|TbY 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 2eef1578b0df425e9cae02511653917ecb2865a3..ede14e1c5ad23193b77f0431c476a323c2dcf048 100644 GIT binary patch delta 71 zcmX@bc8YC6KZl&KzQ9h=53C!f+c1e58-y5|TbY?(_1H2DRS45rL! L?ZmmtT}V`<;yx0|WC+ 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

We4 zVf9a$skO>0(n!GHCSV%-zWrPRCO_!+OB1k&a><~4sT`8@r!2LeV=?y+a7Z}|Dav&E z@FLJ@Og|~yFp=8E3I!g|uzh}KH}M0dXubtu>raNbm#Dq5cG}?n+yc%X;K0q4CfE6) zE}KaLAOC%*Pm6~-nxy*5`88{@8d=$1PUxroX?z9X3h#U94tE3dwIT4KHSL0kK6oF5 zH;GjWP>DZz*daGJeaRT!YhP-n8_+_@U|J)cCC@5tdotCJ4U+ z?%m9em~4xCt+|!m<%BJZV-WQW)R8lZ9TUUWd84O4G*1YX%+=!}Za!6c?q0=zkH;}g z0(;dup$QUMq=up9J+kwv@EO?Mm43##t1yZg$U#FT#qvAB4u6I>-k%0Sm&-gCnU~s= zBE_qmJV{r~hvrSx{r|4wZ)~g>+Q9~Ohhlbt&xJF|A9tcyxH0claUZ+0@KJYiV!E$% zZ=6Pku}Ph}lg07q#sn1q&}orvYR^*#846s*agvm#x#`&O_attmm}0hggfi7^m&RbD zYOch@ivKBT(0lwYDqeJtilyoBxj3x>e6b^NtVPu1Thr#;#up0C7sYQuCHkAv*ON`C z3sYM_^6zFE14!G|#`d)iSBP-5!Yo)B{{-!W9>n`Nu)DMly&90a(oc${bP#KN`z^o{ zLX|$e;E*ao`7VVyZz);x7YR%lh!|V$T84l&#N~h~(a`d@7vh|MOrI$Ag@77!flkw6 z^&scWh1zAwtJQX`K|g`y!lhak3}Sf<(Q18oQg1Gb> z&B$TNXS#&A&tVS+();!=$+|AG+paS206-}N?$Nc)tI`I?yq=I?pp|?s!&am&E3&tf zdW-hW0+}tpOULlcn!oIK7L_Z1m1NihD>^(=v-`;Ihh&5ZsX~(<&yPL)p4Il%jv?

cp!kDZo?@N!aWw^?rqslM755c#@ z^8k8K4+N*pA0AeI@eF<9r?D%fJh62(2wxXoZ$F7#)+xX6t`A$%$=j`csM`EU;;`yn zWt!0*Fj# z$?+H}qKVUKOPz~I=!w+?O?27}=c*8`@$~RE2y>`Cl~nLbQCH;;yp*x?$qU9^iI|P&rgVj&C&2+Sbl}(L1}` zz`}Z}Gppt;$$?;DCjQwSN-ZjukF@DTQS*rAnU>U}O40+nIhWQXmIk z-s2AD^CXPfF7GCL??foZ<5e(SBw+s#$xc$O)p9wX1}Nz zu8`vXDX+X39e{VhTpQ2h81WGW$5v*FLZ$-WW90#VWN?@L^1LYDz5)u#i9v{h{Y6H) zMiJOv_heZskTs1a0=4xmQ)QvP)F(!SzK#s5mcH`WhaV(E66z}=*_Al{e)@omJ z7!aM%c9JJshXez(^;@8CqMdHMNKQ?G2%&z%UNOBa4Kv&ek8t3JaarSo#a$7uZ` zm-;llwgC(vaWmAN?Cb~C)ev7qqYiecp_T8obSidCBv1PwMu08*RFVDi@}&$=zEYAm z&Z|1GG@0)9DGdCeNhLGIeEBu%s#~>Sr$WHsTqN}|Y%&5WNMm_}}~wJYZ9 zH0JRXv{Wq)6DJ4*<@M*GIUkCgwFikIoM4qo(+{%YY2U32D1T5!)7HeVpZXylbnaH9 zOxRdRfPn3R!%TTexGt4iZ6`;2YL_)JNfO3yY^IBl_OfQUON+4Ek`E+%OTz?n}O&O(sw1fK1kd*=kXAY#k zP}6pRj_Uc5#lNO_p&YX|XnUU7rqK-c>0YBjx;=8syo>QzP3bk*Hx9rYK07L>e)M8r zh&h}`Oy#Sr$IO#IVvV|pu41DI=Vr*){qI_JRZ^t0AR4H$M`mQxt0hXwi@Mdvmifa2?Y^GbS|V$h#IvwLSRR=IQ%AnYvek zTlX|*&+{qQ{NQGQVH=bNvGaCq2+hSV=!^Jh6*;65>;xT-?&KQrzh@YM?cBQ#{4nah zIIHq#05H(G4ypB55z>5Md%Z5YFvi%!a!ntB?AWT1@~%@2wtvdB&x1g;jkSa^t|7nG zKmH?+L{vTAkmPnUP*+mDMwv`}`bG~1#+991%H|VF6BvSB)Ohm^P8EMo`{=yrq8d0& z45`@!1E<<;P^Fyu$(XOErB(Pb%o55^LyU$4!z!b1=fb((&CEI+3U9pDzrVR6C3d%) z{1hXR$(h+f_<}1%!0Z3o(+YggWzk-BcT*rfoD731E9`@>(of;}*(Cn5By`zr*+{znaOoWXx~DvcsfP7(idH5??|=4?QihIs)Q>^i1|-nb`H$9NvvV+g8A%- zQNy5I&ijt1{|1}W4uF~Tc)dbGM5N);&1<*Wx%4U2e_LojHeBB}as(y0GY}qyk)dNq zyfUpna7*ZJ&!KO7pcBa0C^vFtZ5bfaA8aSz6Xn2Y;Hvs6-__q;eAU^SovxMBd<6IE z+WvdicMte8MQQt({Gk}3>hAaaUM524U$9sAra>7uole9TLnpKpp}94QK>IJN} zeD-n1P6`RFYQZoy#`GK_h>eQQT|W}h8U>L(mHk+DS*+qZYUat9#Vs&}D(w{xesg{D z)!W2l{c5t}x$t048{$0iBzlbm#WOtXQ+HowYrRd>_AOon*v$Re)RJruvJ5LldUpF$ kfBgR}5C3PY5FfiDdl*`6f}Cvq&lm%k7?|rf>N>{#5AXNvF8}}l literal 0 HcmV?d00001 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 7a808774417615079cc5a0aca511d4732fe47f4a..a434778b870bfe5009693707ac3c4168852c781b 100644 GIT binary patch delta 71 zcmdnMv4LYkKZl&KzOaJK!u=bkD=~{28-y5|TbY*igDGR1 LJo)?N|ICH}wjmSD delta 71 zcmdnMv4LYkKZm5KuvG^;i~7dtO3b20rXhxwRzPHGp>1GfWnjS8vO;k3Ic6D5nev+g KYbXC_HUt2W^%E!n 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 7d43e81774df63a7d636a6032de1da571b892952..9b56460406615b86dad0d8f232c3420306df2d7c 100644 GIT binary patch delta 94 zcmaFP@tk8qKZl&KzTi2b1CuvSk7RZ;HV82^w=yxeGBnpVFtRc*kYNlEV_;xVEpd$~ jNl7e8waU#;$xN$cFfuT-&^0jA#n6x&^J3LxGZsSt1wHV82^w=yxeGBDLPFtRc*a5r&W$iTp$TH+c} jl9E`GYL%Oxl9^V?U}Ruup=)5Ki=n|R^QY|OZ!CrY5C0mm delta 94 zcmZ3^wVZ20KZm5Ku>R9q56*0yF3I9%WEx^+mt=7>G7T}bv;rbiLu~^iD+7bcGH(wtFfgc=xJHzu iB$lLF<>sekrd2W+85mmV8kp%~Xn5opS~vL{iy;7X8XS87 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 5dfaf22a18bd03cbb590ba3189e09ff111fe6f6f..6ecb828869c66ad030738e73e4e6e67f2044aecd 100644 GIT binary patch delta 94 zcmbQrHI-{ZKZl&KKD*Vi(nA}kv$MDv8-y5|TbYIUG7T}bv;ra%OKk%qD+7a^$i>zS3=FCzt`Q|E ii6yC4x%nxXX_X8{28I^824=b#8f00d+b2I^F$4evTN%ax 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 ad8eae0946d5c745a9ac4559c6cb3dab783b0905..acc18615ce61212227236c52ea7c69a5de9c7676 100644 GIT binary patch delta 94 zcmdnOwS{X!KZl&KKDTj!<;#uJ)mYq&4MGgftxU|V3=OpnjI0a{>My41FfcHvmbgZg jq$HN4TIJ@aWTsUz7#SE^=o*;mVrby&xjSJpGpiv0^lcgA delta 94 zcmdnOwS{X!KZm5Kuu1-YO@)oq)mYq&OhXJUt$@hXNZY{3%D`aJm+$cm3=FCzt`Q|E ii6yC4x%nxXX_X8{28I^824=b#8ZNK&e>|C))erz8z#C-% 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 24bcaa98fb1e9d1d7c86d9c4c59fd80807def782..bfd2b9319dd4d9f10253b5ac71ea65ae4935be15 100644 GIT binary patch delta 71 zcmX@Wb%1L^KZl&KKJP8zId?WrH)0VrHV82^w=yxeGBnmUFtRc*c;Q#VI{6-p45rLX M;WIlY^RpTP00$8ikpKVy delta 71 zcmX@Wb%1L^KZm5Ku&Klj$!!~_8?lHQnT8lzS^<%%v9^Jcm4N|E^Pks~@3F{W%9P%7 K37^c*Y6t-GcNAg( 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 5c61e28d524fd8735dce2018d53e2fbc6c326b39..5fde2041a8c82bde9849267ed98ad8ada463c80d 100644 GIT binary patch delta 94 zcmZ3+xr}o{KZl&KKEH|V-wzw7&tP^lHV82^w=yxeGBnjTFtRc*=$_|j&%nT-TH+c} jl9E`GYL%Oxl9^V?U}Ruup=)5Ki=pAy;h)WuOIZv7GI<;( delta 94 zcmZ3+xr}o{KZm5Kuvz0q51x(FXE3`NnT8lzS^<%%iMD}}m4U%Ql|!iv3=FCzt`Q|E ii6yC4x%nxXX_X8{28I^824=b#8a7l`-JD#?Vh8{a4I3-~ 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 960758ae30e81c69a62e5c3e0188fbfa2b13b804..8a30c3a083fc763db0c00ef40ed57769ec82a90f 100644 GIT binary patch delta 94 zcmbQrIhAulKZl&KzQ9h=53C!fw==sL8-y5|TbY?(_1#K6FyTH+c} jl9E`GYL%Oxl9^V?U}Ruup=)5Ki=pAP_G7upDJ+Hn5QZA8 delta 94 zcmbQrIhAulKZm5Ku=%%b6|*)@Z)bKhG7T}bv;rbiQ*8qyD+7b{;FA{_7#LJbTq8GvP=1xvwF$4f*mm2W^ 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 0000000000000000000000000000000000000000..f49471c5d8525e9be8761bf9a2f6f23cc4ad4298 GIT binary patch literal 1580 zcmeAS@N?(olHy`uVBq!ia0y~yU~B+k4mP03lH*(V11XkbC(jTLAgJL;=>YOM3p^r= z85rJJfG}gB_9-BPL9)a(q9iy!t)x7$D3zhSyj(9cFS|H7u^?41zbJk7I~yqm2G*6H zE{-7;bKc(FyMA7&?D3D=bDLcq7OORIs<<7v;+o{c)5)13{-4d9U82Xj-X+{aV#bV) z)qZ*k{LI=qW`-{897o#J>|9nY?LGYV!ViYb_rE{vUjMmv@1%9h=l?0H+I#+PaqLcz zSB4*Gxcr8DH9JpHdFjHcEx+8pUVT+lTK04W^NzsS)$79KL*9o?U)>+xx9jLq;eWr* z&--xQ{^j53@=MR_SIv#KO=$SNy8CLm_1x#R+jc#_UoW?=E}%AOZ|vTn*hq8UA12E` zHkIhSTU8hIcZ1dDiqp^E#(e$zs`~2etNE*5dy6yvcV+N9c765yP=1|z8*j~gb@J8i ztKL`Lv!63;pY?6>%jXaOy?Va$$Ng6aUx~l1`_3rC$!M{rdUg6L{q=H1E$`OOJ9qQI zMS+IHmcf6w{5thY(f!T7vh*jv`T82_JKBpkR^@!{;H_Bl>*FiI1_qf^(^oAI>%TE! z`K`H<8zwNw9P3}DzfNwW;&Wl22tS5z<=d)~fXb^Dy|riMovp_3`*+yx`58NH%fs)- z*fOaJH~fu_uuV9;Z*vuc4^Xx`<7)$NaT(B{H|Lvu%irz0%A8=tcw>I|jk%vM$mpmr z{N9}nQ^CNa%D~{v$#5WmiNRnB1A-gJRsUyRtZhT^+3(jA>=+ReXzK8D@3Av%j*a^B zy;}aD6COk6F+bQm=S%IQcg*dsHB1Z=UJMLvLJSQHSr`&D7>VWX;b%zuY}a-Wj{!F9 z1%LF`$}!CG!{ZpH4g2fY$!$pdA7RS?3fsS_u)unA-u2BnOX~;^hHu~hy<(L9cJCFV z!AyoX@5@)S=iGP-3FiCqtFl+S=Ug~`RX)7G;Ur5!z4@x`z{p*&{nlLB4IRM9%`f(T z{B}p!-fdMZKI#nZAHT<3$UC0>_8bq;V7}*5UwsE^Kd}4i>ecQHOa&h1tEaEF2d1eb z|8BkO$y+DK#BpHZ-?deS{1$)p&$0FOFv#4Sze;=+e>lITl6=_t=ROZsa2z=KV@1`v zs>L_|-!0r+wfAmhOza-kAN95qUt6w>N&G?$|bt@>h-QCi>FPw|Pj W0)iBUKomr(AcRgLh@c2cmm;8m(j<|hQlp}% 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 literal 0 HcmV?d00001 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 0232b31edc6a01a07ae9613c780989375ff845c9..988c84bd3469b4570bc6cfecaa5553c527d21275 100644 GIT binary patch delta 71 zcmeys^?_?bKZl&KzOaJK!u=bkC$Wec8-y5|TbY1GfWnjS8vO;h&AFB+eO!-ZL JwUg~x4FRh95^(?k 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 bb9790601c6b435d2a9386f004b0e7ff723694b1..5c8e380d80066b49890bbc523d7c4e3cbcb176d2 100644 GIT binary patch delta 94 zcmbQnJ&k)pKZl&KzEHUIM9q!UJ6PO|4MGgftxU|V3@x+`jI0a{UU@3~XJBAZEpd$~ jNl7e8waU#;$xN$cFfuT-&^0jA#n3QYWQOzPR8~U(=zAI6 delta 94 zcmbQnJ&k)pKZm5Ku%#eh=-iFdJ6PO|OhXJUt$@hXT-(6N%D~{mnHxVC7#LJbTq8JH 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 6cd3bd1997da9c496ff46935cc9855c0b64a3346..f7ac3672324211f1c06e8fe13c06a8cd9eefe839 100644 GIT binary patch delta 71 zcmbQuJDYbxKZl&KK9@^5XZ^+?`0J=G7T}bv;rbiLu~^iD+7bcGH(w}wq%pRlzHSB KS~oe1%@6=Ir4^9? 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 810c46a7053b21c804b890e339bf8d2ed05f61e7..9980ba6c4a019888af968ec9cf597d779bf97d79 100644 GIT binary patch delta 94 zcmZqUZR4HL&mkwQ&u(?B^w7rX6|8Q?1|f##Rwm|F2IkrZMpgy}7gh&!FfcHvmbgZg jq$HN4TIJ@aWTsUz7#SE^=o*;mVrW?S?S;wYP&PvVGL9R` delta 94 zcmZqUZR4HL&mk!)Y$$*A?)r_>D_Gr(OhXJUt$@hHQrp1D%D^BeaKZl&KKF?Z_HC`L1Phu4{HV82^w=yxeGBnaQFtRc*@UyJnIN6R(22;j9 L&L0yP delta 71 zcmZ3$yMT8>KZm5Kuu1-YO@)oqC$WkenT8lzS^<%%k+y-6m4U&cFW=)Q+p)=D%3NOQ K|9Emfn;`)7eidH; 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 620f8aef51244d7c9eb17d88cd19cafd2f3b15a2..aa7348d8440adb0e3eece530d84544a1287d302a 100644 GIT binary patch delta 94 zcmZ3 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 51429d3d32372344e65396eea924d42a2e13fb82..3caa0af433114529abfd4d59d200e5e3146b0fc4 100644 GIT binary patch delta 71 zcmcc3eVcniKZl&KKEH|V-wzw7pJfp>HV82^w=yxeGBnjTFtRc*=$_|jKe>`s22TH;#L#b delta 71 zcmdnPy@z{3KZm5KutgeYD)+|e>sUmMOhXJUt$@hXOxwW7%D^CO_hp~SQLHkUGV0H= KKTPgsH3R_9b`(|s 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 0000000000000000000000000000000000000000..bec4804d5ca599f580d074e15852507794bb6233 GIT binary patch literal 2334 zcmeHHZA_C_6h19Qip&8rzmN}|3fnYTgKMGC5|q!W$h5Rf`6xrikcCzXR$hd@U|feX zYG#zOf&@t%l$4ecl~PK(_<<;;cG|&^8NMu76)m(VFlBF-EX)2(e=PCOPI7afbMLt~ z_dMr0w=6y`ddXtX#Q?A*HYQ>Z03L&QATGit>yf*2*hJuMi)9coJ4!t84dy#@V?N^n zjyhu>IBE-viY>0ni%iVh%lRUYpO%{ed_F%QD<_+mp2p1x;N)gXCPO^|h?3ZdaK@49 zC%|(o9XI?N9b>dlVJAj~4oDR?i$? zL|>Q?ESRHw1&8ytBCx=E&hl;S9ZO8TRc5Z(C@PdXNkuFZa!Xe>mw0MfTk zzBe^Au@>Z&Aik%P>P_`W*Ht&rD=GW%r3>q(FR==8WQb_YfHhiQ);t# zfxg#~9kz#tb4Z zc(%{xa=S;vj^; znhlqvJCGc4q#sgEa$V8XPTM1U_Xt7U=b;?#p&x0{wSf2p=4zxI)5*-Jc1Dp3$-)}; z(FQQh2+b1llozcKAiQ?V|aiVf(vxvyHo(x(1REoOl2$npab$^y$Tdy14`S$m8l`7L_^L z$Z>iKL1g?8sfNRg9qKe+F>W_@*PS%+8g=9IwKrYDxF|uJ79x0HiLjj`hjTgkeA@-{ zjQMKAp~8_|Tdqw{zMNRxtXufC)!5{~$BOm8y)_1&QGa)>8!Nd%d2z+h{)pJfxQOO$ H?9x8~xchhT literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ecb0a2bd019a25f39dc4c7d574091fe4321cb2e9 GIT binary patch literal 13926 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 L=6cW1x!?SMR-t;o literal 0 HcmV?d00001 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