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