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