Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
deckerst committed Nov 27, 2020
2 parents 4822027 + 154ceec commit 3a3f336
Show file tree
Hide file tree
Showing 164 changed files with 2,706 additions and 1,433 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [v1.2.8] - 2020-11-27
### Added
- Albums / Countries / Tags: pinch to change tile size
- Album picker: added a field to filter by name
- check free space before moving entries
- SVG source viewer

### Changed
- Navigation: changed page history handling
- Info: improved layout, especially for XMP
- About: improved layout
- faster locating of new entries

## [v1.2.7] - 2020-11-15
### Added
- Support for TIFF images (single page)
Expand Down
5 changes: 4 additions & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ repositories {

dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
implementation 'androidx.core:core-ktx:1.5.0-alpha04' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
implementation 'androidx.core:core-ktx:1.5.0-alpha05' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
implementation 'androidx.exifinterface:exifinterface:1.3.1'
implementation 'com.commonsware.cwac:document:0.4.1'
implementation 'com.drewnoakes:metadata-extractor:2.15.0'
Expand All @@ -109,6 +109,9 @@ dependencies {
implementation 'com.github.deckerst:Android-TiffBitmapFactory:7efb450636'
implementation 'com.github.bumptech.glide:glide:4.11.0'

// TODO TLAD remove when this is fixed: https://github.com/firebase/firebase-android-sdk/issues/1662 https://github.com/FirebaseExtended/flutterfire/issues/3990
implementation 'com.google.firebase:firebase-analytics:18.0.0'

kapt 'androidx.annotation:annotation:1.1.0'
kapt 'com.github.bumptech.glide:compiler:4.11.0'

Expand Down
24 changes: 24 additions & 0 deletions android/app/google-services.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
}
},
"oauth_client": [
{
"client_id": "100907092477-1mredcehjo66opfirr6k3kokjqmc99ee.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "deckers.thibault.aves",
"certificate_hash": "59a50013fa7a2f97911b52d681cafaebf83505e8"
}
},
{
"client_id": "100907092477-ml1c4hr4l24ekg7l7nqid06n03kek6c8.apps.googleusercontent.com",
"client_type": 1,
Expand Down Expand Up @@ -51,6 +59,14 @@
}
},
"oauth_client": [
{
"client_id": "100907092477-8vgakbtass73c6dad5mqflq2dd4h4904.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "deckers.thibault.aves.debug",
"certificate_hash": "744592fedb021fd82372966a6cb30569579fa9cc"
}
},
{
"client_id": "100907092477-u7sm8gp5t2sotn42oq20ufhtn3craodu.apps.googleusercontent.com",
"client_type": 3
Expand Down Expand Up @@ -80,6 +96,14 @@
}
},
"oauth_client": [
{
"client_id": "100907092477-4a6968gloaaq70uti1offkk7raduond6.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "deckers.thibault.aves.profile",
"certificate_hash": "744592fedb021fd82372966a6cb30569579fa9cc"
}
},
{
"client_id": "100907092477-u7sm8gp5t2sotn42oq20ufhtn3craodu.apps.googleusercontent.com",
"client_type": 3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
uri ?: return false

val intent = Intent(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setDataAndType(uri, mimeType)
return safeStartActivityChooser(title, intent)
}
Expand All @@ -177,20 +178,21 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
uri ?: return false

val intent = Intent(Intent.ACTION_ATTACH_DATA)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setDataAndType(uri, mimeType)
return safeStartActivityChooser(title, intent)
}

private fun shareSingle(title: String?, uri: Uri, mimeType: String): Boolean {
val intent = Intent(Intent.ACTION_SEND)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setType(mimeType)
when (uri.scheme?.toLowerCase(Locale.ROOT)) {
ContentResolver.SCHEME_FILE -> {
val path = uri.path ?: return false
val applicationId = context.applicationContext.packageName
val apkUri = FileProvider.getUriForFile(context, "$applicationId.fileprovider", File(path))
intent.putExtra(Intent.EXTRA_STREAM, apkUri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
else -> intent.putExtra(Intent.EXTRA_STREAM, uri)
}
Expand Down Expand Up @@ -222,25 +224,32 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
}

val intent = Intent(Intent.ACTION_SEND_MULTIPLE)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList)
.setType(mimeType)
return safeStartActivityChooser(title, intent)
}

