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 final boolean encrypted = !decrypted
62 && (message.getEncryption() == Message.ENCRYPTION_PGP
63 || message.getEncryption() == Message.ENCRYPTION_DECRYPTED);
64 final DownloadableFile file;
65 String path = message.getRelativeFilePath();
66 if (path == null) {
67 path = message.getUuid();
68 }
69 if (path.startsWith("/")) {
70 file = new DownloadableFile(path);
71 } else {
72 String mime = message.getMimeType();
73 if (mime != null && mime.startsWith("image")) {
74 file = new DownloadableFile(getConversationsImageDirectory() + path);
75 } else {
76 file = new DownloadableFile(getConversationsFileDirectory() + path);
77 }
78 }
79 if (encrypted) {
80 return new DownloadableFile(getConversationsFileDirectory() + file.getName() + ".pgp");
81 } else {
82 return file;
83 }
84 }
85
86 public static String getConversationsFileDirectory() {
87 return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/";
88 }
89
90 public static String getConversationsImageDirectory() {
91 return Environment.getExternalStoragePublicDirectory(
92 Environment.DIRECTORY_PICTURES).getAbsolutePath()
93 + "/Conversations/";
94 }
95
96 public Bitmap resize(Bitmap originalBitmap, int size) {
97 int w = originalBitmap.getWidth();
98 int h = originalBitmap.getHeight();
99 if (Math.max(w, h) > size) {
100 int scalledW;
101 int scalledH;
102 if (w <= h) {
103 scalledW = (int) (w / ((double) h / size));
104 scalledH = size;
105 } else {
106 scalledW = size;
107 scalledH = (int) (h / ((double) w / size));
108 }
109 Bitmap result = Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true);
110 if (originalBitmap != null && !originalBitmap.isRecycled()) {
111 originalBitmap.recycle();
112 }
113 return result;
114 } else {
115 return originalBitmap;
116 }
117 }
118
119 public Bitmap rotate(Bitmap bitmap, int degree) {
120 int w = bitmap.getWidth();
121 int h = bitmap.getHeight();
122 Matrix mtx = new Matrix();
123 mtx.postRotate(degree);
124 Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
125 if (bitmap != null && !bitmap.isRecycled()) {
126 bitmap.recycle();
127 }
128 return result;
129 }
130
131 public boolean useImageAsIs(Uri uri) {
132 String path = getOriginalPath(uri);
133 if (path == null) {
134 return false;
135 }
136 File file = new File(path);
137 long size = file.length();
138 if (size == 0 || size >= 524288 ) {
139 return false;
140 }
141 BitmapFactory.Options options = new BitmapFactory.Options();
142 options.inJustDecodeBounds = true;
143 try {
144 BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri), null, options);
145 if (options == null || options.outMimeType == null || options.outHeight <= 0 || options.outWidth <= 0) {
146 return false;
147 }
148 return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase()));
149 } catch (FileNotFoundException e) {
150 return false;
151 }
152 }
153
154 public String getOriginalPath(Uri uri) {
155 Log.d(Config.LOGTAG,"get original path for uri: "+uri.toString());
156 return FileUtils.getPath(mXmppConnectionService,uri);
157 }
158
159 public DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
160 Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage");
161 String mime = mXmppConnectionService.getContentResolver().getType(uri);
162 String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
163 message.setRelativeFilePath(message.getUuid() + "." + extension);
164 DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
165 file.getParentFile().mkdirs();
166 OutputStream os = null;
167 InputStream is = null;
168 try {
169 file.createNewFile();
170 os = new FileOutputStream(file);
171 is = mXmppConnectionService.getContentResolver().openInputStream(uri);
172 byte[] buffer = new byte[1024];
173 int length;
174 while ((length = is.read(buffer)) > 0) {
175 os.write(buffer, 0, length);
176 }
177 os.flush();
178 } catch(FileNotFoundException e) {
179 throw new FileCopyException(R.string.error_file_not_found);
180 } catch (IOException e) {
181 e.printStackTrace();
182 throw new FileCopyException(R.string.error_io_exception);
183 } finally {
184 close(os);
185 close(is);
186 }
187 Log.d(Config.LOGTAG, "output file name " + mXmppConnectionService.getFileBackend().getFile(message));
188 return file;
189 }
190
191 public DownloadableFile copyImageToPrivateStorage(Message message, Uri image)
192 throws FileCopyException {
193 return this.copyImageToPrivateStorage(message, image, 0);
194 }
195
196 private DownloadableFile copyImageToPrivateStorage(Message message,Uri image, int sampleSize) throws FileCopyException {
197 switch(Config.IMAGE_FORMAT) {
198 case JPEG:
199 message.setRelativeFilePath(message.getUuid()+".jpg");
200 break;
201 case PNG:
202 message.setRelativeFilePath(message.getUuid()+".png");
203 break;
204 case WEBP:
205 message.setRelativeFilePath(message.getUuid()+".webp");
206 break;
207 }
208 DownloadableFile file = getFile(message);
209 file.getParentFile().mkdirs();
210 InputStream is = null;
211 OutputStream os = null;
212 try {
213 file.createNewFile();
214 is = mXmppConnectionService.getContentResolver().openInputStream(image);
215 os = new FileOutputStream(file);
216
217 Bitmap originalBitmap;
218 BitmapFactory.Options options = new BitmapFactory.Options();
219 int inSampleSize = (int) Math.pow(2, sampleSize);
220 Log.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize);
221 options.inSampleSize = inSampleSize;
222 originalBitmap = BitmapFactory.decodeStream(is, null, options);
223 is.close();
224 if (originalBitmap == null) {
225 throw new FileCopyException(R.string.error_not_an_image_file);
226 }
227 Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE);
228 int rotation = getRotation(image);
229 if (rotation > 0) {
230 scaledBitmap = rotate(scaledBitmap, rotation);
231 }
232 boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, Config.IMAGE_QUALITY, os);
233 if (!success) {
234 throw new FileCopyException(R.string.error_compressing_image);
235 }
236 os.flush();
237 long size = file.getSize();
238 int width = scaledBitmap.getWidth();
239 int height = scaledBitmap.getHeight();
240 message.setBody(Long.toString(size) + '|' + width + '|' + height);
241 return file;
242 } catch (FileNotFoundException e) {
243 throw new FileCopyException(R.string.error_file_not_found);
244 } catch (IOException e) {
245 e.printStackTrace();
246 throw new FileCopyException(R.string.error_io_exception);
247 } catch (SecurityException e) {
248 throw new FileCopyException(R.string.error_security_exception_during_image_copy);
249 } catch (OutOfMemoryError e) {
250 ++sampleSize;
251 if (sampleSize <= 3) {
252 return copyImageToPrivateStorage(message, image, sampleSize);
253 } else {
254 throw new FileCopyException(R.string.error_out_of_memory);
255 }
256 } catch (NullPointerException e) {
257 throw new FileCopyException(R.string.error_io_exception);
258 } finally {
259 close(os);
260 close(is);
261 }
262 }
263
264 private int getRotation(File file) {
265 return getRotation(Uri.parse("file://"+file.getAbsolutePath()));
266 }
267
268 private int getRotation(Uri image) {
269 InputStream is = null;
270 try {
271 is = mXmppConnectionService.getContentResolver().openInputStream(image);
272 return ExifHelper.getOrientation(is);
273 } catch (FileNotFoundException e) {
274 return 0;
275 } finally {
276 close(is);
277 }
278 }
279
280 public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
281 throws FileNotFoundException {
282 Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(message.getUuid());
283 if ((thumbnail == null) && (!cacheOnly)) {
284 File file = getFile(message);
285 BitmapFactory.Options options = new BitmapFactory.Options();
286 options.inSampleSize = calcSampleSize(file, size);
287 Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),options);
288 if (fullsize == null) {
289 throw new FileNotFoundException();
290 }
291 thumbnail = resize(fullsize, size);
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 if (source != null && !source.isRecycled()) {
451 source.recycle();
452 }
453 return dest;
454 } catch (FileNotFoundException e) {
455 return null;
456 } finally {
457 close(is);
458 }
459 }
460
461 public Bitmap cropCenterSquare(Bitmap input, int size) {
462 int w = input.getWidth();
463 int h = input.getHeight();
464
465 float scale = Math.max((float) size / h, (float) size / w);
466
467 float outWidth = scale * w;
468 float outHeight = scale * h;
469 float left = (size - outWidth) / 2;
470 float top = (size - outHeight) / 2;
471 RectF target = new RectF(left, top, left + outWidth, top + outHeight);
472
473 Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
474 Canvas canvas = new Canvas(output);
475 canvas.drawBitmap(input, null, target, null);
476 if (input != null && !input.isRecycled()) {
477 input.recycle();
478 }
479 return output;
480 }
481
482 private int calcSampleSize(Uri image, int size) throws FileNotFoundException {
483 BitmapFactory.Options options = new BitmapFactory.Options();
484 options.inJustDecodeBounds = true;
485 BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(image), null, options);
486 return calcSampleSize(options, size);
487 }
488
489 private int calcSampleSize(File image, int size) {
490 BitmapFactory.Options options = new BitmapFactory.Options();
491 options.inJustDecodeBounds = true;
492 BitmapFactory.decodeFile(image.getAbsolutePath(), options);
493 return calcSampleSize(options, size);
494 }
495
496 private int calcSampleSize(BitmapFactory.Options options, int size) {
497 int height = options.outHeight;
498 int width = options.outWidth;
499 int inSampleSize = 1;
500
501 if (height > size || width > size) {
502 int halfHeight = height / 2;
503 int halfWidth = width / 2;
504
505 while ((halfHeight / inSampleSize) > size
506 && (halfWidth / inSampleSize) > size) {
507 inSampleSize *= 2;
508 }
509 }
510 return inSampleSize;
511 }
512
513 public Uri getJingleFileUri(Message message) {
514 File file = getFile(message);
515 return Uri.parse("file://" + file.getAbsolutePath());
516 }
517
518 public void updateFileParams(Message message) {
519 updateFileParams(message,null);
520 }
521
522 public void updateFileParams(Message message, URL url) {
523 DownloadableFile file = getFile(message);
524 if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) {
525 BitmapFactory.Options options = new BitmapFactory.Options();
526 options.inJustDecodeBounds = true;
527 BitmapFactory.decodeFile(file.getAbsolutePath(), options);
528 int rotation = getRotation(file);
529 boolean rotated = rotation == 90 || rotation == 270;
530 int imageHeight = rotated ? options.outWidth : options.outHeight;
531 int imageWidth = rotated ? options.outHeight : options.outWidth;
532 if (url == null) {
533 message.setBody(Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
534 } else {
535 message.setBody(url.toString()+"|"+Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
536 }
537 } else {
538 if (url != null) {
539 message.setBody(url.toString()+"|"+Long.toString(file.getSize()));
540 } else {
541 message.setBody(Long.toString(file.getSize()));
542 }
543 }
544
545 }
546
547 public class FileCopyException extends Exception {
548 private static final long serialVersionUID = -1010013599132881427L;
549 private int resId;
550
551 public FileCopyException(int resId) {
552 this.resId = resId;
553 }
554
555 public int getResId() {
556 return resId;
557 }
558 }
559
560 public Bitmap getAvatar(String avatar, int size) {
561 if (avatar == null) {
562 return null;
563 }
564 Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
565 if (bm == null) {
566 return null;
567 }
568 return bm;
569 }
570
571 public boolean isFileAvailable(Message message) {
572 return getFile(message).exists();
573 }
574
575 public static void close(Closeable stream) {
576 if (stream != null) {
577 try {
578 stream.close();
579 } catch (IOException e) {
580 }
581 }
582 }
583
584 public static void close(Socket socket) {
585 if (socket != null) {
586 try {
587 socket.close();
588 } catch (IOException e) {
589 }
590 }
591 }
592}