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