1package eu.siacs.conversations.persistance;
2
3import android.graphics.Bitmap;
4import android.graphics.BitmapFactory;
5import android.graphics.Canvas;
6import android.graphics.Matrix;
7import android.graphics.RectF;
8import android.net.Uri;
9import android.os.Environment;
10import android.util.Base64;
11import android.util.Base64OutputStream;
12import android.util.Log;
13import android.webkit.MimeTypeMap;
14
15import java.io.ByteArrayOutputStream;
16import java.io.Closeable;
17import java.io.File;
18import java.io.FileNotFoundException;
19import java.io.FileOutputStream;
20import java.io.IOException;
21import java.io.InputStream;
22import java.io.OutputStream;
23import java.net.Socket;
24import java.net.URL;
25import java.security.DigestOutputStream;
26import java.security.MessageDigest;
27import java.security.NoSuchAlgorithmException;
28import java.text.SimpleDateFormat;
29import java.util.Arrays;
30import java.util.Date;
31import java.util.Locale;
32
33import eu.siacs.conversations.Config;
34import eu.siacs.conversations.R;
35import eu.siacs.conversations.entities.DownloadableFile;
36import eu.siacs.conversations.entities.Message;
37import eu.siacs.conversations.entities.Transferable;
38import eu.siacs.conversations.services.XmppConnectionService;
39import eu.siacs.conversations.utils.CryptoHelper;
40import eu.siacs.conversations.utils.ExifHelper;
41import eu.siacs.conversations.utils.FileUtils;
42import eu.siacs.conversations.xmpp.pep.Avatar;
43
44public class FileBackend {
45
46 private static int IMAGE_SIZE = 1920;
47
48 private final SimpleDateFormat imageDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
49
50 private XmppConnectionService mXmppConnectionService;
51
52 public FileBackend(XmppConnectionService service) {
53 this.mXmppConnectionService = service;
54 }
55
56 public DownloadableFile getFile(Message message) {
57 return getFile(message, true);
58 }
59
60 public DownloadableFile getFile(Message message, boolean decrypted) {
61 String path = message.getRelativeFilePath();
62 String extension;
63 if (path != null && !path.isEmpty()) {
64 String[] parts = path.split("\\.");
65 extension = "."+parts[parts.length - 1];
66 } else {
67 if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_TEXT) {
68 extension = ".webp";
69 } else {
70 extension = "";
71 }
72 path = message.getUuid()+extension;
73 }
74 final boolean encrypted = !decrypted
75 && (message.getEncryption() == Message.ENCRYPTION_PGP
76 || message.getEncryption() == Message.ENCRYPTION_DECRYPTED);
77 if (encrypted) {
78 return new DownloadableFile(getConversationsFileDirectory()+message.getUuid()+extension+".pgp");
79 } else {
80 if (path.startsWith("/")) {
81 return new DownloadableFile(path);
82 } else {
83 if (Arrays.asList(Transferable.VALID_IMAGE_EXTENSIONS).contains(extension)) {
84 return new DownloadableFile(getConversationsFileDirectory() + path);
85 } else {
86 return new DownloadableFile(getConversationsImageDirectory() + path);
87 }
88 }
89 }
90 }
91
92 public static String getConversationsFileDirectory() {
93 return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/";
94 }
95
96 public static String getConversationsImageDirectory() {
97 return Environment.getExternalStoragePublicDirectory(
98 Environment.DIRECTORY_PICTURES).getAbsolutePath()
99 + "/Conversations/";
100 }
101
102 public Bitmap resize(Bitmap originalBitmap, int size) {
103 int w = originalBitmap.getWidth();
104 int h = originalBitmap.getHeight();
105 if (Math.max(w, h) > size) {
106 int scalledW;
107 int scalledH;
108 if (w <= h) {
109 scalledW = (int) (w / ((double) h / size));
110 scalledH = size;
111 } else {
112 scalledW = size;
113 scalledH = (int) (h / ((double) w / size));
114 }
115 Bitmap result = Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true);
116 if (originalBitmap != null && !originalBitmap.isRecycled()) {
117 originalBitmap.recycle();
118 }
119 return result;
120 } else {
121 return originalBitmap;
122 }
123 }
124
125 public Bitmap rotate(Bitmap bitmap, int degree) {
126 int w = bitmap.getWidth();
127 int h = bitmap.getHeight();
128 Matrix mtx = new Matrix();
129 mtx.postRotate(degree);
130 Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
131 if (bitmap != null && !bitmap.isRecycled()) {
132 bitmap.recycle();
133 }
134 return result;
135 }
136
137 public boolean useImageAsIs(Uri uri) {
138 String path = getOriginalPath(uri);
139 if (path == null) {
140 return false;
141 }
142 File file = new File(path);
143 long size = file.length();
144 if (size == 0 || size >= 524288 ) {
145 return false;
146 }
147 BitmapFactory.Options options = new BitmapFactory.Options();
148 options.inJustDecodeBounds = true;
149 try {
150 BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri), null, options);
151 if (options == null || options.outMimeType == null || options.outHeight <= 0 || options.outWidth <= 0) {
152 return false;
153 }
154 return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase()));
155 } catch (FileNotFoundException e) {
156 return false;
157 }
158 }
159
160 public String getOriginalPath(Uri uri) {
161 Log.d(Config.LOGTAG,"get original path for uri: "+uri.toString());
162 return FileUtils.getPath(mXmppConnectionService,uri);
163 }
164
165 public DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
166 Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage");
167 String mime = mXmppConnectionService.getContentResolver().getType(uri);
168 String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
169 message.setRelativeFilePath(message.getUuid() + "." + extension);
170 DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
171 file.getParentFile().mkdirs();
172 OutputStream os = null;
173 InputStream is = null;
174 try {
175 file.createNewFile();
176 os = new FileOutputStream(file);
177 is = mXmppConnectionService.getContentResolver().openInputStream(uri);
178 byte[] buffer = new byte[1024];
179 int length;
180 while ((length = is.read(buffer)) > 0) {
181 os.write(buffer, 0, length);
182 }
183 os.flush();
184 } catch(FileNotFoundException e) {
185 throw new FileCopyException(R.string.error_file_not_found);
186 } catch (IOException e) {
187 e.printStackTrace();
188 throw new FileCopyException(R.string.error_io_exception);
189 } finally {
190 close(os);
191 close(is);
192 }
193 Log.d(Config.LOGTAG, "output file name " + mXmppConnectionService.getFileBackend().getFile(message));
194 return file;
195 }
196
197 public DownloadableFile copyImageToPrivateStorage(Message message, Uri image)
198 throws FileCopyException {
199 return this.copyImageToPrivateStorage(message, image, 0);
200 }
201
202 private DownloadableFile copyImageToPrivateStorage(Message message,Uri image, int sampleSize) throws FileCopyException {
203 switch(Config.IMAGE_FORMAT) {
204 case JPEG:
205 message.setRelativeFilePath(message.getUuid()+".jpg");
206 break;
207 case PNG:
208 message.setRelativeFilePath(message.getUuid()+".png");
209 break;
210 case WEBP:
211 message.setRelativeFilePath(message.getUuid()+".webp");
212 break;
213 }
214 DownloadableFile file = getFile(message);
215 file.getParentFile().mkdirs();
216 InputStream is = null;
217 OutputStream os = null;
218 try {
219 file.createNewFile();
220 is = mXmppConnectionService.getContentResolver().openInputStream(image);
221 os = new FileOutputStream(file);
222
223 Bitmap originalBitmap;
224 BitmapFactory.Options options = new BitmapFactory.Options();
225 int inSampleSize = (int) Math.pow(2, sampleSize);
226 Log.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize);
227 options.inSampleSize = inSampleSize;
228 originalBitmap = BitmapFactory.decodeStream(is, null, options);
229 is.close();
230 if (originalBitmap == null) {
231 throw new FileCopyException(R.string.error_not_an_image_file);
232 }
233 Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE);
234 int rotation = getRotation(image);
235 if (rotation > 0) {
236 scaledBitmap = rotate(scaledBitmap, rotation);
237 }
238 boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, Config.IMAGE_QUALITY, os);
239 if (!success) {
240 throw new FileCopyException(R.string.error_compressing_image);
241 }
242 os.flush();
243 long size = file.getSize();
244 int width = scaledBitmap.getWidth();
245 int height = scaledBitmap.getHeight();
246 message.setBody(Long.toString(size) + '|' + width + '|' + height);
247 return file;
248 } catch (FileNotFoundException e) {
249 throw new FileCopyException(R.string.error_file_not_found);
250 } catch (IOException e) {
251 e.printStackTrace();
252 throw new FileCopyException(R.string.error_io_exception);
253 } catch (SecurityException e) {
254 throw new FileCopyException(R.string.error_security_exception_during_image_copy);
255 } catch (OutOfMemoryError e) {
256 ++sampleSize;
257 if (sampleSize <= 3) {
258 return copyImageToPrivateStorage(message, image, sampleSize);
259 } else {
260 throw new FileCopyException(R.string.error_out_of_memory);
261 }
262 } catch (NullPointerException e) {
263 throw new FileCopyException(R.string.error_io_exception);
264 } finally {
265 close(os);
266 close(is);
267 }
268 }
269
270 private int getRotation(File file) {
271 return getRotation(Uri.parse("file://"+file.getAbsolutePath()));
272 }
273
274 private int getRotation(Uri image) {
275 InputStream is = null;
276 try {
277 is = mXmppConnectionService.getContentResolver().openInputStream(image);
278 return ExifHelper.getOrientation(is);
279 } catch (FileNotFoundException e) {
280 return 0;
281 } finally {
282 close(is);
283 }
284 }
285
286 public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
287 throws FileNotFoundException {
288 Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(message.getUuid());
289 if ((thumbnail == null) && (!cacheOnly)) {
290 File file = getFile(message);
291 BitmapFactory.Options options = new BitmapFactory.Options();
292 options.inSampleSize = calcSampleSize(file, size);
293 Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),options);
294 if (fullsize == null) {
295 throw new FileNotFoundException();
296 }
297 thumbnail = resize(fullsize, size);
298 int rotation = getRotation(file);
299 if (rotation > 0) {
300 thumbnail = rotate(thumbnail, rotation);
301 }
302 this.mXmppConnectionService.getBitmapCache().put(message.getUuid(),thumbnail);
303 }
304 return thumbnail;
305 }
306
307 public Uri getTakePhotoUri() {
308 StringBuilder pathBuilder = new StringBuilder();
309 pathBuilder.append(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
310 pathBuilder.append('/');
311 pathBuilder.append("Camera");
312 pathBuilder.append('/');
313 pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date()) + ".jpg");
314 Uri uri = Uri.parse("file://" + pathBuilder.toString());
315 File file = new File(uri.toString());
316 file.getParentFile().mkdirs();
317 return uri;
318 }
319
320 public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
321 try {
322 Avatar avatar = new Avatar();
323 Bitmap bm = cropCenterSquare(image, size);
324 if (bm == null) {
325 return null;
326 }
327 ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
328 Base64OutputStream mBase64OutputSttream = new Base64OutputStream(
329 mByteArrayOutputStream, Base64.DEFAULT);
330 MessageDigest digest = MessageDigest.getInstance("SHA-1");
331 DigestOutputStream mDigestOutputStream = new DigestOutputStream(
332 mBase64OutputSttream, digest);
333 if (!bm.compress(format, 75, mDigestOutputStream)) {
334 return null;
335 }
336 mDigestOutputStream.flush();
337 mDigestOutputStream.close();
338 avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
339 avatar.image = new String(mByteArrayOutputStream.toByteArray());
340 return avatar;
341 } catch (NoSuchAlgorithmException e) {
342 return null;
343 } catch (IOException e) {
344 return null;
345 }
346 }
347
348 public boolean isAvatarCached(Avatar avatar) {
349 File file = new File(getAvatarPath(avatar.getFilename()));
350 return file.exists();
351 }
352
353 public boolean save(Avatar avatar) {
354 File file;
355 if (isAvatarCached(avatar)) {
356 file = new File(getAvatarPath(avatar.getFilename()));
357 } else {
358 String filename = getAvatarPath(avatar.getFilename());
359 file = new File(filename + ".tmp");
360 file.getParentFile().mkdirs();
361 OutputStream os = null;
362 try {
363 file.createNewFile();
364 os = new FileOutputStream(file);
365 MessageDigest digest = MessageDigest.getInstance("SHA-1");
366 digest.reset();
367 DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest);
368 mDigestOutputStream.write(avatar.getImageAsBytes());
369 mDigestOutputStream.flush();
370 mDigestOutputStream.close();
371 String sha1sum = CryptoHelper.bytesToHex(digest.digest());
372 if (sha1sum.equals(avatar.sha1sum)) {
373 file.renameTo(new File(filename));
374 } else {
375 Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner);
376 file.delete();
377 return false;
378 }
379 } catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) {
380 return false;
381 } finally {
382 close(os);
383 }
384 }
385 avatar.size = file.length();
386 return true;
387 }
388
389 public String getAvatarPath(String avatar) {
390 return mXmppConnectionService.getFilesDir().getAbsolutePath()+ "/avatars/" + avatar;
391 }
392
393 public Uri getAvatarUri(String avatar) {
394 return Uri.parse("file:" + getAvatarPath(avatar));
395 }
396
397 public Bitmap cropCenterSquare(Uri image, int size) {
398 if (image == null) {
399 return null;
400 }
401 InputStream is = null;
402 try {
403 BitmapFactory.Options options = new BitmapFactory.Options();
404 options.inSampleSize = calcSampleSize(image, size);
405 is = mXmppConnectionService.getContentResolver().openInputStream(image);
406 if (is == null) {
407 return null;
408 }
409 Bitmap input = BitmapFactory.decodeStream(is, null, options);
410 if (input == null) {
411 return null;
412 } else {
413 int rotation = getRotation(image);
414 if (rotation > 0) {
415 input = rotate(input, rotation);
416 }
417 return cropCenterSquare(input, size);
418 }
419 } catch (FileNotFoundException e) {
420 return null;
421 } finally {
422 close(is);
423 }
424 }
425
426 public Bitmap cropCenter(Uri image, int newHeight, int newWidth) {
427 if (image == null) {
428 return null;
429 }
430 InputStream is = null;
431 try {
432 BitmapFactory.Options options = new BitmapFactory.Options();
433 options.inSampleSize = calcSampleSize(image,Math.max(newHeight, newWidth));
434 is = mXmppConnectionService.getContentResolver().openInputStream(image);
435 if (is == null) {
436 return null;
437 }
438 Bitmap source = BitmapFactory.decodeStream(is, null, options);
439 if (source == null) {
440 return null;
441 }
442 int sourceWidth = source.getWidth();
443 int sourceHeight = source.getHeight();
444 float xScale = (float) newWidth / sourceWidth;
445 float yScale = (float) newHeight / sourceHeight;
446 float scale = Math.max(xScale, yScale);
447 float scaledWidth = scale * sourceWidth;
448 float scaledHeight = scale * sourceHeight;
449 float left = (newWidth - scaledWidth) / 2;
450 float top = (newHeight - scaledHeight) / 2;
451
452 RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);
453 Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
454 Canvas canvas = new Canvas(dest);
455 canvas.drawBitmap(source, null, targetRect, null);
456 if (source != null && !source.isRecycled()) {
457 source.recycle();
458 }
459 return dest;
460 } catch (FileNotFoundException e) {
461 return null;
462 } finally {
463 close(is);
464 }
465 }
466
467 public Bitmap cropCenterSquare(Bitmap input, int size) {
468 int w = input.getWidth();
469 int h = input.getHeight();
470
471 float scale = Math.max((float) size / h, (float) size / w);
472
473 float outWidth = scale * w;
474 float outHeight = scale * h;
475 float left = (size - outWidth) / 2;
476 float top = (size - outHeight) / 2;
477 RectF target = new RectF(left, top, left + outWidth, top + outHeight);
478
479 Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
480 Canvas canvas = new Canvas(output);
481 canvas.drawBitmap(input, null, target, null);
482 if (input != null && !input.isRecycled()) {
483 input.recycle();
484 }
485 return output;
486 }
487
488 private int calcSampleSize(Uri image, int size) throws FileNotFoundException {
489 BitmapFactory.Options options = new BitmapFactory.Options();
490 options.inJustDecodeBounds = true;
491 BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(image), null, options);
492 return calcSampleSize(options, size);
493 }
494
495 private int calcSampleSize(File image, int size) {
496 BitmapFactory.Options options = new BitmapFactory.Options();
497 options.inJustDecodeBounds = true;
498 BitmapFactory.decodeFile(image.getAbsolutePath(), options);
499 return calcSampleSize(options, size);
500 }
501
502 private int calcSampleSize(BitmapFactory.Options options, int size) {
503 int height = options.outHeight;
504 int width = options.outWidth;
505 int inSampleSize = 1;
506
507 if (height > size || width > size) {
508 int halfHeight = height / 2;
509 int halfWidth = width / 2;
510
511 while ((halfHeight / inSampleSize) > size
512 && (halfWidth / inSampleSize) > size) {
513 inSampleSize *= 2;
514 }
515 }
516 return inSampleSize;
517 }
518
519 public Uri getJingleFileUri(Message message) {
520 File file = getFile(message);
521 return Uri.parse("file://" + file.getAbsolutePath());
522 }
523
524 public void updateFileParams(Message message) {
525 updateFileParams(message,null);
526 }
527
528 public void updateFileParams(Message message, URL url) {
529 DownloadableFile file = getFile(message);
530 if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) {
531 BitmapFactory.Options options = new BitmapFactory.Options();
532 options.inJustDecodeBounds = true;
533 BitmapFactory.decodeFile(file.getAbsolutePath(), options);
534 int rotation = getRotation(file);
535 boolean rotated = rotation == 90 || rotation == 270;
536 int imageHeight = rotated ? options.outWidth : options.outHeight;
537 int imageWidth = rotated ? options.outHeight : options.outWidth;
538 if (url == null) {
539 message.setBody(Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
540 } else {
541 message.setBody(url.toString()+"|"+Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
542 }
543 } else {
544 if (url != null) {
545 message.setBody(url.toString()+"|"+Long.toString(file.getSize()));
546 } else {
547 message.setBody(Long.toString(file.getSize()));
548 }
549 }
550
551 }
552
553 public class FileCopyException extends Exception {
554 private static final long serialVersionUID = -1010013599132881427L;
555 private int resId;
556
557 public FileCopyException(int resId) {
558 this.resId = resId;
559 }
560
561 public int getResId() {
562 return resId;
563 }
564 }
565
566 public Bitmap getAvatar(String avatar, int size) {
567 if (avatar == null) {
568 return null;
569 }
570 Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
571 if (bm == null) {
572 return null;
573 }
574 return bm;
575 }
576
577 public boolean isFileAvailable(Message message) {
578 return getFile(message).exists();
579 }
580
581 public static void close(Closeable stream) {
582 if (stream != null) {
583 try {
584 stream.close();
585 } catch (IOException e) {
586 }
587 }
588 }
589
590 public static void close(Socket socket) {
591 if (socket != null) {
592 try {
593 socket.close();
594 } catch (IOException e) {
595 }
596 }
597 }
598}