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.style.LocaleSpan;
|
||||
import android.text.style.TtsSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
@ -748,7 +749,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
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");
|
||||
}
|
||||
if (semanticsNode.hasFlag(Flag.IS_IMAGE)) {
|
||||
@ -2262,6 +2263,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
private enum StringAttributeType {
|
||||
SPELLOUT,
|
||||
LOCALE,
|
||||
URL
|
||||
}
|
||||
|
||||
private static class StringAttribute {
|
||||
@ -2276,6 +2278,10 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
String locale;
|
||||
}
|
||||
|
||||
private static class UrlStringAttribute extends StringAttribute {
|
||||
String url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@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
|
||||
// order.
|
||||
//
|
||||
@ -2540,6 +2549,9 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
stringIndex = buffer.getInt();
|
||||
tooltip = stringIndex == -1 ? null : strings[stringIndex];
|
||||
|
||||
stringIndex = buffer.getInt();
|
||||
linkUrl = stringIndex == -1 ? null : strings[stringIndex];
|
||||
|
||||
textDirection = TextDirection.fromInt(buffer.getInt());
|
||||
|
||||
left = buffer.getFloat();
|
||||
@ -2832,7 +2844,21 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -2891,6 +2917,13 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
spannableString.setSpan(localeSpan, attribute.start, attribute.end, 0);
|
||||
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(
|
||||
const flutter::SemanticsNodeUpdates& update,
|
||||
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 kBytesPerCustomAction = sizeof(int32_t);
|
||||
constexpr size_t kBytesPerAction = 4 * sizeof(int32_t);
|
||||
@ -165,6 +165,13 @@ void PlatformViewAndroidDelegate::UpdateSemantics(
|
||||
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_float32[position++] = node.rect.left();
|
||||
buffer_float32[position++] = node.rect.top();
|
||||
|
@ -23,7 +23,7 @@ TEST(PlatformViewShell, UpdateSemanticsDoesFlutterViewUpdateSemantics) {
|
||||
node0.tooltip = "tooltip";
|
||||
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);
|
||||
size_t position = 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++] = expected_strings.size(); // 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_float32[position++] = node0.rect.left();
|
||||
buffer_float32[position++] = node0.rect.top();
|
||||
@ -100,7 +165,7 @@ TEST(PlatformViewShell,
|
||||
node0.hintAttributes.push_back(locale_attribute);
|
||||
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;
|
||||
size_t position = 0;
|
||||
int32_t* buffer_int32 = reinterpret_cast<int32_t*>(&expected_buffer[0]);
|
||||
@ -145,6 +210,7 @@ TEST(PlatformViewShell,
|
||||
expected_string_attribute_args.push_back(
|
||||
{locale_attribute->locale.begin(), locale_attribute->locale.end()});
|
||||
buffer_int32[position++] = -1; // 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();
|
||||
|
@ -37,6 +37,7 @@ import android.text.SpannableString;
|
||||
import android.text.SpannedString;
|
||||
import android.text.style.LocaleSpan;
|
||||
import android.text.style.TtsSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewParent;
|
||||
@ -193,6 +194,26 @@ public class AccessibilityBridgeTest {
|
||||
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
|
||||
public void itUnfocusesPlatformViewWhenPlatformViewGoesAway() {
|
||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
@ -2311,6 +2332,7 @@ public class AccessibilityBridgeTest {
|
||||
String hint = null;
|
||||
List<TestStringAttribute> hintAttributes;
|
||||
String tooltip = null;
|
||||
String linkUrl = null;
|
||||
int textDirection = 0;
|
||||
float left = 0.0f;
|
||||
float top = 0.0f;
|
||||
@ -2374,6 +2396,12 @@ public class AccessibilityBridgeTest {
|
||||
strings.add(tooltip);
|
||||
bytes.putInt(strings.size() - 1);
|
||||
}
|
||||
if (linkUrl == null) {
|
||||
bytes.putInt(-1);
|
||||
} else {
|
||||
strings.add(linkUrl);
|
||||
bytes.putInt(strings.size() - 1);
|
||||
}
|
||||
bytes.putInt(textDirection);
|
||||
bytes.putFloat(left);
|
||||
bytes.putFloat(top);
|
||||
|
Loading…
x
Reference in New Issue
Block a user