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