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		try {
 539			Avatar avatar = new Avatar();
 540			Bitmap bm = cropCenterSquare(image, size);
 541			if (bm == null) {
 542				return null;
 543			}
 544			ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
 545			Base64OutputStream mBase64OutputStream = new Base64OutputStream(
 546					mByteArrayOutputStream, Base64.DEFAULT);
 547			MessageDigest digest = MessageDigest.getInstance("SHA-1");
 548			DigestOutputStream mDigestOutputStream = new DigestOutputStream(
 549					mBase64OutputStream, digest);
 550			if (!bm.compress(format, 75, mDigestOutputStream)) {
 551				return null;
 552			}
 553			mDigestOutputStream.flush();
 554			mDigestOutputStream.close();
 555			avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
 556			avatar.image = new String(mByteArrayOutputStream.toByteArray());
 557			return avatar;
 558		} catch (NoSuchAlgorithmException e) {
 559			return null;
 560		} catch (IOException e) {
 561			return null;
 562		}
 563	}
 564
 565	public Avatar getStoredPepAvatar(String hash) {
 566		if (hash == null) {
 567			return null;
 568		}
 569		Avatar avatar = new Avatar();
 570		File file = new File(getAvatarPath(hash));
 571		FileInputStream is = null;
 572		try {
 573			avatar.size = file.length();
 574			BitmapFactory.Options options = new BitmapFactory.Options();
 575			options.inJustDecodeBounds = true;
 576			BitmapFactory.decodeFile(file.getAbsolutePath(), options);
 577			is = new FileInputStream(file);
 578			ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
 579			Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
 580			MessageDigest digest = MessageDigest.getInstance("SHA-1");
 581			DigestOutputStream os = new DigestOutputStream(mBase64OutputStream, digest);
 582			byte[] buffer = new byte[4096];
 583			int length;
 584			while ((length = is.read(buffer)) > 0) {
 585				os.write(buffer, 0, length);
 586			}
 587			os.flush();
 588			os.close();
 589			avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
 590			avatar.image = new String(mByteArrayOutputStream.toByteArray());
 591			avatar.height = options.outHeight;
 592			avatar.width = options.outWidth;
 593			avatar.type = options.outMimeType;
 594			return avatar;
 595		} catch (IOException e) {
 596			return null;
 597		} catch (NoSuchAlgorithmException e) {
 598			return null;
 599		} finally {
 600			close(is);
 601		}
 602	}
 603
 604	public boolean isAvatarCached(Avatar avatar) {
 605		File file = new File(getAvatarPath(avatar.getFilename()));
 606		return file.exists();
 607	}
 608
 609	public boolean save(Avatar avatar) {
 610		File file;
 611		if (isAvatarCached(avatar)) {
 612			file = new File(getAvatarPath(avatar.getFilename()));
 613			avatar.size = file.length();
 614		} else {
 615			String filename = getAvatarPath(avatar.getFilename());
 616			file = new File(filename + ".tmp");
 617			file.getParentFile().mkdirs();
 618			OutputStream os = null;
 619			try {
 620				file.createNewFile();
 621				os = new FileOutputStream(file);
 622				MessageDigest digest = MessageDigest.getInstance("SHA-1");
 623				digest.reset();
 624				DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest);
 625				final byte[] bytes = avatar.getImageAsBytes();
 626				mDigestOutputStream.write(bytes);
 627				mDigestOutputStream.flush();
 628				mDigestOutputStream.close();
 629				String sha1sum = CryptoHelper.bytesToHex(digest.digest());
 630				if (sha1sum.equals(avatar.sha1sum)) {
 631					file.renameTo(new File(filename));
 632				} else {
 633					Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner);
 634					file.delete();
 635					return false;
 636				}
 637				avatar.size = bytes.length;
 638			} catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) {
 639				return false;
 640			} finally {
 641				close(os);
 642			}
 643		}
 644		return true;
 645	}
 646
 647	public String getAvatarPath(String avatar) {
 648		return mXmppConnectionService.getFilesDir().getAbsolutePath()+ "/avatars/" + avatar;
 649	}
 650
 651	public Uri getAvatarUri(String avatar) {
 652		return Uri.parse("file:" + getAvatarPath(avatar));
 653	}
 654
 655	public Bitmap cropCenterSquare(Uri image, int size) {
 656		if (image == null) {
 657			return null;
 658		}
 659		InputStream is = null;
 660		try {
 661			BitmapFactory.Options options = new BitmapFactory.Options();
 662			options.inSampleSize = calcSampleSize(image, size);
 663			is = mXmppConnectionService.getContentResolver().openInputStream(image);
 664			if (is == null) {
 665				return null;
 666			}
 667			Bitmap input = BitmapFactory.decodeStream(is, null, options);
 668			if (input == null) {
 669				return null;
 670			} else {
 671				input = rotate(input, getRotation(image));
 672				return cropCenterSquare(input, size);
 673			}
 674		} catch (SecurityException e) {
 675			return null; // happens for example on Android 6.0 if contacts permissions get revoked
 676		} catch (FileNotFoundException e) {
 677			return null;
 678		} finally {
 679			close(is);
 680		}
 681	}
 682
 683	public Bitmap cropCenter(Uri image, int newHeight, int newWidth) {
 684		if (image == null) {
 685			return null;
 686		}
 687		InputStream is = null;
 688		try {
 689			BitmapFactory.Options options = new BitmapFactory.Options();
 690			options.inSampleSize = calcSampleSize(image, Math.max(newHeight, newWidth));
 691			is = mXmppConnectionService.getContentResolver().openInputStream(image);
 692			if (is == null) {
 693				return null;
 694			}
 695			Bitmap source = BitmapFactory.decodeStream(is, null, options);
 696			if (source == null) {
 697				return null;
 698			}
 699			int sourceWidth = source.getWidth();
 700			int sourceHeight = source.getHeight();
 701			float xScale = (float) newWidth / sourceWidth;
 702			float yScale = (float) newHeight / sourceHeight;
 703			float scale = Math.max(xScale, yScale);
 704			float scaledWidth = scale * sourceWidth;
 705			float scaledHeight = scale * sourceHeight;
 706			float left = (newWidth - scaledWidth) / 2;
 707			float top = (newHeight - scaledHeight) / 2;
 708
 709			RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);
 710			Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
 711			Canvas canvas = new Canvas(dest);
 712			canvas.drawBitmap(source, null, targetRect, null);
 713			if (source.isRecycled()) {
 714				source.recycle();
 715			}
 716			return dest;
 717		} catch (SecurityException e) {
 718			return null; //android 6.0 with revoked permissions for example
 719		} catch (FileNotFoundException e) {
 720			return null;
 721		} finally {
 722			close(is);
 723		}
 724	}
 725
 726	public Bitmap cropCenterSquare(Bitmap input, int size) {
 727		int w = input.getWidth();
 728		int h = input.getHeight();
 729
 730		float scale = Math.max((float) size / h, (float) size / w);
 731
 732		float outWidth = scale * w;
 733		float outHeight = scale * h;
 734		float left = (size - outWidth) / 2;
 735		float top = (size - outHeight) / 2;
 736		RectF target = new RectF(left, top, left + outWidth, top + outHeight);
 737
 738		Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
 739		Canvas canvas = new Canvas(output);
 740		canvas.drawBitmap(input, null, target, null);
 741		if (input != null && !input.isRecycled()) {
 742			input.recycle();
 743		}
 744		return output;
 745	}
 746
 747	private int calcSampleSize(Uri image, int size) throws FileNotFoundException, SecurityException {
 748		BitmapFactory.Options options = new BitmapFactory.Options();
 749		options.inJustDecodeBounds = true;
 750		BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(image), null, options);
 751		return calcSampleSize(options, size);
 752	}
 753
 754	private static int calcSampleSize(File image, int size) {
 755		BitmapFactory.Options options = new BitmapFactory.Options();
 756		options.inJustDecodeBounds = true;
 757		BitmapFactory.decodeFile(image.getAbsolutePath(), options);
 758		return calcSampleSize(options, size);
 759	}
 760
 761	public static int calcSampleSize(BitmapFactory.Options options, int size) {
 762		int height = options.outHeight;
 763		int width = options.outWidth;
 764		int inSampleSize = 1;
 765
 766		if (height > size || width > size) {
 767			int halfHeight = height / 2;
 768			int halfWidth = width / 2;
 769
 770			while ((halfHeight / inSampleSize) > size
 771					&& (halfWidth / inSampleSize) > size) {
 772				inSampleSize *= 2;
 773			}
 774		}
 775		return inSampleSize;
 776	}
 777
 778	public void updateFileParams(Message message) {
 779		updateFileParams(message,null);
 780	}
 781
 782	public void updateFileParams(Message message, URL url) {
 783		DownloadableFile file = getFile(message);
 784		final String mime = file.getMimeType();
 785		boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/"));
 786		boolean video = mime != null && mime.startsWith("video/");
 787		boolean audio = mime != null && mime.startsWith("audio/");
 788		final StringBuilder body = new StringBuilder();
 789		if (url != null) {
 790			body.append(url.toString());
 791		}
 792		body.append('|').append(file.getSize());
 793		if (image || video) {
 794			try {
 795				Dimensions dimensions = image ? getImageDimensions(file) : getVideoDimensions(file);
 796				body.append('|').append(dimensions.width).append('|').append(dimensions.height);
 797			} catch (NotAVideoFile notAVideoFile) {
 798				Log.d(Config.LOGTAG,"file with mime type "+file.getMimeType()+" was not a video file");
 799				//fall threw
 800			}
 801		} else if (audio) {
 802			body.append("|0|0|").append(getMediaRuntime(file));
 803		}
 804		message.setBody(body.toString());
 805	}
 806
 807	public int getMediaRuntime(Uri uri) {
 808		try {
 809			MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
 810			mediaMetadataRetriever.setDataSource(mXmppConnectionService,uri);
 811			return Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
 812		} catch (RuntimeException  e) {
 813			return 0;
 814		}
 815	}
 816
 817	private int getMediaRuntime(File file) {
 818		try {
 819			MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
 820			mediaMetadataRetriever.setDataSource(file.toString());
 821			return Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
 822		} catch (RuntimeException e) {
 823			return 0;
 824		}
 825	}
 826
 827	private Dimensions getImageDimensions(File file) {
 828		BitmapFactory.Options options = new BitmapFactory.Options();
 829		options.inJustDecodeBounds = true;
 830		BitmapFactory.decodeFile(file.getAbsolutePath(), options);
 831		int rotation = getRotation(file);
 832		boolean rotated = rotation == 90 || rotation == 270;
 833		int imageHeight = rotated ? options.outWidth : options.outHeight;
 834		int imageWidth = rotated ? options.outHeight : options.outWidth;
 835		return new Dimensions(imageHeight, imageWidth);
 836	}
 837
 838	private Dimensions getVideoDimensions(File file) throws NotAVideoFile {
 839		MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
 840		try {
 841			metadataRetriever.setDataSource(file.getAbsolutePath());
 842		} catch (RuntimeException e) {
 843			throw new NotAVideoFile(e);
 844		}
 845		return getVideoDimensions(metadataRetriever);
 846	}
 847
 848	private static Dimensions getVideoDimensions(Context context, Uri uri) throws NotAVideoFile {
 849		MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
 850		try {
 851			mediaMetadataRetriever.setDataSource(context, uri);
 852		} catch (RuntimeException e) {
 853			throw new NotAVideoFile(e);
 854		}
 855		return getVideoDimensions(mediaMetadataRetriever);
 856	}
 857
 858	private static Dimensions getVideoDimensions(MediaMetadataRetriever metadataRetriever) throws NotAVideoFile {
 859		String hasVideo = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
 860		if (hasVideo == null) {
 861			throw new NotAVideoFile();
 862		}
 863		int rotation = extractRotationFromMediaRetriever(metadataRetriever);
 864		boolean rotated = rotation == 90 || rotation == 270;
 865		int height;
 866		try {
 867			String h = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
 868			height = Integer.parseInt(h);
 869		} catch (Exception e) {
 870			height = -1;
 871		}
 872		int width;
 873		try {
 874			String w = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
 875			width = Integer.parseInt(w);
 876		} catch (Exception e) {
 877			width = -1;
 878		}
 879		metadataRetriever.release();
 880		Log.d(Config.LOGTAG,"extracted video dims "+width+"x"+height);
 881		return rotated ? new Dimensions(width, height) : new Dimensions(height, width);
 882	}
 883
 884	private static int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) {
 885		int rotation;
 886		if (Build.VERSION.SDK_INT >= 17) {
 887			String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
 888			try {
 889				rotation = Integer.parseInt(r);
 890			} catch (Exception e) {
 891				rotation = 0;
 892			}
 893		} else {
 894			rotation = 0;
 895		}
 896		return rotation;
 897	}
 898
 899	private static class Dimensions {
 900		public final int width;
 901		public final int height;
 902
 903		public Dimensions(int height, int width) {
 904			this.width = width;
 905			this.height = height;
 906		}
 907
 908		public int getMin() {
 909			return Math.min(width,height);
 910		}
 911	}
 912
 913	private static class NotAVideoFile extends Exception {
 914		public NotAVideoFile(Throwable t) {
 915			super(t);
 916		}
 917
 918		public NotAVideoFile() {
 919			super();
 920		}
 921	}
 922
 923	public class FileCopyException extends Exception {
 924		private static final long serialVersionUID = -1010013599132881427L;
 925		private int resId;
 926
 927		public FileCopyException(int resId) {
 928			this.resId = resId;
 929		}
 930
 931		public int getResId() {
 932			return resId;
 933		}
 934	}
 935
 936	public Bitmap getAvatar(String avatar, int size) {
 937		if (avatar == null) {
 938			return null;
 939		}
 940		Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
 941		if (bm == null) {
 942			return null;
 943		}
 944		return bm;
 945	}
 946
 947	public boolean isFileAvailable(Message message) {
 948		return getFile(message).exists();
 949	}
 950
 951	public static void close(Closeable stream) {
 952		if (stream != null) {
 953			try {
 954				stream.close();
 955			} catch (IOException e) {
 956			}
 957		}
 958	}
 959
 960	public static void close(Socket socket) {
 961		if (socket != null) {
 962			try {
 963				socket.close();
 964			} catch (IOException e) {
 965			}
 966		}
 967	}
 968
 969
 970	public static boolean weOwnFile(Context context, Uri uri) {
 971		if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
 972			return false;
 973		} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
 974			return fileIsInFilesDir(context, uri);
 975		} else {
 976			return weOwnFileLollipop(uri);
 977		}
 978	}
 979
 980
 981	/**
 982	 * This is more than hacky but probably way better than doing nothing
 983	 * Further 'optimizations' might contain to get the parents of CacheDir and NoBackupDir
 984	 * and check against those as well
 985	 */
 986	private static boolean fileIsInFilesDir(Context context, Uri uri) {
 987		try {
 988			final String haystack = context.getFilesDir().getParentFile().getCanonicalPath();
 989			final String needle = new File(uri.getPath()).getCanonicalPath();
 990			return needle.startsWith(haystack);
 991		} catch (IOException e) {
 992			return false;
 993		}
 994	}
 995
 996	@TargetApi(Build.VERSION_CODES.LOLLIPOP)
 997	private static boolean weOwnFileLollipop(Uri uri) {
 998		try {
 999			File file = new File(uri.getPath());
1000			FileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).getFileDescriptor();
1001			StructStat st = Os.fstat(fd);
1002			return st.st_uid == android.os.Process.myUid();
1003		} catch (FileNotFoundException e) {
1004			return false;
1005		} catch (Exception e) {
1006			return true;
1007		}
1008	}
1009}