Add a11y support for selected text (#14254)

Framework side for https://github.com/flutter/engine/pull/4584 & https://github.com/flutter/engine/pull/4587.

Also rolls engine to 4c82c566edf394a5cfc237a266aea5bd37a6c172.
diff --git a/bin/internal/engine.version b/bin/internal/engine.version
index 1d937e4..61f9bc0 100644
--- a/bin/internal/engine.version
+++ b/bin/internal/engine.version
@@ -1 +1 @@
-93296fb4ea653a3064643266d89dddd97d062f4a
+4c82c566edf394a5cfc237a266aea5bd37a6c172
diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart
index 3a4a36c..63b95b8 100644
--- a/packages/flutter/lib/src/rendering/editable.dart
+++ b/packages/flutter/lib/src/rendering/editable.dart
@@ -357,6 +357,7 @@
       ..isTextField = true;
 
     if (_selection?.isValid == true) {
+      config.textSelection = _selection;
       if (_textPainter.getOffsetBefore(_selection.extentOffset) != null)
         config.onMoveCursorBackwardByCharacter = _handleMoveCursorBackwardByCharacter;
       if (_textPainter.getOffsetAfter(_selection.extentOffset) != null)
diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart
index 11ad6f0..407d1ae 100644
--- a/packages/flutter/lib/src/semantics/semantics.dart
+++ b/packages/flutter/lib/src/semantics/semantics.dart
@@ -90,6 +90,7 @@
     @required this.hint,
     @required this.textDirection,
     @required this.rect,
+    @required this.textSelection,
     this.tags,
     this.transform,
   }) : assert(flags != null),
@@ -143,6 +144,10 @@
   /// [increasedValue], and [decreasedValue].
   final TextDirection textDirection;
 
+  /// The currently selected text (or the position of the cursor) within [value]
+  /// if this node represents a text field.
+  final TextSelection textSelection;
+
   /// The bounding box for this node in its coordinate system.
   final Rect rect;
 
@@ -189,6 +194,8 @@
     properties.add(new StringProperty('decreasedValue', decreasedValue, defaultValue: ''));
     properties.add(new StringProperty('hint', hint, defaultValue: ''));
     properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
+    if (textSelection?.isValid == true)
+      properties.add(new MessageProperty('text selection', '[${textSelection.start}, ${textSelection.end}]'));
   }
 
   @override
@@ -206,11 +213,12 @@
         && typedOther.textDirection == textDirection
         && typedOther.rect == rect
         && setEquals(typedOther.tags, tags)
+        && typedOther.textSelection == textSelection
         && typedOther.transform == transform;
   }
 
   @override
-  int get hashCode => ui.hashValues(flags, actions, label, value, increasedValue, decreasedValue, hint, textDirection, rect, tags, transform);
+  int get hashCode => ui.hashValues(flags, actions, label, value, increasedValue, decreasedValue, hint, textDirection, rect, tags, textSelection, transform);
 }
 
 class _SemanticsDiagnosticableNode extends DiagnosticableNode<SemanticsNode> {
@@ -840,6 +848,7 @@
         _increasedValue != config.increasedValue ||
         _flags != config._flags ||
         _textDirection != config.textDirection ||
+        _textSelection != config._textSelection ||
         _actionsAsBits != config._actionsAsBits ||
         _mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
   }
@@ -906,6 +915,11 @@
   TextDirection get textDirection => _textDirection;
   TextDirection _textDirection = _kEmptyConfig.textDirection;
 
+  /// The currently selected text (or the position of the cursor) within [value]
+  /// if this node represents a text field.
+  TextSelection get textSelection => _textSelection;
+  TextSelection _textSelection;
+
   bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
 
   static final SemanticsConfiguration _kEmptyConfig = new SemanticsConfiguration();
@@ -936,6 +950,7 @@
     _textDirection = config.textDirection;
     _actions = new Map<SemanticsAction, _SemanticsActionHandler>.from(config._actions);
     _actionsAsBits = config._actionsAsBits;
+    _textSelection = config._textSelection;
     _mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants;
     _replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
 
@@ -965,6 +980,7 @@
     String decreasedValue = _decreasedValue;
     TextDirection textDirection = _textDirection;
     Set<SemanticsTag> mergedTags = tags == null ? null : new Set<SemanticsTag>.from(tags);
