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.utils.UIHelper;
39import eu.siacs.conversations.xmpp.pep.Avatar;
40
41public class FileBackend {
42
43 private static int IMAGE_SIZE = 1920;
44
45 private Context context;
46 private LruCache<String, Bitmap> thumbnailCache;
47
48 private SimpleDateFormat imageDateFormat = new SimpleDateFormat(
49 "yyyyMMdd_HHmmssSSS", Locale.US);
50
51 public FileBackend(Context context) {
52 this.context = context;
53 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
54 int cacheSize = maxMemory / 8;
55 thumbnailCache = new LruCache<String, Bitmap>(cacheSize) {
56 @Override
57 protected int sizeOf(String key, Bitmap bitmap) {
58 return bitmap.getByteCount() / 1024;
59 }
60 };
61
62 }
63
64 public LruCache<String, Bitmap> getThumbnailCache() {
65 return thumbnailCache;
66 }
67
68 public DownloadableFile getFile(Message message) {
69 return getFile(message, true);
70 }
71
72 public DownloadableFile getFile(Message message,
73 boolean decrypted) {
74 StringBuilder filename = new StringBuilder();
75 filename.append(Environment.getExternalStoragePublicDirectory(
76 Environment.DIRECTORY_PICTURES).getAbsolutePath());
77 filename.append("/Conversations/");
78 filename.append(message.getUuid());
79 if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) {
80 filename.append(".webp");
81 } else {
82 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
83 filename.append(".webp");
84 } else {
85 filename.append(".webp.pgp");
86 }
87 }
88 return new DownloadableFile(filename.toString());
89 }
90
91 public Bitmap resize(Bitmap originalBitmap, int size) {
92 int w = originalBitmap.getWidth();
93 int h = originalBitmap.getHeight();
94 if (Math.max(w, h) > size) {
95 int scalledW;
96 int scalledH;
97 if (w <= h) {
98 scalledW = (int) (w / ((double) h / size));
99 scalledH = size;
100 } else {
101 scalledW = size;
102 scalledH = (int) (h / ((double) w / size));
103 }
104 Bitmap scalledBitmap = Bitmap.createScaledBitmap(originalBitmap,
105 scalledW, scalledH, true);
106 return scalledBitmap;
107 } else {
108 return originalBitmap;
109 }
110 }
111
112 public Bitmap rotate(Bitmap bitmap, int degree) {
113 int w = bitmap.getWidth();
114 int h = bitmap.getHeight();
115 Matrix mtx = new Matrix();
116 mtx.postRotate(degree);
117 return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
118 }
119
120 public DownloadableFile copyImageToPrivateStorage(Message message, Uri image)
121 throws ImageCopyException {
122 return this.copyImageToPrivateStorage(message, image, 0);
123 }
124
125 private DownloadableFile copyImageToPrivateStorage(Message message,
126 Uri image, int sampleSize) throws ImageCopyException {
127 try {
128 InputStream is = context.getContentResolver()
129 .openInputStream(image);
130 DownloadableFile file = getFile(message);
131 file.getParentFile().mkdirs();
132 file.createNewFile();
133 Bitmap originalBitmap;
134 BitmapFactory.Options options = new BitmapFactory.Options();
135 int inSampleSize = (int) Math.pow(2, sampleSize);
136 Log.d(Config.LOGTAG, "reading bitmap with sample size "
137 + inSampleSize);
138 options.inSampleSize = inSampleSize;
139 originalBitmap = BitmapFactory.decodeStream(is, null, options);
140 is.close();
141 if (originalBitmap == null) {
142 throw new ImageCopyException(R.string.error_not_an_image_file);
143 }
144 Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE);
145 originalBitmap = null;
146 int rotation = getRotation(image);
147 if (rotation > 0) {
148 scalledBitmap = rotate(scalledBitmap, rotation);
149 }
150 OutputStream os = new FileOutputStream(file);
151 boolean success = scalledBitmap.compress(
152 Bitmap.CompressFormat.WEBP, 75, os);
153 if (!success) {
154 throw new ImageCopyException(R.string.error_compressing_image);
155 }
156 os.flush();
157 os.close();
158 long size = file.getSize();
159 int width = scalledBitmap.getWidth();
160 int height = scalledBitmap.getHeight();
161 message.setBody(Long.toString(size) + ',' + width + ',' + height);
162 return file;
163 } catch (FileNotFoundException e) {
164 throw new ImageCopyException(R.string.error_file_not_found);
165 } catch (IOException e) {
166 throw new ImageCopyException(R.string.error_io_exception);
167 } catch (SecurityException e) {
168 throw new ImageCopyException(
169 R.string.error_security_exception_during_image_copy);
170 } catch (OutOfMemoryError e) {
171 ++sampleSize;
172 if (sampleSize <= 3) {
173 return copyImageToPrivateStorage(message, image, sampleSize);
174 } else {
175 throw new ImageCopyException(R.string.error_out_of_memory);
176 }
177 }
178 }
179
180 private int getRotation(Uri image) {
181 if ("content".equals(image.getScheme())) {
182 try {
183 Cursor cursor = context
184 .getContentResolver()
185 .query(image,
186 new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
187 null, null, null);
188 if (cursor.getCount() != 1) {
189 return -1;
190 }
191 cursor.moveToFirst();
192 return cursor.getInt(0);
193 } catch (IllegalArgumentException e) {
194 return -1;
195 }
196 } else {
197 ExifInterface exif;
198 try {
199 exif = new ExifInterface(image.toString());
200 if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
201 .equalsIgnoreCase("6")) {
202 return 90;
203 } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
204 .equalsIgnoreCase("8")) {
205 return 270;
206 } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
207 .equalsIgnoreCase("3")) {
208 return 180;
209 } else {
210 return 0;
211 }
212 } catch (IOException e) {
213 return -1;
214 }
215 }
216 }
217
218 public Bitmap getImageFromMessage(Message message) {
219 return BitmapFactory.decodeFile(getFile(message)
220 .getAbsolutePath());
221 }
222
223 public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
224 throws FileNotFoundException {
225 Bitmap thumbnail = thumbnailCache.get(message.getUuid());
226 if ((thumbnail == null) && (!cacheOnly)) {
227 File file = getFile(message);
228 BitmapFactory.Options options = new BitmapFactory.Options();
229 options.inSampleSize = calcSampleSize(file, size);
230 Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),
231 options);
232 if (fullsize == null) {
233 throw new FileNotFoundException();
234 }
235 thumbnail = resize(fullsize, size);
236 this.thumbnailCache.put(message.getUuid(), thumbnail);
237 }
238 return thumbnail;
239 }
240
241 public void removeFiles(Conversation conversation) {
242 String prefix = context.getFilesDir().getAbsolutePath();
243 String path = prefix + "/" + conversation.getAccount().getJid() + "/"
244 + conversation.getContactJid();
245 File file = new File(path);
246 try {
247 this.deleteFile(file);
248 } catch (IOException e) {
249 Log.d(Config.LOGTAG,
250 "error deleting file: " + file.getAbsolutePath());
251 }
252 }
253
254 private void deleteFile(File f) throws IOException {
255 if (f.isDirectory()) {
256 for (File c : f.listFiles())
257 deleteFile(c);
258 }
259 f.delete();
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(context, 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(context, 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 static String getAvatarPath(Context context, String avatar) {
347 return context.getFilesDir().getAbsolutePath() + "/avatars/" + avatar;
348 }
349
350 public Bitmap cropCenterSquare(Uri image, int size) {
351 try {
352 BitmapFactory.Options options = new BitmapFactory.Options();
353 options.inSampleSize = calcSampleSize(image, size);
354 InputStream is = context.getContentResolver()
355 .openInputStream(image);
356 Bitmap input = BitmapFactory.decodeStream(is, null, options);
357 if (input == null) {
358 return null;
359 } else {
360 int rotation = getRotation(image);
361 if (rotation > 0) {
362 input = rotate(input, rotation);
363 }
364 return cropCenterSquare(input, size);
365 }
366 } catch (FileNotFoundException e) {
367 return null;
368 }
369 }
370
371 public static Bitmap cropCenterSquare(Bitmap input, int size) {
372 int w = input.getWidth();
373 int h = input.getHeight();
374
375 float scale = Math.max((float) size / h, (float) size / w);
376
377 float outWidth = scale * w;
378 float outHeight = scale * h;
379 float left = (size - outWidth) / 2;
380 float top = (size - outHeight) / 2;
381 RectF target = new RectF(left, top, left + outWidth, top + outHeight);
382
383 Bitmap output = Bitmap.createBitmap(size, size, input.getConfig());
384 Canvas canvas = new Canvas(output);
385 canvas.drawBitmap(input, null, target, null);
386 return output;
387 }
388
389 private int calcSampleSize(Uri image, int size)
390 throws FileNotFoundException {
391 BitmapFactory.Options options = new BitmapFactory.Options();
392 options.inJustDecodeBounds = true;
393 BitmapFactory.decodeStream(context.getContentResolver()
394 .openInputStream(image), null, options);
395 return calcSampleSize(options, size);
396 }
397
398 private int calcSampleSize(File image, int size) {
399 BitmapFactory.Options options = new BitmapFactory.Options();
400 options.inJustDecodeBounds = true;
401 BitmapFactory.decodeFile(image.getAbsolutePath(), options);
402 return calcSampleSize(options, size);
403 }
404
405 private int calcSampleSize(BitmapFactory.Options options, int size) {
406 int height = options.outHeight;
407 int width = options.outWidth;
408 int inSampleSize = 1;
409
410 if (height > size || width > size) {
411 int halfHeight = height / 2;
412 int halfWidth = width / 2;
413
414 while ((halfHeight / inSampleSize) > size
415 && (halfWidth / inSampleSize) > size) {
416 inSampleSize *= 2;
417 }
418 }
419 return inSampleSize;
420 }
421
422 public Uri getJingleFileUri(Message message) {
423 File file = getFile(message);
424 return Uri.parse("file://" + file.getAbsolutePath());
425 }
426
427 public class ImageCopyException extends Exception {
428 private static final long serialVersionUID = -1010013599132881427L;
429 private int resId;
430
431 public ImageCopyException(int resId) {
432 this.resId = resId;
433 }
434
435 public int getResId() {
436 return resId;
437 }
438 }
439
440 public static Bitmap getAvatar(String avatar, int size, Context context) {
441 Bitmap bm = BitmapFactory.decodeFile(FileBackend.getAvatarPath(context,
442 avatar));
443 if (bm == null) {
444 return null;
445 }
446 return cropCenterSquare(bm, UIHelper.getRealPx(size, context));
447 }
448
449 public boolean isFileAvailable(Message message) {
450 return getFile(message).exists();
451 }
452}