FileBackend.java

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