package eu.siacs.conversations.persistance; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.RectF; import android.net.Uri; import android.os.Environment; import android.util.Base64; import android.util.Base64OutputStream; import android.util.Log; import android.webkit.MimeTypeMap; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.net.URL; 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.Locale; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Environment; import android.webkit.MimeTypeMap; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import de.thedevstack.conversationsplus.exceptions.FileCopyException; import de.thedevstack.conversationsplus.utils.StreamUtil; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.ExifHelper; import eu.siacs.conversations.utils.FileUtils; import eu.siacs.conversations.xmpp.pep.Avatar; public class FileBackend { private final SimpleDateFormat imageDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); private XmppConnectionService mXmppConnectionService; public FileBackend(XmppConnectionService service) { this.mXmppConnectionService = service; } public DownloadableFile getFile(Message message) { return getFile(message, true); } public DownloadableFile getFile(Message message, boolean decrypted) { final boolean encrypted = !decrypted && (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED); final DownloadableFile file; String path = message.getRelativeFilePath(); if (path == null) { path = message.getUuid(); } if (path.startsWith("/")) { file = new DownloadableFile(path); } else { String mime = message.getMimeType(); if (mime != null && mime.startsWith("image")) { file = new DownloadableFile(getConversationsImageDirectory() + path); } else { file = new DownloadableFile(getConversationsFileDirectory() + path); } } if (encrypted) { return new DownloadableFile(getConversationsFileDirectory() + file.getName() + ".pgp"); } else { return file; } } public static String getConversationsFileDirectory() { return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + ConversationsPlusPreferences.fileTransferFolder() + File.separator; } public static String getConversationsImageDirectory() { return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + File.separator + ConversationsPlusPreferences.imgTransferFolder() + File.separator; } private static String getPrivateFileDirectoryPath() { return ConversationsPlusApplication.getPrivateFilesDir().getAbsolutePath(); } private static String getPrivateImageDirectoryPath() { return FileBackend.getPrivateFileDirectoryPath() + File.separator + "Images" + File.separator; } public boolean useImageAsIs(Uri uri) { String path = getOriginalPath(uri); if (path == null) { return false; } File file = new File(path); long size = file.length(); if (size == 0 || size >= Config.IMAGE_MAX_SIZE ) { return false; } BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; try { BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri), null, options); if (options == null || 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); } public void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { file.getParentFile().mkdirs(); OutputStream os = null; InputStream is = null; try { file.createNewFile(); os = new FileOutputStream(file); is = StreamUtil.openInputStreamFromContentResolver(uri); byte[] buffer = new byte[1024]; int length; while ((length = is.read(buffer)) > 0) { os.write(buffer, 0, length); } os.flush(); } 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); } finally { StreamUtil.close(os); StreamUtil.close(is); } Logging.d(Config.LOGTAG, "output file name " + file); return file; } public void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage"); String mime = mXmppConnectionService.getContentResolver().getType(uri); String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); message.setRelativeFilePath(message.getUuid() + "." + extension); copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri); } private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException { file.getParentFile().mkdirs(); InputStream is = null; OutputStream os = null; try { file.createNewFile(); is = mXmppConnectionService.getContentResolver().openInputStream(image); 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; 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() <= Config.IMAGE_MAX_SIZE || quality <= 50; quality -= 5; } scaledBitmap.recycle(); return; } 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); } } catch (NullPointerException e) { throw new FileCopyException(R.string.error_io_exception); } finally { StreamUtil.close(os); StreamUtil.close(is); } } public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException { 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); } public static Uri getTakePhotoUri() { StringBuilder pathBuilder = new StringBuilder(); pathBuilder.append(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)); pathBuilder.append('/'); pathBuilder.append("Camera"); pathBuilder.append('/'); pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date()) + ".jpg"); Uri uri = Uri.parse("file://" + pathBuilder.toString()); File file = new File(uri.toString()); file.getParentFile().mkdirs(); return uri; } public static Uri getJingleFileUri(Message message) { File file = getFile(message); return Uri.parse("file://" + file.getAbsolutePath()); } public static boolean isFileAvailable(Message message) { return getFile(message).exists(); } private FileBackend() { // Static helper class } }