From 4a287d3d9fd2d2f1788ebbe88a147e9834fac260 Mon Sep 17 00:00:00 2001 From: Sean Mac Gillicuddy Date: Thu, 19 Mar 2020 12:03:49 +0000 Subject: [PATCH] #3408 Refactoring the FileProcessor and GPSExtractor classes - convert FileProcessor to kotlin --- .../nrw/commons/upload/FileProcessor.java | 240 ------------------ .../free/nrw/commons/upload/FileProcessor.kt | 198 +++++++++++++++ .../nrw/commons/upload/ImageCoordinates.kt | 2 + 3 files changed, 200 insertions(+), 240 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java deleted file mode 100644 index 28fe666111..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java +++ /dev/null @@ -1,240 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.annotation.SuppressLint; -import android.content.ContentResolver; -import android.content.Context; -import android.net.Uri; -import androidx.exifinterface.media.ExifInterface; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.caching.CacheController; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.mwapi.CategoryApi; -import fr.free.nrw.commons.settings.Prefs; -import io.reactivex.Observable; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.io.File; -import java.io.IOException; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import javax.inject.Inject; -import javax.inject.Named; -import org.jetbrains.annotations.NotNull; -import timber.log.Timber; - -/** - * Processing of the image filePath that is about to be uploaded via ShareActivity is done here - */ -public class FileProcessor { - - private final Context context; - private final ContentResolver contentResolver; - private final CacheController cacheController; - private final GpsCategoryModel gpsCategoryModel; - private final JsonKvStore defaultKvStore; - private final CategoryApi apiCall; - - private final CompositeDisposable compositeDisposable = new CompositeDisposable(); - - @Inject - public FileProcessor(Context context, ContentResolver contentResolver, - CacheController cacheController, GpsCategoryModel gpsCategoryModel, - @Named("default_preferences") JsonKvStore defaultKvStore, CategoryApi apiCall) { - this.context = context; - this.contentResolver = contentResolver; - this.cacheController = cacheController; - this.gpsCategoryModel = gpsCategoryModel; - this.defaultKvStore = defaultKvStore; - this.apiCall = apiCall; - } - - public void cleanup() { - compositeDisposable.clear(); - } - - /** - * Processes filePath coordinates, either from EXIF data or user location - */ - ImageCoordinates processFileCoordinates(SimilarImageInterface similarImageInterface, - String filePath) { - ExifInterface exifInterface = null; - try { - exifInterface = new ExifInterface(filePath); - } catch (IOException e) { - Timber.e(e); - } - // Redact EXIF data as indicated in preferences. - redactExifTags(exifInterface, getExifTagsToRedact()); - - Timber.d("Calling GPSExtractor"); - ImageCoordinates originalImageExtractor = new ImageCoordinates(exifInterface); - String decimalCoords = originalImageExtractor.getDecimalCoords(); - if (decimalCoords == null || !originalImageExtractor.imageCoordsExists) { - //Find other photos taken around the same time which has gps coordinates - findOtherImages(originalImageExtractor, new File(filePath), - similarImageInterface);// Do not do repeat the process - } else { - useImageCoords(originalImageExtractor); - } - - return originalImageExtractor; - } - - /** - * Gets EXIF Tags from preferences to be redacted. - * - * @return tags to be redacted - */ - private Set getExifTagsToRedact() { - Set prefManageEXIFTags = defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS); - - Set redactTags = new HashSet<>(Arrays.asList( - context.getResources().getStringArray(R.array.pref_exifTag_values))); - Timber.d(redactTags.toString()); - - if (prefManageEXIFTags != null) { - redactTags.removeAll(prefManageEXIFTags); - } - - return redactTags; - } - - /** - * Redacts EXIF metadata as indicated in preferences. - * - * @param exifInterface ExifInterface object - * @param redactTags tags to be redacted - */ - private void redactExifTags(ExifInterface exifInterface, Set redactTags) { - compositeDisposable.add( - Observable.fromIterable(redactTags) - .flatMap(tag -> Observable.fromArray(FileMetadataUtils.getTagsFromPref(tag))) - .subscribe( - (tag) -> redactTag(exifInterface, tag), - Timber::d, - () -> save(exifInterface) - )); - } - - private void save(ExifInterface exifInterface) { - try { - exifInterface.saveAttributes(); - } catch (IOException e) { - Timber.w("EXIF redaction failed: %s", e.toString()); - } - } - - private void redactTag(ExifInterface exifInterface, String tag) { - Timber.d("Checking for tag: %s", tag); - String oldValue = exifInterface.getAttribute(tag); - if (oldValue != null && !oldValue.isEmpty()) { - Timber.d("Exif tag %s with value %s redacted.", tag, oldValue); - exifInterface.setAttribute(tag, null); - } - } - - /** - * Find other images around the same location that were taken within the last 20 sec - * - * @param originalImageExtractor - * @param fileBeingProcessed - * @param similarImageInterface - */ - private void findOtherImages( - ImageCoordinates originalImageExtractor, - File fileBeingProcessed, - SimilarImageInterface similarImageInterface) { - //Time when the original image was created - long timeOfCreation = fileBeingProcessed.lastModified(); - File[] files = fileBeingProcessed.getParentFile().listFiles(); - Timber.d("folderTime Number:" + files.length); - - for (File file : files) { - if (file.lastModified() - timeOfCreation <= (120 * 1000) - && file.lastModified() - timeOfCreation >= -(120 * 1000)) { - //Make sure the photos were taken within 20seconds - Timber.d("fild date:" + file.lastModified() + " time of creation" + timeOfCreation); - ImageCoordinates similarPictureExtractor = createGpsExtractor(file); - Timber.d("not null fild EXIF" + similarPictureExtractor.imageCoordsExists + " coords" - + similarPictureExtractor.getDecimalCoords()); - if (similarPictureExtractor.getDecimalCoords() != null - && similarPictureExtractor.imageCoordsExists) { - // Current image has gps coordinates and it's not current gps locaiton - Timber.d("This filePath has image coords:" + file.getAbsolutePath()); - similarImageInterface.showSimilarImageFragment( - fileBeingProcessed.getPath(), - file.getAbsolutePath(), - originalImageExtractor, - similarPictureExtractor - ); - break; - } - } - } - } - - @NotNull - private ImageCoordinates createGpsExtractor(File file) { - try { - return new ImageCoordinates(contentResolver.openInputStream(Uri.fromFile(file))); - } catch (IOException e) { - Timber.e(e); - try { - return new ImageCoordinates(file.getAbsolutePath()); - } catch (IOException ex) { - Timber.e(ex); - return null; - } - } - } - - /** - * Initiates retrieval of image coordinates or user coordinates, and caching of coordinates. Then - * initiates the calls to MediaWiki API through an instance of CategoryApi. - * - * @param imageCoordinates - */ - @SuppressLint("CheckResult") - public void useImageCoords(ImageCoordinates imageCoordinates) { - useImageCoords(imageCoordinates, imageCoordinates.getDecimalCoords()); - } - - private void useImageCoords(ImageCoordinates imageCoordinates, String decimalCoords) { - if (decimalCoords != null) { - Timber.d("Decimal coords of image: %s", decimalCoords); - Timber.d("is EXIF data present:" + imageCoordinates.imageCoordsExists + - " from findOther image"); - - // Only set cache for this point if image has coords - if (imageCoordinates.imageCoordsExists) { - cacheController - .setQtPoint(imageCoordinates.getDecLongitude(), imageCoordinates.getDecLatitude()); - } - - List displayCatList = cacheController.findCategory(); - boolean catListEmpty = displayCatList.isEmpty(); - - // If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories - if (catListEmpty) { - compositeDisposable.add(apiCall.request(decimalCoords) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .subscribe( - gpsCategoryModel::setCategoryList, - throwable -> { - Timber.e(throwable); - gpsCategoryModel.clear(); - } - )); - Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList); - } else { - Timber.d("Cache found, setting categoryList in model to %s", displayCatList); - gpsCategoryModel.setCategoryList(displayCatList); - } - } else { - Timber.d("EXIF: no coords"); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt new file mode 100644 index 0000000000..ec67d055b3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt @@ -0,0 +1,198 @@ +package fr.free.nrw.commons.upload + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import androidx.exifinterface.media.ExifInterface +import fr.free.nrw.commons.R +import fr.free.nrw.commons.caching.CacheController +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.mwapi.CategoryApi +import fr.free.nrw.commons.settings.Prefs +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import java.io.File +import java.io.IOException +import javax.inject.Inject +import javax.inject.Named + +/** + * Processing of the image filePath that is about to be uploaded via ShareActivity is done here + */ +class FileProcessor @Inject constructor( + private val context: Context, + private val contentResolver: ContentResolver, + private val cacheController: CacheController, + private val gpsCategoryModel: GpsCategoryModel, + @param:Named("default_preferences") private val defaultKvStore: JsonKvStore, + private val apiCall: CategoryApi +) { + private val compositeDisposable = CompositeDisposable() + fun cleanup() { + compositeDisposable.clear() + } + + /** + * Processes filePath coordinates, either from EXIF data or user location + */ + fun processFileCoordinates( + similarImageInterface: SimilarImageInterface, + filePath: String? + ): ImageCoordinates { + val exifInterface: ExifInterface? = try { + ExifInterface(filePath!!) + } catch (e: IOException) { + Timber.e(e) + null + } + // Redact EXIF data as indicated in preferences. + redactExifTags(exifInterface, getExifTagsToRedact()) + Timber.d("Calling GPSExtractor") + val originalImageCoordinates = ImageCoordinates(exifInterface) + if (originalImageCoordinates.decimalCoords == null ) { + //Find other photos taken around the same time which has gps coordinates + findOtherImages( + originalImageCoordinates, + File(filePath), + similarImageInterface + ) + } else { + useImageCoords(originalImageCoordinates) + } + return originalImageCoordinates + } + + /** + * Gets EXIF Tags from preferences to be redacted. + * + * @return tags to be redacted + */ + private fun getExifTagsToRedact(): Set { + val prefManageEXIFTags = + defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS) ?: emptySet() + val redactTags: Set = + context.resources.getStringArray(R.array.pref_exifTag_values).toSet() + return redactTags - prefManageEXIFTags + } + + /** + * Redacts EXIF metadata as indicated in preferences. + * + * @param exifInterface ExifInterface object + * @param redactTags tags to be redacted + */ + private fun redactExifTags( + exifInterface: ExifInterface?, + redactTags: Set + ) { + compositeDisposable.add( + Observable.fromIterable(redactTags) + .flatMap { Observable.fromArray(*FileMetadataUtils.getTagsFromPref(it)) } + .subscribe( + { redactTag(exifInterface, it) }, + { Timber.d(it) }, + { save(exifInterface) } + ) + ) + } + + private fun save(exifInterface: ExifInterface?) { + try { + exifInterface?.saveAttributes() + } catch (e: IOException) { + Timber.w("EXIF redaction failed: %s", e.toString()) + } + } + + private fun redactTag(exifInterface: ExifInterface?, tag: String) { + Timber.d("Checking for tag: %s", tag) + exifInterface?.getAttribute(tag) + ?.takeIf { it.isNotEmpty() } + ?.let { + exifInterface.setAttribute(tag, null).also { + Timber.d("Exif tag $tag with value $it redacted.") + } + } + } + + /** + * Find other images around the same location that were taken within the last 20 sec + * + * @param originalImageCoordinates + * @param fileBeingProcessed + * @param similarImageInterface + */ + private fun findOtherImages( + originalImageCoordinates: ImageCoordinates, + fileBeingProcessed: File, + similarImageInterface: SimilarImageInterface + ) { + val oneHundredAndTwentySeconds = 120 * 1000 + //Time when the original image was created + val timeOfCreation = fileBeingProcessed.lastModified() + LongRange + val timeOfCreationRange = + timeOfCreation - oneHundredAndTwentySeconds..timeOfCreation + oneHundredAndTwentySeconds + fileBeingProcessed.parentFile + .listFiles() + .asSequence() + .filter { it.lastModified() in timeOfCreationRange } + .map { Pair(it, readimageCoordinates(it)) } + .firstOrNull { it.second?.decimalCoords != null } + ?.let { fileCoordinatesPair -> + similarImageInterface.showSimilarImageFragment( + fileBeingProcessed.path, + fileCoordinatesPair.first.absolutePath, + originalImageCoordinates, + fileCoordinatesPair.second + ) + } + } + + private fun readimageCoordinates(file: File) = try { + ImageCoordinates(contentResolver.openInputStream(Uri.fromFile(file))) + } catch (e: IOException) { + Timber.e(e) + try { + ImageCoordinates(file.absolutePath) + } catch (ex: IOException) { + Timber.e(ex) + null + } + } + + /** + * Initiates retrieval of image coordinates or user coordinates, and caching of coordinates. Then + * initiates the calls to MediaWiki API through an instance of CategoryApi. + * + * @param imageCoordinates + */ + fun useImageCoords(imageCoordinates: ImageCoordinates) { + if (imageCoordinates.decimalCoords != null) { + cacheController.setQtPoint(imageCoordinates.decLongitude, imageCoordinates.decLatitude) + val displayCatList = cacheController.findCategory() + + // If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories + if (displayCatList.isEmpty()) { + compositeDisposable.add( + apiCall.request(imageCoordinates.decimalCoords) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe( + { gpsCategoryModel.categoryList = it }, + { + Timber.e(it) + gpsCategoryModel.clear() + } + ) + ) + Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList) + } else { + Timber.d("Cache found, setting categoryList in model to %s", displayCatList) + gpsCategoryModel.categoryList = displayCatList + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageCoordinates.kt b/app/src/main/java/fr/free/nrw/commons/upload/ImageCoordinates.kt index 7fb6ce5775..dc79f1673c 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ImageCoordinates.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/ImageCoordinates.kt @@ -2,6 +2,7 @@ package fr.free.nrw.commons.upload import androidx.exifinterface.media.ExifInterface import timber.log.Timber +import java.io.IOException import java.io.InputStream /** @@ -26,6 +27,7 @@ class ImageCoordinates internal constructor(exif: ExifInterface?) { * Construct from the file path of the image. * @param path file path of the image */ + @Throws(IOException::class) internal constructor(path: String) : this(ExifInterface(path))