From feaa790674f013c8ab92e814a537ae7c1ed35f90 Mon Sep 17 00:00:00 2001 From: k3b <1374583+k3b@users.noreply.github.com> Date: Sat, 12 Dec 2020 20:14:58 +0100 Subject: [PATCH] Squashed commit of the following: commit 155a8fb1500a240323fb327e16bc38901e7c7731 Author: k3b <1374583+k3b@users.noreply.github.com> Date: Sat Dec 12 20:11:58 2020 +0100 #169: Update exif-file-info also updates mediadb without changing mediadb-id commit 04cef1afe4039d8603c1aa7d40970e0aa76bbe55 Author: k3b <1374583+k3b@users.noreply.github.com> Date: Fri Dec 11 12:58:29 2020 +0100 #155: Fix crash if missing file permissions commit 461b889a6acf6e666ac00d95ca24ddb6d51dde56 Author: k3b <1374583+k3b@users.noreply.github.com> Date: Thu Dec 10 20:27:05 2020 +0100 #169: Update exif-file-info also updates mediadb without changing mediadb-id commit 1f51e1ce75141f388fc9f2b38b7b1408aa810aaa Author: k3b <1374583+k3b@users.noreply.github.com> Date: Mon Sep 28 19:54:02 2020 +0200 #169: Experiment to trace file rename --- app/build.gradle | 2 +- .../androFotoFinder/AndroFotoFinderApp.java | 83 ++++++++--- .../media/AndroidExifInterfaceEx.java | 113 ++++++++++++++ ...otoPropertiesMediaDBCsvImportActivity.java | 2 +- .../queries/DatabaseHelper.java | 19 ++- .../androFotoFinder/queries/FotoSql.java | 4 +- .../queries/MediaContent2DBUpdateService.java | 2 +- .../queries/MediaDBRepository.java | 39 ++++- .../queries/MediaRepositoryApiWrapper.java | 46 +++--- .../android/androFotoFinder/tagDB/TagSql.java | 6 + .../k3b/android/io/AndroidFileCommands.java | 5 +- .../android/io/AndroidFileCommandsDbImpl.java | 138 ++++++++++++++++++ .../java/de/k3b/database/QueryParameter.java | 8 +- .../src/main/java/de/k3b/io/FileCommands.java | 62 ++++---- .../main/java/de/k3b/media/ExifInterface.java | 52 ++++--- .../java/de/k3b/media/ExifInterfaceEx.java | 4 +- .../media/PhotoPropertiesUpdateHandler.java | 3 +- 17 files changed, 489 insertions(+), 99 deletions(-) create mode 100644 app/src/main/java/de/k3b/android/io/AndroidFileCommandsDbImpl.java diff --git a/app/build.gradle b/app/build.gradle index bfdf146d..6b0ee2c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -19,7 +19,7 @@ android { minSdkVersion 14 // Android 4.0 Ice Cream Sandwich (API 14); Android 4.4 KitKat (API 19); Android 5.0 Lollipop (API 21); // Android 6.0 Marshmallow (API 23); Android 7.0 Nougat (API 24) - maxSdkVersion 28 // #155: android-10=api29 + // maxSdkVersion 28 // #155: android-10=api29 targetSdkVersion 28 diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/AndroFotoFinderApp.java b/app/src/main/java/de/k3b/android/androFotoFinder/AndroFotoFinderApp.java index 8706ffba..943993cd 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/AndroFotoFinderApp.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/AndroFotoFinderApp.java @@ -48,6 +48,7 @@ import de.k3b.android.androFotoFinder.queries.MediaContentproviderRepository; import de.k3b.android.androFotoFinder.queries.MediaContentproviderRepositoryImpl; import de.k3b.android.androFotoFinder.queries.MediaDBRepository; +import de.k3b.android.androFotoFinder.queries.MediaRepositoryApiWrapper; import de.k3b.android.androFotoFinder.queries.MergedMediaRepository; import de.k3b.android.io.AndroidFileFacade; import de.k3b.android.io.DocumentFileTranslator; @@ -106,31 +107,77 @@ public static void setMediaImageDbReplacement(Context context, boolean useMediaI Global.useAo10MediaImageDbReplacement = useMediaImageDbReplacement; - final MediaContentproviderRepository mediaContentproviderRepository = new MediaContentproviderRepository(context); if (Global.useAo10MediaImageDbReplacement) { - final SQLiteDatabase writableDatabase = DatabaseHelper.getWritableDatabase(context); - final MediaDBRepository mediaDBRepository = new MediaDBRepository(writableDatabase); - FotoSql.setMediaDBApi(new MergedMediaRepository(mediaDBRepository, mediaContentproviderRepository)); + registerAo10MediaImageDbReplacement(context); + } else { + registerMediaContentProvider(context, oldMediaDBApi); + } + } + } - MediaContent2DBUpdateService.instance = new MediaContent2DBUpdateService(context, writableDatabase); + private static void registerMediaContentProvider(Context context, IMediaRepositoryApi oldMediaDBApi) { + final MediaContentproviderRepository mediaContentproviderRepository = new MediaContentproviderRepository(context); + PhotoChangeNotifyer.unregisterContentObserver(context, GlobalMediaContentObserver.getInstance(context)); + if ((oldMediaDBApi != null) && (MediaContent2DBUpdateService.instance != null)) { + // switching from mediaImageDbReplacement to Contentprovider + MediaContent2DBUpdateService.instance.clearMediaCopy(); + } + FotoSql.setMediaDBApi(mediaContentproviderRepository); + MediaContent2DBUpdateService.instance = null; + } - if (FotoSql.getCount(new QueryParameter().addWhere("1 = 1")) == 0) { - // database is empty; reload from Contentprovider - MediaContent2DBUpdateService.instance.rebuild(context, null); - } + /** + * Android-10-ff use copy of media database for reading to circumvent android-10-media-contentprovider-restrictions + */ + private static IMediaRepositoryApi registerAo10MediaImageDbReplacement(Context context) { + File databaseFile = DatabaseHelper.getDatabasePath(context); + try { + final SQLiteDatabase writableDatabase = DatabaseHelper.getWritableDatabase(context); + //!!! throws SQLiteCantOpenDatabaseException("Failed to open database '/storage/emulated/0/databases/APhotoManager.db'") if no permission - PhotoChangeNotifyer.registerContentObserver(context, GlobalMediaContentObserver.getInstance(context)); + final MediaDBRepository mediaDBRepository = new MediaDBRepository(writableDatabase); + final MediaContentproviderRepository mediaContentproviderRepository = new MediaContentproviderRepository(context); - } else { - PhotoChangeNotifyer.unregisterContentObserver(context, GlobalMediaContentObserver.getInstance(context)); - if ((oldMediaDBApi != null) && (MediaContent2DBUpdateService.instance != null)) { - // switching from mediaImageDbReplacement to Contentprovider - MediaContent2DBUpdateService.instance.clearMediaCopy(); - } - FotoSql.setMediaDBApi(mediaContentproviderRepository); - MediaContent2DBUpdateService.instance = null; + // read from copy database, write to both: copy-database and content-provider + final MergedMediaRepository mediaDBApi = new MergedMediaRepository(mediaDBRepository, mediaContentproviderRepository); + FotoSql.setMediaDBApi(mediaDBApi); + + MediaContent2DBUpdateService.instance = new MediaContent2DBUpdateService(context, writableDatabase); + + if (FotoSql.getCount(new QueryParameter().addWhere("1 = 1")) == 0) { + // database is empty; reload from Contentprovider + MediaContent2DBUpdateService.instance.rebuild(context, null); } + + PhotoChangeNotifyer.registerContentObserver(context, GlobalMediaContentObserver.getInstance(context)); + return mediaDBApi; + } catch (RuntimeException ignore) { + Log.w(Global.LOG_CONTEXT, + "Cannot open Database (missing permissions) " + + DatabaseHelper.getDatabasePath(context) + " " + + ignore.getMessage(), ignore); + FotoSql.setMediaDBApi(new MediaDBRepositoryLoadOnDemand(context)); + } + return null; + } + + /** + * if Open Database failes because of missing File permissions + * postpone opening database until permission is granted + */ + private static class MediaDBRepositoryLoadOnDemand extends MediaRepositoryApiWrapper { + + private final Context context; + + public MediaDBRepositoryLoadOnDemand(Context context) { + super(null); + this.context = context; + } + + @Override + protected IMediaRepositoryApi getReadChild() { + return registerAo10MediaImageDbReplacement(context); } } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/media/AndroidExifInterfaceEx.java b/app/src/main/java/de/k3b/android/androFotoFinder/media/AndroidExifInterfaceEx.java index 21364ae7..19193708 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/media/AndroidExifInterfaceEx.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/media/AndroidExifInterfaceEx.java @@ -1,8 +1,35 @@ package de.k3b.android.androFotoFinder.media; +import android.content.ContentValues; +import android.util.Log; + +import java.io.IOException; +import java.util.Date; + +import de.k3b.android.androFotoFinder.Global; +import de.k3b.android.androFotoFinder.queries.FotoSql; +import de.k3b.android.androFotoFinder.tagDB.TagSql; +import de.k3b.io.FileUtils; +import de.k3b.io.StringUtils; +import de.k3b.io.VISIBILITY; +import de.k3b.io.collections.SelectedFiles; +import de.k3b.io.filefacade.IFile; import de.k3b.media.ExifInterfaceEx; +import de.k3b.media.PhotoPropertiesUtil; +/** + * Android specific Version of {@link ExifInterfaceEx} that updates the + * Database, when saving exif changes. + */ public class AndroidExifInterfaceEx extends ExifInterfaceEx { + // set to true to log what happens to database-ID when changing exif + private static final boolean DBG_RENAME_IN_DB_ENABLED = true; + + private boolean overwriteOriginal; + private String inPath; + private String outPath; + private Boolean hasXmp; + public static void init() { setFactory(new Factory() { @Override @@ -11,4 +38,90 @@ public ExifInterfaceEx create() { } }); } + + @Override + public void saveAttributes(IFile inFile, IFile outFile, + boolean deleteInFileOnFinish, Boolean hasXmp) throws IOException { + super.saveAttributes(inFile, outFile, deleteInFileOnFinish, hasXmp); + this.hasXmp = hasXmp; + } + + @Override + protected IFile renameSouraceFileBeforeReplaceOrThrow(IFile oldSourcefile, String newName) throws IOException { + debugIdPaths("renameSouraceFileBeforeReplaceOrThrow begin", oldSourcefile.getAbsolutePath(), newName); + this.overwriteOriginal = true; + this.inPath = oldSourcefile.getAbsolutePath(); + this.outPath = this.inPath + TMP_FILE_SUFFIX; + + if (!renameInDatabase(":renameSouraceFileBeforeReplaceOrThrow", this.inPath, this.outPath, false)) { + this.outPath = null; // failed + } + + final IFile result = super.renameSouraceFileBeforeReplaceOrThrow(oldSourcefile, newName); + debugIdPaths("renameSouraceFileBeforeReplaceOrThrow end", oldSourcefile.getAbsolutePath(), newName); + return result; + } + + @Override + protected void beforeCloseSaveOutputStream() { + if (this.outPath != null) { + renameInDatabase(":beforeCloseSaveOutputStream", this.outPath, this.inPath, true); + this.outPath = null; + } + super.beforeCloseSaveOutputStream(); + } + + // TODO additional database parameters (see scanner) + // DateLastModified, xmpDate, .... + private boolean renameInDatabase(String dbgContext, String fromPath, String toPath, boolean thransferExif) { + ContentValues values = new ContentValues(); + if (thransferExif) { + PhotoPropertiesMediaDBContentValues mediaValueAdapter = new PhotoPropertiesMediaDBContentValues().set(values, null); + + PhotoPropertiesUtil.copyNonEmpty(mediaValueAdapter, this); + + Date lastModified = new Date(); + TagSql.setFileModifyDate(values, lastModified); + if (this.hasXmp != null) { + if (this.hasXmp) { + TagSql.setXmpFileModifyDate(values, lastModified); + } else { + TagSql.setXmpFileModifyDate(values, TagSql.EXT_LAST_EXT_SCAN_NO_XMP); + } + } + } + values.put(FotoSql.SQL_COL_PATH, toPath); + debugIdPaths(dbgContext + " renameInDatabase begin", fromPath, toPath); + final int execResultCount = FotoSql.getMediaDBApi(). + execUpdate(this.getClass().getSimpleName() + dbgContext, fromPath, values, null); + + debugIdPaths(dbgContext + " renameInDatabase end " + execResultCount, fromPath, toPath); + if ((execResultCount != 1) && DBG_RENAME_IN_DB_ENABLED) { +// !!!! debug ausgabe path+ id failed + } + return 1 == execResultCount; + } + + private void debugIdPaths(String dbgContext, String... paths) { + if (DBG_RENAME_IN_DB_ENABLED) { + StringBuilder sqlWhere = new StringBuilder(); + for (String path : paths) { + if (sqlWhere.length() > 0) { + sqlWhere.append(" OR "); + } + sqlWhere.append("(").append(FotoSql.SQL_COL_PATH).append(" like '") + .append(FileUtils.replaceExtension(path, "")).append("%')"); + } + + // to prevent adding visibility + sqlWhere.append(" and " + + FotoSql.SQL_COL_EXT_MEDIA_TYPE + + " is not null"); + final SelectedFiles selectedfiles = FotoSql.getSelectedfiles(sqlWhere.toString(), VISIBILITY.PRIVATE_PUBLIC); + Log.d(Global.LOG_CONTEXT, dbgContext + "\n\t[" + + StringUtils.appendMessage(null, paths) + + "] :\n\t\t" + + selectedfiles.toIdString() + " -> " + selectedfiles.toPathListString()); + } + } } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/media/PhotoPropertiesMediaDBCsvImportActivity.java b/app/src/main/java/de/k3b/android/androFotoFinder/media/PhotoPropertiesMediaDBCsvImportActivity.java index c068d0f3..e087c754 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/media/PhotoPropertiesMediaDBCsvImportActivity.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/media/PhotoPropertiesMediaDBCsvImportActivity.java @@ -204,7 +204,7 @@ private void updateDB(String dbgContext, String _path, long xmlLastFileModifyDat TagSql.setXmpFileModifyDate(dbValues, xmlLastFileModifyDate); } - TagSql.setFileModifyDate(dbValues, new Date().getTime() / 1000); + TagSql.setFileModifyDate(dbValues, new Date()); mUpdateCount += TagSql.execUpdate(dbgContext, path, xmlLastFileModifyDate, dbValues, VISIBILITY.PRIVATE_PUBLIC); mItemCount++; diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/DatabaseHelper.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/DatabaseHelper.java index 62daffd6..14653642 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/queries/DatabaseHelper.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/DatabaseHelper.java @@ -24,6 +24,8 @@ import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; +import java.io.File; + import de.k3b.android.androFotoFinder.transactionlog.TransactionLogSql; import de.k3b.android.util.DatabaseContext; @@ -39,20 +41,33 @@ public class DatabaseHelper extends SQLiteOpenHelper { public static final int DATABASE_VERSION_2_MEDIA_DB_COPY = 2; public static final int DATABASE_VERSION = DatabaseHelper.DATABASE_VERSION_2_MEDIA_DB_COPY; + public static final String DATABASE_NAME = "APhotoManager"; private static DatabaseHelper instance = null; + private static DatabaseContext databaseContext = null; public DatabaseHelper(final Context context, final String databaseName) { super(context, databaseName, null, DatabaseHelper.DATABASE_VERSION); } public static SQLiteDatabase getWritableDatabase(Context context) { + return getInstance(context).getWritableDatabase(); + } + + public static File getDatabasePath(Context context) { + getInstance(context); + return databaseContext.getDatabasePath(DATABASE_NAME); + } + + private static DatabaseHelper getInstance(Context context) { if (instance == null) { - instance = new DatabaseHelper(new DatabaseContext(context), "APhotoManager"); + databaseContext = new DatabaseContext(context); + instance = new DatabaseHelper(databaseContext, DATABASE_NAME); } - return instance.getWritableDatabase(); + return instance; } + public static void version2Upgrade_RecreateMediDbCopy(final SQLiteDatabase db) { for (String sql : MediaDBRepository.Impl.DDL) { db.execSQL(sql); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/FotoSql.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/FotoSql.java index 4392141c..34bbc5f8 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/queries/FotoSql.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/FotoSql.java @@ -1127,9 +1127,9 @@ public static String getUriString(long imageID) { return SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME + "/" + imageID; } - public static SelectedFiles getSelectedfiles(String sqlWhere, VISIBILITY visibility) { + public static SelectedFiles getSelectedfiles(String sqlWhere, VISIBILITY visibility, String... parameters) { QueryParameter query = new QueryParameter(FotoSql.queryChangePath); - query.addWhere(sqlWhere); + query.addWhere(sqlWhere, parameters); query.addOrderBy(FotoSql.SQL_COL_PATH); return getSelectedfiles(query, FotoSql.SQL_COL_PATH, visibility); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaContent2DBUpdateService.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaContent2DBUpdateService.java index 0036b423..a663a295 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaContent2DBUpdateService.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaContent2DBUpdateService.java @@ -60,7 +60,7 @@ public void clearMediaCopy() { public void rebuild(Context context, IProgessListener progessListener) { long start = new Date().getTime(); clearMediaCopy(); - MediaDBRepository.Impl.updateMedaiCopy(context, writableDatabase, null, progessListener); + MediaDBRepository.Impl.updateMediaCopy(context, writableDatabase, null, progessListener); start = (new Date().getTime() - start) / 1000; final String text = "load db " + start + " secs"; Toast.makeText(context, text, Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaDBRepository.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaDBRepository.java index 81cdf536..09475844 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaDBRepository.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaDBRepository.java @@ -72,7 +72,7 @@ public class MediaDBRepository implements IMediaRepositoryApi { // #155 public static final boolean debugEnabledSqlRefresh = true; - private static final String MODUL_NAME = MediaContentproviderRepositoryImpl.class.getName(); + private static final String MODUL_NAME = MediaDBRepository.class.getSimpleName(); private static String currentUpdateReason = null; private static long currentUpdateId = 1; private static int transactionNumber = 0; @@ -360,6 +360,41 @@ public void endTransaction() { db.endTransaction(); } + /** + * generic method to get values from current MediaDBApi-Implementation + * + * @param fullFilePathFilter + * @param destination + * @param dbgContext + * @return + */ + public static ContentValues getContentValues(String fullFilePathFilter, ContentValues destination, String dbgContext) { + final String meldung = MODUL_NAME + ".getContentValues(" + dbgContext + "," + fullFilePathFilter + ")"; + QueryParameter query = new QueryParameter().addColumn(MediaDBRepository.Impl.USED_MEDIA_COLUMNS); + query.removeFirstColumnThatContains(FotoSql.SQL_COL_PK); + FotoSql.setWhereFileNames(query, fullFilePathFilter); + + Cursor c = null; + + try { + c = FotoSql.getMediaDBApi().createCursorForQuery(null, meldung, query, null, null); + if (c.moveToNext()) { + if (destination == null) { + destination = new ContentValues(); + } + return Impl.getContentValues(c, destination); + } + } catch (Exception ex) { + Log.e(LOG_TAG, meldung + + " error :", ex); + } finally { + if (c != null) c.close(); + } + + return null; + } + + public static class Impl { /** * SQL to create copy of contentprovider MediaStore.Images. @@ -561,7 +596,7 @@ public static void clearMedaiCopy(SQLiteDatabase db) { } - public static int updateMedaiCopy(Context context, SQLiteDatabase db, Date lastUpdate, IProgessListener progessListener) { + public static int updateMediaCopy(Context context, SQLiteDatabase db, Date lastUpdate, IProgessListener progessListener) { int progress = 0; java.util.Date startTime = new java.util.Date(); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaRepositoryApiWrapper.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaRepositoryApiWrapper.java index 702de338..0530b615 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaRepositoryApiWrapper.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaRepositoryApiWrapper.java @@ -33,9 +33,9 @@ * Created by k3b on 30.11.2019. */ public class MediaRepositoryApiWrapper implements IMediaRepositoryApi { - protected final IMediaRepositoryApi readChild; - protected final IMediaRepositoryApi writeChild; - protected final IMediaRepositoryApi transactionChild; + private final IMediaRepositoryApi readChild; + private final IMediaRepositoryApi writeChild; + private final IMediaRepositoryApi transactionChild; /** * count the non path write calls @@ -54,27 +54,27 @@ public MediaRepositoryApiWrapper(IMediaRepositoryApi readChild, IMediaRepository @Override public Cursor createCursorForQuery(StringBuilder out_debugMessage, String dbgContext, QueryParameter parameters, VISIBILITY visibility, CancellationSignal cancellationSignal) { - return readChild.createCursorForQuery(out_debugMessage, dbgContext, parameters, visibility, cancellationSignal); + return getReadChild().createCursorForQuery(out_debugMessage, dbgContext, parameters, visibility, cancellationSignal); } @Override public Cursor createCursorForQuery(StringBuilder out_debugMessage, String dbgContext, String from, String sqlWhereStatement, String[] sqlWhereParameters, String sqlSortOrder, CancellationSignal cancellationSignal, String... sqlSelectColums) { - return readChild.createCursorForQuery(out_debugMessage, dbgContext, from, sqlWhereStatement, sqlWhereParameters, sqlSortOrder, cancellationSignal, sqlSelectColums); + return getReadChild().createCursorForQuery(out_debugMessage, dbgContext, from, sqlWhereStatement, sqlWhereParameters, sqlSortOrder, cancellationSignal, sqlSelectColums); } @Override public int execUpdate(String dbgContext, long id, ContentValues values) { - return writeChild.execUpdate(dbgContext, id, values); + return getWriteChild().execUpdate(dbgContext, id, values); } @Override public int execUpdate(String dbgContext, String path, ContentValues values, VISIBILITY visibility) { - return writeChild.execUpdate(dbgContext, path, values, visibility); + return getWriteChild().execUpdate(dbgContext, path, values, visibility); } @Override public int exexUpdateImpl(String dbgContext, ContentValues values, String sqlWhere, String[] selectionArgs) { - return writeChild.exexUpdateImpl(dbgContext, values, sqlWhere, selectionArgs); + return getWriteChild().exexUpdateImpl(dbgContext, values, sqlWhere, selectionArgs); } /** @@ -88,7 +88,7 @@ public int exexUpdateImpl(String dbgContext, ContentValues values, String sqlWhe */ @Override public Long insertOrUpdateMediaDatabase(String dbgContext, String dbUpdateFilterJpgFullPathName, ContentValues values, VISIBILITY visibility, Long updateSuccessValue) { - return writeChild.insertOrUpdateMediaDatabase(dbgContext, dbUpdateFilterJpgFullPathName, values, visibility, updateSuccessValue); + return getWriteChild().insertOrUpdateMediaDatabase(dbgContext, dbUpdateFilterJpgFullPathName, values, visibility, updateSuccessValue); } /** @@ -99,7 +99,7 @@ public Long insertOrUpdateMediaDatabase(String dbgContext, String dbUpdateFilter */ @Override public Uri execInsert(String dbgContext, ContentValues values) { - return writeChild.execInsert(dbgContext, values); + return getWriteChild().execInsert(dbgContext, values); } /** @@ -107,36 +107,48 @@ public Uri execInsert(String dbgContext, ContentValues values) { */ @Override public int deleteMedia(String dbgContext, String where, String[] selectionArgs, boolean preventDeleteImageFile) { - return writeChild.deleteMedia(dbgContext, where, selectionArgs, preventDeleteImageFile); + return getWriteChild().deleteMedia(dbgContext, where, selectionArgs, preventDeleteImageFile); } @Override public ContentValues getDbContent(long id) { - return readChild.getDbContent(id); + return getReadChild().getDbContent(id); } @Override public long getCurrentUpdateId() { - return transactionChild.getCurrentUpdateId(); + return getTransactionChild().getCurrentUpdateId(); } @Override public boolean mustRequery(long updateId) { - return transactionChild.mustRequery(updateId); + return getTransactionChild().mustRequery(updateId); } @Override public void beginTransaction() { - transactionChild.beginTransaction(); + getTransactionChild().beginTransaction(); } @Override public void setTransactionSuccessful() { - transactionChild.setTransactionSuccessful(); + getTransactionChild().setTransactionSuccessful(); } @Override public void endTransaction() { - transactionChild.endTransaction(); + getTransactionChild().endTransaction(); + } + + protected IMediaRepositoryApi getReadChild() { + return readChild; + } + + protected IMediaRepositoryApi getWriteChild() { + return writeChild; + } + + protected IMediaRepositoryApi getTransactionChild() { + return transactionChild; } } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/tagDB/TagSql.java b/app/src/main/java/de/k3b/android/androFotoFinder/tagDB/TagSql.java index bd3ec960..b607cfa9 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/tagDB/TagSql.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/tagDB/TagSql.java @@ -318,6 +318,12 @@ public static void setFileModifyDate(ContentValues values, String path) { } } + public static void setFileModifyDate(ContentValues values, Date fileModifyDate) { + if (fileModifyDate != null) { + setFileModifyDate(values, fileModifyDate.getTime() / 1000); + } + } + public static void setFileModifyDate(ContentValues values, long fileModifyDateSecs) { if (fileModifyDateSecs != 0) { values.put(SQL_COL_LAST_MODIFIED, fileModifyDateSecs); diff --git a/app/src/main/java/de/k3b/android/io/AndroidFileCommands.java b/app/src/main/java/de/k3b/android/io/AndroidFileCommands.java index 9d7f2d9f..d5456224 100644 --- a/app/src/main/java/de/k3b/android/io/AndroidFileCommands.java +++ b/app/src/main/java/de/k3b/android/io/AndroidFileCommands.java @@ -61,7 +61,6 @@ import de.k3b.android.widget.FilePermissionActivity; import de.k3b.database.QueryParameter; import de.k3b.io.DirectoryFormatter; -import de.k3b.io.FileCommands; import de.k3b.io.FileUtils; import de.k3b.io.IDirectory; import de.k3b.io.IProgessListener; @@ -78,10 +77,10 @@ /** * Api to manipulate files/photos. * Same as FileCommands with update media database. - * + *
* Created by k3b on 03.08.2015.
*/
-public class AndroidFileCommands extends FileCommands {
+public class AndroidFileCommands extends AndroidFileCommandsDbImpl {
private static final String SETTINGS_KEY_LAST_COPY_TO_PATH = "last_copy_to_path";
private static final String mDebugPrefix = "AndroidFileCommands.";
private boolean isInBackground = false;
diff --git a/app/src/main/java/de/k3b/android/io/AndroidFileCommandsDbImpl.java b/app/src/main/java/de/k3b/android/io/AndroidFileCommandsDbImpl.java
new file mode 100644
index 00000000..f0dc49d8
--- /dev/null
+++ b/app/src/main/java/de/k3b/android/io/AndroidFileCommandsDbImpl.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2015-2020 by k3b.
+ *
+ * This file is part of AndroFotoFinder / #APhotoManager.
+ *
+ * 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
+ * Created by k3b on 03.08.2015.
+ */
+public class AndroidFileCommandsDbImpl extends FileCommands {
+ /**
+ * copies a file from the sourceFullPath path to the target path.
+ * Android specific: also updates database.
+ *
+ * @param sourceFullPath the path of the file that shall be copied including the file name with ending
+ * @param targetFullPath the path of the file that shall be written to with filename
+ */
+ @Override
+ protected boolean osFileCopy(IFile targetFullPath, IFile sourceFullPath) {
+ //!!! TODO muss noch getestet werden
+ final String srcPath = sourceFullPath.getAbsolutePath();
+ String toPath = null;
+ boolean dbSuccess = false;
+ if (PhotoPropertiesUtil.isImage(srcPath, PhotoPropertiesUtil.IMG_TYPE_ALL)) {
+ toPath = new File(targetFullPath.getFile(), targetFullPath.getName()).getAbsolutePath();
+ dbSuccess = (null != copyInDatabase("osFileCopy", srcPath, toPath));
+ }
+
+ if (dbSuccess) {
+ dbSuccess = super.osFileCopy(targetFullPath, sourceFullPath);
+ }
+ return dbSuccess;
+ }
+
+ /**
+ * Moves a file from the sourceFullPath path to the target path.
+ * Android specific: also updates database.
+ *
+ * @param sourceFullPath the path of the file that shall be copied including the file name with ending
+ * @param targetFullPath the path of the file that shall be written to with filename
+ */
+ @Override
+ protected boolean osFileMove(IFile targetFullPath, IFile sourceFullPath) {
+ final String srcPath = sourceFullPath.getAbsolutePath();
+ String toPath = null;
+ boolean dbSuccess = false;
+ // Database update must be done before super.osFileMove to avoid deleting/recreating old file entry
+ if (PhotoPropertiesUtil.isImage(srcPath, PhotoPropertiesUtil.IMG_TYPE_ALL)) {
+ toPath = new File(targetFullPath.getFile(), targetFullPath.getName()).getAbsolutePath();
+ dbSuccess = renameInDatabase("osFileMove", srcPath, toPath);
+ }
+ final boolean osSuccess = super.osFileMove(targetFullPath, sourceFullPath);
+ if (!osSuccess && dbSuccess) {
+ // os falled. Rollback
+ renameInDatabase("osFileMove-rollback", toPath, srcPath);
+ }
+ return osSuccess;
+ }
+
+ /**
+ * Renames a file from the sourceFullPath path to the target path.
+ * Android specific: also updates database.
+ *
+ * @param sourceFullPath the path of the file that shall be copied including the file name with ending
+ * @param targetFullPath the path of the file that shall be written to with filename
+ */
+ @Override
+ protected boolean osRenameTo(IFile targetFullPath, IFile sourceFullPath) {
+ final String srcPath = sourceFullPath.getAbsolutePath();
+ String toPath = null;
+ boolean dbSuccess = false;
+ if (PhotoPropertiesUtil.isImage(srcPath, PhotoPropertiesUtil.IMG_TYPE_ALL)) {
+ toPath = targetFullPath.getAbsolutePath();
+ dbSuccess = renameInDatabase("osRenameTo", srcPath, toPath);
+ }
+ final boolean osSuccess = super.osRenameTo(targetFullPath, sourceFullPath);
+ if (!osSuccess && dbSuccess) {
+ // os falled. Rollback
+ renameInDatabase("osRenameTo-rollback", toPath, srcPath);
+ }
+ return osSuccess;
+ }
+
+ @Override
+ protected boolean osDeleteFile(IFile file) {
+ return super.osDeleteFile(file);
+ }
+
+ private boolean renameInDatabase(String dbgContext, String fromPath, String toPath) {
+ ContentValues values = new ContentValues();
+ values.put(FotoSql.SQL_COL_PATH, toPath);
+ final int execResultCount = FotoSql.getMediaDBApi().
+ execUpdate(this.getClass().getSimpleName() + dbgContext, fromPath, values, null);
+
+ return 1 == execResultCount;
+ }
+
+ private Uri copyInDatabase(String dbgContext, String fromPath, String toPath) {
+ ContentValues values = MediaDBRepository.getContentValues(fromPath, null, dbgContext);
+ if (values != null) {
+ values.put(FotoSql.SQL_COL_PATH, toPath);
+ return FotoSql.getMediaDBApi().
+ execInsert(this.getClass().getSimpleName() + dbgContext, values);
+ }
+ return null;
+ }
+
+}
+
diff --git a/fotolib2/src/main/java/de/k3b/database/QueryParameter.java b/fotolib2/src/main/java/de/k3b/database/QueryParameter.java
index ec036ee3..115dd092 100644
--- a/fotolib2/src/main/java/de/k3b/database/QueryParameter.java
+++ b/fotolib2/src/main/java/de/k3b/database/QueryParameter.java
@@ -479,9 +479,11 @@ public String toString() {
/************************** local helpers *********************/
private QueryParameter addToList(final List
+ * Copies a file from the sourceFullPath path to the target path.
*/
private static boolean _osFileCopy(IFile targetFullPath, IFile sourceFullPath, FileCommands owner) {
boolean result = true;
@@ -454,22 +453,25 @@ protected int moveOrCopyFiles(final boolean move, String what, PhotoPropertiesDi
/**
* executes os specific move or copy operation and updates the list of modified files
+ *
+ * @param sourceFullPath the path of the file that shall be copied including the file name with ending
+ * @param targetFullPath the path of the file that shall be written to with filename
*/
- protected boolean osFileMoveOrCopy(boolean move, IFile dest, IFile source) {
+ protected boolean osFileMoveOrCopy(boolean move, IFile targetFullPath, IFile sourceFullPath) {
boolean result = false;
- long fileTime = source.lastModified();
+ long fileTime = sourceFullPath.lastModified();
if (move) {
- result = osFileMove(dest, source);
+ result = osFileMove(targetFullPath, sourceFullPath);
} else {
- result = osFileCopy(dest, source);
+ result = osFileCopy(targetFullPath, sourceFullPath);
}
- if (dest.lastModified() != fileTime) {
- dest.setLastModified(fileTime);
+ if (targetFullPath.lastModified() != fileTime) {
+ targetFullPath.setLastModified(fileTime);
}
if (result) {
- addProcessedFiles(move, dest, source);
+ addProcessedFiles(move, targetFullPath, sourceFullPath);
}
return result;
@@ -484,35 +486,38 @@ private void addProcessedFiles(boolean move, IFile dest, IFile source) {
/**
* can be replaced by mock/stub in unittests
+ *
+ * @param sourceFullPath the path of the file that shall be copied including the file name with ending
+ * @param targetFullPath the path of the file that shall be written to with filename
*/
- protected boolean osFileMove(IFile dest, IFile source) {
- if (osRenameTo(dest, source)) {
+ protected boolean osFileMove(IFile targetFullPath, IFile sourceFullPath) {
+ if (osRenameTo(targetFullPath, sourceFullPath)) {
// move within same mountpoint
if (LibGlobal.debugEnabledJpg) {
- logger.info("osFileMove(rename) '" + source
- + "' => '" + dest + "'");
+ logger.info("osFileMove(rename) '" + sourceFullPath
+ + "' => '" + targetFullPath + "'");
}
return true;
}
// #61 cannot move between different mountpoints/devices/partitions. do Copy+Delete instead
- if (osFileExists(source) && source.isFile() && source.canRead()
- && source.canWrite() // to delete after success
- && !osFileExists(dest)
- && osFileCopy(dest, source)) {
- if (osDeleteFile(source)) {
+ if (osFileExists(sourceFullPath) && sourceFullPath.isFile() && sourceFullPath.canRead()
+ && sourceFullPath.canWrite() // to delete after success
+ && !osFileExists(targetFullPath)
+ && osFileCopy(targetFullPath, sourceFullPath)) {
+ if (osDeleteFile(sourceFullPath)) {
if (LibGlobal.debugEnabledJpg) {
- logger.info("osFileMove(copy+delete) '" + source
- + "' => '" + dest + "'");
+ logger.info("osFileMove(copy+delete) '" + sourceFullPath
+ + "' => '" + targetFullPath + "'");
}
- return true; // move: copy + delete(source) : success
+ return true; // move: copy + delete(sourceFullPath) : success
} else {
// cannot delete souce: undo copy
if (LibGlobal.debugEnabledJpg) {
- logger.info("osFileMove failed for '" + source
- + "' => '" + dest + "'");
+ logger.info("osFileMove failed for '" + sourceFullPath
+ + "' => '" + targetFullPath + "'");
}
- osDeleteFile(dest);
+ osDeleteFile(targetFullPath);
}
}
return false;
@@ -530,11 +535,10 @@ protected boolean osRenameTo(IFile dest, IFile source) {
}
/**
+ * Copies a file from the sourceFullPath path to the target path.
*
* @param sourceFullPath the path of the file that shall be copied including the file name with ending
- * @param targetFullPath the path of the file that shall be written to without filename
- *
- * Copies a file from the sourceFullPath path to the target path.
+ * @param targetFullPath the path of the file that shall be written to with filename
*/
protected boolean osFileCopy(IFile targetFullPath, IFile sourceFullPath) {
return _osFileCopy(targetFullPath, sourceFullPath, this);
diff --git a/fotolib2/src/main/java/de/k3b/media/ExifInterface.java b/fotolib2/src/main/java/de/k3b/media/ExifInterface.java
index cb05b66b..979860c1 100644
--- a/fotolib2/src/main/java/de/k3b/media/ExifInterface.java
+++ b/fotolib2/src/main/java/de/k3b/media/ExifInterface.java
@@ -85,14 +85,25 @@ public class ExifInterface {
// false for unittests because UserComment = null is not implemented for COM - Marker
protected static boolean fixDateOnSave = true;
+ // Used when overwriting original file
+ protected static final String TMP_FILE_SUFFIX = ".tmp";
+
// The Exif tag names
- /** Type is String. */
+ /**
+ * Type is String.
+ */
public static final String TAG_ARTIST = "Artist";
- /** Type is int. @hide */
+ /**
+ * Type is int. @hide
+ */
public static final String TAG_BITS_PER_SAMPLE = "BitsPerSample";
- /** Type is int. @hide */
+ /**
+ * Type is int. @hide
+ */
public static final String TAG_COMPRESSION = "Compression";
- /** Type is String. */
+ /**
+ * Type is String.
+ */
public static final String TAG_COPYRIGHT = "Copyright";
/** Type is String. @hide */
public static final String TAG_DATETIME = "DateTime";
@@ -1163,20 +1174,23 @@ public String getDebugString(String lineDelimiter, String... _keysToExclude) {
* and make a single call rather than multiple calls for each attribute.
*/
public void saveAttributes() throws IOException {
- saveAttributes(mExifFile, mExifFile, true);
+ saveAttributes(mExifFile, mExifFile, true, null);
}
/**
* Old File based implementation.
*
- * @deprecated use {@link #saveAttributes(IFile, IFile, boolean)} instead
+ * @deprecated use {@link #saveAttributes(IFile, IFile, boolean, Boolean)} instead
*/
@Deprecated
public void saveAttributes(File inFile, File outFile, boolean deleteInFileOnFinish) throws IOException {
- saveAttributes(FileFacade.convert("ExifInterface.saveAttributes in", inFile), FileFacade.convert("ExifInterface.saveAttributes out", outFile), deleteInFileOnFinish);
+ saveAttributes(FileFacade.convert("ExifInterface.saveAttributes in", inFile),
+ FileFacade.convert("ExifInterface.saveAttributes out", outFile),
+ deleteInFileOnFinish, null);
}
- public void saveAttributes(IFile inFile, IFile outFile, boolean deleteInFileOnFinish) throws IOException {
+ public void saveAttributes(IFile inFile, IFile outFile,
+ boolean deleteInFileOnFinish, Boolean hasXmp) throws IOException {
String debugContext = String.format("%s.saveAttributes(%s=>%s,deleteInFileOnFinish=%s)", this.getClass().getSimpleName(), inFile, outFile, deleteInFileOnFinish);
IFile currentOutFile = outFile;
fixAttributes();
@@ -1188,9 +1202,8 @@ public void saveAttributes(IFile inFile, IFile outFile, boolean deleteInFileOnFi
if (overwriteOriginal) {
final String name = inFile.getName();
- final String tempName = name + ".tmp";
- renameOrThrow(inFile, tempName);
- inFile = inFile.getParentFile().create(tempName);
+ final String tempName = name + TMP_FILE_SUFFIX;
+ inFile = renameSouraceFileBeforeReplaceOrThrow(inFile, tempName);
currentOutFile = outFile.getParentFile().create(name);
}
@@ -1207,13 +1220,14 @@ public void saveAttributes(IFile inFile, IFile outFile, boolean deleteInFileOnFi
mThumbnailBytes = null;
}
- private void renameOrThrow(IFile file, String newName) throws IOException {
- logDebug(String.format("rename %s to %s", file, newName));
+ protected IFile renameSouraceFileBeforeReplaceOrThrow(IFile oldSourcefile, String newName) throws IOException {
+ logDebug(String.format("rename %s to %s", oldSourcefile, newName));
- if (!file.renameTo(newName)) {
- throw new IOException("Could'nt rename sourcefile from " + file +
+ if (!oldSourcefile.renameTo(newName)) {
+ throw new IOException("Could'nt rename sourcefile from " + oldSourcefile +
" to " + newName);
}
+ return oldSourcefile.getParentFile().create(newName);
}
/** repairs wrong/missing attributes */
@@ -1273,7 +1287,7 @@ public byte[] getThumbnail() {
}
/**
- * @deprecated use {@link #saveAttributes(IFile, IFile, boolean)} instead
+ * @deprecated use {@link #getThumbnail(IFile)} instead
*/
@Deprecated
public byte[] getThumbnail(File inFile) {
@@ -1719,11 +1733,15 @@ public void saveJpegAttributes(InputStream inputStream, OutputStream outputStrea
}
}
} finally {
- closeSilently(dataOutputStream, "ExifInterface saveJpegAttributes out " + outputStream);
closeSilently(dataInputStream, "ExifInterface saveJpegAttributes in " + dataInputStream);
+ beforeCloseSaveOutputStream();
+ closeSilently(dataOutputStream, "ExifInterface saveJpegAttributes out " + outputStream);
}
}
+ protected void beforeCloseSaveOutputStream() {
+ }
+
// Reads the given EXIF byte area and save its tag data into attributes.
private void readExifSegment(byte[] exifBytes, int exifOffsetFromBeginning) throws IOException {
// Parse TIFF Headers. See JEITA CP-3451C Table 1. page 10.
diff --git a/fotolib2/src/main/java/de/k3b/media/ExifInterfaceEx.java b/fotolib2/src/main/java/de/k3b/media/ExifInterfaceEx.java
index 3ecdae38..ac3d9467 100644
--- a/fotolib2/src/main/java/de/k3b/media/ExifInterfaceEx.java
+++ b/fotolib2/src/main/java/de/k3b/media/ExifInterfaceEx.java
@@ -169,9 +169,9 @@ public static int getOrientationId(IFile fullPath) {
}
@Override
- public void saveAttributes(IFile inFile, IFile outFile, boolean deleteInFileOnFinish) throws IOException {
+ public void saveAttributes(IFile inFile, IFile outFile, boolean deleteInFileOnFinish, Boolean hasXmp) throws IOException {
fixDateTakenIfNeccessary(inFile);
- super.saveAttributes(inFile, outFile, deleteInFileOnFinish);
+ super.saveAttributes(inFile, outFile, deleteInFileOnFinish, hasXmp);
setFilelastModified(outFile);
}
diff --git a/fotolib2/src/main/java/de/k3b/media/PhotoPropertiesUpdateHandler.java b/fotolib2/src/main/java/de/k3b/media/PhotoPropertiesUpdateHandler.java
index 42c8eaab..7d311baa 100644
--- a/fotolib2/src/main/java/de/k3b/media/PhotoPropertiesUpdateHandler.java
+++ b/fotolib2/src/main/java/de/k3b/media/PhotoPropertiesUpdateHandler.java
@@ -278,7 +278,8 @@ private int transferExif(String dbg_context) throws IOException {
exif.saveAttributes(
inJpgFullPath,
outJpgFullPath,
- this.deleteOriginalAfterFinish);
+ this.deleteOriginalAfterFinish,
+ this.xmp != null);
} else if (!isSameFile) {
// changes are NOT written to exif. Do File copy instead.
FileUtils.copyReplace(inJpgFullPath, outJpgFullPath, this.deleteOriginalAfterFinish, dbg_context + "-transferExif");