diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..f81639ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Smartphone (please complete the following information):** + - Android version [e.g. Android-7.1] : + - [A Photo Manager Version](https://github.com/k3b/APhotoManager/releases) (i.e. 0.4.6.160304) : + +**Additional context** +Add any other context about the problem here. + +**Crash Report** +If you report an app crash: Can you add the crash logfile to this ticket? + +When APhotoManager crashes it tries to write a crash log file in the [**Error Log Folder**](https://github.com/k3b/APhotoManager/wiki/settings#logfolder). + +* [ExternalStorageDirectory]/copy/log/androFotofinder.logcat-2017....txt + * i.e. /storage/sdcard0/copy/log/androFotofinder.logcat-20170728-135704.txt + * crash data from 2017-07-28 13:57 + +For more details on Error Loging see [diagnostic settings](https://github.com/k3b/APhotoManager/wiki/settings#Diagnostics). diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..26971205 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,26 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. + +* Affected Module: + * [ ] [Gallery-View](https://github.com/k3b/APhotoManager/wiki/Gallery-View) + * [ ] [Geographic-Map](https://github.com/k3b/APhotoManager/wiki/geographic-map) + * [ ] [Image-View](https://github.com/k3b/APhotoManager/wiki/Image-View) + * [ ] [Filtering](https://github.com/k3b/APhotoManager/wiki/Filter-View) + * [ ] [Folder-Picker](https://github.com/k3b/APhotoManager/wiki/Folder-Picker) + * [ ] [Settings](https://github.com/k3b/APhotoManager/wiki/settings) + * [ ] [Intent API](https://github.com/k3b/APhotoManager/wiki/intentapi) diff --git a/.github/old_ISSUE_TEMPLATE.md b/.github/feature_request.md similarity index 53% rename from .github/old_ISSUE_TEMPLATE.md rename to .github/feature_request.md index cb1e772b..6e7c2330 100644 --- a/.github/old_ISSUE_TEMPLATE.md +++ b/.github/feature_request.md @@ -1,17 +1,15 @@ -### Expected behavior +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -... +**Describe the solution you'd like** +A clear and concise description of what you want to happen. -### Actual behavior +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. -... +**Additional context** +Add any other context or screenshots about the feature request here. -### Environment - -* Android Version (i.e. 5.1): - * ... -* [A Photo Manager Version](https://github.com/k3b/APhotoManager/releases) (i.e. 0.4.6.160304) : - * ... * Affected Module: * [ ] [Gallery-View](https://github.com/k3b/APhotoManager/wiki/Gallery-View) * [ ] [Geographic-Map](https://github.com/k3b/APhotoManager/wiki/geographic-map) diff --git a/FAQs b/FAQs new file mode 100644 index 00000000..e80fb2f4 --- /dev/null +++ b/FAQs @@ -0,0 +1,52 @@ +Q. What is APhotoManager? + +A. APhotoManager is an opensource mobile gallery app that can manage your local photos. + +Q. What is geotagging? + +A. Geotagging is the act of assigning geographical location to a photographs or videos. + +Q. How do I add geotags to my photos using APhotoManager? + +A. Highlight the photos(s), click the option button and select "set geo" option, then you can decide to pick a location from map or from an existing photo. + +Q. What is exif data? + +A. Exif stands for Exchangeable image file format. It allows you to store certain information within your photos.This information is known as "metadata" and can include things like the date and time the shot was taken, camera settings like shutter speed and focal length, and copyright information. + +Q. How do I add, remove or edit exif data on my photo using APhotoManager? + +A. Highlight the photo(s), click the option button and select "set geo" option, then you can decide to pick a location from map or from an existing photo. + + +Q. Can I select a folder to view pictures? + +A. Yes, by clicking the folder icon on the top, you are shown all the folders that contain pictures on your deivce, selecting any folder and clicking ok allows you to view only pictures contained in that folder. + +Q. How do I find pictures on APhotoManager? + +A. Specific photos can be found on APhotoManager using the filter option by providing some information of the photo which may include the filename, path, tags etc. Providing any of those information will open up results relating to information provided. + +Q. Can I sort my photos on APhotoManager? + +A. Yes the sorting feature is available on APhotoManager. Photos can be sorted by name, date, place, rating, last modified, width, size and file path length, all in ascending order. + +Q. Can I share photos from APhotoManager? + +A. You can share photos from APhotoManager to any other location that is supported for such operation. To share photo(s), from APhotomanager you can highlight the picture(s) and the share button will be visible at the top, clicking it will pop-up several locations where the picture(s) can be shared to. The share button is automatically visible when a picture is viewed. + + +Q. Which photo management options are available on APhotoManager? + +A. File management options like, rename, copy and move are available on APhotoManager. + +Q. Are my photos uploaded to third party apps? + +A. No, photos are stored locally on the device, there's no upload to third party applications. + +Q. Does APhotoManager contain ads? + +A. APhotoManager does not contain ads, no usertracking, it is absolutely free and open source. + + +Thank you for using APhotoManager, if you like the app and wish to make contributions to the project, contact the developer via [github](https://github.com/k3b/APhotoManager) diff --git a/app/build.gradle b/app/build.gradle index 30836a1f..bfad9aa9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,10 +14,13 @@ android { defaultConfig { // fdroid-release 'de.k3b.android.androFotoFinder' // main-develop-branch 'de.k3b.android.androFotoFinder.dev' - applicationId 'de.k3b.android.androFotoFinder.dev' - + // applicationId 'de.k3b.android.androFotoFinder' + applicationId 'de.k3b.android.androFotoFinder' + 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 + targetSdkVersion 21 // non-fdroid release diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/AdapterArrayHelper.java b/app/src/main/java/de/k3b/android/androFotoFinder/AdapterArrayHelper.java index 36a1dcd5..e0c215d7 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/AdapterArrayHelper.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/AdapterArrayHelper.java @@ -26,9 +26,9 @@ import java.util.ArrayList; import de.k3b.android.androFotoFinder.queries.FotoSql; +import de.k3b.io.FileUtils; import de.k3b.io.VISIBILITY; import de.k3b.io.collections.SelectedItems; -import de.k3b.io.FileUtils; import de.k3b.media.PhotoPropertiesUtil; /** @@ -47,7 +47,7 @@ public AdapterArrayHelper(Activity context, String fullPhotoPath, String debugCo if (Global.mustRemoveNOMEDIAfromDB && (mRootDir != null) && (mFullPhotoPaths != null)) { String parentDirString = mRootDir.getAbsolutePath(); - FotoSql.execDeleteByPath(debugContext + " AdapterArrayHelper mustRemoveNOMEDIAfromDB ", context, parentDirString, VISIBILITY.PRIVATE_PUBLIC); + FotoSql.execDeleteByPath(debugContext + " AdapterArrayHelper mustRemoveNOMEDIAfromDB ", parentDirString, VISIBILITY.PRIVATE_PUBLIC); } } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/AffUtils.java b/app/src/main/java/de/k3b/android/androFotoFinder/AffUtils.java index e68386cb..8406b0b2 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/AffUtils.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/AffUtils.java @@ -122,7 +122,7 @@ public static SelectedFiles querySelectedFiles(Context context, SelectedItems it List paths = new ArrayList(); List datesPhotoTaken = new ArrayList(); - if (FotoSql.getFileNames(context, items, ids, paths, datesPhotoTaken) != null) { + if (FotoSql.getFileNames(items, ids, paths, datesPhotoTaken) != null) { return new SelectedFiles(paths.toArray(new String[paths.size()]), ids.toArray(new Long[ids.size()]), datesPhotoTaken.toArray(new Date[datesPhotoTaken.size()])); } } 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 97b76eaf..00ba24d7 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/AndroFotoFinderApp.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/AndroFotoFinderApp.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2019 by k3b. + * Copyright (c) 2015-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -19,8 +19,10 @@ package de.k3b.android.androFotoFinder; +import android.app.Activity; import android.app.Application; import android.content.Context; +import android.database.sqlite.SQLiteDatabase; import android.support.annotation.NonNull; import android.util.Log; import android.widget.Toast; @@ -36,11 +38,21 @@ import de.k3b.LibGlobal; import de.k3b.android.GuiUtil; import de.k3b.android.androFotoFinder.imagedetail.HugeImageLoader; +import de.k3b.android.androFotoFinder.queries.DatabaseHelper; import de.k3b.android.androFotoFinder.queries.FotoSql; import de.k3b.android.androFotoFinder.queries.FotoSqlBase; +import de.k3b.android.androFotoFinder.queries.GlobalMediaContentObserver; +import de.k3b.android.androFotoFinder.queries.IMediaRepositoryApi; +import de.k3b.android.androFotoFinder.queries.MediaContent2DBUpdateService; +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.MergedMediaRepository; import de.k3b.android.osmdroid.forge.MapsForgeSupport; import de.k3b.android.util.LogCat; +import de.k3b.android.util.PhotoChangeNotifyer; import de.k3b.android.widget.ActivityWithCallContext; +import de.k3b.android.widget.LocalizedActivity; import de.k3b.database.QueryParameter; import de.k3b.io.PhotoAutoprocessingDto; import de.k3b.media.ExifInterface; @@ -58,6 +70,10 @@ public class AndroFotoFinderApp extends Application { private static String fileNamePrefix = "androFotofinder.logcat-"; + public static MediaContent2DBUpdateService getMediaContent2DbUpdateService() { + return MediaContent2DBUpdateService.instance; + } + private LogCat mCrashSaveToFile = null; @@ -76,6 +92,43 @@ public static String getGetTeaserText(Context context, String linkUrlForDetails) return result; } + public static void setMediaImageDbReplacement(Context context, boolean useMediaImageDbReplacement) { + final IMediaRepositoryApi oldMediaDBApi = FotoSql.getMediaDBApi(); + if ((oldMediaDBApi == null) || (Global.useAo10MediaImageDbReplacement != useMediaImageDbReplacement)) { + + // menu must be recreated + LocalizedActivity.setMustRecreate(); + + 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)); + + 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)); + + } 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; + } + } + } + /* private RefWatcher refWatcher; @@ -114,7 +167,10 @@ public static RefWatcher getRefWatcher(Context context) { mCrashSaveToFile = new LogCat(Global.LOG_CONTEXT, HugeImageLoader.LOG_TAG, PhotoViewAttacher.LOG_TAG, CupcakeGestureDetector.LOG_TAG, LibGlobal.LOG_TAG, ThumbNailUtils.LOG_TAG, IMapView.LOGTAG, - ExifInterface.LOG_TAG, PhotoPropertiesImageReader.LOG_TAG) { + ExifInterface.LOG_TAG, PhotoPropertiesImageReader.LOG_TAG, + FotoSql.LOG_TAG, + MediaDBRepository.LOG_TAG, + MediaContentproviderRepositoryImpl.LOG_TAG) { @Override public void uncaughtException(Thread thread, Throwable ex) { @@ -124,13 +180,14 @@ public void uncaughtException(Thread thread, Throwable ex) { super.uncaughtException(thread, ex); } - public void saveToFile() { + public void saveToFile(Activity activity) { final File logFile = getOutpuFile(); String message = (logFile != null) ? "saving errorlog ('LocCat') to " + logFile.getAbsolutePath() : "Saving errorlog ('LocCat') is disabled. See Settings 'Diagnostics' for details"; Log.e(Global.LOG_CONTEXT, message); - Toast.makeText(AndroFotoFinderApp.this , message, Toast.LENGTH_LONG).show(); + final Context context = (activity != null) ? activity : AndroFotoFinderApp.this; + Toast.makeText(context, message, Toast.LENGTH_LONG).show(); saveLogCat(logFile, null, mTags); } @@ -165,7 +222,7 @@ private File getOutpuFile() { // #60: configure some of the mapsforge settings first MapsForgeSupport.createInstance(this); - FotoSql.deleteMediaWithNullPath(this); + FotoSql.deleteMediaWithNullPath(); Log.i(Global.LOG_CONTEXT, getAppId() + " created"); } @@ -185,9 +242,9 @@ public void onTerminate() { super.onTerminate(); } - public void saveToFile() { + public void saveToFile(Activity activity) { if (mCrashSaveToFile != null) { - mCrashSaveToFile.saveToFile(); + mCrashSaveToFile.saveToFile(activity); } } public void clear() { diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/AndroidTransactionLogger.java b/app/src/main/java/de/k3b/android/androFotoFinder/AndroidTransactionLogger.java index bdc68c6f..a06087ed 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/AndroidTransactionLogger.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/AndroidTransactionLogger.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 by k3b. + * Copyright (c) 2017-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -19,8 +19,6 @@ package de.k3b.android.androFotoFinder; -import android.app.Activity; - import java.io.Closeable; import java.io.IOException; @@ -37,7 +35,7 @@ public class AndroidTransactionLogger extends TransactionLoggerBase implements Closeable { private AndroidFileCommands execLog; - public AndroidTransactionLogger(Activity ctx, long now, AndroidFileCommands execLog) { + public AndroidTransactionLogger(AndroidFileCommands execLog, long now) { super(execLog, now); this.execLog = execLog; diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/FotoGalleryActivity.java b/app/src/main/java/de/k3b/android/androFotoFinder/FotoGalleryActivity.java index 41ca82d3..736386e3 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/FotoGalleryActivity.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/FotoGalleryActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2018 by k3b. + * Copyright (c) 2015-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -44,8 +44,8 @@ import de.k3b.android.widget.AboutDialogPreference; import de.k3b.android.widget.BaseQueryActivity; import de.k3b.database.QueryParameter; -import de.k3b.io.collections.SelectedItems; import de.k3b.io.IDirectory; +import de.k3b.io.collections.SelectedItems; /** * Gallery: Show zeoro or more images in a grid optionally filtered by a @@ -167,6 +167,11 @@ public boolean onCreateOptionsMenu(Menu menu) { inflater.inflate(R.menu.menu_gallery_non_selected_only, menu); inflater.inflate(R.menu.menu_gallery_non_multiselect, menu); + + if (Global.useAo10MediaImageDbReplacement) { + inflater.inflate(R.menu.menu_ao10, menu); + } + /* getActionBar().setListNavigationCallbacks(); MenuItem sorter = menu.getItem(R.id.cmd_sort); @@ -202,6 +207,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.cmd_about: AboutDialogPreference.createAboutDialog(this).show(); return true; + case R.id.cmd_db_reload: + AndroFotoFinderApp.getMediaContent2DbUpdateService().rebuild(this, null); + return true; case R.id.cmd_more: new Handler().postDelayed(new Runnable() { public void run() { diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/Global.java b/app/src/main/java/de/k3b/android/androFotoFinder/Global.java index c307438f..0d0d8cdb 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/Global.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/Global.java @@ -20,6 +20,7 @@ package de.k3b.android.androFotoFinder; import android.content.Context; +import android.os.Build; import android.os.Environment; import android.util.Log; import android.view.Menu; @@ -124,11 +125,26 @@ public static class Media { public static boolean initialImageDetailResolutionHigh = false; // false: MediaStore.Images.Thumbnails.MINI_KIND; true: FULL_SCREEN_KIND; public static boolean mapsForgeEnabled = false; + // #153: Feature-Toggel: set to false while rename multible is not implemented completely + public final static boolean allowRenameMultible = false; + + // #155: Feature-Toggel: set to false while android10 incompatibility is not fixed + public final static boolean allow_emulate_ao10 = !isAndroid10OrAbove() && false; + // #155: fix android10 incompatibility + // Build.VERSION_CODES.??ANDROID10?? = 29 + public static boolean useAo10MediaImageDbReplacement = isAndroid10OrAbove(); + /** map with blue selection markers: how much to area to increase */ public static final double mapMultiselectionBoxIncreaseByProcent = 100.0; /** map with blue selection markers: minimum size of zoom box in degrees */ public static final double mapMultiselectionBoxIncreaseMinSizeInDegrees = 0.01; + private static boolean isAndroid10OrAbove() { + // #155: fix android10 incompatibility + // Build.VERSION_CODES.??ANDROID10?? = 29 + return Build.VERSION.SDK_INT >= 29; + } + public static void debugMemory(String modul, String message) { if (Global.debugEnabledMemory) { Runtime r = Runtime.getRuntime(); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/LockScreen.java b/app/src/main/java/de/k3b/android/androFotoFinder/LockScreen.java index cfffb82c..f9e2ce64 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/LockScreen.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/LockScreen.java @@ -28,6 +28,7 @@ import android.view.MenuItem; import de.k3b.android.util.MenuUtils; +import de.k3b.android.widget.LocalizedActivity; /** * #105: Management of app locking (aka Android "Screen"-pinning, "Kiosk Mode", "LockTask") @@ -52,6 +53,7 @@ public static boolean onOptionsItemSelected(Activity parent, MenuItem item) { Global.locked = true; SettingsActivity.global2Prefs(parent.getApplication()); } + LocalizedActivity.setMustRecreate(); } return true; case R.id.cmd_app_unpin2: @@ -59,6 +61,7 @@ public static boolean onOptionsItemSelected(Activity parent, MenuItem item) { Global.locked = false; SettingsActivity.global2Prefs(parent.getApplication()); parent.invalidateOptionsMenu(); + LocalizedActivity.setMustRecreate(); return true; } return false; diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/PhotoAutoprocessingEditActivity.java b/app/src/main/java/de/k3b/android/androFotoFinder/PhotoAutoprocessingEditActivity.java index f13ad9c6..879c3d94 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/PhotoAutoprocessingEditActivity.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/PhotoAutoprocessingEditActivity.java @@ -639,7 +639,7 @@ private SelectedFiles getSelectedFiles(String dbgContext, Intent intent, boolean if (itemCount > 0) { if ((mustLoadIDs) && (ids == null)) { ids = new Long[itemCount]; - Map idMap = FotoSql.execGetPathIdMap(this, fileNames); + Map idMap = FotoSql.execGetPathIdMap(fileNames); for (int i = 0; i < itemCount; i++) { ids[i] = idMap.get(fileNames[i]); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/PhotoPropertiesEditActivity.java b/app/src/main/java/de/k3b/android/androFotoFinder/PhotoPropertiesEditActivity.java index dd3da811..e862c303 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/PhotoPropertiesEditActivity.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/PhotoPropertiesEditActivity.java @@ -284,7 +284,7 @@ private static SelectedFiles getSelectedFiles(String dbgContext, Context ctx, In if (itemCount > 0) { if ((mustLoadIDs) && (ids == null)) { ids = new Long[itemCount]; - Map idMap = FotoSql.execGetPathIdMap(ctx, fileNames); + Map idMap = FotoSql.execGetPathIdMap(fileNames); for (int i = 0; i < itemCount; i++) { ids[i] = idMap.get(fileNames[i]); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/SettingsActivity.java b/app/src/main/java/de/k3b/android/androFotoFinder/SettingsActivity.java index fb04cffa..66256c9c 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/SettingsActivity.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/SettingsActivity.java @@ -53,6 +53,7 @@ import uk.co.senab.photoview.log.LogManager; public class SettingsActivity extends PreferenceActivity { + private static final String PREF_KEY_USE_MEDIA_IMAGE_DB_REPLACEMENT = "useMediaImageDbReplacement"; private static Boolean sOldEnableNonStandardIptcMediaScanner = null; private SharedPreferences prefsInstance = null; private ListPreference defaultLocalePreference; // #21: Support to change locale at runtime @@ -60,82 +61,6 @@ public class SettingsActivity extends PreferenceActivity { private int INSTALL_REQUEST_CODE = 1927; - @Override - protected void onCreate(final Bundle savedInstanceState) { - LocalizedActivity.fixLocale(this); // #21: Support to change locale at runtime - super.onCreate(savedInstanceState); - - if (Global.debugEnabled) { - // todo create junit integration tests with arabic locale from this. - StringFormatResourceTests.test(this); - } - - final Intent intent = getIntent(); - if (Global.debugEnabled && (intent != null)){ - Log.d(Global.LOG_CONTEXT, "SettingsActivity onCreate " + intent.toUri(Intent.URI_INTENT_SCHEME)); - } - - this.addPreferencesFromResource(R.xml.preferences); - prefsInstance = PreferenceManager - .getDefaultSharedPreferences(this); - global2Prefs(this.getApplication()); - - // #21: Support to change locale at runtime - defaultLocalePreference = - (ListPreference) findPreference(Global.PREF_KEY_USER_LOCALE); - defaultLocalePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - setLanguage((String) newValue); - LocalizedActivity.recreate(SettingsActivity.this); - return true; // change is allowed - } - }); - - mediaUpdateStrategyPreference = - (ListPreference) findPreference("mediaUpdateStrategy"); - mediaUpdateStrategyPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - LibGlobal.mediaUpdateStrategy = (String) newValue; - setPref(LibGlobal.mediaUpdateStrategy, mediaUpdateStrategyPreference, R.array.pref_media_update_strategy_names); - return true; - } - }); - setPref(LibGlobal.mediaUpdateStrategy, mediaUpdateStrategyPreference, R.array.pref_media_update_strategy_names); - - findPreference("debugClearLog").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - onDebugClearLogCat(); - return false; // donot close - } - }); - findPreference("debugSaveLog").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - onDebugSaveLogCat(); - return false; // donot close - } - }); - findPreference("translate").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - onTranslate(); - return false; // donot close - } - }); - - // #21: Support to change locale at runtime - updateSummary(); - } - - @Override - public void onPause() { - prefs2Global(this); - super.onPause(); - } - public static void global2Prefs(Context context) { fixDefaults(context, null, null); @@ -168,6 +93,9 @@ public static void global2Prefs(Context context) { prefs.putBoolean("xmp_file_schema_long", LibGlobal.preferLongXmpFormat); prefs.putBoolean("mapsForgeEnabled", Global.mapsForgeEnabled); + if (Global.allow_emulate_ao10) { + prefs.putBoolean(PREF_KEY_USE_MEDIA_IMAGE_DB_REPLACEMENT, Global.useAo10MediaImageDbReplacement); + } prefs.putBoolean("locked", Global.locked); prefs.putString("passwordHash", Global.passwordHash); @@ -242,7 +170,12 @@ public static void prefs2Global(Context context) { Global.mapsForgeEnabled = getPref(prefs, "mapsForgeEnabled", Global.mapsForgeEnabled); - + boolean useAo10MediaImageDbReplacement = Global.useAo10MediaImageDbReplacement; + if (Global.allow_emulate_ao10) { + useAo10MediaImageDbReplacement = getPref(prefs, PREF_KEY_USE_MEDIA_IMAGE_DB_REPLACEMENT, Global.useAo10MediaImageDbReplacement); + } + AndroFotoFinderApp.setMediaImageDbReplacement(context.getApplicationContext(), useAo10MediaImageDbReplacement); + Global.imageDetailThumbnailIfBiggerThan = getPref(prefs, "imageDetailThumbnailIfBiggerThan" , Global.imageDetailThumbnailIfBiggerThan); Global.maxSelectionMarkersInMap = getPref(prefs, "maxSelectionMarkersInMap" , Global.maxSelectionMarkersInMap); @@ -285,6 +218,85 @@ public static void prefs2Global(Context context) { fixDefaults(context, previousCacheRoot, previousMapsForgeDir); } + @Override + public void onPause() { + prefs2Global(this); + super.onPause(); + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + LocalizedActivity.fixLocale(this); // #21: Support to change locale at runtime + super.onCreate(savedInstanceState); + + if (Global.debugEnabled) { + // todo create junit integration tests with arabic locale from this. + StringFormatResourceTests.test(this); + } + + final Intent intent = getIntent(); + if (Global.debugEnabled && (intent != null)) { + Log.d(Global.LOG_CONTEXT, "SettingsActivity onCreate " + intent.toUri(Intent.URI_INTENT_SCHEME)); + } + + this.addPreferencesFromResource(R.xml.preferences); + if (Global.allow_emulate_ao10) { + this.addPreferencesFromResource(R.xml.preferences_ao10_test); + } + prefsInstance = PreferenceManager + .getDefaultSharedPreferences(this); + global2Prefs(this.getApplication()); + + // #21: Support to change locale at runtime + defaultLocalePreference = + (ListPreference) findPreference(Global.PREF_KEY_USER_LOCALE); + defaultLocalePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + setLanguage((String) newValue); + LocalizedActivity.recreate(SettingsActivity.this); + return true; // change is allowed + } + }); + + mediaUpdateStrategyPreference = + (ListPreference) findPreference("mediaUpdateStrategy"); + mediaUpdateStrategyPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + LibGlobal.mediaUpdateStrategy = (String) newValue; + setPref(LibGlobal.mediaUpdateStrategy, mediaUpdateStrategyPreference, R.array.pref_media_update_strategy_names); + return true; + } + }); + setPref(LibGlobal.mediaUpdateStrategy, mediaUpdateStrategyPreference, R.array.pref_media_update_strategy_names); + + findPreference("debugClearLog").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + onDebugClearLogCat(); + return false; // donot close + } + }); + findPreference("debugSaveLog").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + onDebugSaveLogCat(); + return false; // donot close + } + }); + findPreference("translate").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + onTranslate(); + return false; // donot close + } + }); + + // #21: Support to change locale at runtime + updateSummary(); + } + private static void fixDefaults(Context context, File previousCacheRoot, File previousMapsForgeDir) { boolean mustSave = false; @@ -425,7 +437,7 @@ private void onDebugClearLogCat() { private void onDebugSaveLogCat() { Log.e(Global.LOG_CONTEXT, "SettingsActivity-SaveLogCat(): " + ActivityWithCallContext.readCallContext(getIntent())); - ((AndroFotoFinderApp) getApplication()).saveToFile(); + ((AndroFotoFinderApp) getApplication()).saveToFile(this); } private void onTranslate() { diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/backup/Backup2ZipService.java b/app/src/main/java/de/k3b/android/androFotoFinder/backup/Backup2ZipService.java index 2c9e1a1e..8ca9daad 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/backup/Backup2ZipService.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/backup/Backup2ZipService.java @@ -19,10 +19,8 @@ package de.k3b.android.androFotoFinder.backup; -import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; -import android.net.Uri; import android.support.annotation.NonNull; import android.util.Log; @@ -208,13 +206,12 @@ public static QueryParameter getEffectiveQueryParameter(@NonNull IZipConfig zipC /** calls consumers for each found query-result-item */ private void execQuery(QueryParameter query, IItemSaver... consumers) { - ContentResolver contentResolver = context.getContentResolver(); - Cursor cursor = null; try { this.onProgress(0,0, "Calculate"); - cursor = contentResolver.query(Uri.parse(query.toFrom()), query.toColumns(), - query.toAndroidWhere(), query.toAndroidParameters(), query.toOrderBy()); + cursor = FotoSql.getMediaDBApi().createCursorForQuery( + null, "ZipExecute", + query, null, null); int itemCount = cursor.getCount(); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/backup/BackupActivity.java b/app/src/main/java/de/k3b/android/androFotoFinder/backup/BackupActivity.java index 972e1f9b..b76a7303 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/backup/BackupActivity.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/backup/BackupActivity.java @@ -492,7 +492,7 @@ private SelectedFiles getSelectedFiles(String dbgContext, Intent intent, boolean if (itemCount > 0) { if ((mustLoadIDs) && (ids == null)) { ids = new Long[itemCount]; - Map idMap = FotoSql.execGetPathIdMap(this, fileNames); + Map idMap = FotoSql.execGetPathIdMap(fileNames); for (int i = 0; i < itemCount; i++) { ids[i] = idMap.get(fileNames[i]); @@ -703,7 +703,7 @@ private void updateHistory(QueryParameter query) { paths.add(DCIM_ROOT); - String minFolder = FotoSql.getMinFolder(getApplicationContext(), query, true); + String minFolder = FotoSql.getMinFolder(query, true); updateHistory(FileUtils.getDir(minFolder), filenames, paths, 1); String queryFolder = FotoSql.getFilePath(query, false); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/backup/BackupAsyncTask.java b/app/src/main/java/de/k3b/android/androFotoFinder/backup/BackupAsyncTask.java index b95c9dee..9f683891 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/backup/BackupAsyncTask.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/backup/BackupAsyncTask.java @@ -21,82 +21,39 @@ import android.app.Activity; import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; import android.util.Log; import android.widget.ProgressBar; import android.widget.TextView; import java.util.Date; -import java.util.concurrent.atomic.AtomicBoolean; -import de.k3b.android.androFotoFinder.Common; import de.k3b.android.androFotoFinder.Global; import de.k3b.android.androFotoFinder.R; +import de.k3b.android.widget.ProgressableAsyncTask; import de.k3b.io.IProgessListener; import de.k3b.zip.IZipConfig; import de.k3b.zip.LibZipGlobal; -import de.k3b.zip.ProgressFormatter; import de.k3b.zip.ZipConfigDto; import de.k3b.zip.ZipStorage; -/** - * ProgressData: Text that can be displayed as progress message - * in owning Activity. Translated from android independant {@link de.k3b.io.IProgessListener} - */ -class ProgressData { - final int itemcount; - final int size; - final String message; - - ProgressData(int itemcount, int size, String message) { - this.itemcount = itemcount; - this.size = size; - this.message = message; - } -} - /** * Async ancapsulation of * {@link de.k3b.android.androFotoFinder.backup.Backup2ZipService} */ -public class BackupAsyncTask extends AsyncTask implements IProgessListener { +public class BackupAsyncTask extends ProgressableAsyncTask implements IProgessListener { private final String mDebugPrefix = "BackupAsyncTask "; private final Backup2ZipService service; - private Activity activity; - private ProgressBar mProgressBar = null; - private TextView status; - private AtomicBoolean isActive = new AtomicBoolean(true); - - private final ProgressFormatter formatter; - // last known number of items to be processed - private int lastSize = 0; public BackupAsyncTask(Context context, ZipConfigDto mZipConfigData, ZipStorage zipStorage, Date backupDate) { this.service = new Backup2ZipService(context.getApplicationContext(), mZipConfigData, zipStorage, backupDate, BackupOptions.ALL); - - formatter = new ProgressFormatter(); } - /** - * (Re-)Attach owning Activity to BackupAsyncTask - * (i.e. after Device rotation - * - * @param why - * @param activity new owner - * @param progressBar To be updated while compression task is running - * @param status To be updated while compression task is running - */ + @Override public void setContext(String why, Activity activity, ProgressBar progressBar, TextView status) { - if (LibZipGlobal.debugEnabled) { - Log.d(LibZipGlobal.LOG_TAG, mDebugPrefix + why + " setContext " + activity); - } - this.activity = activity; - mProgressBar = progressBar; - this.status = status; + super.setContext(why, activity, progressBar, status); service.setProgessListener((progressBar != null) ? this : null); } @@ -136,71 +93,12 @@ protected void onPostExecute(IZipConfig zipConfig) { } } - private void finish(String why, int resultCode, CharSequence message) { - if (message != null) { - Intent intent = new Intent(); - intent.putExtra(Common.EXTRA_TITLE, message); - activity.setResult(resultCode, intent); - } else { - activity.setResult(resultCode); - } - activity.finish(); - - setContext(why + mDebugPrefix + " finish ", null, null, null); - - } - - /** called on error */ - @Override - protected void onCancelled() { - if (activity != null) { - if (LibZipGlobal.debugEnabled || Global.debugEnabled) { - Log.d(LibZipGlobal.LOG_TAG, activity.getClass().getSimpleName() + ": " + activity.getText(android.R.string.cancel)); - } - - finish(mDebugPrefix + " onCancelled ", Activity.RESULT_CANCELED, activity.getText(android.R.string.cancel)); - } - } - - public static boolean isActive(BackupAsyncTask backupAsyncTask) { - return (backupAsyncTask != null) && (backupAsyncTask.isActive.get()); - } - - /** - * de.k3b.io.IProgessListener: - * - * called every time when command makes some little progress in non gui thread. - * return true to continue - */ @Override public boolean onProgress(int itemcount, int size, String message) { - publishProgress(new ProgressData(itemcount, size, message)); - - final boolean cancelled = this.isCancelled(); - if (cancelled) { + boolean result = onProgress(itemcount, size, message); + if (isCancelled()) { if (this.service != null) this.service.cancel(); - if (LibZipGlobal.debugEnabled) { - Log.d(LibZipGlobal.LOG_TAG, mDebugPrefix + " cancel backup pressed "); - } - } - return !cancelled; - } - - /** called from {@link AsyncTask} in gui task */ - @Override - protected void onProgressUpdate(ProgressData... values) { - final ProgressData progressData = values[0]; - if (mProgressBar != null) { - int size = progressData.size; - if ((size != 0) && (size > lastSize)) { - mProgressBar.setMax(size); - lastSize = size; - } - mProgressBar.setProgress(progressData.itemcount); - } - if (this.status != null) { - this.status.setText(formatter.format(progressData.itemcount, progressData.size)); - // values[0].message); } + return result; } } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/backup/BackupProgressActivity.java b/app/src/main/java/de/k3b/android/androFotoFinder/backup/BackupProgressActivity.java index 1c17f7b5..b21d7b73 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/backup/BackupProgressActivity.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/backup/BackupProgressActivity.java @@ -29,9 +29,6 @@ import android.support.annotation.NonNull; import android.support.v4.provider.DocumentFile; import android.util.Log; -import android.view.View; -import android.widget.Button; -import android.widget.ProgressBar; import android.widget.TextView; import java.io.File; @@ -42,7 +39,8 @@ import de.k3b.android.androFotoFinder.Global; import de.k3b.android.androFotoFinder.R; import de.k3b.android.util.IntentUtil; -import de.k3b.android.widget.LocalizedActivity; +import de.k3b.android.widget.ProgressActivity; +import de.k3b.android.widget.ProgressableAsyncTask; import de.k3b.zip.IZipConfig; import de.k3b.zip.LibZipGlobal; import de.k3b.zip.ZipConfigDto; @@ -52,7 +50,7 @@ /** * #108: Showing progress while backup/compression-to-zip is executed */ -public class BackupProgressActivity extends LocalizedActivity { +public class BackupProgressActivity extends ProgressActivity { /** * document tree supported since andrid-5.0. For older devices use folder picker */ @@ -62,7 +60,7 @@ public class BackupProgressActivity extends LocalizedActivity { private static String mDebugPrefix = "BuProgressActivity: "; // != null while async backup is running - private static BackupAsyncTask backupAsyncTask = null; + private static ProgressableAsyncTask asyncTask = null; private static Date backupDate = null; private IZipConfig mZipConfigData = null; @@ -123,7 +121,17 @@ private static DocumentFile getDocFile(Context context, @NonNull String dir) { } -/* + @Override + protected ProgressableAsyncTask getAsyncTask() { + return asyncTask; + } + + @Override + protected void setAsyncTask(ProgressableAsyncTask asyncTask) { + BackupProgressActivity.asyncTask = asyncTask; + } + + /* @Override protected void onPause() { setBackupAsyncTaskProgessReceiver(null); @@ -141,16 +149,16 @@ protected void onCreate(Bundle savedInstanceState) { mZipConfigData = (IZipConfig) intent.getSerializableExtra(EXTRA_STATE_ZIP_CONFIG); - if (backupAsyncTask == null) { + if (getAsyncTask() == null) { backupDate = new Date(); final String zipDir = mZipConfigData.getZipDir(); final String zipName = ZipConfigDto.getZipFileName(mZipConfigData, backupDate); ZipStorage zipStorage = getCurrentStorage(this, zipDir, zipName); - backupAsyncTask = new BackupAsyncTask(this, new ZipConfigDto(mZipConfigData), zipStorage, - backupDate); - setBackupAsyncTaskProgessReceiver(mDebugPrefix + "onCreate create backupAsyncTask ", this); - backupAsyncTask.execute(); + setAsyncTask(new BackupAsyncTask(this, new ZipConfigDto(mZipConfigData), zipStorage, + backupDate)); + setAsyncTaskProgessReceiver(mDebugPrefix + "onCreate create asyncTask ", this); + getAsyncTask().execute(); } final TextView lblContext = (TextView) findViewById(R.id.lbl_context); @@ -165,60 +173,5 @@ protected void onCreate(Bundle savedInstanceState) { lblContext.setText(contextMessage); } - @Override - protected void onDestroy() { - setBackupAsyncTaskProgessReceiver(mDebugPrefix + "onDestroy ", null); - super.onDestroy(); - } - - @Override - protected void onResume() { - setBackupAsyncTaskProgessReceiver(mDebugPrefix + "onResume ", this); - Global.debugMemory(mDebugPrefix, "onResume"); - super.onResume(); - - } - - /** - * (Re-)Connects this activity back with static backupAsyncTask - */ - private void setBackupAsyncTaskProgessReceiver(String why, Activity progressReceiver) { - boolean isActive = BackupAsyncTask.isActive(backupAsyncTask); - boolean running = (progressReceiver != null) && isActive; - - String debugContext = why + mDebugPrefix + " setBackupAsyncTaskProgessReceiver isActive=" + isActive + - ", running=" + running + - " "; - - if (backupAsyncTask != null) { - final ProgressBar progressBar = (ProgressBar) this.findViewById(R.id.progressBar); - final TextView status = (TextView) this.findViewById(R.id.lbl_status); - final Button buttonCancel = (Button) this.findViewById(R.id.cmd_cancel); - - // setVisibility(running, progressBar, buttonCancel); - - if (running) { - backupAsyncTask.setContext(debugContext, this, progressBar, status); - final String _debugContext = debugContext; - buttonCancel.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (LibZipGlobal.debugEnabled) { - Log.d(LibZipGlobal.LOG_TAG, mDebugPrefix + " button Cancel backup pressed initialized by " + _debugContext); - } - backupAsyncTask.cancel(false); - buttonCancel.setVisibility(View.INVISIBLE); - } - }); - - } else { - backupAsyncTask.setContext(debugContext, null, null, null); - buttonCancel.setOnClickListener(null); - if (!isActive) { - backupAsyncTask = null; - } - } - } - } } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/backup/package-info.java b/app/src/main/java/de/k3b/android/androFotoFinder/backup/package-info.java index 166a8b4f..c4cbd97c 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/backup/package-info.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/backup/package-info.java @@ -26,7 +26,7 @@ * * {@link de.k3b.android.androFotoFinder.backup.Backup2ZipService} : Collects Files to backed up from database via Filter * * {@link de.k3b.android.androFotoFinder.backup.ApmZipCompressJob} : Executes the compression (with android specific Filesystem) * * {@link de.k3b.zip.CompressJob} : Executes the compression (with android independant implementation) - * * {@link de.k3b.android.androFotoFinder.backup.ProgressData} : Data containing compression progress + * * {@link de.k3b.android.androFotoFinder.backup.BackupProgressActivity} : Data containing compression progress * * {@link de.k3b.io.IProgessListener} : android independant compression progress **/ package de.k3b.android.androFotoFinder.backup; diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/directory/DirectoryListAdapter.java b/app/src/main/java/de/k3b/android/androFotoFinder/directory/DirectoryListAdapter.java index 2448ac65..9865b073 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/directory/DirectoryListAdapter.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/directory/DirectoryListAdapter.java @@ -32,9 +32,9 @@ import de.k3b.android.androFotoFinder.Global; import de.k3b.android.androFotoFinder.R; +import de.k3b.android.util.PhotoChangeNotifyer; import de.k3b.io.AlbumFile; import de.k3b.io.Directory; -import de.k3b.io.FileUtils; import de.k3b.io.IDirectory; import de.k3b.io.IExpandableListViewNavigation; @@ -42,7 +42,8 @@ * Maps android independent IExpandableListViewNavigation to android specific ExpandableListAdapter so it can be viewed in ExpandableList */ -public class DirectoryListAdapter extends BaseExpandableListAdapter implements IExpandableListViewNavigation { +public class DirectoryListAdapter extends BaseExpandableListAdapter implements + IExpandableListViewNavigation, PhotoChangeNotifyer.PhotoChangedListener { private LayoutInflater inflater; @@ -219,4 +220,12 @@ public static Spanned getDirectoryDisplayText(String prefix, IDirectory director Directory.appendCount(result, directory, options); return Html.fromHtml(result.toString()); } + + /** + * PhotoChangeNotifyer.PhotoChangedListener + **/ + @Override + public void onNotifyPhotoChanged() { + notifyDataSetChanged(); + } } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/directory/DirectoryLoaderTask.java b/app/src/main/java/de/k3b/android/androFotoFinder/directory/DirectoryLoaderTask.java index b5ddfbd7..92ebcd62 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/directory/DirectoryLoaderTask.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/directory/DirectoryLoaderTask.java @@ -22,7 +22,6 @@ import android.app.Activity; import android.content.Context; import android.database.Cursor; -import android.net.Uri; import android.os.AsyncTask; import android.util.Log; @@ -102,8 +101,9 @@ protected IDirectory doInBackground(QueryParameter... queryParameter) { } try { - cursor = context.getContentResolver().query(Uri.parse(queryParameters.toFrom()), queryParameters.toColumns(), - queryParameters.toAndroidWhere(), queryParameters.toAndroidParameters(), queryParameters.toOrderBy()); + cursor = FotoSql.getMediaDBApi().createCursorForQuery( + null, "ZipExecute", + queryParameters, null, null); int itemCount = cursor.getCount(); final int expectedCount = itemCount + itemCount; diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/directory/DirectoryPickerFragment.java b/app/src/main/java/de/k3b/android/androFotoFinder/directory/DirectoryPickerFragment.java index ef35b1ca..274ffadb 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/directory/DirectoryPickerFragment.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/directory/DirectoryPickerFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2019 by k3b. + * Copyright (c) 2015-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -70,6 +70,7 @@ import de.k3b.android.util.ClipboardUtil; import de.k3b.android.util.FileManagerUtil; import de.k3b.android.util.IntentUtil; +import de.k3b.android.util.PhotoChangeNotifyer; import de.k3b.android.util.PhotoPropertiesMediaFilesScanner; import de.k3b.android.widget.Dialogs; import de.k3b.database.QueryParameter; @@ -456,8 +457,8 @@ protected boolean onPopUpClick(MenuItem menuItem, IDirectory popUpSelection) { } } - public void notifyDataSetChanged() { - if (this.mAdapter != null) this.mAdapter.notifyDataSetChanged(); + public void notifyPhotoChanged() { + PhotoChangeNotifyer.notifyPhotoChanged(this.getActivity(), this.mAdapter); } private boolean onCopy(IDirectory selection) { @@ -544,7 +545,7 @@ private void onDeleteAnswer(File file, IDirectory dir) { } // delete from database - if (FotoSql.deleteMedia("delete album", getActivity(), + if (FotoSql.deleteMedia("delete album", ListUtils.toStringList(file.getAbsolutePath()),false) > 0) { deleteSuccess = true; } @@ -607,7 +608,7 @@ private void onRenameDirAnswer(final IDirectory srcDir, String newFolderName) { } else { // update dirpicker srcDir.rename(srcDirFile.getName(), newFolderName); - notifyDataSetChanged(); + notifyPhotoChanged(); } } @@ -663,11 +664,11 @@ private boolean fixLinks(IDirectory linkDir) { if (!canonicalPath.endsWith("/")) canonicalPath+="/"; String sqlWhereLink = FotoSql.SQL_COL_PATH + " like '" + linkPath + "%'"; - SelectedFiles linkFiles = FotoSql.getSelectedfiles(context, sqlWhereLink, VISIBILITY.PRIVATE_PUBLIC); + SelectedFiles linkFiles = FotoSql.getSelectedfiles(sqlWhereLink, VISIBILITY.PRIVATE_PUBLIC); String sqlWhereCanonical = FotoSql.SQL_COL_PATH + " in (" + linkFiles.toString() + ")"; sqlWhereCanonical = sqlWhereCanonical.replace(linkPath,canonicalPath); - SelectedFiles canonicalFiles = FotoSql.getSelectedfiles(context, sqlWhereCanonical, VISIBILITY.PRIVATE_PUBLIC); + SelectedFiles canonicalFiles = FotoSql.getSelectedfiles(sqlWhereCanonical, VISIBILITY.PRIVATE_PUBLIC); HashMap link2canonical = new HashMap(); for(String cann : canonicalFiles.getFileNames()) { link2canonical.put(linkPath + cann.substring(canonicalPath.length()), cann); @@ -692,9 +693,9 @@ private boolean fixLinks(IDirectory linkDir) { if (cann == null) { // rename linkFile to canonicalFile updateValues.put(FotoSql.SQL_COL_PATH, canonicalPath + lin.substring(linkPath.length())); - FotoSql.execUpdate("fixLinks", context, linkIds[i].intValue() ,updateValues); + FotoSql.getMediaDBApi().execUpdate("fixLinks", linkIds[i].intValue(), updateValues); } else { - FotoSql.deleteMedia("DirectoryPickerFragment.fixLinks", context, FotoSql.FILTER_COL_PK, new String[] {linkIds[i].toString()}, true); + FotoSql.getMediaDBApi().deleteMedia("DirectoryPickerFragment.fixLinks", FotoSql.FILTER_COL_PK, new String[]{linkIds[i].toString()}, true); } } PhotoPropertiesMediaFilesScanner.notifyChanges(context, "Fixed link/canonical duplicates"); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/directory/SaveAsPickerFragment.java b/app/src/main/java/de/k3b/android/androFotoFinder/directory/SaveAsPickerFragment.java index ffcd0231..1e72545c 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/directory/SaveAsPickerFragment.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/directory/SaveAsPickerFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 by k3b. + * Copyright (c) 2018-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -20,7 +20,6 @@ package de.k3b.android.androFotoFinder.directory; import android.app.Activity; -import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -29,11 +28,8 @@ import java.io.File; -import de.k3b.android.androFotoFinder.AffUtils; import de.k3b.android.androFotoFinder.R; -import de.k3b.android.androFotoFinder.imagedetail.ImageDetailActivityViewPager; import de.k3b.android.androFotoFinder.queries.FotoSql; -import de.k3b.android.util.AndroidFileCommands; import de.k3b.android.util.OsUtils; import de.k3b.io.AlbumFile; import de.k3b.io.FileUtils; @@ -41,7 +37,6 @@ import de.k3b.io.OSDirOrVirtualAlbumFile; import de.k3b.io.OSDirectory; import de.k3b.io.StringUtils; -import de.k3b.io.collections.SelectedFiles; /** * a picker with a fale name field and a directory picker. @@ -132,7 +127,7 @@ protected void onDirectoryPick(IDirectory selection) { // close dialog and return to caller super.onDirectoryPick(result); onFilePick(new File(result.getAbsolute())); - this.notifyDataSetChanged(); + this.notifyPhotoChanged(); dismiss(); } } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/directory/ShowInMenuHandler.java b/app/src/main/java/de/k3b/android/androFotoFinder/directory/ShowInMenuHandler.java index 45a72c9a..4e0395bb 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/directory/ShowInMenuHandler.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/directory/ShowInMenuHandler.java @@ -134,7 +134,7 @@ private long getPickCount(String dbgContext, QueryParameter query = AndroidAlbumUtils.getAsAlbumOrMergedNewQuery( dbgContext, mContext, baseQuery, filter); if (query == null) return 0; - return FotoSql.getCount(mContext, query); + return FotoSql.getCount(query); } private boolean showPhoto(String dbgContext, QueryParameter baseQuery) { @@ -162,7 +162,7 @@ private boolean showMap(String dbgContext, QueryParameter baseQuery) { QueryParameter query = AndroidAlbumUtils.getAsAlbumOrMergedNewQuery( dbgContext, mContext, baseQuery, currentSelectionFilter); if (query != null) { - IGeoRectangle area = FotoSql.execGetGeoRectangle(null, mContext, query, + IGeoRectangle area = FotoSql.execGetGeoRectangle(null, query, null, "Calculate visible arean", dbgContext); MapGeoPickerActivity.showActivity(dbgContext, mContext, null, query, area, 0); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/gallery/cursor/GalleryCursorAdapter.java b/app/src/main/java/de/k3b/android/androFotoFinder/gallery/cursor/GalleryCursorAdapter.java index 972cf9ef..c6aa79fe 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/gallery/cursor/GalleryCursorAdapter.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/gallery/cursor/GalleryCursorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2019 by k3b. + * Copyright (c) 2015-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -40,6 +40,7 @@ import de.k3b.android.androFotoFinder.imagedetail.HugeImageLoader; import de.k3b.android.androFotoFinder.queries.FotoSql; import de.k3b.android.util.DBUtils; +import de.k3b.android.util.PhotoChangeNotifyer; import de.k3b.io.collections.SelectedFiles; import de.k3b.io.collections.SelectedItems; import de.k3b.media.PhotoPropertiesUtil; @@ -60,7 +61,7 @@ * * Created by k3b on 02.06.2015. */ -public class GalleryCursorAdapter extends CursorAdapter { +public class GalleryCursorAdapter extends CursorAdapter implements PhotoChangeNotifyer.PhotoChangedListener { private static final int MAX_IMAGE_DIMENSION = HugeImageLoader.getMaxTextureSize(); // Identifies a particular Loader or a LoaderManager being used in this component @@ -234,4 +235,12 @@ public long getImageId(int position) { return DBUtils.getLong(cursor, FotoSql.SQL_COL_PK, 0); } + /** + * PhotoChangeNotifyer.PhotoChangedListener + **/ + @Override + public void onNotifyPhotoChanged() { + notifyDataSetChanged(); + } + } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/gallery/cursor/GalleryCursorFragment.java b/app/src/main/java/de/k3b/android/androFotoFinder/gallery/cursor/GalleryCursorFragment.java index 325fdf0d..8259c739 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/gallery/cursor/GalleryCursorFragment.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/gallery/cursor/GalleryCursorFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2019 by k3b. + * Copyright (c) 2015-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -72,6 +72,7 @@ import de.k3b.android.androFotoFinder.imagedetail.ImageDetailMetaDialogBuilder; import de.k3b.android.androFotoFinder.locationmap.GeoEditActivity; import de.k3b.android.androFotoFinder.locationmap.MapGeoPickerActivity; +import de.k3b.android.androFotoFinder.queries.CursorLoaderWithException; import de.k3b.android.androFotoFinder.queries.FotoSql; import de.k3b.android.androFotoFinder.queries.FotoViewerParameter; import de.k3b.android.androFotoFinder.queries.Queryable; @@ -83,6 +84,7 @@ import de.k3b.android.util.AndroidFileCommands; import de.k3b.android.util.DBUtils; import de.k3b.android.util.OsUtils; +import de.k3b.android.util.PhotoChangeNotifyer; import de.k3b.android.util.PhotoPropertiesMediaFilesScanner; import de.k3b.android.util.ResourceUtils; import de.k3b.android.widget.AboutDialogPreference; @@ -121,7 +123,8 @@ * if (isMultiSelectionActive()) menu_gallery_multiselect_mode_all + menu_image_commands * if (view-non-select) menu_gallery_non_selected_only + menu_gallery_non_multiselect */ -public class GalleryCursorFragment extends Fragment implements Queryable, DirectoryGui,Common, TagsPickerFragment.ITagsPicker { +public class GalleryCursorFragment extends Fragment implements Queryable, DirectoryGui, Common, + TagsPickerFragment.ITagsPicker, PhotoChangeNotifyer.PhotoChangedListener { private static final String INSTANCE_STATE_LAST_VISIBLE_POSITION = "lastVisiblePosition"; private static final String INSTANCE_STATE_SELECTED_ITEM_IDS = "selectedItems"; private static final String INSTANCE_STATE_OLD_TITLE = "oldTitle"; @@ -151,6 +154,8 @@ public class GalleryCursorFragment extends Fragment implements Queryable, Direc private GridView mGalleryView; private ShareActionProvider mShareActionProvider; + long mUpdateId = FotoSql.getMediaDBApi().getCurrentUpdateId(); + private GalleryCursorAdapterFromArray mAdapter = null; private OnGalleryInteractionListener mGalleryListener; @@ -212,125 +217,54 @@ public SelectedItems getSelectedItems() { return mSelectedItems; } - class LocalCursorLoader implements LoaderManager.LoaderCallbacks { - /** called by LoaderManager.getLoader(ACTIVITY_ID) to (re)create loader - * that attaches to last query/cursor if it still exist i.e. after rotation */ - @Override - public Loader onCreateLoader(int aLoaderID, Bundle bundle) { - if (loaderID == aLoaderID) { - QueryParameter query = getCurrentQuery(); - mRequeryInstanceCount++; - if (Global.debugEnabledSql) { - Log.i(Global.LOG_CONTEXT, mDebugPrefix + " onCreateLoader" - + getDebugContext() + - " : query = " + query); - } - return FotoSql.createCursorLoader(getActivity().getApplicationContext(), query); - } - - // An invalid id was passed in - return null; - } - - /** called after media db content has changed */ - @Override - public void onLoadFinished(Loader _loader, Cursor data) { - mLastVisiblePosition = mGalleryView.getLastVisiblePosition(); - - final Activity context = getActivity(); - if (data == null) { - FotoSql.CursorLoaderWithException loader = (FotoSql.CursorLoaderWithException) _loader; - String title; - String message = context.getString(R.string.global_err_sql_message_format, loader.getException().getMessage(), loader.getQuery().toSqlString()); - if (loader.getException() != null) { - if (0 != loader.getQuery().toSqlString().compareTo(getCurrentQuery(FotoSql.queryDetail).toSqlString())) { - // query is not default query. revert to default query - mGalleryContentQuery = FotoSql.queryDetail; - requery("requery after query-errror"); - title = context.getString(R.string.global_err_sql_title_reload); - } else { - title = context.getString(R.string.global_err_system); - context.finish(); - } - Dialogs.messagebox(context, title, message); - return; - } - } - - // do change the data - mAdapter.swapCursor(data); - - if (mLastVisiblePosition > 0) { - mGalleryView.smoothScrollToPosition(mLastVisiblePosition); - mLastVisiblePosition = -1; - } - - final int resultCount = (data == null) ? 0 : data.getCount(); - if (Global.debugEnabled) { - Log.i(Global.LOG_CONTEXT, mDebugPrefix + " onLoadFinished" - + getDebugContext() + - " fount " + resultCount + " rows"); - } - - // do change the data - mAdapter.notifyDataSetChanged(); + public void onNotifyPhotoChanged() { + requeryIfDataHasChanged(); + } - if (mLastVisiblePosition > 0) { - mGalleryView.smoothScrollToPosition(mLastVisiblePosition); - mLastVisiblePosition = -1; - } + /** incremented every time a new curster/query is generated */ + private int mRequeryInstanceCount = 0; - // show the changes + protected LocalCursorLoader mCurorLoader = null; - if (context instanceof OnGalleryInteractionListener) { - ((OnGalleryInteractionListener) context).setResultCount(resultCount); - } - multiSelectionReplaceTitleIfNecessary(); + private void requeryIfDataHasChanged() { + if (FotoSql.getMediaDBApi().mustRequery(mUpdateId)) { + requery("requeryIfDataHasChanged"); } + } - /** called by LoaderManager. after search criteria were changed or if activity is destroyed. */ - @Override - public void onLoaderReset(Loader loader) { - if (Global.debugEnabled) { - Log.i(Global.LOG_CONTEXT, mDebugPrefix + " onLoaderReset" + getDebugContext()); - } - // rember position where we have to scroll to after refreshLocal is finished. - mLastVisiblePosition = mGalleryView.getLastVisiblePosition(); - - mAdapter.swapCursor(null); - mAdapter.notifyDataSetChanged(); + private void requery(String why) { + mUpdateId = FotoSql.getMediaDBApi().getCurrentUpdateId(); + if (Global.debugEnabled) { + Log.i(Global.LOG_CONTEXT, mDebugPrefix + why + " requery\n" + ((mGalleryContentQuery != null) ? mGalleryContentQuery.toSqlString() : null)); } - @NonNull - protected String getDebugContext() { - return "(@" + loaderID + ", #" + mRequeryInstanceCount + - ", LastVisiblePosition=" + mLastVisiblePosition + -// ", Path='" + mInitialFilePath + - "')"; + if (mGalleryContentQuery != null) { + // query has been initialized + if (mCurorLoader == null) { + mCurorLoader = new LocalCursorLoader(); + getLoaderManager().initLoader(loaderID, null, mCurorLoader); + } else { + getLoaderManager().restartLoader(loaderID, null, this.mCurorLoader); + } } } - /** incremented every time a new curster/query is generated */ - private int mRequeryInstanceCount = 0; - - protected LocalCursorLoader mCurorLoader = null; - - protected class LocalFileCommands extends AndroidFileCommands { + private boolean cmdMoveOrCopyWithDestDirPicker(final boolean move, String lastCopyToPath, final SelectedFiles fotos) { + if (AndroidFileCommands.canProcessFile(this.getActivity(), false)) { + PhotoChangeNotifyer.setPhotoChangedListener(this); + mDestDirPicker = MoveOrCopyDestDirPicker.newInstance(move, fotos); - @Override - protected void onPostProcess(String what, int opCode, SelectedFiles selectedFiles, int modifyCount, int itemCount, String[] oldPathNames, String[] newPathNames) { - if (Global.clearSelectionAfterCommand || (opCode == OP_DELETE) || (opCode == OP_MOVE)) { - mShowSelectedOnly = true; - multiSelectionCancel(); + mDestDirPicker.defineDirectoryNavigation(OsUtils.getRootOSDirectory(null), + (move) ? FotoSql.QUERY_TYPE_GROUP_MOVE : FotoSql.QUERY_TYPE_GROUP_COPY, + lastCopyToPath); + if (!LockScreen.isLocked(this.getActivity())) { + mDestDirPicker.setContextMenuId(R.menu.menu_context_pick_osdir); } - super.onPostProcess(what, opCode, selectedFiles, modifyCount, itemCount, oldPathNames, newPathNames); - - if ((mAdapter.isInArrayMode()) && ((opCode == OP_RENAME) || (opCode == OP_MOVE) || (opCode == OP_DELETE))) { - mAdapter.refreshLocal(); - mGalleryView.setAdapter(mAdapter); - } + mDestDirPicker.setBaseQuery(getCurrentQuery()); + mDestDirPicker.show(getActivity().getFragmentManager(), "osdir"); } + return false; } public GalleryCursorFragment() { @@ -515,7 +449,7 @@ public void onResume() { // workaround fragment lifecycle is newFragment.attach oldFragment.detach. // this makes shure that the visible fragment has commands MoveOrCopyDestDirPicker.sFileCommands = mFileCommands; - + requeryIfDataHasChanged(); } /** @@ -620,20 +554,31 @@ public void requery(Activity context, QueryParameter parameters, String why) { requery(why); } - private void requery(String why) { - if (Global.debugEnabled) { - Log.i(Global.LOG_CONTEXT, mDebugPrefix + why + " requery\n" + ((mGalleryContentQuery != null) ? mGalleryContentQuery.toSqlString() : null)); - } + /** + * image entries may not have DISPLAY_NAME which is essential for calculating the item-s folder. + */ + private void repairMissingDisplayNames() { + SqlJobTaskBase task = new SqlJobTaskBase(this.getActivity(), "Searching media database for missing 'displayname'-s:\n", null) { + private int mPathColNo = -2; + private int mResultCount = 0; - if (mGalleryContentQuery != null) { - // query has been initialized - if (mCurorLoader == null) { - mCurorLoader = new LocalCursorLoader(); - getLoaderManager().initLoader(loaderID, null, mCurorLoader); - } else { - getLoaderManager().restartLoader(loaderID, null, this.mCurorLoader); + @Override + protected void doInBackground(Long id, Cursor cursor) { + if (mPathColNo == -2) mPathColNo = cursor.getColumnIndex(FotoSql.SQL_COL_PATH); + mResultCount += PhotoPropertiesMediaFilesScanner.getInstance(getActivity()).updatePathRelatedFields(getActivity(), cursor, cursor.getString(mPathColNo), mColumnIndexPK, mPathColNo); } - } + + @Override + protected void onPostExecute(SelectedItems selectedItems) { + if (!isCancelled()) { + onMissingDisplayNamesComplete(mStatus); + onNotifyPhotoChanged(); + } + } + }; + QueryParameter query = FotoSql.queryGetMissingDisplayNames; + FotoSql.setWhereVisibility(query, VISIBILITY.PRIVATE_PUBLIC); + task.execute(query); } private QueryParameter getCurrentQuery() { @@ -853,6 +798,9 @@ public void onPrepareOptionsMenu(Menu menu) { AboutDialogPreference.onPrepareOptionsMenu(getActivity(), menu); } else if (this.isMultiSelectionActive()) { // view-multiselect inflater.inflate(R.menu.menu_gallery_multiselect_mode_all, menu); + if (Global.allowRenameMultible) { + inflater.inflate(R.menu.menu_gallery_multiselect_rename, menu); + } mShareOnlyToggle = menu.findItem(R.id.cmd_selected_only); if (mShowSelectedOnly && (mShareOnlyToggle != null)) { @@ -911,7 +859,7 @@ public boolean onOptionsItemSelected(MenuItem menuItem) { AndroidFileCommands fileCommands = mFileCommands; final SelectedFiles selectedFiles = this.mAdapter.createSelectedFiles(getActivity(), this.mSelectedItems); - if ((mSelectedItems != null) && (fileCommands.onOptionsItemSelected(menuItem, selectedFiles))) { + if ((mSelectedItems != null) && (fileCommands.onOptionsItemSelected(menuItem, selectedFiles, this))) { return true; } switch (menuItem.getItemId()) { @@ -989,19 +937,43 @@ private void cmdShowDetails() { // setAutoClose(null, dlg, null); } - private class TagUpdateTask extends TagTask> { - - TagUpdateTask(SelectedFiles fotos) { - super(GalleryCursorFragment.this.getActivity(),R.string.tags_activity_title); - this.getWorkflow().init(GalleryCursorFragment.this.getActivity(), fotos, null); - - } + private void removeDuplicates() { + SqlJobTaskBase task = new SqlJobTaskBase(this.getActivity(), "Searching for duplcates in media database:\n", null) { + @Override + protected void doInBackground(Long id, Cursor cursor) { + this.mSelectedItems.add(id); + if (mStatus != null) { + mStatus + .append("\nduplicate found ") + .append(id) + .append("#") + .append(DBUtils.getString(cursor, FotoSql.SQL_COL_DISPLAY_TEXT, "???")) + //.append("\n") + ; + } + } - @Override - protected Integer doInBackground(List... params) { - return getWorkflow().updateTags(params[0], params[1]); - } + @Override + protected void onPostExecute(SelectedItems selectedItems) { + if (!isCancelled()) { + if ((selectedItems != null) && (selectedItems.size() > 0)) { + onDuplicatesFound(selectedItems, mStatus); + } else { + onDuplicatesFound(null, mStatus); + } + onNotifyPhotoChanged(); + } else { + if (mStatus != null) { + mStatus.append("\nTask canceled"); + Log.w(Global.LOG_CONTEXT, mDebugPrefix + mStatus); + } + } + } + }; + QueryParameter query = FotoSql.queryGetDuplicates; + FotoSql.setWhereVisibility(query, VISIBILITY.PRIVATE_PUBLIC); + task.execute(query); } private boolean onEditExif(MenuItem menuItem, SelectedFiles fotos) { @@ -1093,6 +1065,11 @@ protected void onDirectoryPick(IDirectory selection) { } } + public void notifyPhotoChanged() { + PhotoChangeNotifyer.notifyPhotoChanged(this.getActivity(), this.mAdapter); + } + + private boolean cmdRenameMultible(MenuItem menuItem, final SelectedFiles fotos) { /* showActivity(String debugContext, Activity context, @@ -1126,23 +1103,6 @@ private static void updateExifUpdateTask(Activity activity) { } } - private boolean cmdMoveOrCopyWithDestDirPicker(final boolean move, String lastCopyToPath, final SelectedFiles fotos) { - if (AndroidFileCommands.canProcessFile(this.getActivity(), false)) { - mDestDirPicker = MoveOrCopyDestDirPicker.newInstance(move, fotos); - - mDestDirPicker.defineDirectoryNavigation(OsUtils.getRootOSDirectory(null), - (move) ? FotoSql.QUERY_TYPE_GROUP_MOVE : FotoSql.QUERY_TYPE_GROUP_COPY, - lastCopyToPath); - if (!LockScreen.isLocked(this.getActivity())) { - mDestDirPicker.setContextMenuId(R.menu.menu_context_pick_osdir); - } - - mDestDirPicker.setBaseQuery(getCurrentQuery()); - mDestDirPicker.show(getActivity().getFragmentManager(), "osdir"); - } - return false; - } - private boolean onPickOk() { mDestDirPicker = null; Activity parent = getActivity(); @@ -1209,7 +1169,7 @@ private Uri getUri(Activity parent, long id) { Uri resultUri = null; if (mModePickGeoElsePickImaage) { // mode pick gep - IGeoPoint initialPoint = FotoSql.execGetPosition(null, parent, + IGeoPoint initialPoint = FotoSql.execGetPosition(null, null, id, mDebugPrefix, "getSelectedUri"); if (initialPoint != null) { @@ -1386,76 +1346,9 @@ private void fixMediaDatabase() { } } - /** image entries may not have DISPLAY_NAME which is essential for calculating the item-s folder. */ - private void repairMissingDisplayNames() { - SqlJobTaskBase task = new SqlJobTaskBase(this.getActivity(), "Searching media database for missing 'displayname'-s:\n", null) { - private int mPathColNo = -2; - private int mResultCount = 0; - - @Override - protected void doInBackground(Long id, Cursor cursor) { - if (mPathColNo == -2) mPathColNo = cursor.getColumnIndex(FotoSql.SQL_COL_PATH); - mResultCount += PhotoPropertiesMediaFilesScanner.getInstance(getActivity()).updatePathRelatedFields(getActivity(), cursor, cursor.getString(mPathColNo), mColumnIndexPK, mPathColNo); - } - - @Override - protected void onPostExecute(SelectedItems selectedItems) { - if (!isCancelled()) { - onMissingDisplayNamesComplete(mStatus); - } - } - }; - QueryParameter query = FotoSql.queryGetMissingDisplayNames; - FotoSql.setWhereVisibility(query, VISIBILITY.PRIVATE_PUBLIC); - task.execute(query); - } - - /** called after MissingDisplayNamesComplete finished */ - private void onMissingDisplayNamesComplete(StringBuffer debugMessage) { - if (debugMessage != null) { - Log.w(Global.LOG_CONTEXT, mDebugPrefix + debugMessage); - } - } - - private void removeDuplicates() { - SqlJobTaskBase task = new SqlJobTaskBase(this.getActivity(), "Searching for duplcates in media database:\n", null) { - @Override - protected void doInBackground(Long id, Cursor cursor) { - this.mSelectedItems.add(id); - if (mStatus != null) { - mStatus - .append("\nduplicate found ") - .append(id) - .append("#") - .append(DBUtils.getString(cursor,FotoSql.SQL_COL_DISPLAY_TEXT,"???")) - //.append("\n") - ; - } - } - - @Override - protected void onPostExecute(SelectedItems selectedItems) { - if (!isCancelled()) { - if ((selectedItems != null) && (selectedItems.size() > 0)) { - onDuplicatesFound(selectedItems, mStatus); - } else { - onDuplicatesFound(null, mStatus); - } - } else { - if (mStatus != null) { - mStatus.append("\nTask canceled"); - Log.w(Global.LOG_CONTEXT, mDebugPrefix + mStatus); - } - - } - } - }; - QueryParameter query = FotoSql.queryGetDuplicates; - FotoSql.setWhereVisibility(query, VISIBILITY.PRIVATE_PUBLIC); - task.execute(query); - } - - /** is called when removeDuplicates() found duplicates */ + /** + * is called when removeDuplicates() found duplicates + */ private void onDuplicatesFound(SelectedItems selectedItems, StringBuffer debugMessage) { if (debugMessage != null) { Log.w(Global.LOG_CONTEXT, mDebugPrefix + debugMessage); @@ -1473,7 +1366,7 @@ private void onDuplicatesFound(SelectedItems selectedItems, StringBuffer debugMe String sqlWhere = query.toAndroidWhere(); // + " OR " + FotoSql.SQL_COL_PATH + " is null"; try { - delCount = FotoSql.deleteMedia(mDebugPrefix + "onDuplicatesFound", activity, sqlWhere, null, true); + delCount = FotoSql.getMediaDBApi().deleteMedia(mDebugPrefix + "onDuplicatesFound", sqlWhere, null, true); } catch (Exception ex) { Log.w(Global.LOG_CONTEXT, "deleteMedia via update failed for 'where " + sqlWhere + "'."); @@ -1490,6 +1383,141 @@ private void onDuplicatesFound(SelectedItems selectedItems, StringBuffer debugMe } } + /** + * called after MissingDisplayNamesComplete finished + */ + private void onMissingDisplayNamesComplete(StringBuffer debugMessage) { + if (debugMessage != null) { + Log.w(Global.LOG_CONTEXT, mDebugPrefix + debugMessage); + } + } + + protected class LocalFileCommands extends AndroidFileCommands { + + @Override + protected void onPostProcess(String what, int opCode, SelectedFiles selectedFiles, int modifyCount, int itemCount, String[] oldPathNames, String[] newPathNames) { + if (Global.clearSelectionAfterCommand || (opCode == OP_DELETE) || (opCode == OP_MOVE)) { + mShowSelectedOnly = true; + multiSelectionCancel(); + } + + super.onPostProcess(what, opCode, selectedFiles, modifyCount, itemCount, oldPathNames, newPathNames); + + if ((mAdapter.isInArrayMode()) && ((opCode == OP_RENAME) || (opCode == OP_MOVE) || (opCode == OP_DELETE))) { + mAdapter.refreshLocal(); + mGalleryView.setAdapter(mAdapter); + } + if ((opCode == OP_RENAME) || (opCode == OP_MOVE) || (opCode == OP_DELETE) || (opCode == OP_RENAME)) { + requeryIfDataHasChanged(); + } + } + } + + class LocalCursorLoader implements LoaderManager.LoaderCallbacks { + /** + * called by LoaderManager.getLoader(ACTIVITY_ID) to (re)create loader + * that attaches to last query/cursor if it still exist i.e. after rotation + */ + @Override + public Loader onCreateLoader(int aLoaderID, Bundle bundle) { + if (loaderID == aLoaderID) { + QueryParameter query = getCurrentQuery(); + mRequeryInstanceCount++; + if (Global.debugEnabledSql) { + Log.i(Global.LOG_CONTEXT, mDebugPrefix + " onCreateLoader" + + getDebugContext() + + " : query = " + query); + } + return FotoSql.createCursorLoader(getActivity().getApplicationContext(), query); + } + + // An invalid id was passed in + return null; + } + + /** + * called after media db content has changed + */ + @Override + public void onLoadFinished(Loader _loader, Cursor data) { + mLastVisiblePosition = mGalleryView.getLastVisiblePosition(); + + final Activity context = getActivity(); + if (data == null) { + CursorLoaderWithException loader = (CursorLoaderWithException) _loader; + String title; + String message = context.getString(R.string.global_err_sql_message_format, loader.getException().getMessage(), loader.getQuery().toSqlString()); + if (loader.getException() != null) { + if (0 != loader.getQuery().toSqlString().compareTo(getCurrentQuery(FotoSql.queryDetail).toSqlString())) { + // query is not default query. revert to default query + mGalleryContentQuery = FotoSql.queryDetail; + requery("requery after query-errror"); + title = context.getString(R.string.global_err_sql_title_reload); + } else { + title = context.getString(R.string.global_err_system); + context.finish(); + } + Dialogs.messagebox(context, title, message); + return; + } + } + + mUpdateId = FotoSql.getMediaDBApi().getCurrentUpdateId(); + // do change the data + mAdapter.swapCursor(data); + + if (mLastVisiblePosition > 0) { + mGalleryView.smoothScrollToPosition(mLastVisiblePosition); + mLastVisiblePosition = -1; + } + + final int resultCount = (data == null) ? 0 : data.getCount(); + if (Global.debugEnabled) { + Log.i(Global.LOG_CONTEXT, mDebugPrefix + " onLoadFinished" + + getDebugContext() + + " fount " + resultCount + " rows"); + } + + // do change the data + notifyPhotoChanged(); + + if (mLastVisiblePosition > 0) { + mGalleryView.smoothScrollToPosition(mLastVisiblePosition); + mLastVisiblePosition = -1; + } + + // show the changes + + if (context instanceof OnGalleryInteractionListener) { + ((OnGalleryInteractionListener) context).setResultCount(resultCount); + } + multiSelectionReplaceTitleIfNecessary(); + } + + /** + * called by LoaderManager. after search criteria were changed or if activity is destroyed. + */ + @Override + public void onLoaderReset(Loader loader) { + if (Global.debugEnabled) { + Log.i(Global.LOG_CONTEXT, mDebugPrefix + " onLoaderReset" + getDebugContext()); + } + // rember position where we have to scroll to after refreshLocal is finished. + mLastVisiblePosition = mGalleryView.getLastVisiblePosition(); + + mAdapter.swapCursor(null); + notifyPhotoChanged(); + } + + @NonNull + protected String getDebugContext() { + return "(@" + loaderID + ", #" + mRequeryInstanceCount + + ", LastVisiblePosition=" + mLastVisiblePosition + +// ", Path='" + mInitialFilePath + + "')"; + } + } + private boolean isMultiSelectionActive() { if (mMode != MODE_VIEW_PICKER_NONE) return true; return !mSelectedItems.isEmpty(); @@ -1531,4 +1559,25 @@ private boolean toggleSelection(long imageID) { } } + private class TagUpdateTask extends TagTask> { + + TagUpdateTask(SelectedFiles fotos) { + super(GalleryCursorFragment.this.getActivity(), R.string.tags_activity_title); + this.getWorkflow().init(GalleryCursorFragment.this.getActivity(), fotos, null); + + } + + @Override + protected Integer doInBackground(List... params) { + return getWorkflow().updateTags(params[0], params[1]); + } + + @Override + protected void onPostExecute(Integer itemCount) { + super.onPostExecute(itemCount); + onNotifyPhotoChanged(); + } + + } + } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/imagedetail/ImageDetailActivityViewPager.java b/app/src/main/java/de/k3b/android/androFotoFinder/imagedetail/ImageDetailActivityViewPager.java index baddba71..f7ba9b52 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/imagedetail/ImageDetailActivityViewPager.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/imagedetail/ImageDetailActivityViewPager.java @@ -1,6 +1,6 @@ /******************************************************************************* * Copyright 2011, 2012 Chris Banes. - * Copyright (c) 2015-2019 by k3b. + * Copyright (c) 2015-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -67,6 +67,7 @@ import de.k3b.android.util.FileManagerUtil; import de.k3b.android.util.IntentUtil; import de.k3b.android.util.OsUtils; +import de.k3b.android.util.PhotoChangeNotifyer; import de.k3b.android.util.PhotoPropertiesMediaFilesScanner; import de.k3b.android.util.PhotoPropertiesMediaFilesScannerAsyncTask; import de.k3b.android.widget.AboutDialogPreference; @@ -92,7 +93,8 @@ * Swipe left/right to show previous/next image. */ -public class ImageDetailActivityViewPager extends ActivityWithAutoCloseDialogs implements Common, TagsPickerFragment.ITagsPicker { +public class ImageDetailActivityViewPager extends ActivityWithAutoCloseDialogs implements Common, TagsPickerFragment.ITagsPicker, + PhotoChangeNotifyer.PhotoChangedListener { private static final String INSTANCE_STATE_MODIFY_COUNT = "mModifyCount"; private static final String INSTANCE_STATE_LAST_SCROLL_POSITION = "lastScrollPosition"; /** #70: remember on config change (screen rotation) */ @@ -165,180 +167,52 @@ public class ImageDetailActivityViewPager extends ActivityWithAutoCloseDialogs i // if not null this one image that cannot be translated to a file uri will be shown private Uri imageUri = null; - /** executes sql to load image detail data in a background task that may survive - * conriguration change (i.e. device rotation) */ - class LocalCursorLoader implements LoaderManager.LoaderCallbacks { - /** incremented every time a new curster/query is generated */ - private int mRequeryInstanceCount = 0; - - /** called by LoaderManager.getLoader(ACTIVITY_ID) to (re)create loader - * that attaches to last query/cursor if it still exist i.e. after rotation */ - @Override - public Loader onCreateLoader(int loaderID, Bundle bundle) { - switch (loaderID) { - case ACTIVITY_ID: - mRequeryInstanceCount++; - mWaitingForMediaScannerResult = false; - if (Global.debugEnabledSql) { - Log.i(Global.LOG_CONTEXT, mDebugPrefix + " onCreateLoader" + - getDebugContext() + - " : query = " + mGalleryContentQuery); - } - return FotoSql.createCursorLoader(getApplicationContext(), mGalleryContentQuery); - default: - // An invalid id was passed in - return null; - } - } - - /** called after media db content has changed */ - @Override - public void onLoadFinished(Loader loader, Cursor data) { - // to be restored after refreshLocal if there is no mInitialFilePath - if ((mInitialScrollPosition == NO_INITIAL_SCROLL_POSITION) && (mViewPager != null)) { - mInitialScrollPosition = mViewPager.getCurrentItem(); - } - // do change the data - mAdapter.swapCursor(data); - - // restore position is invalid - final int newItemCount = mAdapter.getCount(); + long mUpdateId = FotoSql.getMediaDBApi().getCurrentUpdateId(); - if (((newItemCount == 0)) || (mInitialScrollPosition >= newItemCount)) mInitialScrollPosition = NO_INITIAL_SCROLL_POSITION; - - if (Global.debugEnabledSql) { - Log.i(Global.LOG_CONTEXT, mDebugPrefix + " onLoadFinished" + - getDebugContext() + - " found " + ((data == null) ? 0 : newItemCount) + " rows"); - } - - // do change the data - mAdapter.notifyDataSetChanged(); - mViewPager.setAdapter(mAdapter); + private static int updateIncompleteMediaDatabase(String debugPrefix, Context context, String why, File dirToScan) { + if (dirToScan == null) return 0; - // show the changes - onLoadCompleted(); - } + String dbPathSearch = null; + ArrayList missing = new ArrayList(); + dbPathSearch = dirToScan.getPath() + "/%"; + List known = FotoSql.execGetFotoPaths(dbPathSearch); + File[] existing = dirToScan.listFiles(); - /** called by LoaderManager. after search criteria were changed or if activity is destroyed. */ - @Override - public void onLoaderReset(Loader loader) { - // rember position where we have to scroll to after refreshLocal is finished. - mInitialScrollPosition = mViewPager.getCurrentItem(); - mAdapter.swapCursor(null); - if (Global.debugEnabledSql) { - Log.i(Global.LOG_CONTEXT, mDebugPrefix + " onLoaderReset" + - getDebugContext()); + if (existing != null) { + for (File file : existing) { + String found = file.getAbsolutePath(); + if (PhotoPropertiesUtil.isImage(found, PhotoPropertiesUtil.IMG_TYPE_ALL) && !known.contains(found)) { + missing.add(found); + } } - mAdapter.notifyDataSetChanged(); } - @NonNull - private String getDebugContext() { - return "(#" + mRequeryInstanceCount - + ", mScrollPosition=" + mInitialScrollPosition + - ", Path='" + mInitialFilePath + - "')"; - } - } - - class LocalFileCommands extends AndroidFileCommands { - @Override - protected void onPostProcess(String what, int opCode, SelectedFiles selectedFiles, int modifyCount, int itemCount, String[] oldPathNames, String[] newPathNames) { - mInitialFilePath = null; - switch (opCode) { - case OP_MOVE: - case OP_RENAME: - if ((newPathNames!= null) && (newPathNames.length > 0)) { - // so selection will be restored to this after load complete - mInitialFilePath = newPathNames[0]; - } - break; - case OP_COPY: - if ((oldPathNames!= null) && (oldPathNames.length > 0)) { - // so selection will be restored to this after load complete - mInitialFilePath = oldPathNames[0]; - } - break; - default:break; - } - - super.onPostProcess(what, opCode, selectedFiles, modifyCount, itemCount, oldPathNames, newPathNames); + if (Global.debugEnabled) { + StringBuilder message = new StringBuilder(); + message.append(debugPrefix).append("updateIncompleteMediaDatabase('") + .append(dbPathSearch).append("') : \n\t"); - if ((opCode == OP_RENAME) || (opCode == OP_MOVE) || (opCode == OP_DELETE)) { - refreshIfNecessary(); + for (String s : missing) { + message.append(s).append("; "); } + Log.d(Global.LOG_CONTEXT, message.toString()); } + PhotoPropertiesMediaFilesScannerAsyncTask scanner = new PhotoPropertiesMediaFilesScannerAsyncTask(PhotoPropertiesMediaFilesScanner.getInstance(context), context, why); + scanner.execute(null, missing.toArray(new String[missing.size()])); + return missing.size(); } - public static class MoveOrCopyDestDirPicker extends DirectoryPickerFragment { - protected static AndroidFileCommands sFileCommands = null; - - public static MoveOrCopyDestDirPicker newInstance(boolean move, final SelectedFiles srcFotos) { - MoveOrCopyDestDirPicker f = new MoveOrCopyDestDirPicker(); - - // Supply index input as an argument. - Bundle args = new Bundle(); - args.putBoolean("move", move); - AffUtils.putSelectedFiles(args, srcFotos); - f.setArguments(args); - - return f; - } - - /* do not use activity callback */ - @Override - protected void setDirectoryListener(Activity activity) {/* do not use activity callback */} - - public boolean getMove() { - return getArguments().getBoolean("move", false); - } - - public SelectedFiles getSrcFotos() { - return AffUtils.getSelectedFiles(getArguments()); - } - - /** - * To be overwritten to check if a path can be picked. - * - * @param path to be checked if it cannot be handled - * @return null if no error else error message with the reason why it cannot be selected - */ - @Override - protected String getStatusErrorMessage(String path) { - String errorMessage = (sFileCommands == null) ? null : sFileCommands.checkWriteProtected(0, new File(path)); - if (errorMessage != null) { - int pos = errorMessage.indexOf('\n'); - return (pos > 0) ? errorMessage.substring(0,pos) : errorMessage; + /** + * #70: adds/removes/replases contextColumnExpression + */ + private static void addContextColumn(QueryParameter query, String contextColumnExpression) { + if (query != null) { + query.removeFirstColumnThatContains(CONTEXT_COLUMN_ALIAS); + if ((contextColumnExpression != null) && (contextColumnExpression.trim().length() > 0)) { + query.addColumn(contextColumnExpression + CONTEXT_COLUMN_ALIAS); } - return super.getStatusErrorMessage(path); - } - - @Override - protected void onDirectoryPick(IDirectory selection) { - // super.onDirectoryPick(selection); - mModifyCount++; // copy or move initiated - getActivity().setResult((mModifyCount == 0) ? RESULT_NOCHANGE : RESULT_CHANGE); - - sFileCommands.onMoveOrCopyDirectoryPick(getMove(), getSrcFotos(), selection); - dismiss(); - } - } - - private class TagUpdateTask extends TagTask> { - - TagUpdateTask(SelectedFiles fotos) { - super(ImageDetailActivityViewPager.this,R.string.tags_activity_title); - this.getWorkflow().init(ImageDetailActivityViewPager.this, fotos, null); - - } - - @Override - protected Integer doInBackground(List... params) { - return getWorkflow().updateTags(params[0], params[1]); } - } /** @@ -535,10 +409,10 @@ private void copyExtras(Intent dest, Bundle source, String... keys) { } @Override - protected void onActivityResult(final int requestCode, - final int resultCode, final Intent intent) { - super.onActivityResult(requestCode, resultCode, intent); - if (mDestDirPicker != null) mDestDirPicker.onActivityResult(requestCode,resultCode,intent); + protected void onResume() { + unhideActionBar(Global.actionBarHideTimeInMilliSecs, "onResume"); + Global.debugMemory(mDebugPrefix, "onResume"); + super.onResume(); final boolean locked = LockScreen.isLocked(this); if (this.locked != locked) { @@ -547,20 +421,17 @@ protected void onActivityResult(final int requestCode, invalidateOptionsMenu(); } - if (requestCode == ACTION_RESULT_FORWARD) { - // forward result from child-activity to parent-activity - setResult(resultCode, intent); - finish(); - } else if (requestCode == ACTION_RESULT_MUST_MEDIA_SCAN) { - // #64 after edit the content might have been changed. update media DB. - String orgiginalFileToScan = getCurrentFilePath(); + if (Global.debugEnabledMemory) { + Log.d(Global.LOG_CONTEXT, mDebugPrefix + " - onResume cmd (" + + MoveOrCopyDestDirPicker.sFileCommands + ") => (" + mFileCommands + + ")"); - if (orgiginalFileToScan != null) { - PhotoPropertiesMediaFilesScanner.getInstance(this).updateMediaDatabase_Android42(this, null, orgiginalFileToScan); - } } - refreshIfNecessary(); + // workaround fragment lifecycle is newFragment.attach oldFragment.detach. + // this makes shure that the visible fragment has commands + MoveOrCopyDestDirPicker.sFileCommands = mFileCommands; + reloadIfDataHasChanged(); } private static boolean mustForward(Intent intent) { @@ -651,18 +522,11 @@ public void setTitle(CharSequence title) { */ @Override - protected void onPause () { - unhideActionBar(DISABLE_HIDE_ACTIONBAR, "onPause"); - Global.debugMemory(mDebugPrefix, "onPause"); - startStopSlideShow(false); - super.onPause(); - } - - @Override - protected void onResume () { - unhideActionBar(Global.actionBarHideTimeInMilliSecs, "onResume"); - Global.debugMemory(mDebugPrefix, "onResume"); - super.onResume(); + protected void onActivityResult(final int requestCode, + final int resultCode, final Intent intent) { + super.onActivityResult(requestCode, resultCode, intent); + if (mDestDirPicker != null) + mDestDirPicker.onActivityResult(requestCode, resultCode, intent); final boolean locked = LockScreen.isLocked(this); if (this.locked != locked) { @@ -671,16 +535,29 @@ protected void onResume () { invalidateOptionsMenu(); } - if (Global.debugEnabledMemory) { - Log.d(Global.LOG_CONTEXT, mDebugPrefix + " - onResume cmd (" + - MoveOrCopyDestDirPicker.sFileCommands + ") => (" + mFileCommands + - ")"); + if (requestCode == ACTION_RESULT_FORWARD) { + // forward result from child-activity to parent-activity + setResult(resultCode, intent); + finish(); + } else if (requestCode == ACTION_RESULT_MUST_MEDIA_SCAN) { + // #64 after edit the content might have been changed. update media DB. + String orgiginalFileToScan = getCurrentFilePath(); + if (orgiginalFileToScan != null) { + PhotoPropertiesMediaFilesScanner.getInstance(this).updateMediaDatabase_Android42(this, null, orgiginalFileToScan); + } } - // workaround fragment lifecycle is newFragment.attach oldFragment.detach. - // this makes shure that the visible fragment has commands - MoveOrCopyDestDirPicker.sFileCommands = mFileCommands; + reloadIfDataHasChanged(); + } + + @Override + protected void onPause() { + unhideActionBar(DISABLE_HIDE_ACTIONBAR, "onPause"); + Global.debugMemory(mDebugPrefix, "onPause"); + startStopSlideShow(false); + PhotoChangeNotifyer.setPhotoChangedListener(null); // notify triggererd in onResume + super.onPause(); } @Override @@ -760,16 +637,6 @@ private void onLoadCompleted() { } } - private void refreshIfNecessary() { - if ((mAdapter != null) && (mViewPager != null) && (mAdapter.isInArrayMode())) { - mAdapter.refreshLocal(); - mViewPager.setAdapter(mAdapter); - - // show the changes - onLoadCompleted(); - } - } - /** * gets called if no file is found by a db-query or if jpgFullFilePath is not found in media db * return false; activity must me closed @@ -794,38 +661,43 @@ private boolean checkForIncompleteMediaDatabase(String jpgFullFilePath, String w return false; } - private static int updateIncompleteMediaDatabase(String debugPrefix, Context context, String why, File dirToScan) { - if (dirToScan == null) return 0; + private void onRenameAnswer(SelectedFiles currentFoto, final long fotoId, final String fotoSourcePath, String newFileName) { + File src = new File(fotoSourcePath); + File dest = new File(src.getParentFile(), newFileName); - String dbPathSearch = null; - ArrayList missing = new ArrayList(); - dbPathSearch = dirToScan.getPath() + "/%"; - List known = FotoSql.execGetFotoPaths(context, dbPathSearch); - File[] existing = dirToScan.listFiles(); + File srcXmpShort = FileProcessor.getSidecar(src, false); + boolean hasSideCarShort = ((srcXmpShort != null) && (mFileCommands.osFileExists(srcXmpShort))); + File srcXmpLong = FileProcessor.getSidecar(src, true); + boolean hasSideCarLong = ((srcXmpLong != null) && (mFileCommands.osFileExists(srcXmpLong))); - if (existing != null) { - for (File file : existing) { - String found = file.getAbsolutePath(); - if (PhotoPropertiesUtil.isImage(found, PhotoPropertiesUtil.IMG_TYPE_ALL) && !known.contains(found)) { - missing.add(found); - } - } - } + File destXmpShort = FileProcessor.getSidecar(dest, false); + File destXmpLong = FileProcessor.getSidecar(dest, true); - if (Global.debugEnabled) { - StringBuilder message = new StringBuilder(); - message.append(debugPrefix).append("updateIncompleteMediaDatabase('") - .append(dbPathSearch).append("') : \n\t"); + if (src.equals(dest)) return; // new name == old name ==> nothing to do - for(String s : missing) { - message.append(s).append("; "); - } - Log.d(Global.LOG_CONTEXT, message.toString()); + String errorMessage = null; + if (hasSideCarShort && mFileCommands.osFileExists(destXmpShort)) { + errorMessage = getString(R.string.image_err_file_exists_format, destXmpShort.getAbsoluteFile()); + } + if (hasSideCarLong && mFileCommands.osFileExists(destXmpLong)) { + errorMessage = getString(R.string.image_err_file_exists_format, destXmpLong.getAbsoluteFile()); + } + if (mFileCommands.osFileExists(dest)) { + errorMessage = getString(R.string.image_err_file_exists_format, dest.getAbsoluteFile()); } - PhotoPropertiesMediaFilesScannerAsyncTask scanner = new PhotoPropertiesMediaFilesScannerAsyncTask(PhotoPropertiesMediaFilesScanner.getInstance(context), context, why); - scanner.execute(null, missing.toArray(new String[missing.size()])); - return missing.size(); + PhotoChangeNotifyer.setPhotoChangedListener(this); + if (errorMessage != null) { + // dest-file already exists + Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show(); + onRenameQueston(currentFoto, fotoId, fotoSourcePath, newFileName); + } else if (mFileCommands.rename(currentFoto, dest, null)) { + mModifyCount++; + } else { + // rename failed + errorMessage = getString(R.string.image_err_file_rename_format, src.getAbsoluteFile()); + Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show(); + } } @Override @@ -889,6 +761,90 @@ public boolean onPrepareOptionsMenu(Menu menu) { return super.onPrepareOptionsMenu(menu); } + private boolean onRenameQueston(final SelectedFiles currentFoto, final long fotoId, final String fotoPath, final String _newName) { + if (AndroidFileCommands.canProcessFile(this, false)) { + final String newName = (_newName == null) + ? new File(getCurrentFilePath()).getName() + : _newName; + + + Dialogs dialog = new Dialogs() { + @Override + protected void onDialogResult(String newFileName, Object... parameters) { + if (newFileName != null) { + onRenameAnswer(currentFoto, (Long) parameters[0], (String) parameters[1], newFileName); + } + } + }; + dialog.editFileName(this, getString(R.string.rename_menu_title), newName, fotoId, fotoPath); + } + return true; + } + + private boolean cmdMoveOrCopyWithDestDirPicker(final boolean move, String lastCopyToPath, final SelectedFiles fotos) { + if (AndroidFileCommands.canProcessFile(this, false)) { + PhotoChangeNotifyer.setPhotoChangedListener(this); + mDestDirPicker = MoveOrCopyDestDirPicker.newInstance(move, fotos); + + mDestDirPicker.defineDirectoryNavigation(OsUtils.getRootOSDirectory(null), + (move) ? FotoSql.QUERY_TYPE_GROUP_MOVE : FotoSql.QUERY_TYPE_GROUP_COPY, + lastCopyToPath); + mDestDirPicker.setContextMenuId(LockScreen.isLocked(this) ? 0 : R.menu.menu_context_pick_osdir); + mDestDirPicker.setBaseQuery(mGalleryContentQuery); + mDestDirPicker.show(this.getFragmentManager(), "osdirimage"); + } + return false; + } + + private static final int SLIDESHOW_HANDLER_ID = 2; + private boolean mSlideShowStarted = false; + private Handler mSlideShowTimer = new Handler() { + public void handleMessage(Message m) { + if (mSlideShowStarted) { + onSlideShowNext(); + sendMessageDelayed(Message.obtain(this, SLIDESHOW_HANDLER_ID), Global.slideshowIntervalInMilliSecs); + } + } + }; + + private void startStopSlideShow(boolean start) { + if (mViewPager != null) { + mViewPager.setLocked(start); + if (start != mSlideShowStarted) { + if (start) { + onSlideShowNext(); + mSlideShowTimer.sendMessageDelayed(Message.obtain(mSlideShowTimer, SLIDESHOW_HANDLER_ID), Global.slideshowIntervalInMilliSecs); + } else { + mSlideShowTimer.removeMessages(SLIDESHOW_HANDLER_ID); + } + mSlideShowStarted = start; + + // #24 Prevent sleepmode while slideshow is active + if (this.mViewPager != null) this.mViewPager.setKeepScreenOn(start); + + if (mMenuSlideshow != null) mMenuSlideshow.setChecked(start); + } + } + } + + private void onSlideShowNext() { + int pos = mViewPager.getCurrentItem() + 1; + if (pos >= mAdapter.getCount()) pos = 0; + mViewPager.setCurrentItem(pos); + } + + private void cmdShowDetails(String fullFilePath, long currentImageId) { + + CharSequence countMsg = TagSql.getStatisticsMessage(this, R.string.show_photo, + mGalleryContentQuery); + + ImageDetailMetaDialogBuilder.createImageDetailDialog(this, fullFilePath, currentImageId, + mGalleryContentQuery, + mViewPager.getCurrentItem(), + countMsg).show(); + + } + @Override public boolean onOptionsItemSelected(MenuItem menuItem) { boolean reloadContext = true; @@ -897,11 +853,11 @@ public boolean onOptionsItemSelected(MenuItem menuItem) { onGuiTouched(); if (LockScreen.onOptionsItemSelected(this, menuItem)) { - mMustReplaceMenue = true; + mMustReplaceMenue = true; this.invalidateOptionsMenu(); return true; } - if (mFileCommands.onOptionsItemSelected(menuItem, getCurrentFoto())) { + if (mFileCommands.onOptionsItemSelected(menuItem, getCurrentFoto(), this)) { mModifyCount++; } else { // Handle presses on the action bar items @@ -941,10 +897,11 @@ public boolean onOptionsItemSelected(MenuItem menuItem) { result = cmdMoveOrCopyWithDestDirPicker(false, mFileCommands.getLastCopyToPath(), getCurrentFoto()); break; case R.id.cmd_move: - result = cmdMoveOrCopyWithDestDirPicker(true, mFileCommands.getLastCopyToPath(), getCurrentFoto()); + result = cmdMoveOrCopyWithDestDirPicker(true, mFileCommands.getLastCopyToPath(), getCurrentFoto()); break; case R.id.menu_item_rename: - result = onRenameDirQueston(getCurrentFoto(), getCurrentImageId(), getCurrentFilePath(), null); + PhotoChangeNotifyer.setPhotoChangedListener(this); + result = onRenameQueston(getCurrentFoto(), getCurrentImageId(), getCurrentFilePath(), null); break; case R.id.menu_exif: result = onEditExif(menuItem, getCurrentFoto(), getCurrentImageId(), getCurrentFilePath()); @@ -955,8 +912,8 @@ public boolean onOptionsItemSelected(MenuItem menuItem) { String dirPath = getCurrentFilePath(); // PhotoPropertiesMediaFilesScanner.getDir().getAbsolutePath(); if (dirPath != null) { dirPath = FileUtils.getDir(dirPath).getAbsolutePath(); - QueryParameter query = FotoSql.addWhereFolderWithoutSubfolders(new QueryParameter(FotoSql.queryDetail), dirPath); + QueryParameter query = FotoSql.addWhereFolderWithoutSubfolders(new QueryParameter(FotoSql.queryDetail), dirPath); FotoGalleryActivity.showActivity(" menu " + menuItem.getTitle() + "[13]" + dirPath, this, query, 0); } @@ -970,14 +927,14 @@ public boolean onOptionsItemSelected(MenuItem menuItem) { case R.id.cmd_show_geo_as: { final long imageId = getCurrentImageId(); - IGeoPoint _geo = FotoSql.execGetPosition(null, this, + IGeoPoint _geo = FotoSql.execGetPosition(null, null, imageId, mDebugPrefix, "on cmd_show_geo_as"); final String currentFilePath = getCurrentFilePath(); GeoPointDto geo = new GeoPointDto(_geo.getLatitude(), _geo.getLongitude(), GeoPointDto.NO_ZOOM); geo.setDescription(currentFilePath); - geo.setId(""+imageId); - geo.setName("#"+imageId); + geo.setId("" + imageId); + geo.setName("#" + imageId); GeoUri PARSER = new GeoUri(GeoUri.OPT_PARSE_INFER_MISSING); String uri = PARSER.toUriString(geo); @@ -1028,74 +985,55 @@ public void run() { } - private static final int SLIDESHOW_HANDLER_ID = 2; - private boolean mSlideShowStarted = false; - private Handler mSlideShowTimer = new Handler() { - public void handleMessage(Message m) { - if (mSlideShowStarted) { - onSlideShowNext(); - sendMessageDelayed(Message.obtain(this, SLIDESHOW_HANDLER_ID), Global.slideshowIntervalInMilliSecs); - } - } - }; + private boolean onEditExif(MenuItem menuItem, SelectedFiles currentFoto, final long fotoId, final String fotoPath) { + PhotoPropertiesEditActivity.showActivity(" menu " + menuItem.getTitle(), + this, null, fotoPath, currentFoto, 0, true); + return true; + } - private void startStopSlideShow(boolean start) { - if (mViewPager != null) { - mViewPager.setLocked(start); - if (start != mSlideShowStarted) { - if (start) { - onSlideShowNext(); - mSlideShowTimer.sendMessageDelayed(Message.obtain(mSlideShowTimer, SLIDESHOW_HANDLER_ID), Global.slideshowIntervalInMilliSecs); - } else { - mSlideShowTimer.removeMessages(SLIDESHOW_HANDLER_ID); - } - mSlideShowStarted = start; + /** + * #70: Gui has changed ContextExpression + * + * @param modeName property name. if starting with "auto" then the image detail quick-botton is redefined. + * @param contextSqlColumnExpression if not empy the result of this expression is shown as context data in image detail view. + */ + private void onDefineContext(String modeName, String contextSqlColumnExpression) { + addContextColumn(mGalleryContentQuery, contextSqlColumnExpression); + if ((mGalleryContentQuery != null) + && (0 != StringUtils.compare(contextSqlColumnExpression, mContextColumnExpression)) + && (this.mAdapter != null)) { + // sql detail expression has changed and initialization has completed: requery - // #24 Prevent sleepmode while slideshow is active - if (this.mViewPager != null) this.mViewPager.setKeepScreenOn(start); + this.mContextColumnExpression = contextSqlColumnExpression; // prevent executing again + requery(); + } + this.mContextColumnExpression = contextSqlColumnExpression; // prevent executing again - if (mMenuSlideshow != null) mMenuSlideshow.setChecked(start); + if (modeName != null) { + this.mContextName = modeName; + if (this.mAdapter != null) { + this.mAdapter.setIconResourceName(modeName); } } - } - private void onSlideShowNext() { - int pos = mViewPager.getCurrentItem() + 1; - if (pos >= mAdapter.getCount()) pos = 0; - mViewPager.setCurrentItem(pos); } - private void cmdShowDetails(String fullFilePath, long currentImageId) { - - CharSequence countMsg = TagSql.getStatisticsMessage(this, R.string.show_photo, - mGalleryContentQuery); - - ImageDetailMetaDialogBuilder.createImageDetailDialog(this, fullFilePath, currentImageId, - mGalleryContentQuery, - mViewPager.getCurrentItem(), - countMsg).show(); - + public void onNotifyPhotoChanged() { + requeryIfDataHasChanged(); } - private boolean cmdMoveOrCopyWithDestDirPicker(final boolean move, String lastCopyToPath, final SelectedFiles fotos) { - if (AndroidFileCommands.canProcessFile(this, false)) { - mDestDirPicker = MoveOrCopyDestDirPicker.newInstance(move, fotos); + private void reloadIfDataHasChanged() { + if ((mAdapter != null) && (mViewPager != null) && (mAdapter.isInArrayMode())) { + mAdapter.refreshLocal(); + mViewPager.setAdapter(mAdapter); - mDestDirPicker.defineDirectoryNavigation(OsUtils.getRootOSDirectory(null), - (move) ? FotoSql.QUERY_TYPE_GROUP_MOVE : FotoSql.QUERY_TYPE_GROUP_COPY, - lastCopyToPath); - mDestDirPicker.setContextMenuId(LockScreen.isLocked(this) ? 0 : R.menu.menu_context_pick_osdir); - mDestDirPicker.setBaseQuery(mGalleryContentQuery); - mDestDirPicker.show(this.getFragmentManager(), "osdirimage"); + // show the changes + onLoadCompleted(); + } else { + requeryIfDataHasChanged(); } - return false; } - private boolean onEditExif(MenuItem menuItem, SelectedFiles currentFoto, final long fotoId, final String fotoPath) { - PhotoPropertiesEditActivity.showActivity(" menu " + menuItem.getTitle(), - this, null, fotoPath, currentFoto, 0, true); - return true; - } private boolean onRenameDirQueston(final SelectedFiles currentFoto, final long fotoId, final String fotoPath, final String _newName) { if (AndroidFileCommands.canProcessFile(this, false)) { final String newName = (_newName == null) @@ -1154,6 +1092,12 @@ private void onRenameSubDirAnswer(SelectedFiles currentFoto, final long fotoId, } } + private void requeryIfDataHasChanged() { + if (FotoSql.getMediaDBApi().mustRequery(mUpdateId)) { + requery(); + } + } + private boolean tagsShowEditDialog(SelectedFiles fotos) { mTagWorflow = new TagUpdateTask(fotos); TagsPickerFragment dlg = new TagsPickerFragment(); @@ -1164,6 +1108,7 @@ private boolean tagsShowEditDialog(SelectedFiles fotos) { dlg.setRemoveNames(new ArrayList()); dlg.setBaseQuery(mGalleryContentQuery); setAutoClose(dlg, null, null); + PhotoChangeNotifyer.setPhotoChangedListener(this); dlg.show(getFragmentManager(), "editTags"); return true; } @@ -1294,45 +1239,215 @@ private void setContextMode(Object modeName) { } } + private void requery() { + mUpdateId = FotoSql.getMediaDBApi().getCurrentUpdateId(); + if (mCurorLoader == null) { + // query has not been initialized + mCurorLoader = new LocalCursorLoader(); + getLoaderManager().initLoader(ACTIVITY_ID, null, mCurorLoader); + } else { + // query has changed + getLoaderManager().restartLoader(ACTIVITY_ID, null, this.mCurorLoader); + } + } + + public void notifyPhotoChanged() { + PhotoChangeNotifyer.notifyPhotoChanged(this, this.mAdapter); + } + + public static class MoveOrCopyDestDirPicker extends DirectoryPickerFragment { + protected static AndroidFileCommands sFileCommands = null; + + public static MoveOrCopyDestDirPicker newInstance(boolean move, final SelectedFiles srcFotos) { + MoveOrCopyDestDirPicker f = new MoveOrCopyDestDirPicker(); + + // Supply index input as an argument. + Bundle args = new Bundle(); + args.putBoolean("move", move); + AffUtils.putSelectedFiles(args, srcFotos); + f.setArguments(args); + + return f; + } + + /* do not use activity callback */ + @Override + protected void setDirectoryListener(Activity activity) {/* do not use activity callback */} + + public boolean getMove() { + return getArguments().getBoolean("move", false); + } + + public SelectedFiles getSrcFotos() { + return AffUtils.getSelectedFiles(getArguments()); + } + + /** + * To be overwritten to check if a path can be picked. + * + * @param path to be checked if it cannot be handled + * @return null if no error else error message with the reason why it cannot be selected + */ + @Override + protected String getStatusErrorMessage(String path) { + String errorMessage = (sFileCommands == null) ? null : sFileCommands.checkWriteProtected(0, new File(path)); + if (errorMessage != null) { + int pos = errorMessage.indexOf('\n'); + return (pos > 0) ? errorMessage.substring(0, pos) : errorMessage; + } + return super.getStatusErrorMessage(path); + } + + @Override + protected void onDirectoryPick(IDirectory selection) { + // super.onDirectoryPick(selection); + mModifyCount++; // copy or move initiated + getActivity().setResult((mModifyCount == 0) ? RESULT_NOCHANGE : RESULT_CHANGE); + + PhotoChangeNotifyer.setPhotoChangedListener(((ImageDetailActivityViewPager) getActivity())); + sFileCommands.onMoveOrCopyDirectoryPick(getMove(), getSrcFotos(), selection); + dismiss(); + } + } + + class LocalFileCommands extends AndroidFileCommands { + @Override + protected void onPostProcess(String what, int opCode, SelectedFiles selectedFiles, int modifyCount, int itemCount, String[] oldPathNames, String[] newPathNames) { + mInitialFilePath = null; + switch (opCode) { + case OP_MOVE: + case OP_RENAME: + if ((newPathNames != null) && (newPathNames.length > 0)) { + // so selection will be restored to this after load complete + mInitialFilePath = newPathNames[0]; + } + break; + case OP_COPY: + if ((oldPathNames != null) && (oldPathNames.length > 0)) { + // so selection will be restored to this after load complete + mInitialFilePath = oldPathNames[0]; + } + break; + default: + break; + } + + super.onPostProcess(what, opCode, selectedFiles, modifyCount, itemCount, oldPathNames, newPathNames); + + if ((opCode == OP_RENAME) || (opCode == OP_MOVE) || (opCode == OP_DELETE) || (opCode == OP_RENAME)) { + reloadIfDataHasChanged(); + } + } + + } + + private class TagUpdateTask extends TagTask> { + + TagUpdateTask(SelectedFiles fotos) { + super(ImageDetailActivityViewPager.this, R.string.tags_activity_title); + this.getWorkflow().init(ImageDetailActivityViewPager.this, fotos, null); + + } + + @Override + protected Integer doInBackground(List... params) { + return getWorkflow().updateTags(params[0], params[1]); + } + + @Override + protected void onPostExecute(Integer itemCount) { + super.onPostExecute(itemCount); + reloadIfDataHasChanged(); + } + } + /** - * #70: Gui has changed ContextExpression - * @param modeName property name. if starting with "auto" then the image detail quick-botton is redefined. - * @param contextSqlColumnExpression if not empy the result of this expression is shown as context data in image detail view. + * executes sql to load image detail data in a background task that may survive + * conriguration change (i.e. device rotation) */ - private void onDefineContext(String modeName, String contextSqlColumnExpression) { - addContextColumn(mGalleryContentQuery, contextSqlColumnExpression); - if ((mGalleryContentQuery != null) - && (0 != StringUtils.compare(contextSqlColumnExpression, mContextColumnExpression)) - && (this.mAdapter != null)) { - // sql detail expression has changed and initialization has completed: requery + class LocalCursorLoader implements LoaderManager.LoaderCallbacks { + /** + * incremented every time a new curster/query is generated + */ + private int mRequeryInstanceCount = 0; - this.mContextColumnExpression = contextSqlColumnExpression; // prevent executing again - if (mCurorLoader == null) { - // query has not been initialized - mCurorLoader = new LocalCursorLoader(); - getLoaderManager().initLoader(ACTIVITY_ID, null, mCurorLoader); - } else { - // query has changed - getLoaderManager().restartLoader(ACTIVITY_ID, null, this.mCurorLoader); + /** + * called by LoaderManager.getLoader(ACTIVITY_ID) to (re)create loader + * that attaches to last query/cursor if it still exist i.e. after rotation + */ + @Override + public Loader onCreateLoader(int loaderID, Bundle bundle) { + switch (loaderID) { + case ACTIVITY_ID: + mRequeryInstanceCount++; + mWaitingForMediaScannerResult = false; + if (Global.debugEnabledSql) { + Log.i(Global.LOG_CONTEXT, mDebugPrefix + " onCreateLoader" + + getDebugContext() + + " : query = " + mGalleryContentQuery); + } + return FotoSql.createCursorLoader(getApplicationContext(), mGalleryContentQuery); + default: + // An invalid id was passed in + return null; } } - this.mContextColumnExpression = contextSqlColumnExpression; // prevent executing again - if (modeName != null) { - this.mContextName = modeName; - if (this.mAdapter != null) { - this.mAdapter.setIconResourceName(modeName); + /** + * called after media db content has changed + */ + @Override + public void onLoadFinished(Loader loader, Cursor data) { + // to be restored after refreshLocal if there is no mInitialFilePath + if ((mInitialScrollPosition == NO_INITIAL_SCROLL_POSITION) && (mViewPager != null)) { + mInitialScrollPosition = mViewPager.getCurrentItem(); } + // do change the data + mUpdateId = FotoSql.getMediaDBApi().getCurrentUpdateId(); + mAdapter.swapCursor(data); + // currentUpdateId + + // restore position is invalid + final int newItemCount = mAdapter.getCount(); + + if (((newItemCount == 0)) || (mInitialScrollPosition >= newItemCount)) + mInitialScrollPosition = NO_INITIAL_SCROLL_POSITION; + + if (Global.debugEnabledSql) { + Log.i(Global.LOG_CONTEXT, mDebugPrefix + " onLoadFinished" + + getDebugContext() + + " found " + ((data == null) ? 0 : newItemCount) + " rows"); + } + + // do change the data + notifyPhotoChanged(); + mViewPager.setAdapter(mAdapter); + + // show the changes + onLoadCompleted(); } - } - /** #70: adds/removes/replases contextColumnExpression */ - private static void addContextColumn(QueryParameter query, String contextColumnExpression) { - if (query != null) { - query.removeFirstColumnThatContains(CONTEXT_COLUMN_ALIAS); - if ((contextColumnExpression != null) && (contextColumnExpression.trim().length() > 0)) { - query.addColumn(contextColumnExpression + CONTEXT_COLUMN_ALIAS); + /** + * called by LoaderManager. after search criteria were changed or if activity is destroyed. + */ + @Override + public void onLoaderReset(Loader loader) { + // rember position where we have to scroll to after refreshLocal is finished. + mInitialScrollPosition = mViewPager.getCurrentItem(); + mAdapter.swapCursor(null); + if (Global.debugEnabledSql) { + Log.i(Global.LOG_CONTEXT, mDebugPrefix + " onLoaderReset" + + getDebugContext()); } + notifyPhotoChanged(); + } + + @NonNull + private String getDebugContext() { + return "(#" + mRequeryInstanceCount + + ", mScrollPosition=" + mInitialScrollPosition + + ", Path='" + mInitialFilePath + + "')"; } } } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/imagedetail/ImageDetailMetaDialogBuilder.java b/app/src/main/java/de/k3b/android/androFotoFinder/imagedetail/ImageDetailMetaDialogBuilder.java index c6e1c239..13e7fe8f 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/imagedetail/ImageDetailMetaDialogBuilder.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/imagedetail/ImageDetailMetaDialogBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2018 by k3b. + * Copyright (c) 2015-2020 by k3b. * * This file is part of AndroFotoFinder. * @@ -33,13 +33,14 @@ import java.util.Date; import java.util.List; +import de.k3b.android.androFotoFinder.queries.MediaContentproviderRepositoryImpl; import de.k3b.android.androFotoFinder.tagDB.TagSql; import de.k3b.android.widget.ActivityWithCallContext; +import de.k3b.database.QueryParameter; import de.k3b.io.DateUtil; +import de.k3b.io.FileCommands; import de.k3b.media.ExifInterfaceEx; -import de.k3b.database.QueryParameter; import de.k3b.media.PhotoPropertiesImageReader; -import de.k3b.io.FileCommands; import de.k3b.media.XmpSegment; /** @@ -137,7 +138,7 @@ private static void appendExifInfo(StringBuilder result, Activity context, Strin if (currentImageId != 0) { - ContentValues dbContent = TagSql.getDbContent(context, currentImageId); + ContentValues dbContent = MediaContentproviderRepositoryImpl.getDbContent(context, currentImageId); if (dbContent != null) { result.append(NL).append(line).append(NL); result.append(NL).append(TagSql.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE).append(NL).append(NL); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/imagedetail/ImagePagerAdapterFromCursor.java b/app/src/main/java/de/k3b/android/androFotoFinder/imagedetail/ImagePagerAdapterFromCursor.java index a26e5276..4b42854c 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/imagedetail/ImagePagerAdapterFromCursor.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/imagedetail/ImagePagerAdapterFromCursor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2019 by k3b. + * Copyright (c) 2015-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -49,6 +49,7 @@ import de.k3b.android.util.DBUtils; import de.k3b.android.util.GarbageCollector; import de.k3b.android.util.MenuUtils; +import de.k3b.android.util.PhotoChangeNotifyer; import de.k3b.android.util.ResourceUtils; import de.k3b.media.PhotoPropertiesBulkUpdateService; @@ -58,7 +59,7 @@ * Translates between position in ViewPager and content page content with image * Created by k3b on 04.07.2015. */ -public class ImagePagerAdapterFromCursor extends PagerAdapter { +public class ImagePagerAdapterFromCursor extends PagerAdapter implements PhotoChangeNotifyer.PhotoChangedListener { private static final int MAX_IMAGE_DIMENSION = HugeImageLoader.getMaxTextureSize(); /** colum alias for optinal sql expression to show ContextDetails */ @@ -463,4 +464,12 @@ public int getMenuId() { public void setContext(MenuItem context) { if (mImageButtonController != null) mImageButtonController.setContext(context); } + + /** + * PhotoChangeNotifyer.PhotoChangedListener + **/ + @Override + public void onNotifyPhotoChanged() { + notifyDataSetChanged(); + } } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/LocationMapFragment.java b/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/LocationMapFragment.java index 75d9954d..d7af20f1 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/LocationMapFragment.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/LocationMapFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2018 by k3b. + * Copyright (c) 2015-2019 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -1120,7 +1120,7 @@ private boolean zoomToFit(IGeoPoint geoCenterPoint, Object... dbgContext) { QueryParameter baseQuery = getQueryForPositionRectangle(geoCenterPoint); BoundingBox boundingBox = null; - IGeoRectangle fittingRectangle = FotoSql.execGetGeoRectangle(null, this.getActivity(), + IGeoRectangle fittingRectangle = FotoSql.execGetGeoRectangle(null, baseQuery, null, mDebugPrefix, "zoomToFit", dbgContext); double delta = getDelta(fittingRectangle); if ((geoCenterPoint != null) && (delta < 1e-6)) { @@ -1194,7 +1194,7 @@ private double getMarkerDelta() { private IGeoPoint getGeoPointById(int markerId, IGeoPoint notFoundValue, Object... dbgContext) { if (markerId != NO_MARKER_ID) { - IGeoPoint pos = FotoSql.execGetPosition(null, this.getActivity(), + IGeoPoint pos = FotoSql.execGetPosition(null, null, markerId, mDebugPrefix, "getGeoPointById", dbgContext); if (pos != null) { return pos; diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/MapGeoPickerActivity.java b/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/MapGeoPickerActivity.java index 72c81766..4e3d7d0a 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/MapGeoPickerActivity.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/MapGeoPickerActivity.java @@ -94,7 +94,7 @@ public static void showActivity(String debugContext, Activity context, SelectedF } if (AffUtils.putSelectedFiles(intent, selectedItems)) { - IGeoPoint initialPoint = FotoSql.execGetPosition(null, context, + IGeoPoint initialPoint = FotoSql.execGetPosition(null, null, selectedItems.getId(0), context, mDebugPrefix, "showActivity"); if (initialPoint != null) { GeoUri PARSER = new GeoUri(GeoUri.OPT_PARSE_INFER_MISSING); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/MarkerLoaderTask.java b/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/MarkerLoaderTask.java index b450f46b..8e8ba0c7 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/MarkerLoaderTask.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/MarkerLoaderTask.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2017 by k3b. + * Copyright (c) 2015-2019 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -22,7 +22,6 @@ import android.app.Activity; import android.database.Cursor; import android.graphics.drawable.BitmapDrawable; -import android.net.Uri; import android.os.AsyncTask; import android.util.Log; @@ -35,8 +34,8 @@ import de.k3b.android.androFotoFinder.Global; import de.k3b.android.androFotoFinder.R; import de.k3b.android.androFotoFinder.queries.FotoSql; -import de.k3b.android.osmdroid.IconFactory; import de.k3b.android.osmdroid.ClickableIconOverlay; +import de.k3b.android.osmdroid.IconFactory; import de.k3b.android.util.ResourceUtils; import de.k3b.database.QueryParameter; @@ -96,8 +95,9 @@ protected OverlayManager doInBackground(QueryParameter... queryParameter) { Cursor cursor = null; try { - cursor = mContext.getContentResolver().query(Uri.parse(queryParameters.toFrom()), queryParameters.toColumns(), - queryParameters.toAndroidWhere(), queryParameters.toAndroidParameters(), queryParameters.toOrderBy()); + cursor = FotoSql.getMediaDBApi().createCursorForQuery( + null, "MakerLoader", + queryParameters, null, null); int itemCount = cursor.getCount(); final int expectedCount = itemCount + itemCount; diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/PickerLocationMapFragment.java b/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/PickerLocationMapFragment.java index fbb6c292..53b4bb73 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/PickerLocationMapFragment.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/locationmap/PickerLocationMapFragment.java @@ -170,7 +170,7 @@ protected void onOk() { Activity activity = getActivity(); if (mMarkerId != NO_MARKER_ID) { - result = FotoSql.execGetPosition(null, activity, null, mMarkerId, + result = FotoSql.execGetPosition(null, null, mMarkerId, mDebugPrefix,"onOk"); } 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 ed4ecff4..4fd1bf49 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 @@ -19,12 +19,12 @@ package de.k3b.android.androFotoFinder.media; +import android.app.Activity; import android.content.ContentValues; import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import android.app.Activity; import android.util.Log; import android.view.View; import android.widget.TextView; @@ -206,7 +206,7 @@ private void updateDB(String dbgContext, String _path, long xmlLastFileModifyDat TagSql.setFileModifyDate(dbValues, new Date().getTime() / 1000); - mUpdateCount += TagSql.execUpdate(dbgContext, PhotoPropertiesMediaDBCsvImportActivity.this, path, xmlLastFileModifyDate, dbValues, VISIBILITY.PRIVATE_PUBLIC); + mUpdateCount += TagSql.execUpdate(dbgContext, path, xmlLastFileModifyDate, dbValues, VISIBILITY.PRIVATE_PUBLIC); mItemCount++; } } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/AndroidAlbumUtils.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/AndroidAlbumUtils.java index 5a4e38f6..56f8b5c8 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/queries/AndroidAlbumUtils.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/AndroidAlbumUtils.java @@ -155,7 +155,7 @@ public static QueryParameter getQueryFromUriOrNull( QueryParameter query = QueryParameter.load(context.getContentResolver().openInputStream(uri)); if (query != null) { - Map found = FotoSql.execGetPathIdMap(context, path); + Map found = FotoSql.execGetPathIdMap(path); if ((found == null) || (found.size() == 0)) { AndroidAlbumUtils.albumMediaScan(dbgContext + " not found mediadb => ", context, new File(path), 1); } @@ -368,7 +368,7 @@ public static void insertToMediaDB(String dbgContext, @NonNull Context context, ContentValues values = new ContentValues(); String newAbsolutePath = PhotoPropertiesMediaFilesScanner.setFileFields(values, fileToBeScannedAndInserted); values.put(FotoSql.SQL_COL_EXT_MEDIA_TYPE, FotoSql.MEDIA_TYPE_ALBUM_FILE); - FotoSql.insertOrUpdateMediaDatabase(dbgContext, context, newAbsolutePath, values, null, 1l); + FotoSql.getMediaDBApi().insertOrUpdateMediaDatabase(dbgContext, newAbsolutePath, values, null, 1l); } } @@ -381,7 +381,7 @@ public static int albumMediaScan(String dbgContext, Context context, File _root, int result = 0; if (root != null) { List currentFiles = AlbumFile.getFilePaths(null, root, subDirLevels); - List databaseFiles = FotoSql.getAlbumFiles(context, FileUtils.tryGetCanonicalPath(root, null), subDirLevels); + List databaseFiles = FotoSql.getAlbumFiles(FileUtils.tryGetCanonicalPath(root, null), subDirLevels); List added = new ArrayList(); List removed = new ArrayList(); @@ -392,7 +392,7 @@ public static int albumMediaScan(String dbgContext, Context context, File _root, result++; } - result += FotoSql.deleteMedia(dbgMessage + "delete-obsolete", context, removed, false); + result += FotoSql.deleteMedia(dbgMessage + "delete-obsolete", removed, false); } return result; } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/CursorLoaderWithException.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/CursorLoaderWithException.java new file mode 100644 index 00000000..8e114514 --- /dev/null +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/CursorLoaderWithException.java @@ -0,0 +1,189 @@ +package de.k3b.android.androFotoFinder.queries; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.database.Cursor; +import android.os.Build; +import android.os.CancellationSignal; +import android.os.OperationCanceledException; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +import de.k3b.android.androFotoFinder.Global; +import de.k3b.database.QueryParameter; + +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Copied from android.content.CursorLoaderWithException + */ +public class CursorLoaderWithException extends AsyncTaskLoader { + final ForceLoadContentObserver mObserver; + + private final QueryParameter query; + Cursor mCursor; + CancellationSignal mCancellationSignal; + private Exception mException; + + public CursorLoaderWithException(Context context, QueryParameter query) { + super(context); + this.query = query; + mObserver = new ForceLoadContentObserver(); + } + + /* Runs on a worker thread */ + @Override + public Cursor loadInBackground() { + mException = null; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + synchronized (this) { + if (isLoadInBackgroundCanceled()) { + throw new OperationCanceledException(); + } + mCancellationSignal = new CancellationSignal(); + } + } + try { + Cursor cursor; + + cursor = FotoSql.getMediaDBApi().createCursorForQuery(null, "loader", this.query, null, mCancellationSignal); + if (cursor != null) { + try { + // Ensure the cursor window is filled. + cursor.getCount(); + cursor.registerContentObserver(mObserver); + } catch (RuntimeException ex) { + cursor.close(); + throw ex; + } + } + return cursor; + } catch (Exception ex) { + final String msg = "FotoSql.createCursorLoader()#loadInBackground failed:\n\t" + query.toSqlString(); + Log.e(Global.LOG_CONTEXT, msg, ex); + mException = ex; + return null; + } finally { + synchronized (this) { + mCancellationSignal = null; + } + } + } + + @Override + public void cancelLoadInBackground() { + super.cancelLoadInBackground(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + synchronized (this) { + if (mCancellationSignal != null) { + mCancellationSignal.cancel(); + } + } + } + } + + /* Runs on the UI thread */ + @Override + public void deliverResult(Cursor cursor) { + if (isReset()) { + // An async query came in while the loader is stopped + if (cursor != null) { + cursor.close(); + } + return; + } + Cursor oldCursor = mCursor; + mCursor = cursor; + + if (isStarted()) { + super.deliverResult(cursor); + } + + if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { + oldCursor.close(); + } + } + + /** + * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks + * will be called on the UI thread. If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. + *

+ * Must be called from the UI thread + */ + @Override + protected void onStartLoading() { + if (mCursor != null) { + deliverResult(mCursor); + } + if (takeContentChanged() || mCursor == null) { + forceLoad(); + } + } + + /** + * Must be called from the UI thread + */ + @Override + protected void onStopLoading() { + // Attempt to cancel the current load task if possible. + cancelLoad(); + } + + @Override + public void onCanceled(Cursor cursor) { + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } + + @Override + protected void onReset() { + super.onReset(); + + // Ensure the loader is stopped + onStopLoading(); + + if (mCursor != null && !mCursor.isClosed()) { + mCursor.close(); + } + mCursor = null; + } + + public QueryParameter getQuery() { + return query; + } + + public Exception getException() { + return mException; + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + super.dump(prefix, fd, writer, args); + writer.print(prefix); + writer.print("query="); + writer.println(this.query.toSqlString()); + writer.print(prefix); + writer.print("mCursor="); + writer.println(mCursor); + } +} 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 e03b8a9e..62daffd6 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 by k3b. + * Copyright (c) 2017-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -19,7 +19,6 @@ package de.k3b.android.androFotoFinder.queries; -import android.app.Activity; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; @@ -37,13 +36,29 @@ */ public class DatabaseHelper extends SQLiteOpenHelper { public static final int DATABASE_VERSION_1_TransactionLog = 1; + public static final int DATABASE_VERSION_2_MEDIA_DB_COPY = 2; - public static final int DATABASE_VERSION = DatabaseHelper.DATABASE_VERSION_1_TransactionLog; + public static final int DATABASE_VERSION = DatabaseHelper.DATABASE_VERSION_2_MEDIA_DB_COPY; + + private static DatabaseHelper instance = null; public DatabaseHelper(final Context context, final String databaseName) { super(context, databaseName, null, DatabaseHelper.DATABASE_VERSION); } + public static SQLiteDatabase getWritableDatabase(Context context) { + if (instance == null) { + instance = new DatabaseHelper(new DatabaseContext(context), "APhotoManager"); + } + return instance.getWritableDatabase(); + } + + public static void version2Upgrade_RecreateMediDbCopy(final SQLiteDatabase db) { + for (String sql : MediaDBRepository.Impl.DDL) { + db.execSQL(sql); + } + } + /** * called if database doesn-t exist yet */ @@ -51,7 +66,7 @@ public DatabaseHelper(final Context context, final String databaseName) { public void onCreate(final SQLiteDatabase db) { db.execSQL(TransactionLogSql.CREATE_TABLE); - this.version3Upgrade_TIMESLICE_WITH_NOTES(db); + this.version2Upgrade_RecreateMediDbCopy(db); } @Override @@ -59,24 +74,8 @@ public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { Log.w(this.getClass().toString(), "Upgrading database from version " + oldVersion + " to " + newVersion + ". (Old data is kept.)"); - if (oldVersion < DatabaseHelper.DATABASE_VERSION_1_TransactionLog) { - this.version3Upgrade_TIMESLICE_WITH_NOTES(db); + if (oldVersion < DatabaseHelper.DATABASE_VERSION_2_MEDIA_DB_COPY) { + this.version2Upgrade_RecreateMediDbCopy(db); } } - - private void version3Upgrade_TIMESLICE_WITH_NOTES(final SQLiteDatabase db) { - // added timeslice.notes - /* - db.execSQL("ALTER TABLE " + TransactionLogSql.TABLE - + " ADD COLUMN " + TransactionLogSql.COL_NOTES + " TEXT"); - */ - } - - private static DatabaseHelper instance = null; - public static SQLiteDatabase getWritableDatabase(Context context) { - if (instance == null) { - instance = new DatabaseHelper(new DatabaseContext(context), "APhotoManager"); - } - return instance.getWritableDatabase(); - } } 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 54965cd9..00d0bb90 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2019 by k3b. + * Copyright (c) 2015-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -19,12 +19,8 @@ package de.k3b.android.androFotoFinder.queries; -import android.app.Activity; -import android.content.ContentProviderOperation; -import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; -import android.content.CursorLoader; import android.database.Cursor; import android.net.Uri; import android.os.Build; @@ -66,6 +62,7 @@ * Created by k3b on 04.06.2015. */ public class FotoSql extends FotoSqlBase { + public static final String LOG_TAG = Global.LOG_CONTEXT + "-sql"; public static final int SORT_BY_DATE_OLD = 1; public static final int SORT_BY_NAME_OLD = 2; @@ -108,6 +105,7 @@ public class FotoSql extends FotoSqlBase { public static final String SQL_COL_DISPLAY_TEXT = "disp_txt"; public static final String SQL_COL_LAT = MediaStore.Images.Media.LATITUDE; public static final String SQL_COL_LON = MediaStore.Images.Media.LONGITUDE; + public static final String SQL_COL_EXT_TITLE = MediaStore.Images.Media.TITLE; // new col id for with since ver 0.6.3 public static final String SQL_COL_WIDTH = "col_width"; @@ -133,8 +131,11 @@ public class FotoSql extends FotoSqlBase { // either code 0..8 or rotation angle 0, 90, 180, 270 public static final String SQL_COL_ORIENTATION = MediaStore.Images.ImageColumns.ORIENTATION; + public static final String SQL_COL__IMPL_DISPLAY_NAME = MediaStore.Images.Media.DISPLAY_NAME; + // only works with api >= 16 public static final String SQL_COL_MAX_WITH = + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "max(" + MediaStore.Images.Media.WIDTH + "," + MediaStore.Images.Media.HEIGHT +")" @@ -152,7 +153,7 @@ public class FotoSql extends FotoSqlBase { protected static final String FILTER_EXPR_PRIVATE = "(" + SQL_COL_EXT_MEDIA_TYPE + " = " + MEDIA_TYPE_IMAGE_PRIVATE + ")"; - protected static final String FILTER_EXPR_PRIVATE_PUBLIC + public static final String FILTER_EXPR_PRIVATE_PUBLIC = "(" + SQL_COL_EXT_MEDIA_TYPE + " in (" + MEDIA_TYPE_IMAGE_PRIVATE + "," + MEDIA_TYPE_IMAGE +"))"; protected static final String FILTER_EXPR_PUBLIC = "(" + SQL_COL_EXT_MEDIA_TYPE + " = " + MEDIA_TYPE_IMAGE + ")"; @@ -204,7 +205,7 @@ public class FotoSql extends FotoSqlBase { .addGroupBy(SQL_EXPR_DAY_MODIFIED) .addOrderBy(SQL_EXPR_DAY_MODIFIED); - public static final String SQL_EXPR_FOLDER = "substr(" + SQL_COL_PATH + ",1,length(" + SQL_COL_PATH + ") - length(" + MediaStore.Images.Media.DISPLAY_NAME + "))"; + public static final String SQL_EXPR_FOLDER = "substr(" + SQL_COL_PATH + ",1,length(" + SQL_COL_PATH + ") - length(" + SQL_COL__IMPL_DISPLAY_NAME + "))"; public static final QueryParameter queryGroupByDir = new QueryParameter() .setID(QUERY_TYPE_GROUP_ALBUM) .addColumn( @@ -266,7 +267,7 @@ public class FotoSql extends FotoSqlBase { /** to avoid cascade delete of linked file when mediaDB-item is deleted * the links are first set to null before delete. */ - private static final String DELETED_FILE_MARKER = null; + public static final String DELETED_FILE_MARKER = null; /** * translate from bytes to kilobytes @@ -278,6 +279,16 @@ public class FotoSql extends FotoSqlBase { */ private static final int SIZE_TRANLATION_LIMIT = SIZE_K * 10; + private static IMediaRepositoryApi mediaDBApi; + + public static IMediaRepositoryApi getMediaDBApi() { + return FotoSql.mediaDBApi; + } + + public static void setMediaDBApi(IMediaRepositoryApi mediaDBApi) { + FotoSql.mediaDBApi = mediaDBApi; + } + public static final double getGroupFactor(final double _zoomLevel) { double zoomLevel = _zoomLevel; double result = GROUPFACTOR_FOR_Z0; @@ -288,7 +299,7 @@ public static final double getGroupFactor(final double _zoomLevel) { } if (Global.debugEnabled) { - Log.e(Global.LOG_CONTEXT, "FotoSql.getGroupFactor(" + _zoomLevel + ") => " + result); + Log.e(FotoSql.LOG_TAG, "FotoSql.getGroupFactor(" + _zoomLevel + ") => " + result); } return result; @@ -630,7 +641,7 @@ public static QueryParameter getQuery(int queryID) { case QUERY_TYPE_GROUP_MOVE: return null; default: - Log.e(Global.LOG_CONTEXT, "FotoSql.getQuery(" + queryID + "): unknown ID"); + Log.e(FotoSql.LOG_TAG, "FotoSql.getQuery(" + queryID + "): unknown ID"); return null; } } @@ -714,7 +725,7 @@ public static QueryParameter setSort(QueryParameter result, int sortID, boolean case SORT_BY_LOCATION_OLD: case SORT_BY_LOCATION: - return result.replaceOrderBy(SQL_COL_GPS + asc, MediaStore.Images.Media.LATITUDE + asc); + return result.replaceOrderBy(SQL_COL_GPS + asc, SQL_COL_LAT + asc); case SORT_BY_NAME_LEN_OLD: case SORT_BY_NAME_LEN: return result.replaceOrderBy("length(" + SQL_COL_PATH + ")" + asc, SQL_COL_PATH + asc); @@ -755,21 +766,27 @@ public static boolean set(GalleryFilterParameter dest, String selectedAbsolutePa return true; default:break; } - Log.e(Global.LOG_CONTEXT, "FotoSql.setFilter(queryTypeId = " + queryTypeId + ") : unknown type"); + Log.e(FotoSql.LOG_TAG, "FotoSql.setFilter(queryTypeId = " + queryTypeId + ") : unknown type"); return false; } /** converts content-Uri-with-id to full path */ - public static String execGetFotoPath(Context context, Uri uriWithID) { + public static String execGetFotoPath(Uri uriWithID) { Cursor c = null; try { - c = createCursorForQuery(null, "execGetFotoPath(uri)", context, uriWithID.toString(), null, null, null, FotoSql.SQL_COL_PATH); + c = mediaDBApi.createCursorForQuery( + null, + "execGetFotoPath(uri)", + uriWithID.toString(), + null, + null, null, + null, FotoSql.SQL_COL_PATH); if (c.moveToFirst()) { return DBUtils.getString(c,FotoSql.SQL_COL_PATH, null); } } catch (Exception ex) { - Log.e(Global.LOG_CONTEXT, "FotoSql.execGetFotoPath() Cannot get path from " + uriWithID, ex); + Log.e(FotoSql.LOG_TAG, "FotoSql.execGetFotoPath() Cannot get path from " + uriWithID, ex); } finally { if (c != null) c.close(); } @@ -777,67 +794,36 @@ public static String execGetFotoPath(Context context, Uri uriWithID) { } /** search for all full-image-file-paths that matches pathfilter */ - public static List execGetFotoPaths(Context context, String pathFilter) { + public static List execGetFotoPaths(String pathFilter) { ArrayList result = new ArrayList(); Cursor c = null; try { - c = createCursorForQuery(null, "execGetFotoPaths(pathFilter)", context,SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, - FotoSql.SQL_COL_PATH + " like ? and " + FILTER_EXPR_PRIVATE_PUBLIC, - new String[]{pathFilter}, FotoSql.SQL_COL_PATH, FotoSql.SQL_COL_PATH); + QueryParameter query = new QueryParameter() + .addFrom(SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME) + .addWhere(FotoSql.SQL_COL_PATH + " like ? and " + FILTER_EXPR_PRIVATE_PUBLIC, pathFilter) + .addColumn(FotoSql.SQL_COL_PATH) + .addOrderBy(FotoSql.SQL_COL_PATH); + c = mediaDBApi.createCursorForQuery( + null, + "execGetFotoPaths(pathFilter)", + query, null, null); while (c.moveToNext()) { result.add(c.getString(0)); } } catch (Exception ex) { - Log.e(Global.LOG_CONTEXT, "FotoSql.execGetFotoPaths() Cannot get path from: " + FotoSql.SQL_COL_PATH + " like '" + pathFilter +"'", ex); + Log.e(FotoSql.LOG_TAG, "FotoSql.execGetFotoPaths() Cannot get path from: " + FotoSql.SQL_COL_PATH + " like '" + pathFilter + "'", ex); } finally { if (c != null) c.close(); } if (Global.debugEnabled) { - Log.d(Global.LOG_CONTEXT, "FotoSql.execGetFotoPaths() result count=" + result.size()); + Log.d(FotoSql.LOG_TAG, "FotoSql.execGetFotoPaths() result count=" + result.size()); } return result; } - public static Cursor createCursorForQuery( - StringBuilder out_debugMessage, String dbgContext, final Context context, - QueryParameter parameters, VISIBILITY visibility) { - if (visibility != null) setWhereVisibility(parameters, visibility); - return createCursorForQuery(out_debugMessage, dbgContext, context, parameters.toFrom(), parameters.toAndroidWhere(), - parameters.toAndroidParameters(), parameters.toOrderBy(), - parameters.toColumns() - ); - } - - /** every cursor query should go through this. adds logging if enabled */ - private static Cursor createCursorForQuery(StringBuilder out_debugMessage, String dbgContext, final Context context, final String from, final String sqlWhereStatement, - final String[] sqlWhereParameters, final String sqlSortOrder, - final String... sqlSelectColums) { - ContentResolver resolver = context.getContentResolver(); - Cursor query = null; - - Exception excpetion = null; - try { - query = resolver.query(Uri.parse(from), sqlSelectColums, sqlWhereStatement, sqlWhereParameters, sqlSortOrder); - } catch (Exception ex) { - excpetion = ex; - } finally { - if ((excpetion != null) || Global.debugEnabledSql || (out_debugMessage != null)) { - StringBuilder message = StringUtils.appendMessage(out_debugMessage, excpetion, - dbgContext,"FotoSql.createCursorForQuery:\n" , - QueryParameter.toString(sqlSelectColums, null, from, sqlWhereStatement, - sqlWhereParameters, sqlSortOrder, query.getCount())); - if (out_debugMessage == null) { - Log.i(Global.LOG_CONTEXT, message.toString(), excpetion); - } // else logging is done by caller - } - } - - return query; - } - - public static IGeoRectangle execGetGeoRectangle(StringBuilder out_debugMessage, Context context, QueryParameter baseQuery, + public static IGeoRectangle execGetGeoRectangle(StringBuilder out_debugMessage, QueryParameter baseQuery, SelectedItems selectedItems, Object... dbgContext) { StringBuilder debugMessage = (out_debugMessage == null) ? StringUtils.createDebugMessage(Global.debugEnabledSql, dbgContext) @@ -866,7 +852,7 @@ public static IGeoRectangle execGetGeoRectangle(StringBuilder out_debugMessage, GeoRectangle result = null; Cursor c = null; try { - c = createCursorForQuery(debugMessage, "execGetGeoRectangle", context, query, VISIBILITY.PRIVATE_PUBLIC); + c = mediaDBApi.createCursorForQuery(debugMessage, "execGetGeoRectangle", query, VISIBILITY.PRIVATE_PUBLIC, null); if (c.moveToFirst()) { result = new GeoRectangle(); result.setLatitude(c.getDouble(0), c.getDouble(1)); @@ -875,13 +861,13 @@ public static IGeoRectangle execGetGeoRectangle(StringBuilder out_debugMessage, return result; } } catch (Exception ex) { - Log.e(Global.LOG_CONTEXT, "FotoSql.execGetGeoRectangle(): error executing " + query, ex); + Log.e(FotoSql.LOG_TAG, "FotoSql.execGetGeoRectangle(): error executing " + query, ex); } finally { if (c != null) c.close(); if (debugMessage != null) { StringUtils.appendMessage(debugMessage, "result", result); if (out_debugMessage == null) { - Log.i(Global.LOG_CONTEXT, debugMessage.toString()); + Log.i(FotoSql.LOG_TAG, debugMessage.toString()); } } } @@ -889,7 +875,7 @@ public static IGeoRectangle execGetGeoRectangle(StringBuilder out_debugMessage, } /** gets IGeoPoint either from file if fullPath is not null else from db via id */ - public static IGeoPoint execGetPosition(StringBuilder out_debugMessage, Context context, + public static IGeoPoint execGetPosition(StringBuilder out_debugMessage, String fullPath, long id, Object... dbgContext) { StringBuilder debugMessage = (out_debugMessage == null) ? StringUtils.createDebugMessage(Global.debugEnabledSql, dbgContext) : out_debugMessage; QueryParameter query = new QueryParameter() @@ -910,19 +896,19 @@ public static IGeoPoint execGetPosition(StringBuilder out_debugMessage, Context GeoPoint result = null; Cursor c = null; try { - c = createCursorForQuery(debugMessage, "execGetPosition", context, query, VISIBILITY.PRIVATE_PUBLIC); + c = mediaDBApi.createCursorForQuery(debugMessage, "execGetPosition", query, VISIBILITY.PRIVATE_PUBLIC, null); if (c.moveToFirst()) { result = new GeoPoint(c.getDouble(0),c.getDouble(1)); return result; } } catch (Exception ex) { - Log.e(Global.LOG_CONTEXT, "FotoSql.execGetPosition: error executing " + query, ex); + Log.e(FotoSql.LOG_TAG, "FotoSql.execGetPosition: error executing " + query, ex); } finally { if (c != null) c.close(); if (debugMessage != null) { StringUtils.appendMessage(debugMessage, "result", result); if (out_debugMessage == null) { - Log.i(Global.LOG_CONTEXT, debugMessage.toString()); + Log.i(FotoSql.LOG_TAG, debugMessage.toString()); } // else logging by caller } } @@ -932,7 +918,7 @@ public static IGeoPoint execGetPosition(StringBuilder out_debugMessage, Context /** * @return returns a hashmap filename => mediaID */ - public static Map execGetPathIdMap(Context context, String... fileNames) { + public static Map execGetPathIdMap(String... fileNames) { Map result = new HashMap(); String whereFileNames = getWhereInFileNames(fileNames); @@ -945,12 +931,12 @@ public static Map execGetPathIdMap(Context context, String... file Cursor c = null; try { - c = createCursorForQuery(null, "execGetPathIdMap", context, query, null); + c = mediaDBApi.createCursorForQuery(null, "execGetPathIdMap", query, null, null); while (c.moveToNext()) { result.put(c.getString(1),c.getLong(0)); } } catch (Exception ex) { - Log.e(Global.LOG_CONTEXT, "FotoSql.execGetPathIdMap: error executing " + query, ex); + Log.e(FotoSql.LOG_TAG, "FotoSql.execGetPathIdMap: error executing " + query, ex); } finally { if (c != null) c.close(); } @@ -979,20 +965,12 @@ public static String getWhereInFileNames(String... fileNames) { return null; } - public static int execUpdate(String dbgContext, Context context, long id, ContentValues values) { - return exexUpdateImpl(dbgContext, context, values, FILTER_COL_PK, new String[]{Long.toString(id)}); - } - - public static int execUpdate(String dbgContext, Context context, String path, ContentValues values, VISIBILITY visibility) { - return exexUpdateImpl(dbgContext, context, values, getFilterExprPathLikeWithVisibility(visibility), new String[]{path}); - } - /** * execRenameFolder(getActivity(),"/storage/sdcard0/testFolder/", "/storage/sdcard0/renamedFolder/") * "/storage/sdcard0/testFolder/image.jpg" becomes "/storage/sdcard0/renamedFolder/image.jpg" * @return number of updated items */ - public static int execRenameFolder(Context context, String pathOld, String pathNew) { + public static int execRenameFolder(String pathOld, String pathNew) { final String dbgContext = "FotoSql.execRenameFolder('" + pathOld + "' => '" + pathNew + "')"; // sql update file set path = newBegin + substing(path, begin+len) where path like newBegin+'%' @@ -1012,7 +990,7 @@ public static int execRenameFolder(Context context, String pathOld, String pathN .addWhere(SQL_COL_EXT_MEDIA_TYPE + " IS NOT NULL") ; - SelectedFiles selectedFiles= getSelectedfiles(context, queryAffectedFiles, sqlColNewPathAlias, null); + SelectedFiles selectedFiles = getSelectedfiles(queryAffectedFiles, sqlColNewPathAlias, null); String[] paths = selectedFiles.getFileNames(); Long[] ids = selectedFiles.getIds(); @@ -1023,91 +1001,13 @@ public static int execRenameFolder(Context context, String pathOld, String pathN for (int i = 0; i < ids.length; i++) { values.put(SQL_COL_PATH, paths[i]); selectionArgs[0] = ids[i].toString(); - if (exexUpdateImpl(_dbgContext, context, values, FILTER_COL_PK, selectionArgs) < 0) return -1; + if (mediaDBApi.exexUpdateImpl(_dbgContext, values, FILTER_COL_PK, selectionArgs) < 0) + return -1; _dbgContext = null; } return ids.length; } - /** - * execRenameFolder(getActivity(),"/storage/sdcard0/testFolder/", "/storage/sdcard0/renamedFolder/") - * "/storage/sdcard0/testFolder/image.jpg" becomes "/storage/sdcard0/renamedFolder/image.jpg" - * @return number of updated items - */ - private static int _del_execRenameFolder_batch_not_working(Context context, String pathOld, String pathNew) { - final String dbgContext = "FotoSql.execRenameFolder('" + - pathOld + "' => '" + pathNew + "')"; - // sql update file set path = newBegin + substing(path, begin+len) where path like newBegin+'%' - // public static final String SQL_EXPR_FOLDER = "substr(" + SQL_COL_PATH + ",1,length(" + SQL_COL_PATH + ") - length(" + MediaStore.Images.Media.DISPLAY_NAME + "))"; - - final String sqlColNewPathAlias = "new_path"; - final String sql_col_pathnew = "'" + pathNew + "' || substr(" + SQL_COL_PATH + - "," + (pathOld.length() + 1) + ",255) AS " + sqlColNewPathAlias; - - QueryParameter queryAffectedFiles = new QueryParameter() - .setID(QUERY_TYPE_DEFAULT) - .addColumn(SQL_COL_PK, - sql_col_pathnew) - .addFrom(SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME) - .addWhere(SQL_COL_PATH + " like '" + pathOld + "%'") - // SQL_COL_EXT_MEDIA_TYPE IS NOT NULL enshures that all media types (mp3, mp4, txt,...) are updated - .addWhere(SQL_COL_EXT_MEDIA_TYPE + " IS NOT NULL") - ; - - ArrayList ops = new ArrayList(); - - Cursor c = null; - try { - c = createCursorForQuery(null, dbgContext, context, queryAffectedFiles, null); - int pkColNo = c.getColumnIndex(FotoSql.SQL_COL_PK); - int pathColNo = c.getColumnIndex(sqlColNewPathAlias); - - while (c.moveToNext()) { - // paths[row] = c.getString(pathColNo); - // ids[row] = c.getLong(pkColNo); - ops.add(ContentProviderOperation.newUpdate(SQL_TABLE_EXTERNAL_CONTENT_URI_FILE) - .withSelection(FILTER_COL_PK, new String[]{c.getString(pkColNo)}) - .withValue(SQL_COL_PATH, c.getString(pathColNo)) - .build()); - } - } catch (Exception ex) { - Log.e(Global.LOG_CONTEXT, dbgContext + "-getAffected error :", ex); - return -1; - } finally { - if (c != null) c.close(); - } - - try { - context.getContentResolver().applyBatch(SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, ops); - } catch (Exception ex) { - // java.lang.IllegalArgumentException: Unknown authority content://media/external/file - // i assume not batch support for file - Log.e(Global.LOG_CONTEXT, dbgContext + "-updateAffected error :", ex); - return -1; - } - return ops.size(); - } - - /** every database update should go through this. adds logging if enabled */ - protected static int exexUpdateImpl(String dbgContext, Context context, ContentValues values, String sqlWhere, String[] selectionArgs) { - int result = -1; - Exception excpetion = null; - try { - result = context.getContentResolver().update(SQL_TABLE_EXTERNAL_CONTENT_URI_FILE, - values, sqlWhere, - selectionArgs); - } catch (Exception ex) { - excpetion = ex; - } finally { - if ((excpetion != null) || ((dbgContext != null) && (Global.debugEnabledSql || LibGlobal.debugEnabledJpg))) { - Log.i(Global.LOG_CONTEXT, dbgContext + ":FotoSql.exexUpdate " + excpetion + "\n" + - QueryParameter.toString(null, values.toString(), SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, - sqlWhere, selectionArgs, null, result), excpetion); - } - } - return result; - } - protected static String getFilterExprPathLikeWithVisibility(VISIBILITY visibility) { // visibility VISIBILITY.PRIVATE_PUBLIC String resultExpression = FotoSql.FILTER_EXPR_PATH_LIKE; @@ -1117,68 +1017,28 @@ protected static String getFilterExprPathLikeWithVisibility(VISIBILITY visibilit return resultExpression; } - /** return id of inserted item */ - public static Long insertOrUpdateMediaDatabase(String dbgContext, Context context, - String dbUpdateFilterJpgFullPathName, - ContentValues values, VISIBILITY visibility, - Long updateSuccessValue) { - Long result = updateSuccessValue; - - int modifyCount = FotoSql.execUpdate(dbgContext, context, dbUpdateFilterJpgFullPathName, - values, visibility); - - if (modifyCount == 0) { - // update failed (probably becauce oldFullPathName not found. try insert it. - FotoSql.addDateAdded(values); - - Uri uriWithId = FotoSql.execInsert(dbgContext, context, values); - result = getId(uriWithId); - } - return result; - } - - /** every database insert should go through this. adds logging if enabled */ - public static Uri execInsert(String dbgContext, Context context, ContentValues values) { - Uri providerUri = (null != values.get(SQL_COL_EXT_MEDIA_TYPE)) ? SQL_TABLE_EXTERNAL_CONTENT_URI_FILE : SQL_TABLE_EXTERNAL_CONTENT_URI; - - Uri result = null; - Exception excpetion = null; - try { - // on my android-4.4 insert with media_type=1001 (private) does insert with media_type=1 (image) - result = context.getContentResolver().insert(providerUri, values); - } catch (Exception ex) { - excpetion = ex; - } finally { - if ((excpetion != null) || Global.debugEnabledSql || LibGlobal.debugEnabledJpg) { - Log.i(Global.LOG_CONTEXT, dbgContext + ":FotoSql.execInsert " + excpetion + " " + - values.toString() + " => " + result + " " + excpetion, excpetion); - } - } - return result; - } - @NonNull - public static CursorLoader createCursorLoader(Context context, final QueryParameter query) { + public static CursorLoaderWithException createCursorLoader(Context context, final QueryParameter query) { FotoSql.setWhereVisibility(query, VISIBILITY.DEFAULT); - final CursorLoader loader = new CursorLoaderWithException(context, query); + final CursorLoaderWithException loader = new CursorLoaderWithException(context, query); return loader; } - public static int execDeleteByPath(String dbgContext, Activity context, String parentDirString, VISIBILITY visibility) { - int delCount = FotoSql.deleteMedia(dbgContext, context, getFilterExprPathLikeWithVisibility(visibility), new String[] {parentDirString + "/%"}, true); + public static int execDeleteByPath(String dbgContext, String parentDirString, VISIBILITY visibility) { + int delCount = mediaDBApi.deleteMedia(dbgContext, getFilterExprPathLikeWithVisibility(visibility), new String[]{parentDirString + "/%"}, true); return delCount; } - public static int deleteMedia(String dbgContext, Context context, List pathsToBeRemoved, + public static int deleteMedia(String dbgContext, List pathsToBeRemoved, boolean preventDeleteImageFile) { if ((pathsToBeRemoved != null) && (pathsToBeRemoved.size() > 0)) { String whereDelete = SQL_COL_PATH + " in ('" + ListUtils.toString("','", pathsToBeRemoved) + "')"; - return deleteMedia(dbgContext, context, whereDelete, null, preventDeleteImageFile); + return mediaDBApi.deleteMedia(dbgContext, whereDelete, null, preventDeleteImageFile); } return 0; } - public static int deleteMediaWithNullPath(Context context) { + public static int deleteMediaWithNullPath() { /// delete where SQL_COL_PATH + " is null" throws null pointer exception QueryParameter wherePathIsNull = new QueryParameter(); wherePathIsNull.addWhere(SQL_COL_PATH + " is null"); @@ -1186,63 +1046,16 @@ public static int deleteMediaWithNullPath(Context context) { // return deleteMedia("delete without path (_data = null)", context, wherePathIsNull.toAndroidWhere(), null, false); - SelectedFiles filesWitoutPath = getSelectedfiles(context, wherePathIsNull, FotoSql.SQL_COL_PATH, VISIBILITY.PRIVATE_PUBLIC); + SelectedFiles filesWitoutPath = getSelectedfiles(wherePathIsNull, FotoSql.SQL_COL_PATH, VISIBILITY.PRIVATE_PUBLIC); String pksAsString = filesWitoutPath.toIdString(); if ((pksAsString != null) && (pksAsString.length() > 0)) { QueryParameter whereInIds = new QueryParameter(); FotoSql.setWhereSelectionPks(whereInIds, pksAsString); - return deleteMedia("delete without path (_data = null)", context, whereInIds.toAndroidWhere(), null, true); + return mediaDBApi.deleteMedia("delete without path (_data = null)", whereInIds.toAndroidWhere(), null, true); } return 0; } - /** - * Deletes media items specified by where with the option to prevent cascade delete of the image. - */ - public static int deleteMedia(String dbgContext, Context context, String where, String[] selectionArgs, boolean preventDeleteImageFile) - { - String[] lastSelectionArgs = selectionArgs; - String lastUsedWhereClause = where; - int delCount = 0; - try { - if (preventDeleteImageFile) { - // set SQL_COL_PATH empty so sql-delete cannot cascade delete the referenced image-file via delete trigger - ContentValues values = new ContentValues(); - values.put(FotoSql.SQL_COL_PATH, DELETED_FILE_MARKER); - values.put(FotoSql.SQL_COL_EXT_MEDIA_TYPE, 0); // so it will not be shown as image any more - exexUpdateImpl(dbgContext + "-a: FotoSql.deleteMedia: ", - context, values, lastUsedWhereClause, lastSelectionArgs); - - lastUsedWhereClause = FotoSql.SQL_COL_PATH + " is null"; - lastSelectionArgs = null; - delCount = context.getContentResolver() - .delete(SQL_TABLE_EXTERNAL_CONTENT_URI_FILE, lastUsedWhereClause, lastSelectionArgs); - if (Global.debugEnabledSql || LibGlobal.debugEnabledJpg) { - Log.i(Global.LOG_CONTEXT, dbgContext + "-b: FotoSql.deleteMedia delete\n" + - QueryParameter.toString(null, null, SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, - lastUsedWhereClause, lastSelectionArgs, null, delCount)); - } - } else { - delCount = context.getContentResolver() - .delete(SQL_TABLE_EXTERNAL_CONTENT_URI_FILE, lastUsedWhereClause, lastSelectionArgs); - if (Global.debugEnabledSql || LibGlobal.debugEnabledJpg) { - Log.i(Global.LOG_CONTEXT, dbgContext +": FotoSql.deleteMedia\ndelete " + - QueryParameter.toString(null, null, - SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, - lastUsedWhereClause, lastSelectionArgs, null, delCount)); - } - } - } catch (Exception ex) { - // null pointer exception when delete matches not items?? - final String msg = dbgContext + ": Exception in FotoSql.deleteMedia:\n" + - QueryParameter.toString(null, null, SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, - lastUsedWhereClause, lastSelectionArgs, null, -1) - + " : " + ex.getMessage(); - Log.e(Global.LOG_CONTEXT, msg, ex); - - } - return delCount; - } /** converts imageID to content-uri */ public static Uri getUri(long imageID) { @@ -1258,7 +1071,7 @@ public static Long getId(Uri uriWithId) { try { imageID = (idString == null) ? null : Long.valueOf(idString); } catch (NumberFormatException e) { - Log.e(Global.LOG_CONTEXT, "FotoSql.getId(" + uriWithId + ") => " + e.getMessage()); + Log.e(FotoSql.LOG_TAG, "FotoSql.getId(" + uriWithId + ") => " + e.getMessage()); } } return imageID; @@ -1274,17 +1087,17 @@ public static String getUriString(long imageID) { return SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME + "/" + imageID; } - public static SelectedFiles getSelectedfiles(Context context, String sqlWhere, VISIBILITY visibility) { + public static SelectedFiles getSelectedfiles(String sqlWhere, VISIBILITY visibility) { QueryParameter query = new QueryParameter(FotoSql.queryChangePath); query.addWhere(sqlWhere); query.addOrderBy(FotoSql.SQL_COL_PATH); - return getSelectedfiles(context, query, FotoSql.SQL_COL_PATH, visibility); + return getSelectedfiles(query, FotoSql.SQL_COL_PATH, visibility); } @Nullable - public static String getMinFolder(Context context, QueryParameter query, + public static String getMinFolder(QueryParameter query, boolean removeLastModifiedFromFilter) { QueryParameter queryModified = new QueryParameter(query); queryModified.clearColumns().addColumn("min(" + SQL_EXPR_FOLDER + ")"); @@ -1297,12 +1110,12 @@ public static String getMinFolder(Context context, QueryParameter query, Cursor c = null; try { - c = FotoSql.createCursorForQuery(null, "getCount", context, queryModified, null); + c = mediaDBApi.createCursorForQuery(null, "getCount", queryModified, null, null); if (c.moveToNext()) { return c.getString(0); } } catch (Exception ex) { - Log.e(Global.LOG_CONTEXT, "FotoSql.getMinFolder() error :", ex); + Log.e(FotoSql.LOG_TAG, "FotoSql.getMinFolder() error :", ex); } finally { if (c != null) c.close(); } @@ -1310,18 +1123,18 @@ public static String getMinFolder(Context context, QueryParameter query, } @Nullable - public static long getCount(Context context, QueryParameter query) { + public static long getCount(QueryParameter query) { QueryParameter queryModified = new QueryParameter(query); queryModified.clearColumns().addColumn("count(*)"); Cursor c = null; try { - c = FotoSql.createCursorForQuery(null, "getCount", context, queryModified, null); + c = mediaDBApi.createCursorForQuery(null, "getCount", queryModified, null, null); if (c.moveToNext()) { return c.getLong(0); } } catch (Exception ex) { - Log.e(Global.LOG_CONTEXT, "FotoSql.getCount() error :", ex); + Log.e(FotoSql.LOG_TAG, "FotoSql.getCount() error :", ex); } finally { if (c != null) c.close(); } @@ -1345,7 +1158,7 @@ public static CharSequence getStatisticsMessage(Context context, int prefixStrin Cursor c = null; try { - c = FotoSql.createCursorForQuery(null, "getCount", context, queryModified, null); + c = mediaDBApi.createCursorForQuery(null, "getCount", queryModified, null, null); if (c.moveToNext()) { final long count = c.getLong(0); long size = c.getLong(1); @@ -1368,7 +1181,7 @@ public static CharSequence getStatisticsMessage(Context context, int prefixStrin ); } } catch (Exception ex) { - Log.e(Global.LOG_CONTEXT, "FotoSql.getStatisticsMessage() error :", ex); + Log.e(FotoSql.LOG_TAG, "FotoSql.getStatisticsMessage() error :", ex); } finally { if (c != null) c.close(); } @@ -1376,12 +1189,12 @@ public static CharSequence getStatisticsMessage(Context context, int prefixStrin } @Nullable - private static SelectedFiles getSelectedfiles(Context context, QueryParameter query, String colnameForPath, VISIBILITY visibility) { + private static SelectedFiles getSelectedfiles(QueryParameter query, String colnameForPath, VISIBILITY visibility) { SelectedFiles result = null; Cursor c = null; try { - c = FotoSql.createCursorForQuery(null, "getSelectedfiles", context, query, visibility); + c = mediaDBApi.createCursorForQuery(null, "getSelectedfiles", query, visibility, null); int len = c.getCount(); Long[] ids = new Long[len]; String[] paths = new String[len]; @@ -1396,13 +1209,13 @@ private static SelectedFiles getSelectedfiles(Context context, QueryParameter qu result = new SelectedFiles(paths, ids, null); } catch (Exception ex) { - Log.e(Global.LOG_CONTEXT, "FotoSql.getSelectedfiles() error :", ex); + Log.e(FotoSql.LOG_TAG, "FotoSql.getSelectedfiles() error :", ex); } finally { if (c != null) c.close(); } if (Global.debugEnabled) { - Log.d(Global.LOG_CONTEXT, "FotoSql.getSelectedfiles result count=" + ((result != null) ? result.size():0)); + Log.d(FotoSql.LOG_TAG, "FotoSql.getSelectedfiles result count=" + ((result != null) ? result.size() : 0)); } return result; @@ -1415,13 +1228,13 @@ public static Date getDate(Cursor cursor,int colDateTimeTaken) { } /** converts internal ID-list to string array of filenNames via media database. */ - public static List getFileNames(Context context, SelectedItems items, List ids, List paths, List datesPhotoTaken) { + public static List getFileNames(SelectedItems items, List ids, List paths, List datesPhotoTaken) { if (!items.isEmpty()) { // query ordered by DatePhotoTaken so that lower rename-numbers correspond to older images. QueryParameter parameters = new QueryParameter(queryAutoRename); setWhereSelectionPks(parameters, items); - List result = getFileNamesImpl(context, parameters, ids, paths, datesPhotoTaken); + List result = getFileNamesImpl(parameters, ids, paths, datesPhotoTaken); int size = result.size(); if (size > 0) { @@ -1433,13 +1246,13 @@ public static List getFileNames(Context context, SelectedItems items, Li } /** converts internal ID-list to string array of filenNames via media database. */ - public static String[] getFileNames(Context context, String pksAsListString , List ids, List paths, List datesPhotoTaken) { + public static String[] getFileNames(String pksAsListString, List ids, List paths, List datesPhotoTaken) { if ((pksAsListString != null) && !pksAsListString.isEmpty()) { // query ordered by DatePhotoTaken so that lower rename-numbers correspond to older images. QueryParameter parameters = new QueryParameter(queryAutoRename); setWhereSelectionPks(parameters, pksAsListString); - List result = getFileNamesImpl(context, parameters, ids, paths, datesPhotoTaken); + List result = getFileNamesImpl(parameters, ids, paths, datesPhotoTaken); int size = result.size(); if (size > 0) { @@ -1449,13 +1262,13 @@ public static String[] getFileNames(Context context, String pksAsListString , Li return null; } - private static List getFileNamesImpl(Context context, QueryParameter parameters, List ids, List paths, List datesPhotoTaken) { + private static List getFileNamesImpl(QueryParameter parameters, List ids, List paths, List datesPhotoTaken) { List result = (paths != null) ? paths : new ArrayList(); Cursor cursor = null; try { - cursor = createCursorForQuery(null, "getFileNames", context, parameters, VISIBILITY.PRIVATE_PUBLIC); + cursor = mediaDBApi.createCursorForQuery(null, "getFileNames", parameters, VISIBILITY.PRIVATE_PUBLIC, null); int colPath = cursor.getColumnIndex(SQL_COL_DISPLAY_TEXT); if (colPath == -1) colPath = cursor.getColumnIndex(SQL_COL_PATH); @@ -1510,8 +1323,8 @@ public static QueryParameter setWhereVisibility(QueryParameter parameters, VISIB return parameters; } - public static List getAlbumFiles(Context context, String path, int subDirLevels) { - SelectedFiles databaseFiles = FotoSql.getSelectedfiles(context, + public static List getAlbumFiles(String path, int subDirLevels) { + SelectedFiles databaseFiles = FotoSql.getSelectedfiles( SQL_COL_PATH +" like '" + path + "/%" + AlbumFile.SUFFIX_VALBUM + "' OR " + SQL_COL_PATH +" like '" + path + "/%" + AlbumFile.SUFFIX_QUERY + "'", null); String[] fileNames = (databaseFiles == null) ? null : databaseFiles.getFileNames(); @@ -1530,44 +1343,12 @@ public static List getAlbumFiles(Context context, String path, int subDi return paths; } - public static int execRename(Context context, String oldFullPath, String newFullPath) { + public static int execRename(String oldFullPath, String newFullPath) { ContentValues values = new ContentValues(); values.put(SQL_COL_PATH, newFullPath); - return FotoSql.execUpdate("rename file", context, oldFullPath, + return mediaDBApi.execUpdate("rename file", oldFullPath, values, null); } - - public static class CursorLoaderWithException extends CursorLoader { - private final QueryParameter query; - private Exception mException; - - public CursorLoaderWithException(Context context, QueryParameter query) { - super(context, Uri.parse(query.toFrom()), query.toColumns(), query.toAndroidWhere(), query.toAndroidParameters(), query.toOrderBy()); - this.query = query; - } - - @Override - public Cursor loadInBackground() { - mException = null; - try { - Cursor result = super.loadInBackground(); - return result; - } catch (Exception ex) { - final String msg = "FotoSql.createCursorLoader()#loadInBackground failed:\n\t" + query.toSqlString(); - Log.e(Global.LOG_CONTEXT, msg, ex); - mException = ex; - return null; - } - } - - public QueryParameter getQuery() { - return query; - } - - public Exception getException() { - return mException; - } - } } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/FotoThumbSql.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/FotoThumbSql.java index 137eb258..bef81bc2 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/queries/FotoThumbSql.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/FotoThumbSql.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2017 by k3b. + * Copyright (c) 2015-2019 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -66,7 +66,7 @@ private static String getStatistic(Context context, QueryParameter query, String Cursor c = null; try { - c = FotoSql.createCursorForQuery(null, mDebugPrefix + "getStatistic", context, query, VISIBILITY.PRIVATE_PUBLIC); + c = FotoSql.getMediaDBApi().createCursorForQuery(null, mDebugPrefix + "getStatistic", query, VISIBILITY.PRIVATE_PUBLIC, null); if (Global.debugEnabledSql) { Log.i(Global.LOG_CONTEXT, mDebugPrefix + "getStatistic " + c.getCount() + "\n\t" + query.toSqlString()); diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/GlobalMediaContentObserver.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/GlobalMediaContentObserver.java new file mode 100644 index 00000000..66685fba --- /dev/null +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/GlobalMediaContentObserver.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 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.androFotoFinder.queries; + +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.util.Log; + +import de.k3b.android.util.PhotoChangeNotifyer; + +/** + * collect notifications that media content has changed + */ +public class GlobalMediaContentObserver extends ContentObserver { + private static GlobalMediaContentObserver instance = null; + private static Handler delayedChangeNotifiyHandler = null; + private static Runnable delayedRunner = null; + private static Context appContext; + private static PhotoChangeNotifyer.PhotoChangedListener photoChangedListener = null; + + private GlobalMediaContentObserver() { + super(null); + } + + public static GlobalMediaContentObserver getInstance(final Context appContext) { + if (instance == null) { + GlobalMediaContentObserver.appContext = appContext; + + delayedRunner = new Runnable() { + public void run() { + onExternalDataChangeCompleted(appContext); + + } + }; + delayedChangeNotifiyHandler = new Handler(); + instance = new GlobalMediaContentObserver(); + } + return instance; + } + + /** + * called in gui thread after external media-content changes are completed + * + * @param appContext + */ + private static void onExternalDataChangeCompleted(Context appContext) { + Log.d(MediaDBRepository.LOG_TAG, "Media content changed "); + // todo fix database + + if (photoChangedListener != null) { + photoChangedListener.onNotifyPhotoChanged(); + } + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + if (!selfChange) { + Log.d(MediaDBRepository.LOG_TAG, "Media content changing " + uri); + + delayedChangeNotifiyHandler.removeCallbacks(delayedRunner); + delayedChangeNotifiyHandler.postDelayed(delayedRunner, 500); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/IMediaRepositoryApi.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/IMediaRepositoryApi.java new file mode 100644 index 00000000..b82e1d5f --- /dev/null +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/IMediaRepositoryApi.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-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.androFotoFinder.queries; + +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.CancellationSignal; + +import de.k3b.database.QueryParameter; +import de.k3b.io.VISIBILITY; + +/** + * RepositoryApi for media database access. + */ +public interface IMediaRepositoryApi { + Cursor createCursorForQuery( + StringBuilder out_debugMessage, String dbgContext, + QueryParameter parameters, VISIBILITY visibility, CancellationSignal cancellationSignal); + + Cursor createCursorForQuery(StringBuilder out_debugMessage, String dbgContext, final String from, final String sqlWhereStatement, + final String[] sqlWhereParameters, final String sqlSortOrder, + CancellationSignal cancellationSignal, final String... sqlSelectColums); + + int execUpdate(String dbgContext, long id, ContentValues values); + + int execUpdate(String dbgContext, String path, ContentValues values, VISIBILITY visibility); + + int exexUpdateImpl(String dbgContext, ContentValues values, String sqlWhere, String[] selectionArgs); + + /** + * return id of inserted item + */ + Long insertOrUpdateMediaDatabase(String dbgContext, + String dbUpdateFilterJpgFullPathName, + ContentValues values, VISIBILITY visibility, + Long updateSuccessValue); + + /** + * every database insert should go through this. adds logging if enabled + */ + Uri execInsert(String dbgContext, ContentValues values); + + /** + * Deletes media items specified by where with the option to prevent cascade delete of the image. + */ + int deleteMedia(String dbgContext, String where, String[] selectionArgs, boolean preventDeleteImageFile); + + ContentValues getDbContent(long id); + + long getCurrentUpdateId(); + + boolean mustRequery(long updateId); + + void beginTransaction(); + + void setTransactionSuccessful(); + + void endTransaction(); +} 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 new file mode 100644 index 00000000..0036b423 --- /dev/null +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaContent2DBUpdateService.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019-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.androFotoFinder.queries; + +import android.content.Context; +import android.database.ContentObserver; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.widget.Toast; + +import java.util.Date; + +import de.k3b.io.IProgessListener; + +/** + * #155: takes care that chages from + * {@link MediaContentproviderRepository} are transfered to {@link MediaDBRepository} + */ +public class MediaContent2DBUpdateService { + // called when image-/file-mediacontent has changed to indicate that data must + // be loaded from content-provider to content-copy + private static final ContentObserver mMediaObserverDirectory = new ContentObserver(null) { + + // ignore version with 3rd param: int userId + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + + } + }; + public static MediaContent2DBUpdateService instance = null; + private final Context context; + private final SQLiteDatabase writableDatabase; + + public MediaContent2DBUpdateService(Context context, SQLiteDatabase writableDatabase) { + this.context = context; + this.writableDatabase = writableDatabase; + } + + public void clearMediaCopy() { + DatabaseHelper.version2Upgrade_RecreateMediDbCopy(writableDatabase); + } + + public void rebuild(Context context, IProgessListener progessListener) { + long start = new Date().getTime(); + clearMediaCopy(); + MediaDBRepository.Impl.updateMedaiCopy(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(); + if (progessListener != null) progessListener.onProgress(0, 0, text); + } + + +} diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaContentproviderRepository.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaContentproviderRepository.java new file mode 100644 index 00000000..1d285d81 --- /dev/null +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaContentproviderRepository.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2019-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.androFotoFinder.queries; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.CancellationSignal; + +import de.k3b.database.QueryParameter; +import de.k3b.io.VISIBILITY; + +/** + * Access Media Data through Android media contentprovider. + *

+ * Implementation of Context.getContentResolver()-ContentProvider based media api + */ +public class MediaContentproviderRepository implements IMediaRepositoryApi { + private final Context context; + + public MediaContentproviderRepository(final Context context) { + this.context = context; + } + + @Override + public Cursor createCursorForQuery( + StringBuilder out_debugMessage, String dbgContext, + QueryParameter parameters, VISIBILITY visibility, CancellationSignal cancellationSignal) { + return MediaContentproviderRepositoryImpl.createCursorForQuery( + out_debugMessage, dbgContext, context, parameters, visibility, cancellationSignal); + } + + @Override + public Cursor createCursorForQuery(StringBuilder out_debugMessage, String dbgContext, final String from, final String sqlWhereStatement, + final String[] sqlWhereParameters, final String sqlSortOrder, + CancellationSignal cancellationSignal, final String... sqlSelectColums) { + return MediaContentproviderRepositoryImpl.createCursorForQuery( + out_debugMessage, dbgContext, context, from, sqlWhereStatement, + sqlWhereParameters, sqlSortOrder, null, sqlSelectColums); + } + + @Override + public int execUpdate(String dbgContext, long id, ContentValues values) { + return exexUpdateImpl(dbgContext, values, FotoSql.FILTER_COL_PK, new String[]{Long.toString(id)}); + } + + @Override + public int execUpdate(String dbgContext, String path, ContentValues values, VISIBILITY visibility) { + return exexUpdateImpl(dbgContext, values, FotoSql.getFilterExprPathLikeWithVisibility(visibility), new String[]{path}); + } + + @Override + public int exexUpdateImpl(String dbgContext, ContentValues values, String sqlWhere, String[] selectionArgs) { + return MediaContentproviderRepositoryImpl.exexUpdateImpl(dbgContext, context, values, sqlWhere, selectionArgs); + } + + /** + * return id of inserted item + */ + @Override + public Long insertOrUpdateMediaDatabase(String dbgContext, + String dbUpdateFilterJpgFullPathName, + ContentValues values, VISIBILITY visibility, + Long updateSuccessValue) { + return MediaContentproviderRepositoryImpl.insertOrUpdateMediaDatabase(dbgContext, context, + dbUpdateFilterJpgFullPathName, + values, visibility, + updateSuccessValue); + } + + /** + * every database insert should go through this. adds logging if enabled + */ + @Override + public Uri execInsert(String dbgContext, ContentValues values) { + return MediaContentproviderRepositoryImpl.execInsert(dbgContext, context, values); + } + + /** + * Deletes media items specified by where with the option to prevent cascade delete of the image. + */ + @Override + public int deleteMedia(String dbgContext, String where, String[] selectionArgs, boolean preventDeleteImageFile) { + return MediaContentproviderRepositoryImpl.deleteMedia(dbgContext, context, where, selectionArgs, preventDeleteImageFile); + } + + @Override + public ContentValues getDbContent(final long id) { + return MediaContentproviderRepositoryImpl.getDbContent(context, id); + } + + @Override + public long getCurrentUpdateId() { + return 0; + } + + @Override + public boolean mustRequery(long updateId) { + return false; + } + + @Override + public void beginTransaction() { + + } + + @Override + public void setTransactionSuccessful() { + + } + + @Override + public void endTransaction() { + + } + +} diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaContentproviderRepositoryImpl.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaContentproviderRepositoryImpl.java new file mode 100644 index 00000000..65716b03 --- /dev/null +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaContentproviderRepositoryImpl.java @@ -0,0 +1,302 @@ +/* + * 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.androFotoFinder.queries; + +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.net.Uri; +import android.os.Build; +import android.os.CancellationSignal; +import android.util.Log; + +import java.util.ArrayList; + +import de.k3b.LibGlobal; +import de.k3b.android.androFotoFinder.Global; +import de.k3b.database.QueryParameter; +import de.k3b.io.StringUtils; +import de.k3b.io.VISIBILITY; + +/** + * Static Implementation of Context.getContentResolver()-ContentProvider based media api + */ +public class MediaContentproviderRepositoryImpl { + public static final String LOG_TAG = FotoSql.LOG_TAG + "Content"; + private static final String MODUL_NAME = MediaContentproviderRepositoryImpl.class.getName(); + + public static Cursor createCursorForQuery( + StringBuilder out_debugMessage, String dbgContext, final Context context, + QueryParameter parameters, VISIBILITY visibility, CancellationSignal cancellationSignal) { + if (visibility != null) FotoSql.setWhereVisibility(parameters, visibility); + return createCursorForQuery(out_debugMessage, dbgContext, context, parameters.toFrom(), + parameters.toAndroidWhere(), + parameters.toAndroidParameters(), parameters.toOrderBy(), + cancellationSignal, parameters.toColumns() + ); + } + + /** + * every cursor query should go through this. adds logging if enabled + */ + static Cursor createCursorForQuery( + StringBuilder out_debugMessage, String dbgContext, final Context context, + final String from, final String sqlWhereStatement, + final String[] sqlWhereParameters, final String sqlSortOrder, + CancellationSignal cancellationSignal, final String... sqlSelectColums) { + ContentResolver resolver = context.getContentResolver(); + Cursor query = null; + + Exception excpetion = null; + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + query = resolver.query(Uri.parse(from), sqlSelectColums, sqlWhereStatement, sqlWhereParameters, sqlSortOrder, cancellationSignal); + } else { + query = resolver.query(Uri.parse(from), sqlSelectColums, sqlWhereStatement, sqlWhereParameters, sqlSortOrder); + } + } catch (Exception ex) { + excpetion = ex; + } finally { + if ((excpetion != null) || Global.debugEnabledSql || (out_debugMessage != null)) { + StringBuilder message = StringUtils.appendMessage(out_debugMessage, excpetion, + dbgContext, MODUL_NAME + + ".createCursorForQuery:\n", + QueryParameter.toString(sqlSelectColums, null, from, sqlWhereStatement, + sqlWhereParameters, sqlSortOrder, query.getCount())); + if (out_debugMessage == null) { + Log.i(LOG_TAG, message.toString(), excpetion); + } // else logging is done by caller + } + } + + return query; + } + + public static int execUpdate(String dbgContext, Context context, String path, ContentValues values, VISIBILITY visibility) { + return exexUpdateImpl(dbgContext, context, values, FotoSql.getFilterExprPathLikeWithVisibility(visibility), new String[]{path}); + } + + /** + * every database update should go through this. adds logging if enabled + */ + public static int exexUpdateImpl(String dbgContext, Context context, ContentValues values, String sqlWhere, String[] selectionArgs) { + int result = -1; + Exception excpetion = null; + try { + result = context.getContentResolver().update(FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE, + values, sqlWhere, + selectionArgs); + } catch (Exception ex) { + excpetion = ex; + } finally { + if ((excpetion != null) || ((dbgContext != null) && (Global.debugEnabledSql || LibGlobal.debugEnabledJpg))) { + Log.i(LOG_TAG, dbgContext + ":" + + MODUL_NAME + + ".exexUpdate " + excpetion + "\n" + + QueryParameter.toString(null, values.toString(), FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, + sqlWhere, selectionArgs, null, result), excpetion); + } + } + return result; + } + + /** + * return id of inserted item + */ + public static Long insertOrUpdateMediaDatabase(String dbgContext, Context context, + String dbUpdateFilterJpgFullPathName, + ContentValues values, VISIBILITY visibility, + Long updateSuccessValue) { + Long result = updateSuccessValue; + + int modifyCount = execUpdate(dbgContext, context, dbUpdateFilterJpgFullPathName, + values, visibility); + + if (modifyCount == 0) { + // update failed (probably becauce oldFullPathName not found. try insert it. + FotoSql.addDateAdded(values); + + Uri uriWithId = execInsert(dbgContext, context, values); + result = FotoSql.getId(uriWithId); + } + return result; + } + + /** + * every database insert should go through this. adds logging if enabled + */ + public static Uri execInsert(String dbgContext, Context context, ContentValues values) { + Uri providerUri = (null != values.get(FotoSql.SQL_COL_EXT_MEDIA_TYPE)) ? FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE : FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI; + + Uri result = null; + Exception excpetion = null; + try { + // on my android-4.4 insert with media_type=1001 (private) does insert with media_type=1 (image) + result = context.getContentResolver().insert(providerUri, values); + } catch (Exception ex) { + excpetion = ex; + } finally { + if ((excpetion != null) || Global.debugEnabledSql || LibGlobal.debugEnabledJpg) { + Log.i(LOG_TAG, dbgContext + ":" + + MODUL_NAME + + ".execInsert " + excpetion + " " + + values.toString() + " => " + result + " " + excpetion, excpetion); + } + } + return result; + } + + /** + * Deletes media items specified by where with the option to prevent cascade delete of the image. + */ + public static int deleteMedia(String dbgContext, Context context, String where, String[] selectionArgs, boolean preventDeleteImageFile) { + String[] lastSelectionArgs = selectionArgs; + String lastUsedWhereClause = where; + int delCount = 0; + try { + if (preventDeleteImageFile) { + // set SQL_COL_PATH empty so sql-delete cannot cascade delete the referenced image-file via delete trigger + ContentValues values = new ContentValues(); + values.put(FotoSql.SQL_COL_PATH, FotoSql.DELETED_FILE_MARKER); + values.put(FotoSql.SQL_COL_EXT_MEDIA_TYPE, 0); // so it will not be shown as image any more + exexUpdateImpl(dbgContext + "-a: " + + MODUL_NAME + + ".deleteMedia: ", + context, values, lastUsedWhereClause, lastSelectionArgs); + + lastUsedWhereClause = FotoSql.SQL_COL_PATH + " is null"; + lastSelectionArgs = null; + delCount = context.getContentResolver() + .delete(FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE, lastUsedWhereClause, lastSelectionArgs); + if (Global.debugEnabledSql || LibGlobal.debugEnabledJpg) { + Log.i(LOG_TAG, dbgContext + "-b: " + + MODUL_NAME + + ".deleteMedia delete\n" + + QueryParameter.toString(null, null, FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, + lastUsedWhereClause, lastSelectionArgs, null, delCount)); + } + } else { + delCount = context.getContentResolver() + .delete(FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE, lastUsedWhereClause, lastSelectionArgs); + if (Global.debugEnabledSql || LibGlobal.debugEnabledJpg) { + Log.i(LOG_TAG, dbgContext + ": " + + MODUL_NAME + + ".deleteMedia\ndelete " + + QueryParameter.toString(null, null, + FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, + lastUsedWhereClause, lastSelectionArgs, null, delCount)); + } + } + } catch (Exception ex) { + // null pointer exception when delete matches not items?? + final String msg = dbgContext + ": Exception in " + + MODUL_NAME + + ".deleteMedia:\n" + + QueryParameter.toString(null, null, FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, + lastUsedWhereClause, lastSelectionArgs, null, -1) + + " : " + ex.getMessage(); + Log.e(LOG_TAG, msg, ex); + + } + return delCount; + } + + /** + * execRenameFolder(getActivity(),"/storage/sdcard0/testFolder/", "/storage/sdcard0/renamedFolder/") + * "/storage/sdcard0/testFolder/image.jpg" becomes "/storage/sdcard0/renamedFolder/image.jpg" + * + * @return number of updated items + */ + private static int _del_execRenameFolder_batch_not_working(Context context, String pathOld, String pathNew) { + final String dbgContext = MODUL_NAME + + ".execRenameFolder('" + + pathOld + "' => '" + pathNew + "')"; + // sql update file set path = newBegin + substing(path, begin+len) where path like newBegin+'%' + // public static final String SQL_EXPR_FOLDER = "substr(" + SQL_COL_PATH + ",1,length(" + SQL_COL_PATH + ") - length(" + MediaStore.Images.Media.DISPLAY_NAME + "))"; + + final String sqlColNewPathAlias = "new_path"; + final String sql_col_pathnew = "'" + pathNew + "' || substr(" + FotoSql.SQL_COL_PATH + + "," + (pathOld.length() + 1) + ",255) AS " + sqlColNewPathAlias; + + QueryParameter queryAffectedFiles = new QueryParameter() + .setID(FotoSql.QUERY_TYPE_DEFAULT) + .addColumn(FotoSql.SQL_COL_PK, + sql_col_pathnew) + .addFrom(FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME) + .addWhere(FotoSql.SQL_COL_PATH + " like '" + pathOld + "%'") + // SQL_COL_EXT_MEDIA_TYPE IS NOT NULL enshures that all media types (mp3, mp4, txt,...) are updated + .addWhere(FotoSql.SQL_COL_EXT_MEDIA_TYPE + " IS NOT NULL"); + + ArrayList ops = new ArrayList(); + + Cursor c = null; + try { + c = createCursorForQuery(null, dbgContext, context, queryAffectedFiles, null, null); + int pkColNo = c.getColumnIndex(FotoSql.SQL_COL_PK); + int pathColNo = c.getColumnIndex(sqlColNewPathAlias); + + while (c.moveToNext()) { + // paths[row] = c.getString(pathColNo); + // ids[row] = c.getLong(pkColNo); + ops.add(ContentProviderOperation.newUpdate(FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE) + .withSelection(FotoSql.FILTER_COL_PK, new String[]{c.getString(pkColNo)}) + .withValue(FotoSql.SQL_COL_PATH, c.getString(pathColNo)) + .build()); + } + } catch (Exception ex) { + Log.e(LOG_TAG, dbgContext + "-getAffected error :", ex); + return -1; + } finally { + if (c != null) c.close(); + } + + try { + context.getContentResolver().applyBatch(FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, ops); + } catch (Exception ex) { + // java.lang.IllegalArgumentException: Unknown authority content://media/external/file + // i assume not batch support for file + Log.e(LOG_TAG, dbgContext + "-updateAffected error :", ex); + return -1; + } + return ops.size(); + } + + public static ContentValues getDbContent(Context context, final long id) { + ContentResolver resolver = context.getContentResolver(); + + Cursor c = null; + try { + c = resolver.query(FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE, new String[]{"*"}, FotoSql.FILTER_COL_PK, new String[]{"" + id}, null); + if (c.moveToNext()) { + ContentValues values = new ContentValues(); + DatabaseUtils.cursorRowToContentValues(c, values); + return values; + } + } catch (Exception ex) { + Log.e(LOG_TAG, MODUL_NAME + + ".getDbContent(id=" + id + ") failed", ex); + } finally { + if (c != null) c.close(); + } + return null; + } +} 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 new file mode 100644 index 00000000..81cdf536 --- /dev/null +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaDBRepository.java @@ -0,0 +1,670 @@ +/* + * 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.androFotoFinder.queries; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.net.Uri; +import android.os.Build; +import android.os.CancellationSignal; +import android.provider.MediaStore; +import android.util.Log; + +import java.sql.Date; + +import de.k3b.LibGlobal; +import de.k3b.android.androFotoFinder.Global; +import de.k3b.android.androFotoFinder.R; +import de.k3b.database.QueryParameter; +import de.k3b.io.AlbumFile; +import de.k3b.io.IProgessListener; +import de.k3b.io.StringUtils; +import de.k3b.io.VISIBILITY; + +import static de.k3b.android.androFotoFinder.queries.FotoSql.QUERY_TYPE_UNDEFINED; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_COL_DATE_ADDED; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_COL_DATE_TAKEN; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_COL_EXT_MEDIA_TYPE; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_COL_EXT_RATING; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_COL_EXT_TITLE; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_COL_LAST_MODIFIED; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_COL_LAT; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_COL_LON; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_COL_ORIENTATION; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_COL_PATH; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_COL_PK; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_COL_SIZE; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_COL__IMPL_DISPLAY_NAME; +import static de.k3b.android.androFotoFinder.queries.FotoSql.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME; +import static de.k3b.android.androFotoFinder.tagDB.TagSql.SQL_COL_EXT_DESCRIPTION; +import static de.k3b.android.androFotoFinder.tagDB.TagSql.SQL_COL_EXT_TAGS; +import static de.k3b.android.androFotoFinder.tagDB.TagSql.SQL_COL_EXT_XMP_LAST_MODIFIED_DATE; + +/** + * Access Media Data through stand alone database-table. + *

+ * Since Android-10 (api 29) using sqLite functions as content-provider-columns is not possible anymore. + * Therefore apm uses a copy of contentprovider MediaStore.Images with same column names. + */ +public class MediaDBRepository implements IMediaRepositoryApi { + public static final String LOG_TAG = FotoSql.LOG_TAG + "DB"; + + // #155 + public static final boolean debugEnabledSqlRefresh = true; + + private static final String MODUL_NAME = MediaContentproviderRepositoryImpl.class.getName(); + private static String currentUpdateReason = null; + private static long currentUpdateId = 1; + private static int transactionNumber = 0; + private final SQLiteDatabase db; + + public MediaDBRepository(SQLiteDatabase db) { + this.db = db; + } + + @Override + public Cursor createCursorForQuery(StringBuilder out_debugMessage, String dbgContext, + QueryParameter parameters, VISIBILITY visibility, + CancellationSignal cancellationSignal) { + if (visibility != null) FotoSql.setWhereVisibility(parameters, visibility); + return createCursorForQuery(out_debugMessage, dbgContext, + parameters.toWhere(), parameters.toAndroidParameters(), + parameters.toGroupBy(), parameters.toHaving(), + parameters.toOrderBy(), + cancellationSignal, parameters.toColumns() + ); + } + + @Override + public Cursor createCursorForQuery(StringBuilder out_debugMessage, String dbgContext, String from, + String sqlWhereStatement, String[] sqlWhereParameters, + String sqlSortOrder, CancellationSignal cancellationSignal, + String... sqlSelectColums) { + return createCursorForQuery(out_debugMessage, dbgContext, + sqlWhereStatement, sqlWhereParameters, + null, null, + sqlSortOrder, cancellationSignal, sqlSelectColums); + } + + /** + * every cursor query should go through this. adds logging if enabled + */ + private Cursor createCursorForQuery(StringBuilder out_debugMessage, String dbgContext, + String sqlWhereStatement, String[] selectionArgs, String groupBy, + String having, String sqlSortOrder, + CancellationSignal cancellationSignal, final String... sqlSelectColums) { + Cursor query = null; + + Exception excpetion = null; + try { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + query = db.query(false, Impl.table, sqlSelectColums, sqlWhereStatement, selectionArgs, + groupBy, having, sqlSortOrder, null, cancellationSignal); + } else { + query = db.query(false, Impl.table, sqlSelectColums, sqlWhereStatement, selectionArgs, + groupBy, having, sqlSortOrder, null); + } + + } catch (Exception ex) { + excpetion = ex; + } finally { + if ((excpetion != null) || Global.debugEnabledSql || (out_debugMessage != null)) { + final int count = (query == null) ? 0 : query.getCount(); + StringBuilder message = StringUtils.appendMessage(out_debugMessage, excpetion, + dbgContext, MODUL_NAME + + ".createCursorForQuery:\n", + QueryParameter.toString(sqlSelectColums, null, Impl.table, sqlWhereStatement, + selectionArgs, sqlSortOrder, count)); + if (out_debugMessage == null) { + Log.i(LOG_TAG, message.toString(), excpetion); + } // else logging is done by caller + } + } + + return query; + } + + @Override + public int execUpdate(String dbgContext, long id, ContentValues values) { + return exexUpdateImpl(dbgContext, values, FotoSql.FILTER_COL_PK, new String[]{Long.toString(id)}); + } + + @Override + public int execUpdate(String dbgContext, String path, ContentValues values, VISIBILITY visibility) { + return exexUpdateImpl(dbgContext, values, FotoSql.getFilterExprPathLikeWithVisibility(visibility), new String[]{path}); + } + + /** + * return id of inserted item + * + * @param dbgContext + * @param dbUpdateFilterJpgFullPathName + * @param values + * @param visibility + * @param updateSuccessValue + */ + @Override + public Long insertOrUpdateMediaDatabase(String dbgContext, String dbUpdateFilterJpgFullPathName, + ContentValues values, VISIBILITY visibility, Long updateSuccessValue) { + Long result = updateSuccessValue; + + int modifyCount = execUpdate(dbgContext, dbUpdateFilterJpgFullPathName, + values, visibility); + + if (modifyCount == 0) { + // update failed (probably becauce oldFullPathName not found. try insert it. + FotoSql.addDateAdded(values); + + Uri uriWithId = execInsert(dbgContext, values); + result = FotoSql.getId(uriWithId); + } + return result; + } + + @Override + public int exexUpdateImpl(String dbgContext, ContentValues values, String sqlWhere, String[] selectionArgs) { + int result = -1; + Exception excpetion = null; + try { + result = db.update(Impl.table, values, sqlWhere, selectionArgs); + if (result != 0) { + currentUpdateId++; + currentUpdateReason = dbgContext; + } + } catch (Exception ex) { + excpetion = ex; + } finally { + if ((excpetion != null) || ((dbgContext != null) && (Global.debugEnabledSql || LibGlobal.debugEnabledJpg))) { + Log.i(LOG_TAG, dbgContext + ":" + + MODUL_NAME + + ".exexUpdate " + excpetion + "\n" + + QueryParameter.toString(null, values.toString(), FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, + sqlWhere, selectionArgs, null, result), excpetion); + } + } + return result; + } + + @Override + public ContentValues getDbContent(long id) { + Cursor c = null; + try { + c = this.createCursorForQuery(null, "getDbContent", + Impl.table, FotoSql.FILTER_COL_PK, new String[]{"" + id}, null, null, "*"); + if (c.moveToNext()) { + ContentValues values = new ContentValues(); + DatabaseUtils.cursorRowToContentValues(c, values); + return values; + } + } catch (Exception ex) { + Log.e(LOG_TAG, MODUL_NAME + + ".getDbContent(id=" + id + ") failed", ex); + } finally { + if (c != null) c.close(); + } + return null; + } + + /** + * every database insert should go through this. adds logging if enabled + * + * @param dbgContext + * @param values + */ + @Override + public Uri execInsert(String dbgContext, ContentValues values) { + long result = 0; + Exception excpetion = null; + try { + // on my android-4.4 insert with media_type=1001 (private) does insert with media_type=1 (image) + result = db.insert(Impl.table, null, values); + if (result > 0) { + currentUpdateId++; + currentUpdateReason = dbgContext; + } + + } catch (Exception ex) { + excpetion = ex; + } finally { + if ((excpetion != null) || Global.debugEnabledSql || LibGlobal.debugEnabledJpg) { + Log.i(LOG_TAG, dbgContext + ":" + + MODUL_NAME + + ".execInsert " + excpetion + " " + + values.toString() + " => " + result + " " + excpetion, excpetion); + } + } + return Uri.parse("content://apm/photo/" + result); + } + + /** + * Deletes media items specified by where with the option to prevent cascade delete of the image. + * + * @param dbgContext + * @param where + * @param selectionArgs + * @param preventDeleteImageFile + */ + @Override + public int deleteMedia(String dbgContext, String where, String[] selectionArgs, boolean preventDeleteImageFile) { + String[] lastSelectionArgs = selectionArgs; + String lastUsedWhereClause = where; + int delCount = 0; + try { + if (preventDeleteImageFile) { + // set SQL_COL_PATH empty so sql-delete cannot cascade delete the referenced image-file via delete trigger + ContentValues values = new ContentValues(); + values.put(FotoSql.SQL_COL_PATH, FotoSql.DELETED_FILE_MARKER); + values.put(FotoSql.SQL_COL_EXT_MEDIA_TYPE, 0); // so it will not be shown as image any more + exexUpdateImpl(dbgContext + "-a: " + + MODUL_NAME + + ".deleteMedia: ", + values, lastUsedWhereClause, lastSelectionArgs); + + lastUsedWhereClause = FotoSql.SQL_COL_PATH + " is null"; + lastSelectionArgs = null; + delCount = db.delete(Impl.table, lastUsedWhereClause, lastSelectionArgs); + if (Global.debugEnabledSql || LibGlobal.debugEnabledJpg) { + Log.i(LOG_TAG, dbgContext + "-b: " + + MODUL_NAME + + ".deleteMedia delete\n" + + QueryParameter.toString(null, null, FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, + lastUsedWhereClause, lastSelectionArgs, null, delCount)); + } + } else { + delCount = db.delete(Impl.table, lastUsedWhereClause, lastSelectionArgs); + if (Global.debugEnabledSql || LibGlobal.debugEnabledJpg) { + Log.i(LOG_TAG, dbgContext + ": " + + MODUL_NAME + + ".deleteMedia\ndelete " + + QueryParameter.toString(null, null, + FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, + lastUsedWhereClause, lastSelectionArgs, null, delCount)); + } + } + if (delCount > 0) { + currentUpdateId++; + currentUpdateReason = dbgContext; + } + + } catch (Exception ex) { + // null pointer exception when delete matches not items?? + final String msg = dbgContext + ": Exception in " + + MODUL_NAME + + ".deleteMedia:\n" + + QueryParameter.toString(null, null, FotoSqlBase.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME, + lastUsedWhereClause, lastSelectionArgs, null, -1) + + " : " + ex.getMessage(); + Log.e(LOG_TAG, msg, ex); + + } + return delCount; + } + + @Override + public long getCurrentUpdateId() { + return currentUpdateId; + } + + @Override + public boolean mustRequery(long updateId) { + final boolean modified = currentUpdateId != updateId; + if (modified && MediaDBRepository.debugEnabledSqlRefresh) { + Log.i(MediaDBRepository.LOG_TAG, "mustRequery: true because of " + currentUpdateReason); + } + return modified; + } + + @Override + public void beginTransaction() { + if (Global.debugEnabledSql) { + Log.i(LOG_TAG, "beginTransaction #" + (++transactionNumber)); + } + + db.beginTransaction(); + } + + @Override + public void setTransactionSuccessful() { + if (Global.debugEnabledSql) { + Log.i(LOG_TAG, "setTransactionSuccessful #" + transactionNumber); + } + db.setTransactionSuccessful(); + } + + @Override + public void endTransaction() { + if (Global.debugEnabledSql) { + Log.i(LOG_TAG, "endTransaction #" + transactionNumber); + } + db.endTransaction(); + } + + public static class Impl { + /** + * SQL to create copy of contentprovider MediaStore.Images. + * copied from android-4.4 android database. Removed columns not used + */ + public static final String[] DDL = new String[]{ + "DROP TABLE IF EXISTS \"files\"", + "CREATE TABLE \"files\" (\n" + + "\t_id INTEGER PRIMARY KEY AUTOINCREMENT,\n" + + "\t_size INTEGER,\n" + + "\tdate_added INTEGER,\n" + + "\tdate_modified INTEGER,\n" + + "\tdatetaken INTEGER,\n" + + "\torientation INTEGER,\n" + + "\tduration INTEGER,\n" + + "\tbookmark INTEGER,\n" + + "\tmedia_type INTEGER,\n" + + "\twidth INTEGER,\n" + + "\theight INTEGER,\n" + + + "\t_data TEXT UNIQUE COLLATE NOCASE,\n" + + "\ttitle TEXT,\n" + + "\tdescription TEXT,\n" + + "\t_display_name TEXT,\n" + + "\tmime_type TEXT,\n" + + "\ttags TEXT,\n" + + + "\tlatitude DOUBLE,\n" + + "\tlongitude DOUBLE\n" + + "\t )", + "CREATE INDEX media_type_index ON files(media_type)", + "CREATE INDEX path_index ON files(_data)", + "CREATE INDEX sort_index ON files(datetaken ASC, _id ASC)", + "CREATE INDEX title_idx ON files(title)", + }; + + public static final String table = "files"; + // same colum order as in DDL + private static final String[] USED_MEDIA_COLUMNS = new String[]{ + // INTEGER 0 .. 10 + SQL_COL_PK, + SQL_COL_DATE_ADDED, + SQL_COL_LAST_MODIFIED, + SQL_COL_SIZE, + SQL_COL_DATE_TAKEN, + SQL_COL_ORIENTATION, + SQL_COL_EXT_XMP_LAST_MODIFIED_DATE, // duration + SQL_COL_EXT_RATING, // bookmark + SQL_COL_EXT_MEDIA_TYPE, + MediaStore.MediaColumns.WIDTH, + MediaStore.MediaColumns.HEIGHT, + + // TEXT 11 .. 16 + SQL_COL_PATH, // _data + SQL_COL_EXT_TITLE, + SQL_COL_EXT_DESCRIPTION, + SQL_COL__IMPL_DISPLAY_NAME, + MediaStore.MediaColumns.MIME_TYPE, + SQL_COL_EXT_TAGS, + + // DOUBLE 17..18 + SQL_COL_LAT, + SQL_COL_LON, + }; + + private static final int intMin = 0; + private static final int intMax = 10; + private static final int txtMin = 11; + private static final int txtMax = 16; + private static final int dblMin = 17; + private static final int dblMax = 18; + + private static final int colID = 0; + private static final int colDATE_ADDED = 1; + private static final int colLAST_MODIFIED = 2; + private static final String FILTER_EXPR_AFFECTED_FILES + = "(" + FotoSql.FILTER_EXPR_PRIVATE_PUBLIC + + " OR " + SQL_COL_PATH + " like '%" + AlbumFile.SUFFIX_VALBUM + "' " + + " OR " + SQL_COL_PATH + " like '%" + AlbumFile.SUFFIX_QUERY + "' " + + ")"; + private static final QueryParameter queryGetAllColumns = new QueryParameter() + .setID(QUERY_TYPE_UNDEFINED) + .addColumn(USED_MEDIA_COLUMNS) + .addFrom(SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME) + .addWhere(FILTER_EXPR_AFFECTED_FILES); + + private static boolean isLomg(int index) { + return index >= intMin && index <= intMax; + } + + // private Object get(Cursor cursor, columIndex) + + private static boolean isString(int index) { + return index >= txtMin && index <= txtMax; + } + + private static boolean isDouble(int index) { + return index >= dblMin && index <= dblMax; + } + + private static String getSqlInsertWithParams() { + StringBuilder sql = new StringBuilder(); + + sql.append("INSERT INTO ").append(table).append("(").append(USED_MEDIA_COLUMNS[0]); + for (int i = 1; i < USED_MEDIA_COLUMNS.length; i++) { + sql.append(", ").append(USED_MEDIA_COLUMNS[i]); + } + sql.append(") VALUES (?"); + for (int i = 1; i < USED_MEDIA_COLUMNS.length; i++) { + sql.append(", ?"); + } + sql.append(")"); + return sql.toString(); + } + + private static String getSqlUpdateWithParams() { + StringBuilder sql = new StringBuilder(); + + sql.append("UPDATE ").append(table).append(" SET "); + for (int i = 1; i < USED_MEDIA_COLUMNS.length; i++) { + if (i > 1) sql.append(", "); + sql.append(USED_MEDIA_COLUMNS[i]).append("=?"); + } + sql.append(" WHERE ").append(USED_MEDIA_COLUMNS[0]).append("=?"); + return sql.toString(); + } + + private static int bindAndExecUpdate(Cursor c, SQLiteStatement sql) { + sql.clearBindings(); + + // sql where + sql.bindLong(dblMax + 1, c.getLong(intMin)); + + for (int i = intMin + 1; i <= intMax; i++) { + if (!c.isNull(i)) { + sql.bindLong(i, c.getLong(i)); + } + } + for (int i = txtMin; i <= txtMax; i++) { + if (!c.isNull(i)) { + sql.bindString(i, c.getString(i)); + } + } + for (int i = dblMin; i <= dblMax; i++) { + if (!c.isNull(i)) { + sql.bindDouble(i, c.getDouble(i)); + } + } + return sql.executeUpdateDelete(); + } + + private static void bindAndExecInsert(Cursor c, SQLiteStatement sql) { + sql.clearBindings(); + + for (int i = intMin; i <= intMax; i++) { + if (!c.isNull(i)) { + sql.bindLong(i + 1, c.getLong(i)); + } + } + for (int i = txtMin; i <= txtMax; i++) { + if (!c.isNull(i)) { + sql.bindString(i + 1, c.getString(i)); + } + } + for (int i = dblMin; i <= dblMax; i++) { + if (!c.isNull(i)) { + sql.bindDouble(i + 1, c.getDouble(i)); + } + } + sql.executeInsert(); + } + + private static ContentValues getContentValues(Cursor cursor, ContentValues destination) { + destination.clear(); + int colCount = cursor.getColumnCount(); + String columnName; + for (int i = 0; i < colCount; i++) { + columnName = cursor.getColumnName(i); + if (cursor.isNull(i)) { + destination.putNull(columnName); + } else if (isLomg(i)) { + destination.put(columnName, cursor.getLong(i)); + } else if (isString(i)) { + destination.put(columnName, cursor.getString(i)); + } else if (isDouble(i)) { + destination.put(columnName, cursor.getDouble(i)); + } + } + return destination; + } + + public static void clearMedaiCopy(SQLiteDatabase db) { + try { + db.execSQL("DROP TABLE " + table); + } catch (Exception ex) { + // Log.e(LOG_TAG, "FotoSql.execGetFotoPaths() Cannot get path from: " + FotoSql.SQL_COL_PATH + " like '" + pathFilter +"'", ex); + } finally { + } + } + + + public static int updateMedaiCopy(Context context, SQLiteDatabase db, Date lastUpdate, IProgessListener progessListener) { + int progress = 0; + java.util.Date startTime = new java.util.Date(); + + QueryParameter query = queryGetAllColumns; + long _lastUpdate = (lastUpdate != null) ? (lastUpdate.getTime() / 1000L) : 0L; + + if (_lastUpdate != 0) { + query = new QueryParameter().getFrom(queryGetAllColumns); + FotoSql.addWhereDateModifiedMinMax(query, _lastUpdate, 0); + // FotoSql.createCursorForQuery() + } + Cursor c = null; + SQLiteStatement sqlInsert = null; + SQLiteStatement sqlUpdate = null; + SQLiteStatement lastSql = null; + boolean isUpdate = false; + int itemCount = 0; + int insertCout = 0; + int updateCount = 0; + // ContentValues contentValues = new ContentValues(); + try { + db.beginTransaction(); // Performance boost: all db-inserts/updates in one transaction + + if (progessListener != null) progessListener.onProgress(progress, 0, + context.getString(R.string.load_db_menu_title)); + + c = MediaContentproviderRepositoryImpl.createCursorForQuery(null, "updateMedaiCopy-source", context, + query, null, null); + itemCount = c.getCount(); + + sqlInsert = db.compileStatement(getSqlInsertWithParams()); + sqlUpdate = db.compileStatement(getSqlUpdateWithParams()); + while (c.moveToNext()) { + // getContentValues(c, contentValues); + + isUpdate = (c.getLong(colDATE_ADDED) <= _lastUpdate); + + if (isUpdate) { + updateCount++; + lastSql = sqlUpdate; + isUpdate = bindAndExecUpdate(c, sqlUpdate) > 0; + // 0 affected update rows: must insert + } + + if (!isUpdate) { + insertCout++; + lastSql = sqlInsert; + bindAndExecInsert(c, sqlInsert); + } + + lastSql = null; + // save(db, c, contentValues, _lastUpdate); + if ((progessListener != null) && (progress % 100) == 0) { + if (!progessListener.onProgress(progress, itemCount, context.getString(R.string.scanner_update_result_format, progress))) { + // canceled in gui thread + return -progress; + } + } + progress++; + } + db.setTransactionSuccessful(); // This commits the transaction if there were no exceptions + if (Global.debugEnabledSql) { + java.util.Date endTime = new java.util.Date(); + final String message = "MediaDBRepository.updateMedaiCopy(inserted:" + insertCout + + ", updated:" + updateCount + + ", toal:" + progress + + " / " + itemCount + + ") in " + ((endTime.getTime() - startTime.getTime()) / 1000) + + " Secs"; + Log.i(LOG_TAG, message); + } + } catch (Exception ex) { + java.util.Date endTime = new java.util.Date(); + final String message = "MediaDBRepository.updateMedaiCopy(inserted:" + insertCout + + ", updated:" + updateCount + + ", toal:" + progress + + " / " + itemCount + + ") in " + ((endTime.getTime() - startTime.getTime()) / 1000) + + " Secs"; + Log.e(LOG_TAG, "Cannot insert/update: " + lastSql + " from " + c + " in " + message, ex); + } finally { + sqlInsert.close(); + sqlUpdate.close(); + db.endTransaction(); + if (c != null) c.close(); + } + + if (Global.debugEnabled) { + // Log.d(LOG_TAG, "FotoSql.execGetFotoPaths() result count=" + result.size()); + } + return progress; + } + + private static void save(SQLiteDatabase db, Cursor c, ContentValues contentValues, long lastUpdate) { + boolean isNew = (c.getLong(colDATE_ADDED) > lastUpdate); + + if (isNew) { + db.insert(table, null, contentValues); + } else { + String[] params = new String[]{"" + c.getLong(colID)}; + contentValues.remove(SQL_COL_PK); + db.update(table, contentValues, FotoSql.FILTER_COL_PK, params); + } + } + } +} 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 new file mode 100644 index 00000000..702de338 --- /dev/null +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MediaRepositoryApiWrapper.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2019-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.androFotoFinder.queries; + +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.CancellationSignal; + +import de.k3b.database.QueryParameter; +import de.k3b.io.VISIBILITY; +import de.k3b.media.IPhotoProperties; + +/** + * (Default) Implementation of {@link IMediaRepositoryApi} to forward all methods to an inner child {@link IPhotoProperties}. + *

+ * Created by k3b on 30.11.2019. + */ +public class MediaRepositoryApiWrapper implements IMediaRepositoryApi { + protected final IMediaRepositoryApi readChild; + protected final IMediaRepositoryApi writeChild; + protected final IMediaRepositoryApi transactionChild; + + /** + * count the non path write calls + */ + private int modifyCount = 0; + + public MediaRepositoryApiWrapper(IMediaRepositoryApi child) { + this(child, child, child); + } + + public MediaRepositoryApiWrapper(IMediaRepositoryApi readChild, IMediaRepositoryApi writeChild, IMediaRepositoryApi transactionChild) { + this.readChild = readChild; + this.writeChild = writeChild; + this.transactionChild = transactionChild; + } + + @Override + public Cursor createCursorForQuery(StringBuilder out_debugMessage, String dbgContext, QueryParameter parameters, VISIBILITY visibility, CancellationSignal cancellationSignal) { + return readChild.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); + } + + @Override + public int execUpdate(String dbgContext, long id, ContentValues values) { + return writeChild.execUpdate(dbgContext, id, values); + } + + @Override + public int execUpdate(String dbgContext, String path, ContentValues values, VISIBILITY visibility) { + return writeChild.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 id of inserted item + * + * @param dbgContext + * @param dbUpdateFilterJpgFullPathName + * @param values + * @param visibility + * @param updateSuccessValue + */ + @Override + public Long insertOrUpdateMediaDatabase(String dbgContext, String dbUpdateFilterJpgFullPathName, ContentValues values, VISIBILITY visibility, Long updateSuccessValue) { + return writeChild.insertOrUpdateMediaDatabase(dbgContext, dbUpdateFilterJpgFullPathName, values, visibility, updateSuccessValue); + } + + /** + * every database insert should go through this. adds logging if enabled + * + * @param dbgContext + * @param values + */ + @Override + public Uri execInsert(String dbgContext, ContentValues values) { + return writeChild.execInsert(dbgContext, values); + } + + /** + * Deletes media items specified by where with the option to prevent cascade delete of the image. + */ + @Override + public int deleteMedia(String dbgContext, String where, String[] selectionArgs, boolean preventDeleteImageFile) { + return writeChild.deleteMedia(dbgContext, where, selectionArgs, preventDeleteImageFile); + } + + @Override + public ContentValues getDbContent(long id) { + return readChild.getDbContent(id); + } + + @Override + public long getCurrentUpdateId() { + return transactionChild.getCurrentUpdateId(); + } + + @Override + public boolean mustRequery(long updateId) { + return transactionChild.mustRequery(updateId); + } + + @Override + public void beginTransaction() { + transactionChild.beginTransaction(); + } + + @Override + public void setTransactionSuccessful() { + transactionChild.setTransactionSuccessful(); + } + + @Override + public void endTransaction() { + transactionChild.endTransaction(); + } +} diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/MergedMediaRepository.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MergedMediaRepository.java new file mode 100644 index 00000000..f8f31800 --- /dev/null +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/MergedMediaRepository.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2019-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.androFotoFinder.queries; + +import android.content.ContentValues; +import android.net.Uri; + +import de.k3b.io.VISIBILITY; + +/** + * #155: All reads are done through database while writes are + * applied to database and contentProvider. + *

+ * Since Android-10 (api 29) using sqLite functions as content-provider-columns is not possible anymore. + * Therefore apm uses a copy of contentprovider MediaStore.Images with same column names and same pk. + */ +public class MergedMediaRepository extends MediaRepositoryApiWrapper { + private final IMediaRepositoryApi database; + private final IMediaRepositoryApi contentProvider; + + public MergedMediaRepository(IMediaRepositoryApi database, IMediaRepositoryApi contentProvider) { + super(database, contentProvider, database); + this.database = database; + this.contentProvider = contentProvider; + } + + @Override + public int execUpdate(String dbgContext, long id, ContentValues values) { + int result = super.execUpdate(dbgContext, id, values); + database.execUpdate(dbgContext, id, values); + return result; + } + + @Override + public int execUpdate(String dbgContext, String path, ContentValues values, VISIBILITY visibility) { + int result = super.execUpdate(dbgContext, path, values, visibility); + database.execUpdate(dbgContext, path, values, visibility); + return result; + } + + @Override + public int exexUpdateImpl(String dbgContext, ContentValues values, String sqlWhere, String[] selectionArgs) { + int result = super.exexUpdateImpl(dbgContext, values, sqlWhere, selectionArgs); + database.exexUpdateImpl(dbgContext, values, sqlWhere, selectionArgs); + return result; + } + + /** + * return id of inserted item + * + * @param dbgContext + * @param dbUpdateFilterJpgFullPathName + * @param values + * @param visibility + * @param updateSuccessValue + */ + @Override + public Long insertOrUpdateMediaDatabase(String dbgContext, String dbUpdateFilterJpgFullPathName, + ContentValues values, VISIBILITY visibility, Long updateSuccessValue) { + Long result = updateSuccessValue; + + int modifyCount = contentProvider.execUpdate(dbgContext, dbUpdateFilterJpgFullPathName, + values, visibility); + + if (modifyCount == 0) { + // update failed (probably becauce oldFullPathName not found. try insert it. + FotoSql.addDateAdded(values); + + // insert into contentProvider and database + Uri uriWithId = execInsert(dbgContext, values); + result = FotoSql.getId(uriWithId); + } else { + // update into contentprovider successfull. also add to database + database.execUpdate(dbgContext, dbUpdateFilterJpgFullPathName, + values, visibility); + } + return result; + } + + /** + * every database insert should go through this. adds logging if enabled + * + * @param dbgContext + * @param values + */ + @Override + public Uri execInsert(String dbgContext, ContentValues values) { + Uri result = super.execInsert(dbgContext, values); + + // insert with same pk as contentprovider does + values.put(FotoSql.SQL_COL_PK, FotoSql.getId(result)); + database.execInsert(dbgContext, values); + values.remove(FotoSql.SQL_COL_PK); + return result; + } + + /** + * Deletes media items specified by where with the option to prevent cascade delete of the image. + * + * @param dbgContext + * @param where + * @param selectionArgs + * @param preventDeleteImageFile + */ + @Override + public int deleteMedia(String dbgContext, String where, String[] selectionArgs, boolean preventDeleteImageFile) { + int result = super.deleteMedia(dbgContext, where, selectionArgs, preventDeleteImageFile); + database.deleteMedia(dbgContext, where, selectionArgs, preventDeleteImageFile); + return result; + } +} diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/queries/SqlJobTaskBase.java b/app/src/main/java/de/k3b/android/androFotoFinder/queries/SqlJobTaskBase.java index 949bac19..fa741099 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/queries/SqlJobTaskBase.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/queries/SqlJobTaskBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2017 by k3b. + * Copyright (c) 2015-2019 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -21,7 +21,6 @@ import android.app.Activity; import android.database.Cursor; -import android.net.Uri; import android.os.AsyncTask; import android.util.Log; @@ -66,8 +65,9 @@ protected SelectedItems doInBackground(QueryParameter... querys) { Cursor cursor = null; try { - cursor = mContext.getContentResolver().query(Uri.parse(query.toFrom()), query.toColumns(), - query.toAndroidWhere(), query.toAndroidParameters(), query.toOrderBy()); + cursor = FotoSql.getMediaDBApi().createCursorForQuery( + null, "SqlJobTask", + query, null, null); int itemCount = cursor.getCount(); final int expectedCount = itemCount + itemCount; 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 4dee2740..16af38d5 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 @@ -19,11 +19,8 @@ package de.k3b.android.androFotoFinder.tagDB; -import android.content.ContentResolver; import android.content.ContentValues; -import android.content.Context; import android.database.Cursor; -import android.database.DatabaseUtils; import android.provider.MediaStore; import android.util.Log; @@ -61,7 +58,6 @@ public class TagSql extends FotoSql { public static final String SQL_COL_EXT_TAGS = MediaStore.Video.Media.TAGS; public static final String SQL_COL_EXT_DESCRIPTION = MediaStore.Images.Media.DESCRIPTION; - public static final String SQL_COL_EXT_TITLE = MediaStore.Images.Media.TITLE; /** The date & time when last non standard media-scan took place *

Type: INTEGER (long) as milliseconds since jan 1, 1970

*/ @@ -261,7 +257,7 @@ public static void addWhereTagsIncluded(QueryParameter resultQuery, List } } - public static int fixPrivate(Context context) { + public static int fixPrivate() { // update ... set media_type=1001 where media_type=1 and tags like '%;PRIVATE;%' ContentValues values = new ContentValues(); values.put(SQL_COL_EXT_MEDIA_TYPE, MEDIA_TYPE_IMAGE_PRIVATE); @@ -275,7 +271,7 @@ public static int fixPrivate(Context context) { PhotoPropertiesUtil.IMG_TYPE_PRIVATE + "'")); } where.append(")"); - return exexUpdateImpl("Fix visibility private", context, + return getMediaDBApi().exexUpdateImpl("Fix visibility private", values, where.toString(), new String[] {"%;" + VISIBILITY.TAG_PRIVATE + ";%"}); } @@ -327,25 +323,6 @@ public static void setFileModifyDate(ContentValues values, long fileModifyDateSe } } - public static ContentValues getDbContent(Context context, final long id) { - ContentResolver resolver = context.getContentResolver(); - - Cursor c = null; - try { - c = resolver.query(SQL_TABLE_EXTERNAL_CONTENT_URI_FILE, new String[]{"*"}, FILTER_COL_PK, new String[]{"" + id}, null); - if (c.moveToNext()) { - ContentValues values = new ContentValues(); - DatabaseUtils.cursorRowToContentValues(c, values); - return values; - } - } catch (Exception ex) { - Log.e(Global.LOG_CONTEXT, "FotoSql.getDbContent(id=" + id + ") failed", ex); - } finally { - if (c != null) c.close(); - } - return null; - } - /** * Copies non null content of jpg to existing media database item. * @@ -353,7 +330,7 @@ public static ContentValues getDbContent(Context context, final long id) { * @param allowSetNulls if one of these columns are null, the set null is copied, too * @return number of changed db items */ - public static int updateDB(String dbgContext, Context context, String oldFullJpgFilePath, + public static int updateDB(String dbgContext, String oldFullJpgFilePath, PhotoPropertiesUpdateHandler jpg, MediaFormatter.FieldID... allowSetNulls) { if ((jpg != null) && (!PhotoPropertiesMediaFilesScanner.isNoMedia(oldFullJpgFilePath))) { ContentValues dbValues = new ContentValues(); @@ -379,7 +356,7 @@ public static int updateDB(String dbgContext, Context context, String oldFullJpg TagSql.setXmpFileModifyDate(dbValues, xmpFilelastModified); TagSql.setFileModifyDate(dbValues, newFullJpgFilePath); - return TagSql.execUpdate(dbgContext, context, oldFullJpgFilePath, + return TagSql.execUpdate(dbgContext, oldFullJpgFilePath, TagSql.EXT_LAST_EXT_SCAN_UNKNOWN, dbValues, VISIBILITY.PRIVATE_PUBLIC); } @@ -389,21 +366,21 @@ public static int updateDB(String dbgContext, Context context, String oldFullJpg } - public static int execUpdate(String dbgContext, Context context, String path, long xmpFileDate, ContentValues values, VISIBILITY visibility) { + public static int execUpdate(String dbgContext, String path, long xmpFileDate, ContentValues values, VISIBILITY visibility) { if ((!Global.Media.enableXmpNone) || (xmpFileDate == EXT_LAST_EXT_SCAN_UNKNOWN)) { - return execUpdate(dbgContext, context, path, values, visibility); + return getMediaDBApi().execUpdate(dbgContext, path, values, visibility); } - return exexUpdateImpl(dbgContext, context, values, FILTER_EXPR_PATH_LIKE_XMP_DATE, new String[]{path, Long.toString(xmpFileDate)}); + return getMediaDBApi().exexUpdateImpl(dbgContext, values, FILTER_EXPR_PATH_LIKE_XMP_DATE, new String[]{path, Long.toString(xmpFileDate)}); } /** return how many photos exist that have one or more tags from list */ - public static int getTagRefCount(Context context, List tags) { + public static int getTagRefCount(List tags) { QueryParameter query = new QueryParameter() .addColumn("count(*)").addFrom(SQL_TABLE_EXTERNAL_CONTENT_URI_FILE_NAME); if (addWhereAnyOfTags(query, tags) > 0) { Cursor c = null; try { - c = createCursorForQuery(null, "getTagRefCount", context, query, VISIBILITY.PRIVATE_PUBLIC); + c = getMediaDBApi().createCursorForQuery(null, "getTagRefCount", query, VISIBILITY.PRIVATE_PUBLIC, null); if (c.moveToFirst()) { return c.getInt(0); } @@ -433,12 +410,11 @@ public TagWorflowItem(long id, String path, List tags, long xmpLastModif /** * converts selectedItemPks and/or anyOfTags to TagWorflowItem-s - * @param context * @param selectedItemPks if not null list of comma seperated item-pks * @param anyOfTags if not null list of tag-s where at least one oft the tag must be in the photo. * @return */ - public static List loadTagWorflowItems(Context context, String selectedItemPks, List anyOfTags) { + public static List loadTagWorflowItems(String selectedItemPks, List anyOfTags) { QueryParameter query = new QueryParameter() .addColumn(TagSql.SQL_COL_PK, TagSql.SQL_COL_PATH, TagSql.SQL_COL_EXT_TAGS, TagSql.SQL_COL_EXT_XMP_LAST_MODIFIED_DATE); @@ -457,7 +433,7 @@ public static List loadTagWorflowItems(Context context, String s if (filterCount > 0) { try { - c = createCursorForQuery(null, "loadTagWorflowItems", context, query, VISIBILITY.PRIVATE_PUBLIC); + c = getMediaDBApi().createCursorForQuery(null, "loadTagWorflowItems", query, VISIBILITY.PRIVATE_PUBLIC, null); if (c.moveToFirst()) { do { result.add(new TagWorflowItem(c.getLong(0), c.getString(1), TagConverter.fromString(c.getString(2)), diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/tagDB/TagWorflow.java b/app/src/main/java/de/k3b/android/androFotoFinder/tagDB/TagWorflow.java index 20962708..b1629047 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/tagDB/TagWorflow.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/tagDB/TagWorflow.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2019 by k3b. + * Copyright (c) 2017-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -29,13 +29,15 @@ import java.util.List; import de.k3b.android.androFotoFinder.Global; +import de.k3b.android.androFotoFinder.queries.FotoSql; +import de.k3b.android.androFotoFinder.queries.IMediaRepositoryApi; import de.k3b.android.util.AndroidFileCommands; +import de.k3b.io.FileCommands; import de.k3b.io.IProgessListener; import de.k3b.io.collections.SelectedFiles; -import de.k3b.io.FileCommands; import de.k3b.media.MediaFormatter; -import de.k3b.media.PhotoPropertiesXmpSegment; import de.k3b.media.PhotoPropertiesUpdateHandler; +import de.k3b.media.PhotoPropertiesXmpSegment; import de.k3b.tagDB.Tag; import de.k3b.tagDB.TagConverter; import de.k3b.tagDB.TagProcessor; @@ -78,21 +80,27 @@ public TagWorflow init(Activity context, SelectedFiles selectedItems, List /** execute the updates for all affected files in the Workflow. */ public int updateTags(List addedTags, List removedTags) { - int itemCount = 0; - if (items != null) { - int progressCountDown = 0; - int total = items.size(); - for (TagSql.TagWorflowItem item : items) { - itemCount+=updateTags(item, addedTags, removedTags); - progressCountDown--; - if (progressCountDown < 0) { - progressCountDown = 10; - if (!onProgress(itemCount, total, item.path)) break; - } - } // for each image + final IMediaRepositoryApi mediaDBApi = FotoSql.getMediaDBApi(); + try { + mediaDBApi.beginTransaction(); // Performance boost: all db-inserts/updates in one transaction + int itemCount = 0; + if (items != null) { + int progressCountDown = 0; + int total = items.size(); + for (TagSql.TagWorflowItem item : items) { + itemCount += updateTags(item, addedTags, removedTags); + progressCountDown--; + if (progressCountDown < 0) { + progressCountDown = 10; + if (!onProgress(itemCount, total, item.path)) break; + } + } // for each image + } + mediaDBApi.setTransactionSuccessful(); + return itemCount; + } finally { + mediaDBApi.endTransaction(); } - - return itemCount; } /** update one file if tags change or xmp does not exist yet: xmp-sidecar-file, media-db and batch */ @@ -124,7 +132,7 @@ protected int updateTags(TagSql.TagWorflowItem tagWorflowItemFromDB, List loadTagWorflowItems(Context context, String selectedItems, List anyOfTags) { - return TagSql.loadTagWorflowItems(context, selectedItems, anyOfTags); + return TagSql.loadTagWorflowItems(selectedItems, anyOfTags); } } diff --git a/app/src/main/java/de/k3b/android/androFotoFinder/tagDB/TagsPickerFragment.java b/app/src/main/java/de/k3b/android/androFotoFinder/tagDB/TagsPickerFragment.java index a66f9cf6..4d34dbec 100644 --- a/app/src/main/java/de/k3b/android/androFotoFinder/tagDB/TagsPickerFragment.java +++ b/app/src/main/java/de/k3b/android/androFotoFinder/tagDB/TagsPickerFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2019 by k3b. + * Copyright (c) 2017-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -580,14 +580,14 @@ public boolean showTagDeleteDialog(final Tag item) { List rootList = new ArrayList(); rootList.add(item); - final int rootTagReferenceCount = TagSql.getTagRefCount(getActivity(), rootList); + final int rootTagReferenceCount = TagSql.getTagRefCount(rootList); List children = item.getChildren(loadTagRepositoryItems(false), true, false); if (children != null) rootList.addAll(children); final int allTagReferenceCount = (children == null) ? rootTagReferenceCount - : TagSql.getTagRefCount(getActivity(), rootList); + : TagSql.getTagRefCount(rootList); if (children == null) { chkDeleteChildren.setVisibility(View.GONE); @@ -690,7 +690,7 @@ protected View onCreateContentView(Activity parent) { List rootList = new ArrayList(); rootList.add(tag); - final int rootTagReferenceCount = TagSql.getTagRefCount(getActivity(), rootList); + final int rootTagReferenceCount = TagSql.getTagRefCount(rootList); chkUpdatePhotos.setText(getString(R.string.tags_update_photos) + " (" + rootTagReferenceCount + ")"); @@ -843,10 +843,10 @@ private int tagAdd(Tag parent, String itemExpression) { } } mDataAdapter.notifyDataSetInvalidated(); - mDataAdapter.notifyDataSetChanged(); + notifyDataSetChanged(); mDataAdapter.reloadList(); TagRepository.getInstance().save(); - mDataAdapter.notifyDataSetChanged(); + notifyDataSetChanged(); } return changeCount; } @@ -860,7 +860,7 @@ private void tagChange(Tag tag, Tag parent) { mDataAdapter.reloadList(); } TagRepository.getInstance().save(); - mDataAdapter.notifyDataSetChanged(); + notifyDataSetChanged(); } private boolean onPaste(Tag currentMenuSelection) { @@ -940,4 +940,8 @@ private void refershResultList() { private void refreshCounter() { TagsPickerFragment.this.mDataAdapter.getCount(); } + + public void notifyDataSetChanged() { + mDataAdapter.notifyDataSetChanged(); + } } diff --git a/app/src/main/java/de/k3b/android/util/AndroidFileCommands.java b/app/src/main/java/de/k3b/android/util/AndroidFileCommands.java index ceaf9807..d97fd113 100644 --- a/app/src/main/java/de/k3b/android/util/AndroidFileCommands.java +++ b/app/src/main/java/de/k3b/android/util/AndroidFileCommands.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2019 by k3b. + * Copyright (c) 2015-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -37,6 +37,7 @@ import java.io.File; import java.util.Date; +import de.k3b.android.androFotoFinder.AndroidTransactionLogger; import de.k3b.android.androFotoFinder.Global; import de.k3b.android.androFotoFinder.LockScreen; import de.k3b.android.androFotoFinder.R; @@ -55,6 +56,7 @@ import de.k3b.io.collections.SelectedFiles; import de.k3b.media.MediaFormatter; import de.k3b.media.PhotoPropertiesBulkUpdateService; +import de.k3b.media.PhotoPropertiesDiffCopy; import de.k3b.media.PhotoPropertiesUpdateHandler; import de.k3b.transactionlog.MediaTransactionLogEntryType; import de.k3b.transactionlog.TransactionLoggerBase; @@ -171,12 +173,12 @@ private static int getResourceId(int opCode) { } - public boolean onOptionsItemSelected(final MenuItem item, final SelectedFiles selectedFileNames) { + public boolean onOptionsItemSelected(final MenuItem item, final SelectedFiles selectedFileNames, PhotoChangeNotifyer.PhotoChangedListener photoChangedListener) { if ((selectedFileNames != null) && (selectedFileNames.size() > 0)) { // Handle item selection switch (item.getItemId()) { case R.id.cmd_delete: - return cmdDeleteFileWithQuestion(selectedFileNames); + return cmdDeleteFileWithQuestion(selectedFileNames, photoChangedListener); default:break; } } @@ -218,9 +220,9 @@ public int execRename(File srcDirFile, String newFolderName) { boolean isDir = srcDirFile.isDirectory(); if (srcDirFile.renameTo(destDirFile)) { if (isDir) { - modifyCount = FotoSql.execRenameFolder(this.mContext, srcDirFile.getAbsolutePath() + "/", destDirFile.getAbsolutePath() + "/"); + modifyCount = FotoSql.execRenameFolder(srcDirFile.getAbsolutePath() + "/", destDirFile.getAbsolutePath() + "/"); } else { - modifyCount = FotoSql.execRename(mContext, srcDirFile.getAbsolutePath(), destDirFile.getAbsolutePath()); + modifyCount = FotoSql.execRename(srcDirFile.getAbsolutePath(), destDirFile.getAbsolutePath()); } if (modifyCount < 0) { destDirFile.renameTo(srcDirFile); // error: undo change @@ -252,6 +254,15 @@ public void onMoveOrCopyDirectoryPick(boolean move, SelectedFiles selectedFiles, } } + @Override + protected int moveOrCopyFiles(final boolean move, String what, PhotoPropertiesDiffCopy exifChanges, + SelectedFiles fotos, File[] destFiles, + IProgessListener progessListener) { + int result = super.moveOrCopyFiles(move, what, exifChanges, fotos, destFiles, progessListener); + // api.setTransactionSuccessful(); + return result; + } + @NonNull public String getLastCopyToPath() { SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(mContext); @@ -265,7 +276,8 @@ private void setLastCopyToPath(String copyToPath) { edit.apply(); } - public boolean cmdDeleteFileWithQuestion(final SelectedFiles fotos) { + public boolean cmdDeleteFileWithQuestion(final SelectedFiles fotos, + final PhotoChangeNotifyer.PhotoChangedListener photoChangedListener) { String[] pathNames = fotos.getFileNames(); String errorMessage = checkWriteProtected(R.string.delete_menu_title, SelectedFiles.getFiles(pathNames)); @@ -296,6 +308,9 @@ public void onClick( final int id) { mActiveAlert = null; deleteFiles(fotos, null); + if (photoChangedListener != null) { + photoChangedListener.onNotifyPhotoChanged(); + } } } ) @@ -328,7 +343,7 @@ public int deleteFiles(SelectedFiles fotos, IProgessListener progessListener) { QueryParameter where = new QueryParameter(); FotoSql.setWhereSelectionPks (where, fotos.toIdString()); - FotoSql.deleteMedia("AndroidFileCommands.deleteFiles", mContext, where.toAndroidWhere(), null, true); + FotoSql.getMediaDBApi().deleteMedia("AndroidFileCommands.deleteFiles", where.toAndroidWhere(), null, true); } return deleteCount; } @@ -468,7 +483,7 @@ public int setGeo(double latitude, double longitude, SelectedFiles selectedItems } File file = files[i]; PhotoPropertiesUpdateHandler jpg = createWorkflow(null, dbgContext).saveLatLon(file, latitude, longitude); - resultFile += TagSql.updateDB(dbgContext, applicationContext, + resultFile += TagSql.updateDB(dbgContext, file.getAbsolutePath(), jpg, MediaFormatter.FieldID.latitude_longitude); itemcount++; addTransactionLog(selectedItems.getId(i), file.getAbsolutePath(), now, MediaTransactionLogEntryType.GPS, latLong); @@ -556,6 +571,10 @@ public PhotoPropertiesBulkUpdateService createWorkflow(TransactionLoggerBase log return new AndroidPhotoPropertiesBulkUpdateService(mContext, logger, dbgContext); } + @Override + protected TransactionLoggerBase createTransactionLogger(long now) { + return new AndroidTransactionLogger(this, now); + } /** adds android database specific logging to base implementation */ @Override @@ -571,6 +590,9 @@ public void addTransactionLog( ContentValues values = TransactionLogSql.set(null, currentMediaID, fileFullPath, modificationDate, mediaTransactionLogEntryType, commandData); db.insert(TransactionLogSql.TABLE, null, values); + if (Global.debugEnabledSql) { + Log.i(FotoSql.LOG_TAG, "addTransactionLog: " + values); + } } } } diff --git a/app/src/main/java/de/k3b/android/util/IntentUtil.java b/app/src/main/java/de/k3b/android/util/IntentUtil.java index 945a335a..9bf1ab41 100644 --- a/app/src/main/java/de/k3b/android/util/IntentUtil.java +++ b/app/src/main/java/de/k3b/android/util/IntentUtil.java @@ -65,7 +65,7 @@ public static File getExistingFileOrNull(Context context, Uri uri) { path = uri.getPath(); } else if ("content".equals(scheme)) { // try to translate via media db - path = FotoSql.execGetFotoPath(context, uri); + path = FotoSql.execGetFotoPath(uri); if (path != null) { if (Global.debugEnabled) { Log.i(Global.LOG_CONTEXT, "Translate from '" + uri + diff --git a/app/src/main/java/de/k3b/android/util/LogCat.java b/app/src/main/java/de/k3b/android/util/LogCat.java index ba82e7af..81eaee9a 100644 --- a/app/src/main/java/de/k3b/android/util/LogCat.java +++ b/app/src/main/java/de/k3b/android/util/LogCat.java @@ -19,6 +19,7 @@ package de.k3b.android.util; +import android.app.Activity; import android.util.Log; import java.io.File; @@ -57,7 +58,7 @@ public LogCat(String... tags) { Thread.setDefaultUncaughtExceptionHandler(this); } - public abstract void saveToFile(); + public abstract void saveToFile(Activity activity); protected void saveLogCat(File logFile, OutputStream outputStream, String[] tags) { StringBuilder cmdline = new StringBuilder(); @@ -122,7 +123,7 @@ public void uncaughtException(Thread thread, Throwable ex) { try { // Do your stuff with the exception Log.e(mTags[0],"LogCat.uncaughtException " + ex, ex); - saveToFile(); + saveToFile(null); } catch (Exception e) { /* Ignore */ } finally { diff --git a/app/src/main/java/de/k3b/android/util/PhotoChangeNotifyer.java b/app/src/main/java/de/k3b/android/util/PhotoChangeNotifyer.java new file mode 100644 index 00000000..da422188 --- /dev/null +++ b/app/src/main/java/de/k3b/android/util/PhotoChangeNotifyer.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019-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.util; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.provider.MediaStore; + +import de.k3b.android.androFotoFinder.queries.FotoSql; + +/** + * implements hiding Android specific Data change notifikation + **/ +public class PhotoChangeNotifyer { + public static PhotoChangedListener photoChangedListener = null; + + public static void setPhotoChangedListener(PhotoChangedListener photoChangedListener) { + PhotoChangeNotifyer.photoChangedListener = photoChangedListener; + } + + public static void notifyPhotoChanged(Context context, PhotoChangedListener adapter) { + if (adapter != null) adapter.onNotifyPhotoChanged(); + + if (false) { + context.getApplicationContext().getContentResolver().notifyChange(FotoSql.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE, null, false); + } + } + + public static final void registerContentObserver(Context context, ContentObserver observer) { + final ContentResolver contentResolver = context.getApplicationContext().getContentResolver(); + contentResolver.registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, observer); + contentResolver.registerContentObserver(MediaStore.Files.getContentUri("external"), true, observer); + } + + public static void unregisterContentObserver(Context context, ContentObserver instance) { + context.getApplicationContext().getContentResolver().unregisterContentObserver(instance); + } + + public interface PhotoChangedListener { + void onNotifyPhotoChanged(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/k3b/android/util/PhotoPropertiesMediaFilesScanner.java b/app/src/main/java/de/k3b/android/util/PhotoPropertiesMediaFilesScanner.java index 8b620476..0b8b9a6e 100644 --- a/app/src/main/java/de/k3b/android/util/PhotoPropertiesMediaFilesScanner.java +++ b/app/src/main/java/de/k3b/android/util/PhotoPropertiesMediaFilesScanner.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2018 by k3b. + * Copyright (c) 2015-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -45,6 +45,7 @@ import de.k3b.android.androFotoFinder.Global; import de.k3b.android.androFotoFinder.media.PhotoPropertiesMediaDBContentValues; import de.k3b.android.androFotoFinder.queries.FotoSql; +import de.k3b.android.androFotoFinder.queries.IMediaRepositoryApi; import de.k3b.android.androFotoFinder.tagDB.TagSql; import de.k3b.database.QueryParameter; import de.k3b.geo.api.GeoPointDto; @@ -52,9 +53,9 @@ import de.k3b.io.FileUtils; import de.k3b.io.VISIBILITY; import de.k3b.media.IPhotoProperties; +import de.k3b.media.PhotoPropertiesChainReader; import de.k3b.media.PhotoPropertiesUtil; import de.k3b.media.PhotoPropertiesXmpSegment; -import de.k3b.media.PhotoPropertiesChainReader; import de.k3b.tagDB.TagRepository; /** @@ -75,15 +76,15 @@ abstract public class PhotoPropertiesMediaFilesScanner { protected static final String DB_LATITUDE = MediaStore.Images.Media.LATITUDE; */ - private static final String DB_SIZE = MediaStore.MediaColumns.SIZE; + protected static final String DB_TITLE = FotoSql.SQL_COL_EXT_TITLE; private static final String DB_WIDTH = MediaStore.MediaColumns.WIDTH; private static final String DB_HEIGHT = MediaStore.MediaColumns.HEIGHT; private static final String DB_MIME_TYPE = MediaStore.MediaColumns.MIME_TYPE; protected static final String DB_ORIENTATION = MediaStore.Images.Media.ORIENTATION; - protected static final String DB_TITLE = MediaStore.MediaColumns.TITLE; + protected static final String DB_DATA = FotoSql.SQL_COL_PATH; // _data protected static final String DB_DISPLAY_NAME = MediaStore.MediaColumns.DISPLAY_NAME; - protected static final String DB_DATA = MediaStore.MediaColumns.DATA; + private static final String DB_SIZE = FotoSql.SQL_COL_SIZE; public static final int DEFAULT_SCAN_DEPTH = 22; public static final String MEDIA_IGNORE_FILENAME = FileUtils.MEDIA_IGNORE_FILENAME; // MediaStore.MEDIA_IGNORE_FILENAME; @@ -149,7 +150,7 @@ public static int hideFolderMedia(Activity context, String path) { if (Global.debugEnabled) { Log.i(Global.LOG_CONTEXT, CONTEXT + " hideFolderMedia: delete from media db " + path + "/**"); } - result = FotoSql.execDeleteByPath(CONTEXT + " hideFolderMedia", context, path, VISIBILITY.PRIVATE_PUBLIC); + result = FotoSql.execDeleteByPath(CONTEXT + " hideFolderMedia", path, VISIBILITY.PRIVATE_PUBLIC); if (result > 0) { PhotoPropertiesMediaFilesScanner.notifyChanges(context, "hide " + path + "/**"); } @@ -159,19 +160,27 @@ public static int hideFolderMedia(Activity context, String path) { } public int updateMediaDatabase_Android42(Context context, String[] oldPathNames, String... newPathNames) { - final boolean hasNew = excludeNomediaFiles(newPathNames) > 0; - final boolean hasOld = excludeNomediaFiles(oldPathNames) > 0; - int result = 0; - - if (hasNew && hasOld) { - result = renameInMediaDatabase(context, oldPathNames, newPathNames); - } else if (hasOld) { - result = deleteInMediaDatabase(context, oldPathNames); - } if (hasNew) { - result = insertIntoMediaDatabase(context, newPathNames); + IMediaRepositoryApi api = FotoSql.getMediaDBApi(); + try { + api.beginTransaction(); + final boolean hasNew = excludeNomediaFiles(newPathNames) > 0; + final boolean hasOld = excludeNomediaFiles(oldPathNames) > 0; + int result = 0; + + if (hasNew && hasOld) { + result = renameInMediaDatabase(context, oldPathNames, newPathNames); + } else if (hasOld) { + result = deleteInMediaDatabase(context, oldPathNames); + } + if (hasNew) { + result = insertIntoMediaDatabase(context, newPathNames); + } + TagSql.fixPrivate(); + api.setTransactionSuccessful(); + return result; + } finally { + api.endTransaction(); } - TagSql.fixPrivate(context); - return result; } /** @@ -207,7 +216,7 @@ private int insertIntoMediaDatabase(Context context, String[] newPathNames) { Log.i(Global.LOG_CONTEXT, CONTEXT + "A42 scanner starting with " + newPathNames.length + " files " + newPathNames[0] + "..."); } - Map inMediaDb = FotoSql.execGetPathIdMap(context.getApplicationContext(), newPathNames); + Map inMediaDb = FotoSql.execGetPathIdMap(newPathNames); for (String fileName : newPathNames) { if (fileName != null) { @@ -232,8 +241,8 @@ public Long insertOrUpdateMediaDatabase(String dbgContext, Context context, if ((currentJpgFile != null) && currentJpgFile.exists() && currentJpgFile.canRead()) { ContentValues values = createDefaultContentValues(); getExifFromFile(values, currentJpgFile); - Long result = FotoSql.insertOrUpdateMediaDatabase( - dbgContext, context, dbUpdateFilterJpgFullPathName, + Long result = FotoSql.getMediaDBApi().insertOrUpdateMediaDatabase( + dbgContext, dbUpdateFilterJpgFullPathName, values, VISIBILITY.PRIVATE_PUBLIC, updateSuccessValue); return result; @@ -248,7 +257,7 @@ private int deleteInMediaDatabase(Context context, String[] oldPathNames) { if ((oldPathNames != null) && (oldPathNames.length > 0)) { String sqlWhere = FotoSql.getWhereInFileNames(oldPathNames); try { - modifyCount = FotoSql.deleteMedia(CONTEXT + "deleteInMediaDatabase", context, sqlWhere, null, true); + modifyCount = FotoSql.getMediaDBApi().deleteMedia(CONTEXT + "deleteInMediaDatabase", sqlWhere, null, true); if (Global.debugEnabled) { Log.d(Global.LOG_CONTEXT, CONTEXT + "deleteInMediaDatabase(len=" + oldPathNames.length + ", files='" + oldPathNames[0] + "'...) result count=" + modifyCount); } @@ -275,7 +284,10 @@ private int renameInMediaDatabase(Context context, String[] oldPathNames, String String newPathName = newPathNames[i]; if ((oldPathName != null) && (newPathName != null)) { - old2NewFileNames.put(oldPathName, newPathName); + //!!! ?seiteneffekt update other fields? + if (oldPathName.compareToIgnoreCase(newPathName) != 0) { + old2NewFileNames.put(oldPathName, newPathName); + } } else if (oldPathName != null) { deleteFileNames.add(oldPathName); } else if (newPathName != null) { @@ -300,7 +312,7 @@ private int renameInMediaDatabase(Context context, Map old2NewFi Cursor c = null; try { - c = FotoSql.createCursorForQuery(null, "renameInMediaDatabase", context, query, VISIBILITY.PRIVATE_PUBLIC); + c = FotoSql.getMediaDBApi().createCursorForQuery(null, "renameInMediaDatabase", query, VISIBILITY.PRIVATE_PUBLIC, null); int pkColNo = c.getColumnIndex(FotoSql.SQL_COL_PK); int pathColNo = c.getColumnIndex(FotoSql.SQL_COL_PATH); while (c.moveToNext()) { @@ -431,7 +443,7 @@ public int updatePathRelatedFields(Context context, Cursor cursor, String newAbs String oldAbsolutePath = cursor.getString(columnIndexPath); int id = cursor.getInt(columnIndexPk); setPathRelatedFieldsIfNeccessary(values, newAbsolutePath, oldAbsolutePath); - return FotoSql.execUpdate("updatePathRelatedFields", context, id, values); + return FotoSql.getMediaDBApi().execUpdate("updatePathRelatedFields", id, values); } /** sets the path related fields */ @@ -463,7 +475,7 @@ private int update_Android42(String dbgContext, Context context, long id, File f if ((file != null) && file.exists() && file.canRead()) { ContentValues values = createDefaultContentValues(); getExifFromFile(values, file); - return FotoSql.execUpdate(dbgContext, context, id, values); + return FotoSql.getMediaDBApi().execUpdate(dbgContext, id, values); } return 0; } @@ -487,7 +499,7 @@ private int insert_Android42(String dbgContext, Context context, File file) { FotoSql.addDateAdded(values); getExifFromFile(values, file); - return (null != FotoSql.execInsert(dbgContext, context, values)) ? 1 : 0; + return (null != FotoSql.getMediaDBApi().execInsert(dbgContext, values)) ? 1 : 0; } return 0; } diff --git a/app/src/main/java/de/k3b/android/util/PhotoPropertiesMediaFilesScannerAsyncTask.java b/app/src/main/java/de/k3b/android/util/PhotoPropertiesMediaFilesScannerAsyncTask.java index 2d2439bf..0fab7106 100644 --- a/app/src/main/java/de/k3b/android/util/PhotoPropertiesMediaFilesScannerAsyncTask.java +++ b/app/src/main/java/de/k3b/android/util/PhotoPropertiesMediaFilesScannerAsyncTask.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2019 by k3b. + * Copyright (c) 2016-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -50,17 +50,13 @@ protected Integer doInBackground(String[]... pathNames) { return mScanner.updateMediaDatabase_Android42(mContext, pathNames[0], pathNames[1]); } - @Override - protected void onPostExecute(Integer modifyCount) { - super.onPostExecute(modifyCount); - String message = this.mContext.getString(R.string.scanner_update_result_format, modifyCount); - Toast.makeText(this.mContext, message, Toast.LENGTH_LONG).show(); - if (Global.debugEnabled) { - Log.i(Global.LOG_CONTEXT, CONTEXT + "A42 scanner finished: " + message); - } - + private static void notifyIfThereAreChanges(Integer modifyCount, Context context, String why) { if (modifyCount > 0) { - PhotoPropertiesMediaFilesScanner.notifyChanges(mContext, mWhy); + PhotoPropertiesMediaFilesScanner.notifyChanges(context, why); + if (PhotoChangeNotifyer.photoChangedListener != null) { + PhotoChangeNotifyer.photoChangedListener.onNotifyPhotoChanged(); + } + } } @@ -73,12 +69,22 @@ public static void updateMediaDBInBackground(PhotoPropertiesMediaFilesScanner sc } else { // Continute in background task int modifyCount = scanner.updateMediaDatabase_Android42(context.getApplicationContext(), oldPathNames, newPathNames); - if (modifyCount > 0) { - PhotoPropertiesMediaFilesScanner.notifyChanges(context, why + " within current non-gui-task"); - } + notifyIfThereAreChanges(modifyCount, context, why + " within current non-gui-task"); } } + @Override + protected void onPostExecute(Integer modifyCount) { + super.onPostExecute(modifyCount); + String message = this.mContext.getString(R.string.scanner_update_result_format, modifyCount); + Toast.makeText(this.mContext, message, Toast.LENGTH_LONG).show(); + if (Global.debugEnabled) { + Log.i(Global.LOG_CONTEXT, CONTEXT + "A42 scanner finished: " + message); + } + + notifyIfThereAreChanges(modifyCount, mContext, mWhy); + } + /** return true if this is executed in the gui thread */ private static boolean isGuiThread() { return (Looper.myLooper() == Looper.getMainLooper()); diff --git a/app/src/main/java/de/k3b/android/widget/BaseQueryActivity.java b/app/src/main/java/de/k3b/android/widget/BaseQueryActivity.java index 9b7635b3..e9e17774 100644 --- a/app/src/main/java/de/k3b/android/widget/BaseQueryActivity.java +++ b/app/src/main/java/de/k3b/android/widget/BaseQueryActivity.java @@ -55,6 +55,7 @@ import de.k3b.android.androFotoFinder.tagDB.TagSql; import de.k3b.android.androFotoFinder.tagDB.TagsPickerFragment; import de.k3b.android.osmdroid.OsmdroidUtil; +import de.k3b.android.util.PhotoChangeNotifyer; import de.k3b.android.util.PhotoPropertiesMediaFilesScanner; import de.k3b.database.QueryParameter; import de.k3b.io.AlbumFile; @@ -673,8 +674,8 @@ public boolean onOk(List addNames, List removeNames) { protected void onCreate(Bundle savedInstanceState) { Global.debugMemory(mDebugPrefix, "onCreate"); super.onCreate(savedInstanceState); - this.getContentResolver().registerContentObserver(FotoSql.SQL_TABLE_EXTERNAL_CONTENT_URI, true, mMediaObserverDirectory); - this.getContentResolver().registerContentObserver(FotoSql.SQL_TABLE_EXTERNAL_CONTENT_URI_FILE, true, mMediaObserverDirectory); + PhotoChangeNotifyer.registerContentObserver(this, mMediaObserverDirectory); + } protected void onCreateData(Bundle savedInstanceState) { @@ -966,7 +967,7 @@ public void onLowMemory() { @Override protected void onDestroy() { super.onDestroy(); - this.getContentResolver().unregisterContentObserver(mMediaObserverDirectory); + PhotoChangeNotifyer.unregisterContentObserver(this, mMediaObserverDirectory); this.mGalleryQueryParameter.mGalleryContentBaseQuery = null; invalidateDirectories(mDebugPrefix + "#onDestroy"); } diff --git a/app/src/main/java/de/k3b/android/widget/LocalizedActivity.java b/app/src/main/java/de/k3b/android/widget/LocalizedActivity.java index a2b5e696..6ee9b8fb 100644 --- a/app/src/main/java/de/k3b/android/widget/LocalizedActivity.java +++ b/app/src/main/java/de/k3b/android/widget/LocalizedActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2018 by k3b. + * Copyright (c) 2015-2019 by k3b. * * This file is part of AndroFotoFinder and of ToGoZip. * @@ -41,26 +41,28 @@ * Created by k3b on 07.01.2016. */ public abstract class LocalizedActivity extends ActivityWithCallContext { + /** + * if this.recreationId != LocalizedActivity.currentRecreationId : activity must be recreated in on resume + */ + private static int currentRecreationId = 0; + private int recreationId = 0; + /** if myLocale != Locale.Default : activity must be recreated in on resume */ private Locale myLocale = null; + /** + * All activities will be recreated in on resume. I.E. after basic configuration change. + */ + public static void setMustRecreate() { + LocalizedActivity.currentRecreationId++; + } + @Override protected void onCreate(Bundle savedInstanceState) { fixLocale(this); super.onCreate(savedInstanceState); } - @Override - protected void onResume() { - super.onResume(); - - // Locale has changed by other Activity ? - if ((myLocale != null) && (myLocale.getLanguage() != Locale.getDefault().getLanguage())) { - myLocale = null; - recreate(LocalizedActivity.this); - } - } - /** * Set Activity-s locale to SharedPreferences-setting. * Must be called before @@ -85,11 +87,29 @@ public static void fixLocale(Context context) { // recreate(); if (context instanceof LocalizedActivity) { - ((LocalizedActivity) context).myLocale = locale; + final LocalizedActivity localizedActivity = (LocalizedActivity) context; + localizedActivity.myLocale = locale; + localizedActivity.recreationId = LocalizedActivity.currentRecreationId; } } } + @Override + protected void onResume() { + super.onResume(); + + // Locale has changed by other Activity ? + if (mustRecreate()) { + myLocale = null; + recreate(LocalizedActivity.this); + } + } + + protected boolean mustRecreate() { + return ((this.recreationId != LocalizedActivity.currentRecreationId) || + (this.myLocale != null) && (this.myLocale.getLanguage() != Locale.getDefault().getLanguage())); + } + /** force all open activity to recreate */ public static void recreate(Activity child) { Activity context = child; diff --git a/app/src/main/java/de/k3b/android/widget/ProgressActivity.java b/app/src/main/java/de/k3b/android/widget/ProgressActivity.java new file mode 100644 index 00000000..1a1fa1c8 --- /dev/null +++ b/app/src/main/java/de/k3b/android/widget/ProgressActivity.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2019 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.widget; + +import android.app.Activity; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; + +import de.k3b.android.androFotoFinder.Global; +import de.k3b.android.androFotoFinder.R; +import de.k3b.zip.LibZipGlobal; + +public abstract class ProgressActivity extends LocalizedActivity { + private static String mDebugPrefix = "ProgressActivity: "; + + abstract protected ProgressableAsyncTask getAsyncTask(); + + abstract protected void setAsyncTask(ProgressableAsyncTask asyncTask); + + @Override + protected void onDestroy() { + setAsyncTaskProgessReceiver(mDebugPrefix + "onDestroy ", null); + super.onDestroy(); + } + + @Override + protected void onResume() { + setAsyncTaskProgessReceiver(mDebugPrefix + "onResume ", this); + Global.debugMemory(mDebugPrefix, "onResume"); + super.onResume(); + + } + + /** + * (Re-)Connects this activity back with static asyncTask + */ + protected void setAsyncTaskProgessReceiver(String why, Activity progressReceiver) { + boolean isActive = ProgressableAsyncTask.isActive(getAsyncTask()); + boolean running = (progressReceiver != null) && isActive; + + String debugContext = why + mDebugPrefix + " setBackupAsyncTaskProgessReceiver isActive=" + isActive + + ", running=" + running + + " "; + + if (getAsyncTask() != null) { + final ProgressBar progressBar = (ProgressBar) this.findViewById(R.id.progressBar); + final TextView status = (TextView) this.findViewById(R.id.lbl_status); + final Button buttonCancel = (Button) this.findViewById(R.id.cmd_cancel); + + // setVisibility(running, progressBar, buttonCancel); + + if (running) { + getAsyncTask().setContext(debugContext, this, progressBar, status); + final String _debugContext = debugContext; + buttonCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (LibZipGlobal.debugEnabled) { + Log.d(LibZipGlobal.LOG_TAG, mDebugPrefix + " button Cancel backup pressed initialized by " + _debugContext); + } + getAsyncTask().cancel(false); + buttonCancel.setVisibility(View.INVISIBLE); + } + }); + + } else { + getAsyncTask().setContext(debugContext, null, null, null); + buttonCancel.setOnClickListener(null); + if (!isActive) { + setAsyncTask(null); + } + } + } + } +} diff --git a/app/src/main/java/de/k3b/android/widget/ProgressableAsyncTask.java b/app/src/main/java/de/k3b/android/widget/ProgressableAsyncTask.java new file mode 100644 index 00000000..ca74abd7 --- /dev/null +++ b/app/src/main/java/de/k3b/android/widget/ProgressableAsyncTask.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2019 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.widget; + +import android.app.Activity; +import android.content.Intent; +import android.os.AsyncTask; +import android.util.Log; +import android.widget.ProgressBar; +import android.widget.TextView; + +import java.util.concurrent.atomic.AtomicBoolean; + +import de.k3b.android.androFotoFinder.Common; +import de.k3b.android.androFotoFinder.Global; +import de.k3b.io.IProgessListener; +import de.k3b.zip.LibZipGlobal; +import de.k3b.zip.ProgressFormatter; + +public abstract class ProgressableAsyncTask extends AsyncTask implements IProgessListener { + protected final ProgressFormatter formatter = new ProgressFormatter(); + private final String mDebugPrefix = "ProgressableAsyncTask "; + protected Activity activity; + protected AtomicBoolean isActive = new AtomicBoolean(true); + // last known number of items to be processed + protected int lastSize = 0; + private ProgressBar mProgressBar = null; + private TextView status; + + public static boolean isActive(ProgressableAsyncTask backupAsyncTask) { + return (backupAsyncTask != null) && (backupAsyncTask.isActive.get()); + } + + /** + * (Re-)Attach owning Activity to BackupAsyncTask + * (i.e. after Device rotation + * + * @param why + * @param activity new owner + * @param progressBar To be updated while compression task is running + * @param status To be updated while compression task is running + */ + public void setContext(String why, Activity activity, ProgressBar progressBar, TextView status) { + if (LibZipGlobal.debugEnabled) { + Log.d(LibZipGlobal.LOG_TAG, mDebugPrefix + why + " setContext " + activity); + } + this.activity = activity; + mProgressBar = progressBar; + this.status = status; + } + + /** + * called on error + */ + @Override + protected void onCancelled() { + if (activity != null) { + if (LibZipGlobal.debugEnabled || Global.debugEnabled) { + Log.d(LibZipGlobal.LOG_TAG, activity.getClass().getSimpleName() + ": " + activity.getText(android.R.string.cancel)); + } + + finish(mDebugPrefix + " onCancelled ", Activity.RESULT_CANCELED, activity.getText(android.R.string.cancel)); + } + } + + protected void finish(String why, int resultCode, CharSequence message) { + if (message != null) { + Intent intent = new Intent(); + intent.putExtra(Common.EXTRA_TITLE, message); + activity.setResult(resultCode, intent); + } else { + activity.setResult(resultCode); + } + activity.finish(); + + setContext(why + mDebugPrefix + " finish ", null, null, null); + + } + + /** + * de.k3b.io.IProgessListener: + *

+ * called every time when command makes some little progress in non gui thread. + * return true to continue + */ + @Override + public boolean onProgress(int itemcount, int size, String message) { + publishProgress(new ProgressData(itemcount, size, message)); + + final boolean cancelled = this.isCancelled(); + if (cancelled) { + if (LibZipGlobal.debugEnabled) { + Log.d(LibZipGlobal.LOG_TAG, mDebugPrefix + " cancel backup pressed "); + } + } + return !cancelled; + } + + + /** + * called from {@link AsyncTask} in gui task + */ + @Override + protected void onProgressUpdate(ProgressData... values) { + final ProgressData progressData = values[0]; + if (mProgressBar != null) { + int size = progressData.size; + if ((size != 0) && (size > lastSize)) { + mProgressBar.setMax(size); + lastSize = size; + } + mProgressBar.setProgress(progressData.itemcount); + } + if (this.status != null) { + this.status.setText(formatter.format(progressData.itemcount, progressData.size)); + // values[0].message); + } + } +} + +/** + * ProgressData: Text that can be displayed as progress message + * in owning Activity. Translated from android independant {@link de.k3b.io.IProgessListener} + */ +class ProgressData { + final int itemcount; + final int size; + final String message; + + ProgressData(int itemcount, int size, String message) { + this.itemcount = itemcount; + this.size = size; + this.message = message; + } +} + diff --git a/app/src/main/java/de/k3b/android/widget/UpdateTask.java b/app/src/main/java/de/k3b/android/widget/UpdateTask.java index 926b0f58..515f3b07 100644 --- a/app/src/main/java/de/k3b/android/widget/UpdateTask.java +++ b/app/src/main/java/de/k3b/android/widget/UpdateTask.java @@ -1,8 +1,22 @@ -package de.k3b.android.widget; - -/** - * Created by EVE on 20.11.2017. +/* + * Copyright (c) 2017-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.widget; import android.app.Activity; import android.util.Log; @@ -10,6 +24,8 @@ import java.io.File; import de.k3b.android.androFotoFinder.Global; +import de.k3b.android.androFotoFinder.queries.FotoSql; +import de.k3b.android.androFotoFinder.queries.IMediaRepositoryApi; import de.k3b.android.util.AndroidFileCommands; import de.k3b.io.IProgessListener; import de.k3b.io.PhotoAutoprocessingDto; @@ -34,15 +50,15 @@ public UpdateTask(int resIdDlgTitle, Activity ctx, AndroidFileCommands cmd, } public UpdateTask(int resIdDlgTitle, Activity ctx, AndroidFileCommands cmd, - boolean move, File destDirFolder, - PhotoAutoprocessingDto autoProccessData) { + boolean move, File destDirFolder, + PhotoAutoprocessingDto autoProccessData) { this(resIdDlgTitle, ctx, cmd, null, move, destDirFolder, autoProccessData); } private UpdateTask(int resIdDlgTitle, Activity ctx, AndroidFileCommands cmd, - PhotoPropertiesDiffCopy exifChanges, - boolean move, File destDirFolder, - PhotoAutoprocessingDto autoProccessData) { + PhotoPropertiesDiffCopy exifChanges, + boolean move, File destDirFolder, + PhotoAutoprocessingDto autoProccessData) { super(ctx, resIdDlgTitle); this.exifChanges = exifChanges; this.cmd = cmd; @@ -55,17 +71,27 @@ private UpdateTask(int resIdDlgTitle, Activity ctx, AndroidFileCommands cmd, protected Integer doInBackground(SelectedFiles... params) { publishProgress("..."); + int result = 0; + SelectedFiles items = params[0]; if (exifChanges != null) { - SelectedFiles items = params[0]; - - return cmd.applyExifChanges(move, exifChanges, items, this); + if (true) { + result = cmd.applyExifChanges(move, exifChanges, items, null); + } else { + // disabled: does not work because of overlapping transactions + IMediaRepositoryApi api = FotoSql.getMediaDBApi(); + try { + api.beginTransaction(); + result = cmd.applyExifChanges(true, exifChanges, items, null); + api.setTransactionSuccessful(); + } finally { + api.endTransaction(); + } + } } else { - SelectedFiles items = params[0]; - - return cmd.moveOrCopyFilesTo(move, items, destDirFolder, autoProccessData, this); - + result = cmd.moveOrCopyFilesTo(move, items, destDirFolder, autoProccessData, this); } + return result; } @Override diff --git a/app/src/main/res/menu/menu_ao10.xml b/app/src/main/res/menu/menu_ao10.xml new file mode 100644 index 00000000..f2749423 --- /dev/null +++ b/app/src/main/res/menu/menu_ao10.xml @@ -0,0 +1,10 @@ + +

+ + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_gallery_multiselect_mode_all.xml b/app/src/main/res/menu/menu_gallery_multiselect_mode_all.xml index c3ca66b3..5f8e3f96 100644 --- a/app/src/main/res/menu/menu_gallery_multiselect_mode_all.xml +++ b/app/src/main/res/menu/menu_gallery_multiselect_mode_all.xml @@ -51,18 +51,6 @@ android:visible="true" android:orderInCategory="61" android:showAsAction="never" /> - - + + + + diff --git a/app/src/main/res/values/donottranslate_version.xml b/app/src/main/res/values/donottranslate_version.xml index 110c050d..06e83d18 100644 --- a/app/src/main/res/values/donottranslate_version.xml +++ b/app/src/main/res/values/donottranslate_version.xml @@ -24,9 +24,9 @@ this program. If not, see "(dev)" for development version @master --> - A Photo Manager (dev) - A Photo Viewer (dev) - A Photo Map (dev) - - " (dev)" + A Photo Manager + A Photo Viewer + A Photo Map + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 38ec4e98..8030dfd9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -279,5 +279,8 @@ You can undo hiding by calling the mediascanner from gallery-menu."
< ! - - what type of data should be saven ? - - > What: --> + + + (Re)Load Media Database diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 671db294..85fd213e 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -1,6 +1,6 @@ + + + + + + + + diff --git a/bug_report.md b/bug_report.md new file mode 100644 index 00000000..e69de29b diff --git a/fastlane/metadata/android/en-US/changelogs/42.txt b/fastlane/metadata/android/en-US/changelogs/42.txt index 823dc1c1..29dcda8b 100644 --- a/fastlane/metadata/android/en-US/changelogs/42.txt +++ b/fastlane/metadata/android/en-US/changelogs/42.txt @@ -1,2 +1,4 @@ Changes from 0.7.3 to 0.7.4 -Translation updates: pt-BR + +* #143 hotfix to reallow openstreetmap online map download +* Translation updates: pt-BR diff --git a/fotolib2/build.gradle b/fotolib2/build.gradle index ca7ab6a7..153d5154 100644 --- a/fotolib2/build.gradle +++ b/fotolib2/build.gradle @@ -25,7 +25,7 @@ dependencies { // compile 'com.adobe.xmp:xmpcore:6.1.10' // update for drewnoakes:metadata-extractor requires java-8 compiler does not run on my android-4.4. compile 'com.adobe.xmp:xmpcore:5.1.2' // current version for drewnoakes:metadata-extractor - // https://github.com/drewnoakes/metadata-extractor/wiki/GettingStarted licence: http://www.apache.org/licenses/LICENSE-2.0 + // https://github.com/drewnoakes/metadata-extractor/wiki/GettingStarted licence: https://www.apache.org/licenses/LICENSE-2.0 // 2.10.1 includes more recent xmpcore that generates java-8 bytecode not supported by android yet compile ('com.drewnoakes:metadata-extractor:2.10.1') { transitive = false } // 2.8.1 diff --git a/fotolib2/src/main/java/de/k3b/database/QueryParameter.java b/fotolib2/src/main/java/de/k3b/database/QueryParameter.java index f422e026..bbe10f0d 100644 --- a/fotolib2/src/main/java/de/k3b/database/QueryParameter.java +++ b/fotolib2/src/main/java/de/k3b/database/QueryParameter.java @@ -277,12 +277,7 @@ public QueryParameter addOrderBy(String... orders) { } public String toOrderBy() { - StringBuilder result = new StringBuilder(); - if (!Helper.append(result, null, mOrderBy, ", ", "", "")) { - return null; - } - - return result.toString(); + return Helper.toCommaSeperatedFieldListOrNull(mOrderBy); } /************************** end properties *********************/ @@ -336,6 +331,18 @@ public StringBuilder toParsableWhere(StringBuilder result) { return result; } + public String toWhere() { + return Helper.toSeperatedFieldListOrNull(mWhere, " AND "); + } + + public String toHaving() { + return Helper.toCommaSeperatedFieldListOrNull(mHaving); + } + + public String toGroupBy() { + return Helper.toCommaSeperatedFieldListOrNull(mGroupBy); + } + private static final String PARSER_KEYWORDS = ";FROM;QUERY-TYPE-ID;SELECT;WHERE;WHERE-PARAMETERS;GROUP-BY;HAVING;HAVING-PARAMETERS;ORDER-BY;"; public static QueryParameter parse(String stringToBeParsed) { @@ -503,7 +510,7 @@ private static String[] toList(List... lists) { return result; } - private static boolean append(StringBuilder result, String blockPrefix, List list, String delimiter, String before, String after) { + private static boolean append(StringBuilder result, String blockPrefix, List list, String seperator, String before, String after) { if (isNotEmpty(list)) { if (blockPrefix != null) { result.append(blockPrefix); @@ -512,13 +519,13 @@ private static boolean append(StringBuilder result, String blockPrefix, List list) { + return toSeperatedFieldListOrNull(list, ", "); + } + + private static String toSeperatedFieldListOrNull(List list, String seperator) { + if (isNotEmpty(list)) { + StringBuilder result = new StringBuilder(); + append(result, null, list, seperator, null, null); + return result.toString(); + } + return null; + } + private static boolean isNotEmpty(List list) { return (list != null) && (list.size() > 0); } diff --git a/fotolib2/src/main/java/de/k3b/io/FileCommands.java b/fotolib2/src/main/java/de/k3b/io/FileCommands.java index 75dc4392..d8ce6c08 100644 --- a/fotolib2/src/main/java/de/k3b/io/FileCommands.java +++ b/fotolib2/src/main/java/de/k3b/io/FileCommands.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2018 by k3b. + * Copyright (c) 2015-2020 by k3b. * * This file is part of AndroFotoFinder / #APhotoManager. * @@ -273,7 +273,7 @@ protected int moveOrCopyFiles(final boolean move, String what, PhotoPropertiesDi int pos = 0; long now = new Date().getTime(); MediaTransactionLogEntryType moveOrCopyCommand = (move) ? MediaTransactionLogEntryType.MOVE : MediaTransactionLogEntryType.COPY; - TransactionLoggerBase logger = (exifChanges == null) ? null : new TransactionLoggerBase(this, now); + TransactionLoggerBase logger = (exifChanges == null) ? null : createTransactionLogger(now); boolean sameFile; while (pos < fileCount) { @@ -372,6 +372,10 @@ protected int moveOrCopyFiles(final boolean move, String what, PhotoPropertiesDi return itemCount; } + protected TransactionLoggerBase createTransactionLogger(long now) { + return new TransactionLoggerBase(this, now); + } + private PhotoAutoprocessingDto getPhotoAutoprocessingDto(File destDirFolder) { PhotoAutoprocessingDto autoProccessData = null; try { diff --git a/fotolib2/src/main/java/de/k3b/media/PhotoPropertiesWrapper.java b/fotolib2/src/main/java/de/k3b/media/PhotoPropertiesWrapper.java index 01b6e22d..6fa4a4cf 100644 --- a/fotolib2/src/main/java/de/k3b/media/PhotoPropertiesWrapper.java +++ b/fotolib2/src/main/java/de/k3b/media/PhotoPropertiesWrapper.java @@ -29,7 +29,6 @@ * * Created by k3b on 09.10.2016. */ - public class PhotoPropertiesWrapper implements IPhotoProperties { protected final IPhotoProperties readChild; protected final IPhotoProperties writeChild; diff --git a/privacy.md b/privacy.md new file mode 100644 index 00000000..21998d7c --- /dev/null +++ b/privacy.md @@ -0,0 +1,25 @@ +# Privacy Policy + +This app "A Photo Manager" is an absolutely Open Source app. All services the software provides comes at no cost and is intended for use as is. + +## Data collection and Usage + +You can use this app to add personal informations (geo data and comments) to your local photos which will also been transfered into android-s local media database for faster search and that will also become visible to other android apps. + +This app will never transfer local photos or content of android-s local media database to any other computer/server. + +Other Android Apps like "goolge photos" might transfer the photos with personal informations to other computer/server. + +This app does not collect any other personal informoation. + +If you use the geographic map then map data may be fetched from OpenStreetMap servers so OpenStreetMap servers may be logging your ip address, date/time and the geographic area you are currently looking at. + +Each mapdata piece is only loaded once and then locally stored. + +## Changes to This Privacy Policy + +This privacy policy may be updated from time to time. Thus, it is advisable to review this document periodically for any changes. . These changes are effective immediately after the privacy policy document is updated + +## Contact Us + +If you have any questions or suggestions about this Privacy Policy, do not hesitate to contact the developer via [github](https://github.com/k3b/APhotoManager)