1package eu.siacs.conversations.persistance;
2
3import java.io.ByteArrayOutputStream;
4import java.io.File;
5import java.io.FileNotFoundException;
6import java.io.FileOutputStream;
7import java.io.IOException;
8import java.io.InputStream;
9import java.io.OutputStream;
10import java.security.DigestOutputStream;
11import java.security.MessageDigest;
12import java.security.NoSuchAlgorithmException;
13import java.text.SimpleDateFormat;
14import java.util.Date;
15import java.util.Locale;
16
17import android.database.Cursor;
18import android.graphics.Bitmap;
19import android.graphics.BitmapFactory;
20import android.graphics.Canvas;
21import android.graphics.Matrix;
22import android.graphics.RectF;
23import android.media.ExifInterface;
24import android.net.Uri;
25import android.os.Environment;
26import android.provider.MediaStore;
27import android.util.Base64;
28import android.util.Base64OutputStream;
29import android.util.Log;
30import eu.siacs.conversations.Config;
31import eu.siacs.conversations.R;
32import eu.siacs.conversations.entities.Conversation;
33import eu.siacs.conversations.entities.DownloadableFile;
34import eu.siacs.conversations.entities.Message;
35import eu.siacs.conversations.services.XmppConnectionService;
36import eu.siacs.conversations.utils.CryptoHelper;
37import eu.siacs.conversations.xmpp.pep.Avatar;
38
39public class FileBackend {
40
41 private static int IMAGE_SIZE = 1920;
42
43 private SimpleDateFormat imageDateFormat = new SimpleDateFormat(
44 "yyyyMMdd_HHmmssSSS", Locale.US);
45
46 private XmppConnectionService mXmppConnectionService;
47
48 public FileBackend(XmppConnectionService service) {
49 this.mXmppConnectionService = service;
50 }
51
52 public DownloadableFile getFile(Message message) {
53 return getFile(message, true);
54 }
55
56 public DownloadableFile getFile(Message message, boolean decrypted) {
57 StringBuilder filename = new StringBuilder();
58 filename.append(getConversationsDirectory());
59 filename.append(message.getUuid());
60 if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) {
61 filename.append(".webp");
62 } else {
63 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
64 filename.append(".webp");
65 } else {
66 filename.append(".webp.pgp");
67 }
68 }
69 return new DownloadableFile(filename.toString());
70 }
71
72 public static String getConversationsDirectory() {
73 return Environment.getExternalStoragePublicDirectory(
74 Environment.DIRECTORY_PICTURES).getAbsolutePath()
75 + "/Conversations/";
76 }
77
78 public Bitmap resize(Bitmap originalBitmap, int size) {
79 int w = originalBitmap.getWidth();
80 int h = originalBitmap.getHeight();
81 if (Math.max(w, h) > size) {
82 int scalledW;
83 int scalledH;
84 if (w <= h) {
85 scalledW = (int) (w / ((double) h / size));
86 scalledH = size;
87 } else {
88 scalledW = size;
89 scalledH = (int) (h / ((double) w / size));
90 }
91 Bitmap scalledBitmap = Bitmap.createScaledBitmap(originalBitmap,
92 scalledW, scalledH, true);
93 return scalledBitmap;
94 } else {
95 return originalBitmap;
96 }
97 }
98
99 public Bitmap rotate(Bitmap bitmap, int degree) {
100 int w = bitmap.getWidth();
101 int h = bitmap.getHeight();
102 Matrix mtx = new Matrix();
103 mtx.postRotate(degree);
104 return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
105 }
106
107 public DownloadableFile copyImageToPrivateStorage(Message message, Uri image)
108 throws ImageCopyException {
109 return this.copyImageToPrivateStorage(message, image, 0);
110 }
111
112 private DownloadableFile copyImageToPrivateStorage(Message message,
113 Uri image, int sampleSize) throws ImageCopyException {
114 try {
115 InputStream is = mXmppConnectionService.getContentResolver()
116 .openInputStream(image);
117 DownloadableFile file = getFile(message);
118 file.getParentFile().mkdirs();
119 file.createNewFile();
120 Bitmap originalBitmap;
121 BitmapFactory.Options options = new BitmapFactory.Options();
122 int inSampleSize = (int) Math.pow(2, sampleSize);
123 Log.d(Config.LOGTAG, "reading bitmap with sample size "
124 + inSampleSize);
125 options.inSampleSize = inSampleSize;
126 originalBitmap = BitmapFactory.decodeStream(is, null, options);
127 is.close();
128 if (originalBitmap == null) {
129 throw new ImageCopyException(R.string.error_not_an_image_file);
130 }
131 Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE);
132 originalBitmap = null;
133 int rotation = getRotation(image);
134 if (rotation > 0) {
135 scalledBitmap = rotate(scalledBitmap, rotation);
136 }
137 OutputStream os = new FileOutputStream(file);
138 boolean success = scalledBitmap.compress(
139 Bitmap.CompressFormat.WEBP, 75, os);
140 if (!success) {
141 throw new ImageCopyException(R.string.error_compressing_image);
142 }
143 os.flush();
144 os.close();
145 long size = file.getSize();
146 int width = scalledBitmap.getWidth();
147 int height = scalledBitmap.getHeight();
148 message.setBody(Long.toString(size) + ',' + width + ',' + height);
149 return file;
150 } catch (FileNotFoundException e) {
151 throw new ImageCopyException(R.string.error_file_not_found);
152 } catch (IOException e) {
153 throw new ImageCopyException(R.string.error_io_exception);
154 } catch (SecurityException e) {
155 throw new ImageCopyException(
156 R.string.error_security_exception_during_image_copy);
157 } catch (OutOfMemoryError e) {
158 ++sampleSize;
159 if (sampleSize <= 3) {
160 return copyImageToPrivateStorage(message, image, sampleSize);
161 } else {
162 throw new ImageCopyException(R.string.error_out_of_memory);
163 }
164 }
165 }
166
167 private int getRotation(Uri image) {
168 if ("content".equals(image.getScheme())) {
169 try {
170 Cursor cursor = mXmppConnectionService
171 .getContentResolver()
172 .query(image,
173 new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
174 null, null, null);
175 if (cursor.getCount() != 1) {
176 return -1;
177 }
178 cursor.moveToFirst();
179 return cursor.getInt(0);
180 } catch (IllegalArgumentException e) {
181 return -1;
182 }
183 } else {
184 ExifInterface exif;
185 try {
186 exif = new ExifInterface(image.toString());
187 if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
188 .equalsIgnoreCase("6")) {
189 return 90;
190 } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
191 .equalsIgnoreCase("8")) {
192 return 270;
193 } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
194 .equalsIgnoreCase("3")) {
195 return 180;
196 } else {
197 return 0;
198 }
199 } catch (IOException e) {
200 return -1;
201 }
202 }
203 }
204
205 public Bitmap getImageFromMessage(Message message) {
206 return BitmapFactory.decodeFile(getFile(message).getAbsolutePath());
207 }
208
209 public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
210 throws FileNotFoundException {
211 Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(
212 message.getUuid());
213 if ((thumbnail == null) && (!cacheOnly)) {
214 File file = getFile(message);
215 BitmapFactory.Options options = new BitmapFactory.Options();
216 options.inSampleSize = calcSampleSize(file, size);
217 Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),
218 options);
219 if (fullsize == null) {
220 throw new FileNotFoundException();
221 }
222 thumbnail = resize(fullsize, size);
223 this.mXmppConnectionService.getBitmapCache().put(message.getUuid(),
224 thumbnail);
225 }
226 return thumbnail;
227 }
228
229 public void removeFiles(Conversation conversation) {
230 String prefix = mXmppConnectionService.getFilesDir().getAbsolutePath();
231 String path = prefix + "/" + conversation.getAccount().getJid() + "/"
232 + conversation.getContactJid();
233 File file = new File(path);
234 try {
235 this.deleteFile(file);
236 } catch (IOException e) {
237 Log.d(Config.LOGTAG,
238 "error deleting file: " + file.getAbsolutePath());
239 }
240 }
241
242 private void deleteFile(File f) throws IOException {
243 if (f.isDirectory()) {
244 for (File c : f.listFiles())
245 deleteFile(c);
246 }
247 f.delete();
248 }
249
250 public Uri getTakePhotoUri() {
251 StringBuilder pathBuilder = new StringBuilder();
252 pathBuilder.append(Environment
253 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
254 pathBuilder.append('/');
255 pathBuilder.append("Camera");
256 pathBuilder.append('/');
257 pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date())
258 + ".jpg");
259 Uri uri = Uri.parse("file://" + pathBuilder.toString());
260 File file = new File(uri.toString());
261 file.getParentFile().mkdirs();
262 return uri;
263 }
264
265 public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
266 try {
267 Avatar avatar = new Avatar();
268 Bitmap bm = cropCenterSquare(image, size);
269 if (bm == null) {
270 return null;
271 }
272 ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
273 Base64OutputStream mBase64OutputSttream = new Base64OutputStream(
274 mByteArrayOutputStream, Base64.DEFAULT);
275 MessageDigest digest = MessageDigest.getInstance("SHA-1");
276 DigestOutputStream mDigestOutputStream = new DigestOutputStream(
277 mBase64OutputSttream, digest);
278 if (!bm.compress(format, 75, mDigestOutputStream)) {
279 return null;
280 }
281 mDigestOutputStream.flush();
282 mDigestOutputStream.close();
283 avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
284 avatar.image = new String(mByteArrayOutputStream.toByteArray());
285 return avatar;
286 } catch (NoSuchAlgorithmException e) {
287 return null;
288 } catch (IOException e) {
289 return null;
290 }
291 }
292
293 public boolean isAvatarCached(Avatar avatar) {
294 File file = new File(getAvatarPath(avatar.getFilename()));
295 return file.exists();
296 }
297
298 public boolean save(Avatar avatar) {
299 if (isAvatarCached(avatar)) {
300 return true;
301 }
302 String filename = getAvatarPath(avatar.getFilename());
303 File file = new File(filename + ".tmp");
304 file.getParentFile().mkdirs();
305 try {
306 file.createNewFile();
307 FileOutputStream mFileOutputStream = new FileOutputStream(file);
308 MessageDigest digest = MessageDigest.getInstance("SHA-1");
309 digest.reset();
310 DigestOutputStream mDigestOutputStream = new DigestOutputStream(
311 mFileOutputStream, digest);
312 mDigestOutputStream.write(avatar.getImageAsBytes());
313 mDigestOutputStream.flush();
314 mDigestOutputStream.close();
315 avatar.size = file.length();
316 String sha1sum = CryptoHelper.bytesToHex(digest.digest());
317 if (sha1sum.equals(avatar.sha1sum)) {
318 file.renameTo(new File(filename));
319 return true;
320 } else {
321 Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner);
322 file.delete();
323 return false;
324 }
325 } catch (FileNotFoundException e) {
326 return false;
327 } catch (IOException e) {
328 return false;
329 } catch (NoSuchAlgorithmException e) {
330 return false;
331 }
332 }
333
334 public String getAvatarPath(String avatar) {
335 return mXmppConnectionService.getFilesDir().getAbsolutePath()
336 + "/avatars/" + avatar;
337 }
338
339 public Uri getAvatarUri(String avatar) {
340 return Uri.parse("file:" + getAvatarPath(avatar));
341 }
342
343 public Bitmap cropCenterSquare(Uri image, int size) {
344 try {
345 BitmapFactory.Options options = new BitmapFactory.Options();
346 options.inSampleSize = calcSampleSize(image, size);
347 InputStream is = mXmppConnectionService.getContentResolver()
348 .openInputStream(image);
349 Bitmap input = BitmapFactory.decodeStream(is, null, options);
350 if (input == null) {
351 return null;
352 } else {
353 int rotation = getRotation(image);
354 if (rotation > 0) {
355 input = rotate(input, rotation);
356 }
357 return cropCenterSquare(input, size);
358 }
359 } catch (FileNotFoundException e) {
360 return null;
361 }
362 }
363
364 public Bitmap cropCenter(Uri image, int newHeight, int newWidth) {
365 try {
366 BitmapFactory.Options options = new BitmapFactory.Options();
367 options.inSampleSize = calcSampleSize(image,
368 Math.max(newHeight, newWidth));
369 InputStream is = mXmppConnectionService.getContentResolver()
370 .openInputStream(image);
371 Bitmap source = BitmapFactory.decodeStream(is, null, options);
372
373 int sourceWidth = source.getWidth();
374 int sourceHeight = source.getHeight();
375 float xScale = (float) newWidth / sourceWidth;
376 float yScale = (float) newHeight / sourceHeight;
377 float scale = Math.max(xScale, yScale);
378 float scaledWidth = scale * sourceWidth;
379 float scaledHeight = scale * sourceHeight;
380 float left = (newWidth - scaledWidth) / 2;
381 float top = (newHeight - scaledHeight) / 2;
382
383 RectF targetRect = new RectF(left, top, left + scaledWidth, top
384 + scaledHeight);
385 Bitmap dest = Bitmap.createBitmap(newWidth, newHeight,
386 source.getConfig());
387 Canvas canvas = new Canvas(dest);
388 canvas.drawBitmap(source, null, targetRect, null);
389
390 return dest;
391 } catch (FileNotFoundException e) {
392 return null;
393 }
394
395 }
396
397 public Bitmap cropCenterSquare(Bitmap input, int size) {
398 int w = input.getWidth();
399 int h = input.getHeight();
400
401 float scale = Math.max((float) size / h, (float) size / w);
402
403 float outWidth = scale * w;
404 float outHeight = scale * h;
405 float left = (size - outWidth) / 2;
406 float top = (size - outHeight) / 2;
407 RectF target = new RectF(left, top, left + outWidth, top + outHeight);
408
409 Bitmap output = Bitmap.createBitmap(size, size, input.getConfig());
410 Canvas canvas = new Canvas(output);
411 canvas.drawBitmap(input, null, target, null);
412 return output;
413 }
414
415 private int calcSampleSize(Uri image, int size)
416 throws FileNotFoundException {
417 BitmapFactory.Options options = new BitmapFactory.Options();
418 options.inJustDecodeBounds = true;
419 BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver()
420 .openInputStream(image), null, options);
421 return calcSampleSize(options, size);
422 }
423
424 private int calcSampleSize(File image, int size) {
425 BitmapFactory.Options options = new BitmapFactory.Options();
426 options.inJustDecodeBounds = true;
427 BitmapFactory.decodeFile(image.getAbsolutePath(), options);
428 return calcSampleSize(options, size);
429 }
430
431 private int calcSampleSize(BitmapFactory.Options options, int size) {
432 int height = options.outHeight;
433 int width = options.outWidth;
434 int inSampleSize = 1;
435
436 if (height > size || width > size) {
437 int halfHeight = height / 2;
438 int halfWidth = width / 2;
439
440 while ((halfHeight / inSampleSize) > size
441 && (halfWidth / inSampleSize) > size) {
442 inSampleSize *= 2;
443 }
444 }
445 return inSampleSize;
446 }
447
448 public Uri getJingleFileUri(Message message) {
449 File file = getFile(message);
450 return Uri.parse("file://" + file.getAbsolutePath());
451 }
452
453 public class ImageCopyException extends Exception {
454 private static final long serialVersionUID = -1010013599132881427L;
455 private int resId;
456
457 public ImageCopyException(int resId) {
458 this.resId = resId;
459 }
460
461 public int getResId() {
462 return resId;
463 }
464 }
465
466 public Bitmap getAvatar(String avatar, int size) {
467 if (avatar == null) {
468 return null;
469 }
470 Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
471 if (bm == null) {
472 return null;
473 }
474 return bm;
475 }
476
477 public boolean isFileAvailable(Message message) {
478 return getFile(message).exists();
479 }
480}