Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
deckerst committed Jul 22, 2021
2 parents fbca631 + 4df9060 commit 55e4710
Show file tree
Hide file tree
Showing 128 changed files with 3,299 additions and 1,809 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [v1.4.6] - 2021-07-22
### Added
- Albums / Countries / Tags: multiple selection
- Albums: action to create empty albums
- Collection: burst shot grouping (Samsung naming pattern)
- Collection: support motion photos defined by XMP Container namespace
- Settings: hidden paths to exclude folders and their subfolders
- Settings: option to disable viewer overlay blur effect (for older/slower devices)
- Settings: option to exclude cutout area in viewer

### Changed
- Video: restored overlay hiding when pressing play button

### Fixed
- Viewer: fixed manual screen rotation to follow sensor

## [v1.4.5] - 2021-07-08
### Added
- Video: added OGV/Theora/Vorbis support
Expand Down
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ repositories {

dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.exifinterface:exifinterface:1.3.2'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'com.caverock:androidsvg-aar:1.4'
Expand Down
Binary file modified android/app/libs/fijkplayer-full-release.aar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class MainActivity : FlutterActivity() {
MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this))
MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(AppShortcutHandler(this))
MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this))
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler())
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(ImageFileHandler(this))
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
Expand Down Expand Up @@ -114,7 +115,7 @@ class MainActivity : FlutterActivity() {
MediaStoreImageProvider.pendingDeleteCompleter?.complete(resultCode == RESULT_OK)
}
}
CREATE_FILE_REQUEST, OPEN_FILE_REQUEST -> {
CREATE_FILE_REQUEST, OPEN_FILE_REQUEST, SELECT_DIRECTORY_REQUEST -> {
onPermissionResult(requestCode, data?.data)
}
}
Expand Down Expand Up @@ -196,6 +197,7 @@ class MainActivity : FlutterActivity() {
const val DELETE_PERMISSION_REQUEST = 2
const val CREATE_FILE_REQUEST = 3
const val OPEN_FILE_REQUEST = 4
const val SELECT_DIRECTORY_REQUEST = 5

// permission request code to pending runnable
val pendingResultHandlers = ConcurrentHashMap<Int, PendingResultHandler>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package deckers.thibault.aves.channel.calls

import android.os.Build
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler

class DeviceHandler : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getPerformanceClass" -> result.success(getPerformanceClass())
else -> result.notImplemented()
}
}

private fun getPerformanceClass(): Int {
// TODO TLAD uncomment when the future is here
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// return Build.VERSION.MEDIA_PERFORMANCE_CLASS
// }
return Build.VERSION.SDK_INT
}

