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 void save(Avatar avatar) {
252		File file = new File(getAvatarPath(context, avatar.getFilename()));
253		file.getParentFile().mkdirs();
254		Log.d("xmppService",file.getAbsolutePath());
255		try {
256			file.createNewFile();
257			FileOutputStream mFileOutputStream = new FileOutputStream(file);
258			MessageDigest digest = MessageDigest.getInstance("SHA-1");
259			DigestOutputStream mDigestOutputStream = new DigestOutputStream(mFileOutputStream, digest);
260			mDigestOutputStream.write(avatar.getImageAsBytes());
261			mDigestOutputStream.flush();
262			mDigestOutputStream.close();
263			avatar.size = file.length();
264		} catch (FileNotFoundException e) {
265			
266		} catch (IOException e) {
267			Log.d("xmppService",e.getMessage());
268		} catch (NoSuchAlgorithmException e) {
269			// TODO Auto-generated catch block
270			e.printStackTrace();
271		}
272	}
273	
274	public static String getAvatarPath(Context context, String avatar) {
275		return context.getFilesDir().getAbsolutePath() + "/avatars/"+avatar;
276	}
277
278	public Bitmap cropCenterSquare(Uri image, int size) {
279		try {
280			BitmapFactory.Options options = new BitmapFactory.Options();
281			options.inSampleSize = calcSampleSize(image, size);
282			InputStream is = context.getContentResolver()
283					.openInputStream(image);
284			Bitmap input = BitmapFactory.decodeStream(is, null, options);
285			int w = input.getWidth();
286			int h = input.getHeight();
287
288			float scale = Math.max((float) size / h, (float) size / w);
289
290			float outWidth = scale * w;
291			float outHeight = scale * h;
292			float left = (size - outWidth) / 2;
293			float top = (size - outHeight) / 2;
294			RectF target = new RectF(left, top, left + outWidth, top
295					+ outHeight);
296
297			Bitmap output = Bitmap.createBitmap(size, size, input.getConfig());
298			Canvas canvas = new Canvas(output);
299			canvas.drawBitmap(input, null, target, null);
300			return output;
301		} catch (FileNotFoundException e) {
302			return null;
303		}
304	}
305
306	private int calcSampleSize(Uri image, int size)
307			throws FileNotFoundException {
308		BitmapFactory.Options options = new BitmapFactory.Options();
309		options.inJustDecodeBounds = true;
310		BitmapFactory.decodeStream(context.getContentResolver()
311				.openInputStream(image), null, options);
312		int height = options.outHeight;
313		int width = options.outWidth;
314		int inSampleSize = 1;
315
316		if (height > size || width > size) {
317			int halfHeight = height / 2;
318			int halfWidth = width / 2;
319
320			while ((halfHeight / inSampleSize) > size
321					&& (halfWidth / inSampleSize) > size) {
322				inSampleSize *= 2;
323			}
324		}
325		return inSampleSize;
326
327	}
328
329	public class ImageCopyException extends Exception {
330		private static final long serialVersionUID = -1010013599132881427L;
331		private int resId;
332
333		public ImageCopyException(int resId) {
334			this.resId = resId;
335		}
336
337		public int getResId() {
338			return resId;
339		}
340	}
341}