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
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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>{
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue