Skip to content

Commit

Permalink
Merge branch 'develop' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
deckerst committed Mar 18, 2021
2 parents 9b74dd2 + f5b38d2 commit 3758e5e
Show file tree
Hide file tree
Showing 232 changed files with 5,279 additions and 2,354 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ jobs:
steps:
- uses: subosito/flutter-action@v1
with:
channel: stable
flutter-version: '1.22.6'
channel: dev
flutter-version: '2.1.0-12.1.pre'

- name: Clone the repository.
uses: actions/checkout@v2
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ jobs:

- uses: subosito/flutter-action@v1
with:
channel: stable
flutter-version: '1.22.6'
channel: dev
flutter-version: '2.1.0-12.1.pre'

# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
# https://issuetracker.google.com/issues/144111441
Expand Down Expand Up @@ -50,8 +50,8 @@ jobs:
echo "${{ secrets.KEY_JKS }}" > release.keystore.asc
gpg -d --passphrase "${{ secrets.KEY_JKS_PASSPHRASE }}" --batch release.keystore.asc > $AVES_STORE_FILE
rm release.keystore.asc
flutter build apk --bundle-sksl-path shaders_1.22.6.sksl.json
flutter build appbundle --bundle-sksl-path shaders_1.22.6.sksl.json
flutter build apk --bundle-sksl-path shaders_2.1.0-12.1.pre.sksl.json
flutter build appbundle --bundle-sksl-path shaders_2.1.0-12.1.pre.sksl.json
rm $AVES_STORE_FILE
env:
AVES_STORE_FILE: ${{ github.workspace }}/key.jks
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [v1.3.6] - 2021-03-18
### Added
- Korean translation
- cover selection for albums / countries / tags

### Changed
- Upgraded Flutter to dev v2.1.0-12.1.pre

### Fixed
- various TIFF decoding fixes

## [v1.3.5] - 2021-02-26
### Added
- support Android KitKat, Lollipop & Marshmallow (API 19 ~ 23)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Aves is a gallery and metadata explorer app. It is built for Android, with Flutter.

<img src="https://raw.githubusercontent.com/deckerst/aves/develop/extra/play/screenshots%20v1.2.1/S10/1-S10-collection.png" alt='Collection screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves/develop/extra/play/screenshots%20v1.2.1/S10/2-S10-image.png" alt='Image screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves/develop/extra/play/screenshots%20v1.2.1/S10/5-S10-stats.png" alt='Stats screenshot' height="400" />
<img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/1-S10-collection.png" alt='Collection screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/2-S10-viewer.png" alt='Image screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/5-S10-stats.png" alt='Stats screenshot' height="400" />

## Features

Expand Down
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ repositories {

dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
implementation 'androidx.core:core-ktx:1.5.0-beta01' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
implementation 'androidx.core:core-ktx:1.5.0-beta03' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
implementation 'androidx.exifinterface:exifinterface:1.3.2'
implementation 'com.commonsware.cwac:document:0.4.1'
implementation 'com.drewnoakes:metadata-extractor:2.15.0'
implementation 'com.github.deckerst:Android-TiffBitmapFactory:f87db4305d' // forked, built by JitPack
implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a' // forked, built by JitPack
implementation 'com.github.bumptech.glide:glide:4.12.0'

kapt 'androidx.annotation:annotation:1.1.0'
Expand Down
4 changes: 4 additions & 0 deletions android/app/src/debug/res/values-ko/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<resources>
<string name="app_name">아베스 [Debug]</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class MainActivity : FlutterActivity() {
MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(AppShortcutHandler(this))
MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this))
MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(ImageFileHandler(this))
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataHandler.CHANNEL).setMethodCallHandler(MetadataHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package deckers.thibault.aves.channel.calls

import android.content.Context
import android.location.Geocoder
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.*

// as of 2021/03/10, geocoding packages exist but:
// - `geocoder` is unmaintained
// - `geocoding` method does not return `addressLine` (v2.0.0)
class GeocodingHandler(private val context: Context) : MethodCallHandler {
private var geocoder: Geocoder? = null

override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getAddress" -> GlobalScope.launch(Dispatchers.IO) { Coresult.safe(call, result, ::getAddress) }
else -> result.notImplemented()
}
}

