#1084 crashfix for png with large exif via ExifInterface,

fixed ExifInterface disambiguation,
improved safe mode
This commit is contained in:
Thibault Deckers 2024-07-17 21:11:36 +02:00
parent a38c5b72ee
commit 2d143f139f
22 changed files with 75 additions and 55 deletions

View file

@ -211,6 +211,7 @@ dependencies {
implementation 'com.github.deckerst.mp4parser:isoparser:4cc0c5d06c' implementation 'com.github.deckerst.mp4parser:isoparser:4cc0c5d06c'
implementation 'com.github.deckerst.mp4parser:muxer:4cc0c5d06c' implementation 'com.github.deckerst.mp4parser:muxer:4cc0c5d06c'
implementation 'com.github.deckerst:pixymeta-android:9ec7097f17' implementation 'com.github.deckerst:pixymeta-android:9ec7097f17'
implementation project(':exifinterface')
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2'

View file

@ -12,7 +12,7 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.drew.metadata.file.FileTypeDirectory import com.drew.metadata.file.FileTypeDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.metadata.ExifInterfaceHelper import deckers.thibault.aves.metadata.ExifInterfaceHelper

View file

@ -6,7 +6,7 @@ import android.media.MediaMetadataRetriever
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.adobe.internal.xmp.XMPException import com.adobe.internal.xmp.XMPException
import com.adobe.internal.xmp.XMPMeta import com.adobe.internal.xmp.XMPMeta
import com.adobe.internal.xmp.XMPMetaFactory import com.adobe.internal.xmp.XMPMetaFactory

View file

@ -21,11 +21,13 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
private var knownEntries: Map<Long?, Int?>? = null private var knownEntries: Map<Long?, Int?>? = null
private var directory: String? = null private var directory: String? = null
private var safe: Boolean = false
init { init {
if (arguments is Map<*, *>) { if (arguments is Map<*, *>) {
knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to it.value as Int? }?.toMap() knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to it.value as Int? }?.toMap()
directory = arguments["directory"] as String? directory = arguments["directory"] as String?
safe = arguments.getOrDefault("safe", false) as Boolean
} }
} }
@ -59,7 +61,7 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
} }
private fun fetchAll() { private fun fetchAll() {
MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory) { success(it) } MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap(), directory, safe) { success(it) }
endOfStream() endOfStream()
} }

View file

@ -1,7 +1,7 @@
package deckers.thibault.aves.metadata package deckers.thibault.aves.metadata
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.drew.lang.Rational import com.drew.lang.Rational
import com.drew.metadata.Directory import com.drew.metadata.Directory
import com.drew.metadata.exif.ExifDirectoryBase import com.drew.metadata.exif.ExifDirectoryBase

View file

@ -2,7 +2,7 @@ package deckers.thibault.aves.metadata
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import deckers.thibault.aves.utils.FileUtils.transferFrom import deckers.thibault.aves.utils.FileUtils.transferFrom
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils

View file

@ -9,7 +9,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.adobe.internal.xmp.XMPMeta import com.adobe.internal.xmp.XMPMeta
import com.drew.imaging.jpeg.JpegSegmentType import com.drew.imaging.jpeg.JpegSegmentType
import com.drew.metadata.exif.ExifDirectoryBase import com.drew.metadata.exif.ExifDirectoryBase

View file

