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