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