FileBackend.java

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