private fun getAddress(call: MethodCall, result: MethodChannel.Result) {
val latitude = call.argument<Number>("latitude")?.toDouble()
val longitude = call.argument<Number>("longitude")?.toDouble()
val localeString = call.argument<String>("locale")
val maxResults = call.argument<Int>("maxResults") ?: 1
if (latitude == null || longitude == null) {
result.error("getAddress-args", "failed because of missing arguments", null)
return
}

if (!Geocoder.isPresent()) {
result.error("getAddress-unavailable", "Geocoder is unavailable", null)
return
}

geocoder = geocoder ?: if (localeString != null) {
val split = localeString.split("_")
val language = split[0]
val country = if (split.size > 1) split[1] else ""
Geocoder(context, Locale(language, country))
} else {
Geocoder(context)
}

val addresses = try {
geocoder!!.getFromLocation(latitude, longitude, maxResults) ?: ArrayList()
} catch (e: Exception) {
result.error("getAddress-exception", "failed to get address", e.message)
return
}

if (addresses.isEmpty()) {
result.error("getAddress-empty", "failed to find any address for latitude=$latitude, longitude=$longitude", null)
} else {
val addressMapList: ArrayList<Map<String, String?>> = ArrayList(addresses.map { address ->
hashMapOf(
"addressLine" to (0..address.maxAddressLineIndex).joinToString(", ") { i -> address.getAddressLine(i) },
"adminArea" to address.adminArea,
"countryCode" to address.countryCode,
"countryName" to address.countryName,
"featureName" to address.featureName,
"locality" to address.locality,
"postalCode" to address.postalCode,
"subAdminArea" to address.subAdminArea,
"subLocality" to address.subLocality,
"subThoroughfare" to address.subThoroughfare,
"thoroughfare" to address.thoroughfare,
)
})
result.success(addressMapList)
}
}

companion object {
const val CHANNEL = "deckers.thibault/aves/geocoding"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.adobe.internal.xmp.properties.XMPPropertyInfo
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import com.drew.imaging.ImageMetadataReader
import com.drew.lang.Rational
import com.drew.metadata.Tag
import com.drew.metadata.exif.*
import com.drew.metadata.file.FileTypeDirectory
import com.drew.metadata.gif.GifAnimationDirectory
Expand Down Expand Up @@ -47,7 +48,9 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
import deckers.thibault.aves.metadata.MetadataExtractorHelper.isGeoTiff
import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
import deckers.thibault.aves.metadata.XMP.getSafeInt
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
import deckers.thibault.aves.metadata.XMP.getSafeString
import deckers.thibault.aves.metadata.XMP.isPanorama
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.FileImageProvider
Expand Down Expand Up @@ -123,17 +126,29 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
metadataMap[dirName] = dirMap

// tags
val tags = dir.tags
if (mimeType == MimeTypes.TIFF && (dir is ExifIFD0Directory || dir is ExifThumbnailDirectory)) {
dirMap.putAll(dir.tags.map {
fun tagMapper(it: Tag): Pair<String, String> {
val name = if (it.hasTagName()) {
it.tagName
} else {
TiffTags.getTagName(it.tagType) ?: it.tagName
}
Pair(name, it.description)
})
return Pair(name, it.description)
}

if (dir is ExifIFD0Directory && dir.isGeoTiff()) {
// split GeoTIFF tags in their own directory
val byGeoTiff = tags.groupBy { TiffTags.isGeoTiffTag(it.tagType) }
metadataMap["GeoTIFF"] = HashMap<String, String>().apply {
byGeoTiff[true]?.map { tagMapper(it) }?.let { putAll(it) }
}
byGeoTiff[false]?.map { tagMapper(it) }?.let { dirMap.putAll(it) }
} else {
dirMap.putAll(tags.map { tagMapper(it) })
}
} else {
dirMap.putAll(dir.tags.map { Pair(it.tagName, it.description) })
dirMap.putAll(tags.map { Pair(it.tagName, it.description) })
}
if (dir is XmpDirectory) {
try {
Expand Down Expand Up @@ -593,10 +608,11 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
KEY_PAGE to i,
KEY_MIME_TYPE to trackMime,
)

// do not use `MediaFormat.KEY_TRACK_ID` as it is actually not unique between tracks
// e.g. there could be both a video track and an image track with KEY_TRACK_ID == 1

format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { page[KEY_IS_DEFAULT] = it != 0 }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
format.getSafeInt(MediaFormat.KEY_TRACK_ID) { page[KEY_TRACK_ID] = it }
}
format.getSafeInt(MediaFormat.KEY_WIDTH) { page[KEY_WIDTH] = it }
format.getSafeInt(MediaFormat.KEY_HEIGHT) { page[KEY_HEIGHT] = it }
if (isVideo(trackMime)) {
Expand Down Expand Up @@ -626,25 +642,21 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
try {
fun getIntProp(propName: String): Int? = xmpDirs.map { it.xmpMeta.getPropertyInteger(XMP.GPANO_SCHEMA_NS, propName) }.firstOrNull { it != null }
fun getStringProp(propName: String): String? = xmpDirs.map { it.xmpMeta.getPropertyString(XMP.GPANO_SCHEMA_NS, propName) }.firstOrNull { it != null }
val fields: FieldMap = hashMapOf(
"croppedAreaLeft" to getIntProp(XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME),
"croppedAreaTop" to getIntProp(XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME),
"croppedAreaWidth" to getIntProp(XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME),
"croppedAreaHeight" to getIntProp(XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME),
"fullPanoWidth" to getIntProp(XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME),
"fullPanoHeight" to getIntProp(XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME),
"projectionType" to (getStringProp(XMP.GPANO_PROJECTION_TYPE_PROP_NAME) ?: XMP.GPANO_PROJECTION_TYPE_DEFAULT),
)
result.success(fields)
return
} catch (e: XMPException) {
result.error("getPanoramaInfo-args", "failed to read XMP for uri=$uri", e.message)
return
val fields = hashMapOf<String, Any?>(
"projectionType" to XMP.GPANO_PROJECTION_TYPE_DEFAULT,
)
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
val xmpMeta = dir.xmpMeta
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME) { fields["croppedAreaLeft"] = it }
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME) { fields["croppedAreaTop"] = it }
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME) { fields["croppedAreaWidth"] = it }
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME) { fields["croppedAreaHeight"] = it }
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME) { fields["fullPanoWidth"] = it }
xmpMeta.getSafeInt(XMP.GPANO_SCHEMA_NS, XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME) { fields["fullPanoHeight"] = it }
xmpMeta.getSafeString(XMP.GPANO_SCHEMA_NS, XMP.GPANO_PROJECTION_TYPE_PROP_NAME) { fields["projectionType"] = it }
}
result.success(fields)
return
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to read XMP", e)
Expand Down Expand Up @@ -875,7 +887,6 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
private const val KEY_HEIGHT = "height"
private const val KEY_WIDTH = "width"
private const val KEY_PAGE = "page"
private const val KEY_TRACK_ID = "trackId"
private const val KEY_IS_DEFAULT = "isDefault"
private const val KEY_DURATION = "durationMillis"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class MultiTrackImageGlideModule : LibraryGlideModule() {
}
}

