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