Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/path_provider_watchos/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 11 additions & 4 deletions packages/path_provider_watchos/PORTING_REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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 |

---

Expand Down
13 changes: 11 additions & 2 deletions packages/path_provider_watchos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,145 @@
// 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';

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<Directory?> result = getLibraryDirectory();
await expectLater(result, throwsA(isInstanceOf<UnsupportedError>()));
}
});

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<Directory?> result = getExternalStorageDirectory();
await expectLater(result, throwsA(isInstanceOf<UnsupportedError>()));
} 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<List<Directory>?> result = getExternalCacheDirectories();
await expectLater(result, throwsA(isInstanceOf<UnsupportedError>()));
} else if (Platform.isAndroid) {
final List<Directory>? 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 = <StorageDirectory?>[
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<List<Directory>?> result = getExternalStorageDirectories();
await expectLater(result, throwsA(isInstanceOf<UnsupportedError>()));
} else if (Platform.isAndroid) {
final List<Directory>? 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', <String>[directory.path]).stdout, contains(name));
} else {
expect(directory.listSync(), isNotEmpty);
}
file.deleteSync();
}
Loading
Loading