diff --git a/dev/devicelab/bin/tasks/android_semantics_integration_test.dart b/dev/devicelab/bin/tasks/android_semantics_integration_test.dart
new file mode 100644
index 00000000000..fa9b10e3366
--- /dev/null
+++ b/dev/devicelab/bin/tasks/android_semantics_integration_test.dart
@@ -0,0 +1,12 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_devicelab/framework/adb.dart';
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/integration_tests.dart';
+
+void main() async {
+ deviceOperatingSystem = DeviceOperatingSystem.android;
+ await task(createAndroidSemanticsIntegrationTest());
+}
diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart
index bb5a41c6458..ea991bde540 100644
--- a/dev/devicelab/lib/tasks/integration_tests.dart
+++ b/dev/devicelab/lib/tasks/integration_tests.dart
@@ -52,6 +52,13 @@ TaskFunction createEmbeddedAndroidViewsIntegrationTest() {
);
}
+TaskFunction createAndroidSemanticsIntegrationTest() {
+ return new DriverTest(
+ '${flutterDirectory.path}/dev/integration_tests/android_semantics_testing',
+ 'lib/main.dart',
+ );
+}
+
class DriverTest {
DriverTest(
diff --git a/dev/devicelab/manifest.yaml b/dev/devicelab/manifest.yaml
index 2bae9d7d7ca..8ffc13f06f7 100644
--- a/dev/devicelab/manifest.yaml
+++ b/dev/devicelab/manifest.yaml
@@ -136,6 +136,13 @@ tasks:
stage: devicelab
required_agent_capabilities: ["mac/android"]
+ android_semantics_integration_test:
+ description: >
+ Tests that the Android accessibility bridge produces correct semantics.
+ stage: devicelab
+ required_agent_capabilities: ["mac/android"]
+ flaky: true
+
run_release_test:
description: >
Checks that `flutter run --release` does not crash.
diff --git a/dev/integration_tests/android_semantics_testing/android/app/build.gradle b/dev/integration_tests/android_semantics_testing/android/app/build.gradle
new file mode 100644
index 00000000000..3228d776220
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/android/app/build.gradle
@@ -0,0 +1,57 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withInputStream { stream ->
+ localProperties.load(stream)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+apply plugin: 'com.android.application'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion 27
+
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+
+ defaultConfig {
+ minSdkVersion 16
+ targetSdkVersion 27
+ versionCode 1
+ versionName "0.0.1"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ }
+ }
+
+ aaptOptions {
+ // TODO(goderbauer): remove when https://github.com/flutter/flutter/issues/8986 is resolved.
+ if(System.getenv("FLUTTER_CI_WIN")) {
+ println "AAPT cruncher disabled when running on Win CI."
+ cruncherEnabled false
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'com.android.support.test:runner:1.0.2'
+ androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+}
diff --git a/dev/integration_tests/android_semantics_testing/android/app/src/main/AndroidManifest.xml b/dev/integration_tests/android_semantics_testing/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..67036414330
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dev/integration_tests/android_semantics_testing/android/app/src/main/java/com/yourcompany/platforminteraction/MainActivity.java b/dev/integration_tests/android_semantics_testing/android/app/src/main/java/com/yourcompany/platforminteraction/MainActivity.java
new file mode 100644
index 00000000000..22733fe5288
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/android/app/src/main/java/com/yourcompany/platforminteraction/MainActivity.java
@@ -0,0 +1,111 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.yourcompany.platforminteraction;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+import java.lang.StringBuilder;
+
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+import android.content.Context;
+
+import io.flutter.app.FlutterActivity;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
+import io.flutter.plugins.GeneratedPluginRegistrant;
+import io.flutter.view.FlutterView;
+
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+public class MainActivity extends FlutterActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ GeneratedPluginRegistrant.registerWith(this);
+ new MethodChannel(getFlutterView(), "semantics").setMethodCallHandler(new SemanticsTesterMethodHandler());
+ }
+
+ class SemanticsTesterMethodHandler implements MethodCallHandler {
+ Float mScreenDensity = 1.0f;
+
+ @Override
+ public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
+ FlutterView flutterView = getFlutterView();
+ AccessibilityNodeProvider provider = flutterView.getAccessibilityNodeProvider();
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(displayMetrics);
+ mScreenDensity = displayMetrics.density;
+ if (methodCall.method.equals("getSemanticsNode")) {
+ Map data = methodCall.arguments();
+ @SuppressWarnings("unchecked")
+ Integer id = (Integer) data.get("id");
+ if (id == null) {
+ result.error("No ID provided", "", null);
+ return;
+ }
+ if (provider == null) {
+ result.error("Semantics not enabled", "", null);
+ return;
+ }
+ AccessibilityNodeInfo node = provider.createAccessibilityNodeInfo(id);
+ result.success(convertSemantics(node, id));
+ return;
+ }
+ result.notImplemented();
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map convertSemantics(AccessibilityNodeInfo node, int id) {
+ if (node == null)
+ return null;
+ Map result = new HashMap<>();
+ Map flags = new HashMap<>();
+ Map rect = new HashMap<>();
+ result.put("id", id);
+ result.put("text", node.getText());
+ result.put("contentDescription", node.getContentDescription());
+ flags.put("isChecked", node.isChecked());
+ flags.put("isCheckable", node.isCheckable());
+ flags.put("isDismissable", node.isDismissable());
+ flags.put("isEditable", node.isEditable());
+ flags.put("isEnabled", node.isEnabled());
+ flags.put("isFocusable", node.isFocusable());
+ flags.put("isFocused", node.isFocused());
+ flags.put("isPassword", node.isPassword());
+ flags.put("isLongClickable", node.isLongClickable());
+ result.put("flags", flags);
+ Rect nodeRect = new Rect();
+ node.getBoundsInScreen(nodeRect);
+ rect.put("left", nodeRect.left / mScreenDensity);
+ rect.put("top", nodeRect.top/ mScreenDensity);
+ rect.put("right", nodeRect.right / mScreenDensity);
+ rect.put("bottom", nodeRect.bottom/ mScreenDensity);
+ rect.put("width", nodeRect.width());
+ rect.put("height", nodeRect.height());
+ result.put("rect", rect);
+ result.put("className", node.getClassName());
+ result.put("contentDescription", node.getContentDescription());
+ result.put("liveRegion", node.getLiveRegion());
+ List actionList = node.getActionList();
+ if (actionList.size() > 0) {
+ ArrayList actions = new ArrayList<>();
+ for (AccessibilityNodeInfo.AccessibilityAction action : actionList) {
+ actions.add(action.getId());
+ }
+ result.put("actions", actions);
+ }
+ return result;
+ }
+ }
+}
diff --git a/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000000..db77bb4b7b0
Binary files /dev/null and b/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000000..17987b79bb8
Binary files /dev/null and b/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000000..09d4391482b
Binary files /dev/null and b/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000000..d5f1c8d34e7
Binary files /dev/null and b/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000000..4d6372eebdb
Binary files /dev/null and b/dev/integration_tests/android_semantics_testing/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/dev/integration_tests/android_semantics_testing/android/build.gradle b/dev/integration_tests/android_semantics_testing/android/build.gradle
new file mode 100644
index 00000000000..d4225c7905b
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/android/build.gradle
@@ -0,0 +1,29 @@
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.1.2'
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/dev/integration_tests/android_semantics_testing/android/gradle.properties b/dev/integration_tests/android_semantics_testing/android/gradle.properties
new file mode 100644
index 00000000000..8bd86f68051
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/android/gradle.properties
@@ -0,0 +1 @@
+org.gradle.jvmargs=-Xmx1536M
diff --git a/dev/integration_tests/android_semantics_testing/android/gradle/wrapper/gradle-wrapper.properties b/dev/integration_tests/android_semantics_testing/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100755
index 00000000000..9372d0f3f41
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/dev/integration_tests/android_semantics_testing/android/settings.gradle b/dev/integration_tests/android_semantics_testing/android/settings.gradle
new file mode 100644
index 00000000000..115da6cb4f4
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+ pluginsFile.withInputStream { stream -> plugins.load(stream) }
+}
+
+plugins.each { name, path ->
+ def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+ include ":$name"
+ project(":$name").projectDir = pluginDirectory
+}
diff --git a/dev/integration_tests/android_semantics_testing/lib/main.dart b/dev/integration_tests/android_semantics_testing/lib/main.dart
new file mode 100644
index 00000000000..1a08aa46141
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/lib/main.dart
@@ -0,0 +1,63 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_driver/driver_extension.dart';
+
+import 'src/tests/controls_page.dart';
+
+void main() {
+ enableFlutterDriverExtension(handler: dataHandler);
+ runApp(const TestApp());
+}
+
+const MethodChannel kSemanticsChannel = MethodChannel('semantics');
+
+Future dataHandler(String message) async {
+ if (message.contains('getSemanticsNode')) {
+ final int id = int.tryParse(message.split('#')[1]) ?? 0;
+ final dynamic result = await kSemanticsChannel.invokeMethod('getSemanticsNode', {
+ 'id': id,
+ });
+ return json.encode(result);
+ }
+ throw new UnimplementedError();
+}
+
+const List routes = [
+ selectionControlsRoute,
+];
+
+class TestApp extends StatelessWidget {
+ const TestApp();
+
+ @override
+ Widget build(BuildContext context) {
+ return new MaterialApp(
+ routes: {
+ selectionControlsRoute: (BuildContext context) => new SelectionControlsPage(),
+ },
+ home: new Builder(
+ builder: (BuildContext context) {
+ return new Scaffold(
+ body: new Column(
+ children: routes.map((String value) {
+ return new MaterialButton(
+ child: new Text(value),
+ onPressed: () {
+ Navigator.of(context).pushNamed(value);
+ },
+ );
+ }).toList(),
+ ),
+ );
+ }
+ ),
+ );
+ }
+}
diff --git a/dev/integration_tests/android_semantics_testing/lib/src/common.dart b/dev/integration_tests/android_semantics_testing/lib/src/common.dart
index a088cda0e35..e993f82966c 100644
--- a/dev/integration_tests/android_semantics_testing/lib/src/common.dart
+++ b/dev/integration_tests/android_semantics_testing/lib/src/common.dart
@@ -37,6 +37,7 @@ class AndroidSemanticsNode {
/// "isLongClickable": bool,
/// },
/// "text": String,
+ /// "contentDescription": String,
/// "className": String,
/// "id": int,
/// "rect": {
@@ -64,6 +65,16 @@ class AndroidSemanticsNode {
/// the Flutter [SemanticsNode].
String get text => _values['text'];
+ /// The contentDescription of the semantics node.
+ ///
+ /// This field is used for the Switch, Radio, and Checkbox widgets
+ /// instead of [text]. If the text property is used for these, TalkBack
+ /// will not read out the "checked" or "not checked" label by default.
+ ///
+ /// This is produced by combining the value, label, and hint fields from
+ /// the Flutter [SemanticsNode].
+ String get contentDescription => _values['contentDescription'];
+
/// The className of the semantics node.
///
/// Certain kinds of Flutter semantics are mapped to Android classes to
diff --git a/dev/integration_tests/android_semantics_testing/lib/src/constants.dart b/dev/integration_tests/android_semantics_testing/lib/src/constants.dart
index fb641e5bb7d..b1fe9480580 100644
--- a/dev/integration_tests/android_semantics_testing/lib/src/constants.dart
+++ b/dev/integration_tests/android_semantics_testing/lib/src/constants.dart
@@ -21,6 +21,9 @@ class AndroidClassName {
/// The class name used for read only text fields.
static const String textView = 'android.widget.TextView';
+
+ /// The class name used for toggle switches.
+ static const String toggleSwitch = 'android.widget.Switch';
}
/// Action constants which correspond to `AccessibilityAction` in Android.
diff --git a/dev/integration_tests/android_semantics_testing/lib/src/flutter_test_alternative.dart b/dev/integration_tests/android_semantics_testing/lib/src/flutter_test_alternative.dart
new file mode 100644
index 00000000000..79d7310b120
--- /dev/null
+++ b/dev/integration_tests/android_semantics_testing/lib/src/flutter_test_alternative.dart
@@ -0,0 +1,14 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Defines a 'package:test' shim.
+// TODO(ianh): Remove this file once https://github.com/dart-lang/matcher/issues/98 is fixed
+
+import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
+import 'package:test/test.dart' as test_package show TypeMatcher;
+
+export 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
+
+/// A matcher that compares the type of the actual value to the type argument T.
+Matcher isInstanceOf() => new test_package.TypeMatcher(); // ignore: prefer_const_constructors, https://github.com/dart-lang/sdk/issues/32544
diff --git a/dev/integration_tests/android_semantics_testing/lib/src/matcher.dart b/dev/integration_tests/android_semantics_testing/lib/src/matcher.dart
index 896866c866f..a3ce3be86fc 100644
--- a/dev/integration_tests/android_semantics_testing/lib/src/matcher.dart
+++ b/dev/integration_tests/android_semantics_testing/lib/src/matcher.dart
@@ -2,10 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'package:flutter_test/flutter_test.dart';
-
import 'common.dart';
import 'constants.dart';
+import 'flutter_test_alternative.dart';
/// Matches an [AndroidSemanticsNode].
///
@@ -16,6 +15,7 @@ import 'constants.dart';
/// the Flutter framework.
Matcher hasAndroidSemantics({
String text,
+ String contentDescription,
String className,
int id,
Rect rect,
@@ -33,6 +33,7 @@ Matcher hasAndroidSemantics({
}) {
return new _AndroidSemanticsMatcher(
text: text,
+ contentDescription: contentDescription,
className: className,
rect: rect,
size: size,
@@ -52,6 +53,7 @@ Matcher hasAndroidSemantics({
class _AndroidSemanticsMatcher extends Matcher {
_AndroidSemanticsMatcher({
this.text,
+ this.contentDescription,
this.className,
this.id,
this.actions,
@@ -69,6 +71,7 @@ class _AndroidSemanticsMatcher extends Matcher {
final String text;
final String className;
+ final String contentDescription;
final int id;
final List actions;
final Rect rect;
@@ -87,6 +90,8 @@ class _AndroidSemanticsMatcher extends Matcher {
description.add('AndroidSemanticsNode');
if (text != null)
description.add(' with text: $text');
+ if (contentDescription != null)
+ description.add( 'with contentDescription $contentDescription');
if (className != null)
description.add(' with className: $className');
if (id != null)
@@ -118,6 +123,8 @@ class _AndroidSemanticsMatcher extends Matcher {
bool matches(covariant AndroidSemanticsNode item, Map