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 index d51ab1796..385e5ae7d 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt @@ -32,204 +32,205 @@ private const val RADIUS_STEP_SIZE_IN_METRES = 100 private const val MIN_NEARBY_RESULTS = 5 class FileProcessor - @Inject - constructor( - private val context: Context, - private val contentResolver: ContentResolver, - private val gpsCategoryModel: GpsCategoryModel, - private val depictsModel: DepictModel, - @param:Named("default_preferences") private val defaultKvStore: JsonKvStore, - private val apiCall: CategoryApi, - private val okHttpJsonApiClient: OkHttpJsonApiClient, - ) { - private val compositeDisposable = CompositeDisposable() +@Inject +constructor( + private val context: Context, + private val contentResolver: ContentResolver, + private val gpsCategoryModel: GpsCategoryModel, + private val depictsModel: DepictModel, + @param:Named("default_preferences") private val defaultKvStore: JsonKvStore, + private val apiCall: CategoryApi, + private val okHttpJsonApiClient: OkHttpJsonApiClient, +) { + private val compositeDisposable = CompositeDisposable() - fun cleanup() { - compositeDisposable.clear() - } + fun cleanup() { + compositeDisposable.clear() + } - /** - * Processes filePath coordinates, either from EXIF data or user location - */ - fun processFileCoordinates( - similarImageInterface: SimilarImageInterface, - filePath: String?, - inAppPictureLocation: LatLng?, - ): 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, inAppPictureLocation) - if (originalImageCoordinates.decimalCoords == null) { - // Find other photos taken around the same time which has gps coordinates - findOtherImages( - File(filePath), - similarImageInterface, - ) - } else { - prePopulateCategoriesAndDepictionsBy(originalImageCoordinates) - } - return originalImageCoordinates - } - - /** - * Gets EXIF Tags from preferences to be redacted. - * - * @return tags to be redacted - */ - 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 - */ - 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?) { + /** + * Processes filePath coordinates, either from EXIF data or user location + */ + fun processFileCoordinates( + similarImageInterface: SimilarImageInterface, + filePath: String?, + inAppPictureLocation: LatLng?, + ): ImageCoordinates { + val 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 { attributeName -> - exifInterface.setAttribute(tag, null).also { - Timber.d("Exif tag $tag with value $attributeName 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( - fileBeingProcessed: File, - similarImageInterface: SimilarImageInterface, - ) { - val oneHundredAndTwentySeconds = 120 * 1000L - // 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, - fileCoordinatesPair.second, - ) - } - } - - private fun readImageCoordinates(file: File) = - try { - /* Used null location as location for similar images captured before is not available - in case it is not present in the EXIF. */ - ImageCoordinates(contentResolver.openInputStream(Uri.fromFile(file))!!, null) + ExifInterface(filePath!!) } catch (e: IOException) { Timber.e(e) - try { - ImageCoordinates(file.absolutePath, null) - } catch (ex: IOException) { - Timber.e(ex) - null - } + 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 prePopulateCategoriesAndDepictionsBy(imageCoordinates: ImageCoordinates) { - requireNotNull(imageCoordinates.decimalCoords) - compositeDisposable.add( - apiCall - .request(imageCoordinates.decimalCoords!!) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .subscribe( - gpsCategoryModel::setCategoriesFromLocation, - { - Timber.e(it) - gpsCategoryModel.clear() - }, - ), + // Redact EXIF data as indicated in preferences. + redactExifTags(exifInterface, getExifTagsToRedact()) + Timber.d("Calling GPSExtractor") + val originalImageCoordinates = ImageCoordinates(exifInterface, inAppPictureLocation) + if (originalImageCoordinates.decimalCoords == null) { + // Find other photos taken around the same time which has gps coordinates + findOtherImages( + File(filePath), + similarImageInterface, ) + } else { + prePopulateCategoriesAndDepictionsBy(originalImageCoordinates) + } + return originalImageCoordinates + } - compositeDisposable.add( - suggestNearbyDepictions(imageCoordinates), - ) + /** + * Gets EXIF Tags from preferences to be redacted. + * + * @return tags to be redacted + */ + 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 + */ + 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 { attributeName -> + exifInterface.setAttribute(tag, "").also { + Timber.d("EXIF tag $tag removed or set to empty.") + } + + } + } + + /** + * Find other images around the same location that were taken within the last 20 sec + * + * @param originalImageCoordinates + * @param fileBeingProcessed + * @param similarImageInterface + */ + private fun findOtherImages( + fileBeingProcessed: File, + similarImageInterface: SimilarImageInterface, + ) { + val oneHundredAndTwentySeconds = 120 * 1000L + // 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, + fileCoordinatesPair.second, + ) + } + } + + private fun readImageCoordinates(file: File) = + try { + /* Used null location as location for similar images captured before is not available + in case it is not present in the EXIF. */ + ImageCoordinates(contentResolver.openInputStream(Uri.fromFile(file))!!, null) + } catch (e: IOException) { + Timber.e(e) + try { + ImageCoordinates(file.absolutePath, null) + } catch (ex: IOException) { + Timber.e(ex) + null + } } - private val radiiProgressionInMetres = - (DEFAULT_SUGGESTION_RADIUS_IN_METRES..MAX_SUGGESTION_RADIUS_IN_METRES step RADIUS_STEP_SIZE_IN_METRES) - - private fun suggestNearbyDepictions(imageCoordinates: ImageCoordinates): Disposable = - Observable - .fromIterable(radiiProgressionInMetres.map { it / 1000.0 }) - .concatMap { - Observable.fromCallable { - okHttpJsonApiClient.getNearbyPlaces( - imageCoordinates.latLng!!, - Locale.getDefault().language, - it, - ) - } - }.subscribeOn(Schedulers.io()) - .filter { it.size >= MIN_NEARBY_RESULTS } - .take(1) + /** + * 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 prePopulateCategoriesAndDepictionsBy(imageCoordinates: ImageCoordinates) { + requireNotNull(imageCoordinates.decimalCoords) + compositeDisposable.add( + apiCall + .request(imageCoordinates.decimalCoords!!) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) .subscribe( - { depictsModel.nearbyPlaces.offer(it) }, - { Timber.e(it) }, - ) + gpsCategoryModel::setCategoriesFromLocation, + { + Timber.e(it) + gpsCategoryModel.clear() + }, + ), + ) + + compositeDisposable.add( + suggestNearbyDepictions(imageCoordinates), + ) } + + private val radiiProgressionInMetres = + (DEFAULT_SUGGESTION_RADIUS_IN_METRES..MAX_SUGGESTION_RADIUS_IN_METRES step RADIUS_STEP_SIZE_IN_METRES) + + private fun suggestNearbyDepictions(imageCoordinates: ImageCoordinates): Disposable = + Observable + .fromIterable(radiiProgressionInMetres.map { it / 1000.0 }) + .concatMap { + Observable.fromCallable { + okHttpJsonApiClient.getNearbyPlaces( + imageCoordinates.latLng!!, + Locale.getDefault().language, + it, + ) + } + }.subscribeOn(Schedulers.io()) + .filter { it.size >= MIN_NEARBY_RESULTS } + .take(1) + .subscribe( + { depictsModel.nearbyPlaces.offer(it) }, + { Timber.e(it) }, + ) +}