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