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