Adds urlspan to support link semantics in Android (#162419)
<!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> fixes https://github.com/flutter/flutter/issues/102535 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
950b5ac7b0
commit
0e1df622a1
@ -24,6 +24,7 @@ import android.text.SpannableString;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.LocaleSpan;
|
import android.text.style.LocaleSpan;
|
||||||
import android.text.style.TtsSpan;
|
import android.text.style.TtsSpan;
|
||||||
|
import android.text.style.URLSpan;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
@ -748,7 +749,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
|||||||
result.addAction(AccessibilityNodeInfo.ACTION_SET_TEXT);
|
result.addAction(AccessibilityNodeInfo.ACTION_SET_TEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (semanticsNode.hasFlag(Flag.IS_BUTTON) || semanticsNode.hasFlag(Flag.IS_LINK)) {
|
if (semanticsNode.hasFlag(Flag.IS_BUTTON)) {
|
||||||
result.setClassName("android.widget.Button");
|
result.setClassName("android.widget.Button");
|
||||||
}
|
}
|
||||||
if (semanticsNode.hasFlag(Flag.IS_IMAGE)) {
|
if (semanticsNode.hasFlag(Flag.IS_IMAGE)) {
|
||||||
@ -2262,6 +2263,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
|||||||
private enum StringAttributeType {
|
private enum StringAttributeType {
|
||||||
SPELLOUT,
|
SPELLOUT,
|
||||||
LOCALE,
|
LOCALE,
|
||||||
|
URL
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class StringAttribute {
|
private static class StringAttribute {
|
||||||
@ -2276,6 +2278,10 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
|||||||
String locale;
|
String locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class UrlStringAttribute extends StringAttribute {
|
||||||
|
String url;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flutter {@code SemanticsNode} represented in Java/Android.
|
* Flutter {@code SemanticsNode} represented in Java/Android.
|
||||||
*
|
*
|
||||||
@ -2329,6 +2335,9 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
|||||||
// API level >= 28; otherwise, this is attached to the end of content description.
|
// API level >= 28; otherwise, this is attached to the end of content description.
|
||||||
@Nullable private String tooltip;
|
@Nullable private String tooltip;
|
||||||
|
|
||||||
|
// The Url the widget's points to.
|
||||||
|
@Nullable private String linkUrl;
|
||||||
|
|
||||||
// The id of the sibling node that is before this node in traversal
|
// The id of the sibling node that is before this node in traversal
|
||||||
// order.
|
// order.
|
||||||
//
|
//
|
||||||
@ -2540,6 +2549,9 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
|||||||
stringIndex = buffer.getInt();
|
stringIndex = buffer.getInt();
|
||||||
tooltip = stringIndex == -1 ? null : strings[stringIndex];
|
tooltip = stringIndex == -1 ? null : strings[stringIndex];
|
||||||
|
|
||||||
|
stringIndex = buffer.getInt();
|
||||||
|
linkUrl = stringIndex == -1 ? null : strings[stringIndex];
|
||||||
|
|
||||||
textDirection = TextDirection.fromInt(buffer.getInt());
|
textDirection = TextDirection.fromInt(buffer.getInt());
|
||||||
|
|
||||||
left = buffer.getFloat();
|
left = buffer.getFloat();
|
||||||
@ -2832,7 +2844,21 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private CharSequence getLabel() {
|
private CharSequence getLabel() {
|
||||||
return createSpannableString(label, labelAttributes);
|
List<StringAttribute> attributes = labelAttributes;
|
||||||
|
if (linkUrl != null && linkUrl.length() > 0) {
|
||||||
|
if (attributes == null) {
|
||||||
|
attributes = new ArrayList<StringAttribute>();
|
||||||
|
} else {
|
||||||
|
attributes = new ArrayList<StringAttribute>(attributes);
|
||||||
|
}
|
||||||
|
UrlStringAttribute uriStringAttribute = new UrlStringAttribute();
|
||||||
|
uriStringAttribute.start = 0;
|
||||||
|
uriStringAttribute.end = label.length();
|
||||||
|
uriStringAttribute.url = linkUrl;
|
||||||
|
uriStringAttribute.type = StringAttributeType.URL;
|
||||||
|
attributes.add(uriStringAttribute);
|
||||||
|
}
|
||||||
|
return createSpannableString(label, attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CharSequence getHint() {
|
private CharSequence getHint() {
|
||||||
@ -2891,6 +2917,13 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
|||||||
spannableString.setSpan(localeSpan, attribute.start, attribute.end, 0);
|
spannableString.setSpan(localeSpan, attribute.start, attribute.end, 0);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case URL:
|
||||||
|
{
|
||||||
|
UrlStringAttribute uriAttribute = (UrlStringAttribute) attribute;
|
||||||
|
final URLSpan urlSpan = new URLSpan(uriAttribute.url);
|
||||||
|
spannableString.setSpan(urlSpan, attribute.start, attribute.end, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ PlatformViewAndroidDelegate::PlatformViewAndroidDelegate(
|
|||||||
void PlatformViewAndroidDelegate::UpdateSemantics(
|
void PlatformViewAndroidDelegate::UpdateSemantics(
|
||||||
const flutter::SemanticsNodeUpdates& update,
|
const flutter::SemanticsNodeUpdates& update,
|
||||||
const flutter::CustomAccessibilityActionUpdates& actions) {
|
const flutter::CustomAccessibilityActionUpdates& actions) {
|
||||||
constexpr size_t kBytesPerNode = 48 * sizeof(int32_t);
|
constexpr size_t kBytesPerNode = 49 * sizeof(int32_t);
|
||||||
constexpr size_t kBytesPerChild = sizeof(int32_t);
|
constexpr size_t kBytesPerChild = sizeof(int32_t);
|
||||||
constexpr size_t kBytesPerCustomAction = sizeof(int32_t);
|
constexpr size_t kBytesPerCustomAction = sizeof(int32_t);
|
||||||
constexpr size_t kBytesPerAction = 4 * sizeof(int32_t);
|
constexpr size_t kBytesPerAction = 4 * sizeof(int32_t);
|
||||||
@ -165,6 +165,13 @@ void PlatformViewAndroidDelegate::UpdateSemantics(
|
|||||||
strings.push_back(node.tooltip);
|
strings.push_back(node.tooltip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.linkUrl.empty()) {
|
||||||
|
buffer_int32[position++] = -1;
|
||||||
|
} else {
|
||||||
|
buffer_int32[position++] = strings.size();
|
||||||
|
strings.push_back(node.linkUrl);
|
||||||
|
}
|
||||||
|
|
||||||
buffer_int32[position++] = node.textDirection;
|
buffer_int32[position++] = node.textDirection;
|
||||||
buffer_float32[position++] = node.rect.left();
|
buffer_float32[position++] = node.rect.left();
|
||||||
buffer_float32[position++] = node.rect.top();
|
buffer_float32[position++] = node.rect.top();
|
||||||
|
@ -23,7 +23,7 @@ TEST(PlatformViewShell, UpdateSemanticsDoesFlutterViewUpdateSemantics) {
|
|||||||
node0.tooltip = "tooltip";
|
node0.tooltip = "tooltip";
|
||||||
update.insert(std::make_pair(0, node0));
|
update.insert(std::make_pair(0, node0));
|
||||||
|
|
||||||
std::vector<uint8_t> expected_buffer(192);
|
std::vector<uint8_t> expected_buffer(196);
|
||||||
std::vector<std::vector<uint8_t>> expected_string_attribute_args(0);
|
std::vector<std::vector<uint8_t>> expected_string_attribute_args(0);
|
||||||
size_t position = 0;
|
size_t position = 0;
|
||||||
int32_t* buffer_int32 = reinterpret_cast<int32_t*>(&expected_buffer[0]);
|
int32_t* buffer_int32 = reinterpret_cast<int32_t*>(&expected_buffer[0]);
|
||||||
@ -57,6 +57,71 @@ TEST(PlatformViewShell, UpdateSemanticsDoesFlutterViewUpdateSemantics) {
|
|||||||
buffer_int32[position++] = -1; // node0.hintAttributes
|
buffer_int32[position++] = -1; // node0.hintAttributes
|
||||||
buffer_int32[position++] = expected_strings.size(); // node0.tooltip
|
buffer_int32[position++] = expected_strings.size(); // node0.tooltip
|
||||||
expected_strings.push_back(node0.tooltip);
|
expected_strings.push_back(node0.tooltip);
|
||||||
|
buffer_int32[position++] = -1; // node0.linkUrl
|
||||||
|
buffer_int32[position++] = node0.textDirection;
|
||||||
|
buffer_float32[position++] = node0.rect.left();
|
||||||
|
buffer_float32[position++] = node0.rect.top();
|
||||||
|
buffer_float32[position++] = node0.rect.right();
|
||||||
|
buffer_float32[position++] = node0.rect.bottom();
|
||||||
|
node0.transform.getColMajor(&buffer_float32[position]);
|
||||||
|
position += 16;
|
||||||
|
buffer_int32[position++] = 0; // node0.childrenInTraversalOrder.size();
|
||||||
|
buffer_int32[position++] = 0; // node0.customAccessibilityActions.size();
|
||||||
|
EXPECT_CALL(*jni_mock,
|
||||||
|
FlutterViewUpdateSemantics(expected_buffer, expected_strings,
|
||||||
|
expected_string_attribute_args));
|
||||||
|
// Creates empty custom actions.
|
||||||
|
flutter::CustomAccessibilityActionUpdates actions;
|
||||||
|
delegate->UpdateSemantics(update, actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PlatformViewShell, UpdateSemanticsDoesUpdatelinkUrl) {
|
||||||
|
auto jni_mock = std::make_shared<JNIMock>();
|
||||||
|
auto delegate = std::make_unique<PlatformViewAndroidDelegate>(jni_mock);
|
||||||
|
|
||||||
|
flutter::SemanticsNodeUpdates update;
|
||||||
|
flutter::SemanticsNode node0;
|
||||||
|
node0.id = 0;
|
||||||
|
node0.identifier = "identifier";
|
||||||
|
node0.label = "label";
|
||||||
|
node0.linkUrl = "url";
|
||||||
|
update.insert(std::make_pair(0, node0));
|
||||||
|
|
||||||
|
std::vector<uint8_t> expected_buffer(196);
|
||||||
|
std::vector<std::vector<uint8_t>> expected_string_attribute_args(0);
|
||||||
|
size_t position = 0;
|
||||||
|
int32_t* buffer_int32 = reinterpret_cast<int32_t*>(&expected_buffer[0]);
|
||||||
|
float* buffer_float32 = reinterpret_cast<float*>(&expected_buffer[0]);
|
||||||
|
std::vector<std::string> expected_strings;
|
||||||
|
buffer_int32[position++] = node0.id;
|
||||||
|
buffer_int32[position++] = node0.flags;
|
||||||
|
buffer_int32[position++] = node0.actions;
|
||||||
|
buffer_int32[position++] = node0.maxValueLength;
|
||||||
|
buffer_int32[position++] = node0.currentValueLength;
|
||||||
|
buffer_int32[position++] = node0.textSelectionBase;
|
||||||
|
buffer_int32[position++] = node0.textSelectionExtent;
|
||||||
|
buffer_int32[position++] = node0.platformViewId;
|
||||||
|
buffer_int32[position++] = node0.scrollChildren;
|
||||||
|
buffer_int32[position++] = node0.scrollIndex;
|
||||||
|
buffer_float32[position++] = static_cast<float>(node0.scrollPosition);
|
||||||
|
buffer_float32[position++] = static_cast<float>(node0.scrollExtentMax);
|
||||||
|
buffer_float32[position++] = static_cast<float>(node0.scrollExtentMin);
|
||||||
|
buffer_int32[position++] = expected_strings.size(); // node0.identifier
|
||||||
|
expected_strings.push_back(node0.identifier);
|
||||||
|
buffer_int32[position++] = expected_strings.size(); // node0.label
|
||||||
|
expected_strings.push_back(node0.label);
|
||||||
|
buffer_int32[position++] = -1; // node0.labelAttributes
|
||||||
|
buffer_int32[position++] = -1; // node0.value
|
||||||
|
buffer_int32[position++] = -1; // node0.valueAttributes
|
||||||
|
buffer_int32[position++] = -1; // node0.increasedValue
|
||||||
|
buffer_int32[position++] = -1; // node0.increasedValueAttributes
|
||||||
|
buffer_int32[position++] = -1; // node0.decreasedValue
|
||||||
|
buffer_int32[position++] = -1; // node0.decreasedValueAttributes
|
||||||
|
buffer_int32[position++] = -1; // node0.hint
|
||||||
|
buffer_int32[position++] = -1; // node0.hintAttributes
|
||||||
|
buffer_int32[position++] = -1; // node0.tooltip
|
||||||
|
buffer_int32[position++] = expected_strings.size(); // node0.tooltip
|
||||||
|
expected_strings.push_back(node0.linkUrl);
|
||||||
buffer_int32[position++] = node0.textDirection;
|
buffer_int32[position++] = node0.textDirection;
|
||||||
buffer_float32[position++] = node0.rect.left();
|
buffer_float32[position++] = node0.rect.left();
|
||||||
buffer_float32[position++] = node0.rect.top();
|
buffer_float32[position++] = node0.rect.top();
|
||||||
@ -100,7 +165,7 @@ TEST(PlatformViewShell,
|
|||||||
node0.hintAttributes.push_back(locale_attribute);
|
node0.hintAttributes.push_back(locale_attribute);
|
||||||
update.insert(std::make_pair(0, node0));
|
update.insert(std::make_pair(0, node0));
|
||||||
|
|
||||||
std::vector<uint8_t> expected_buffer(224);
|
std::vector<uint8_t> expected_buffer(228);
|
||||||
std::vector<std::vector<uint8_t>> expected_string_attribute_args;
|
std::vector<std::vector<uint8_t>> expected_string_attribute_args;
|
||||||
size_t position = 0;
|
size_t position = 0;
|
||||||
int32_t* buffer_int32 = reinterpret_cast<int32_t*>(&expected_buffer[0]);
|
int32_t* buffer_int32 = reinterpret_cast<int32_t*>(&expected_buffer[0]);
|
||||||
@ -145,6 +210,7 @@ TEST(PlatformViewShell,
|
|||||||
expected_string_attribute_args.push_back(
|
expected_string_attribute_args.push_back(
|
||||||
{locale_attribute->locale.begin(), locale_attribute->locale.end()});
|
{locale_attribute->locale.begin(), locale_attribute->locale.end()});
|
||||||
buffer_int32[position++] = -1; // node0.tooltip
|
buffer_int32[position++] = -1; // node0.tooltip
|
||||||
|
buffer_int32[position++] = -1; // node0.linkUrl
|
||||||
buffer_int32[position++] = node0.textDirection;
|
buffer_int32[position++] = node0.textDirection;
|
||||||
buffer_float32[position++] = node0.rect.left();
|
buffer_float32[position++] = node0.rect.left();
|
||||||
buffer_float32[position++] = node0.rect.top();
|
buffer_float32[position++] = node0.rect.top();
|
||||||
|
@ -37,6 +37,7 @@ import android.text.SpannableString;
|
|||||||
import android.text.SpannedString;
|
import android.text.SpannedString;
|
||||||
import android.text.style.LocaleSpan;
|
import android.text.style.LocaleSpan;
|
||||||
import android.text.style.TtsSpan;
|
import android.text.style.TtsSpan;
|
||||||
|
import android.text.style.URLSpan;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewParent;
|
import android.view.ViewParent;
|
||||||
@ -193,6 +194,26 @@ public class AccessibilityBridgeTest {
|
|||||||
assertEquals(nodeInfo.getText(), null);
|
assertEquals(nodeInfo.getText(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void itCreatesURLSpanForlinkURL() {
|
||||||
|
AccessibilityBridge accessibilityBridge = setUpBridge();
|
||||||
|
|
||||||
|
TestSemanticsNode testSemanticsNode = new TestSemanticsNode();
|
||||||
|
testSemanticsNode.label = "Hello";
|
||||||
|
testSemanticsNode.linkUrl = "https://flutter.dev";
|
||||||
|
testSemanticsNode.addFlag(AccessibilityBridge.Flag.IS_LINK);
|
||||||
|
TestSemanticsUpdate testSemanticsUpdate = testSemanticsNode.toUpdate();
|
||||||
|
|
||||||
|
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
|
||||||
|
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
|
||||||
|
SpannableString actual = (SpannableString) nodeInfo.getContentDescription();
|
||||||
|
assertEquals(actual.toString(), "Hello");
|
||||||
|
Object[] objectSpans = actual.getSpans(0, actual.length(), Object.class);
|
||||||
|
assertEquals(objectSpans.length, 1);
|
||||||
|
URLSpan span = (URLSpan) objectSpans[0];
|
||||||
|
assertEquals(span.getURL(), "https://flutter.dev");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void itUnfocusesPlatformViewWhenPlatformViewGoesAway() {
|
public void itUnfocusesPlatformViewWhenPlatformViewGoesAway() {
|
||||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||||
@ -2311,6 +2332,7 @@ public class AccessibilityBridgeTest {
|
|||||||
String hint = null;
|
String hint = null;
|
||||||
List<TestStringAttribute> hintAttributes;
|
List<TestStringAttribute> hintAttributes;
|
||||||
String tooltip = null;
|
String tooltip = null;
|
||||||
|
String linkUrl = null;
|
||||||
int textDirection = 0;
|
int textDirection = 0;
|
||||||
float left = 0.0f;
|
float left = 0.0f;
|
||||||
float top = 0.0f;
|
float top = 0.0f;
|
||||||
@ -2374,6 +2396,12 @@ public class AccessibilityBridgeTest {
|
|||||||
strings.add(tooltip);
|
strings.add(tooltip);
|
||||||
bytes.putInt(strings.size() - 1);
|
bytes.putInt(strings.size() - 1);
|
||||||
}
|
}
|
||||||
|
if (linkUrl == null) {
|
||||||
|
bytes.putInt(-1);
|
||||||
|
} else {
|
||||||
|
strings.add(linkUrl);
|
||||||
|
bytes.putInt(strings.size() - 1);
|
||||||
|
}
|
||||||
bytes.putInt(textDirection);
|
bytes.putInt(textDirection);
|
||||||
bytes.putFloat(left);
|
bytes.putFloat(left);
|
||||||
bytes.putFloat(top);
|
bytes.putFloat(top);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user