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