companion object {
const val CHANNEL = "deckers.thibault/aves/device"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
import deckers.thibault.aves.metadata.MultiPage
import deckers.thibault.aves.metadata.XMP
import deckers.thibault.aves.metadata.XMP.getSafeStructField
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.ContentImageProvider
import deckers.thibault.aves.model.provider.ImageProvider
Expand Down Expand Up @@ -157,18 +158,11 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
// which is returned as a second XMP directory
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
try {
val pathParts = dataPropPath.split('/')

val embedBytes: ByteArray = if (pathParts.size == 1) {
val propName = pathParts[0]
val propNs = XMP.namespaceForPropPath(propName)
xmpDirs.map { it.xmpMeta.getPropertyBase64(propNs, propName) }.first { it != null }
val embedBytes: ByteArray = if (!dataPropPath.contains('/')) {
val propNs = XMP.namespaceForPropPath(dataPropPath)
xmpDirs.map { it.xmpMeta.getPropertyBase64(propNs, dataPropPath) }.filterNotNull().first()
} else {
val structName = pathParts[0]
val structNs = XMP.namespaceForPropPath(structName)
val fieldName = pathParts[1]
val fieldNs = XMP.namespaceForPropPath(fieldName)
xmpDirs.map { it.xmpMeta.getStructField(structNs, structName, fieldNs, fieldName) }.first { it != null }.let {
xmpDirs.map { it.xmpMeta.getSafeStructField(dataPropPath) }.filterNotNull().first().let {
XMPUtils.decodeBase64(it.value)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,33 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
metadataMap["Spherical Video"] = HashMap(GSpherical(bytes).describe())
metadataMap.remove(dirName)
}
SonyVideoMetadata.USMT_UUID -> {
QuickTimeMetadata.PROF_UUID -> {
// redundant with info derived on the Dart side
metadataMap.remove(dirName)
}
QuickTimeMetadata.USMT_UUID -> {
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
val fields = SonyVideoMetadata.parseUsmt(bytes)
if (fields.isNotEmpty()) {
dirMap.remove("Data")
dirMap.putAll(fields)
val blocks = QuickTimeMetadata.parseUuidUsmt(bytes)
if (blocks.isNotEmpty()) {
metadataMap.remove(dirName)
dirName = "QuickTime User Media"
val usmt = metadataMap[dirName] ?: HashMap()
metadataMap[dirName] = usmt

blocks.forEach {
var key = it.type
var value = it.value
val language = it.language

var i = 0
while (usmt.containsKey(key)) {
key = it.type + " (${++i})"
}
if (language != "und") {
value += " ($language)"
}
usmt[key] = value
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package deckers.thibault.aves.channel.calls

import android.app.Activity
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.view.WindowManager
Expand All @@ -16,6 +17,8 @@ class WindowHandler(private val activity: Activity) : MethodCallHandler {
"keepScreenOn" -> safe(call, result, ::keepScreenOn)
"isRotationLocked" -> safe(call, result, ::isRotationLocked)
"requestOrientation" -> safe(call, result, ::requestOrientation)
"canSetCutoutMode" -> result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
"setCutoutMode" -> safe(call, result, ::setCutoutMode)
else -> result.notImplemented()
}
}
Expand Down Expand Up @@ -57,6 +60,24 @@ class WindowHandler(private val activity: Activity) : MethodCallHandler {
result.success(true)
}

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

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val mode = if (use) {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
} else {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
activity.window.attributes.layoutInDisplayCutoutMode = mode
}
result.success(true)
}

companion object {
private val LOG_TAG = LogUtils.createTag<WindowHandler>()
const val CHANNEL = "deckers.thibault/aves/window"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.PendingResultHandler
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.PermissionManager
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -41,6 +42,7 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
"requestVolumeAccess" -> requestVolumeAccess()
"createFile" -> GlobalScope.launch(Dispatchers.IO) { createFile() }
"openFile" -> GlobalScope.launch(Dispatchers.IO) { openFile() }
"selectDirectory" -> GlobalScope.launch(Dispatchers.IO) { selectDirectory() }
else -> endOfStream()
}
}
Expand Down Expand Up @@ -128,6 +130,24 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST)
}

private fun selectDirectory() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)

MainActivity.pendingResultHandlers[MainActivity.SELECT_DIRECTORY_REQUEST] = PendingResultHandler(null, { uri ->
success(StorageUtils.convertTreeUriToDirPath(activity, uri))
endOfStream()
}, {
success(null)
endOfStream()
})
activity.startActivityForResult(intent, MainActivity.SELECT_DIRECTORY_REQUEST)
} else {
success(null)
endOfStream()
}
}

override fun onCancel(arguments: Any?) {}

private fun success(result: Any?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ object Metadata {
val VIDEO_LOCATION_PATTERN: Pattern = Pattern.compile("([+-][.0-9]+)([+-][.0-9]+).*")

private val VIDEO_DATE_SUBSECOND_PATTERN = Pattern.compile("(\\d{6})(\\.\\d+)")
private val VIDEO_TIMEZONE_PATTERN = Pattern.compile("(Z|[+-]\\d{4})$")
private val VIDEO_TIME_ZONE_PATTERN = Pattern.compile("(Z|[+-]\\d{4})$")

// directory names, as shown when listing all metadata
const val DIR_GPS = "GPS" // from metadata-extractor
Expand Down Expand Up @@ -71,7 +71,7 @@ object Metadata {

// optional time zone
var timeZone: TimeZone? = null
val timeZoneMatcher = VIDEO_TIMEZONE_PATTERN.matcher(dateString)
val timeZoneMatcher = VIDEO_TIME_ZONE_PATTERN.matcher(dateString)
if (timeZoneMatcher.find()) {
timeZone = TimeZone.getTimeZone("GMT${timeZoneMatcher.group().replace("Z", "")}")
dateString = timeZoneMatcher.replaceAll("")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.util.Log
import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.metadata.XMP.getSafeLong
import deckers.thibault.aves.metadata.XMP.getSafeStructField
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
Expand Down Expand Up @@ -140,7 +141,23 @@ object MultiPage {
val metadata = ImageMetadataReader.readMetadata(input)
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
var offsetFromEnd: Long? = null
dir.xmpMeta.getSafeLong(XMP.GCAMERA_SCHEMA_NS, XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it }
val xmpMeta = dir.xmpMeta
if (xmpMeta.doesPropertyExist(XMP.GCAMERA_SCHEMA_NS, XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME)) {
// GCamera motion photo
xmpMeta.getSafeLong(XMP.GCAMERA_SCHEMA_NS, XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it }
} else if (xmpMeta.doesPropertyExist(XMP.CONTAINER_SCHEMA_NS, XMP.CONTAINER_DIRECTORY_PROP_NAME)) {
// Container motion photo
val count = xmpMeta.countArrayItems(XMP.CONTAINER_SCHEMA_NS, XMP.CONTAINER_DIRECTORY_PROP_NAME)
if (count == 2) {
// expect the video to be the second item
val i = 2
val mime = xmpMeta.getSafeStructField("${XMP.CONTAINER_DIRECTORY_PROP_NAME}[$i]/${XMP.CONTAINER_ITEM_PROP_NAME}/${XMP.CONTAINER_ITEM_MIME_PROP_NAME}")?.value
val length = xmpMeta.getSafeStructField("${XMP.CONTAINER_DIRECTORY_PROP_NAME}[$i]/${XMP.CONTAINER_ITEM_PROP_NAME}/${XMP.CONTAINER_ITEM_LENGTH_PROP_NAME}")?.value
if (MimeTypes.isVideo(mime) && length != null) {
offsetFromEnd = length.toLong()
}
}
}
return offsetFromEnd
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package deckers.thibault.aves.metadata

import java.math.BigInteger
import java.nio.charset.Charset
import java.util.*

class QuickTimeMetadataBlock(val type: String, val value: String, val language: String)

object QuickTimeMetadata {
// QuickTime Profile Tags
// cf https://exiftool.org/TagNames/QuickTime.html#Profile
const val PROF_UUID = "50524f46-21d2-4fce-bb88-695cfac9c740"

// QuickTime UserMedia Tags
// cf https://exiftool.org/TagNames/QuickTime.html#UserMedia
const val USMT_UUID = "55534d54-21d2-4fce-bb88-695cfac9c740"

private const val METADATA_BOX_ID = "MTDT"

fun parseUuidUsmt(data: ByteArray): List<QuickTimeMetadataBlock> {
val blocks = ArrayList<QuickTimeMetadataBlock>()
val boxHeader = BoxHeader(data)
if (boxHeader.boxType == METADATA_BOX_ID) {
blocks.addAll(parseQuicktimeMtdtBox(boxHeader, data))
}
return blocks
}

private fun parseQuicktimeMtdtBox(boxHeader: BoxHeader, data: ByteArray): List<QuickTimeMetadataBlock> {
val blocks = ArrayList<QuickTimeMetadataBlock>()
var bytes = data
val blockCount = BigInteger(bytes.copyOfRange(8, 10)).toInt()
bytes = bytes.copyOfRange(10, boxHeader.boxDataSize)

for (i in 0 until blockCount) {
val blockSize = BigInteger(bytes.copyOfRange(0, 2)).toInt()
val blockType = BigInteger(bytes.copyOfRange(2, 6)).toInt()
val language = parseLanguage(bytes.copyOfRange(6, 8))
val encoding = BigInteger(bytes.copyOfRange(8, 10)).toInt()
val payload = bytes.copyOfRange(10, blockSize)

val payloadString = when (encoding) {
// 0x00: short array
0x00 -> {
payload
.asList()
.chunked(2)
.map { (h, l) -> ((h.toInt() shl 8) + l.toInt()).toShort() }
.joinToString()
}
// 0x01: string
0x01 -> String(payload, Charset.forName("UTF-16BE")).trim()
// 0x101: artwork/icon
else -> "0x${payload.joinToString("") { "%02x".format(it) }}"
}

val blockTypeString = when (blockType) {
0x01 -> "Title"
0x03 -> "Creation Time"
0x04 -> "Software"
0x0A -> "Track property"
0x0B -> "Time zone"
0x0C -> "Modification Time"
else -> "0x${"%02x".format(blockType)}"
}

blocks.add(
QuickTimeMetadataBlock(
type = blockTypeString,
value = payloadString,
language = language,
)
)
bytes = bytes.copyOfRange(blockSize, bytes.size)
}

return blocks
}

// ISO 639 language code written as 3 groups of 5 bits for each letter (ascii code - 0x60)
// e.g. 0x55c4 -> 10101 01110 00100 -> 21 14 4 -> "und"
private fun parseLanguage(bytes: ByteArray): String {
val i = BigInteger(bytes).toInt()
val c1 = Character.toChars((i shr 10 and 0x1F) + 0x60)[0]
val c2 = Character.toChars((i shr 5 and 0x1F) + 0x60)[0]
val c3 = Character.toChars((i and 0x1F) + 0x60)[0]
return "$c1$c2$c3"
}
}

class BoxHeader(bytes: ByteArray) {
var boxDataSize: Int = 0
var boxType: String

init {
boxDataSize = BigInteger(bytes.copyOfRange(0, 4)).toInt()
boxType = String(bytes.copyOfRange(4, 8))
}
}
Loading

0 comments on commit 55e4710

Please sign in to comment.