FileBackend.java

   1package eu.siacs.conversations.persistance;
   2
   3import android.annotation.TargetApi;
   4import android.content.ContentResolver;
   5import android.content.Context;
   6import android.content.Intent;
   7import android.database.Cursor;
   8import android.graphics.Bitmap;
   9import android.graphics.BitmapFactory;
  10import android.graphics.Canvas;
  11import android.graphics.Matrix;
  12import android.graphics.Paint;
  13import android.graphics.RectF;
  14import android.media.MediaMetadataRetriever;
  15import android.net.Uri;
  16import android.os.Build;
  17import android.os.Environment;
  18import android.os.ParcelFileDescriptor;
  19import android.provider.MediaStore;
  20import android.provider.OpenableColumns;
  21import android.support.v4.content.FileProvider;
  22import android.system.Os;
  23import android.system.StructStat;
  24import android.util.Base64;
  25import android.util.Base64OutputStream;
  26import android.util.Log;
  27import android.util.LruCache;
  28import android.webkit.MimeTypeMap;
  29
  30import java.io.ByteArrayOutputStream;
  31import java.io.Closeable;
  32import java.io.File;
  33import java.io.FileDescriptor;
  34import java.io.FileInputStream;
  35import java.io.FileNotFoundException;
  36import java.io.FileOutputStream;
  37import java.io.IOException;
  38import java.io.InputStream;
  39import java.io.OutputStream;
  40import java.net.Socket;
  41import java.net.URL;
  42import java.security.DigestOutputStream;
  43import java.security.MessageDigest;
  44import java.security.NoSuchAlgorithmException;
  45import java.text.SimpleDateFormat;
  46import java.util.Date;
  47import java.util.List;
  48import java.util.Locale;
  49
  50import eu.siacs.conversations.Config;
  51import eu.siacs.conversations.R;
  52import eu.siacs.conversations.entities.DownloadableFile;
  53import eu.siacs.conversations.entities.Message;
  54import eu.siacs.conversations.services.XmppConnectionService;
  55import eu.siacs.conversations.utils.CryptoHelper;
  56import eu.siacs.conversations.utils.ExifHelper;
  57import eu.siacs.conversations.utils.FileUtils;
  58import eu.siacs.conversations.utils.FileWriterException;
  59import eu.siacs.conversations.utils.MimeUtils;
  60import eu.siacs.conversations.xmpp.pep.Avatar;
  61
  62public class FileBackend {
  63
  64	private static final Object THUMBNAIL_LOCK = new Object();
  65
  66	private static final SimpleDateFormat IMAGE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
  67
  68	public static final String FILE_PROVIDER = ".files";
  69
  70	private XmppConnectionService mXmppConnectionService;
  71
  72	public FileBackend(XmppConnectionService service) {
  73		this.mXmppConnectionService = service;
  74	}
  75
  76	private void createNoMedia() {
  77		final File nomedia = new File(getConversationsDirectory("Files")+".nomedia");
  78		if (!nomedia.exists()) {
  79			try {
  80				nomedia.createNewFile();
  81			} catch (Exception e) {
  82				Log.d(Config.LOGTAG, "could not create nomedia file");
  83			}
  84		}
  85	}
  86
  87	public void updateMediaScanner(File file) {
  88		String path = file.getAbsolutePath();
  89		if (!path.startsWith(getConversationsDirectory("Files"))) {
  90			Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
  91			intent.setData(Uri.fromFile(file));
  92			mXmppConnectionService.sendBroadcast(intent);
  93		} else {
  94			createNoMedia();
  95		}
  96	}
  97
  98	public boolean deleteFile(Message message) {
  99		File file = getFile(message);
 100		if (file.delete()) {
 101			updateMediaScanner(file);
 102			return true;
 103		} else {
 104			return false;
 105		}
 106	}
 107
 108	public DownloadableFile getFile(Message message) {
 109		return getFile(message, true);
 110	}
 111
 112	public DownloadableFile getFile(Message message, boolean decrypted) {
 113		final boolean encrypted = !decrypted
 114				&& (message.getEncryption() == Message.ENCRYPTION_PGP
 115				|| message.getEncryption() == Message.ENCRYPTION_DECRYPTED);
 116		final DownloadableFile file;
 117		String path = message.getRelativeFilePath();
 118		if (path == null) {
 119			path = message.getUuid();
 120		}
 121		if (path.startsWith("/")) {
 122			file = new DownloadableFile(path);
 123		} else {
 124			String mime = message.getMimeType();
 125			if (mime != null && mime.startsWith("image/")) {
 126				file = new DownloadableFile(getConversationsDirectory("Images") + path);
 127			} else if (mime != null && mime.startsWith("video/")) {
 128				file = new DownloadableFile(getConversationsDirectory("Videos") + path);
 129			} else {
 130				file = new DownloadableFile(getConversationsDirectory("Files") + path);
 131			}
 132		}
 133		if (encrypted) {
 134			return new DownloadableFile(getConversationsDirectory("Files") + file.getName() + ".pgp");
 135		} else {
 136			return file;
 137		}
 138	}
 139
 140	public static long getFileSize(Context context, Uri uri) {
 141		try {
 142			final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
 143			if (cursor != null && cursor.moveToFirst()) {
 144				long size = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE));
 145				cursor.close();
 146				return size;
 147			} else {
 148				return -1;
 149			}
 150		} catch (Exception e) {
 151			return -1;
 152		}
 153	}
 154
 155	public static boolean allFilesUnderSize(Context context, List<Uri> uris, long max) {
 156		if (max <= 0) {
 157			Log.d(Config.LOGTAG,"server did not report max file size for http upload");
 158			return true; //exception to be compatible with HTTP Upload < v0.2
 159		}
 160		for(Uri uri : uris) {
 161			String mime = context.getContentResolver().getType(uri);
 162			if (mime != null && mime.startsWith("video/")) {
 163				try {
 164					Dimensions dimensions = FileBackend.getVideoDimensions(context,uri);
 165					if (dimensions.getMin() > 720) {
 166						Log.d(Config.LOGTAG,"do not consider video file with min width larger than 720 for size check");
 167						continue;
 168					}
 169				} catch (NotAVideoFile notAVideoFile) {
 170					//ignore and fall through
 171				}
 172			}
 173			if (FileBackend.getFileSize(context, uri) > max) {
 174				Log.d(Config.LOGTAG,"not all files are under "+max+" bytes. suggesting falling back to jingle");
 175				return false;
 176			}
 177		}
 178		return true;
 179	}
 180
 181	public String getConversationsDirectory(final String type) {
 182		if (Config.ONLY_INTERNAL_STORAGE) {
 183			return mXmppConnectionService.getFilesDir().getAbsolutePath()+"/"+type+"/";
 184		} else {
 185			return Environment.getExternalStorageDirectory() +"/Conversations/Media/Conversations "+type+"/";
 186		}
 187	}
 188
 189	public static String getConversationsLogsDirectory() {
 190		return  Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/";
 191	}
 192
 193	public Bitmap resize(Bitmap originalBitmap, int size) {
 194		int w = originalBitmap.getWidth();
 195		int h = originalBitmap.getHeight();
 196		if (Math.max(w, h) > size) {
 197			int scalledW;
 198			int scalledH;
 199			if (w <= h) {
 200				scalledW = (int) (w / ((double) h / size));
 201				scalledH = size;
 202			} else {
 203				scalledW = size;
 204				scalledH = (int) (h / ((double) w / size));
 205			}
 206			Bitmap result = Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true);
 207			if (originalBitmap != null && !originalBitmap.isRecycled()) {
 208				originalBitmap.recycle();
 209			}
 210			return result;
 211		} else {
 212			return originalBitmap;
 213		}
 214	}
 215
 216	public static Bitmap rotate(Bitmap bitmap, int degree) {
 217		if (degree == 0) {
 218			return bitmap;
 219		}
 220		int w = bitmap.getWidth();
 221		int h = bitmap.getHeight();
 222		Matrix mtx = new Matrix();
 223		mtx.postRotate(degree);
 224		Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
 225		if (bitmap != null && !bitmap.isRecycled()) {
 226			bitmap.recycle();
 227		}
 228		return result;
 229	}
 230
 231	public boolean useImageAsIs(Uri uri) {
 232		String path = getOriginalPath(uri);
 233		if (path == null) {
 234			return false;
 235		}
 236		File file = new File(path);
 237		long size = file.length();
 238		if (size == 0 || size >= mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize)) {
 239			return false;
 240		}
 241		BitmapFactory.Options options = new BitmapFactory.Options();
 242		options.inJustDecodeBounds = true;
 243		try {
 244			BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri), null, options);
 245			if (options.outMimeType == null || options.outHeight <= 0 || options.outWidth <= 0) {
 246				return false;
 247			}
 248			return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase()));
 249		} catch (FileNotFoundException e) {
 250			return false;
 251		}
 252	}
 253
 254	public String getOriginalPath(Uri uri) {
 255		return FileUtils.getPath(mXmppConnectionService,uri);
 256	}
 257
 258	public void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
 259		Log.d(Config.LOGTAG,"copy file ("+uri.toString()+") to private storage "+file.getAbsolutePath());
 260		file.getParentFile().mkdirs();
 261		OutputStream os = null;
 262		InputStream is = null;
 263		try {
 264			file.createNewFile();
 265			os = new FileOutputStream(file);
 266			is = mXmppConnectionService.getContentResolver().openInputStream(uri);
 267			byte[] buffer = new byte[1024];
 268			int length;
 269			while ((length = is.read(buffer)) > 0) {
 270				try {
 271					os.write(buffer, 0, length);
 272				} catch (IOException e) {
 273					throw new FileWriterException();
 274				}
 275			}
 276			try {
 277				os.flush();
 278			} catch (IOException e) {
 279				throw new FileWriterException();
 280			}
 281		} catch(FileNotFoundException e) {
 282			throw new FileCopyException(R.string.error_file_not_found);
 283		} catch(FileWriterException e) {
 284			throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
 285		} catch (IOException e) {
 286			e.printStackTrace();
 287			throw new FileCopyException(R.string.error_io_exception);
 288		} finally {
 289			close(os);
 290			close(is);
 291		}
 292	}
 293
 294	public void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
 295		String mime = MimeUtils.guessMimeTypeFromUri(mXmppConnectionService, uri);
 296		Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime="+mime+")");
 297		String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
 298		if (extension == null) {
 299			extension = getExtensionFromUri(uri);
 300		}
 301		message.setRelativeFilePath(message.getUuid() + "." + extension);
 302		copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri);
 303	}
 304
 305	private String getExtensionFromUri(Uri uri) {
 306		String[] projection = {MediaStore.MediaColumns.DATA};
 307		String filename = null;
 308		Cursor cursor = mXmppConnectionService.getContentResolver().query(uri, projection, null, null, null);
 309		if (cursor != null) {
 310			try {
 311				if (cursor.moveToFirst()) {
 312					filename = cursor.getString(0);
 313				}
 314			} catch (Exception e) {
 315				filename = null;
 316			} finally {
 317				cursor.close();
 318			}
 319		}
 320		int pos = filename == null ? -1 : filename.lastIndexOf('.');
 321		return pos > 0 ? filename.substring(pos+1) : null;
 322	}
 323
 324	private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException {
 325		file.getParentFile().mkdirs();
 326		InputStream is = null;
 327		OutputStream os = null;
 328		try {
 329			if (!file.exists() && !file.createNewFile()) {
 330				throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
 331			}
 332			is = mXmppConnectionService.getContentResolver().openInputStream(image);
 333			if (is == null) {
 334				throw new FileCopyException(R.string.error_not_an_image_file);
 335			}
 336			Bitmap originalBitmap;
 337			BitmapFactory.Options options = new BitmapFactory.Options();
 338			int inSampleSize = (int) Math.pow(2, sampleSize);
 339			Log.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize);
 340			options.inSampleSize = inSampleSize;
 341			originalBitmap = BitmapFactory.decodeStream(is, null, options);
 342			is.close();
 343			if (originalBitmap == null) {
 344				throw new FileCopyException(R.string.error_not_an_image_file);
 345			}
 346			Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE);
 347			int rotation = getRotation(image);
 348			scaledBitmap = rotate(scaledBitmap, rotation);
 349			boolean targetSizeReached = false;
 350			int quality = Config.IMAGE_QUALITY;
 351			final int imageMaxSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize);
 352			while(!targetSizeReached) {
 353				os = new FileOutputStream(file);
 354				boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, quality, os);
 355				if (!success) {
 356					throw new FileCopyException(R.string.error_compressing_image);
 357				}
 358				os.flush();
 359				targetSizeReached = file.length() <= imageMaxSize|| quality <= 50;
 360				quality -= 5;
 361			}
 362			scaledBitmap.recycle();
 363		} catch (FileNotFoundException e) {
 364			throw new FileCopyException(R.string.error_file_not_found);
 365		} catch (IOException e) {
 366			e.printStackTrace();
 367			throw new FileCopyException(R.string.error_io_exception);
 368		} catch (SecurityException e) {
 369			throw new FileCopyException(R.string.error_security_exception_during_image_copy);
 370		} catch (OutOfMemoryError e) {
 371			++sampleSize;
 372			if (sampleSize <= 3) {
 373				copyImageToPrivateStorage(file, image, sampleSize);
 374			} else {
 375				throw new FileCopyException(R.string.error_out_of_memory);
 376			}
 377		} finally {
 378			close(os);
 379			close(is);
 380		}
 381	}
 382
 383	public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException {
 384		Log.d(Config.LOGTAG,"copy image ("+image.toString()+") to private storage "+file.getAbsolutePath());
 385		copyImageToPrivateStorage(file, image, 0);
 386	}
 387
 388	public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException {
 389		switch(Config.IMAGE_FORMAT) {
 390			case JPEG:
 391				message.setRelativeFilePath(message.getUuid()+".jpg");
 392				break;
 393			case PNG:
 394				message.setRelativeFilePath(message.getUuid()+".png");
 395				break;
 396			case WEBP:
 397				message.setRelativeFilePath(message.getUuid()+".webp");
 398				break;
 399		}
 400		copyImageToPrivateStorage(getFile(message), image);
 401		updateFileParams(message);
 402	}
 403
 404	private int getRotation(File file) {
 405		return getRotation(Uri.parse("file://"+file.getAbsolutePath()));
 406	}
 407
 408	private int getRotation(Uri image) {
 409		InputStream is = null;
 410		try {
 411			is = mXmppConnectionService.getContentResolver().openInputStream(image);
 412			return ExifHelper.getOrientation(is);
 413		} catch (FileNotFoundException e) {
 414			return 0;
 415		} finally {
 416			close(is);
 417		}
 418	}
 419
 420	public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws FileNotFoundException {
 421		final String uuid = message.getUuid();
 422		final LruCache<String,Bitmap> cache = mXmppConnectionService.getBitmapCache();
 423		Bitmap thumbnail = cache.get(uuid);
 424		if ((thumbnail == null) && (!cacheOnly)) {
 425			synchronized (THUMBNAIL_LOCK) {
 426				thumbnail = cache.get(uuid);
 427				if (thumbnail != null) {
 428					return thumbnail;
 429				}
 430				DownloadableFile file = getFile(message);
 431				final String mime = file.getMimeType();
 432				if (mime.startsWith("video/")) {
 433					thumbnail = getVideoPreview(file, size);
 434				} else {
 435					Bitmap fullsize = getFullsizeImagePreview(file, size);
 436					if (fullsize == null) {
 437						throw new FileNotFoundException();
 438					}
 439					thumbnail = resize(fullsize, size);
 440					thumbnail = rotate(thumbnail, getRotation(file));
 441					if (mime.equals("image/gif")) {
 442						Bitmap withGifOverlay = thumbnail.copy(Bitmap.Config.ARGB_8888,true);
 443						drawOverlay(withGifOverlay,R.drawable.play_gif,1.0f);
 444						thumbnail.recycle();
 445						thumbnail = withGifOverlay;
 446					}
 447				}
 448				this.mXmppConnectionService.getBitmapCache().put(uuid, thumbnail);
 449			}
 450		}
 451		return thumbnail;
 452	}
 453
 454	private Bitmap getFullsizeImagePreview(File file, int size) {
 455		BitmapFactory.Options options = new BitmapFactory.Options();
 456		options.inSampleSize = calcSampleSize(file, size);
 457		try {
 458			return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
 459		} catch (OutOfMemoryError e) {
 460			options.inSampleSize *= 2;
 461			return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
 462		}
 463	}
 464
 465	private void drawOverlay(Bitmap bitmap, int resource, float factor) {
 466		Bitmap overlay = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource);
 467		Canvas canvas = new Canvas(bitmap);
 468		Paint paint = new Paint();
 469		paint.setAntiAlias(true);
 470		paint.setFilterBitmap(true);
 471		paint.setDither(true);
 472		float targetSize = Math.min(canvas.getWidth(),canvas.getHeight()) * factor;
 473		Log.d(Config.LOGTAG,"target size overlay: "+targetSize+" overlay bitmap size was "+overlay.getHeight());
 474		float left = (canvas.getWidth() - targetSize) / 2.0f;
 475		float top = (canvas.getHeight() - targetSize) / 2.0f;
 476		RectF dst = new RectF(left,top,left+targetSize-1,top+targetSize-1);
 477		canvas.drawBitmap(overlay,null,dst,paint);
 478	}
 479
 480	private Bitmap getVideoPreview(File file, int size) {
 481		MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
 482		Bitmap frame;
 483		try {
 484			metadataRetriever.setDataSource(file.getAbsolutePath());
 485			frame = metadataRetriever.getFrameAtTime(0);
 486			metadataRetriever.release();
 487			frame = resize(frame, size);
 488		} catch(RuntimeException  e) {
 489			frame = Bitmap.createBitmap(size,size, Bitmap.Config.ARGB_8888);
 490			frame.eraseColor(0xff000000);
 491		}
 492		drawOverlay(frame,R.drawable.play_video,0.75f);
 493		return frame;
 494	}
 495
 496	private static String getTakePhotoPath() {
 497		return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)+"/Camera/";
 498	}
 499
 500	public Uri getTakePhotoUri() {
 501		File file;
 502		if (Config.ONLY_INTERNAL_STORAGE) {
 503			file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath(), "Camera/IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
 504		} else {
 505			file = new File(getTakePhotoPath() + "IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
 506		}
 507		file.getParentFile().mkdirs();
 508		return getUriForFile(mXmppConnectionService,file);
 509	}
 510
 511	public static Uri getUriForFile(Context context, File file) {
 512		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || Config.ONLY_INTERNAL_STORAGE) {
 513			try {
 514				String packageId = context.getPackageName();
 515				return FileProvider.getUriForFile(context, packageId + FILE_PROVIDER, file);
 516			} catch(IllegalArgumentException e) {
 517				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 518					throw new SecurityException(e);
 519				} else {
 520					return Uri.fromFile(file);
 521				}
 522			}
 523		} else {
 524			return Uri.fromFile(file);
 525		}
 526	}
 527
 528	public static Uri getIndexableTakePhotoUri(Uri original) {
 529		if (Config.ONLY_INTERNAL_STORAGE || "file".equals(original.getScheme())) {
 530			return original;
 531		} else {
 532			List<String> segments = original.getPathSegments();
 533			return Uri.parse("file://"+getTakePhotoPath()+segments.get(segments.size() - 1));
 534		}
 535	}
 536
 537	public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
 538		Bitmap bm = cropCenterSquare(image, size);
 539		if (bm == null) {
 540			return null;
 541		}
 542		return getPepAvatar(bm,format,100);
 543	}
 544
 545	private Avatar getPepAvatar(Bitmap bitmap, Bitmap.CompressFormat format, int quality) {
 546		try {
 547			ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
 548			Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
 549			MessageDigest digest = MessageDigest.getInstance("SHA-1");
 550			DigestOutputStream mDigestOutputStream = new DigestOutputStream(mBase64OutputStream, digest);
 551			if (!bitmap.compress(format, quality, mDigestOutputStream)) {
 552				return null;
 553			}
 554			mDigestOutputStream.flush();
 555			mDigestOutputStream.close();
 556			long chars = mByteArrayOutputStream.size();
 557			if (quality >= 50 && chars >= Config.AVATAR_CHAR_LIMIT) {
 558				int q = quality - 2;
 559				Log.d(Config.LOGTAG,"avatar char length was "+chars+" reducing quality to "+q);
 560				return getPepAvatar(bitmap,format,q);
 561			}
 562			Log.d(Config.LOGTAG,"settled on char length "+chars+" with quality="+quality);
 563			final Avatar avatar = new Avatar();
 564			avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
 565			avatar.image = new String(mByteArrayOutputStream.toByteArray());
 566			return avatar;
 567		} catch (Exception e) {
 568			return null;
 569		}
 570	}
 571
 572	public Avatar getStoredPepAvatar(String hash) {
 573		if (hash == null) {
 574			return null;
 575		}
 576		Avatar avatar = new Avatar();
 577		File file = new File(getAvatarPath(hash));
 578		FileInputStream is = null;
 579		try {
 580			avatar.size = file.length();
 581			BitmapFactory.Options options = new BitmapFactory.Options();
 582			options.inJustDecodeBounds = true;
 583			BitmapFactory.decodeFile(file.getAbsolutePath(), options);
 584			is = new FileInputStream(file);
 585			ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
 586			Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
 587			MessageDigest digest = MessageDigest.getInstance("SHA-1");
 588			DigestOutputStream os = new DigestOutputStream(mBase64OutputStream, digest);
 589			byte[] buffer = new byte[4096];
 590			int length;
 591			while ((length = is.read(buffer)) > 0) {
 592				os.write(buffer, 0, length);
 593			}
 594			os.flush();
 595			os.close();
 596			avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
 597			avatar.image = new String(mByteArrayOutputStream.toByteArray());
 598			avatar.height = options.outHeight;
 599			avatar.width = options.outWidth;
 600			avatar.type = options.outMimeType;
 601			return avatar;
 602		} catch (IOException e) {
 603			return null;
 604		} catch (NoSuchAlgorithmException e) {
 605			return null;
 606		} finally {
 607			close(is);
 608		}
 609	}
 610
 611	public boolean isAvatarCached(Avatar avatar) {
 612		File file = new File(getAvatarPath(avatar.getFilename()));
 613		return file.exists();
 614	}
 615
 616	public boolean save(Avatar avatar) {
 617		File file;
 618		if (isAvatarCached(avatar)) {
 619			file = new File(getAvatarPath(avatar.getFilename()));
 620			avatar.size = file.length();
 621		} else {
 622			String filename = getAvatarPath(avatar.getFilename());
 623			file = new File(filename + ".tmp");
 624			file.getParentFile().mkdirs();
 625			OutputStream os = null;
 626			try {
 627				file.createNewFile();
 628				os = new FileOutputStream(file);
 629				MessageDigest digest = MessageDigest.getInstance("SHA-1");
 630				digest.reset();
 631				DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest);
 632				final byte[] bytes = avatar.getImageAsBytes();
 633				mDigestOutputStream.write(bytes);
 634				mDigestOutputStream.flush();
 635				mDigestOutputStream.close();
 636				String sha1sum = CryptoHelper.bytesToHex(digest.digest());
 637				if (sha1sum.equals(avatar.sha1sum)) {
 638					file.renameTo(new File(filename));
 639				} else {
 640					Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner);
 641					file.delete();
 642					return false;
 643				}
 644				avatar.size = bytes.length;
 645			} catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) {
 646				return false;
 647			} finally {
 648				close(os);
 649			}
 650		}
 651		return true;
 652	}
 653
 654	public String getAvatarPath(String avatar) {
 655		return mXmppConnectionService.getFilesDir().getAbsolutePath()+ "/avatars/" + avatar;
 656	}
 657
 658	public Uri getAvatarUri(String avatar) {
 659		return Uri.parse("file:" + getAvatarPath(avatar));
 660	}
 661
 662	public Bitmap cropCenterSquare(Uri image, int size) {
 663		if (image == null) {
 664			return null;
 665		}
 666		InputStream is = null;
 667		try {
 668			BitmapFactory.Options options = new BitmapFactory.Options();
 669			options.inSampleSize = calcSampleSize(image, size);
 670			is = mXmppConnectionService.getContentResolver().openInputStream(image);
 671			if (is == null) {
 672				return null;
 673			}
 674			Bitmap input = BitmapFactory.decodeStream(is, null, options);
 675			if (input == null) {
 676				return null;
 677			} else {
 678				input = rotate(input, getRotation(image));
 679				return cropCenterSquare(input, size);
 680			}
 681		} catch (SecurityException e) {
 682			return null; // happens for example on Android 6.0 if contacts permissions get revoked
 683		} catch (FileNotFoundException e) {
 684			return null;
 685		} finally {
 686			close(is);
 687		}
 688	}
 689
 690	public Bitmap cropCenter(Uri image, int newHeight, int newWidth) {
 691		if (image == null) {
 692			return null;
 693		}
 694		InputStream is = null;
 695		try {
 696			BitmapFactory.Options options = new BitmapFactory.Options();
 697			options.inSampleSize = calcSampleSize(image, Math.max(newHeight, newWidth));
 698			is = mXmppConnectionService.getContentResolver().openInputStream(image);
 699			if (is == null) {
 700				return null;
 701			}
 702			Bitmap source = BitmapFactory.decodeStream(is, null, options);
 703			if (source == null) {
 704				return null;
 705			}
 706			int sourceWidth = source.getWidth();
 707			int sourceHeight = source.getHeight();
 708			float xScale = (float) newWidth / sourceWidth;
 709			float yScale = (float) newHeight / sourceHeight;
 710			float scale = Math.max(xScale, yScale);
 711			float scaledWidth = scale * sourceWidth;
 712			float scaledHeight = scale * sourceHeight;
 713			float left = (newWidth - scaledWidth) / 2;
 714			float top = (newHeight - scaledHeight) / 2;
 715
 716			RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);
 717			Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
 718			Canvas canvas = new Canvas(dest);
 719			Paint p = new Paint();
 720			p.setAntiAlias(true);
 721			p.setFilterBitmap(true);
 722			p.setDither(true);
 723			canvas.drawBitmap(source, null, targetRect, p);
 724			if (source.isRecycled()) {
 725				source.recycle();
 726			}
 727			return dest;
 728		} catch (SecurityException e) {
 729			return null; //android 6.0 with revoked permissions for example
 730		} catch (FileNotFoundException e) {
 731			return null;
 732		} finally {
 733			close(is);
 734		}
 735	}
 736
 737	public Bitmap cropCenterSquare(Bitmap input, int size) {
 738		int w = input.getWidth();
 739		int h = input.getHeight();
 740
 741		float scale = Math.max((float) size / h, (float) size / w);
 742
 743		float outWidth = scale * w;
 744		float outHeight = scale * h;
 745		float left = (size - outWidth) / 2;
 746		float top = (size - outHeight) / 2;
 747		RectF target = new RectF(left, top, left + outWidth, top + outHeight);
 748
 749		Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
 750		Canvas canvas = new Canvas(output);
 751		canvas.drawBitmap(input, null, target, null);
 752		if (input != null && !input.isRecycled()) {
 753			input.recycle();
 754		}
 755		return output;
 756	}
 757
 758	private int calcSampleSize(Uri image, int size) throws FileNotFoundException, SecurityException {
 759		BitmapFactory.Options options = new BitmapFactory.Options();
 760		options.inJustDecodeBounds = true;
 761		BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(image), null, options);
 762		return calcSampleSize(options, size);
 763	}
 764
 765	private static int calcSampleSize(File image, int size) {
 766		BitmapFactory.Options options = new BitmapFactory.Options();
 767		options.inJustDecodeBounds = true;
 768		BitmapFactory.decodeFile(image.getAbsolutePath(), options);
 769		return calcSampleSize(options, size);
 770	}
 771
 772	public static int calcSampleSize(BitmapFactory.Options options, int size) {
 773		int height = options.outHeight;
 774		int width = options.outWidth;
 775		int inSampleSize = 1;
 776
 777		if (height > size || width > size) {
 778			int halfHeight = height / 2;
 779			int halfWidth = width / 2;
 780
 781			while ((halfHeight / inSampleSize) > size
 782					&& (halfWidth / inSampleSize) > size) {
 783				inSampleSize *= 2;
 784			}
 785		}
 786		return inSampleSize;
 787	}
 788
 789	public void updateFileParams(Message message) {
 790		updateFileParams(message,null);
 791	}
 792
 793	public void updateFileParams(Message message, URL url) {
 794		DownloadableFile file = getFile(message);
 795		final String mime = file.getMimeType();
 796		boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/"));
 797		boolean video = mime != null && mime.startsWith("video/");
 798		boolean audio = mime != null && mime.startsWith("audio/");
 799		final StringBuilder body = new StringBuilder();
 800		if (url != null) {
 801			body.append(url.toString());
 802		}
 803		body.append('|').append(file.getSize());
 804		if (image || video) {
 805			try {
 806				Dimensions dimensions = image ? getImageDimensions(file) : getVideoDimensions(file);
 807				body.append('|').append(dimensions.width).append('|').append(dimensions.height);
 808			} catch (NotAVideoFile notAVideoFile) {
 809				Log.d(Config.LOGTAG,"file with mime type "+file.getMimeType()+" was not a video file");
 810				//fall threw
 811			}
 812		} else if (audio) {
 813			body.append("|0|0|").append(getMediaRuntime(file));
 814		}
 815		message.setBody(body.toString());
 816	}
 817
 818	public int getMediaRuntime(Uri uri) {
 819		try {
 820			MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
 821			mediaMetadataRetriever.setDataSource(mXmppConnectionService,uri);
 822			return Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
 823		} catch (RuntimeException  e) {
 824			return 0;
 825		}
 826	}
 827
 828	private int getMediaRuntime(File file) {
 829		try {
 830			MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
 831			mediaMetadataRetriever.setDataSource(file.toString());
 832			return Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
 833		} catch (RuntimeException e) {
 834			return 0;
 835		}
 836	}
 837
 838	private Dimensions getImageDimensions(File file) {
 839		BitmapFactory.Options options = new BitmapFactory.Options();
 840		options.inJustDecodeBounds = true;
 841		BitmapFactory.decodeFile(file.getAbsolutePath(), options);
 842		int rotation = getRotation(file);
 843		boolean rotated = rotation == 90 || rotation == 270;
 844		int imageHeight = rotated ? options.outWidth : options.outHeight;
 845		int imageWidth = rotated ? options.outHeight : options.outWidth;
 846		return new Dimensions(imageHeight, imageWidth);
 847	}
 848
 849	private Dimensions getVideoDimensions(File file) throws NotAVideoFile {
 850		MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
 851		try {
 852			metadataRetriever.setDataSource(file.getAbsolutePath());
 853		} catch (RuntimeException e) {
 854			throw new NotAVideoFile(e);
 855		}
 856		return getVideoDimensions(metadataRetriever);
 857	}
 858
 859	private static Dimensions getVideoDimensions(Context context, Uri uri) throws NotAVideoFile {
 860		MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
 861		try {
 862			mediaMetadataRetriever.setDataSource(context, uri);
 863		} catch (RuntimeException e) {
 864			throw new NotAVideoFile(e);
 865		}
 866		return getVideoDimensions(mediaMetadataRetriever);
 867	}
 868
 869	private static Dimensions getVideoDimensions(MediaMetadataRetriever metadataRetriever) throws NotAVideoFile {
 870		String hasVideo = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
 871		if (hasVideo == null) {
 872			throw new NotAVideoFile();
 873		}
 874		int rotation = extractRotationFromMediaRetriever(metadataRetriever);
 875		boolean rotated = rotation == 90 || rotation == 270;
 876		int height;
 877		try {
 878			String h = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
 879			height = Integer.parseInt(h);
 880		} catch (Exception e) {
 881			height = -1;
 882		}
 883		int width;
 884		try {
 885			String w = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
 886			width = Integer.parseInt(w);
 887		} catch (Exception e) {
 888			width = -1;
 889		}
 890		metadataRetriever.release();
 891		Log.d(Config.LOGTAG,"extracted video dims "+width+"x"+height);
 892		return rotated ? new Dimensions(width, height) : new Dimensions(height, width);
 893	}
 894
 895	private static int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) {
 896		int rotation;
 897		if (Build.VERSION.SDK_INT >= 17) {
 898			String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
 899			try {
 900				rotation = Integer.parseInt(r);
 901			} catch (Exception e) {
 902				rotation = 0;
 903			}
 904		} else {
 905			rotation = 0;
 906		}
 907		return rotation;
 908	}
 909
 910	private static class Dimensions {
 911		public final int width;
 912		public final int height;
 913
 914		public Dimensions(int height, int width) {
 915			this.width = width;
 916			this.height = height;
 917		}
 918
 919		public int getMin() {
 920			return Math.min(width,height);
 921		}
 922	}
 923
 924	private static class NotAVideoFile extends Exception {
 925		public NotAVideoFile(Throwable t) {
 926			super(t);
 927		}
 928
 929		public NotAVideoFile() {
 930			super();
 931		}
 932	}
 933
 934	public class FileCopyException extends Exception {
 935		private static final long serialVersionUID = -1010013599132881427L;
 936		private int resId;
 937
 938		public FileCopyException(int resId) {
 939			this.resId = resId;
 940		}
 941
 942		public int getResId() {
 943			return resId;
 944		}
 945	}
 946
 947	public Bitmap getAvatar(String avatar, int size) {
 948		if (avatar == null) {
 949			return null;
 950		}
 951		Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
 952		if (bm == null) {
 953			return null;
 954		}
 955		return bm;
 956	}
 957
 958	public boolean isFileAvailable(Message message) {
 959		return getFile(message).exists();
 960	}
 961
 962	public static void close(Closeable stream) {
 963		if (stream != null) {
 964			try {
 965				stream.close();
 966			} catch (IOException e) {
 967			}
 968		}
 969	}
 970
 971	public static void close(Socket socket) {
 972		if (socket != null) {
 973			try {
 974				socket.close();
 975			} catch (IOException e) {
 976			}
 977		}
 978	}
 979
 980
 981	public static boolean weOwnFile(Context context, Uri uri) {
 982		if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
 983			return false;
 984		} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
 985			return fileIsInFilesDir(context, uri);
 986		} else {
 987			return weOwnFileLollipop(uri);
 988		}
 989	}
 990
 991
 992	/**
 993	 * This is more than hacky but probably way better than doing nothing
 994	 * Further 'optimizations' might contain to get the parents of CacheDir and NoBackupDir
 995	 * and check against those as well
 996	 */
 997	private static boolean fileIsInFilesDir(Context context, Uri uri) {
 998		try {
 999			final String haystack = context.getFilesDir().getParentFile().getCanonicalPath();
1000			final String needle = new File(uri.getPath()).getCanonicalPath();
1001			return needle.startsWith(haystack);
1002		} catch (IOException e) {
1003			return false;
1004		}
1005	}
1006
1007	@TargetApi(Build.VERSION_CODES.LOLLIPOP)
1008	private static boolean weOwnFileLollipop(Uri uri) {
1009		try {
1010			File file = new File(uri.getPath());
1011			FileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).getFileDescriptor();
1012			StructStat st = Os.fstat(fd);
1013			return st.st_uid == android.os.Process.myUid();
1014		} catch (FileNotFoundException e) {
1015			return false;
1016		} catch (Exception e) {
1017			return true;
1018		}
1019	}
1020}