Loading app/build.gradle +2 −4 Original line number Diff line number Diff line Loading @@ -45,10 +45,8 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'androidx.appcompat:appcompat:1.1.0-beta01' implementation 'androidx.preference:preference:1.1.0-beta01' implementation 'androidx.appcompat:appcompat:1.1.0-rc01' implementation 'androidx.preference:preference:1.1.0-rc01' implementation 'androidx.exifinterface:exifinterface:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' testImplementation 'junit:junit:4.12' implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' } app/src/main/java/net/fabiszewski/ulogger/DbAccess.java +31 −1 Original line number Diff line number Diff line Loading @@ -267,7 +267,7 @@ class DbAccess implements AutoCloseable { new String[]{String.valueOf(id)}); Uri uri = getImageUri(id); if (uri != null) { ImageHelper.deleteImage(context, uri); ImageHelper.deleteLocalImage(context, uri); } } Loading Loading @@ -311,6 +311,36 @@ class DbAccess implements AutoCloseable { } } /** * Get number of not synchronized items. * * @return Count */ private int countImages() { Cursor count = db.query(DbContract.Positions.TABLE_NAME, new String[]{"COUNT(*)"}, DbContract.Positions.COLUMN_IMAGE_URI + " IS NOT NULL", null, null, null, null); int result = 0; if (count.moveToFirst()) { result = count.getInt(0); } count.close(); return result; } /** * Get number of not synchronized items. * * @param context Context * @return Count */ static int countImages(Context context) { try (DbAccess dbAccess = getOpenInstance(context)) { return dbAccess.countImages(); } } /** * Checks if database needs synchronization, * i.e. contains non-synchronized positions. Loading app/src/main/java/net/fabiszewski/ulogger/ImageHelper.java +169 −40 Original line number Diff line number Diff line Loading @@ -12,24 +12,25 @@ package net.fabiszewski.ulogger; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.media.MediaScannerConnection; import android.media.ThumbnailUtils; import android.net.Uri; import android.os.Build; import android.provider.MediaStore; import android.provider.OpenableColumns; import android.util.Log; import android.util.Size; import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.exifinterface.media.ExifInterface; import androidx.preference.PreferenceManager; import java.io.File; import java.io.FileOutputStream; Loading @@ -47,6 +48,7 @@ class ImageHelper { private static final int ORIENTATION_180 = 180; private static final int ORIENTATION_270 = 270; private static final int ORIENTATION_NORMAL = 0; private static final String EXT_JPG = ".jpg"; /** Loading @@ -57,7 +59,7 @@ class ImageHelper { * @param uri Image URI * @return Orientation in degrees */ private static int getOrientation(Context context, Uri uri) { private static int getOrientation(@NonNull Context context, @NonNull Uri uri) { int orientation = getOrientationMediaStore(context, uri); if (orientation == 0) { orientation = getOrientationExif(context, uri); Loading @@ -72,7 +74,7 @@ class ImageHelper { * @param uri Image URI * @return Orientation in degrees */ private static int getOrientationExif(Context context, Uri uri) { private static int getOrientationExif(@NonNull Context context, @NonNull Uri uri) { int orientation = ORIENTATION_NORMAL; try (InputStream in = context.getContentResolver().openInputStream(uri)) { if (in != null) { Loading @@ -99,10 +101,10 @@ class ImageHelper { * @param uri Image URI * @return Orientation in degrees */ private static int getOrientationMediaStore(Context context, Uri photoUri) { private static int getOrientationMediaStore(@NonNull Context context, @NonNull Uri uri) { int orientation = 0; String[] projection = {MEDIA_ORIENTATION}; try (Cursor cursor = context.getContentResolver().query(photoUri, projection, null, null, null)) { try (Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { orientation = cursor.getInt(0); } Loading @@ -123,16 +125,35 @@ class ImageHelper { return "ulogger_" + timeStamp; } /** * Creates media title based on track media count * * @return Name */ @NonNull private static String getUniqueTitle(@NonNull Context context) { int imageNumber = DbAccess.countImages(context) + 1; String trackName = DbAccess.getTrackName(context); return String.format(Locale.ROOT,"%s #%d", trackName, imageNumber); } /** * Create image uri in media collection * * @param context Context * @return URI */ static Uri createImageUri(Context context) { static Uri createImageUri(@NonNull Context context) { ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.TITLE, getUniqueName()); String title = getUniqueTitle(context); long timeMillis = System.currentTimeMillis(); values.put(MediaStore.Images.Media.TITLE, title); values.put(MediaStore.Images.Media.DISPLAY_NAME, title); values.put(MediaStore.Images.Media.MIME_TYPE, JPEG_MIME); values.put(MediaStore.Images.Media.DATE_ADDED, timeMillis / 1000); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { values.put(MediaStore.Images.Media.DATE_TAKEN, timeMillis); } Uri collection; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); Loading @@ -142,23 +163,61 @@ class ImageHelper { return context.getContentResolver().insert(collection, values); } static Bitmap getThumbnail(Context context, Uri uri) throws IOException { int sizeDp = (int) context.getResources().getDimension(R.dimen.thumbnail_size); int sizePx = sizeDp * (int) Resources.getSystem().getDisplayMetrics().density; /** * Extract thumbnail from URI * @param context Context * @param uri URI * @return Thumbnail * @throws IOException IO exception on failure */ static Bitmap getThumbnail(@NonNull Context context, @NonNull Uri uri) throws IOException { int sizePx = getThumbnailSize(context); Bitmap bitmap; ContentResolver cr = context.getContentResolver(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { bitmap = cr.loadThumbnail(uri, new Size(sizePx, sizePx), null); } else { try (InputStream is = cr.openInputStream(uri)) { bitmap = BitmapFactory.decodeStream(is, null, null); } bitmap = ThumbnailUtils.extractThumbnail(bitmap, sizePx, sizePx); } bitmap = fixImageOrientation(context, uri, bitmap); return bitmap; } private static Bitmap fixImageOrientation(Context context, Uri uri, Bitmap bitmap) { /** * Extract thumbnail from URI * @param context Context * @param uri URI * @return Thumbnail */ static Bitmap getThumbnail(@NonNull Context context, @NonNull Bitmap bitmap) { int sizePx = getThumbnailSize(context); return ThumbnailUtils.extractThumbnail(bitmap, sizePx, sizePx); } /** * Get thumbnail size from resources * @param context Context * @return Size in pixels */ private static int getThumbnailSize(@NonNull Context context) { int sizeDp = (int) context.getResources().getDimension(R.dimen.thumbnail_size); int sizePx = sizeDp * (int) Resources.getSystem().getDisplayMetrics().density; if (Logger.DEBUG) { Log.d(TAG, "[getThumbnailSize: " + sizePx + "]" ); } return sizePx; } /** * Fix image orientation if needed * @param context Context * @param uri Image URI as source of orientation data (MediaStore or EXIF) * @param bitmap Bitmap to be rotated * @return Resulting bitmap */ private static Bitmap fixImageOrientation(@NonNull Context context, @NonNull Uri uri, @NonNull Bitmap bitmap) { try { int orientation = ImageHelper.getOrientation(context, uri); if (orientation != 0) { Loading @@ -172,7 +231,13 @@ class ImageHelper { return bitmap; } static long getFileSize(Context context, Uri uri) { /** * Get file size * @param context Context * @param uri File URI * @return Size */ static long getFileSize(@NonNull Context context, @NonNull Uri uri) { long fileSize = 0; try (Cursor cursor = context.getContentResolver() .query(uri, null, null, null, null)) { Loading @@ -185,23 +250,32 @@ class ImageHelper { return fileSize; } /** * Try to get file MIME type * @param context Context * @param uri File URI * @return MIME type or null if not known */ @Nullable static String getFileMime(Context context, Uri uri) { static String getFileMime(@NonNull Context context, @NonNull Uri uri) { ContentResolver cr = context.getContentResolver(); String fileMime = cr.getType(uri); if (fileMime == null) { String fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()); fileMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase()); fileMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase(Locale.ROOT)); } return fileMime; } static Uri resampleIfNeeded(Context context, Uri uri) throws IOException { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); int dstWidth = Integer.parseInt(prefs.getString(SettingsActivity.KEY_IMAGE_SIZE, context.getString(R.string.pref_imagesize_default))); if (dstWidth == 0) { return uri; } /** * Resample image to given size threshold * @param context Context * @param uri Image URI * @param dstWidth Maximum width/height * @return Resampled bitmap * @throws IOException IO exception on error */ static Bitmap getResampledBitmap(@NonNull Context context, @NonNull Uri uri, int dstWidth) throws IOException { ContentResolver cr = context.getContentResolver(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; Loading @@ -227,12 +301,13 @@ class ImageHelper { } } catch (OutOfMemoryError e) { if (Logger.DEBUG) { Log.d(TAG, "[resampleIfNeeded OutOfMemoryError]"); } } if (bitmap == null && scale > 1) { if (retry) { throw new IOException("Out of memory"); } else if (scale > 1) { retry = true; options.inSampleSize = scale; retry = !retry; if (Logger.DEBUG) { Log.d(TAG, "[resampleIfNeeded try sampling: " + retry + "]"); } if (Logger.DEBUG) { Log.d(TAG, "[resampleIfNeeded try sampling]"); } } } } while (retry); Loading @@ -241,8 +316,18 @@ class ImageHelper { } bitmap = fixImageOrientation(context, uri, bitmap); return bitmap; } String filename = getUniqueName() + ".jpg"; /** * Save bitmap to app cache folder as jpeg * @param context Context * @param bitmap Bitmap * @return URI of saved image * @throws IOException IO exception on failure */ static Uri saveToCache(@NonNull Context context, @NonNull Bitmap bitmap) throws IOException { String filename = getUniqueName() + EXT_JPG; File outFile = new File(context.getCacheDir(), filename); try (FileOutputStream os = new FileOutputStream(outFile)) { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os); Loading @@ -250,7 +335,14 @@ class ImageHelper { return Uri.fromFile(outFile); } static Uri moveToAppStorage(@NonNull Context context, @NonNull Uri inUri) { /** * Move cached image file to internal app folder * Ignore files that are not in cache folder * @param context Context * @param inUri Source URI * @return Destination URI */ static Uri moveCachedToAppStorage(@NonNull Context context, @NonNull Uri inUri) { Uri outUri = null; String path = inUri.getPath(); if (path != null) { Loading @@ -262,40 +354,77 @@ class ImageHelper { if (inFile.renameTo(outFile)) { outUri = Uri.fromFile(outFile); } else { if (Logger.DEBUG) { Log.d(TAG, "[moveToAppStorage failed]"); } if (Logger.DEBUG) { Log.d(TAG, "[moveCachedToAppStorage failed]"); } } } return outUri; } /** * Clear images in cache folder * @param context Context */ static void clearImageCache(@NonNull Context context) { File dir = context.getCacheDir(); clearImages(dir); } /** * Clear images in app folder * @param context Context */ static void clearTrackImages(@NonNull Context context) { File dir = context.getFilesDir(); clearImages(dir); } private static void clearImages(File dir) { /** * Clear image jpeg files in given folder * @param dir Folder */ private static void clearImages(@NonNull File dir) { File[] files = dir.listFiles(); if (files != null) { for (File file : files) { if (file.isFile() && file.getPath().endsWith(".jpg") && file.delete()) { if (file.isFile() && file.getPath().endsWith(EXT_JPG) && file.delete()) { if (Logger.DEBUG) { Log.d(TAG, "[clearImages deleted file " + file.getName() + "]"); } } } } } static void deleteImage(Context context, Uri uri) { /** * Delete file only if it is located in app internal folder * @param context Context * @param uri File URI */ static void deleteLocalImage(@NonNull Context context, @NonNull Uri uri) { String path = uri.getPath(); if (path != null) { File file = new File(path); if (file.isFile() && file.getPath().startsWith(context.getFilesDir().getPath()) && file.delete()) { if (Logger.DEBUG) { Log.d(TAG, "[deleteImage deleted file " + file.getName() + "]"); } if (Logger.DEBUG) { Log.d(TAG, "[deleteLocalImage deleted file " + file.getName() + "]"); } } } } /** * Add image to MediaStore * @param context Context * @param uri Image URI */ static void galleryAdd(@NonNull Context context, @NonNull Uri uri) { ContentResolver cr = context.getContentResolver(); String mime = cr.getType(uri); MediaScannerConnection.scanFile(context, new String[] {uri.getPath()}, new String[] {mime}, null); } /** * Get persistable permission for URI * @param context Context * @param uri URI */ static void getPersistablePermision(@NonNull Context context, @NonNull Uri uri) { context.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); } } app/src/main/java/net/fabiszewski/ulogger/ImageTask.java 0 → 100644 +120 −0 Original line number Diff line number Diff line /* * Copyright (c) 2019 Bartek Fabiszewski * http://www.fabiszewski.net * * This file is part of μlogger-android. * Licensed under GPL, either version 3, or any later. * See <http://www.gnu.org/licenses/> */ package net.fabiszewski.ulogger; import android.app.Activity; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.net.Uri; import android.os.AsyncTask; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; import java.io.IOException; import java.lang.ref.WeakReference; import static net.fabiszewski.ulogger.ImageHelper.getPersistablePermision; import static net.fabiszewski.ulogger.ImageHelper.getResampledBitmap; import static net.fabiszewski.ulogger.ImageHelper.getThumbnail; import static net.fabiszewski.ulogger.ImageHelper.saveToCache; /** * Task to downsample image */ class ImageTask extends AsyncTask<Uri, Void, ImageTask.ImageTaskResult> { private static final String TAG = ImageTask.class.getSimpleName(); private final WeakReference<ImageTaskCallback> weakCallback; private String errorMessage = "Image resampling failed"; ImageTask(ImageTaskCallback callback) { weakCallback = new WeakReference<>(callback); } @Override protected ImageTaskResult doInBackground(Uri... params) { if (Logger.DEBUG) { Log.d(TAG, "[doInBackground]"); } Activity activity = getActivity(); if (activity == null || params.length != 1 || params[0] == null) { return null; } Uri inUri = params[0]; ImageTaskResult result = null; try { Uri savedUri; Bitmap thumbnail; SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); int dstWidth = Integer.parseInt(prefs.getString(SettingsActivity.KEY_IMAGE_SIZE, activity.getString(R.string.pref_imagesize_default))); if (dstWidth == 0) { savedUri = inUri; getPersistablePermision(activity, inUri); thumbnail = getThumbnail(activity, inUri); } else { Bitmap bitmap = getResampledBitmap(activity, inUri, dstWidth); savedUri = saveToCache(activity, bitmap); thumbnail = getThumbnail(activity, bitmap); bitmap.recycle(); } if (savedUri != null && thumbnail != null) { result = new ImageTaskResult(savedUri, thumbnail); } } catch (IOException e) { if (e.getMessage() != null) { errorMessage += ": " + e.getMessage(); } } return result; } @Override protected void onPostExecute(@Nullable ImageTaskResult result) { super.onPostExecute(result); ImageTaskCallback callback = weakCallback.get(); if (callback != null) { if (result == null) { callback.onImageTaskFailure(errorMessage); } else { callback.onImageTaskCompleted(result.savedUri, result.thumbnail); } } } @Nullable private Activity getActivity() { ImageTaskCallback callback = weakCallback.get(); if (callback != null) { return callback.getActivity(); } return null; } interface ImageTaskCallback { void onImageTaskCompleted(@NonNull Uri uri, @NonNull Bitmap thumbnail); void onImageTaskFailure(@NonNull String error); Activity getActivity(); } class ImageTaskResult { final Uri savedUri; final Bitmap thumbnail; ImageTaskResult(@NonNull Uri savedUri, @NonNull Bitmap thumbnail) { this.savedUri = savedUri; this.thumbnail = thumbnail; } } } app/src/main/java/net/fabiszewski/ulogger/LocationFormatter.java +1 −1 Original line number Diff line number Diff line Loading @@ -102,7 +102,7 @@ class LocationFormatter { if (LocationHelper.isGps(location)) { provider = context.getString(R.string.provider_gps); } else if (LocationHelper.isNetwork(location)) { provider = context.getString(R.string.provider_network).toLowerCase(); provider = context.getString(R.string.provider_network).toLowerCase(Locale.getDefault()); } else { provider = location.getProvider(); } Loading Loading
app/build.gradle +2 −4 Original line number Diff line number Diff line Loading @@ -45,10 +45,8 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'androidx.appcompat:appcompat:1.1.0-beta01' implementation 'androidx.preference:preference:1.1.0-beta01' implementation 'androidx.appcompat:appcompat:1.1.0-rc01' implementation 'androidx.preference:preference:1.1.0-rc01' implementation 'androidx.exifinterface:exifinterface:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' testImplementation 'junit:junit:4.12' implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' }
app/src/main/java/net/fabiszewski/ulogger/DbAccess.java +31 −1 Original line number Diff line number Diff line Loading @@ -267,7 +267,7 @@ class DbAccess implements AutoCloseable { new String[]{String.valueOf(id)}); Uri uri = getImageUri(id); if (uri != null) { ImageHelper.deleteImage(context, uri); ImageHelper.deleteLocalImage(context, uri); } } Loading Loading @@ -311,6 +311,36 @@ class DbAccess implements AutoCloseable { } } /** * Get number of not synchronized items. * * @return Count */ private int countImages() { Cursor count = db.query(DbContract.Positions.TABLE_NAME, new String[]{"COUNT(*)"}, DbContract.Positions.COLUMN_IMAGE_URI + " IS NOT NULL", null, null, null, null); int result = 0; if (count.moveToFirst()) { result = count.getInt(0); } count.close(); return result; } /** * Get number of not synchronized items. * * @param context Context * @return Count */ static int countImages(Context context) { try (DbAccess dbAccess = getOpenInstance(context)) { return dbAccess.countImages(); } } /** * Checks if database needs synchronization, * i.e. contains non-synchronized positions. Loading
app/src/main/java/net/fabiszewski/ulogger/ImageHelper.java +169 −40 Original line number Diff line number Diff line Loading @@ -12,24 +12,25 @@ package net.fabiszewski.ulogger; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.media.MediaScannerConnection; import android.media.ThumbnailUtils; import android.net.Uri; import android.os.Build; import android.provider.MediaStore; import android.provider.OpenableColumns; import android.util.Log; import android.util.Size; import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.exifinterface.media.ExifInterface; import androidx.preference.PreferenceManager; import java.io.File; import java.io.FileOutputStream; Loading @@ -47,6 +48,7 @@ class ImageHelper { private static final int ORIENTATION_180 = 180; private static final int ORIENTATION_270 = 270; private static final int ORIENTATION_NORMAL = 0; private static final String EXT_JPG = ".jpg"; /** Loading @@ -57,7 +59,7 @@ class ImageHelper { * @param uri Image URI * @return Orientation in degrees */ private static int getOrientation(Context context, Uri uri) { private static int getOrientation(@NonNull Context context, @NonNull Uri uri) { int orientation = getOrientationMediaStore(context, uri); if (orientation == 0) { orientation = getOrientationExif(context, uri); Loading @@ -72,7 +74,7 @@ class ImageHelper { * @param uri Image URI * @return Orientation in degrees */ private static int getOrientationExif(Context context, Uri uri) { private static int getOrientationExif(@NonNull Context context, @NonNull Uri uri) { int orientation = ORIENTATION_NORMAL; try (InputStream in = context.getContentResolver().openInputStream(uri)) { if (in != null) { Loading @@ -99,10 +101,10 @@ class ImageHelper { * @param uri Image URI * @return Orientation in degrees */ private static int getOrientationMediaStore(Context context, Uri photoUri) { private static int getOrientationMediaStore(@NonNull Context context, @NonNull Uri uri) { int orientation = 0; String[] projection = {MEDIA_ORIENTATION}; try (Cursor cursor = context.getContentResolver().query(photoUri, projection, null, null, null)) { try (Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { orientation = cursor.getInt(0); } Loading @@ -123,16 +125,35 @@ class ImageHelper { return "ulogger_" + timeStamp; } /** * Creates media title based on track media count * * @return Name */ @NonNull private static String getUniqueTitle(@NonNull Context context) { int imageNumber = DbAccess.countImages(context) + 1; String trackName = DbAccess.getTrackName(context); return String.format(Locale.ROOT,"%s #%d", trackName, imageNumber); } /** * Create image uri in media collection * * @param context Context * @return URI */ static Uri createImageUri(Context context) { static Uri createImageUri(@NonNull Context context) { ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.TITLE, getUniqueName()); String title = getUniqueTitle(context); long timeMillis = System.currentTimeMillis(); values.put(MediaStore.Images.Media.TITLE, title); values.put(MediaStore.Images.Media.DISPLAY_NAME, title); values.put(MediaStore.Images.Media.MIME_TYPE, JPEG_MIME); values.put(MediaStore.Images.Media.DATE_ADDED, timeMillis / 1000); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { values.put(MediaStore.Images.Media.DATE_TAKEN, timeMillis); } Uri collection; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); Loading @@ -142,23 +163,61 @@ class ImageHelper { return context.getContentResolver().insert(collection, values); } static Bitmap getThumbnail(Context context, Uri uri) throws IOException { int sizeDp = (int) context.getResources().getDimension(R.dimen.thumbnail_size); int sizePx = sizeDp * (int) Resources.getSystem().getDisplayMetrics().density; /** * Extract thumbnail from URI * @param context Context * @param uri URI * @return Thumbnail * @throws IOException IO exception on failure */ static Bitmap getThumbnail(@NonNull Context context, @NonNull Uri uri) throws IOException { int sizePx = getThumbnailSize(context); Bitmap bitmap; ContentResolver cr = context.getContentResolver(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { bitmap = cr.loadThumbnail(uri, new Size(sizePx, sizePx), null); } else { try (InputStream is = cr.openInputStream(uri)) { bitmap = BitmapFactory.decodeStream(is, null, null); } bitmap = ThumbnailUtils.extractThumbnail(bitmap, sizePx, sizePx); } bitmap = fixImageOrientation(context, uri, bitmap); return bitmap; } private static Bitmap fixImageOrientation(Context context, Uri uri, Bitmap bitmap) { /** * Extract thumbnail from URI * @param context Context * @param uri URI * @return Thumbnail */ static Bitmap getThumbnail(@NonNull Context context, @NonNull Bitmap bitmap) { int sizePx = getThumbnailSize(context); return ThumbnailUtils.extractThumbnail(bitmap, sizePx, sizePx); } /** * Get thumbnail size from resources * @param context Context * @return Size in pixels */ private static int getThumbnailSize(@NonNull Context context) { int sizeDp = (int) context.getResources().getDimension(R.dimen.thumbnail_size); int sizePx = sizeDp * (int) Resources.getSystem().getDisplayMetrics().density; if (Logger.DEBUG) { Log.d(TAG, "[getThumbnailSize: " + sizePx + "]" ); } return sizePx; } /** * Fix image orientation if needed * @param context Context * @param uri Image URI as source of orientation data (MediaStore or EXIF) * @param bitmap Bitmap to be rotated * @return Resulting bitmap */ private static Bitmap fixImageOrientation(@NonNull Context context, @NonNull Uri uri, @NonNull Bitmap bitmap) { try { int orientation = ImageHelper.getOrientation(context, uri); if (orientation != 0) { Loading @@ -172,7 +231,13 @@ class ImageHelper { return bitmap; } static long getFileSize(Context context, Uri uri) { /** * Get file size * @param context Context * @param uri File URI * @return Size */ static long getFileSize(@NonNull Context context, @NonNull Uri uri) { long fileSize = 0; try (Cursor cursor = context.getContentResolver() .query(uri, null, null, null, null)) { Loading @@ -185,23 +250,32 @@ class ImageHelper { return fileSize; } /** * Try to get file MIME type * @param context Context * @param uri File URI * @return MIME type or null if not known */ @Nullable static String getFileMime(Context context, Uri uri) { static String getFileMime(@NonNull Context context, @NonNull Uri uri) { ContentResolver cr = context.getContentResolver(); String fileMime = cr.getType(uri); if (fileMime == null) { String fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()); fileMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase()); fileMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase(Locale.ROOT)); } return fileMime; } static Uri resampleIfNeeded(Context context, Uri uri) throws IOException { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); int dstWidth = Integer.parseInt(prefs.getString(SettingsActivity.KEY_IMAGE_SIZE, context.getString(R.string.pref_imagesize_default))); if (dstWidth == 0) { return uri; } /** * Resample image to given size threshold * @param context Context * @param uri Image URI * @param dstWidth Maximum width/height * @return Resampled bitmap * @throws IOException IO exception on error */ static Bitmap getResampledBitmap(@NonNull Context context, @NonNull Uri uri, int dstWidth) throws IOException { ContentResolver cr = context.getContentResolver(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; Loading @@ -227,12 +301,13 @@ class ImageHelper { } } catch (OutOfMemoryError e) { if (Logger.DEBUG) { Log.d(TAG, "[resampleIfNeeded OutOfMemoryError]"); } } if (bitmap == null && scale > 1) { if (retry) { throw new IOException("Out of memory"); } else if (scale > 1) { retry = true; options.inSampleSize = scale; retry = !retry; if (Logger.DEBUG) { Log.d(TAG, "[resampleIfNeeded try sampling: " + retry + "]"); } if (Logger.DEBUG) { Log.d(TAG, "[resampleIfNeeded try sampling]"); } } } } while (retry); Loading @@ -241,8 +316,18 @@ class ImageHelper { } bitmap = fixImageOrientation(context, uri, bitmap); return bitmap; } String filename = getUniqueName() + ".jpg"; /** * Save bitmap to app cache folder as jpeg * @param context Context * @param bitmap Bitmap * @return URI of saved image * @throws IOException IO exception on failure */ static Uri saveToCache(@NonNull Context context, @NonNull Bitmap bitmap) throws IOException { String filename = getUniqueName() + EXT_JPG; File outFile = new File(context.getCacheDir(), filename); try (FileOutputStream os = new FileOutputStream(outFile)) { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os); Loading @@ -250,7 +335,14 @@ class ImageHelper { return Uri.fromFile(outFile); } static Uri moveToAppStorage(@NonNull Context context, @NonNull Uri inUri) { /** * Move cached image file to internal app folder * Ignore files that are not in cache folder * @param context Context * @param inUri Source URI * @return Destination URI */ static Uri moveCachedToAppStorage(@NonNull Context context, @NonNull Uri inUri) { Uri outUri = null; String path = inUri.getPath(); if (path != null) { Loading @@ -262,40 +354,77 @@ class ImageHelper { if (inFile.renameTo(outFile)) { outUri = Uri.fromFile(outFile); } else { if (Logger.DEBUG) { Log.d(TAG, "[moveToAppStorage failed]"); } if (Logger.DEBUG) { Log.d(TAG, "[moveCachedToAppStorage failed]"); } } } return outUri; } /** * Clear images in cache folder * @param context Context */ static void clearImageCache(@NonNull Context context) { File dir = context.getCacheDir(); clearImages(dir); } /** * Clear images in app folder * @param context Context */ static void clearTrackImages(@NonNull Context context) { File dir = context.getFilesDir(); clearImages(dir); } private static void clearImages(File dir) { /** * Clear image jpeg files in given folder * @param dir Folder */ private static void clearImages(@NonNull File dir) { File[] files = dir.listFiles(); if (files != null) { for (File file : files) { if (file.isFile() && file.getPath().endsWith(".jpg") && file.delete()) { if (file.isFile() && file.getPath().endsWith(EXT_JPG) && file.delete()) { if (Logger.DEBUG) { Log.d(TAG, "[clearImages deleted file " + file.getName() + "]"); } } } } } static void deleteImage(Context context, Uri uri) { /** * Delete file only if it is located in app internal folder * @param context Context * @param uri File URI */ static void deleteLocalImage(@NonNull Context context, @NonNull Uri uri) { String path = uri.getPath(); if (path != null) { File file = new File(path); if (file.isFile() && file.getPath().startsWith(context.getFilesDir().getPath()) && file.delete()) { if (Logger.DEBUG) { Log.d(TAG, "[deleteImage deleted file " + file.getName() + "]"); } if (Logger.DEBUG) { Log.d(TAG, "[deleteLocalImage deleted file " + file.getName() + "]"); } } } } /** * Add image to MediaStore * @param context Context * @param uri Image URI */ static void galleryAdd(@NonNull Context context, @NonNull Uri uri) { ContentResolver cr = context.getContentResolver(); String mime = cr.getType(uri); MediaScannerConnection.scanFile(context, new String[] {uri.getPath()}, new String[] {mime}, null); } /** * Get persistable permission for URI * @param context Context * @param uri URI */ static void getPersistablePermision(@NonNull Context context, @NonNull Uri uri) { context.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); } }
app/src/main/java/net/fabiszewski/ulogger/ImageTask.java 0 → 100644 +120 −0 Original line number Diff line number Diff line /* * Copyright (c) 2019 Bartek Fabiszewski * http://www.fabiszewski.net * * This file is part of μlogger-android. * Licensed under GPL, either version 3, or any later. * See <http://www.gnu.org/licenses/> */ package net.fabiszewski.ulogger; import android.app.Activity; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.net.Uri; import android.os.AsyncTask; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; import java.io.IOException; import java.lang.ref.WeakReference; import static net.fabiszewski.ulogger.ImageHelper.getPersistablePermision; import static net.fabiszewski.ulogger.ImageHelper.getResampledBitmap; import static net.fabiszewski.ulogger.ImageHelper.getThumbnail; import static net.fabiszewski.ulogger.ImageHelper.saveToCache; /** * Task to downsample image */ class ImageTask extends AsyncTask<Uri, Void, ImageTask.ImageTaskResult> { private static final String TAG = ImageTask.class.getSimpleName(); private final WeakReference<ImageTaskCallback> weakCallback; private String errorMessage = "Image resampling failed"; ImageTask(ImageTaskCallback callback) { weakCallback = new WeakReference<>(callback); } @Override protected ImageTaskResult doInBackground(Uri... params) { if (Logger.DEBUG) { Log.d(TAG, "[doInBackground]"); } Activity activity = getActivity(); if (activity == null || params.length != 1 || params[0] == null) { return null; } Uri inUri = params[0]; ImageTaskResult result = null; try { Uri savedUri; Bitmap thumbnail; SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); int dstWidth = Integer.parseInt(prefs.getString(SettingsActivity.KEY_IMAGE_SIZE, activity.getString(R.string.pref_imagesize_default))); if (dstWidth == 0) { savedUri = inUri; getPersistablePermision(activity, inUri); thumbnail = getThumbnail(activity, inUri); } else { Bitmap bitmap = getResampledBitmap(activity, inUri, dstWidth); savedUri = saveToCache(activity, bitmap); thumbnail = getThumbnail(activity, bitmap); bitmap.recycle(); } if (savedUri != null && thumbnail != null) { result = new ImageTaskResult(savedUri, thumbnail); } } catch (IOException e) { if (e.getMessage() != null) { errorMessage += ": " + e.getMessage(); } } return result; } @Override protected void onPostExecute(@Nullable ImageTaskResult result) { super.onPostExecute(result); ImageTaskCallback callback = weakCallback.get(); if (callback != null) { if (result == null) { callback.onImageTaskFailure(errorMessage); } else { callback.onImageTaskCompleted(result.savedUri, result.thumbnail); } } } @Nullable private Activity getActivity() { ImageTaskCallback callback = weakCallback.get(); if (callback != null) { return callback.getActivity(); } return null; } interface ImageTaskCallback { void onImageTaskCompleted(@NonNull Uri uri, @NonNull Bitmap thumbnail); void onImageTaskFailure(@NonNull String error); Activity getActivity(); } class ImageTaskResult { final Uri savedUri; final Bitmap thumbnail; ImageTaskResult(@NonNull Uri savedUri, @NonNull Bitmap thumbnail) { this.savedUri = savedUri; this.thumbnail = thumbnail; } } }
app/src/main/java/net/fabiszewski/ulogger/LocationFormatter.java +1 −1 Original line number Diff line number Diff line Loading @@ -102,7 +102,7 @@ class LocationFormatter { if (LocationHelper.isGps(location)) { provider = context.getString(R.string.provider_gps); } else if (LocationHelper.isNetwork(location)) { provider = context.getString(R.string.provider_network).toLowerCase(); provider = context.getString(R.string.provider_network).toLowerCase(Locale.getDefault()); } else { provider = location.getProvider(); } Loading