info: show owner app, if any
This commit is contained in:
parent
2d893d4415
commit
24dcb5b021
4 changed files with 148 additions and 13 deletions
|
@ -1,10 +1,15 @@
|
||||||
package deckers.thibault.aves.channel.calls
|
package deckers.thibault.aves.channel.calls
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
import android.media.MediaExtractor
|
import android.media.MediaExtractor
|
||||||
import android.media.MediaFormat
|
import android.media.MediaFormat
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import com.adobe.internal.xmp.XMPException
|
import com.adobe.internal.xmp.XMPException
|
||||||
|
@ -75,6 +80,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
"getOverlayMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getOverlayMetadata) }
|
"getOverlayMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getOverlayMetadata) }
|
||||||
"getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMultiPageInfo) }
|
"getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMultiPageInfo) }
|
||||||
"getPanoramaInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPanoramaInfo) }
|
"getPanoramaInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPanoramaInfo) }
|
||||||
|
"getContentResolverProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverProp) }
|
||||||
"getEmbeddedPictures" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEmbeddedPictures) }
|
"getEmbeddedPictures" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEmbeddedPictures) }
|
||||||
"getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifThumbnails) }
|
"getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifThumbnails) }
|
||||||
"extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) }
|
"extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) }
|
||||||
|
@ -622,6 +628,55 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.error("getPanoramaInfo-empty", "failed to read XMP from uri=$uri", null)
|
result.error("getPanoramaInfo-empty", "failed to read XMP from uri=$uri", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getContentResolverProp(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val mimeType = call.argument<String>("mimeType")
|
||||||
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
val prop = call.argument<String>("prop")
|
||||||
|
if (mimeType == null || uri == null || prop == null) {
|
||||||
|
result.error("getContentResolverProp-args", "failed because of missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentUri: Uri = uri
|
||||||
|
if (uri.scheme == ContentResolver.SCHEME_CONTENT && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)) {
|
||||||
|
try {
|
||||||
|
val id = ContentUris.parseId(uri)
|
||||||
|
contentUri = when {
|
||||||
|
isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
||||||
|
isVideo(mimeType) -> ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id)
|
||||||
|
else -> uri
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
contentUri = MediaStore.setRequireOriginal(contentUri)
|
||||||
|
}
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val projection = arrayOf(prop)
|
||||||
|
val cursor = context.contentResolver.query(contentUri, projection, null, null, null)
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
var value: Any? = null
|
||||||
|
try {
|
||||||
|
value = when (cursor.getType(0)) {
|
||||||
|
Cursor.FIELD_TYPE_NULL -> null
|
||||||
|
Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0)
|
||||||
|
Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0)
|
||||||
|
Cursor.FIELD_TYPE_STRING -> cursor.getString(0)
|
||||||
|
Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to get value for key=$prop", e)
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
result.success(value?.toString())
|
||||||
|
} else {
|
||||||
|
result.error("getContentResolverProp-null", "failed to get cursor for contentUri=$contentUri", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getEmbeddedPictures(call: MethodCall, result: MethodChannel.Result) {
|
private fun getEmbeddedPictures(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
|
|
|
@ -113,6 +113,19 @@ class MetadataService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<String> getContentResolverProp(AvesEntry entry, String prop) async {
|
||||||
|
try {
|
||||||
|
return await platform.invokeMethod('getContentResolverProp', <String, dynamic>{
|
||||||
|
'mimeType': entry.mimeType,
|
||||||
|
'uri': entry.uri,
|
||||||
|
'prop': prop,
|
||||||
|
});
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
debugPrint('getContentResolverProp failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<List<Uint8List>> getEmbeddedPictures(String uri) async {
|
static Future<List<Uint8List>> getEmbeddedPictures(String uri) async {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getEmbeddedPictures', <String, dynamic>{
|
final result = await platform.invokeMethod('getEmbeddedPictures', <String, dynamic>{
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
|
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||||
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/favourite_repo.dart';
|
import 'package:aves/model/favourite_repo.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
import 'package:aves/model/filters/mime.dart';
|
import 'package:aves/model/filters/mime.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/ref/mime_types.dart';
|
import 'package:aves/ref/mime_types.dart';
|
||||||
|
import 'package:aves/services/metadata_service.dart';
|
||||||
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
import 'package:aves/utils/file_utils.dart';
|
import 'package:aves/utils/file_utils.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||||
|
@ -55,6 +58,7 @@ class BasicSection extends StatelessWidget {
|
||||||
'URI': uri,
|
'URI': uri,
|
||||||
if (path != null) 'Path': path,
|
if (path != null) 'Path': path,
|
||||||
}),
|
}),
|
||||||
|
OwnerProp(entry: entry),
|
||||||
_buildChips(),
|
_buildChips(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -102,3 +106,66 @@ class BasicSection extends StatelessWidget {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class OwnerProp extends StatefulWidget {
|
||||||
|
final AvesEntry entry;
|
||||||
|
|
||||||
|
const OwnerProp({
|
||||||
|
@required this.entry,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_OwnerPropState createState() => _OwnerPropState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OwnerPropState extends State<OwnerProp> {
|
||||||
|
Future<String> _loader;
|
||||||
|
|
||||||
|
static const iconSize = 20.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loader = MetadataService.getContentResolverProp(widget.entry, 'owner_package_name');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<String>(
|
||||||
|
future: _loader,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final packageName = snapshot.data;
|
||||||
|
if (packageName == null) return SizedBox();
|
||||||
|
final appName = androidFileUtils.appNameMap.entries.firstWhere((kv) => kv.value == packageName, orElse: () => null)?.key ?? packageName;
|
||||||
|
return Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: 'Owned by',
|
||||||
|
style: InfoRowGroup.keyStyle,
|
||||||
|
),
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: Image(
|
||||||
|
image: AppIconImage(
|
||||||
|
packageName: packageName,
|
||||||
|
size: iconSize,
|
||||||
|
),
|
||||||
|
width: iconSize,
|
||||||
|
height: iconSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: appName,
|
||||||
|
style: InfoRowGroup.baseStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -42,6 +42,12 @@ class InfoRowGroup extends StatefulWidget {
|
||||||
final int maxValueLength;
|
final int maxValueLength;
|
||||||
final Map<String, InfoLinkHandler> linkHandlers;
|
final Map<String, InfoLinkHandler> linkHandlers;
|
||||||
|
|
||||||
|
static const keyValuePadding = 16;
|
||||||
|
static const linkColor = Colors.blue;
|
||||||
|
static final baseStyle = TextStyle(fontFamily: 'Concourse');
|
||||||
|
static final keyStyle = baseStyle.copyWith(color: Colors.white70, height: 1.7);
|
||||||
|
static final linkStyle = baseStyle.copyWith(color: linkColor, decoration: TextDecoration.underline);
|
||||||
|
|
||||||
const InfoRowGroup(
|
const InfoRowGroup(
|
||||||
this.keyValues, {
|
this.keyValues, {
|
||||||
this.maxValueLength = 0,
|
this.maxValueLength = 0,
|
||||||
|
@ -61,20 +67,14 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
|
||||||
|
|
||||||
Map<String, InfoLinkHandler> get linkHandlers => widget.linkHandlers;
|
Map<String, InfoLinkHandler> get linkHandlers => widget.linkHandlers;
|
||||||
|
|
||||||
static const keyValuePadding = 16;
|
|
||||||
static const linkColor = Colors.blue;
|
|
||||||
static final baseStyle = TextStyle(fontFamily: 'Concourse');
|
|
||||||
static final keyStyle = baseStyle.copyWith(color: Colors.white70, height: 1.7);
|
|
||||||
static final linkStyle = baseStyle.copyWith(color: linkColor, decoration: TextDecoration.underline);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (keyValues.isEmpty) return SizedBox.shrink();
|
if (keyValues.isEmpty) return SizedBox.shrink();
|
||||||
|
|
||||||
// compute the size of keys and space in order to align values
|
// compute the size of keys and space in order to align values
|
||||||
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
|
||||||
final keySizes = Map.fromEntries(keyValues.keys.map((key) => MapEntry(key, _getSpanWidth(TextSpan(text: '$key', style: keyStyle), textScaleFactor))));
|
final keySizes = Map.fromEntries(keyValues.keys.map((key) => MapEntry(key, _getSpanWidth(TextSpan(text: '$key', style: InfoRowGroup.keyStyle), textScaleFactor))));
|
||||||
final baseSpaceWidth = _getSpanWidth(TextSpan(text: '\u200A' * 100, style: baseStyle), textScaleFactor);
|
final baseSpaceWidth = _getSpanWidth(TextSpan(text: '\u200A' * 100, style: InfoRowGroup.baseStyle), textScaleFactor);
|
||||||
|
|
||||||
final lastKey = keyValues.keys.last;
|
final lastKey = keyValues.keys.last;
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
|
@ -100,7 +100,7 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
|
||||||
value = handler.linkText;
|
value = handler.linkText;
|
||||||
// open link on tap
|
// open link on tap
|
||||||
recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context);
|
recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context);
|
||||||
style = linkStyle;
|
style = InfoRowGroup.linkStyle;
|
||||||
} else {
|
} else {
|
||||||
value = kv.value;
|
value = kv.value;
|
||||||
// long values are clipped, and made expandable by tapping them
|
// long values are clipped, and made expandable by tapping them
|
||||||
|
@ -118,18 +118,18 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
|
||||||
|
|
||||||
// as of Flutter v1.22.4, `SelectableText` cannot contain `WidgetSpan`
|
// as of Flutter v1.22.4, `SelectableText` cannot contain `WidgetSpan`
|
||||||
// so we add padding using multiple hair spaces instead
|
// so we add padding using multiple hair spaces instead
|
||||||
final thisSpaceSize = max(0.0, (baseValueX - keySizes[key])) + keyValuePadding;
|
final thisSpaceSize = max(0.0, (baseValueX - keySizes[key])) + InfoRowGroup.keyValuePadding;
|
||||||
final spaceCount = (100 * thisSpaceSize / baseSpaceWidth).round();
|
final spaceCount = (100 * thisSpaceSize / baseSpaceWidth).round();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
TextSpan(text: key, style: keyStyle),
|
TextSpan(text: key, style: InfoRowGroup.keyStyle),
|
||||||
TextSpan(text: '\u200A' * spaceCount),
|
TextSpan(text: '\u200A' * spaceCount),
|
||||||
TextSpan(text: value, style: style, recognizer: recognizer),
|
TextSpan(text: value, style: style, recognizer: recognizer),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
).toList(),
|
).toList(),
|
||||||
),
|
),
|
||||||
style: baseStyle,
|
style: InfoRowGroup.baseStyle,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue