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 + */ + +package de.k3b.android.io; + +import android.content.ContentValues; +import android.net.Uri; + +import java.io.File; + +import de.k3b.android.androFotoFinder.queries.FotoSql; +import de.k3b.android.androFotoFinder.queries.MediaDBRepository; +import de.k3b.io.FileCommands; +import de.k3b.io.filefacade.IFile; +import de.k3b.media.PhotoPropertiesUtil; + +/** + * Api to manipulate files/photos. + * Same as FileCommands with update media database. + *

+ * 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 list, boolean allowNull, final String[] parameters) { - for (String parameter : parameters) { - if ((allowNull) || (parameter != null)) { - list.add(parameter); + if (parameters != null) { + for (String parameter : parameters) { + if ((allowNull) || (parameter != null)) { + list.add(parameter); + } } } return this; diff --git a/fotolib2/src/main/java/de/k3b/io/FileCommands.java b/fotolib2/src/main/java/de/k3b/io/FileCommands.java index 5e23025e..ccd56151 100644 --- a/fotolib2/src/main/java/de/k3b/io/FileCommands.java +++ b/fotolib2/src/main/java/de/k3b/io/FileCommands.java @@ -127,11 +127,10 @@ protected boolean canProcessFile(int opCode) { } /** - * * @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 + *

+ * 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");