1package eu.siacs.conversations.persistance;
2
3import java.io.ByteArrayOutputStream;
4import java.io.File;
5import java.io.FileInputStream;
6import java.io.FileNotFoundException;
7import java.io.FileOutputStream;
8import java.io.IOException;
9import java.io.InputStream;
10import java.io.OutputStream;
11import java.net.URL;
12import java.net.URLConnection;
13import java.security.DigestOutputStream;
14import java.security.MessageDigest;
15import java.security.NoSuchAlgorithmException;
16import java.text.SimpleDateFormat;
17import java.util.Date;
18import java.util.Locale;
19
20import android.content.ContentResolver;
21import android.database.Cursor;
22import android.graphics.Bitmap;
23import android.graphics.BitmapFactory;
24import android.graphics.Canvas;
25import android.graphics.Matrix;
26import android.graphics.RectF;
27import android.net.Uri;
28import android.os.Environment;
29import android.provider.MediaStore;
30import android.util.Base64;
31import android.util.Base64OutputStream;
32import android.util.Log;
33import android.webkit.MimeTypeMap;
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.services.XmppConnectionService;
40import eu.siacs.conversations.utils.CryptoHelper;
41import eu.siacs.conversations.utils.ExifHelper;
42import eu.siacs.conversations.xmpp.pep.Avatar;
43
44public class FileBackend {
45
46 private static int IMAGE_SIZE = 1920;
47
48 private SimpleDateFormat imageDateFormat = new SimpleDateFormat(
49 "yyyyMMdd_HHmmssSSS", Locale.US);
50
51 private XmppConnectionService mXmppConnectionService;
52
53 public FileBackend(XmppConnectionService service) {
54 this.mXmppConnectionService = service;
55 }
56
57 public DownloadableFile getFile(Message message) {
58 return getFile(message, true);
59 }
60
61 public DownloadableFile getFile(Message message, boolean decrypted) {
62 String path = message.getRelativeFilePath();
63 if (path != null && !path.isEmpty()) {
64 if (path.startsWith("/")) {
65 return new DownloadableFile(path);
66 } else {
67 return new DownloadableFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)+"/"+path);
68 }
69 } else {
70 StringBuilder filename = new StringBuilder();
71 filename.append(getConversationsDirectory());
72 filename.append(message.getUuid());
73 if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) {
74 filename.append(".webp");
75 } else {
76 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
77 filename.append(".webp");
78 } else {
79 filename.append(".webp.pgp");
80 }
81 }
82 return new DownloadableFile(filename.toString());
83 }
84 }
85
86 public static String getConversationsDirectory() {
87 return Environment.getExternalStoragePublicDirectory(
88 Environment.DIRECTORY_PICTURES).getAbsolutePath()
89 + "/Conversations/";
90 }
91
92 public Bitmap resize(Bitmap originalBitmap, int size) {
93 int w = originalBitmap.getWidth();
94 int h = originalBitmap.getHeight();
95 if (Math.max(w, h) > size) {
96 int scalledW;
97 int scalledH;
98 if (w <= h) {
99 scalledW = (int) (w / ((double) h / size));
100 scalledH = size;
101 } else {
102 scalledW = size;
103 scalledH = (int) (h / ((double) w / size));
104 }
105 Bitmap scalledBitmap = Bitmap.createScaledBitmap(originalBitmap,
106 scalledW, scalledH, true);
107 return scalledBitmap;
108 } else {
109 return originalBitmap;
110 }
111 }
112
113 public Bitmap rotate(Bitmap bitmap, int degree) {
114 int w = bitmap.getWidth();
115 int h = bitmap.getHeight();
116 Matrix mtx = new Matrix();
117 mtx.postRotate(degree);
118 return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
119 }
120
121 public String getOriginalPath(Uri uri) {
122 String path = null;
123 if (uri.getScheme().equals("file")) {
124 return uri.getPath();
125 } else if (uri.toString().startsWith("content://media/")) {
126 String[] projection = {MediaStore.MediaColumns.DATA};
127 Cursor metaCursor = mXmppConnectionService.getContentResolver().query(uri,
128 projection, null, null, null);
129 if (metaCursor != null) {
130 try {
131 if (metaCursor.moveToFirst()) {
132 path = metaCursor.getString(0);
133 }
134 } finally {
135 metaCursor.close();
136 }
137 }
138 }
139 return path;
140 }
141
142 public DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
143 try {
144 Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage");
145 String mime = mXmppConnectionService.getContentResolver().getType(uri);
146 String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
147 message.setRelativeFilePath(message.getUuid() + "." + extension);
148 DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
149 OutputStream os = new FileOutputStream(file);
150 InputStream is = mXmppConnectionService.getContentResolver().openInputStream(uri);
151 byte[] buffer = new byte[1024];
152 int length;
153 while ((length = is.read(buffer)) > 0) {
154 os.write(buffer, 0, length);
155 }
156 os.flush();
157 os.close();
158 is.close();
159 Log.d(Config.LOGTAG, "output file name " + mXmppConnectionService.getFileBackend().getFile(message));
160 return file;
161 } catch (FileNotFoundException e) {
162 throw new FileCopyException(R.string.error_file_not_found);
163 } catch (IOException e) {
164 throw new FileCopyException(R.string.error_io_exception);
165 }
166 }
167
168 public DownloadableFile copyImageToPrivateStorage(Message message, Uri image)
169 throws FileCopyException {
170 return this.copyImageToPrivateStorage(message, image, 0);
171 }
172
173 private DownloadableFile copyImageToPrivateStorage(Message message,
174 Uri image, int sampleSize) throws FileCopyException {
175 try {
176 InputStream is = mXmppConnectionService.getContentResolver()
177 .openInputStream(image);
178 DownloadableFile file = getFile(message);
179 file.getParentFile().mkdirs();
180 file.createNewFile();
181 Bitmap originalBitmap;
182 BitmapFactory.Options options = new BitmapFactory.Options();
183 int inSampleSize = (int) Math.pow(2, sampleSize);
184 Log.d(Config.LOGTAG, "reading bitmap with sample size "
185 + inSampleSize);
186 options.inSampleSize = inSampleSize;
187 originalBitmap = BitmapFactory.decodeStream(is, null, options);
188 is.close();
189 if (originalBitmap == null) {
190 throw new FileCopyException(R.string.error_not_an_image_file);
191 }
192 Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE);
193 originalBitmap = null;
194 int rotation = getRotation(image);
195 if (rotation > 0) {
196 scalledBitmap = rotate(scalledBitmap, rotation);
197 }
198 OutputStream os = new FileOutputStream(file);
199 boolean success = scalledBitmap.compress(
200 Bitmap.CompressFormat.WEBP, 75, os);
201 if (!success) {
202 throw new FileCopyException(R.string.error_compressing_image);
203 }
204 os.flush();
205 os.close();
206 long size = file.getSize();
207 int width = scalledBitmap.getWidth();
208 int height = scalledBitmap.getHeight();
209 message.setBody(Long.toString(size) + ',' + width + ',' + height);
210 return file;
211 } catch (FileNotFoundException e) {
212 throw new FileCopyException(R.string.error_file_not_found);
213 } catch (IOException e) {
214 throw new FileCopyException(R.string.error_io_exception);
215 } catch (SecurityException e) {
216 throw new FileCopyException(
217 R.string.error_security_exception_during_image_copy);
218 } catch (OutOfMemoryError e) {
219 ++sampleSize;
220 if (sampleSize <= 3) {
221 return copyImageToPrivateStorage(message, image, sampleSize);
222 } else {
223 throw new FileCopyException(R.string.error_out_of_memory);
224 }
225 }
226 }
227
228 private int getRotation(Uri image) {
229 try {
230 InputStream is = mXmppConnectionService.getContentResolver()
231 .openInputStream(image);
232 return ExifHelper.getOrientation(is);
233 } catch (FileNotFoundException e) {
234 return 0;
235 }
236 }
237
238 public Bitmap getImageFromMessage(Message message) {
239 return BitmapFactory.decodeFile(getFile(message).getAbsolutePath());
240 }
241
242 public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
243 throws FileNotFoundException {
244 Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(
245 message.getUuid());
246 if ((thumbnail == null) && (!cacheOnly)) {
247 File file = getFile(message);
248 BitmapFactory.Options options = new BitmapFactory.Options();
249 options.inSampleSize = calcSampleSize(file, size);
250 Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),
251 options);
252 if (fullsize == null) {
253 throw new FileNotFoundException();
254 }
255 thumbnail = resize(fullsize, size);
256 this.mXmppConnectionService.getBitmapCache().put(message.getUuid(),
257 thumbnail);
258 }
259 return thumbnail;
260 }
261
262 public Uri getTakePhotoUri() {
263 StringBuilder pathBuilder = new StringBuilder();
264 pathBuilder.append(Environment
265 .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
266 pathBuilder.append('/');
267 pathBuilder.append("Camera");
268 pathBuilder.append('/');
269 pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date())
270 + ".jpg");
271 Uri uri = Uri.parse("file://" + pathBuilder.toString());
272 File file = new File(uri.toString());
273 file.getParentFile().mkdirs();
274 return uri;
275 }
276
277 public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
278 try {
279 Avatar avatar = new Avatar();
280 Bitmap bm = cropCenterSquare(image, size);
281 if (bm == null) {
282 return null;
283 }
284 ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
285 Base64OutputStream mBase64OutputSttream = new Base64OutputStream(
286 mByteArrayOutputStream, Base64.DEFAULT);
287 MessageDigest digest = MessageDigest.getInstance("SHA-1");
288 DigestOutputStream mDigestOutputStream = new DigestOutputStream(
289 mBase64OutputSttream, digest);
290 if (!bm.compress(format, 75, mDigestOutputStream)) {
291 return null;
292 }
293 mDigestOutputStream.flush();
294 mDigestOutputStream.close();
295 avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
296 avatar.image = new String(mByteArrayOutputStream.toByteArray());
297 return avatar;
298 } catch (NoSuchAlgorithmException e) {
299 return null;
300 } catch (IOException e) {
301 return null;
302 }
303 }
304
305 public boolean isAvatarCached(Avatar avatar) {
306 File file = new File(getAvatarPath(avatar.getFilename()));
307 return file.exists();
308 }
309
310 public boolean save(Avatar avatar) {
311 if (isAvatarCached(avatar)) {
312 return true;
313 }
314 String filename = getAvatarPath(avatar.getFilename());
315 File file = new File(filename + ".tmp");
316 file.getParentFile().mkdirs();
317 try {
318 file.createNewFile();
319 FileOutputStream mFileOutputStream = new FileOutputStream(file);
320 MessageDigest digest = MessageDigest.getInstance("SHA-1");
321 digest.reset();
322 DigestOutputStream mDigestOutputStream = new DigestOutputStream(
323 mFileOutputStream, digest);
324 mDigestOutputStream.write(avatar.getImageAsBytes());
325 mDigestOutputStream.flush();
326 mDigestOutputStream.close();
327 avatar.size = file.length();
328 String sha1sum = CryptoHelper.bytesToHex(digest.digest());
329 if (sha1sum.equals(avatar.sha1sum)) {
330 file.renameTo(new File(filename));
331 return true;
332 } else {
333 Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner);
334 file.delete();
335 return false;
336 }
337 } catch (FileNotFoundException e) {
338 return false;
339 } catch (IOException e) {
340 return false;
341 } catch (NoSuchAlgorithmException e) {
342 return false;
343 }
344 }
345
346 public String getAvatarPath(String avatar) {
347 return mXmppConnectionService.getFilesDir().getAbsolutePath()
348 + "/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 = mXmppConnectionService.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 = mXmppConnectionService.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(mXmppConnectionService.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 void updateFileParams(Message message) {
466 updateFileParams(message,null);
467 }
468
469 public void updateFileParams(Message message, URL url) {
470 DownloadableFile file = getFile(message);
471 if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) {
472 BitmapFactory.Options options = new BitmapFactory.Options();
473 options.inJustDecodeBounds = true;
474 BitmapFactory.decodeFile(file.getAbsolutePath(), options);
475 int imageHeight = options.outHeight;
476 int imageWidth = options.outWidth;
477 if (url == null) {
478 message.setBody(Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
479 } else {
480 message.setBody(url.toString()+"|"+Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
481 }
482 } else {
483 message.setBody(Long.toString(file.getSize()));
484 }
485
486 }
487
488 public class FileCopyException extends Exception {
489 private static final long serialVersionUID = -1010013599132881427L;
490 private int resId;
491
492 public FileCopyException(int resId) {
493 this.resId = resId;
494 }
495
496 public int getResId() {
497 return resId;
498 }
499 }
500
501 public Bitmap getAvatar(String avatar, int size) {
502 if (avatar == null) {
503 return null;
504 }
505 Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
506 if (bm == null) {
507 return null;
508 }
509 return bm;
510 }
511
512 public boolean isFileAvailable(Message message) {
513 return getFile(message).exists();
514 }
515}