info: show owner app, if any

This commit is contained in:
Thibault Deckers 2021-01-28 17:16:06 +09:00
parent 2d893d4415
commit 24dcb5b021
4 changed files with 148 additions and 13 deletions

View file

@ -1,10 +1,15 @@
package deckers.thibault.aves.channel.calls
import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.media.MediaExtractor
import android.media.MediaFormat
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import androidx.exifinterface.media.ExifInterface
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) }
"getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMultiPageInfo) }
"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) }
"getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifThumbnails) }
"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)
}
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) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (uri == null) {

View file

@ -113,6 +113,19 @@ class MetadataService {
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 {
try {
final result = await platform.invokeMethod('getEmbeddedPictures', <String, dynamic>{

View file

@ -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/filters/album.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/mime.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/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/file_utils.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
@ -55,6 +58,7 @@ class BasicSection extends StatelessWidget {
'URI': uri,
if (path != null) 'Path': path,
}),
OwnerProp(entry: entry),
_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,
),
],
),
);
},
);
}
}

View file

@ -42,6 +42,12 @@ class InfoRowGroup extends StatefulWidget {
final int maxValueLength;
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(
this.keyValues, {
this.maxValueLength = 0,
@ -61,20 +67,14 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
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
Widget build(BuildContext context) {
if (keyValues.isEmpty) return SizedBox.shrink();
// compute the size of keys and space in order to align values
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final keySizes = Map.fromEntries(keyValues.keys.map((key) => MapEntry(key, _getSpanWidth(TextSpan(text: '$key', style: keyStyle), textScaleFactor))));
final baseSpaceWidth = _getSpanWidth(TextSpan(text: '\u200A' * 100, style: baseStyle), 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: InfoRowGroup.baseStyle), textScaleFactor);
final lastKey = keyValues.keys.last;
return LayoutBuilder(
@ -100,7 +100,7 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
value = handler.linkText;
// open link on tap
recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context);
style = linkStyle;
style = InfoRowGroup.linkStyle;
} else {
value = kv.value;
// 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`
// 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();
return [
TextSpan(text: key, style: keyStyle),
TextSpan(text: key, style: InfoRowGroup.keyStyle),
TextSpan(text: '\u200A' * spaceCount),
TextSpan(text: value, style: style, recognizer: recognizer),
];
},
).toList(),
),
style: baseStyle,
style: InfoRowGroup.baseStyle,
),
],
);