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