FileBackend.java

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