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