+    TextSelection textSelection = _textSelection;
 
     if (mergeAllDescendantsIntoThisNode) {
       _visitDescendants((SemanticsNode node) {
@@ -972,6 +988,7 @@
         flags |= node._flags;
         actions |= node._actionsAsBits;
         textDirection ??= node._textDirection;
+        textSelection ??= node._textSelection;
         if (value == '' || value == null)
           value = node._value;
         if (increasedValue == '' || increasedValue == null)
@@ -1010,6 +1027,7 @@
       rect: rect,
       transform: transform,
       tags: mergedTags,
+      textSelection: textSelection,
     );
   }
 
@@ -1043,6 +1061,8 @@
       increasedValue: data.increasedValue,
       hint: data.hint,
       textDirection: data.textDirection,
+      textSelectionBase: data.textSelection != null ? data.textSelection.baseOffset : -1,
+      textSelectionExtent: data.textSelection != null ? data.textSelection.extentOffset : -1,
       transform: data.transform?.storage ?? _kIdentityTransform,
       children: children,
     );
@@ -1110,6 +1130,8 @@
     properties.add(new StringProperty('decreasedValue', _decreasedValue, defaultValue: ''));
     properties.add(new StringProperty('hint', _hint, defaultValue: ''));
     properties.add(new EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null));
+    if (_textSelection?.isValid == true)
+      properties.add(new MessageProperty('text selection', '[${_textSelection.start}, ${_textSelection.end}]'));
   }
 
   /// Returns a string representation of this node and its descendants.
@@ -1819,6 +1841,16 @@
     _setFlag(SemanticsFlag.isTextField, value);
   }
 
+  /// The currently selected text (or the position of the cursor) within [value]
+  /// if this node represents a text field.
+  TextSelection get textSelection => _textSelection;
+  TextSelection _textSelection;
+  set textSelection(TextSelection value) {
+    assert(value != null);
+    _textSelection = value;
+    _hasBeenAnnotated = true;
+  }
+
   // TAGS
 
   /// The set of tags that this configuration wants to add to all child
@@ -1901,6 +1933,7 @@
     _actions.addAll(other._actions);
     _actionsAsBits |= other._actionsAsBits;
     _flags |= other._flags;
+    _textSelection ??= other._textSelection;
 
     textDirection ??= other.textDirection;
     _label = _concatStrings(
@@ -1941,6 +1974,7 @@
       .._hint = _hint
       .._flags = _flags
       .._tagsForChildren = _tagsForChildren
+      .._textSelection = _textSelection
       .._actionsAsBits = _actionsAsBits
       .._actions.addAll(_actions);
   }
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index 2cd1afc..6123185 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -1808,6 +1808,7 @@
           id: 2,
           textDirection: TextDirection.ltr,
           value: 'Guten Tag',