class MultiTrackImage(val context: Context, val uri: Uri, val trackId: Int?)
class MultiTrackImage(val context: Context, val uri: Uri, val trackIndex: Int?)

internal class MultiTrackThumbnailLoader : ModelLoader<MultiTrackImage, Bitmap> {
override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
Expand All @@ -52,9 +52,9 @@ internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int

val context = model.context
val uri = model.uri
val trackId = model.trackId
val trackIndex = model.trackIndex

val bitmap = MultiTrackMedia.getImage(context, uri, trackId)
val bitmap = MultiTrackMedia.getImage(context, uri, trackIndex)
if (bitmap == null) {
callback.onLoadFailed(Exception("null bitmap"))
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,41 @@ object MultiTrackMedia {
private val LOG_TAG = LogUtils.createTag(MultiTrackMedia::class.java)

@RequiresApi(Build.VERSION_CODES.P)
fun getImage(context: Context, uri: Uri, trackId: Int?): Bitmap? {
fun getImage(context: Context, uri: Uri, trackIndex: Int?): Bitmap? {
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return null
try {
return if (trackId != null) {
val imageIndex = trackIdToImageIndex(context, uri, trackId) ?: return null
return if (trackIndex != null) {
val imageIndex = trackIndexToImageIndex(context, uri, trackIndex) ?: return null
retriever.getImageAtIndex(imageIndex)
} else {
retriever.primaryImage
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to extract image from uri=$uri trackId=$trackId", e)
Log.w(LOG_TAG, "failed to extract image from uri=$uri trackIndex=$trackIndex", e)
} finally {
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
retriever.release()
}
return null
}

private fun trackIdToImageIndex(context: Context, uri: Uri, trackId: Int): Int? {
private fun trackIndexToImageIndex(context: Context, uri: Uri, trackIndex: Int): Int? {
val extractor = MediaExtractor()
try {
extractor.setDataSource(context, uri, null)
val trackCount = extractor.trackCount
var imageIndex = 0
for (i in 0 until trackCount) {
val trackFormat = extractor.getTrackFormat(i)
if (trackId == trackFormat.getInteger(MediaFormat.KEY_TRACK_ID)) {
if (trackIndex == i) {
return imageIndex
}
if (MimeTypes.isImage(trackFormat.getString(MediaFormat.KEY_MIME))) {
imageIndex++
}
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackId=$trackId", e)
Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackIndex=$trackIndex", e)
} finally {
extractor.release()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ object TiffTags {
// Count = variable
const val TAG_ORIGINAL_RAW_FILE_NAME = 0xc68b

private val geotiffTags = listOf(
TAG_GEO_ASCII_PARAMS,
TAG_GEO_DOUBLE_PARAMS,
TAG_GEO_KEY_DIRECTORY,
TAG_MODEL_PIXEL_SCALE,
TAG_MODEL_TIEPOINT,
TAG_MODEL_TRANSFORMATION,
)

private val tagNameMap = hashMapOf(
TAG_X_POSITION to "X Position",
TAG_Y_POSITION to "Y Position",
Expand All @@ -132,6 +141,8 @@ object TiffTags {
TAG_ORIGINAL_RAW_FILE_NAME to "Original Raw File Name",
)

fun isGeoTiffTag(tag: Int) = geotiffTags.contains(tag)

fun getTagName(tag: Int): String? {
return tagNameMap[tag]
}
Expand Down
Loading

0 comments on commit 3758e5e

Please sign in to comment.