From f6cd2d4b354afa8bb00031b06b78995b8dc0202c Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 29 Jun 2020 09:43:03 -0700 Subject: [PATCH] Don't access clipboard passively on iOS (#60316) --- .../lib/src/widgets/text_selection.dart | 19 +++++++ .../test/cupertino/text_selection_test.dart | 9 ++-- .../test/material/text_selection_test.dart | 54 ++++++++++++++++++- 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index 4000b61503..046aa8f02d 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -1522,6 +1522,25 @@ class ClipboardStatusNotifier extends ValueNotifier with Widget /// Check the [Clipboard] and update [value] if needed. void update() { + // iOS 14 added a notification that appears when an app accesses the + // clipboard. To avoid the notification, don't access the clipboard on iOS, + // and instead always shown the paste button, even when the clipboard is + // empty. + // TODO(justinmc): Use the new iOS 14 clipboard API method hasStrings that + // won't trigger the notification. + // https://github.com/flutter/flutter/issues/60145 + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + value = ClipboardStatus.pasteable; + return; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + break; + } + Clipboard.getData(Clipboard.kTextPlain).then((ClipboardData data) { final ClipboardStatus clipboardStatus = data != null && data.text != null && data.text.isNotEmpty ? ClipboardStatus.pasteable diff --git a/packages/flutter/test/cupertino/text_selection_test.dart b/packages/flutter/test/cupertino/text_selection_test.dart index 35bed86a9b..f77d9267e2 100644 --- a/packages/flutter/test/cupertino/text_selection_test.dart +++ b/packages/flutter/test/cupertino/text_selection_test.dart @@ -176,7 +176,8 @@ void main() { }); }); - testWidgets('Paste only appears when clipboard has contents', (WidgetTester tester) async { + // TODO(justinmc): https://github.com/flutter/flutter/issues/60145 + testWidgets('Paste always appears regardless of clipboard content on iOS', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); @@ -202,8 +203,8 @@ void main() { await tester.tapAt(textOffsetToPosition(tester, index)); await tester.pumpAndSettle(); - // No Paste yet, because nothing has been copied. - expect(find.text('Paste'), findsNothing); + // Paste is showing even though clipboard is empty. + expect(find.text('Paste'), findsOneWidget); expect(find.text('Copy'), findsOneWidget); expect(find.text('Cut'), findsOneWidget); @@ -219,7 +220,7 @@ void main() { await tester.tapAt(textOffsetToPosition(tester, index)); await tester.pumpAndSettle(); - // Paste now shows. + // Paste still shows. expect(find.text('Paste'), findsOneWidget); expect(find.text('Copy'), findsOneWidget); expect(find.text('Cut'), findsOneWidget); diff --git a/packages/flutter/test/material/text_selection_test.dart b/packages/flutter/test/material/text_selection_test.dart index 518923bd2a..ec36761010 100644 --- a/packages/flutter/test/material/text_selection_test.dart +++ b/packages/flutter/test/material/text_selection_test.dart @@ -636,5 +636,57 @@ void main() { expect(find.text('Cut'), findsOneWidget); expect(find.text('Paste'), findsOneWidget); expect(find.text('Select all'), findsOneWidget); - }, skip: isBrowser); + }, skip: isBrowser, variant: const TargetPlatformVariant({ TargetPlatform.android })); + + // TODO(justinmc): https://github.com/flutter/flutter/issues/60145 + testWidgets('Paste always appears regardless of clipboard content on iOS', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'Atwater Peel Sherbrooke Bonaventure', + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Column( + children: [ + TextField( + controller: controller, + ), + ], + ), + ), + ), + ); + + // Make sure the clipboard is empty. + await Clipboard.setData(const ClipboardData(text: '')); + + // Double tap to select the first word. + const int index = 4; + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pumpAndSettle(); + + // Paste is showing even though clipboard is empty. + expect(find.text('Paste'), findsOneWidget); + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Cut'), findsOneWidget); + + // Tap copy to add something to the clipboard and close the menu. + await tester.tapAt(tester.getCenter(find.text('Copy'))); + await tester.pumpAndSettle(); + expect(find.text('Copy'), findsNothing); + expect(find.text('Cut'), findsNothing); + + // Double tap to show the menu again. + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pump(const Duration(milliseconds: 50)); + await tester.tapAt(textOffsetToPosition(tester, index)); + await tester.pumpAndSettle(); + + // Paste still shows. + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Cut'), findsOneWidget); + expect(find.text('Paste'), findsOneWidget); + }, skip: isBrowser, variant: const TargetPlatformVariant({ TargetPlatform.iOS })); }