@ -5,7 +5,7 @@ import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.net.Uri import android.net.Uri
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.drew.metadata.avi.AviDirectory import com.drew.metadata.avi.AviDirectory
import com.drew.metadata.exif.ExifIFD0Directory import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.jpeg.JpegDirectory import com.drew.metadata.jpeg.JpegDirectory
@ -116,8 +116,8 @@ class SourceEntry {
// metadata retrieval // metadata retrieval
// expects entry with: uri, mimeType // expects entry with: uri, mimeType
// finds: width, height, orientation/rotation, date, title, duration // finds: width, height, orientation/rotation, date, title, duration
fun fillPreCatalogMetadata(context: Context): SourceEntry { fun fillPreCatalogMetadata(context: Context, safe: Boolean): SourceEntry {
if (isSvg) return this if (isSvg || safe) return this
if (isVideo) { if (isVideo) {
fillVideoByMediaMetadataRetriever(context) fillVideoByMediaMetadataRetriever(context)
if (isSized && hasDuration) return this if (isSized && hasDuration) return this

View file

@ -52,7 +52,7 @@ internal class FileImageProvider : ImageProvider() {
callback.onFailure(e) callback.onFailure(e)
} }
} }
entry.fillPreCatalogMetadata(context) entry.fillPreCatalogMetadata(context, safe = false)
if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) { if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
callback.onSuccess(entry.toMap()) callback.onSuccess(entry.toMap())

View file

@ -11,7 +11,7 @@ import android.net.Uri
import android.os.Binder import android.os.Binder
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy

View file

@ -51,8 +51,10 @@ class MediaStoreImageProvider : ImageProvider() {
context: Context, context: Context,
knownEntries: Map<Long?, Int?>, knownEntries: Map<Long?, Int?>,
directory: String?, directory: String?,
safe: Boolean,
handleNewEntry: NewEntryHandler, handleNewEntry: NewEntryHandler,
) { ) {
Log.d(LOG_TAG, "fetching all media store items for ${knownEntries.size} known entries, directory=$directory safe=$safe")
val isModified = fun(contentId: Long, dateModifiedSecs: Int): Boolean { val isModified = fun(contentId: Long, dateModifiedSecs: Int): Boolean {
val knownDate = knownEntries[contentId] val knownDate = knownEntries[contentId]
return knownDate == null || knownDate < dateModifiedSecs return knownDate == null || knownDate < dateModifiedSecs
@ -82,8 +84,8 @@ class MediaStoreImageProvider : ImageProvider() {
} else { } else {
handleNew = handleNewEntry handleNew = handleNewEntry
} }
fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs) fetchFrom(context, isModified, handleNew, IMAGE_CONTENT_URI, IMAGE_PROJECTION, selection, selectionArgs, safe = safe)
fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs) fetchFrom(context, isModified, handleNew, VIDEO_CONTENT_URI, VIDEO_PROJECTION, selection, selectionArgs, safe = safe)
} }
// the provided URI can point to the wrong media collection, // the provided URI can point to the wrong media collection,
@ -206,6 +208,7 @@ class MediaStoreImageProvider : ImageProvider() {
selection: String? = null, selection: String? = null,
selectionArgs: Array<String>? = null, selectionArgs: Array<String>? = null,
fileMimeType: String? = null, fileMimeType: String? = null,
safe: Boolean = false,
): Boolean { ): Boolean {
var found = false var found = false
val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC" val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
@ -299,7 +302,7 @@ class MediaStoreImageProvider : ImageProvider() {
// missing some attributes such as width, height, orientation. // missing some attributes such as width, height, orientation.
// Also, the reported size of raw images is inconsistent across devices // Also, the reported size of raw images is inconsistent across devices
// and Android versions (sometimes the raw size, sometimes the decoded size). // and Android versions (sometimes the raw size, sometimes the decoded size).
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context) val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context, safe)
entryMap = entry.toMap() entryMap = entry.toMap()
} }

View file

@ -70,7 +70,7 @@ open class UnknownContentProvider : ImageProvider() {
return return
} }
val entry = SourceEntry(fields).fillPreCatalogMetadata(context) val entry = SourceEntry(fields).fillPreCatalogMetadata(context, safe = false)
if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) { if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
callback.onSuccess(entry.toMap()) callback.onSuccess(entry.toMap())
} else { } else {

View file

@ -1,7 +1,7 @@
package deckers.thibault.aves.utils package deckers.thibault.aves.utils
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import deckers.thibault.aves.decoder.MultiPageImage import deckers.thibault.aves.decoder.MultiPageImage
object MimeTypes { object MimeTypes {

View file

@ -16,12 +16,12 @@
package androidx.exifinterface.media; package androidx.exifinterface.media;
import static androidx.exifinterface.media.ExifInterfaceUtils.closeFileDescriptor; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeFileDescriptor;
import static androidx.exifinterface.media.ExifInterfaceUtils.closeQuietly; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.closeQuietly;
import static androidx.exifinterface.media.ExifInterfaceUtils.convertToLongArray; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.convertToLongArray;
import static androidx.exifinterface.media.ExifInterfaceUtils.copy; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy;
import static androidx.exifinterface.media.ExifInterfaceUtils.parseSubSeconds; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds;
import static androidx.exifinterface.media.ExifInterfaceUtils.startsWith; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
import static java.nio.ByteOrder.BIG_ENDIAN; import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.nio.ByteOrder.LITTLE_ENDIAN; import static java.nio.ByteOrder.LITTLE_ENDIAN;
@ -41,8 +41,8 @@ import androidx.annotation.IntDef;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.exifinterface.media.ExifInterfaceUtils.Api21Impl; import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api21Impl;
import androidx.exifinterface.media.ExifInterfaceUtils.Api23Impl; import androidx.exifinterface.media.ExifInterfaceUtilsFork.Api23Impl;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
@ -84,6 +84,7 @@ import java.util.zip.CRC32;
/* /*
* Forked from 'androidx.exifinterface:exifinterface:1.3.7' on 2024/02/21 * Forked from 'androidx.exifinterface:exifinterface:1.3.7' on 2024/02/21
* Named differently to let ExifInterface be loaded as subdependency.
*/ */
/** /**
@ -97,7 +98,7 @@ import java.util.zip.CRC32;
* it. This class will search both locations for XMP data, but if XMP data exist both inside and * it. This class will search both locations for XMP data, but if XMP data exist both inside and
* outside Exif, will favor the XMP data inside Exif over the one outside. * outside Exif, will favor the XMP data inside Exif over the one outside.
*/ */
public class ExifInterface { public class ExifInterfaceFork {
// TLAD threshold for safer Exif attribute parsing // TLAD threshold for safer Exif attribute parsing
private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB
@ -3949,7 +3950,7 @@ public class ExifInterface {
* @throws IOException if an I/O error occurs while retrieving file descriptor via * @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}. * {@link FileInputStream#getFD()}.
*/ */
public ExifInterface(@NonNull File file) throws IOException { public ExifInterfaceFork(@NonNull File file) throws IOException {
if (file == null) { if (file == null) {
throw new NullPointerException("file cannot be null"); throw new NullPointerException("file cannot be null");
} }
@ -3964,7 +3965,7 @@ public class ExifInterface {
* @throws IOException if an I/O error occurs while retrieving file descriptor via * @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}. * {@link FileInputStream#getFD()}.
*/ */
public ExifInterface(@NonNull String filename) throws IOException { public ExifInterfaceFork(@NonNull String filename) throws IOException {
if (filename == null) { if (filename == null) {
throw new NullPointerException("filename cannot be null"); throw new NullPointerException("filename cannot be null");
} }
@ -3980,7 +3981,7 @@ public class ExifInterface {
* @throws NullPointerException if file descriptor is null * @throws NullPointerException if file descriptor is null
* @throws IOException if an error occurs while duplicating the file descriptor. * @throws IOException if an error occurs while duplicating the file descriptor.
*/ */
public ExifInterface(@NonNull FileDescriptor fileDescriptor) throws IOException { public ExifInterfaceFork(@NonNull FileDescriptor fileDescriptor) throws IOException {
if (fileDescriptor == null) { if (fileDescriptor == null) {
throw new NullPointerException("fileDescriptor cannot be null"); throw new NullPointerException("fileDescriptor cannot be null");
} }
@ -4023,7 +4024,7 @@ public class ExifInterface {
* @param inputStream the input stream that contains the image data * @param inputStream the input stream that contains the image data
* @throws NullPointerException if the input stream is null * @throws NullPointerException if the input stream is null
*/ */
public ExifInterface(@NonNull InputStream inputStream) throws IOException { public ExifInterfaceFork(@NonNull InputStream inputStream) throws IOException {
this(inputStream, STREAM_TYPE_FULL_IMAGE_DATA); this(inputStream, STREAM_TYPE_FULL_IMAGE_DATA);
} }
@ -4039,7 +4040,7 @@ public class ExifInterface {
* @throws IOException if an I/O error occurs while retrieving file descriptor via * @throws IOException if an I/O error occurs while retrieving file descriptor via
* {@link FileInputStream#getFD()}. * {@link FileInputStream#getFD()}.
*/ */
public ExifInterface(@NonNull InputStream inputStream, @ExifStreamType int streamType) public ExifInterfaceFork(@NonNull InputStream inputStream, @ExifStreamType int streamType)
throws IOException { throws IOException {
if (inputStream == null) { if (inputStream == null) {
throw new NullPointerException("inputStream cannot be null"); throw new NullPointerException("inputStream cannot be null");
@ -5071,7 +5072,7 @@ public class ExifInterface {
if (location == null) { if (location == null) {
return; return;
} }
setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider()); setAttribute(ExifInterfaceFork.TAG_GPS_PROCESSING_METHOD, location.getProvider());
setLatLong(location.getLatitude(), location.getLongitude()); setLatLong(location.getLatitude(), location.getLongitude());
setAltitude(location.getAltitude()); setAltitude(location.getAltitude());
// Location objects store speeds in m/sec. Translates it to km/hr here. // Location objects store speeds in m/sec. Translates it to km/hr here.
@ -5080,8 +5081,8 @@ public class ExifInterface {
* TimeUnit.HOURS.toSeconds(1) / 1000).toString()); * TimeUnit.HOURS.toSeconds(1) / 1000).toString());
String[] dateTime = sFormatterPrimary.format( String[] dateTime = sFormatterPrimary.format(
new Date(location.getTime())).split("\\s+", -1); new Date(location.getTime())).split("\\s+", -1);
setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]); setAttribute(ExifInterfaceFork.TAG_GPS_DATESTAMP, dateTime[0]);
setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]); setAttribute(ExifInterfaceFork.TAG_GPS_TIMESTAMP, dateTime[1]);
} }
/** /**
@ -5158,11 +5159,11 @@ public class ExifInterface {
} }
/** /**
* Returns parsed {@link ExifInterface#TAG_DATETIME} value as number of milliseconds since * Returns parsed {@link ExifInterfaceFork#TAG_DATETIME} value as number of milliseconds since
* Jan. 1, 1970, midnight local time. * Jan. 1, 1970, midnight local time.
* *
* <p>Note: The return value includes the first three digits (or less depending on the length * <p>Note: The return value includes the first three digits (or less depending on the length
* of the string) of {@link ExifInterface#TAG_SUBSEC_TIME}. * of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME}.
* *
* @return null if date time information is unavailable or invalid. * @return null if date time information is unavailable or invalid.
*/ */
@ -5175,11 +5176,11 @@ public class ExifInterface {
} }
/** /**
* Returns parsed {@link ExifInterface#TAG_DATETIME_DIGITIZED} value as number of * Returns parsed {@link ExifInterfaceFork#TAG_DATETIME_DIGITIZED} value as number of
* milliseconds since Jan. 1, 1970, midnight local time. * milliseconds since Jan. 1, 1970, midnight local time.
* *
* <p>Note: The return value includes the first three digits (or less depending on the length * <p>Note: The return value includes the first three digits (or less depending on the length
* of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_DIGITIZED}. * of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME_DIGITIZED}.
* *
* @return null if digitized date time information is unavailable or invalid. * @return null if digitized date time information is unavailable or invalid.
*/ */
@ -5192,11 +5193,11 @@ public class ExifInterface {
} }
/** /**
* Returns parsed {@link ExifInterface#TAG_DATETIME_ORIGINAL} value as number of * Returns parsed {@link ExifInterfaceFork#TAG_DATETIME_ORIGINAL} value as number of
* milliseconds since Jan. 1, 1970, midnight local time. * milliseconds since Jan. 1, 1970, midnight local time.
* *
* <p>Note: The return value includes the first three digits (or less depending on the length * <p>Note: The return value includes the first three digits (or less depending on the length
* of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_ORIGINAL}. * of the string) of {@link ExifInterfaceFork#TAG_SUBSEC_TIME_ORIGINAL}.
* *
* @return null if original date time information is unavailable or invalid. * @return null if original date time information is unavailable or invalid.
*/ */
@ -5910,18 +5911,18 @@ public class ExifInterface {
} }
if (rotation != null) { if (rotation != null) {
int orientation = ExifInterface.ORIENTATION_NORMAL; int orientation = ExifInterfaceFork.ORIENTATION_NORMAL;
// all rotation angles in CW // all rotation angles in CW
switch (Integer.parseInt(rotation)) { switch (Integer.parseInt(rotation)) {
case 90: case 90:
orientation = ExifInterface.ORIENTATION_ROTATE_90; orientation = ExifInterfaceFork.ORIENTATION_ROTATE_90;
break; break;
case 180: case 180:
orientation = ExifInterface.ORIENTATION_ROTATE_180; orientation = ExifInterfaceFork.ORIENTATION_ROTATE_180;
break; break;
case 270: case 270:
orientation = ExifInterface.ORIENTATION_ROTATE_270; orientation = ExifInterfaceFork.ORIENTATION_ROTATE_270;
break; break;
} }
@ -6175,7 +6176,11 @@ public class ExifInterface {
// IEND marks the end of the image. // IEND marks the end of the image.
break; break;
} else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) { } else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) {
// TODO: Need to handle potential OutOfMemoryError // TLAD start
if (length > ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
throw new IOException("dangerous exif chunk size=" + length);
}
// TLAD end
byte[] data = new byte[length]; byte[] data = new byte[length];
in.readFully(data); in.readFully(data);
@ -6976,9 +6981,11 @@ public class ExifInterface {
} }
final int bytesOffset = dataInputStream.position() + mOffsetToExifData; final int bytesOffset = dataInputStream.position() + mOffsetToExifData;
if (byteCount > 0 && byteCount < ATTRIBUTE_SIZE_DANGER_THRESHOLD) { // TLAD start
if (byteCount > ATTRIBUTE_SIZE_DANGER_THRESHOLD) {
throw new IOException("dangerous attribute size=" + byteCount); throw new IOException("dangerous attribute size=" + byteCount);
} }
// TLAD end
final byte[] bytes = new byte[(int) byteCount]; final byte[] bytes = new byte[(int) byteCount];
dataInputStream.readFully(bytes); dataInputStream.readFully(bytes);
ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents, ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents,

View file

@ -32,10 +32,10 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
class ExifInterfaceUtils { class ExifInterfaceUtilsFork {
private static final String TAG = "ExifInterfaceUtils"; private static final String TAG = "ExifInterfaceUtils";
private ExifInterfaceUtils() { private ExifInterfaceUtilsFork() {
// Prevent instantiation // Prevent instantiation
} }
/** /**

View file

@ -10,7 +10,7 @@ pluginManagement {
settings.ext.kotlin_version = '1.9.24' settings.ext.kotlin_version = '1.9.24'
settings.ext.ksp_version = "$kotlin_version-1.0.20" settings.ext.ksp_version = "$kotlin_version-1.0.20"
settings.ext.agp_version = '8.5.0' settings.ext.agp_version = '8.5.1'
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")

View file

@ -93,6 +93,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
_rawEntries.forEach((v) => v.dispose()); _rawEntries.forEach((v) => v.dispose());
} }
set safeMode(bool enabled);
final EventBus _eventBus = EventBus(); final EventBus _eventBus = EventBus();
@override @override

View file

@ -23,6 +23,10 @@ class MediaStoreSource extends CollectionSource {
final Set<String> _changedUris = {}; final Set<String> _changedUris = {};
int? _lastGeneration; int? _lastGeneration;
SourceInitializationState _initState = SourceInitializationState.none; SourceInitializationState _initState = SourceInitializationState.none;
bool _safeMode = false;
@override
set safeMode(bool enabled) => _safeMode = enabled;
@override @override
SourceInitializationState get initState => _initState; SourceInitializationState get initState => _initState;
@ -46,7 +50,7 @@ class MediaStoreSource extends CollectionSource {
analysisController: analysisController, analysisController: analysisController,
directory: directory, directory: directory,
loadTopEntriesFirst: loadTopEntriesFirst, loadTopEntriesFirst: loadTopEntriesFirst,
canAnalyze: canAnalyze, canAnalyze: canAnalyze && _safeMode,
)); ));
} }
@ -175,7 +179,7 @@ class MediaStoreSource extends CollectionSource {
pendingNewEntries.clear(); pendingNewEntries.clear();
} }
mediaStoreService.getEntries(knownDateByContentId, directory: directory).listen( mediaStoreService.getEntries(_safeMode, knownDateByContentId, directory: directory).listen(
(entry) { (entry) {
// when discovering modified entry with known content ID, // when discovering modified entry with known content ID,
// reuse known entry ID to overwrite it while preserving favourites, etc. // reuse known entry ID to overwrite it while preserving favourites, etc.

View file

@ -15,7 +15,7 @@ abstract class MediaStoreService {
Future<int?> getGeneration(); Future<int?> getGeneration();
// knownEntries: map of contentId -> dateModifiedSecs // knownEntries: map of contentId -> dateModifiedSecs
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}); Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory});
// returns media URI // returns media URI
Future<Uri?> scanFile(String path, String mimeType); Future<Uri?> scanFile(String path, String mimeType);
@ -75,12 +75,13 @@ class PlatformMediaStoreService implements MediaStoreService {
} }
@override @override
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) { Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory}) {
try { try {
return _stream return _stream
.receiveBroadcastStream(<String, dynamic>{ .receiveBroadcastStream(<String, dynamic>{
'knownEntries': knownEntries, 'knownEntries': knownEntries,
'directory': directory, 'directory': directory,
'safe': safe,
}) })
.where((event) => event is Map) .where((event) => event is Map)
.map((event) => AvesEntry.fromMap(event as Map)); .map((event) => AvesEntry.fromMap(event as Map));

View file

@ -203,10 +203,10 @@ class _HomePageState extends State<HomePage> {
unawaited(GlobalSearch.registerCallback()); unawaited(GlobalSearch.registerCallback());
unawaited(AnalysisService.registerCallback()); unawaited(AnalysisService.registerCallback());
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
source.safeMode = safeMode;
if (source.initState != SourceInitializationState.full) { if (source.initState != SourceInitializationState.full) {
await source.init( await source.init(
loadTopEntriesFirst: settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty, loadTopEntriesFirst: settings.homePage == HomePageSetting.collection && settings.homeCustomCollection.isEmpty,
canAnalyze: !safeMode,
); );
} }
case AppMode.screenSaver: case AppMode.screenSaver:

View file

@ -4,7 +4,7 @@ version '1.0-SNAPSHOT'
buildscript { buildscript {
ext { ext {
kotlin_version = '1.9.24' kotlin_version = '1.9.24'
agp_version = '8.5.0' agp_version = '8.5.1'
} }
repositories { repositories {

View file

@ -33,7 +33,7 @@ class FakeMediaStoreService extends Fake implements MediaStoreService {
} }
@override @override
Stream<AvesEntry> getEntries(Map<int?, int?> knownEntries, {String? directory}) => Stream.fromIterable(entries); Stream<AvesEntry> getEntries(bool safe, Map<int?, int?> knownEntries, {String? directory}) => Stream.fromIterable(entries);
static var _lastId = 1; static var _lastId = 1;