+          textSelection: const TextSelection.collapsed(offset: 9),
           actions: <SemanticsAction>[
             SemanticsAction.tap,
             SemanticsAction.moveCursorBackwardByCharacter,
@@ -1828,6 +1829,7 @@
         new TestSemantics.rootChild(
           id: 2,
           textDirection: TextDirection.ltr,
+          textSelection: const TextSelection.collapsed(offset: 4),
           value: 'Guten Tag',
           actions: <SemanticsAction>[
             SemanticsAction.tap,
@@ -1851,6 +1853,7 @@
         new TestSemantics.rootChild(
           id: 2,
           textDirection: TextDirection.ltr,
+          textSelection: const TextSelection.collapsed(offset: 0),
           value: 'Schönen Feierabend',
           actions: <SemanticsAction>[
             SemanticsAction.tap,
@@ -1867,4 +1870,84 @@
     semantics.dispose();
   });
 
+  testWidgets('TextField semantics for selections', (WidgetTester tester) async {
+    final SemanticsTester semantics = new SemanticsTester(tester);
+    final TextEditingController controller = new TextEditingController()
+      ..text = 'Hello';
+    final Key key = new UniqueKey();
+
+    await tester.pumpWidget(
+      overlay(
+          child: new TextField(
+            key: key,
+            controller: controller,
+          )
+      ),
+    );
+
+    expect(semantics, hasSemantics(new TestSemantics.root(
+      children: <TestSemantics>[
+        new TestSemantics.rootChild(
+          id: 2,
+          value: 'Hello',
+          textDirection: TextDirection.ltr,
+          actions: <SemanticsAction>[
+            SemanticsAction.tap,
+          ],
+          flags: <SemanticsFlag>[
+            SemanticsFlag.isTextField,
+          ],
+        ),
+      ],
+    ), ignoreTransform: true, ignoreRect: true));
+
+    // Focus the text field
+    await tester.tap(find.byKey(key));
+    await tester.pump();
+
+    expect(semantics, hasSemantics(new TestSemantics.root(
+      children: <TestSemantics>[
+        new TestSemantics.rootChild(
+          id: 2,
+          value: 'Hello',
+          textSelection: const TextSelection.collapsed(offset: 5),
+          textDirection: TextDirection.ltr,
+          actions: <SemanticsAction>[
+            SemanticsAction.tap,
+            SemanticsAction.moveCursorBackwardByCharacter,
+          ],
+          flags: <SemanticsFlag>[
+            SemanticsFlag.isTextField,
+            SemanticsFlag.isFocused,
+          ],
+        ),
+      ],
+    ), ignoreTransform: true, ignoreRect: true));
+
+    controller.selection = const TextSelection(baseOffset: 5, extentOffset: 3);
+    await tester.pump();
+
+    expect(semantics, hasSemantics(new TestSemantics.root(
+      children: <TestSemantics>[
+        new TestSemantics.rootChild(
+          id: 2,
+          value: 'Hello',
+          textSelection: const TextSelection(baseOffset: 5, extentOffset: 3),
+          textDirection: TextDirection.ltr,
+          actions: <SemanticsAction>[
+            SemanticsAction.tap,
+            SemanticsAction.moveCursorBackwardByCharacter,
+            SemanticsAction.moveCursorForwardByCharacter,
+          ],
+          flags: <SemanticsFlag>[
+            SemanticsFlag.isTextField,
+            SemanticsFlag.isFocused,
+          ],
+        ),
+      ],
+    ), ignoreTransform: true, ignoreRect: true));
+
+    semantics.dispose();
+  });
+
 }
diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart
index 58791af..d99bbfd 100644
--- a/packages/flutter/test/widgets/semantics_tester.dart
+++ b/packages/flutter/test/widgets/semantics_tester.dart
@@ -44,6 +44,7 @@
     this.textDirection,
     this.rect,
     this.transform,
+    this.textSelection,
     this.children: const <TestSemantics>[],
     Iterable<SemanticsTag> tags,
   }) : assert(flags is int || flags is List<SemanticsFlag>),
@@ -68,6 +69,7 @@
     this.hint: '',
     this.textDirection,
     this.transform,
+    this.textSelection,
     this.children: const <TestSemantics>[],
     Iterable<SemanticsTag> tags,
   }) : id = 0,
@@ -103,6 +105,7 @@
     this.textDirection,
     this.rect,
     Matrix4 transform,
+    this.textSelection,
     this.children: const <TestSemantics>[],
     Iterable<SemanticsTag> tags,
   }) : assert(flags is int || flags is List<SemanticsFlag>),
@@ -195,6 +198,8 @@
   /// parent).
   final Matrix4 transform;
 
+  final TextSelection textSelection;
+
   static Matrix4 _applyRootChildScale(Matrix4 transform) {
     final Matrix4 result = new Matrix4.diagonal3Values(3.0, 3.0, 1.0);
     if (transform != null)
@@ -251,6 +256,9 @@
       return fail('expected node id $id to have rect $rect but found rect ${nodeData.rect}.');
     if (!ignoreTransform && transform != nodeData.transform)
       return fail('expected node id $id to have transform $transform but found transform:\n${nodeData.transform}.');
+    if (textSelection?.baseOffset != nodeData.textSelection?.baseOffset || textSelection?.extentOffset != nodeData.textSelection?.extentOffset) {
+      return fail('expected node id $id to have textDirection [${textSelection?.baseOffset}, ${textSelection?.end}] but found: [${nodeData.textSelection?.baseOffset}, ${nodeData.textSelection?.extentOffset}].');
+    }
     final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
     if (children.length != childrenCount)
       return fail('expected node id $id to have ${children.length} child${ children.length == 1 ? "" : "ren" } but found $childrenCount.');
@@ -293,6 +301,8 @@
       buf.writeln('$indent  hint: \'$hint\',');
     if (textDirection != null)
       buf.writeln('$indent  textDirection: $textDirection,');
+    if (textSelection?.isValid == true)
+      buf.writeln('$indent  textSelection:\n[${textSelection.start}, ${textSelection.end}],');
     if (rect != null)
       buf.writeln('$indent  rect: $rect,');
     if (transform != null)