diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f3bfa1..dd2d6ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2.1.0 + +### New + * `cleanup` method on `LocalImageProvider` removes all temporary files created by the plugin + +### Updates +* fix for `videoFile` call with id that does not exist + ## 2.0.0 ### Breaking diff --git a/README.md b/README.md index aa27455..0b135d0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Local Image Provider Plugin -[![pub package](https://img.shields.io/badge/pub-v2.0.0-blue)](https://pub.dartlang.org/packages/local_image_provider) [![build status](https://github.com/csdcorp/local_image_provider/workflows/build/badge.svg)](https://github.com/csdcorp/local_image_provider/actions?query=workflow%3Abuild) +[![pub package](https://img.shields.io/badge/pub-v2.1.0-blue)](https://pub.dartlang.org/packages/local_image_provider) [![build status](https://github.com/csdcorp/local_image_provider/workflows/build/badge.svg)](https://github.com/csdcorp/local_image_provider/actions?query=workflow%3Abuild) A library for searching and retrieving the metadata and contents of the images and albums on a mobile device. diff --git a/android/.idea/caches/build_file_checksums.ser b/android/.idea/caches/build_file_checksums.ser index 2940eeb..7f52849 100644 Binary files a/android/.idea/caches/build_file_checksums.ser and b/android/.idea/caches/build_file_checksums.ser differ diff --git a/android/src/main/kotlin/com/csdcorp/local_image_provider/LocalImageProviderPlugin.kt b/android/src/main/kotlin/com/csdcorp/local_image_provider/LocalImageProviderPlugin.kt index e195684..351a44a 100644 --- a/android/src/main/kotlin/com/csdcorp/local_image_provider/LocalImageProviderPlugin.kt +++ b/android/src/main/kotlin/com/csdcorp/local_image_provider/LocalImageProviderPlugin.kt @@ -174,6 +174,9 @@ public class LocalImageProviderPlugin : FlutterPlugin, MethodCallHandler, "Missing arg requires id", null) } } + "cleanup" -> { + cleanup(result) + } "images_in_album" -> { val albumId = call.argument("albumId") val maxImages = call.argument("maxImages") @@ -545,6 +548,14 @@ public class LocalImageProviderPlugin : FlutterPlugin, MethodCallHandler, }).start() } + // Since no temporary files are created on android this method is a no-op + private fun cleanup(result: Result) { + if (isNotInitialized(result)) { + return + } + result.success(true ) + } + private fun getImageBytes(id: String, width: Int, height: Int, result: Result) { if (isNotInitialized(result)) { return diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 8efa9d2..5c199b5 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -6,12 +6,6 @@ PODS: - Flutter - local_image_provider (0.0.1): - Flutter - - native_video_view (0.0.1): - - Flutter - - path_provider (0.0.1): - - Flutter - - path_provider_macos (0.0.1): - - Flutter - video_player (0.0.1): - Flutter - video_player_web (0.0.1): @@ -22,9 +16,6 @@ DEPENDENCIES: - flutter_plugin_android_lifecycle (from `.symlinks/plugins/flutter_plugin_android_lifecycle/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) - local_image_provider (from `.symlinks/plugins/local_image_provider/ios`) - - native_video_view (from `.symlinks/plugins/native_video_view/ios`) - - path_provider (from `.symlinks/plugins/path_provider/ios`) - - path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`) - video_player (from `.symlinks/plugins/video_player/ios`) - video_player_web (from `.symlinks/plugins/video_player_web/ios`) @@ -37,12 +28,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker/ios" local_image_provider: :path: ".symlinks/plugins/local_image_provider/ios" - native_video_view: - :path: ".symlinks/plugins/native_video_view/ios" - path_provider: - :path: ".symlinks/plugins/path_provider/ios" - path_provider_macos: - :path: ".symlinks/plugins/path_provider_macos/ios" video_player: :path: ".symlinks/plugins/video_player/ios" video_player_web: @@ -53,9 +38,6 @@ SPEC CHECKSUMS: flutter_plugin_android_lifecycle: 47de533a02850f070f5696a623995e93eddcdb9b image_picker: e3eacd46b94694dde7cf2705955cece853aa1a8f local_image_provider: 05b1af32561482c0ca5a440fbc7f489ad818c962 - native_video_view: 02756aebe7ec9d0ab00d039b7aa3756548900bc3 - path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d - path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0 video_player: 69c5f029fac4ffe4fc8a85ea7f7b793709661549 video_player_web: da8cadb8274ed4f8dbee8d7171b420dedd437ce7 diff --git a/example/lib/local_image_body_widget.dart b/example/lib/local_image_body_widget.dart index 606c0db..9f12dc6 100644 --- a/example/lib/local_image_body_widget.dart +++ b/example/lib/local_image_body_widget.dart @@ -58,6 +58,7 @@ class _LocalImageBodyWidgetState extends State { } hasPermission = await localImageProvider.initialize(); if (hasPermission) { + await localImageProvider.cleanup(); // localImages = await localImageProvider.findLatest(50); localAlbums = await localImageProvider.findAlbums(LocalAlbumType.all); } diff --git a/example/pubspec.lock b/example/pubspec.lock index 9c04166..86fb878 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -113,7 +113,7 @@ packages: path: ".." relative: true source: path - version: "2.0.0" + version: "2.1.0" matcher: dependency: transitive description: diff --git a/ios/Classes/SwiftLocalImageProviderPlugin.swift b/ios/Classes/SwiftLocalImageProviderPlugin.swift index f483cfc..604d69d 100644 --- a/ios/Classes/SwiftLocalImageProviderPlugin.swift +++ b/ios/Classes/SwiftLocalImageProviderPlugin.swift @@ -7,6 +7,7 @@ public enum LocalImageProviderMethods: String { case latest_images case image_bytes case video_file + case cleanup case images_in_album case albums case has_permission @@ -105,6 +106,8 @@ public class SwiftLocalImageProviderPlugin: NSObject, FlutterPlugin { return } getVideoFile( localId, result) + case LocalImageProviderMethods.cleanup.rawValue: + cleanup( result) default: print("Unrecognized method: \(call.method)") result( FlutterMethodNotImplemented) @@ -295,9 +298,8 @@ public class SwiftLocalImageProviderPlugin: NSObject, FlutterPlugin { } } } - let paths = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) - let tempDir = paths[0] - let outputFile = tempDir.appendingPathComponent(UUID().uuidString) + let tempPath = self.getTemporaryPath() + let outputFile = tempPath.appendingPathComponent(UUID().uuidString) let finalFile = outputFile.appendingPathExtension("mov") exportSession?.outputURL = finalFile exportSession?.outputFileType = AVFileType.mov @@ -306,8 +308,39 @@ public class SwiftLocalImageProviderPlugin: NSObject, FlutterPlugin { } }); } + else { + DispatchQueue.main.async { + flutterResult(FlutterError( code: LocalImageProviderErrors.imgNotFound.rawValue, message:"Video not found: \(id)", details: nil )) + } + } } + private func cleanup( _ flutterResult: @escaping FlutterResult) { + let tempPath = getTemporaryPath() + guard let filePaths = try? FileManager.default.contentsOfDirectory(at: tempPath, includingPropertiesForKeys: nil, options: []) else { return } + for filePath in filePaths { + try? FileManager.default.removeItem(at: filePath) + } + DispatchQueue.main.async { + flutterResult( true ) + } + } + + private func getTemporaryPath() -> URL { + let paths = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) + let tempDir = paths[0] + let tempPath = tempDir.appendingPathComponent("csdcorp_lip") + if !FileManager.default.fileExists(atPath: tempPath.path) { + do { + try FileManager.default.createDirectory(atPath: tempPath.path, withIntermediateDirectories: true, attributes: nil) + } catch { + NSLog("Couldn't create folder in tmp directory") + NSLog("==> directory is: \(tempPath)") + } + } + return tempPath + } + private func getPhotoImage(_ id: String, _ pixelHeight: Int, _ pixelWidth: Int, _ flutterResult: @escaping FlutterResult) { let fetchOptions = PHFetchOptions() let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: fetchOptions ) diff --git a/lib/local_image_provider.dart b/lib/local_image_provider.dart index f11dfae..4df7e83 100644 --- a/lib/local_image_provider.dart +++ b/lib/local_image_provider.dart @@ -182,8 +182,13 @@ class LocalImageProvider { return photoBytes; } - /// Returns a temporary file path for the image. + /// Returns a temporary file path for the requested video. /// + /// Call this method to get a playable video file where [id] is from + /// a [LocalImage] that has `isVideo` true. These files can be + /// used for video playback using the `video_player` plugin + /// for example. These files should either be moved or deleted by + /// client code, or cleaned up occasionally using the [cleanup] method. Future videoFile(String id) async { if (!_initWorked) { throw LocalImageProviderNotInitializedException(); @@ -198,6 +203,21 @@ class LocalImageProvider { return filePath; } + /// Call this method to cleanup any temporary files that have been + /// created by the image provider. + /// + /// After this method completes any file paths returned from + /// [videoFile] or other methods will no longer be valid. Only call + /// this method when you no longer depend on those files. Calling this + /// at program startup is safe, or at a point when you have finished + /// using all returned file paths. + Future cleanup() async { + if (!_initWorked) { + throw LocalImageProviderNotInitializedException(); + } + await channel.invokeMethod('cleanup'); + } + /// Resets the [totalLoadTime], [lastLaodTime], and [imgBytesLoaded] /// stats to zero. void resetStats() { diff --git a/pubspec.yaml b/pubspec.yaml index 4c79d25..3df9c65 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: local_image_provider -description: A library for retrieving the metadata and contents of the images and albums on a mobile device. -version: 2.0.0 +description: A library for retrieving the metadata and contents of the images, videos, and albums on a mobile device. +version: 2.1.0 homepage: https://github.com/csdcorp/local_image_provider environment: diff --git a/test/local_image_provider_test.dart b/test/local_image_provider_test.dart index d699c09..4acc70e 100644 --- a/test/local_image_provider_test.dart +++ b/test/local_image_provider_test.dart @@ -10,12 +10,15 @@ import 'package:local_image_provider/local_image_provider.dart'; void main() { LocalImageProvider localImageProvider; bool initResponse; + bool cleanupResponse; bool hasResponse; bool pluginInvocation; List photoJsonList = []; List albumJsonList = []; const String noSuchImageId = "noSuchImage"; const String firstImageId = "image1"; + const String firstVideoId = "video1"; + const String firstVideoPath = "/tmp/video1"; const String firstPhotoJson = '{"id":"$firstImageId","creationDate":"2019-01-01 12:12Z","pixelWidth":1920,"pixelHeight":1024}'; const String secondPhotoJson = @@ -33,6 +36,7 @@ void main() { setUp(() { initResponse = true; hasResponse = true; + cleanupResponse = true; List imgInt = imageBytesStr.codeUnits; imageBytes = Uint8List.fromList(imgInt); pluginInvocation = false; @@ -41,32 +45,52 @@ void main() { localImageProvider.channel .setMockMethodCallHandler((MethodCall methodCall) async { pluginInvocation = true; - if (methodCall.method == "has_permission") { - return hasResponse; - } else if (methodCall.method == "initialize") { - return initResponse; - } else if (methodCall.method == "latest_images") { - return photoJsonList; - } else if (methodCall.method == "request_permission") { - return true; - } else if (methodCall.method == "image_bytes") { - if (null == methodCall.arguments) {} - String imgId = methodCall.arguments["id"]; - if (noSuchImageId == imgId) { - throw PlatformException( - code: "imgNotFound", message: "$noSuchImageId not found"); - } - return imageBytes; - } else if (methodCall.method == "albums") { - return albumJsonList; - } else if (methodCall.method == "images_in_album") { - if (methodCall.arguments["albumId"] == emptyAlbumId) { - return []; - } else { + switch (methodCall.method) { + case "has_permission": + return hasResponse; + break; + case "initialize": + return initResponse; + break; + case "cleanup": + return cleanupResponse; + break; + case "latest_images": return photoJsonList; - } + break; + case "request_permission": + return true; + break; + case "image_bytes": + if (null == methodCall.arguments) {} + String imgId = methodCall.arguments["id"]; + if (noSuchImageId == imgId) { + throw PlatformException( + code: "imgNotFound", message: "$noSuchImageId not found"); + } + return imageBytes; + break; + case "albums": + return albumJsonList; + break; + case "video_file": + String imgId = methodCall.arguments["id"]; + if (noSuchImageId == imgId) { + throw PlatformException( + code: "imgNotFound", message: "$noSuchImageId not found"); + } + return firstVideoPath; + break; + case "images_in_album": + if (methodCall.arguments["albumId"] == emptyAlbumId) { + return []; + } else { + return photoJsonList; + } + break; + default: + return Future.value(true); } - return Future.value(true); }); }); @@ -246,6 +270,44 @@ void main() { } }); + group('videoFile', () { + test('fails if not initialized', () async { + try { + await localImageProvider.videoFile(firstVideoId); + fail("Should have thrown"); + } catch (e) { + // expected + } + }); + test('Returns expected video file', () async { + await localImageProvider.initialize(); + var path = await localImageProvider.videoFile(firstVideoId); + expect(path, firstVideoPath); + }); + test('Handles video file not found', () async { + await localImageProvider.initialize(); + try { + await localImageProvider.videoFile(noSuchImageId); + fail("Expected PlatformException"); + } on PlatformException catch (e) { + expect(e.code, "imgNotFound"); + } + }); + }); + group('cleanup', () { + test('fails if not initialized', () async { + try { + await localImageProvider.cleanup(); + fail("Should have thrown"); + } catch (e) { + // expected + } + }); + test('works silently if initialized', () async { + await localImageProvider.initialize(); + await localImageProvider.cleanup(); + }); + }); group('stats', () { test('start at 0', () { expect(localImageProvider.imgBytesLoaded, 0);