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