Java tutorial
/* * Kontalk Android client * Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org> * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kontalk.util; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URLConnection; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.MediaStore; import android.support.media.ExifInterface; import android.support.v4.app.Fragment; import android.webkit.MimeTypeMap; import org.kontalk.Kontalk; import org.kontalk.Log; /** * Media storage utilities. * @author Daniele Ricci */ public abstract class MediaStorage { private static final String TAG = Kontalk.TAG; public static final String UNKNOWN_FILENAME = "unknown_file.bin"; private static final File DCIM_ROOT = new File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "Kontalk"); private static final File PICTURES_ROOT = new File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Kontalk"); private static final File PICTURES_SENT_ROOT = new File( new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Kontalk"), "Sent"); private static final File AUDIO_ROOT = new File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC), "Kontalk"); private static final File AUDIO_SENT_ROOT = new File( new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC), "Kontalk"), "Sent"); private static final File DOWNLOADS_ROOT = new File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "Kontalk"); private static final DateFormat sDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); private static final int THUMBNAIL_WIDTH = 256; private static final int THUMBNAIL_HEIGHT = 256; public static final String THUMBNAIL_MIME = "image/png"; public static final String THUMBNAIL_MIME_NETWORK = "image/jpeg"; public static final int THUMBNAIL_MIME_COMPRESSION = 60; public static final String COMPRESS_MIME = "image/jpeg"; private static final int COMPRESSION_QUALITY = 85; public static boolean isExternalStorageAvailable() { return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); } public static File getInternalMediaFile(Context context, String filename) { return new File(context.getCacheDir(), filename); } /** Writes a media to the internal cache. */ public static File writeInternalMedia(Context context, String filename, byte[] contents) throws IOException { File file = getInternalMediaFile(context, filename); FileOutputStream fout = new FileOutputStream(file); fout.write(contents); fout.close(); return file; } private static BitmapFactory.Options processOptions(BitmapFactory.Options options, int scaleWidth, int scaleHeight) { int w = options.outWidth; int h = options.outHeight; // error :( if (w < 0 || h < 0) return null; if (w > scaleWidth) options.inSampleSize = (w / scaleWidth); else if (h > scaleHeight) options.inSampleSize = (h / scaleHeight); options.inJustDecodeBounds = false; options.inPreferredConfig = Bitmap.Config.RGB_565; return options; } /** Generates {@link BitmapFactory.Options} for the given {@link InputStream}. */ public static BitmapFactory.Options preloadBitmap(InputStream in, int scaleWidth, int scaleHeight) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(in, null, options); return processOptions(options, scaleWidth, scaleHeight); } /** Writes a thumbnail of a media to the internal cache. */ public static File cacheThumbnail(Context context, Uri media, String filename, boolean forNetwork) throws IOException { File file = new File(context.getCacheDir(), filename); cacheThumbnail(context, media, file, forNetwork); return file; } /** Writes a thumbnail of a media to a {@link File}. */ public static void cacheThumbnail(Context context, Uri media, File destination, boolean forNetwork) throws IOException { FileOutputStream fout = new FileOutputStream(destination); cacheThumbnail(context, media, fout, forNetwork); fout.close(); } private static void cacheThumbnail(Context context, Uri media, FileOutputStream fout, boolean forNetwork) throws IOException { ContentResolver cr = context.getContentResolver(); InputStream in = cr.openInputStream(media); BitmapFactory.Options options = preloadBitmap(in, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT); in.close(); // open again in = cr.openInputStream(media); Bitmap bitmap = BitmapFactory.decodeStream(in, null, options); in.close(); Bitmap thumbnail = ThumbnailUtils.extractThumbnail(bitmap, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT); if (thumbnail != bitmap) bitmap.recycle(); Bitmap rotatedThumbnail = bitmapOrientation(context, media, thumbnail); if (rotatedThumbnail != thumbnail) thumbnail.recycle(); // write down to file rotatedThumbnail.compress(forNetwork ? Bitmap.CompressFormat.JPEG : Bitmap.CompressFormat.PNG, forNetwork ? THUMBNAIL_MIME_COMPRESSION : 0, fout); rotatedThumbnail.recycle(); } /** * Tries various methods for obtaining the rotation of the image. * @return a matrix to rotate the image (if any) */ private static Matrix getRotation(Context context, Uri media) throws IOException { // method 1: query the media storage Cursor cursor = context.getContentResolver().query(media, new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, null, null, null); if (cursor != null) { cursor.moveToFirst(); int orientation = cursor.getInt(0); cursor.close(); if (orientation != 0) { Matrix m = new Matrix(); m.postRotate(orientation); return m; } } // method 2: write media contents to a temporary file and run ExifInterface InputStream in = context.getContentResolver().openInputStream(media); OutputStream out = null; File tmp = null; try { tmp = File.createTempFile("rotation", null, context.getCacheDir()); out = new FileOutputStream(tmp); SystemUtils.copy(in, out); // flush the file out.close(); ExifInterface exif = new ExifInterface(tmp.toString()); int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1); Matrix matrix = new Matrix(); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: matrix.postRotate(90); break; case ExifInterface.ORIENTATION_ROTATE_180: matrix.postRotate(180); break; case ExifInterface.ORIENTATION_ROTATE_270: matrix.postRotate(270); break; default: return null; } return matrix; } finally { if (tmp != null) tmp.delete(); SystemUtils.closeStream(in); SystemUtils.closeStream(out); } } /** Apply a rotation matrix respecting the image orientation. */ static Bitmap bitmapOrientation(Context context, Uri media, Bitmap bitmap) { // check if we have to (and can) rotate the thumbnail try { Matrix m = getRotation(context, media); if (m != null) { bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true); } } catch (Exception e) { Log.w(TAG, "unable to check for rotation data", e); } return bitmap; } public static long getLength(Context context, Uri media) throws IOException { AssetFileDescriptor stat = null; long length = 0; try { stat = context.getContentResolver().openAssetFileDescriptor(media, "r"); if (stat != null) length = stat.getLength(); } finally { try { if (stat != null) stat.close(); } catch (IOException e) { // ignored } } if (length == 0) { // try to count bytes by reading it InputStream in = null; try { in = context.getContentResolver().openInputStream(media); CountingInputStream counter = new CountingInputStream(in); counter.consume(); length = counter.getByteCount(); } finally { try { if (in != null) in.close(); } catch (IOException e) { // ignored } } } return length; } private static final class CountingInputStream extends InputStream { private final InputStream mInputStream; private long mBytes; public CountingInputStream(InputStream in) { mInputStream = in; } @Override public int available() throws IOException { return mInputStream.available(); } @Override public void close() throws IOException { mInputStream.close(); } @Override public int read() throws IOException { int data = mInputStream.read(); if (data >= 0) mBytes++; return data; } public long getByteCount() { return mBytes; } public void consume() throws IOException { while (read() >= 0) ; } } /** Creates a temporary JPEG file for a photo (DCIM). */ public static File getOutgoingPhotoFile() throws IOException { return getOutgoingPhotoFile(new Date()); } private static File getOutgoingPhotoFile(Date date) throws IOException { return createImageFile(DCIM_ROOT, date); } /** Creates a temporary JPEG file for a picture (Pictures). */ public static File getOutgoingPictureFile() throws IOException { return getOutgoingPictureFile(new Date()); } private static File getOutgoingPictureFile(Date date) throws IOException { createNoMedia(PICTURES_SENT_ROOT); return createImageFile(PICTURES_SENT_ROOT, date); } private static File createImageFile(File path, Date date) throws IOException { createMedia(path); String timeStamp = sDateFormat.format(date); File f = new File(path, "IMG_" + timeStamp + ".jpg"); f.createNewFile(); return f; } public static String getOutgoingPictureFilename(Date date, String extension) { String timeStamp = sDateFormat.format(date); return "IMG_" + timeStamp + "." + extension; } /** Creates a file object for an incoming image file. */ public static File getIncomingImageFile(Date date, String extension) { createMedia(PICTURES_ROOT); String timeStamp = sDateFormat.format(date); return new File(PICTURES_ROOT, "IMG_" + timeStamp + "." + extension); } /** Creates a temporary 3gp file. */ public static File getOutgoingAudioFile() throws IOException { return getOutgoingAudioFile(new Date()); } private static File getOutgoingAudioFile(Date date) throws IOException { createNoMedia(AUDIO_SENT_ROOT); String timeStamp = sDateFormat.format(date); File f = new File(AUDIO_SENT_ROOT, "record_" + timeStamp + ".3gp"); f.createNewFile(); return f; } public static String getOutgoingAudioFilename(Date date, String extension) { String timeStamp = sDateFormat.format(date); return "audio_" + timeStamp + "." + extension; } /** Creates a file object for an incoming audio file. */ public static File getIncomingAudioFile(Date date, String extension) { createNoMedia(AUDIO_ROOT); String timeStamp = sDateFormat.format(date); return new File(AUDIO_ROOT, "audio_" + timeStamp + "." + extension); } public static File getIncomingFile(Date date, String extension) { createMedia(DOWNLOADS_ROOT); String timeStamp = sDateFormat.format(date); return new File(DOWNLOADS_ROOT, "file_" + timeStamp + "." + extension); } /** Ensures that the given path exists. */ private static boolean createMedia(File path) { return path.isDirectory() || path.mkdirs(); } /** Ensures that the given path exists and a .nomedia file exists. */ private static boolean createNoMedia(File path) { try { if (createMedia(path)) { File nomedia = new File(path, ".nomedia"); return nomedia.isFile() || nomedia.createNewFile(); } return false; } catch (Exception e) { return false; } } /** Guesses the MIME type of an {@link Uri}. */ public static String getType(Context context, Uri uri) { // try Android detection String mime = context.getContentResolver().getType(uri); // the following methods actually use the same underlying implementation // (libcore.net.MimeUtils), but that could change in the future so no // hurt in trying them all just in case. // Lowercasing the filename seems to help in detecting the correct MIME. if (mime == null) // try WebKit detection mime = MimeTypeMap.getSingleton() .getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(uri.toString()).toLowerCase()); if (mime == null) // try Java detection mime = URLConnection.guessContentTypeFromName(uri.toString().toLowerCase()); return mime; } public static File resizeImage(Context context, Uri uri, int maxSize) throws IOException { return resizeImage(context, uri, maxSize, maxSize, COMPRESSION_QUALITY); } public static File resizeImage(Context context, Uri uri, int maxWidth, int maxHeight, int quality) throws IOException { final int MAX_IMAGE_SIZE = 1200000; // 1.2MP ContentResolver cr = context.getContentResolver(); // compute optimal image scale size int scale = 1; InputStream in = cr.openInputStream(uri); try { // decode image size BitmapFactory.Options o = new BitmapFactory.Options(); o.inJustDecodeBounds = true; BitmapFactory.decodeStream(in, null, o); in.close(); // calculate optimal image scale size while ((o.outWidth * o.outHeight) * (1 / Math.pow(scale, 2)) > MAX_IMAGE_SIZE) scale++; Log.d(TAG, "scale = " + scale + ", orig-width: " + o.outWidth + ", orig-height: " + o.outHeight); } catch (IOException e) { Log.d(TAG, "unable to calculate optimal scale size, using original image"); } finally { try { in.close(); } catch (Exception e) { // ignored } } // open image again for the actual scaling Bitmap bitmap = null; try { in = cr.openInputStream(uri); BitmapFactory.Options o = new BitmapFactory.Options(); if (scale > 1) { o.inSampleSize = scale - 1; } bitmap = BitmapFactory.decodeStream(in, null, o); } finally { try { in.close(); } catch (Exception e) { // ignored } } if (bitmap == null) { return null; } float photoW = bitmap.getWidth(); float photoH = bitmap.getHeight(); if (photoW == 0 || photoH == 0) { return null; } float scaleFactor = Math.max(photoW / maxWidth, photoH / maxHeight); int w = (int) (photoW / scaleFactor); int h = (int) (photoH / scaleFactor); if (h == 0 || w == 0) { return null; } Bitmap scaledBitmap = null; try { scaledBitmap = Bitmap.createScaledBitmap(bitmap, w, h, true); } finally { if (scaledBitmap != bitmap) bitmap.recycle(); } // check for rotation data Bitmap rotatedScaledBitmap = bitmapOrientation(context, uri, scaledBitmap); if (rotatedScaledBitmap != scaledBitmap) scaledBitmap.recycle(); final File compressedFile = getOutgoingPictureFile(); FileOutputStream stream = null; try { stream = new FileOutputStream(compressedFile); rotatedScaledBitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream); return compressedFile; } finally { try { stream.close(); } catch (Exception e) { // ignored } rotatedScaledBitmap.recycle(); } } public static File copyOutgoingMedia(Context context, Uri media) throws IOException { final File outFile = getOutgoingPictureFile(); InputStream in = context.getContentResolver().openInputStream(media); OutputStream out = null; try { out = new FileOutputStream(outFile); SystemUtils.copy(in, out); return outFile; } finally { SystemUtils.closeStream(in); SystemUtils.closeStream(out); } } /** * Returns true if the running platform is using SAF, therefore we'll need * to persist permissions when asking for media files. */ public static boolean isStorageAccessFrameworkAvailable() { return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT; } @TargetApi(Build.VERSION_CODES.KITKAT) public static void requestPersistablePermissions(Context context, Uri uri) { context.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); } public static void scanFile(Context context, File file, String mime) { MediaScannerConnection.scanFile(context.getApplicationContext(), new String[] { file.getPath() }, new String[] { mime }, null); } @TargetApi(Build.VERSION_CODES.KITKAT) public static void createFile(Fragment fragment, String mimeType, String fileName, int requestCode) { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); // Filter to only show results that can be "opened", such as // a file (as opposed to a list of contacts or timezones). intent.addCategory(Intent.CATEGORY_OPENABLE); // Create a file with the requested MIME type. intent.setType(mimeType); // Note: This is not documented, but works intent.putExtra("android.content.extra.SHOW_ADVANCED", true); intent.putExtra(Intent.EXTRA_TITLE, fileName); fragment.startActivityForResult(intent, requestCode); } }