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 39a3c42d9..1b8d67520 100644 Binary files a/src/main/res/drawable-hdpi/date_bubble_grey.9.png and b/src/main/res/drawable-hdpi/date_bubble_grey.9.png differ diff --git a/src/main/res/drawable-hdpi/date_bubble_white.9.png b/src/main/res/drawable-hdpi/date_bubble_white.9.png index 2d3ef050d..090af33a9 100644 Binary files a/src/main/res/drawable-hdpi/date_bubble_white.9.png and b/src/main/res/drawable-hdpi/date_bubble_white.9.png differ diff --git a/src/main/res/drawable-hdpi/message_bubble_received.9.png b/src/main/res/drawable-hdpi/message_bubble_received.9.png index 7216e4ac1..4e4329f91 100644 Binary files a/src/main/res/drawable-hdpi/message_bubble_received.9.png and b/src/main/res/drawable-hdpi/message_bubble_received.9.png differ diff --git a/src/main/res/drawable-hdpi/message_bubble_received_dark.9.png b/src/main/res/drawable-hdpi/message_bubble_received_dark.9.png index 398787321..714568183 100644 Binary files a/src/main/res/drawable-hdpi/message_bubble_received_dark.9.png and b/src/main/res/drawable-hdpi/message_bubble_received_dark.9.png differ diff --git a/src/main/res/drawable-hdpi/message_bubble_received_grey.9.png b/src/main/res/drawable-hdpi/message_bubble_received_grey.9.png index a1b190287..f54f43235 100644 Binary files a/src/main/res/drawable-hdpi/message_bubble_received_grey.9.png and b/src/main/res/drawable-hdpi/message_bubble_received_grey.9.png differ diff --git a/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png index eefb7ef04..69d08da2b 100644 Binary files a/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png and b/src/main/res/drawable-hdpi/message_bubble_received_warning.9.png differ diff --git a/src/main/res/drawable-hdpi/message_bubble_received_white.9.png b/src/main/res/drawable-hdpi/message_bubble_received_white.9.png index 1759963ca..4c1196b9f 100644 Binary files a/src/main/res/drawable-hdpi/message_bubble_received_white.9.png and b/src/main/res/drawable-hdpi/message_bubble_received_white.9.png differ diff --git a/src/main/res/drawable-hdpi/message_bubble_sent.9.png b/src/main/res/drawable-hdpi/message_bubble_sent.9.png index 2fd82ffe1..4179c9d79 100644 Binary files a/src/main/res/drawable-hdpi/message_bubble_sent.9.png and b/src/main/res/drawable-hdpi/message_bubble_sent.9.png differ diff --git a/src/main/res/drawable-hdpi/message_bubble_sent_grey.9.png b/src/main/res/drawable-hdpi/message_bubble_sent_grey.9.png index 1633b2fa6..d9450969d 100644 Binary files a/src/main/res/drawable-hdpi/message_bubble_sent_grey.9.png and b/src/main/res/drawable-hdpi/message_bubble_sent_grey.9.png differ diff --git a/src/main/res/drawable-hdpi/play_gif_black.png b/src/main/res/drawable-hdpi/play_gif_black.png new file mode 100644 index 000000000..054018ad3 Binary files /dev/null and b/src/main/res/drawable-hdpi/play_gif_black.png differ diff --git a/src/main/res/drawable-hdpi/play_gif.png b/src/main/res/drawable-hdpi/play_gif_white.png similarity index 100% rename from src/main/res/drawable-hdpi/play_gif.png rename to src/main/res/drawable-hdpi/play_gif_white.png diff --git a/src/main/res/drawable-hdpi/play_video_black.png b/src/main/res/drawable-hdpi/play_video_black.png new file mode 100644 index 000000000..533437fb8 Binary files /dev/null and b/src/main/res/drawable-hdpi/play_video_black.png differ diff --git a/src/main/res/drawable-hdpi/play_video.png b/src/main/res/drawable-hdpi/play_video_white.png similarity index 100% rename from src/main/res/drawable-hdpi/play_video.png rename to src/main/res/drawable-hdpi/play_video_white.png diff --git a/src/main/res/drawable-mdpi/date_bubble_grey.9.png b/src/main/res/drawable-mdpi/date_bubble_grey.9.png index bb12d5d0b..eb36a1c03 100644 Binary files a/src/main/res/drawable-mdpi/date_bubble_grey.9.png and b/src/main/res/drawable-mdpi/date_bubble_grey.9.png differ diff --git a/src/main/res/drawable-mdpi/date_bubble_white.9.png b/src/main/res/drawable-mdpi/date_bubble_white.9.png index af3c2d491..71d7293b0 100644 Binary files a/src/main/res/drawable-mdpi/date_bubble_white.9.png and b/src/main/res/drawable-mdpi/date_bubble_white.9.png differ diff --git a/src/main/res/drawable-mdpi/message_bubble_received.9.png b/src/main/res/drawable-mdpi/message_bubble_received.9.png index 7406ccc0e..b619b017d 100644 Binary files a/src/main/res/drawable-mdpi/message_bubble_received.9.png and b/src/main/res/drawable-mdpi/message_bubble_received.9.png differ diff --git a/src/main/res/drawable-mdpi/message_bubble_received_dark.9.png b/src/main/res/drawable-mdpi/message_bubble_received_dark.9.png index 387925323..4b2f47c9f 100644 Binary files a/src/main/res/drawable-mdpi/message_bubble_received_dark.9.png and b/src/main/res/drawable-mdpi/message_bubble_received_dark.9.png differ diff --git a/src/main/res/drawable-mdpi/message_bubble_received_grey.9.png b/src/main/res/drawable-mdpi/message_bubble_received_grey.9.png index 9fea89221..1c1f98c0d 100644 Binary files a/src/main/res/drawable-mdpi/message_bubble_received_grey.9.png and b/src/main/res/drawable-mdpi/message_bubble_received_grey.9.png differ diff --git a/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png index 272da4127..3e6c5f620 100644 Binary files a/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png and b/src/main/res/drawable-mdpi/message_bubble_received_warning.9.png differ diff --git a/src/main/res/drawable-mdpi/message_bubble_received_white.9.png b/src/main/res/drawable-mdpi/message_bubble_received_white.9.png index 2013c6e07..981dbd2cc 100644 Binary files a/src/main/res/drawable-mdpi/message_bubble_received_white.9.png and b/src/main/res/drawable-mdpi/message_bubble_received_white.9.png differ diff --git a/src/main/res/drawable-mdpi/message_bubble_sent.9.png b/src/main/res/drawable-mdpi/message_bubble_sent.9.png index eb8992e81..dc946156c 100644 Binary files a/src/main/res/drawable-mdpi/message_bubble_sent.9.png and b/src/main/res/drawable-mdpi/message_bubble_sent.9.png differ diff --git a/src/main/res/drawable-mdpi/message_bubble_sent_grey.9.png b/src/main/res/drawable-mdpi/message_bubble_sent_grey.9.png index d1c94a7a0..bcb340f84 100644 Binary files a/src/main/res/drawable-mdpi/message_bubble_sent_grey.9.png and b/src/main/res/drawable-mdpi/message_bubble_sent_grey.9.png differ diff --git a/src/main/res/drawable-mdpi/play_gif_black.png b/src/main/res/drawable-mdpi/play_gif_black.png new file mode 100644 index 000000000..d0f0aae47 Binary files /dev/null and b/src/main/res/drawable-mdpi/play_gif_black.png differ diff --git a/src/main/res/drawable-mdpi/play_gif.png b/src/main/res/drawable-mdpi/play_gif_white.png similarity index 100% rename from src/main/res/drawable-mdpi/play_gif.png rename to src/main/res/drawable-mdpi/play_gif_white.png diff --git a/src/main/res/drawable-mdpi/play_video_black.png b/src/main/res/drawable-mdpi/play_video_black.png new file mode 100644 index 000000000..6c25cda8e Binary files /dev/null and b/src/main/res/drawable-mdpi/play_video_black.png differ diff --git a/src/main/res/drawable-mdpi/play_video.png b/src/main/res/drawable-mdpi/play_video_white.png similarity index 100% rename from src/main/res/drawable-mdpi/play_video.png rename to src/main/res/drawable-mdpi/play_video_white.png diff --git a/src/main/res/drawable-xhdpi/date_bubble_grey.9.png b/src/main/res/drawable-xhdpi/date_bubble_grey.9.png index d86b8c68c..f55428d98 100644 Binary files a/src/main/res/drawable-xhdpi/date_bubble_grey.9.png and b/src/main/res/drawable-xhdpi/date_bubble_grey.9.png differ diff --git a/src/main/res/drawable-xhdpi/date_bubble_white.9.png b/src/main/res/drawable-xhdpi/date_bubble_white.9.png index e72b81c84..c62901af5 100644 Binary files a/src/main/res/drawable-xhdpi/date_bubble_white.9.png and b/src/main/res/drawable-xhdpi/date_bubble_white.9.png differ diff --git a/src/main/res/drawable-xhdpi/message_bubble_received.9.png b/src/main/res/drawable-xhdpi/message_bubble_received.9.png index e91de7120..742194f17 100644 Binary files a/src/main/res/drawable-xhdpi/message_bubble_received.9.png and b/src/main/res/drawable-xhdpi/message_bubble_received.9.png differ diff --git a/src/main/res/drawable-xhdpi/message_bubble_received_dark.9.png b/src/main/res/drawable-xhdpi/message_bubble_received_dark.9.png index d53c545d4..d3f5f7e58 100644 Binary files a/src/main/res/drawable-xhdpi/message_bubble_received_dark.9.png and b/src/main/res/drawable-xhdpi/message_bubble_received_dark.9.png differ diff --git a/src/main/res/drawable-xhdpi/message_bubble_received_grey.9.png b/src/main/res/drawable-xhdpi/message_bubble_received_grey.9.png index a3ad4bde2..2d4e6af33 100644 Binary files a/src/main/res/drawable-xhdpi/message_bubble_received_grey.9.png and b/src/main/res/drawable-xhdpi/message_bubble_received_grey.9.png differ diff --git a/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png index 784911b93..50e522032 100644 Binary files a/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png and b/src/main/res/drawable-xhdpi/message_bubble_received_warning.9.png differ diff --git a/src/main/res/drawable-xhdpi/message_bubble_received_white.9.png b/src/main/res/drawable-xhdpi/message_bubble_received_white.9.png index 29dd812cb..50584eaac 100644 Binary files a/src/main/res/drawable-xhdpi/message_bubble_received_white.9.png and b/src/main/res/drawable-xhdpi/message_bubble_received_white.9.png differ diff --git a/src/main/res/drawable-xhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xhdpi/message_bubble_sent.9.png index 2c569cd19..0f0c0c579 100644 Binary files a/src/main/res/drawable-xhdpi/message_bubble_sent.9.png and b/src/main/res/drawable-xhdpi/message_bubble_sent.9.png differ diff --git a/src/main/res/drawable-xhdpi/message_bubble_sent_grey.9.png b/src/main/res/drawable-xhdpi/message_bubble_sent_grey.9.png index 2eef1578b..ede14e1c5 100644 Binary files a/src/main/res/drawable-xhdpi/message_bubble_sent_grey.9.png and b/src/main/res/drawable-xhdpi/message_bubble_sent_grey.9.png differ diff --git a/src/main/res/drawable-xhdpi/play_gif_black.png b/src/main/res/drawable-xhdpi/play_gif_black.png new file mode 100644 index 000000000..e621c14b8 Binary files /dev/null and b/src/main/res/drawable-xhdpi/play_gif_black.png differ diff --git a/src/main/res/drawable-xhdpi/play_gif.png b/src/main/res/drawable-xhdpi/play_gif_white.png similarity index 100% rename from src/main/res/drawable-xhdpi/play_gif.png rename to src/main/res/drawable-xhdpi/play_gif_white.png diff --git a/src/main/res/drawable-xhdpi/play_video_black.png b/src/main/res/drawable-xhdpi/play_video_black.png new file mode 100644 index 000000000..79a4d382d Binary files /dev/null and b/src/main/res/drawable-xhdpi/play_video_black.png differ diff --git a/src/main/res/drawable-xhdpi/play_video.png b/src/main/res/drawable-xhdpi/play_video_white.png similarity index 100% rename from src/main/res/drawable-xhdpi/play_video.png rename to src/main/res/drawable-xhdpi/play_video_white.png diff --git a/src/main/res/drawable-xxhdpi/date_bubble_grey.9.png b/src/main/res/drawable-xxhdpi/date_bubble_grey.9.png index 7a8087744..a434778b8 100644 Binary files a/src/main/res/drawable-xxhdpi/date_bubble_grey.9.png and b/src/main/res/drawable-xxhdpi/date_bubble_grey.9.png differ diff --git a/src/main/res/drawable-xxhdpi/date_bubble_white.9.png b/src/main/res/drawable-xxhdpi/date_bubble_white.9.png index 7d43e8177..9b5646040 100644 Binary files a/src/main/res/drawable-xxhdpi/date_bubble_white.9.png and b/src/main/res/drawable-xxhdpi/date_bubble_white.9.png differ diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received.9.png index 0b192f10d..fda98dcec 100644 Binary files a/src/main/res/drawable-xxhdpi/message_bubble_received.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_received.9.png differ diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received_dark.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received_dark.9.png index 83da1c8cc..5a88c0d91 100644 Binary files a/src/main/res/drawable-xxhdpi/message_bubble_received_dark.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_received_dark.9.png differ diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received_grey.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received_grey.9.png index 5dfaf22a1..6ecb82886 100644 Binary files a/src/main/res/drawable-xxhdpi/message_bubble_received_grey.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_received_grey.9.png differ diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png index ad8eae094..acc18615c 100644 Binary files a/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_received_warning.9.png differ diff --git a/src/main/res/drawable-xxhdpi/message_bubble_received_white.9.png b/src/main/res/drawable-xxhdpi/message_bubble_received_white.9.png index 24bcaa98f..bfd2b9319 100644 Binary files a/src/main/res/drawable-xxhdpi/message_bubble_received_white.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_received_white.9.png differ diff --git a/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png index 5c61e28d5..5fde2041a 100644 Binary files a/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_sent.9.png differ diff --git a/src/main/res/drawable-xxhdpi/message_bubble_sent_grey.9.png b/src/main/res/drawable-xxhdpi/message_bubble_sent_grey.9.png index 960758ae3..8a30c3a08 100644 Binary files a/src/main/res/drawable-xxhdpi/message_bubble_sent_grey.9.png and b/src/main/res/drawable-xxhdpi/message_bubble_sent_grey.9.png differ diff --git a/src/main/res/drawable-xxhdpi/play_gif_black.png b/src/main/res/drawable-xxhdpi/play_gif_black.png new file mode 100644 index 000000000..f49471c5d Binary files /dev/null and b/src/main/res/drawable-xxhdpi/play_gif_black.png differ diff --git a/src/main/res/drawable-xxhdpi/play_gif.png b/src/main/res/drawable-xxhdpi/play_gif_white.png similarity index 100% rename from src/main/res/drawable-xxhdpi/play_gif.png rename to src/main/res/drawable-xxhdpi/play_gif_white.png diff --git a/src/main/res/drawable-xxhdpi/play_video_black.png b/src/main/res/drawable-xxhdpi/play_video_black.png new file mode 100644 index 000000000..5c3c48027 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/play_video_black.png differ diff --git a/src/main/res/drawable-xxhdpi/play_video.png b/src/main/res/drawable-xxhdpi/play_video_white.png similarity index 100% rename from src/main/res/drawable-xxhdpi/play_video.png rename to src/main/res/drawable-xxhdpi/play_video_white.png diff --git a/src/main/res/drawable-xxxhdpi/date_bubble_grey.9.png b/src/main/res/drawable-xxxhdpi/date_bubble_grey.9.png index 0232b31ed..988c84bd3 100644 Binary files a/src/main/res/drawable-xxxhdpi/date_bubble_grey.9.png and b/src/main/res/drawable-xxxhdpi/date_bubble_grey.9.png differ diff --git a/src/main/res/drawable-xxxhdpi/date_bubble_white.9.png b/src/main/res/drawable-xxxhdpi/date_bubble_white.9.png index bb9790601..5c8e380d8 100644 Binary files a/src/main/res/drawable-xxxhdpi/date_bubble_white.9.png and b/src/main/res/drawable-xxxhdpi/date_bubble_white.9.png differ diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png index 657fc59fe..63c8b041c 100644 Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_received.9.png differ diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received_dark.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received_dark.9.png index 6cd3bd199..f7ac36723 100644 Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_received_dark.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_received_dark.9.png differ diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received_grey.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received_grey.9.png index 810c46a70..9980ba6c4 100644 Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_received_grey.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_received_grey.9.png differ diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png index 0420c44ac..7aa8cb13c 100644 Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_received_warning.9.png differ diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_received_white.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_received_white.9.png index 620f8aef5..aa7348d84 100644 Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_received_white.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_received_white.9.png differ diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png index 51429d3d3..3caa0af43 100644 Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_sent.9.png differ diff --git a/src/main/res/drawable-xxxhdpi/message_bubble_sent_grey.9.png b/src/main/res/drawable-xxxhdpi/message_bubble_sent_grey.9.png index 7ffa35811..2a8d950d8 100644 Binary files a/src/main/res/drawable-xxxhdpi/message_bubble_sent_grey.9.png and b/src/main/res/drawable-xxxhdpi/message_bubble_sent_grey.9.png differ diff --git a/src/main/res/drawable-xxxhdpi/play_gif_black.png b/src/main/res/drawable-xxxhdpi/play_gif_black.png new file mode 100644 index 000000000..bec4804d5 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/play_gif_black.png differ diff --git a/src/main/res/drawable-xxxhdpi/play_gif.png b/src/main/res/drawable-xxxhdpi/play_gif_white.png similarity index 100% rename from src/main/res/drawable-xxxhdpi/play_gif.png rename to src/main/res/drawable-xxxhdpi/play_gif_white.png diff --git a/src/main/res/drawable-xxxhdpi/play_video_black.png b/src/main/res/drawable-xxxhdpi/play_video_black.png new file mode 100644 index 000000000..ecb0a2bd0 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/play_video_black.png differ diff --git a/src/main/res/drawable-xxxhdpi/play_video.png b/src/main/res/drawable-xxxhdpi/play_video_white.png similarity index 100% rename from src/main/res/drawable-xxxhdpi/play_video.png rename to src/main/res/drawable-xxxhdpi/play_video_white.png