From 2efb3c4215d6bc545b014213837b7da564b06d63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Ali=20Ustao=C4=9Flu?= Date: Sat, 24 Oct 2026 20:55:42 +0200 Subject: [PATCH] path_provider_watchos 0.1.1: upstream's example and test, and iOS's behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The example was a hand-written demo, and its integration test a copy of upstream's "trimmed to the directories that exist on watchOS". AUTHORING.md §3 asks for upstream's own example and official test, unchanged, so both are now path_provider's, ported with `flutter-watchos plugin port --include-example` (#24). Upstream's test then showed two gaps, fixed in the implementation as §3 asks: - getDownloadsPath threw UnimplementedError. It now returns the user-domain NSDownloadsDirectory path, the lookup path_provider_foundation makes on iOS, through a new FFI export, path_provider_watchos_downloads_path. - The external storage getters threw UnimplementedError. They now throw UnsupportedError, as on iOS. On the watchOS 27.0 Simulator all 16 of upstream's tests pass, and the example starts with no exception in its first 15 seconds and no overflow. --- packages/path_provider_watchos/CHANGELOG.md | 9 + .../path_provider_watchos/PORTING_REPORT.md | 15 +- packages/path_provider_watchos/README.md | 13 +- .../integration_test/path_provider_test.dart | 154 +++++++-- .../example/lib/main.dart | 305 ++++++++++++++++-- .../example/lib/readme_excerpts.dart | 20 ++ .../lib/path_provider_watchos.dart | 34 +- packages/path_provider_watchos/pubspec.yaml | 3 +- .../test/path_provider_watchos_test.dart | 18 +- .../Classes/path_provider_watchos_ffi.h | 5 + .../Classes/path_provider_watchos_ffi.m | 17 + 11 files changed, 515 insertions(+), 78 deletions(-) create mode 100644 packages/path_provider_watchos/example/lib/readme_excerpts.dart diff --git a/packages/path_provider_watchos/CHANGELOG.md b/packages/path_provider_watchos/CHANGELOG.md index de49a7f..8650371 100644 --- a/packages/path_provider_watchos/CHANGELOG.md +++ b/packages/path_provider_watchos/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.1.1 + +* `getDownloadsPath` returns the app's `NSDownloadsDirectory` path, and the + external storage getters throw `UnsupportedError`, as on iOS. Before, all + four threw `UnimplementedError`. +* Example: `path_provider`'s own example app and its official integration + test, unchanged, replace a hand-written demo and a trimmed copy of the + test (`AUTHORING.md` §3). + ## 0.1.0 * The version moves from 0.0.1 to 0.1.0, so that a `^0.1.0` constraint diff --git a/packages/path_provider_watchos/PORTING_REPORT.md b/packages/path_provider_watchos/PORTING_REPORT.md index 89484cc..944001a 100644 --- a/packages/path_provider_watchos/PORTING_REPORT.md +++ b/packages/path_provider_watchos/PORTING_REPORT.md @@ -5,9 +5,9 @@ | Aspect | Result | |---|---| | Implementation | ✅ Working (FFI) | -| watchOS capability | Partial — no Downloads / Android external dirs (throw `UnimplementedError`) | +| watchOS capability | Full, as on iOS — Downloads is the app's `NSDownloadsDirectory` path; the Android-only external directories throw `UnsupportedError` | | Host unit tests (`flutter-watchos test`) | ✅ pass | -| Upstream integration test | ✅ passes verbatim | +| Upstream integration test | ✅ passes verbatim on the watchOS 27.0 Simulator (16 tests, 2026-10-24) | | Internal unified demo | ✅ included | Marking: ✅ full / passes · ◐ partial — reason given · ○ not applicable (no upstream test) · ✗ unsupported on watchOS. @@ -28,6 +28,13 @@ is that skeleton **finished by hand** as a working FFI implementation. `path_provider_platform_interface` and resolves the symbols via `DynamicLibrary.process()`. +## Example + +`path_provider`'s own example app and its official integration test, +ported unchanged with `flutter-watchos plugin port --include-example`. There +are no watchOS deviations. On the watchOS 27.0 Simulator the example starts +with no exception in its first 15 seconds and no overflow. + ## Interface coverage Every public member of `path_provider_platform_interface` 2.1.3 @@ -43,8 +50,8 @@ watchOS" table lists the ✗ rows. | `getLibraryPath` | ✅ `NSLibraryDirectory` | | `getApplicationDocumentsPath` | ✅ `NSDocumentDirectory` | | `getApplicationCachePath` | ✅ `NSCachesDirectory` | -| `getDownloadsPath` | ✗ throws `UnimplementedError`: watchOS has no Downloads directory | -| `getExternalStoragePath` / `getExternalStoragePaths` / `getExternalCachePaths` | ✗ throw `UnimplementedError`: external storage is an Android concept | +| `getDownloadsPath` | ✅ `NSDownloadsDirectory`, as on iOS (not created) | +| `getExternalStoragePath` / `getExternalStoragePaths` / `getExternalCachePaths` | ✗ throw `UnsupportedError`, as on iOS: external storage is an Android concept | --- diff --git a/packages/path_provider_watchos/README.md b/packages/path_provider_watchos/README.md index ccb2249..373c1c7 100644 --- a/packages/path_provider_watchos/README.md +++ b/packages/path_provider_watchos/README.md @@ -34,8 +34,17 @@ These members of `path_provider_platform_interface` 2.1.3 throw or fail on watch | Member | On watchOS | Why | |---|---|---| -| `getDownloadsPath` | throws `UnimplementedError` | watchOS has no Downloads directory | -| `getExternalStoragePath` / `getExternalStoragePaths` / `getExternalCachePaths` | throw `UnimplementedError` | external storage is an Android concept | +| `getExternalStoragePath` / `getExternalStoragePaths` / `getExternalCachePaths` | throw `UnsupportedError`, as on iOS | external storage is an Android concept | + +`getDownloadsPath` returns the app's `NSDownloadsDirectory` path, as on iOS. +The directory is not created. + +## Example + +The example is `path_provider`'s own example app, and +`example/integration_test/path_provider_test.dart` is its official test, +both unchanged, ported with `flutter-watchos plugin port --include-example`. +There are no watchOS deviations. ## License diff --git a/packages/path_provider_watchos/example/integration_test/path_provider_test.dart b/packages/path_provider_watchos/example/integration_test/path_provider_test.dart index 103e14b..c311258 100644 --- a/packages/path_provider_watchos/example/integration_test/path_provider_test.dart +++ b/packages/path_provider_watchos/example/integration_test/path_provider_test.dart @@ -1,13 +1,8 @@ -// Copyright 2026 The FlutterWatch Authors. All rights reserved. +// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// -// Runs on the watch simulator against the real FFI implementation -// (path_provider_watchos). Adapted from the upstream path_provider example's -// integration_test, trimmed to the directories that exist on watchOS. import 'dart:io'; - import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path_provider/path_provider.dart'; @@ -15,37 +10,136 @@ import 'package:path_provider/path_provider.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('getTemporaryDirectory returns a usable directory', - (WidgetTester _) async { - final Directory dir = await getTemporaryDirectory(); - expect(dir.path, isNotEmpty); - expect(dir.existsSync(), isTrue); + testWidgets('getTemporaryDirectory', (WidgetTester tester) async { + final Directory result = await getTemporaryDirectory(); + // getTemporaryDirectory does not guarantee that the returned directory exists. + _verifySampleFile(result, 'temporaryDirectory', createDirectory: true); + }); + + testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { + final Directory result = await getApplicationDocumentsDirectory(); + if (Platform.isMacOS) { + // _verifySampleFile causes hangs in driver when sandboxing is disabled + // because the path changes from an app specific directory to + // ~/Documents, which requires additional permissions to access on macOS. + // Instead, validate that a non-empty path was returned. + expect(result.path, isNotEmpty); + } else { + _verifySampleFile(result, 'applicationDocuments'); + } + }); + + testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { + final Directory result = await getApplicationSupportDirectory(); + _verifySampleFile(result, 'applicationSupport'); + }); + + testWidgets('getApplicationCacheDirectory', (WidgetTester tester) async { + final Directory result = await getApplicationCacheDirectory(); + _verifySampleFile(result, 'applicationCache'); }); - testWidgets('getApplicationSupportDirectory is created on demand', - (WidgetTester _) async { - final Directory dir = await getApplicationSupportDirectory(); - expect(dir.path, isNotEmpty); - expect(dir.existsSync(), isTrue); + testWidgets('getLibraryDirectory', (WidgetTester tester) async { + if (Platform.isIOS) { + final Directory result = await getLibraryDirectory(); + _verifySampleFile(result, 'library'); + } else if (Platform.isAndroid) { + final Future result = getLibraryDirectory(); + await expectLater(result, throwsA(isInstanceOf())); + } }); - testWidgets('getApplicationDocumentsDirectory returns a path', - (WidgetTester _) async { - final Directory dir = await getApplicationDocumentsDirectory(); - expect(dir.path, isNotEmpty); + testWidgets('getExternalStorageDirectory', (WidgetTester tester) async { + if (Platform.isIOS) { + final Future result = getExternalStorageDirectory(); + await expectLater(result, throwsA(isInstanceOf())); + } else if (Platform.isAndroid) { + final Directory? result = await getExternalStorageDirectory(); + _verifySampleFile(result, 'externalStorage'); + } }); - testWidgets('getApplicationCacheDirectory returns a path', - (WidgetTester _) async { - final Directory dir = await getApplicationCacheDirectory(); - expect(dir.path, isNotEmpty); + testWidgets('getExternalCacheDirectories', (WidgetTester tester) async { + if (Platform.isIOS) { + final Future?> result = getExternalCacheDirectories(); + await expectLater(result, throwsA(isInstanceOf())); + } else if (Platform.isAndroid) { + final List? directories = await getExternalCacheDirectories(); + expect(directories, isNotNull); + for (final Directory result in directories!) { + _verifySampleFile(result, 'externalCache'); + } + } }); - testWidgets('temp directory round-trips a file', (WidgetTester _) async { - final Directory dir = await getTemporaryDirectory(); - final File f = File('${dir.path}/probe.txt'); - await f.writeAsString('hello watch'); - expect(await f.readAsString(), 'hello watch'); - await f.delete(); + final allDirs = [ + null, + StorageDirectory.music, + StorageDirectory.podcasts, + StorageDirectory.ringtones, + StorageDirectory.alarms, + StorageDirectory.notifications, + StorageDirectory.pictures, + StorageDirectory.movies, + ]; + + for (final type in allDirs) { + testWidgets('getExternalStorageDirectories (type: $type)', + (WidgetTester tester) async { + if (Platform.isIOS) { + final Future?> result = getExternalStorageDirectories(); + await expectLater(result, throwsA(isInstanceOf())); + } else if (Platform.isAndroid) { + final List? directories = + await getExternalStorageDirectories(type: type); + expect(directories, isNotNull); + for (final Directory result in directories!) { + _verifySampleFile(result, '$type'); + } + } + }); + } + + testWidgets('getDownloadsDirectory', (WidgetTester tester) async { + final Directory? result = await getDownloadsDirectory(); + // On recent versions of macOS, actually using the downloads directory + // requires a user prompt (so will fail on CI), and on some platforms the + // directory may not exist. Instead of verifying that it exists, just + // check that it returned a path. + expect(result?.path, isNotEmpty); }); } + +/// Verify a file called [name] in [directory] by recreating it with test +/// contents when necessary. +/// +/// If [createDirectory] is true, the directory will be created if missing. +void _verifySampleFile(Directory? directory, String name, + {bool createDirectory = false}) { + expect(directory, isNotNull); + if (directory == null) { + return; + } + final file = File('${directory.path}/$name'); + + if (file.existsSync()) { + file.deleteSync(); + expect(file.existsSync(), isFalse); + } + + if (createDirectory && !directory.existsSync()) { + directory.createSync(recursive: true); + } + + file.writeAsStringSync('Hello world!'); + expect(file.readAsStringSync(), 'Hello world!'); + // This check intentionally avoids using Directory.listSync on Android due to + // https://github.com/dart-lang/sdk/issues/54287. + if (Platform.isAndroid) { + expect( + Process.runSync('ls', [directory.path]).stdout, contains(name)); + } else { + expect(directory.listSync(), isNotEmpty); + } + file.deleteSync(); +} diff --git a/packages/path_provider_watchos/example/lib/main.dart b/packages/path_provider_watchos/example/lib/main.dart index 1f11b4a..d64c324 100644 --- a/packages/path_provider_watchos/example/lib/main.dart +++ b/packages/path_provider_watchos/example/lib/main.dart @@ -1,65 +1,300 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; -void main() => runApp(const ExampleApp()); +void main() { + runApp(const MyApp()); +} -class ExampleApp extends StatelessWidget { - const ExampleApp({super.key}); +class MyApp extends StatelessWidget { + const MyApp({super.key}); @override Widget build(BuildContext context) { - return const MaterialApp(home: PathList()); + return MaterialApp( + title: 'Path Provider', + theme: ThemeData(primarySwatch: Colors.blue), + home: const MyHomePage(title: 'Path Provider'), + ); } } -class PathList extends StatefulWidget { - const PathList({super.key}); +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + final String title; @override - State createState() => _PathListState(); + State createState() => _MyHomePageState(); } -class _PathListState extends State { - final Map _paths = {}; +class _MyHomePageState extends State { + Future? _tempDirectory; + Future? _appSupportDirectory; + Future? _appLibraryDirectory; + Future? _appDocumentsDirectory; + Future? _appCacheDirectory; + Future? _externalDocumentsDirectory; + Future?>? _externalStorageDirectories; + Future?>? _externalCacheDirectories; + Future? _downloadsDirectory; - @override - void initState() { - super.initState(); - _load(); + void _requestTempDirectory() { + setState(() { + _tempDirectory = getTemporaryDirectory(); + }); } - Future _load() async { - Future put(String label, Future Function() f) async { - try { - final Object? dir = await f(); - _paths[label] = dir?.toString() ?? '(null)'; - } catch (e) { - _paths[label] = 'unsupported'; + Widget _buildDirectory( + BuildContext context, AsyncSnapshot snapshot) { + var text = const Text(''); + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + text = Text('Error: ${snapshot.error}'); + } else if (snapshot.hasData) { + text = Text('path: ${snapshot.data!.path}'); + } else { + text = const Text('path unavailable'); } - if (mounted) { - setState(() {}); + } + return Padding(padding: const EdgeInsets.all(16.0), child: text); + } + + Widget _buildDirectories( + BuildContext context, AsyncSnapshot?> snapshot) { + var text = const Text(''); + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + text = Text('Error: ${snapshot.error}'); + } else if (snapshot.hasData) { + final String combined = + snapshot.data!.map((Directory d) => d.path).join(', '); + text = Text('paths: $combined'); + } else { + text = const Text('path unavailable'); } } + return Padding(padding: const EdgeInsets.all(16.0), child: text); + } + + void _requestAppDocumentsDirectory() { + setState(() { + _appDocumentsDirectory = getApplicationDocumentsDirectory(); + }); + } + + void _requestAppSupportDirectory() { + setState(() { + _appSupportDirectory = getApplicationSupportDirectory(); + }); + } + + void _requestAppLibraryDirectory() { + setState(() { + _appLibraryDirectory = getLibraryDirectory(); + }); + } - await put('temp', getTemporaryDirectory); - await put('support', getApplicationSupportDirectory); - await put('library', getLibraryDirectory); - await put('documents', getApplicationDocumentsDirectory); - await put('cache', getApplicationCacheDirectory); + void _requestAppCacheDirectory() { + setState(() { + _appCacheDirectory = getApplicationCacheDirectory(); + }); + } + + void _requestExternalStorageDirectory() { + setState(() { + _externalDocumentsDirectory = getExternalStorageDirectory(); + }); + } + + void _requestExternalStorageDirectories(StorageDirectory type) { + setState(() { + _externalStorageDirectories = getExternalStorageDirectories(type: type); + }); + } + + void _requestExternalCacheDirectories() { + setState(() { + _externalCacheDirectories = getExternalCacheDirectories(); + }); + } + + void _requestDownloadsDirectory() { + setState(() { + _downloadsDirectory = getDownloadsDirectory(); + }); } @override Widget build(BuildContext context) { return Scaffold( - body: ListView( - children: [ - for (final MapEntry e in _paths.entries) - ListTile( - dense: true, - title: Text(e.key, style: const TextStyle(fontSize: 12)), - subtitle: Text(e.value, style: const TextStyle(fontSize: 9)), + appBar: AppBar(title: Text(widget.title)), + body: Center( + child: ListView( + children: [ + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestTempDirectory, + child: const Text('Get Temporary Directory'), + ), + ), + FutureBuilder( + future: _tempDirectory, builder: _buildDirectory), + ], + ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestAppDocumentsDirectory, + child: const Text('Get Application Documents Directory'), + ), + ), + FutureBuilder( + future: _appDocumentsDirectory, builder: _buildDirectory), + ], + ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestAppSupportDirectory, + child: const Text('Get Application Support Directory'), + ), + ), + FutureBuilder( + future: _appSupportDirectory, builder: _buildDirectory), + ], + ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: + Platform.isAndroid ? null : _requestAppLibraryDirectory, + child: Text( + Platform.isAndroid + ? 'Application Library Directory unavailable' + : 'Get Application Library Directory', + ), + ), + ), + FutureBuilder( + future: _appLibraryDirectory, builder: _buildDirectory), + ], + ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: _requestAppCacheDirectory, + child: const Text('Get Application Cache Directory'), + ), + ), + FutureBuilder( + future: _appCacheDirectory, builder: _buildDirectory), + ], + ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: !Platform.isAndroid + ? null + : _requestExternalStorageDirectory, + child: Text( + !Platform.isAndroid + ? 'External storage is unavailable' + : 'Get External Storage Directory', + ), + ), + ), + FutureBuilder( + future: _externalDocumentsDirectory, + builder: _buildDirectory, + ), + ], + ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: !Platform.isAndroid + ? null + : () { + _requestExternalStorageDirectories( + StorageDirectory.music); + }, + child: Text( + !Platform.isAndroid + ? 'External directories are unavailable' + : 'Get External Storage Directories', + ), + ), + ), + FutureBuilder?>( + future: _externalStorageDirectories, + builder: _buildDirectories, + ), + ], + ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: !Platform.isAndroid + ? null + : _requestExternalCacheDirectories, + child: Text( + !Platform.isAndroid + ? 'External directories are unavailable' + : 'Get External Cache Directories', + ), + ), + ), + FutureBuilder?>( + future: _externalCacheDirectories, + builder: _buildDirectories, + ), + ], + ), + Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: Platform.isAndroid || Platform.isIOS + ? null + : _requestDownloadsDirectory, + child: Text( + Platform.isAndroid || Platform.isIOS + ? 'Downloads directory is unavailable' + : 'Get Downloads Directory', + ), + ), + ), + FutureBuilder( + future: _downloadsDirectory, builder: _buildDirectory), + ], ), - ], + ], + ), ), ); } diff --git a/packages/path_provider_watchos/example/lib/readme_excerpts.dart b/packages/path_provider_watchos/example/lib/readme_excerpts.dart new file mode 100644 index 0000000..0c85da5 --- /dev/null +++ b/packages/path_provider_watchos/example/lib/readme_excerpts.dart @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:path_provider/path_provider.dart'; + +/// Runs example code for README.md. +Future> readmeSnippets() async { + // #docregion Example + final Directory tempDir = await getTemporaryDirectory(); + + final Directory appDocumentsDir = await getApplicationDocumentsDirectory(); + + final Directory? downloadsDir = await getDownloadsDirectory(); + // #enddocregion Example + + return [tempDir, appDocumentsDir, downloadsDir]; +} diff --git a/packages/path_provider_watchos/lib/path_provider_watchos.dart b/packages/path_provider_watchos/lib/path_provider_watchos.dart index 16dd7ce..c40db3b 100644 --- a/packages/path_provider_watchos/lib/path_provider_watchos.dart +++ b/packages/path_provider_watchos/lib/path_provider_watchos.dart @@ -49,6 +49,10 @@ class PathProviderWatchosBindings { .lookupFunction Function(), Pointer Function()>( 'path_provider_watchos_cache_path'); + late final Pointer Function() _downloadsPath = _lib! + .lookupFunction Function(), Pointer Function()>( + 'path_provider_watchos_downloads_path'); + String? _string(Pointer Function() f) { final Pointer p = f(); // Native pointers are cached per-process and owned by the plugin — never @@ -70,6 +74,9 @@ class PathProviderWatchosBindings { /// `NSCachesDirectory`. String? get cachePath => _string(_cachePath); + + /// `NSDownloadsDirectory`, as iOS path_provider reports it; not created. + String? get downloadsPath => _string(_downloadsPath); } /// watchOS implementation of [PathProviderPlatform]. @@ -104,7 +111,28 @@ base class PathProviderWatchos extends PathProviderPlatform { @override Future getApplicationCachePath() async => _b.cachePath; - // The remaining interface methods (external storage, downloads) describe - // directories that do not exist on watchOS; the base class's - // UnimplementedError defaults are the correct behaviour. + @override + Future getDownloadsPath() async => _b.downloadsPath; + + // External storage is an Android concept. As on iOS (path_provider_foundation), + // these getters throw UnsupportedError. + + @override + Future getExternalStoragePath() async { + throw UnsupportedError( + 'getExternalStoragePath is not supported on watchOS'); + } + + @override + Future?> getExternalCachePaths() async { + throw UnsupportedError('getExternalCachePaths is not supported on watchOS'); + } + + @override + Future?> getExternalStoragePaths({ + StorageDirectory? type, + }) async { + throw UnsupportedError( + 'getExternalStoragePaths is not supported on watchOS'); + } } diff --git a/packages/path_provider_watchos/pubspec.yaml b/packages/path_provider_watchos/pubspec.yaml index 13a757d..7c43447 100644 --- a/packages/path_provider_watchos/pubspec.yaml +++ b/packages/path_provider_watchos/pubspec.yaml @@ -1,6 +1,6 @@ name: path_provider_watchos description: "watchOS implementation of the path_provider plugin, using dart:ffi over Foundation to resolve the temporary, application-support, documents, and cache directories." -version: 0.1.0 +version: 0.1.1 homepage: https://flutterwatch.dev repository: https://github.com/flutterwatch/plugins/tree/main/packages/path_provider_watchos issue_tracker: https://github.com/flutterwatch/plugins/issues @@ -49,3 +49,4 @@ flutter: - path_provider_watchos_library_path - path_provider_watchos_documents_path - path_provider_watchos_cache_path + - path_provider_watchos_downloads_path diff --git a/packages/path_provider_watchos/test/path_provider_watchos_test.dart b/packages/path_provider_watchos/test/path_provider_watchos_test.dart index b8cec5b..5889c86 100644 --- a/packages/path_provider_watchos/test/path_provider_watchos_test.dart +++ b/packages/path_provider_watchos/test/path_provider_watchos_test.dart @@ -20,6 +20,8 @@ class _FakeBindings extends PathProviderWatchosBindings { String? get documentsPath => '/sandbox/Documents'; @override String? get cachePath => '/sandbox/Library/Caches'; + @override + String? get downloadsPath => '/sandbox/Downloads'; } void main() { @@ -40,11 +42,21 @@ void main() { expect(await provider.getLibraryPath(), '/sandbox/Library'); expect(await provider.getApplicationDocumentsPath(), '/sandbox/Documents'); expect(await provider.getApplicationCachePath(), '/sandbox/Library/Caches'); + expect(await provider.getDownloadsPath(), '/sandbox/Downloads'); }); - test('watchOS-inapplicable directories keep throwing defaults', () { + test('the external storage getters throw UnsupportedError, as on iOS', + () async { final PathProviderWatchos provider = PathProviderWatchos(); - expect(provider.getDownloadsPath, throwsUnimplementedError); - expect(provider.getExternalStoragePath, throwsUnimplementedError); + await expectLater( + provider.getExternalStoragePath(), throwsUnsupportedError); + await expectLater(provider.getExternalCachePaths(), throwsUnsupportedError); + for (final StorageDirectory? type in [ + null, + ...StorageDirectory.values, + ]) { + await expectLater( + provider.getExternalStoragePaths(type: type), throwsUnsupportedError); + } }); } diff --git a/packages/path_provider_watchos/watchos/Classes/path_provider_watchos_ffi.h b/packages/path_provider_watchos/watchos/Classes/path_provider_watchos_ffi.h index 11869b6..70aa811 100644 --- a/packages/path_provider_watchos/watchos/Classes/path_provider_watchos_ffi.h +++ b/packages/path_provider_watchos/watchos/Classes/path_provider_watchos_ffi.h @@ -38,4 +38,9 @@ PATH_PROVIDER_WATCHOS_EXPORT const char* path_provider_watchos_documents_path(vo /// `NSCachesDirectory` in the user domain. PATH_PROVIDER_WATCHOS_EXPORT const char* path_provider_watchos_cache_path(void); +/// `NSDownloadsDirectory` in the user domain, as +/// `NSSearchPathForDirectoriesInDomains` reports it. This is the path iOS +/// path_provider returns; the directory is not created. +PATH_PROVIDER_WATCHOS_EXPORT const char* path_provider_watchos_downloads_path(void); + #endif // PATH_PROVIDER_WATCHOS_FFI_H diff --git a/packages/path_provider_watchos/watchos/Classes/path_provider_watchos_ffi.m b/packages/path_provider_watchos/watchos/Classes/path_provider_watchos_ffi.m index 6823c3c..438a6e5 100644 --- a/packages/path_provider_watchos/watchos/Classes/path_provider_watchos_ffi.m +++ b/packages/path_provider_watchos/watchos/Classes/path_provider_watchos_ffi.m @@ -83,3 +83,20 @@ }); return path; } + +const char* path_provider_watchos_downloads_path(void) { + static const char* path = NULL; + static dispatch_once_t once; + dispatch_once(&once, ^{ + @autoreleasepool { + // The lookup iOS path_provider makes: the user-domain path, + // whether or not the directory exists yet. + NSString* downloads = NSSearchPathForDirectoriesInDomains( + NSDownloadsDirectory, NSUserDomainMask, YES).firstObject; + if (downloads != nil) { + path = strdup(downloads.UTF8String); + } + } + }); + return path; +}