diff --git a/example/lib/main.dart b/example/lib/main.dart index f28738d..a3fd3eb 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; import 'package:moxplatform/moxplatform.dart'; +import 'package:path/path.dart'; import 'package:permission_handler/permission_handler.dart'; /// The id of the notification channel. @@ -286,6 +287,43 @@ class MyHomePageState extends State { }, child: const Text('Open battery optimisation page'), ), + ElevatedButton( + onPressed: () async { + final result = await FilePicker.platform.pickFiles( + type: FileType.video, + ); + if (result == null) return; + + final path = result.files.single.path!; + final storagePath = + await MoxplatformPlugin.platform.getPersistentDataPath(); + final mediaPath = join(storagePath, 'media'); + if (!Directory(mediaPath).existsSync()) { + await Directory(mediaPath).create(recursive: true); + } + + final internalPath = join(mediaPath, basename(path)); + print('Copying file'); + await File(path).copy(internalPath); + + print('Generating thumbnail'); + final thumbResult = + await MoxplatformPlugin.platform.generateVideoThumbnail( + internalPath, + '$internalPath.thumbnail.jpg', + 720, + ); + print('Success: $thumbResult'); + + await showDialog( + context: context, + builder: (context) => Image.file( + File('$internalPath.thumbnail.jpg'), + ), + ); + }, + child: const Text('Thumbnail'), + ) ], ), ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 243ee5e..77cae0c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -39,6 +39,8 @@ dependencies: file_picker: 5.2.0+1 + path: 1.8.3 + permission_handler: 10.4.3 # The following adds the Cupertino Icons font to your application. diff --git a/packages/moxplatform_android/android/src/main/AndroidManifest.xml b/packages/moxplatform_android/android/src/main/AndroidManifest.xml index 087131b..e2b99f2 100644 --- a/packages/moxplatform_android/android/src/main/AndroidManifest.xml +++ b/packages/moxplatform_android/android/src/main/AndroidManifest.xml @@ -10,7 +10,7 @@ result); void hashFile(@NonNull String sourcePath, @NonNull String hashSpec, @NonNull Result result); + /** Media APIs */ + @NonNull + Boolean generateVideoThumbnail(@NonNull String src, @NonNull String dest, @NonNull Long maxWidth); /** Stubs */ void eventStub(@NonNull NotificationEvent event); @@ -1466,6 +1469,32 @@ public class Api { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.generateVideoThumbnail", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String srcArg = (String) args.get(0); + String destArg = (String) args.get(1); + Number maxWidthArg = (Number) args.get(2); + try { + Boolean output = api.generateVideoThumbnail(srcArg, destArg, (maxWidthArg == null) ? null : maxWidthArg.longValue()); + wrapped.add(0, output); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Constants.kt b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Constants.kt index edd0eef..8866fb0 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Constants.kt +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Constants.kt @@ -25,7 +25,7 @@ const val NOTIFICATION_EXTRA_ID_KEY = "notification_id" const val NOTIFICATION_MESSAGE_EXTRA_MIME = "mime" const val NOTIFICATION_MESSAGE_EXTRA_PATH = "path" -const val MOXPLATFORM_FILEPROVIDER_ID = "me.polynom.moxplatform_android.fileprovider" +const val MOXPLATFORM_FILEPROVIDER_ID = "me.polynom.moxplatform_android.fileprovider2" // Shared preferences keys const val SHARED_PREFERENCES_KEY = "me.polynom.moxplatform_android" diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java index 2bcdea0..8f48b14 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java @@ -3,9 +3,11 @@ package me.polynom.moxplatform_android; import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS; import static androidx.core.content.ContextCompat.getSystemService; import static androidx.core.content.ContextCompat.startActivity; +import static me.polynom.moxplatform_android.ConstantsKt.MOXPLATFORM_FILEPROVIDER_ID; import static me.polynom.moxplatform_android.ConstantsKt.SHARED_PREFERENCES_KEY; import static me.polynom.moxplatform_android.CryptoKt.*; import static me.polynom.moxplatform_android.RecordSentMessageKt.*; +import static me.polynom.moxplatform_android.ThumbnailsKt.generateVideoThumbnailImplementation; import me.polynom.moxplatform_android.Api.*; @@ -13,20 +15,31 @@ import android.app.ActivityManager; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.media.MediaMetadataRetriever; +import android.media.ThumbnailUtils; import android.net.Uri; +import android.os.Build; import android.os.PowerManager; +import android.provider.MediaStore; import android.util.Log; +import android.util.Size; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; import java.util.ArrayList; import java.util.List; @@ -313,6 +326,12 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt CryptoKt.hashFile(sourcePath, hashSpec, result); } + @NonNull + @Override + public Boolean generateVideoThumbnail(@NonNull String src, @NonNull String dest, @NonNull Long maxWidth) { + return generateVideoThumbnailImplementation(src, dest, maxWidth); + } + @Override public void eventStub(@NonNull NotificationEvent event) { // Stub to trick pigeon into diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Thumbnails.kt b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Thumbnails.kt new file mode 100644 index 0000000..0be5e71 --- /dev/null +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Thumbnails.kt @@ -0,0 +1,43 @@ +package me.polynom.moxplatform_android + +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import android.util.Log +import java.io.FileOutputStream + +/* + * Generate a video thumbnail using the first frame of the video at @src. Afterwards, scale it + * down such that its width is equal to @maxWidth (while keeping the aspect ratio) and write it to + * @dest. + * + * If everything went well, returns true. If we're unable to generate the thumbnail, returns false. + * */ +fun generateVideoThumbnailImplementation(src: String, dest: String, maxWidth: Long): Boolean { + try { + val mmr = MediaMetadataRetriever().apply { + setDataSource(src) + } + val unscaledThumbnail = mmr.getFrameAtTime(0) ?: return false + + // Scale down the thumbnail while keeping the aspect ratio + val scalingFactor = maxWidth.toDouble() / unscaledThumbnail.width; + Log.d(TAG, "Scaling to $maxWidth from ${unscaledThumbnail.width} with scalingFactor $scalingFactor"); + val thumbnail = Bitmap.createScaledBitmap( + unscaledThumbnail, + (unscaledThumbnail.width * scalingFactor).toInt(), + (unscaledThumbnail.height * scalingFactor).toInt(), + false, + ) + + // Write it to the destination file + val fos = FileOutputStream(dest) + thumbnail.compress(Bitmap.CompressFormat.JPEG, 75, fos) + fos.flush() + fos.close() + return true; + } catch (ex: Exception) { + Log.e(TAG, "Failed to create thumbnail for $src: ${ex.message}") + ex.printStackTrace() + return false; + } +} \ No newline at end of file diff --git a/packages/moxplatform_android/lib/src/platform_android.dart b/packages/moxplatform_android/lib/src/platform_android.dart index 51e0cc2..435849c 100644 --- a/packages/moxplatform_android/lib/src/platform_android.dart +++ b/packages/moxplatform_android/lib/src/platform_android.dart @@ -1,3 +1,4 @@ +import 'package:moxplatform/moxplatform.dart'; import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; class AndroidPlatformImplementation extends PlatformImplementation { @@ -20,4 +21,10 @@ class AndroidPlatformImplementation extends PlatformImplementation { Future openBatteryOptimisationSettings() { return MoxplatformInterface.api.openBatteryOptimisationSettings(); } + + @override + Future generateVideoThumbnail( + String src, String dest, int width) async { + return MoxplatformInterface.api.generateVideoThumbnail(src, dest, width); + } } diff --git a/packages/moxplatform_platform_interface/lib/src/api.g.dart b/packages/moxplatform_platform_interface/lib/src/api.g.dart index 4ea730e..1df73ca 100644 --- a/packages/moxplatform_platform_interface/lib/src/api.g.dart +++ b/packages/moxplatform_platform_interface/lib/src/api.g.dart @@ -756,6 +756,36 @@ class MoxplatformApi { } } + /// Media APIs + Future generateVideoThumbnail( + String arg_src, String arg_dest, int arg_maxWidth) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.generateVideoThumbnail', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_src, arg_dest, arg_maxWidth]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + /// Stubs Future eventStub(NotificationEvent arg_event) async { final BasicMessageChannel channel = BasicMessageChannel( diff --git a/packages/moxplatform_platform_interface/lib/src/platform.dart b/packages/moxplatform_platform_interface/lib/src/platform.dart index 4874ad9..3f4bd7d 100644 --- a/packages/moxplatform_platform_interface/lib/src/platform.dart +++ b/packages/moxplatform_platform_interface/lib/src/platform.dart @@ -12,4 +12,9 @@ abstract class PlatformImplementation { /// Opens the page for battery optimisations. If not supported on the /// platform, does nothing. Future openBatteryOptimisationSettings(); + + /// Attempt to generate a thumbnail for the video file at [src], scale it, while keeping the + /// aspect ratio in tact to [width], and write it to [dest]. If we were successful, returns true. + /// If no thumbnail was generated, returns false. + Future generateVideoThumbnail(String src, String dest, int width); } diff --git a/packages/moxplatform_platform_interface/lib/src/platform_stub.dart b/packages/moxplatform_platform_interface/lib/src/platform_stub.dart index e67cf56..adc308b 100644 --- a/packages/moxplatform_platform_interface/lib/src/platform_stub.dart +++ b/packages/moxplatform_platform_interface/lib/src/platform_stub.dart @@ -14,4 +14,9 @@ class StubPlatformImplementation extends PlatformImplementation { @override Future openBatteryOptimisationSettings() async {} + + @override + Future generateVideoThumbnail( + String src, String dest, int width) async => + false; } diff --git a/pigeons/api.dart b/pigeons/api.dart index 00e7288..a47caec 100644 --- a/pigeons/api.dart +++ b/pigeons/api.dart @@ -192,6 +192,9 @@ abstract class MoxplatformApi { @async CryptographyResult? decryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec); @async Uint8List? hashFile(String sourcePath, String hashSpec); + /// Media APIs + bool generateVideoThumbnail(String src, String dest, int maxWidth); + /// Stubs void eventStub(NotificationEvent event); }