private fun safeStartActivity(intent: Intent): Boolean {
val canResolve = intent.resolveActivity(context.packageManager) != null
if (canResolve) {
if (intent.resolveActivity(context.packageManager) == null) return false
try {
context.startActivity(intent)
return true
} catch (e: SecurityException) {
Log.w(LOG_TAG, "failed to start activity for intent=$intent", e)
}
return canResolve
return false
}

private fun safeStartActivityChooser(title: String?, intent: Intent): Boolean {
val canResolve = intent.resolveActivity(context.packageManager) != null
if (canResolve) {
if (intent.resolveActivity(context.packageManager) == null) return false
try {
context.startActivity(Intent.createChooser(intent, title))
return true
} catch (e: SecurityException) {
Log.w(LOG_TAG, "failed to start activity chooser for intent=$intent", e)
}
return canResolve
return false
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {

for (dir in metadata.directories.filter { it.tagCount > 0 && it !is FileTypeDirectory }) {
// directory name
val dirName = dir.name ?: ""
var dirName = dir.name
// optional parent to distinguish child directories of the same type
dir.parent?.name?.let { dirName = "$it/$dirName" }

val dirMap = metadataMap.getOrDefault(dirName, HashMap())
metadataMap[dirName] = dirMap

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
}
result.success(volumes)
}
"getFreeSpace" -> getFreeSpace(call, result)
"getGrantedDirectories" -> result.success(ArrayList(PermissionManager.getGrantedDirs(context)))
"getInaccessibleDirectories" -> getInaccessibleDirectories(call, result)
"revokeDirectoryAccess" -> revokeDirectoryAccess(call, result)
Expand Down Expand Up @@ -62,6 +63,35 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
return volumes
}

private fun getFreeSpace(call: MethodCall, result: MethodChannel.Result) {
val path = call.argument<String>("path")
if (path == null) {
result.error("getFreeSpace-args", "failed because of missing arguments", null)
return
}

val sm = context.getSystemService(StorageManager::class.java)
if (sm == null) {
result.error("getFreeSpace-sm", "failed because of missing Storage Manager", null)
return
}

val file = File(path)
val volume = sm.getStorageVolume(file)
if (volume == null) {
result.error("getFreeSpace-volume", "failed because of missing volume for path=$path", null)
return
}

// `StorageStatsManager` `getFreeBytes()` is only available from API 26,
// and non-primary volume UUIDs cannot be used with it
try {
result.success(file.freeSpace)
} catch (e: SecurityException) {
result.error("getFreeSpace-security", "failed because of missing access", e.message)
}
}

private fun getInaccessibleDirectories(call: MethodCall, result: MethodChannel.Result) {
val dirPaths = call.argument<List<String>>("dirPaths")
if (dirPaths == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,11 @@ object ExifInterfaceHelper {
// so that we can rely on metadata-extractor descriptions
val dirs = DirType.values().map { Pair(it, it.createDirectory()) }.toMap()

// exclude Exif directory when it only includes image size
val isUselessExif: (Map<String, String>) -> Boolean = { it.size == 2 && it.containsKey("Image Height") && it.containsKey("Image Width") }

return HashMap<String, Map<String, String>>().apply {
put("Exif", describeDir(exif, dirs, baseTags))
put("Exif", describeDir(exif, dirs, baseTags).takeUnless(isUselessExif) ?: hashMapOf())
put("Exif Thumbnail", describeDir(exif, dirs, thumbnailTags))
put(Metadata.DIR_GPS, describeDir(exif, dirs, gpsTags))
put(Metadata.DIR_XMP, describeDir(exif, dirs, xmpTags))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,17 +122,15 @@ class SourceImageEntry {
if (isVideo) {
fillVideoByMediaMetadataRetriever(context)
if (isSized && hasDuration) return this
}
// skip metadata-extractor for raw images because it reports the decoded dimensions instead of the raw dimensions
if (!MimeTypes.isRaw(sourceMimeType) && MimeTypes.isSupportedByMetadataExtractor(sourceMimeType)) {
fillByMetadataExtractor(context)
} else {
fillByMetadataExtractor(context)
if (isSized && foundExif) return this
}
if (ExifInterface.isSupportedMimeType(sourceMimeType)) {
fillByExifInterface(context)
if (isSized) return this
}
fillByBitmapDecode(context)
if (!isSized) {
fillByBitmapDecode(context)
}
return this
}

Expand All @@ -156,6 +154,9 @@ class SourceImageEntry {

// finds: width, height, orientation, date, duration
private fun fillByMetadataExtractor(context: Context) {
// skip raw images because `metadata-extractor` reports the decoded dimensions instead of the raw dimensions
if (!MimeTypes.isSupportedByMetadataExtractor(sourceMimeType) || MimeTypes.isRaw(sourceMimeType)) return

try {
StorageUtils.openInputStream(context, uri)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
Expand Down Expand Up @@ -206,6 +207,8 @@ class SourceImageEntry {

// finds: width, height, orientation, date
private fun fillByExifInterface(context: Context) {
if (!ExifInterface.isSupportedMimeType(sourceMimeType)) return;

try {
StorageUtils.openInputStream(context, uri)?.use { input ->
val exif = ExifInterface(input)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ abstract class ImageProvider {
}

if (newFields.isEmpty()) {
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri"))
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)"))
} else {
cont.resume(newFields)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ class MediaStoreImageProvider : ImageProvider() {
val contentUri = ContentUris.withAppendedId(VIDEO_CONTENT_URI, id)
if (fetchFrom(context, alwaysValid, onSuccess, contentUri, VIDEO_PROJECTION) > 0) return
}
// the uri can be a file media uri (e.g. "content://0@media/external/file/30050")
// the uri can be a file media URI (e.g. "content://0@media/external/file/30050")
// without an equivalent image/video if it is shared from a file browser
// but the file is not publicly visible
if (fetchFrom(context, alwaysValid, onSuccess, uri, BASE_PROJECTION) > 0) return
if (fetchFrom(context, alwaysValid, onSuccess, uri, BASE_PROJECTION, fileMimeType = mimeType) > 0) return

callback.onFailure(Exception("failed to fetch entry at uri=$uri"))
}
Expand Down Expand Up @@ -87,6 +87,7 @@ class MediaStoreImageProvider : ImageProvider() {
handleNewEntry: NewEntryHandler,
contentUri: Uri,
projection: Array<String>,
fileMimeType: String? = null,
): Int {
var newEntryCount = 0
val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
Expand Down Expand Up @@ -123,45 +124,51 @@ class MediaStoreImageProvider : ImageProvider() {
// for multiple items, `contentUri` is the root without ID,
// but for single items, `contentUri` already contains the ID
val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, contentId.toLong())
val mimeType = cursor.getString(mimeTypeColumn)
// `mimeType` can be registered as null for file media URIs with unsupported media types (e.g. TIFF on old devices)
// in that case we try to use the mime type provided along the URI
val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType
val width = cursor.getInt(widthColumn)
val height = cursor.getInt(heightColumn)
val durationMillis = if (durationColumn != -1) cursor.getLong(durationColumn) else 0L

var entryMap: FieldMap = hashMapOf(
"uri" to itemUri.toString(),
"path" to cursor.getString(pathColumn),
"sourceMimeType" to mimeType,
"width" to width,
"height" to height,
"sourceRotationDegrees" to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0,
"sizeBytes" to cursor.getLong(sizeColumn),
"title" to cursor.getString(titleColumn),
"dateModifiedSecs" to dateModifiedSecs,
"sourceDateTakenMillis" to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null,
"durationMillis" to durationMillis,
// only for map export
"contentId" to contentId,
)

if (MimeTypes.isRaw(mimeType)
|| (width <= 0 || height <= 0) && needSize(mimeType)
|| durationMillis == 0L && needDuration
) {
// Some images are incorrectly registered in the Media Store,
// missing some attributes such as width, height, orientation.
// Also, the reported size of raw images is inconsistent across devices
// and Android versions (sometimes the raw size, sometimes the decoded size).
val entry = SourceImageEntry(entryMap).fillPreCatalogMetadata(context)
entryMap = entry.toMap()
if (mimeType == null) {
Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type")
} else {
var entryMap: FieldMap = hashMapOf(
"uri" to itemUri.toString(),
"path" to cursor.getString(pathColumn),
"sourceMimeType" to mimeType,
"width" to width,
"height" to height,
"sourceRotationDegrees" to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0,
"sizeBytes" to cursor.getLong(sizeColumn),
"title" to cursor.getString(titleColumn),
"dateModifiedSecs" to dateModifiedSecs,
"sourceDateTakenMillis" to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null,
"durationMillis" to durationMillis,
// only for map export
"contentId" to contentId,
)

if (MimeTypes.isRaw(mimeType)
|| (width <= 0 || height <= 0) && needSize(mimeType)
|| durationMillis == 0L && needDuration
) {
// Some images are incorrectly registered in the Media Store,
// missing some attributes such as width, height, orientation.
// Also, the reported size of raw images is inconsistent across devices
// and Android versions (sometimes the raw size, sometimes the decoded size).
val entry = SourceImageEntry(entryMap).fillPreCatalogMetadata(context)
entryMap = entry.toMap()
}

handleNewEntry(entryMap)
// TODO TLAD is this necessary?
if (newEntryCount % 30 == 0) {
delay(10)
}
newEntryCount++
}

handleNewEntry(entryMap)
// TODO TLAD is this necessary?
if (newEntryCount % 30 == 0) {
delay(10)
}
newEntryCount++
}
}
cursor.close()
Expand Down Expand Up @@ -314,7 +321,8 @@ class MediaStoreImageProvider : ImageProvider() {
MediaStore.MediaColumns._ID,
MediaColumns.PATH,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.SIZE, // TODO TLAD use `DISPLAY_NAME` instead/along `TITLE`?
MediaStore.MediaColumns.SIZE,
// TODO TLAD use `DISPLAY_NAME` instead/along `TITLE`?
MediaStore.MediaColumns.TITLE,
MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ object MimeTypes {
else -> isVideo(mimeType)
}

fun isRaw(mimeType: String?): Boolean {
fun isRaw(mimeType: String): Boolean {
return when (mimeType) {
ARW, CR2, DNG, NEF, NRW, ORF, PEF, RAF, RW2, SRW -> true
else -> false
Expand Down
Loading

0 comments on commit 3a3f336

Please sign in to comment.