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(Uri image) {
264 InputStream is = null;
265 try {
266 is = mXmppConnectionService.getContentResolver().openInputStream(image);
267 return ExifHelper.getOrientation(is);
268 } catch (FileNotFoundException e) {
269 return 0;
270 } finally {
271 close(is);
272 }
273 }
274
275 public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
276 throws FileNotFoundException {
277 Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(
278 message.getUuid());
279 if ((thumbnail == null) && (!cacheOnly)) {
280 File file = getFile(message);
281 BitmapFactory.Options options = new BitmapFactory.Options();
282 options.inSampleSize = calcSampleSize(file, size);
283 Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),options);
284 if (fullsize == null) {
285 throw new FileNotFoundException();
286 }
287 thumbnail = resize(fullsize, size);
288 this.mXmppConnectionService.getBitmapCache().put(message.getUuid(),
289 thumbnail);
290 }
291 return thumbnail;
292 }
293
294 public Uri getTakePhotoUri() {
295 StringBuilder pathBuilder = new StringBuilder();
296 pathBuilder.append(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
297 pathBuilder.append('/');
298 pathBuilder.append("Camera");
299 pathBuilder.append('/');
300 pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date()) + ".jpg");
301 Uri uri = Uri.parse("file://" + pathBuilder.toString());
302 File file = new File(uri.toString());
303 file.getParentFile().mkdirs();
304 return uri;
305 }
306
307 public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
308 try {
309 Avatar avatar = new Avatar();
310 Bitmap bm = cropCenterSquare(image, size);
311 if (bm == null) {
312 return null;
313 }
314 ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
315 Base64OutputStream mBase64OutputSttream = new Base64OutputStream(
316 mByteArrayOutputStream, Base64.DEFAULT);
317 MessageDigest digest = MessageDigest.getInstance("SHA-1");
318 DigestOutputStream mDigestOutputStream = new DigestOutputStream(
319 mBase64OutputSttream, digest);
320 if (!bm.compress(format, 75, mDigestOutputStream)) {
321 return null;
322 }
323 mDigestOutputStream.flush();
324 mDigestOutputStream.close();
325 avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
326 avatar.image = new String(mByteArrayOutputStream.toByteArray());
327 return avatar;
328 } catch (NoSuchAlgorithmException e) {
329 return null;
330 } catch (IOException e) {
331 return null;
332 }
333 }
334
335 public boolean isAvatarCached(Avatar avatar) {
336 File file = new File(getAvatarPath(avatar.getFilename()));
337 return file.exists();
338 }
339
340 public boolean save(Avatar avatar) {
341 File file;
342 if (isAvatarCached(avatar)) {
343 file = new File(getAvatarPath(avatar.getFilename()));
344 } else {
345 String filename = getAvatarPath(avatar.getFilename());
346 file = new File(filename + ".tmp");
347 file.getParentFile().mkdirs();
348 OutputStream os = null;
349 try {
350 file.createNewFile();
351 os = new FileOutputStream(file);
352 MessageDigest digest = MessageDigest.getInstance("SHA-1");
353 digest.reset();
354 DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest);
355 mDigestOutputStream.write(avatar.getImageAsBytes());
356 mDigestOutputStream.flush();
357 mDigestOutputStream.close();
358 String sha1sum = CryptoHelper.bytesToHex(digest.digest());
359 if (sha1sum.equals(avatar.sha1sum)) {
360 file.renameTo(new File(filename));
361 } else {
362 Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner);
363 file.delete();
364 return false;
365 }
366 } catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) {
367 return false;
368 } finally {
369 close(os);
370 }
371 }
372 avatar.size = file.length();
373 return true;
374 }
375
376 public String getAvatarPath(String avatar) {
377 return mXmppConnectionService.getFilesDir().getAbsolutePath()+ "/avatars/" + avatar;
378 }
379
380 public Uri getAvatarUri(String avatar) {
381 return Uri.parse("file:" + getAvatarPath(avatar));
382 }
383
384 public Bitmap cropCenterSquare(Uri image, int size) {
385 if (image == null) {
386 return null;
387 }
388 InputStream is = null;
389 try {
390 BitmapFactory.Options options = new BitmapFactory.Options();
391 options.inSampleSize = calcSampleSize(image, size);
392 is = mXmppConnectionService.getContentResolver().openInputStream(image);
393 if (is == null) {
394 return null;
395 }
396 Bitmap input = BitmapFactory.decodeStream(is, null, options);
397 if (input == null) {
398 return null;
399 } else {
400 int rotation = getRotation(image);
401 if (rotation > 0) {
402 input = rotate(input, rotation);
403 }
404 return cropCenterSquare(input, size);
405 }
406 } catch (FileNotFoundException e) {
407 return null;
408 } finally {
409 close(is);
410 }
411 }
412
413 public Bitmap cropCenter(Uri image, int newHeight, int newWidth) {
414 if (image == null) {
415 return null;
416 }
417 InputStream is = null;
418 try {
419 BitmapFactory.Options options = new BitmapFactory.Options();
420 options.inSampleSize = calcSampleSize(image,Math.max(newHeight, newWidth));
421 is = mXmppConnectionService.getContentResolver().openInputStream(image);
422 if (is == null) {
423 return null;
424 }
425 Bitmap source = BitmapFactory.decodeStream(is, null, options);
426 if (source == null) {
427 return null;
428 }
429 int sourceWidth = source.getWidth();
430 int sourceHeight = source.getHeight();
431 float xScale = (float) newWidth / sourceWidth;
432 float yScale = (float) newHeight / sourceHeight;
433 float scale = Math.max(xScale, yScale);
434 float scaledWidth = scale * sourceWidth;
435 float scaledHeight = scale * sourceHeight;
436 float left = (newWidth - scaledWidth) / 2;
437 float top = (newHeight - scaledHeight) / 2;
438
439 RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);
440 Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
441 Canvas canvas = new Canvas(dest);
442 canvas.drawBitmap(source, null, targetRect, null);
443 return dest;
444 } catch (FileNotFoundException e) {
445 return null;
446 } finally {
447 close(is);
448 }
449 }
450
451 public Bitmap cropCenterSquare(Bitmap input, int size) {
452 int w = input.getWidth();
453 int h = input.getHeight();
454
455 float scale = Math.max((float) size / h, (float) size / w);
456
457 float outWidth = scale * w;
458 float outHeight = scale * h;
459 float left = (size - outWidth) / 2;
460 float top = (size - outHeight) / 2;
461 RectF target = new RectF(left, top, left + outWidth, top + outHeight);
462
463 Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
464 Canvas canvas = new Canvas(output);
465 canvas.drawBitmap(input, null, target, null);
466 return output;
467 }
468
469 private int calcSampleSize(Uri image, int size) throws FileNotFoundException {
470 BitmapFactory.Options options = new BitmapFactory.Options();
471 options.inJustDecodeBounds = true;
472 BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(image), null, options);
473 return calcSampleSize(options, size);
474 }
475
476 private int calcSampleSize(File image, int size) {
477 BitmapFactory.Options options = new BitmapFactory.Options();
478 options.inJustDecodeBounds = true;
479 BitmapFactory.decodeFile(image.getAbsolutePath(), options);
480 return calcSampleSize(options, size);
481 }
482
483 private int calcSampleSize(BitmapFactory.Options options, int size) {
484 int height = options.outHeight;
485 int width = options.outWidth;
486 int inSampleSize = 1;
487
488 if (height > size || width > size) {
489 int halfHeight = height / 2;
490 int halfWidth = width / 2;
491
492 while ((halfHeight / inSampleSize) > size
493 && (halfWidth / inSampleSize) > size) {
494 inSampleSize *= 2;
495 }
496 }
497 return inSampleSize;
498 }
499
500 public Uri getJingleFileUri(Message message) {
501 File file = getFile(message);
502 return Uri.parse("file://" + file.getAbsolutePath());
503 }
504
505 public void updateFileParams(Message message) {
506 updateFileParams(message,null);
507 }
508
509 public void updateFileParams(Message message, URL url) {
510 DownloadableFile file = getFile(message);
511 if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) {
512 BitmapFactory.Options options = new BitmapFactory.Options();
513 options.inJustDecodeBounds = true;
514 BitmapFactory.decodeFile(file.getAbsolutePath(), options);
515 int imageHeight = options.outHeight;
516 int imageWidth = options.outWidth;
517 if (url == null) {
518 message.setBody(Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
519 } else {
520 message.setBody(url.toString()+"|"+Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
521 }
522 } else {
523 if (url != null) {
524 message.setBody(url.toString()+"|"+Long.toString(file.getSize()));
525 } else {
526 message.setBody(Long.toString(file.getSize()));
527 }
528 }
529
530 }
531
532 public class FileCopyException extends Exception {
533 private static final long serialVersionUID = -1010013599132881427L;
534 private int resId;
535
536 public FileCopyException(int resId) {
537 this.resId = resId;
538 }
539
540 public int getResId() {
541 return resId;
542 }
543 }
544
545 public Bitmap getAvatar(String avatar, int size) {
546 if (avatar == null) {
547 return null;
548 }
549 Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
550 if (bm == null) {
551 return null;
552 }
553 return bm;
554 }
555
556 public boolean isFileAvailable(Message message) {
557 return getFile(message).exists();
558 }
559
560 public static void close(Closeable stream) {
561 if (stream != null) {
562 try {
563 stream.close();
564 } catch (IOException e) {
565 }
566 }
567 }
568
569 public static void close(Socket socket) {
570 if (socket != null) {
571 try {
572 socket.close();
573 } catch (IOException e) {
574 }
575 }
576 }
577}