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