Commit 60f2bdbc authored by Bartek Fabiszewski's avatar Bartek Fabiszewski
Browse files

Handle file uploads, add preference for image size, more refactoring

parent 8bd63d5b
Loading
Loading
Loading
Loading
+87 −8
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.location.Location;
import android.net.Uri;
import android.util.Log;

import androidx.annotation.Nullable;
@@ -161,6 +162,30 @@ class DbAccess implements AutoCloseable {
                DbContract.Positions._ID);
    }

    /**
     * Get result set containing position with given id
     *
     * @param id Position id
     * @return Image URI
     */
     @Nullable
     private Uri getImageUri(int id) {
        Cursor query = db.query(DbContract.Positions.TABLE_NAME,
                new String[]{DbContract.Positions.COLUMN_IMAGE_URI},
                DbContract.Positions._ID + "=?",
                new String[]{Integer.toString(id)},
                null, null, null);
        Uri uri = null;
        if (query.moveToFirst()) {
            String imageUri = query.getString(0);
            if (imageUri != null) {
                uri = Uri.parse(imageUri);
            }
        }
        query.close();
        return uri;
    }

    /**
     * Get result set containing positions marked as not synchronized.
     *
@@ -232,7 +257,7 @@ class DbAccess implements AutoCloseable {
     *
     * @param id Position id
     */
    void setSynced(int id) {
    void setSynced(Context context, int id) {
        ContentValues values = new ContentValues();
        values.put(DbContract.Positions.COLUMN_SYNCED, "1");
        values.putNull(DbContract.Positions.COLUMN_ERROR);
@@ -240,6 +265,10 @@ class DbAccess implements AutoCloseable {
                values,
                DbContract.Positions._ID + "=?",
                new String[]{String.valueOf(id)});
        Uri uri = getImageUri(id);
        if (uri != null) {
            ImageHelper.deleteImage(context, uri);
        }
    }

    /**
@@ -419,12 +448,12 @@ class DbAccess implements AutoCloseable {
    }

    /**
     * Start new track.
     * Set up new track.
     * Deletes all previous track data and positions. Adds new track.
     *
     * @param name New track name
     */
    void newTrack(String name) {
    private void newTrack(String name) {
        truncateTrack();
        truncatePositions();
        ContentValues values = new ContentValues();
@@ -433,7 +462,7 @@ class DbAccess implements AutoCloseable {
    }

    /**
     * Start new track.
     * Set up new track.
     * Deletes all previous track data and positions. Adds new track.
     *
     * @param context Context
@@ -441,10 +470,23 @@ class DbAccess implements AutoCloseable {
     */
    static void newTrack(Context context, String name) {
        try (DbAccess dbAccess = getOpenInstance(context)) {
            ImageHelper.clearTrackImages(context);
            dbAccess.newTrack(name);
        }
    }


    /**
     * Set up new track with default name if there is no active track.
     * To be used in automated context
     * @param context Context
     */
    static void newAutoTrack(Context context) {
        if (getTrackName(context) == null) {
            newTrack(context, AutoNamePreference.getAutoTrackName(context));
        }
    }

    /**
     * Get track summary
     *
@@ -453,10 +495,7 @@ class DbAccess implements AutoCloseable {
    @Nullable
    static TrackSummary getTrackSummary(Context context) {
        try (DbAccess dbAccess = getOpenInstance(context);
            Cursor positions = db.query(DbContract.Positions.TABLE_NAME,
                    new String[]{"*"},
                    null, null, null, null,
                    DbContract.Positions._ID)) {
            Cursor positions = dbAccess.getPositions()) {
            TrackSummary summary = null;
            if (positions.moveToFirst()) {
                double distance = 0.0;
@@ -621,6 +660,46 @@ class DbAccess implements AutoCloseable {
        return !cursor.isNull(cursor.getColumnIndex(DbContract.Positions.COLUMN_PROVIDER));
    }

    /**
     * Get comment from positions cursor
     *
     * @param cursor Cursor
     * @return String comment
     */
    static String getComment(Cursor cursor) {
        return cursor.getString(cursor.getColumnIndex(DbContract.Positions.COLUMN_COMMENT));
    }

    /**
     * Check if cursor contains image URI
     *
     * @param cursor Cursor
     * @return True if has image URI
     */
    static boolean hasImageUri(Cursor cursor) {
        return !cursor.isNull(cursor.getColumnIndex(DbContract.Positions.COLUMN_IMAGE_URI));
    }

    /**
     * Get image URI from positions cursor
     *
     * @param cursor Cursor
     * @return String URI
     */
    static String getImageUri(Cursor cursor) {
        return cursor.getString(cursor.getColumnIndex(DbContract.Positions.COLUMN_IMAGE_URI));
    }

    /**
     * Check if cursor contains comment data
     *
     * @param cursor Cursor
     * @return True if has comment data
     */
    static boolean hasComment(Cursor cursor) {
        return !cursor.isNull(cursor.getColumnIndex(DbContract.Positions.COLUMN_COMMENT));
    }

    /**
     * Get latitude from positions cursor
     *
+1 −3
Original line number Diff line number Diff line
@@ -54,9 +54,7 @@ public class ExternalCommandReceiver extends BroadcastReceiver {
     * @param context Context
     */
    private void startLoggerService(Context context) {
        if (DbAccess.getTrackName(context) == null) {
            DbAccess.newTrack(context, AutoNamePreference.getAutoTrackName(context));
        }
        DbAccess.newAutoTrack(context);
        Intent intent = new Intent(context, LoggerService.class);
        ContextCompat.startForegroundService(context, intent);
    }
+3 −0
Original line number Diff line number Diff line
@@ -171,6 +171,9 @@ public class GpxExportService extends IntentService {
                }
                writeTag(serializer, "time", DbAccess.getTimeISO8601(cursor));
                writeTag(serializer, "name", DbAccess.getID(cursor));
                if (DbAccess.hasComment(cursor)) {
                    writeTag(serializer, "desc", DbAccess.getComment(cursor));
                }

                // ulogger extensions (accuracy, speed, bearing, provider)
                serializer.startTag(null, "extensions");
+171 −51
Original line number Diff line number Diff line
@@ -9,23 +9,30 @@

package net.fabiszewski.ulogger;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.ImageDecoder;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
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.webkit.MimeTypeMap;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.Nullable;
import androidx.exifinterface.media.ExifInterface;
import androidx.preference.PreferenceManager;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
@@ -36,11 +43,16 @@ class ImageHelper {
    private static final String TAG = ImageHelper.class.getSimpleName();
    private static final String MEDIA_ORIENTATION = "orientation";
    private static final String JPEG_MIME = "image/jpg";
    private static final int ORIENTATION_90 = 90;
    private static final int ORIENTATION_180 = 180;
    private static final int ORIENTATION_270 = 270;
    private static final int ORIENTATION_NORMAL = 0;


    /**
     * Get orientation
     * First try MediaStore, then Exif
     *
     * @param context Context
     * @param uri     Image URI
     * @return Orientation in degrees
@@ -55,31 +67,34 @@ class ImageHelper {

    /**
     * Get orientation data from EXIF
     *
     * @param context Context
     * @param uri     Image URI
     * @return Orientation in degrees
     */
    private static int getOrientationExif(Context context, Uri uri) {
        int orientation = 0;
        int orientation = ORIENTATION_NORMAL;
        try (InputStream in = context.getContentResolver().openInputStream(uri)) {
            if (in != null) {
                ExifInterface exif = new ExifInterface(in);
                int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1);
                if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) {
                    orientation = 90;
                    orientation = ORIENTATION_90;
                } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_180) {
                    orientation = 180;
                    orientation = ORIENTATION_180;
                } else if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
                    orientation = 270;
                    orientation = ORIENTATION_270;
                }
            }
        } catch (Exception ignored) {}
        } catch (Exception ignored) {
        }
        if (Logger.DEBUG) { Log.d(TAG, "[getOrientationExif: " + orientation + "]"); }
        return orientation;
    }

    /**
     * Get orientation data from MediaStore
     *
     * @param context Context
     * @param uri     Image URI
     * @return Orientation in degrees
@@ -91,54 +106,32 @@ class ImageHelper {
            if (cursor != null && cursor.moveToFirst()) {
                orientation = cursor.getInt(0);
            }
        } catch (Exception ignored) {}
        } catch (Exception ignored) {
        }
        if (Logger.DEBUG) { Log.d(TAG, "[getOrientationMediaStore: " + orientation + "]"); }
        return orientation;
    }

    /**
     * Get bitmap from URI
     * @param context Context
     * @param uri URI
     * @return Bitmap
     * @throws IOException Exception
     * Creates pseudounique name based on timestamp
     *
     * @return Name
     */
    @RequiresApi(api = Build.VERSION_CODES.P)
    @NonNull
    private static Bitmap getBitmap(Context context, Uri uri) throws IOException {
        Bitmap bitmap;
        ImageDecoder.Source src = ImageDecoder.createSource(context.getContentResolver(), uri);
        bitmap = ImageDecoder.decodeBitmap(src);
        return bitmap;
    }
    /**
     * Get bitmap from URI
     * @param context Context
     * @param uri URI
     * @return Bitmap
     * @throws IOException Exception
     */
    @SuppressWarnings({"deprecation", "RedundantSuppression"})
    private static Bitmap getBitmapLegacy(Context context, Uri uri) throws IOException {
        Bitmap bitmap;
        bitmap = MediaStore.Images.Media.getBitmap(context.getContentResolver(), uri);
        return bitmap;
    }

    @NonNull
    private static String getFileName() {
    private static String getUniqueName() {
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
        return "ulogger_" + timeStamp + "_";
        return "ulogger_" + timeStamp;
    }

    /**
     * Create image uri in media collection
     *
     * @param context Context
     * @return URI
     */
    static Uri createImageUri(Context context) {
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.TITLE, getFileName());
        values.put(MediaStore.Images.Media.TITLE, getUniqueName());
        values.put(MediaStore.Images.Media.MIME_TYPE, JPEG_MIME);
        Uri collection;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -149,20 +142,20 @@ 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;

        Bitmap bitmap;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
            bitmap = ImageHelper.getBitmapLegacy(context, uri);
        } else {
            bitmap = ImageHelper.getBitmap(context, uri);
        ContentResolver cr = context.getContentResolver();
        try (InputStream is = cr.openInputStream(uri)) {
            bitmap = BitmapFactory.decodeStream(is, null, null);
        }
        Bitmap thumbBitmap = ThumbnailUtils.extractThumbnail(bitmap, sizePx, sizePx);
        bitmap.recycle();
        thumbBitmap = fixImageOrientation(context, uri, thumbBitmap);
        return thumbBitmap;

        bitmap = ThumbnailUtils.extractThumbnail(bitmap, sizePx, sizePx);
        bitmap = fixImageOrientation(context, uri, bitmap);
        return bitmap;
    }

    private static Bitmap fixImageOrientation(Context context, Uri uri, Bitmap bitmap) {
@@ -174,8 +167,135 @@ class ImageHelper {
                bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
            }
        } catch (Exception e) {
            if (Logger.DEBUG) { Log.d(TAG, "[setThumbnail exception: " + e + "]"); }
            if (Logger.DEBUG) { Log.d(TAG, "[fixImageOrientation exception: " + e + "]" ); }
        }
        return bitmap;
    }

    static long getFileSize(Context context, Uri uri) {
        long fileSize = 0;
        try (Cursor cursor = context.getContentResolver()
                .query(uri, null, null, null, null)) {
            if (cursor != null) {
                int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
                cursor.moveToFirst();
                fileSize = cursor.getLong(sizeIndex);
            }
        }
        return fileSize;
    }

    @Nullable
    static String getFileMime(Context context, 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());
        }
        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;
        }
        ContentResolver cr = context.getContentResolver();
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        try (InputStream is = cr.openInputStream(uri)) {
            BitmapFactory.decodeStream(is, null, options);
        }
        int srcWidth = Math.max(options.outWidth, options.outHeight);
        int scale = srcWidth / dstWidth;
        if (Logger.DEBUG) { Log.d(TAG, "[resampleIfNeeded scale: " + scale + "]"); }
        options = new BitmapFactory.Options();
        Bitmap bitmap = null;
        boolean retry = false;
        do {
            try {
                if (scale > 1) {
                    options.inScaled = true;
                    options.inSampleSize = 1;
                    options.inDensity = srcWidth;
                    options.inTargetDensity =  dstWidth * options.inSampleSize;
                }
                try (InputStream is = cr.openInputStream(uri)) {
                    bitmap = BitmapFactory.decodeStream(is, null, options);
                }
            } catch (OutOfMemoryError e) {
                if (Logger.DEBUG) { Log.d(TAG, "[resampleIfNeeded OutOfMemoryError]"); }

            }
            if (bitmap == null && scale > 1) {
                options.inSampleSize = scale;
                retry = !retry;
                if (Logger.DEBUG) { Log.d(TAG, "[resampleIfNeeded try sampling: " + retry + "]"); }
            }
        } while (retry);

        if (bitmap == null) {
            throw new IOException("Failed to decode image");
        }

        bitmap = fixImageOrientation(context, uri, bitmap);

        String filename = getUniqueName() + ".jpg";
        File outFile = new File(context.getCacheDir(), filename);
        try (FileOutputStream os = new FileOutputStream(outFile)) {
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
        }
        return Uri.fromFile(outFile);
    }

    static Uri moveToAppStorage(@NonNull Context context, @NonNull Uri inUri) {
        Uri outUri = null;
        String path = inUri.getPath();
        if (path != null) {
            if (!path.startsWith(context.getCacheDir().getPath())) {
                return inUri;
            }
            File inFile = new File(path);
            File outFile = new File(context.getFilesDir(), inFile.getName());
            if (inFile.renameTo(outFile)) {
                outUri = Uri.fromFile(outFile);
            } else {
                if (Logger.DEBUG) { Log.d(TAG, "[moveToAppStorage failed]"); }
            }
        }
        return outUri;
    }

    static void clearImageCache(@NonNull Context context) {
        File dir = context.getCacheDir();
        clearImages(dir);
    }

    static void clearTrackImages(@NonNull Context context) {
        File dir = context.getFilesDir();
        clearImages(dir);
    }

    private static void clearImages(File dir) {
        File[] files = dir.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isFile() && file.getPath().endsWith(".jpg") && file.delete()) {
                    if (Logger.DEBUG) { Log.d(TAG, "[clearImages deleted file " + file.getName() + "]"); }
                }
            }
        }
    }

    static void deleteImage(Context context, 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() + "]"); }
            }
        }
    }
}
+1 −6
Original line number Diff line number Diff line
@@ -62,12 +62,7 @@ public class RestartBroadcastReceiver extends BroadcastReceiver {
     * @param context Context
     */
    private void startLoggerService(Context context) {
        DbAccess db = DbAccess.getInstance();
        db.open(context);
        if (db.getTrackName() == null) {
            db.newTrack(AutoNamePreference.getAutoTrackName(context));
        }
        db.close();
        DbAccess.newAutoTrack(context);
        Intent intent = new Intent(context, LoggerService.class);
        ContextCompat.startForegroundService(context, intent);
    }
Loading