├── .github └── workflows │ └── android-master.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── google │ └── android │ ├── apps │ └── common │ │ └── testing │ │ └── accessibility │ │ └── framework │ │ ├── AccessibilityCheck.java │ │ ├── AccessibilityCheckPreset.java │ │ ├── AccessibilityCheckPresetAndroid.java │ │ ├── AccessibilityCheckResult.java │ │ ├── AccessibilityCheckResultBaseUtils.java │ │ ├── AccessibilityCheckResultDescriptor.java │ │ ├── AccessibilityCheckResultUtils.java │ │ ├── AccessibilityEventCheck.java │ │ ├── AccessibilityEventCheckResult.java │ │ ├── AccessibilityHierarchyCheck.java │ │ ├── AccessibilityHierarchyCheckResult.java │ │ ├── AccessibilityHierarchyCheckResultWithImage.java │ │ ├── AccessibilityViewCheckResult.java │ │ ├── AccessibilityViewHierarchyCheck.java │ │ ├── AnnouncementEventCheck.java │ │ ├── Answer.java │ │ ├── AnswerType.java │ │ ├── AnswerTypes.java │ │ ├── ClusteringUtils.java │ │ ├── HashMapResultMetadata.java │ │ ├── Parameters.java │ │ ├── Question.java │ │ ├── QuestionHandler.java │ │ ├── QuestionType.java │ │ ├── QuestionTypes.java │ │ ├── ResultMetadata.java │ │ ├── ViewAccessibilityUtils.java │ │ ├── ViewChecker.java │ │ ├── ViewHierarchyElementUtils.java │ │ ├── checks │ │ ├── ClassNameCheck.java │ │ ├── ClickableSpanCheck.java │ │ ├── DuplicateClickableBoundsCheck.java │ │ ├── DuplicateSpeakableTextCheck.java │ │ ├── EditableContentDescCheck.java │ │ ├── ImageContrastCheck.java │ │ ├── LinkPurposeUnclearCheck.java │ │ ├── RedundantDescriptionCheck.java │ │ ├── SpeakableTextPresentCheck.java │ │ ├── TextContrastCheck.java │ │ ├── TextSizeCheck.java │ │ ├── TouchTargetSizeCheck.java │ │ ├── TraversalOrderCheck.java │ │ └── UnexposedTextCheck.java │ │ ├── integrations │ │ ├── AccessibilityViewCheckException.java │ │ └── espresso │ │ │ ├── AccessibilityValidator.java │ │ │ ├── AccessibilityViewCheckException.java │ │ │ ├── CheckResultsCallback.java │ │ │ └── Screenshotter.java │ │ ├── matcher │ │ └── ElementMatchers.java │ │ ├── ocr │ │ ├── OcrEngine.java │ │ ├── OcrResult.java │ │ └── TextComponent.java │ │ ├── replacements │ │ ├── LayoutParams.java │ │ ├── Point.java │ │ ├── Rect.java │ │ ├── Span.java │ │ ├── SpannableString.java │ │ ├── SpannableStringAndroid.java │ │ ├── SpannableStringBuilder.java │ │ ├── Spans.java │ │ ├── TextUtils.java │ │ └── Uri.java │ │ ├── strings │ │ ├── AndroidXMLResourceBundle.java │ │ └── StringManager.java │ │ ├── suggestions │ │ ├── BaseFixSuggestionsProvider.java │ │ ├── BaseTextContrastFixSuggestionProducer.java │ │ ├── CompoundFixSuggestions.java │ │ ├── EditableContentDescFixSuggestionsProvider.java │ │ ├── EditableContentDescProducer.java │ │ ├── ExpandViewSizeFixSuggestionProducer.java │ │ ├── FixSuggestion.java │ │ ├── FixSuggestionPreset.java │ │ ├── FixSuggestionProducer.java │ │ ├── FixSuggestionsProvider.java │ │ ├── MaterialDesignColor.java │ │ ├── RedundantDescriptionFixSuggestionProducer.java │ │ ├── RedundantDescriptionFixSuggestionsProvider.java │ │ ├── RemoveViewAttributeFixSuggestion.java │ │ ├── SetViewAttributeFixSuggestion.java │ │ ├── SpeakableTextPresentFixSuggestionProducer.java │ │ ├── SpeakableTextPresentFixSuggestionsProvider.java │ │ ├── TextBackgroundColorFixSuggestionProducer.java │ │ ├── TextColorFixSuggestionProducer.java │ │ ├── TextContrastFixSuggestionsProvider.java │ │ ├── TouchTargetSizeFixSuggestionsProvider.java │ │ └── ViewAttribute.java │ │ ├── uielement │ │ ├── AccessibilityHierarchy.java │ │ ├── AccessibilityHierarchyAndroid.java │ │ ├── AccessibilityHierarchyOrigin.java │ │ ├── AccessibilityNodeInfoExtraDataExtractor.java │ │ ├── CustomViewBuilderAndroid.java │ │ ├── DefaultCustomViewBuilderAndroid.java │ │ ├── DeviceState.java │ │ ├── DeviceStateAndroid.java │ │ ├── DisplayInfo.java │ │ ├── DisplayInfoAndroid.java │ │ ├── ViewHierarchyAction.java │ │ ├── ViewHierarchyActionAndroid.java │ │ ├── ViewHierarchyElement.java │ │ ├── ViewHierarchyElementAndroid.java │ │ ├── ViewHierarchyElementDescriptor.java │ │ ├── ViewHierarchyElementOrigin.java │ │ ├── WindowHierarchyElement.java │ │ └── WindowHierarchyElementAndroid.java │ │ └── utils │ │ └── contrast │ │ ├── BitmapImage.java │ │ ├── Color.java │ │ ├── ContrastSwatch.java │ │ ├── ContrastUtils.java │ │ └── Image.java │ └── libraries │ └── accessibility │ └── utils │ └── log │ └── LogUtils.java ├── proto └── com │ └── google │ └── android │ └── apps │ └── common │ └── testing │ └── accessibility │ └── framework │ ├── AccessibilityEvaluation.proto │ └── uielement │ ├── AccessibilityHierarchy.proto │ └── AndroidFramework.proto └── resources ├── resources-ar └── strings.xml ├── resources-b+es+419 └── strings.xml ├── resources-bg └── strings.xml ├── resources-ca └── strings.xml ├── resources-cs └── strings.xml ├── resources-da └── strings.xml ├── resources-de └── strings.xml ├── resources-el └── strings.xml ├── resources-en-rGB └── strings.xml ├── resources-es └── strings.xml ├── resources-fi └── strings.xml ├── resources-fil └── strings.xml ├── resources-fr └── strings.xml ├── resources-hi └── strings.xml ├── resources-hr └── strings.xml ├── resources-hu └── strings.xml ├── resources-id └── strings.xml ├── resources-it └── strings.xml ├── resources-iw └── strings.xml ├── resources-ja └── strings.xml ├── resources-ko └── strings.xml ├── resources-lt └── strings.xml ├── resources-lv └── strings.xml ├── resources-nl └── strings.xml ├── resources-no └── strings.xml ├── resources-pl └── strings.xml ├── resources-pt-rBR └── strings.xml ├── resources-pt-rPT └── strings.xml ├── resources-ro └── strings.xml ├── resources-ru └── strings.xml ├── resources-sk └── strings.xml ├── resources-sl └── strings.xml ├── resources-sr └── strings.xml ├── resources-sv └── strings.xml ├── resources-th └── strings.xml ├── resources-tr └── strings.xml ├── resources-uk └── strings.xml ├── resources-vi └── strings.xml ├── resources-zh-rCN └── strings.xml ├── resources-zh-rTW └── strings.xml └── resources └── strings.xml /.github/workflows/android-master.yml: -------------------------------------------------------------------------------- 1 | name: Android CI - Master 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'master' 7 | push: 8 | branches: 9 | - 'master' 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: set up JDK 1.8 19 | uses: actions/setup-java@v1 20 | with: 21 | java-version: 1.8 22 | - name: Build with Gradle 23 | run: sudo ./gradlew build 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle 3 | 4 | # Built application files 5 | *.apk 6 | *.aar 7 | *.ap_ 8 | *.aab 9 | 10 | # Files for the ART/Dalvik VM 11 | *.dex 12 | 13 | # Java class files 14 | *.class 15 | 16 | # Generated files 17 | bin/ 18 | gen/ 19 | out/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # IntelliJ 37 | *.iml 38 | .idea 39 | build 40 | 41 | # External native build folder generated in Android Studio 2.2 and later 42 | .externalNativeBuild 43 | .cxx/ 44 | 45 | # Freeline 46 | freeline.py 47 | freeline/ 48 | freeline_project_description.json 49 | 50 | # fastlane 51 | fastlane/report.xml 52 | fastlane/Preview.html 53 | fastlane/screenshots 54 | fastlane/test_output 55 | fastlane/readme.md 56 | 57 | # Version control 58 | vcs.xml 59 | 60 | # lint 61 | lint/intermediates/ 62 | lint/generated/ 63 | lint/outputs/ 64 | lint/tmp/ 65 | 66 | # Android Profiling 67 | *.hprof 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Accessibility Test Framework for Android 2 | 3 | To help people with disabilities access Android apps, developers of those apps 4 | need to consider how their apps will be presented to accessibility services. 5 | Some good practices can be checked by automated tools, such as if a View has a 6 | contentDescription. Other rules require human judgment, such as whether or not a 7 | contentDescription makes sense to all users. 8 | 9 | For more information about Mobile Accessibility, see 10 | http://www.w3.org/WAI/mobile/. 11 | 12 | This library collects various accessibility-related checks on View objects as 13 | well as AccessibilityNodeInfo objects (which the Android framework derives from 14 | Views and sends to AccessibilityServices). 15 | 16 | ## Building the Library 17 | 18 | The supplied gradle wrapper and build.gradle file can be used to build the 19 | Accessibility Test Framework or import the project into Android Studio. 20 | 21 | ```shell 22 | $ ./gradlew build 23 | ``` 24 | 25 | ## Sample Usage 26 | 27 | Given a view, the following code runs all accessibility checks on all views in 28 | the hierarchy rooted at that view and throws an exception if any errors are 29 | found: 30 | 31 | ```java 32 | ImmutableSet checks = 33 | AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset( 34 | AccessibilityCheckPreset.LATEST); 35 | AccessibilityHierarchyAndroid hierarchy = AccessibilityHierarchyAndroid.newBuilder(view).build(); 36 | List results = new ArrayList<>(); 37 | for (AccessibilityHierarchyCheck check : checks) { 38 | results.addAll(check.runCheckOnHierarchy(hierarchy)); 39 | } 40 | List errors = 41 | AccessibilityCheckResultUtils.getResultsForType( 42 | results, AccessibilityCheckResultType.ERROR); 43 | if (!errors.isEmpty()) { 44 | throw new RuntimeException(errors.get(0).getMessage().toString()); 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | google() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:3.5.4' 8 | classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.14' 9 | classpath 'digital.wup:android-maven-publish:3.6.3' 10 | } 11 | } 12 | 13 | 14 | allprojects { 15 | repositories { 16 | mavenCentral() 17 | google() 18 | } 19 | } 20 | 21 | apply plugin: 'com.android.library' 22 | 23 | android { 24 | compileSdkVersion 34 25 | buildToolsVersion '29.0.3' 26 | defaultConfig { 27 | minSdkVersion 19 28 | targetSdkVersion 34 29 | } 30 | lintOptions { 31 | abortOnError false 32 | } 33 | compileOptions { 34 | targetCompatibility 1.8 35 | sourceCompatibility 1.8 36 | } 37 | } 38 | 39 | apply plugin: 'com.google.protobuf' 40 | 41 | protobuf { 42 | protoc { 43 | artifact = 'com.google.protobuf:protoc:3.19.1' 44 | } 45 | generateProtoTasks { 46 | all().each { task -> 47 | task.builtins { 48 | java { 49 | option "lite" 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | // Creates the source jar for release to maven central. 57 | task sourceJar(type: Jar) { 58 | classifier "sources" 59 | from android.sourceSets.main.java.srcDirs 60 | } 61 | 62 | // Creates javadoc for the project. 63 | task javadoc(type: Javadoc) { 64 | source = android.sourceSets.main.java.srcDirs 65 | failOnError false // Currently cannot import android sdk javadoc references so we ignore errors. 66 | } 67 | 68 | // Creates the source javadoc jar for release to maven central. 69 | task javadocJar(type: Jar) { 70 | classifier "javadoc" 71 | from javadoc 72 | } 73 | apply plugin: 'digital.wup.android-maven-publish' 74 | apply plugin: 'maven-publish' 75 | 76 | // Creates the artifacts for release to maven central. 77 | publishing { 78 | publications { 79 | mavenAar(MavenPublication) { 80 | groupId 'com.google.android.apps.common.testing.accessibility.framework' 81 | artifactId 'accessibility-test-framework' 82 | version '4.1.1' 83 | from components.android 84 | artifact sourceJar 85 | artifact javadocJar 86 | pom { 87 | name = 'Accessibility Test Framework' 88 | description = 'Library used to test for common accessibility issues.' 89 | url = 'https://github.com/google/Accessibility-Test-Framework-for-Android' 90 | licenses { 91 | license { 92 | name = 'The Apache License, Version 2.0' 93 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 94 | distribution = 'repo' 95 | } 96 | } 97 | developers { 98 | developer { 99 | name = 'Casey Burkhardt' 100 | email = 'caseyburkhardt@google.com' 101 | organization = 'Google LLC' 102 | organizationUrl = 'https://www.google.com' 103 | } 104 | } 105 | scm { 106 | connection = 'scm:git:git@github.com:google/Accessibility-Test-Framework-for-Android.git' 107 | developerConnection = 'scm:git:git@github.com:google/Accessibility-Test-Framework-for-Android.git' 108 | url = 'https://github.com/google/Accessibility-Test-Framework-for-Android' 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | dependencies { 116 | implementation 'androidx.core:core:1.8.0' 117 | implementation 'androidx.test.services:storage:1.4.1' 118 | implementation 'androidx.test.espresso:espresso-core:3.4.0' 119 | implementation 'androidx.test:runner:1.4.0' 120 | implementation 'androidx.test:rules:1.4.0' 121 | implementation 'com.google.android.material:material:1.2.0-rc01' 122 | implementation 'com.google.errorprone:error_prone_annotations:2.14.0' 123 | implementation 'com.google.guava:guava:31.0.1-android' 124 | implementation 'com.google.protobuf:protobuf-javalite:3.19.1' 125 | // use same version of checker framework used in guava android, 126 | // to avoid duplicate class and dexing errors 127 | // see https://github.com/android/android-test/issues/861 128 | implementation 'org.checkerframework:checker-qual:3.22.1' 129 | implementation 'org.hamcrest:hamcrest-core:1.3' 130 | implementation 'org.hamcrest:hamcrest-library:1.3' 131 | implementation 'org.jsoup:jsoup:1.15.1' 132 | compileOnly 'com.google.auto.value:auto-value-annotations:1.6.2' 133 | annotationProcessor 'com.google.auto.value:auto-value:1.6.2' 134 | } 135 | 136 | clean { 137 | delete 'src/main/generated' 138 | } 139 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/Accessibility-Test-Framework-for-Android/c65cab02b2a845c29c3da100d6adefd345a144e3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheck.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.apps.common.testing.accessibility.framework; 18 | 19 | /** 20 | * Abstract base class for all accessibility checks. Abstract subclasses include: 21 | *
    22 | *
  • {@link AccessibilityHierarchyCheck} - the base class for all checks that run against 23 | * {@code AccessibilityHierarchy}
  • 24 | *
  • {@link AccessibilityEventCheck} - the base class for all checks that that run against 25 | * {@code AccessibilityEvent}
  • 26 | *
  • Deprecated {@link AccessibilityViewHierarchyCheck} - the base class for all checks that run 27 | * against {@code View}s
  • 28 | *
29 | * 30 | *

Classes extending this one must implement {@code runCheck...} that return {@code List}s of 31 | * a subclass of {@code AccessibilityCheckResult}. 32 | */ 33 | public abstract class AccessibilityCheck { 34 | 35 | /** Categories of accessibility checks. */ 36 | public enum Category { 37 | 38 | /** Checks for controls whose content labels are missing or confusing. */ 39 | CONTENT_LABELING, 40 | 41 | /** Checks for touch targets that could cause difficulty for users with motor impairments. */ 42 | TOUCH_TARGET_SIZE, 43 | 44 | /** Checks for elements that may be difficult to see due to low contrast. */ 45 | LOW_CONTRAST, 46 | 47 | /** 48 | * Checks for conditions that impact accessibility due to the way a UI presents itself to 49 | * accessibility services. 50 | */ 51 | IMPLEMENTATION; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework; 16 | 17 | import static com.google.common.base.Preconditions.checkArgument; 18 | import static com.google.common.base.Preconditions.checkNotNull; 19 | 20 | import com.google.android.apps.common.testing.accessibility.framework.proto.AccessibilityEvaluationProtos.ResultTypeProto; 21 | import java.util.HashMap; 22 | import java.util.Locale; 23 | import java.util.Map; 24 | import org.checkerframework.checker.nullness.qual.Nullable; 25 | 26 | /** 27 | * The result of an accessibility check. The results are "interesting" in the sense that they 28 | * indicate some sort of accessibility issue. {@code AccessibilityCheck}s return lists of classes 29 | * that extend this one. There is no "passing" result; checks that return lists that contain no 30 | * {@code AccessibilityCheckResult}s have passed. 31 | * 32 | *

NOTE: Some subtypes of this class retain copies of resources that should be explicitly 33 | * recycled. Callers should use {@link #recycle()} to dispose of data in this object and release 34 | * these resources. 35 | */ 36 | public abstract class AccessibilityCheckResult { 37 | /** 38 | * Types of results. This must be kept consistent (other than UNKNOWN) with the ResultTypeProto 39 | * enum in {@code AccessibilityEvaluation.proto} 40 | * 41 | *

CONTRACT: These values must be defined in order of decreasing severity, such that any Type 42 | * more severe than another has a lower ordinal value. 43 | * 44 | *

CONTRACT: Once a value is defined here, it must not be removed and its tag number as defined 45 | * in the protocol buffer representation cannot change. Data may be persisted using these values, 46 | * so incompatible changes may result in corruption during deserialization. 47 | */ 48 | public enum AccessibilityCheckResultType { 49 | /** Clearly an accessibility bug, for example no speakable text on a clicked button */ 50 | ERROR(ResultTypeProto.ERROR), 51 | /** 52 | * Potentially an accessibility bug, for example finding another view with the same speakable 53 | * text as a clicked view 54 | */ 55 | WARNING(ResultTypeProto.WARNING), 56 | /** 57 | * Information that may be helpful when evaluating accessibility, for example a listing of all 58 | * speakable text in a view hierarchy in the traversal order used by an accessibility service. 59 | */ 60 | INFO(ResultTypeProto.INFO), 61 | /** 62 | * Indication that a potential issue was identified, but it was resolved as not an accessibility 63 | * problem. 64 | */ 65 | RESOLVED(ResultTypeProto.RESOLVED), 66 | /** A signal that the check was not run at all (ex. because the API level was too low) */ 67 | NOT_RUN(ResultTypeProto.NOT_RUN), 68 | /** 69 | * A result that has been explicitly suppressed from throwing any Exceptions, used to allow for 70 | * known issues. 71 | */ 72 | SUPPRESSED(ResultTypeProto.SUPPRESSED); 73 | 74 | private static final Map PROTO_NUMBER_MAP = 75 | new HashMap<>(); 76 | 77 | static { 78 | for (AccessibilityCheckResultType type : values()) { 79 | PROTO_NUMBER_MAP.put(type.protoNumber, type); 80 | } 81 | } 82 | 83 | final int protoNumber; 84 | 85 | private AccessibilityCheckResultType(ResultTypeProto proto) { 86 | this.protoNumber = proto.getNumber(); 87 | } 88 | 89 | public static AccessibilityCheckResultType fromProto(ResultTypeProto proto) { 90 | AccessibilityCheckResultType type = PROTO_NUMBER_MAP.get(proto.getNumber()); 91 | checkArgument( 92 | (type != null), 93 | "Failed to create AccessibilityCheckResultType from proto with unknown value: %s", 94 | proto.getNumber()); 95 | return checkNotNull(type); 96 | } 97 | 98 | // incompatible types in return. 99 | @SuppressWarnings("nullness:return.type.incompatible") 100 | public ResultTypeProto toProto() { 101 | return ResultTypeProto.forNumber(protoNumber); 102 | } 103 | } 104 | 105 | private final Class checkClass; 106 | private final AccessibilityCheckResultType type; 107 | private final @Nullable CharSequence message; 108 | 109 | /** 110 | * @param checkClass The class of the check that generated the error 111 | * @param type The type of the result 112 | * @param message A human-readable message explaining the error. This may be {@code null} when 113 | * a subclass overrides {@link #getMessage}. 114 | */ 115 | public AccessibilityCheckResult( 116 | Class checkClass, 117 | AccessibilityCheckResultType type, 118 | @Nullable CharSequence message) { 119 | this.checkClass = checkClass; 120 | this.type = type; 121 | this.message = message; 122 | } 123 | 124 | /** 125 | * @return The check that generated the result. 126 | */ 127 | public Class getSourceCheckClass() { 128 | return checkClass; 129 | } 130 | 131 | /** 132 | * @return The type of the result. 133 | */ 134 | public AccessibilityCheckResultType getType() { 135 | return type; 136 | } 137 | 138 | /** 139 | * Returns a human-readable message in English explaining the result. 140 | * 141 | * @deprecated Use {@link #getMessage(Locale)} 142 | */ 143 | @Deprecated 144 | public CharSequence getMessage() { 145 | return getMessage(Locale.ENGLISH); 146 | } 147 | 148 | /** 149 | * Returns a human-readable message explaining the result. 150 | * 151 | * @param locale desired locale for the message 152 | */ 153 | @SuppressWarnings("unused") // locale may be used in some subclasses 154 | public CharSequence getMessage(Locale locale) { 155 | return checkNotNull(message, "No message was provided"); 156 | } 157 | 158 | // For debugging 159 | @Override 160 | public String toString() { 161 | return String.format("AccessibilityCheckResult %s %s \"%s\"", type, checkClass, message); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckResultDescriptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework; 16 | 17 | import android.view.View; 18 | import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElement; 19 | import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElementDescriptor; 20 | import java.util.Locale; 21 | import org.checkerframework.checker.nullness.qual.Nullable; 22 | 23 | /** 24 | * An object that describes an {@link AccessibilityCheckResult}. This can be extended to provide 25 | * descriptions of the result and their contents in a form that is localized to the environment in 26 | * which checks are being run. 27 | */ 28 | 29 | public class AccessibilityCheckResultDescriptor { 30 | 31 | /** 32 | * Returns a String description of the given {@link AccessibilityCheckResult}. 33 | * 34 | * @param result the {@link AccessibilityCheckResult} to describe 35 | * @return a String description of the result 36 | */ 37 | public String describeResult(AccessibilityCheckResult result) { 38 | StringBuilder message = new StringBuilder(); 39 | if (result instanceof AccessibilityViewCheckResult) { 40 | View view = ((AccessibilityViewCheckResult) result).getView(); 41 | message.append( 42 | (view != null) 43 | ? describeView(view) 44 | : describeElement( 45 | ((AccessibilityViewCheckResult) result) 46 | .getAccessibilityHierarchyCheckResult() 47 | .getElement())); 48 | message.append(": "); 49 | } else if (result instanceof AccessibilityHierarchyCheckResult) { 50 | message.append(describeElement(((AccessibilityHierarchyCheckResult) result).getElement())); 51 | message.append(": "); 52 | } 53 | message.append(result.getMessage(Locale.ENGLISH)); 54 | Class checkClass = result.getSourceCheckClass(); 55 | if (checkClass != null) { 56 | message.append(" Reported by "); 57 | message.append(result.getSourceCheckClass().getName()); 58 | if (result instanceof AccessibilityHierarchyCheckResult) { 59 | String helpUrl = ((AccessibilityHierarchyCheckResult) result).getHelpUrl(); 60 | if (helpUrl != null) { 61 | message.append(". Learn more at ").append(helpUrl); 62 | } 63 | } 64 | } 65 | return message.toString(); 66 | } 67 | 68 | /** 69 | * Returns a String description of the given {@link View}. The default is to return the view's 70 | * resource entry name. 71 | * 72 | * @param view the {@link View} to describe 73 | * @return a String description of the given {@link View} 74 | */ 75 | public String describeView(@Nullable View view) { 76 | StringBuilder message = new StringBuilder(); 77 | if ((view != null 78 | && view.getId() != View.NO_ID 79 | && view.getId() != 0 80 | && view.getResources() != null 81 | && !ViewAccessibilityUtils.isViewIdGenerated(view.getId()))) { 82 | message.append("View "); 83 | try { 84 | message.append(view.getResources().getResourceEntryName(view.getId())); 85 | } catch (Exception e) { 86 | /* In some integrations (seen in Robolectric), the resources may behave inconsistently */ 87 | message.append("with no valid resource name"); 88 | } 89 | } else { 90 | message.append("View with no valid resource name"); 91 | } 92 | return message.toString(); 93 | } 94 | 95 | /** 96 | * Returns a String description of the given {@link ViewHierarchyElement}. The default is to 97 | * return the view's resource entry name. If the view has no valid resource entry name, then 98 | * returns the view's bounds. 99 | * 100 | * @param element the {@link ViewHierarchyElement} to describe 101 | * @return a String description of the given {@link ViewHierarchyElement} 102 | */ 103 | public String describeElement(@Nullable ViewHierarchyElement element) { 104 | if (element == null) { 105 | return ""; 106 | } 107 | return new ViewHierarchyElementDescriptor().describe(element); 108 | } 109 | } 110 | 111 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityEventCheck.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework; 16 | 17 | import android.view.accessibility.AccessibilityEvent; 18 | import com.google.errorprone.annotations.CanIgnoreReturnValue; 19 | import java.util.Collections; 20 | import java.util.List; 21 | 22 | /** 23 | * An {@code AccessibilityEventCheck} is used as a mechanism for detecting accessibility issues 24 | * based on the {@link AccessibilityEvent}s dispatched by an application or UI component. 25 | *

26 | * Extending classes should use {@link #shouldHandleEvent(AccessibilityEvent)} as a convenience 27 | * mechanism for filtering events which they are interested in evaluating. If {@code true} is 28 | * returned, the event will be passed to {@link #runCheckOnEvent(AccessibilityEvent)} for 29 | * evaluation. Much like other {@link AccessibilityCheck}s, results are returned through a single 30 | * object, in this case {@link AccessibilityEventCheckResult}, which may include the culprit 31 | * {@link AccessibilityEvent}. 32 | *

33 | * {@code AccessibilityEventCheck}s are different from other {@link AccessibilityCheck}s in that 34 | * they may maintain state internal to the class. This is because {@code AccessibilityEventCheck}s 35 | * operate on a stream of {@link AccessibilityEvent}s, and ordering, timing, or comparison of 36 | * multiple events may be required to properly evaluate an accessibility issue. The class defines 37 | * several callback methods to simplify managing this state. The component responsible for executing 38 | * this check must invoke {@link #onExecutionStarted()} when a new logical "test run" begins. It 39 | * must also invoke {@link #onExecutionEnded()} as it ends. Extending classes may wish to clean up 40 | * any state and return any final results from this method. 41 | *

42 | * NOTE: Although extending classes can access all AccessibilityEvents fired by the system during 43 | * the test run interval, no guarantees about stability of an underlying UI are made. Information 44 | * about the current view hierarchy state may be accessed via 45 | * {@link AccessibilityEvent#getSource()}, but implementations must take care when determining when 46 | * and how to maintain state related to this information across invocations of 47 | * {@link #runCheckOnEvent(AccessibilityEvent)}. A general recommendation is to only store 48 | * information related to the {@link AccessibilityEvent} stream as part of the state maintained by 49 | * an extension of this class. To write an {@link AccessibilityCheck} that verifies some properties 50 | * of a view hierarchy, use an {@link AccessibilityHierarchyCheck}. 51 | */ 52 | public abstract class AccessibilityEventCheck extends AccessibilityCheck { 53 | 54 | /** 55 | * Convenience method for easily filtering the {@link AccessibilityEvent}s to be dispatched to 56 | * {@link #runCheckOnEvent(AccessibilityEvent)} for evaluation. 57 | *

58 | * NOTE: The default implementation accepts all incoming events. 59 | * 60 | * @param event The event to filter 61 | * @return {@code true} if this {@code AccessibilityEventCheck} should handle this event, 62 | * {@code false} otherwise. 63 | */ 64 | protected boolean shouldHandleEvent(AccessibilityEvent event) { 65 | return true; 66 | } 67 | 68 | /** 69 | * Invoked when a new logical test run is beginning. Implementing checks should use this method to 70 | * initialize resources or state needed for evaluation, if needed. 71 | * {@link #shouldHandleEvent(AccessibilityEvent)} and {@link #runCheckOnEvent(AccessibilityEvent)} 72 | * are guaranteed to not be invoked until execution of this method terminates. 73 | */ 74 | public void onExecutionStarted() {} 75 | 76 | /** 77 | * Invoked by the component responsible for executing this {@code AccessibilityCheck} to dispatch 78 | * an {@link AccessibilityEvent} to this check's logic. 79 | * 80 | * @param event The event to dispatch 81 | * @return A {@link List} of {@link AccessibilityEventCheckResult}s generated by the check. If no 82 | * such results are generated, an empty collection will be returned. 83 | */ 84 | public final List dispatchEvent(AccessibilityEvent event) { 85 | if (shouldHandleEvent(event)) { 86 | return runCheckOnEvent(event); 87 | } 88 | return Collections.emptyList(); 89 | } 90 | 91 | /** 92 | * Mechanism by which {@link AccessibilityEvent}s are delivered for evaluation. Extending classes 93 | * should override this method and return {@link AccessibilityEventCheckResult}s to indicate 94 | * results, if appropriate. 95 | * 96 | * @param event The event to evaluate. 97 | * @return A List of {@link AccessibilityEventCheckResult}s indicating an accessibility issue, if 98 | * any. If no issues are found, or if an issue cannot be identified from the stream of 99 | * {@link AccessibilityEvent}s observed, an empty collection will be returned. 100 | */ 101 | protected abstract List runCheckOnEvent(AccessibilityEvent event); 102 | 103 | /** 104 | * Invoked when a logical test run has concluded. Implementing checks should use this to clear any 105 | * state relevant to the previous evaluation, if needed. It is guaranteed that {@link 106 | * #shouldHandleEvent(AccessibilityEvent)} or {@link #runCheckOnEvent(AccessibilityEvent)} will 107 | * not be invoked after execution of this method has begun until a new logical test run is 108 | * signaled by {@link #onExecutionStarted()}; 109 | * 110 | * @return A List of {@link AccessibilityEventCheckResult}s indicating accessibility issues, if 111 | * any. If no issues are found, or if an issue cannot be identified from the stream of {@link 112 | * AccessibilityEvent}s observed, an empty collection will be returned. 113 | */ 114 | @CanIgnoreReturnValue 115 | public List onExecutionEnded() { 116 | return Collections.emptyList(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityEventCheckResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework; 16 | 17 | import android.view.accessibility.AccessibilityEvent; 18 | 19 | /** Result generated when an accessibility check runs on a {@link AccessibilityEvent}. */ 20 | public final class AccessibilityEventCheckResult extends AccessibilityCheckResult { 21 | 22 | private final AccessibilityEvent event; 23 | 24 | /** 25 | * @param checkClass The check that generated the error 26 | * @param type The type of the result 27 | * @param message A human-readable message explaining the error 28 | * @param event The {@link AccessibilityEvent} reported as the cause of the result 29 | */ 30 | public AccessibilityEventCheckResult(Class checkClass, 31 | AccessibilityCheckResultType type, CharSequence message, AccessibilityEvent event) { 32 | super(checkClass, type, message); 33 | this.event = AccessibilityEvent.obtain(event); 34 | } 35 | 36 | /** 37 | * @return The {@link AccessibilityEvent} to which the result applies 38 | */ 39 | public AccessibilityEvent getEvent() { 40 | return event; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityHierarchyCheckResultWithImage.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType; 6 | import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElement; 7 | import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.Image; 8 | 9 | /** 10 | * Result generated when an accessibility check runs on a {@code ViewHierarchyElement}. Includes an 11 | * image of the visible region of the View that is the subject of the result. 12 | * 13 | *

The Image should be regarded as supplemental information regarding the result, and not part of 14 | * the result. It presence does not affect the equality or hashCode of a result, and it will not be 15 | * preserved by the {@link #toProto()} method. 16 | */ 17 | public class AccessibilityHierarchyCheckResultWithImage extends AccessibilityHierarchyCheckResult { 18 | 19 | private final Image viewImage; 20 | 21 | /** 22 | * @param checkClass The class of the check reporting the error 23 | * @param type The type of result 24 | * @param element The element that the result pertains to 25 | * @param resultId an integer unique to all results emitted from a single class 26 | * @param metadata extra data about this result 27 | * @param viewImage An image of the visible region of the view that is the subject of the result 28 | */ 29 | public AccessibilityHierarchyCheckResultWithImage( 30 | Class checkClass, 31 | AccessibilityCheckResultType type, 32 | ViewHierarchyElement element, 33 | int resultId, 34 | ResultMetadata metadata, 35 | Image viewImage) { 36 | super(checkClass, type, element, resultId, metadata); 37 | this.viewImage = viewImage; 38 | } 39 | 40 | @Override 41 | public AccessibilityHierarchyCheckResultWithImage getSuppressedResultCopy() { 42 | return new AccessibilityHierarchyCheckResultWithImage( 43 | getSourceCheckClass().asSubclass(AccessibilityHierarchyCheck.class), 44 | AccessibilityCheckResultType.SUPPRESSED, 45 | checkNotNull(getElement()), 46 | getResultId(), 47 | checkNotNull(getMetadata()), 48 | viewImage); 49 | } 50 | 51 | /** 52 | * Returns an image of the visible region of the View that is the subject of the result, if an 53 | * image was given. 54 | */ 55 | public Image getViewImage() { 56 | return viewImage; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityViewHierarchyCheck.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package com.google.android.apps.common.testing.accessibility.framework; 18 | 19 | import android.view.View; 20 | import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchyAndroid; 21 | import java.util.List; 22 | import org.checkerframework.checker.nullness.qual.Nullable; 23 | 24 | /** 25 | * Base class to check the accessibility of all {@link View}s in a hierarchy. 26 | * 27 | * @deprecated New accessibility checks should use {@link AccessibilityHierarchyCheck} to evaluate 28 | * an {@link AccessibilityHierarchyAndroid} rather than a hierarchy of {@link View}s directly. 29 | */ 30 | @Deprecated 31 | public abstract class AccessibilityViewHierarchyCheck extends AccessibilityCheck { 32 | 33 | /** 34 | * Run the check on the view. 35 | * 36 | * @param root The non-null root view of the hierarchy to check. 37 | * @param parameters Optional input data or preferences. 38 | * @return A list of interesting results encountered while running the check. The list will be 39 | * empty if the check passes without incident. 40 | */ 41 | public abstract List runCheckOnViewHierarchy( 42 | View root, @Nullable Parameters parameters); 43 | 44 | /** @see AccessibilityViewHierarchyCheck#runCheckOnViewHierarchy(View, Parameters) */ 45 | public List runCheckOnViewHierarchy(View root) { 46 | return runCheckOnViewHierarchy(root, null); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/AnnouncementEventCheck.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework; 16 | 17 | import android.view.View; 18 | import android.view.accessibility.AccessibilityEvent; 19 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType; 20 | import com.google.android.apps.common.testing.accessibility.framework.strings.StringManager; 21 | import com.google.common.collect.ImmutableList; 22 | import java.util.List; 23 | import java.util.Locale; 24 | 25 | /** 26 | * Check which may be used to flag use of {@link View#announceForAccessibility(CharSequence)} or 27 | * dispatch of {@link AccessibilityEvent}s of type {@link AccessibilityEvent#TYPE_ANNOUNCEMENT}. The 28 | * use of these events, expect in specific situations, can be disruptive to the user. 29 | */ 30 | public class AnnouncementEventCheck extends AccessibilityEventCheck { 31 | 32 | @Override 33 | public boolean shouldHandleEvent(AccessibilityEvent event) { 34 | return (event != null) && (event.getEventType() == AccessibilityEvent.TYPE_ANNOUNCEMENT); 35 | } 36 | 37 | @Override 38 | public List runCheckOnEvent(AccessibilityEvent event) { 39 | 40 | String message = 41 | StringManager.getString(Locale.getDefault(), "result_message_disruptive_announcement"); 42 | return ImmutableList.of( 43 | new AccessibilityEventCheckResult( 44 | this.getClass(), AccessibilityCheckResultType.WARNING, message, event)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/Answer.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.proto.AccessibilityEvaluationProtos.AnswerProto; 4 | import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; 5 | import com.google.common.annotations.Beta; 6 | import java.util.Objects; 7 | import org.checkerframework.checker.nullness.qual.Nullable; 8 | 9 | /** A response to a {@link Question} about an {@link AccessibilityHierarchyCheckResult} */ 10 | @Beta 11 | public class Answer { 12 | private final Class answerTypeClass; 13 | private final Question question; 14 | private final ResultMetadata metadata; 15 | 16 | /** 17 | * @param answerTypeClass class of the answer type represented by this answer 18 | * @param question the Question this is an answer to 19 | * @param metadata data needed to capture an answer 20 | */ 21 | public Answer( 22 | Class answerTypeClass, Question question, ResultMetadata metadata) { 23 | this.answerTypeClass = answerTypeClass; 24 | this.question = question; 25 | this.metadata = metadata; 26 | } 27 | 28 | /** Returns the {@link AnswerType} class of this answer. */ 29 | public Class getAnswerTypeClass() { 30 | return answerTypeClass; 31 | } 32 | 33 | /** Returns the {@link Question} this is the answer to. */ 34 | public Question getQuestion() { 35 | return question; 36 | } 37 | 38 | /** 39 | * Returns the {@link ResultMetadata} that holds the answer data. The keys of the metadata are 40 | * determined by the {@link AnswerType} and interpreted by the {@link QuestionHandler}. 41 | */ 42 | public ResultMetadata getMetadata() { 43 | return metadata; 44 | } 45 | 46 | /** Creates a protocol buffer for this {@link Answer} following its format */ 47 | public AnswerProto toProto() { 48 | AnswerProto.Builder builder = AnswerProto.newBuilder(); 49 | builder.setAnswerTypeClass(getAnswerTypeClass().getName()); 50 | builder.setQuestion(getQuestion().toProto()); 51 | if (getMetadata() instanceof HashMapResultMetadata) { 52 | builder.setMetadata(((HashMapResultMetadata) getMetadata()).toProto()); 53 | } 54 | return builder.build(); 55 | } 56 | 57 | /** 58 | * Creates an {@link Answer} from its protocol buffer format. 59 | * 60 | * @param proto The protocol buffer representation of an answer, created with {@link #toProto()} 61 | * @param associatedHierarchy The {@link AccessibilityHierarchy} that this answer is about 62 | * @throws IllegalArgumentException passing the {@link ClassNotFoundException} if there is an 63 | * invalid {@link AnswerType} class in the {@link AnswerProto}. This should not occur as the 64 | * {@link AnswerProto} is written in the same environment of ATF in which it is read. 65 | */ 66 | public static Answer fromProto(AnswerProto proto, AccessibilityHierarchy associatedHierarchy) { 67 | Class answerTypeClass = null; 68 | try { 69 | answerTypeClass = Class.forName(proto.getAnswerTypeClass()).asSubclass(AnswerType.class); 70 | } catch (ClassNotFoundException e) { 71 | throw new IllegalArgumentException(e); 72 | } 73 | Question question = Question.fromProto(proto.getQuestion(), associatedHierarchy); 74 | 75 | HashMapResultMetadata metadata = HashMapResultMetadata.fromProto(proto.getMetadata()); 76 | 77 | return new Answer(answerTypeClass, question, metadata); 78 | } 79 | 80 | @Override 81 | public boolean equals(@Nullable Object o) { 82 | if (this == o) { 83 | return true; 84 | } 85 | if (!(o instanceof Answer)) { 86 | return false; 87 | } 88 | 89 | Answer that = (Answer) o; 90 | if (getAnswerTypeClass() != that.getAnswerTypeClass()) { 91 | return false; 92 | } 93 | if (!getQuestion().equals(that.getQuestion())) { 94 | return false; 95 | } 96 | return Objects.equals(getMetadata(), that.getMetadata()); 97 | } 98 | 99 | @Override 100 | public int hashCode() { 101 | return Objects.hash(getAnswerTypeClass(), getQuestion(), getMetadata()); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/AnswerType.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework; 2 | 3 | import com.google.common.annotations.Beta; 4 | 5 | /** 6 | * The type of answer elicited for a question. E.g. multiple choice, a String. Each type of answer 7 | * will extend this class. 8 | */ 9 | @Beta 10 | public abstract class AnswerType {} 11 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/AnswerTypes.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import com.google.android.apps.common.testing.accessibility.framework.replacements.Rect; 6 | import com.google.common.annotations.Beta; 7 | import com.google.common.collect.ImmutableList; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** Container class for answer types */ 12 | @Beta 13 | public final class AnswerTypes { 14 | private AnswerTypes() {} 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/QuestionHandler.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework; 2 | 3 | import com.google.common.annotations.Beta; 4 | import com.google.common.collect.ImmutableList; 5 | import java.util.Locale; 6 | import org.checkerframework.checker.nullness.qual.Nullable; 7 | 8 | /** 9 | * Manages questions, answers, and updating of results related to an {@link 10 | * AccessibilityHierarchyCheck} 11 | */ 12 | @Beta 13 | public abstract class QuestionHandler { 14 | /** Returns whether the result has an unanswered {@link Question} */ 15 | public boolean hasQuestion(AccessibilityHierarchyCheckResult result) { 16 | return getNextQuestion(result) != null; 17 | } 18 | 19 | /** 20 | * Returns the next {@link Question} if the {@link AccessibilityHierarchyCheckResult} has one, 21 | * else returns {@code null} 22 | */ 23 | public abstract @Nullable Question getNextQuestion(AccessibilityHierarchyCheckResult result); 24 | 25 | /** 26 | * Returns the string for the phrasing of the passed question 27 | * 28 | * @param question the question needing to be phrased 29 | * @param locale the desired locale of the message 30 | * @return a human-readable String representing the phrasing of the Question 31 | */ 32 | public abstract String getQuestionMessage(Question question, Locale locale); 33 | 34 | /** 35 | * Returns zero to many results to replace the original result based on an answer to a question 36 | * 37 | * @param answer the {@link Answer} (which contains its associated {@link Question} and original 38 | * {@link AccessibilityHierarchyCheckResult}) that will be used to update the result 39 | * @return a potentially empty list of new result(s) to replace the original result based on the 40 | * information in the original result and the answer list 41 | */ 42 | public abstract ImmutableList updateResult(Answer answer); 43 | 44 | /** 45 | * Returns an {@link AccessibilityHierarchyCheckResult} with the information of the original 46 | * result and the new answer appended to list of answers 47 | */ 48 | // originalResult is an {@link AccessibilityHierarchyCheckResult} so the sourceCheckClass is 49 | // guaranteed to be from a {@link AccessibilityHierarchyCheck} subclass 50 | @SuppressWarnings("unchecked") 51 | protected static ImmutableList updateResultByAppendingAnswer( 52 | Answer answer) { 53 | AccessibilityHierarchyCheckResult originalResult = answer.getQuestion().getOriginalResult(); 54 | ImmutableList answers = 55 | ImmutableList.builder().addAll(originalResult.getAnswers()).add(answer).build(); 56 | return ImmutableList.of( 57 | new AccessibilityHierarchyCheckResult( 58 | (Class) originalResult.getSourceCheckClass(), 59 | originalResult.getType(), 60 | originalResult.getElement(), 61 | originalResult.getResultId(), 62 | originalResult.getMetadata(), 63 | answers)); 64 | } 65 | 66 | /** 67 | * Return a {@link Answer} that answers the question of questionId, if it has been answered, else 68 | * returns {@code null}. Assumes answers are kept in an ordered list. 69 | */ 70 | protected static @Nullable Answer getFirstAnswerForQuestionId( 71 | AccessibilityHierarchyCheckResult result, int questionId) { 72 | for (Answer answer : result.getAnswers()) { 73 | if (answer.getQuestion().getQuestionId() == questionId) { 74 | return answer; 75 | } 76 | } 77 | return null; 78 | } 79 | 80 | /** 81 | * Return a collection of {@link Answer} objects that answers the question of questionId. The 82 | * collection will be empty if the question is unanswered. 83 | */ 84 | protected static ImmutableList getAnswersForQuestionId( 85 | AccessibilityHierarchyCheckResult result, int questionId) { 86 | ImmutableList.Builder answersBuilder = ImmutableList.builder(); 87 | for (Answer answer : result.getAnswers()) { 88 | if (answer.getQuestion().getQuestionId() == questionId) { 89 | answersBuilder.add(answer); 90 | } 91 | } 92 | return answersBuilder.build(); 93 | } 94 | 95 | /** Returns whether a question with questionId has been asked about a given result */ 96 | protected static boolean haveAskedQuestion( 97 | AccessibilityHierarchyCheckResult result, int questionId) { 98 | return getFirstAnswerForQuestionId(result, questionId) != null; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/QuestionType.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework; 2 | 3 | import com.google.common.annotations.Beta; 4 | 5 | /** 6 | * The type of information needed for a question. E.g. identify one element, identify many elements. 7 | * Each type of question will extend this class. 8 | */ 9 | @Beta 10 | public abstract class QuestionType {} 11 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/QuestionTypes.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework; 2 | 3 | import com.google.common.annotations.Beta; 4 | 5 | /** Container class for instances of {@link QuestionType} */ 6 | @Beta 7 | public final class QuestionTypes { 8 | private QuestionTypes() {} 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/ViewChecker.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework; 2 | 3 | import android.view.View; 4 | import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchyAndroid; 5 | import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElement; 6 | import com.google.android.libraries.accessibility.utils.log.LogUtils; 7 | import com.google.common.collect.BiMap; 8 | import com.google.common.collect.HashBiMap; 9 | import com.google.common.collect.ImmutableList; 10 | import com.google.common.collect.ImmutableSet; 11 | import com.google.errorprone.annotations.CanIgnoreReturnValue; 12 | import com.google.errorprone.annotations.CheckReturnValue; 13 | import java.util.List; 14 | import org.checkerframework.checker.nullness.qual.Nullable; 15 | 16 | /** Evaluates accessibility checks against specified Views. */ 17 | @CheckReturnValue 18 | public class ViewChecker { 19 | 20 | private static final String TAG = "ViewChecker"; 21 | private boolean obtainCharacterLocations = false; 22 | 23 | /** 24 | * Indicates whether text character locations should be requested. 25 | * 26 | * @param obtainCharacterLocations The value to which the flag should be set. 27 | * @return this 28 | */ 29 | @CanIgnoreReturnValue 30 | public ViewChecker setObtainCharacterLocations(boolean obtainCharacterLocations) { 31 | this.obtainCharacterLocations = obtainCharacterLocations; 32 | return this; 33 | } 34 | 35 | /** 36 | * Runs a single AccessibilityHierarchyCheck on a View. 37 | * 38 | * @param root The root view of the hierarchy to check. 39 | * @param check The AccessibilityHierarchyCheck to be run. 40 | * @param parameters Optional input data or preferences. 41 | * @return A list of interesting results encountered while running the check. The list will be 42 | * empty if the check passes without incident. 43 | */ 44 | /* package */ ImmutableList runCheckOnView( 45 | AccessibilityHierarchyCheck check, View root, @Nullable Parameters parameters) { 46 | return runChecksOnView(ImmutableSet.of(check), root, parameters); 47 | } 48 | 49 | /** 50 | * Runs a set of AccessibilityHierarchyChecks on a View. 51 | * 52 | * @param root The root view of the hierarchy to check. 53 | * @param checks The AccessibilityHierarchyChecks to be run. 54 | * @param parameters Optional input data or preferences. 55 | * @return A list of interesting results encountered while running the checks. The list will be 56 | * empty if the checks pass without incident. 57 | */ 58 | public ImmutableList runChecksOnView( 59 | ImmutableSet checks, 60 | View root, 61 | @Nullable Parameters parameters) { 62 | 63 | // Construct the AccessibilityHierarchyAndroid from the actual view root, as to capture all 64 | // available information within the view hierarchy. 65 | View actualRoot = root.getRootView(); 66 | BiMap mapFromElementIdToView = HashBiMap.create(); 67 | AccessibilityHierarchyAndroid hierarchy = 68 | AccessibilityHierarchyAndroid.newBuilder(actualRoot) 69 | .setViewOriginMap(mapFromElementIdToView) 70 | .setObtainCharacterLocations(obtainCharacterLocations) 71 | .build(); 72 | 73 | // Although we captured our hierarchy from the actual root view, we pass along information 74 | // about the provided "root" in order to constrain evaluation to the provided sub-hierarchy. 75 | Long rootId = mapFromElementIdToView.inverse().get(root); 76 | ViewHierarchyElement evalRoot = (rootId != null) ? hierarchy.getViewById(rootId) : null; 77 | if (evalRoot == null) { 78 | LogUtils.e(TAG, "Unable to determine root View for evaluation, using full hierarchy."); 79 | } 80 | 81 | // Run each check and collect the results. 82 | ImmutableList.Builder results = ImmutableList.builder(); 83 | for (AccessibilityHierarchyCheck check : checks) { 84 | List hierarchyCheckResults = 85 | check.runCheckOnHierarchy(hierarchy, evalRoot, parameters); 86 | for (AccessibilityHierarchyCheckResult hierarchyCheckResult : hierarchyCheckResults) { 87 | // Try to map each element back to its corresponding View 88 | ViewHierarchyElement element = hierarchyCheckResult.getElement(); 89 | View checkedView = 90 | (element != null) ? mapFromElementIdToView.get(element.getCondensedUniqueId()) : null; 91 | results.add( 92 | new AccessibilityViewCheckResult( 93 | check.getClass(), hierarchyCheckResult, element, checkedView)); 94 | } 95 | } 96 | return results.build(); 97 | } 98 | 99 | /** 100 | * Runs a set of AccessibilityViewHierarchyChecks on a View. 101 | * 102 | * @param checks The checks to be run. 103 | * @param root The root view of the hierarchy to check. 104 | * @param parameters Optional input data or preferences. 105 | * @return A list of interesting results encountered while running the checks. The list will be 106 | * empty if the checks pass without incident. 107 | */ 108 | public ImmutableList runViewChecksOnView( 109 | ImmutableSet checks, 110 | View root, 111 | @Nullable Parameters parameters) { 112 | return runChecksOnView(convertViewChecks(checks), root, parameters); 113 | } 114 | 115 | /** 116 | * Converts a set of AccessibilityViewHierarchyChecks - each assumed to be an {@link 117 | * AccessibilityCheckPresetAndroid.DelegatedViewHierarchyCheck} - to a set of the wrapped 118 | * AccessibilityHierarchyChecks. 119 | */ 120 | private static ImmutableSet convertViewChecks( 121 | ImmutableSet viewChecks) { 122 | ImmutableSet.Builder checks = ImmutableSet.builder(); 123 | for (AccessibilityViewHierarchyCheck viewCheck : viewChecks) { 124 | checks.add( 125 | ((AccessibilityCheckPresetAndroid.DelegatedViewHierarchyCheck) viewCheck) 126 | .getAccessibilityHierarchyCheck()); 127 | } 128 | return checks.build(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/checks/EditableContentDescCheck.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework.checks; 16 | 17 | import static java.lang.Boolean.TRUE; 18 | 19 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheck.Category; 20 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType; 21 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck; 22 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 23 | import com.google.android.apps.common.testing.accessibility.framework.Parameters; 24 | import com.google.android.apps.common.testing.accessibility.framework.ResultMetadata; 25 | import com.google.android.apps.common.testing.accessibility.framework.ViewHierarchyElementUtils; 26 | import com.google.android.apps.common.testing.accessibility.framework.replacements.TextUtils; 27 | import com.google.android.apps.common.testing.accessibility.framework.strings.StringManager; 28 | import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; 29 | import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElement; 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | import java.util.Locale; 33 | import org.checkerframework.checker.nullness.qual.Nullable; 34 | 35 | /** 36 | * Check to ensure that an editable TextView is not labeled by a contentDescription 37 | */ 38 | public class EditableContentDescCheck extends AccessibilityHierarchyCheck { 39 | 40 | /** Result when thew view is not {@code importantForAccessibility} */ 41 | public static final int RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY = 1; 42 | /** Result when the view is an editable {@link TextView} with a {@code contentDescription}. */ 43 | public static final int RESULT_ID_EDITABLE_TEXTVIEW_CONTENT_DESC = 2; 44 | /** Result when the view is not an editable {@link TextView}. */ 45 | public static final int RESULT_ID_NOT_EDITABLE_TEXTVIEW = 3; 46 | 47 | @Override 48 | protected String getHelpTopic() { 49 | return "6378120"; // Editable View labels 50 | } 51 | 52 | @Override 53 | public Category getCategory() { 54 | return Category.IMPLEMENTATION; 55 | } 56 | 57 | @Override 58 | public List runCheckOnHierarchy( 59 | AccessibilityHierarchy hierarchy, 60 | @Nullable ViewHierarchyElement fromRoot, 61 | @Nullable Parameters parameters) { 62 | List results = new ArrayList<>(); 63 | List viewsToEval = getElementsToEvaluate(fromRoot, hierarchy); 64 | for (ViewHierarchyElement view : viewsToEval) { 65 | if (!view.isImportantForAccessibility()) { 66 | results.add(new AccessibilityHierarchyCheckResult( 67 | this.getClass(), 68 | AccessibilityCheckResultType.NOT_RUN, 69 | view, 70 | RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY, 71 | null)); 72 | continue; 73 | } 74 | 75 | if (TRUE.equals(view.isEditable()) 76 | || view.checkInstanceOf(ViewHierarchyElementUtils.EDIT_TEXT_CLASS_NAME)) { 77 | if (!TextUtils.isEmpty(view.getContentDescription())) { 78 | results.add(new AccessibilityHierarchyCheckResult(this.getClass(), 79 | AccessibilityCheckResultType.ERROR, 80 | view, 81 | RESULT_ID_EDITABLE_TEXTVIEW_CONTENT_DESC, 82 | null)); 83 | } 84 | } else { 85 | results.add(new AccessibilityHierarchyCheckResult(this.getClass(), 86 | AccessibilityCheckResultType.NOT_RUN, 87 | view, 88 | RESULT_ID_NOT_EDITABLE_TEXTVIEW, 89 | null)); 90 | } 91 | } 92 | 93 | return results; 94 | } 95 | 96 | @Override 97 | public String getMessageForResultData( 98 | Locale locale, int resultId, @Nullable ResultMetadata metadata) { 99 | return generateMessageForResultId(locale, resultId); 100 | } 101 | 102 | @Override 103 | public String getShortMessageForResultData( 104 | Locale locale, int resultId, @Nullable ResultMetadata metadata) { 105 | return generateMessageForResultId(locale, resultId); 106 | } 107 | 108 | @Override 109 | public String getTitleMessage(Locale locale) { 110 | return StringManager.getString(locale, "check_title_editable_content_desc"); 111 | } 112 | 113 | private static String generateMessageForResultId(Locale locale, int resultId) { 114 | switch(resultId) { 115 | case RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY: 116 | return StringManager.getString(locale, "result_message_not_important_for_accessibility"); 117 | case RESULT_ID_EDITABLE_TEXTVIEW_CONTENT_DESC: 118 | return StringManager.getString(locale, "result_message_editable_textview_content_desc"); 119 | case RESULT_ID_NOT_EDITABLE_TEXTVIEW: 120 | return StringManager.getString(locale, "result_message_not_editable_textview"); 121 | default: 122 | throw new IllegalStateException("Unsupported result id"); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/integrations/AccessibilityViewCheckException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework.integrations; 16 | 17 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult; 18 | import java.util.List; 19 | 20 | /** 21 | * An exception class to be used for throwing exceptions with accessibility results. 22 | * 23 | * @deprecated Use the AccessibilityViewCheckException in the espresso sub-package. 24 | */ 25 | @Deprecated 26 | public abstract class AccessibilityViewCheckException extends RuntimeException { 27 | private final List results; 28 | 29 | protected AccessibilityViewCheckException(List results) { 30 | this.results = results; 31 | } 32 | 33 | /** 34 | * @return the list of results associated with this instance 35 | */ 36 | public List getResults() { 37 | return results; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/integrations/espresso/AccessibilityViewCheckException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework.integrations.espresso; 16 | 17 | import static com.google.common.base.Preconditions.checkArgument; 18 | import static com.google.common.base.Preconditions.checkNotNull; 19 | 20 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultDescriptor; 21 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult; 22 | import java.util.List; 23 | import java.util.Locale; 24 | 25 | /** An exception class to be used for throwing exceptions with accessibility results. */ 26 | public final class AccessibilityViewCheckException 27 | extends com.google.android.apps.common.testing.accessibility.framework.integrations 28 | .AccessibilityViewCheckException { 29 | 30 | private final AccessibilityCheckResultDescriptor resultDescriptor; 31 | 32 | /** Create an instance with the default {@link AccessibilityCheckResultDescriptor} */ 33 | public AccessibilityViewCheckException(List results) { 34 | this(results, new AccessibilityCheckResultDescriptor()); 35 | } 36 | 37 | /** 38 | * Create an exception with results and a result descriptor to generate the message. 39 | * 40 | * @param results a list of {@link AccessibilityViewCheckResult}s that are associated with the 41 | * failure(s) that cause this to be thrown. 42 | * @param resultDescriptor the {@link AccessibilityCheckResultDescriptor} used to generate the 43 | * exception message. 44 | */ 45 | public AccessibilityViewCheckException( 46 | List results, 47 | AccessibilityCheckResultDescriptor resultDescriptor) { 48 | super(results); 49 | checkArgument( 50 | results != null && !results.isEmpty(), 51 | "AccessibilityViewCheckException requires at least 1 AccessibilityViewCheckResult"); 52 | this.resultDescriptor = checkNotNull(resultDescriptor); 53 | } 54 | 55 | @Override 56 | public String getMessage() { 57 | List results = getResults(); 58 | // Lump all result messages into one string to be the exception message 59 | StringBuilder exceptionMessage = new StringBuilder(); 60 | 61 | String resultCountMessage = 62 | (results.size() == 1) 63 | ? "There was 1 accessibility result:\n" 64 | : String.format(Locale.US, "There were %d accessibility results:\n", results.size()); 65 | exceptionMessage.append(resultCountMessage); 66 | for (int i = 0; i < results.size(); i++) { 67 | if (i > 0) { 68 | exceptionMessage.append(",\n"); 69 | } 70 | exceptionMessage.append(resultDescriptor.describeResult(results.get(i))); 71 | } 72 | return exceptionMessage.toString(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/integrations/espresso/CheckResultsCallback.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.integrations.espresso; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult; 4 | import com.google.auto.value.AutoValue; 5 | import com.google.common.collect.ImmutableList; 6 | import org.checkerframework.checker.nullness.qual.Nullable; 7 | 8 | /** Data passed to a listener after one evaluation of accessibility checks. */ 9 | @AutoValue 10 | public abstract class CheckResultsCallback { 11 | 12 | /** 13 | * Results from the evaluation of checks. This may include results whose {@code getType} returns 14 | * {@code NOT_RUN} or {@code SUPPRESSED} if a suppressing result matcher was specified. 15 | */ 16 | public abstract ImmutableList getAccessibilityViewCheckResults(); 17 | 18 | /** Path to a screenshot if one was captured and saved to a file. */ 19 | public abstract @Nullable String getScreenshotPath(); 20 | 21 | public static Builder builder() { 22 | return new AutoValue_CheckResultsCallback.Builder(); 23 | } 24 | 25 | /** Builder for CheckResultsCallback. */ 26 | @AutoValue.Builder 27 | public abstract static class Builder { 28 | 29 | public abstract Builder setAccessibilityViewCheckResults( 30 | ImmutableList results); 31 | 32 | public abstract Builder setScreenshotPath(@Nullable String screenshotPath); 33 | 34 | public abstract CheckResultsCallback build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/integrations/espresso/Screenshotter.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.integrations.espresso; 2 | 3 | 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.graphics.Picture; 7 | import android.graphics.Rect; 8 | import android.os.Build; 9 | import android.os.StrictMode; 10 | import android.os.StrictMode.ThreadPolicy; 11 | import android.view.View; 12 | import com.google.android.libraries.accessibility.utils.log.LogUtils; 13 | import org.checkerframework.checker.nullness.qual.Nullable; 14 | 15 | /** 16 | * Creates a pseudo screenshot. 17 | * 18 | *

The implementation intends to satisfy two requirements: 19 | * 20 | *

    21 | *
  1. It is suitable for use within an Espresso {@link android.support.test.espresso.ViewAction} 22 | * or {@link android.support.test.espresso.ViewAssertion}, which execute within the UI thread. 23 | * So it must be a lightweight operation, unlike {@link 24 | * android.app.UiAutomation#takeScreenshot()}. 25 | *
  2. The coordinates of the visible portion of the View - and its children - match the 26 | * coordinates of the pixels within the Bitmap. 27 | *
28 | */ 29 | class Screenshotter { 30 | private static final String TAG = "Screenshotter"; 31 | 32 | /** 33 | * Returns a Bitmap with a rendering of the visible portion of the view and all of its children. 34 | * In some cases, the Bitmap may be smaller than the size of the display. This assumes that the 35 | * content of the display below and to the right of the View are of no interest. 36 | * 37 | * @return {@code null} if the width or height of the given view is zero 38 | */ 39 | public @Nullable Bitmap getScreenshot(View view) { 40 | if (view.getWidth() <= 0 || view.getHeight() <= 0) { 41 | return null; 42 | } 43 | 44 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 45 | return getScreenShotPPlus(view); 46 | } else { 47 | return getScreenshotPreP(view); 48 | } 49 | } 50 | 51 | private static Bitmap getScreenShotPPlus(View view) { 52 | // Obtaining a hardware Bitmap's image can trigger a strict mode violation, see b/136288531. 53 | StrictMode.ThreadPolicy originalPolicy = StrictMode.getThreadPolicy(); 54 | StrictMode.setThreadPolicy(ThreadPolicy.LAX); 55 | try { 56 | LogUtils.i(TAG, ">= P"); 57 | Picture picture = new Picture(); 58 | int[] windowOffset = getWindowOffset(view); 59 | Canvas canvas = 60 | picture.beginRecording( 61 | windowOffset[0] + view.getWidth(), windowOffset[1] + view.getHeight()); 62 | view.computeScroll(); 63 | canvas.translate(windowOffset[0] - view.getScrollX(), windowOffset[1] - view.getScrollY()); 64 | view.draw(canvas); 65 | 66 | // End recording before creating the Bitmap so that the Picture is fully initialized prior to 67 | // creating the Bitmap copy below. Matches the previous call to beginRecording. See 68 | // b/80539264. 69 | picture.endRecording(); 70 | Bitmap bitmap = 71 | Bitmap.createBitmap( 72 | picture, picture.getWidth(), picture.getHeight(), Bitmap.Config.ARGB_8888); 73 | return bitmap; 74 | } finally { 75 | StrictMode.setThreadPolicy(originalPolicy); 76 | } 77 | } 78 | 79 | private static Bitmap getScreenshotPreP(View view) { 80 | // The drawing cache is a cheap, easy and compatible way to generate our screenshot. 81 | // However, because the cache is capped at a maximum size, this method may not work 82 | // for large views. So, we first try the drawing cache, and then if that fails 83 | // we fall back to building the screenshot bitmap manually. 84 | view.buildDrawingCache(); 85 | Bitmap bitmap = view.getDrawingCache(); 86 | if (bitmap == null) { 87 | LogUtils.i(TAG, "PreP without drawing cache"); 88 | bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); 89 | bitmap.setDensity(view.getResources().getDisplayMetrics().densityDpi); 90 | view.computeScroll(); 91 | Canvas canvas = new Canvas(bitmap); 92 | canvas.translate(-view.getScrollX(), -view.getScrollY()); 93 | view.draw(canvas); 94 | } else { 95 | LogUtils.i(TAG, "PreP from drawing cache"); 96 | bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false); 97 | view.destroyDrawingCache(); 98 | } 99 | 100 | return expandBitmapIfWindowOffset(bitmap, view); 101 | } 102 | 103 | // If the window is offset, expand the bitmap 104 | private static Bitmap expandBitmapIfWindowOffset(Bitmap bitmap, View view) { 105 | int[] windowOffset = getWindowOffset(view); 106 | if ((windowOffset[0] != 0) || (windowOffset[1] != 0)) { 107 | Rect destRect = new Rect(); 108 | if (view.getGlobalVisibleRect(destRect)) { 109 | LogUtils.i(TAG, "Resizing " + destRect); 110 | Bitmap biggerBitmap = 111 | Bitmap.createBitmap( 112 | view.getWidth() + windowOffset[0], 113 | view.getHeight() + windowOffset[1], 114 | Bitmap.Config.ARGB_8888); 115 | Canvas canvas = new Canvas(biggerBitmap); 116 | destRect.offset(windowOffset[0], windowOffset[1]); 117 | canvas.drawBitmap(bitmap, /* src= */ null, destRect, /* paint= */ null); 118 | return biggerBitmap; 119 | } 120 | } 121 | return bitmap; 122 | } 123 | 124 | private static int[] getWindowOffset(View view) { 125 | int[] locationOnScreen = new int[2]; 126 | int[] locationInWindow = new int[2]; 127 | view.getLocationOnScreen(locationOnScreen); 128 | view.getLocationInWindow(locationInWindow); 129 | 130 | // Usually these offsets will be zero, except when the view is in a dialog window. 131 | int xOffset = locationOnScreen[0] - locationInWindow[0]; 132 | int yOffset = locationOnScreen[1] - locationInWindow[1]; 133 | return new int[] {xOffset, yOffset}; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/ocr/OcrEngine.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.ocr; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.Image; 4 | 5 | /** 6 | * Platform-independent representation of an OCR engine which does actual character identification. 7 | */ 8 | public interface OcrEngine { 9 | 10 | /** 11 | * Detects and recognizes texts in a screenshot. 12 | * 13 | * @param screenshot The screenshot to detect and recognize texts from 14 | * @return A {@link OcrResult} which contains a list of recognized texts 15 | */ 16 | OcrResult detect(Image screenshot); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/ocr/OcrResult.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.ocr; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.proto.AccessibilityEvaluationProtos.OcrResultProto; 4 | import com.google.common.collect.ImmutableList; 5 | import com.google.errorprone.annotations.Immutable; 6 | import org.checkerframework.checker.nullness.qual.Nullable; 7 | 8 | /** The OCR text recognition result from a screenshot */ 9 | @Immutable 10 | public class OcrResult { 11 | 12 | public static final OcrResult EMPTY_RESULT = new OcrResult(ImmutableList.of()); 13 | 14 | private final ImmutableList texts; 15 | 16 | public OcrResult(ImmutableList texts) { 17 | this.texts = texts; 18 | } 19 | 20 | public OcrResult(OcrResultProto ocrResultProto) { 21 | ImmutableList.Builder builder = new ImmutableList.Builder<>(); 22 | for (int i = 0; i < ocrResultProto.getTextsCount(); i++) { 23 | builder.add(new TextComponent(ocrResultProto.getTexts(i))); 24 | } 25 | texts = builder.build(); 26 | } 27 | 28 | /** Returns an immutable list of recognized texts from the screenshot. */ 29 | public ImmutableList getTexts() { 30 | return texts; 31 | } 32 | 33 | /** Creates a protocol buffer for this {@link OcrResult} following its format. */ 34 | public OcrResultProto toProto() { 35 | OcrResultProto.Builder builder = OcrResultProto.newBuilder(); 36 | for (TextComponent text : texts) { 37 | builder.addTexts(text.toProto()); 38 | } 39 | return builder.build(); 40 | } 41 | 42 | @Override 43 | public boolean equals(@Nullable Object o) { 44 | if (this == o) { 45 | return true; 46 | } 47 | if (!(o instanceof OcrResult)) { 48 | return false; 49 | } 50 | 51 | OcrResult that = (OcrResult) o; 52 | return texts.equals(that.getTexts()); 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | return texts.hashCode(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/ocr/TextComponent.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.ocr; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.proto.AccessibilityEvaluationProtos.TextComponentProto; 4 | import com.google.android.apps.common.testing.accessibility.framework.replacements.Rect; 5 | import com.google.common.collect.ImmutableList; 6 | import com.google.errorprone.annotations.CanIgnoreReturnValue; 7 | import com.google.errorprone.annotations.Immutable; 8 | import java.util.List; 9 | import java.util.Objects; 10 | import org.checkerframework.checker.nullness.qual.Nullable; 11 | 12 | /** 13 | * Represents every entity across the hierarchy of recognized text. An entity may contain other 14 | * smaller entities, or may be an atom. 15 | */ 16 | @Immutable 17 | public class TextComponent { 18 | 19 | private final String value; 20 | 21 | private final Rect boundsInScreen; 22 | 23 | private final @Nullable String language; 24 | 25 | private final @Nullable Float confidence; 26 | 27 | private final ImmutableList components; 28 | 29 | private TextComponent( 30 | String value, 31 | Rect boundsInScreen, 32 | @Nullable String language, 33 | @Nullable Float confidence, 34 | ImmutableList components) { 35 | this.value = value; 36 | this.boundsInScreen = boundsInScreen; 37 | this.language = language; 38 | this.confidence = confidence; 39 | this.components = components; 40 | } 41 | 42 | TextComponent(TextComponentProto proto) { 43 | this.value = proto.getValue(); 44 | this.boundsInScreen = new Rect(proto.getBoundsInScreen()); 45 | this.language = proto.hasLanguage() ? proto.getLanguage() : null; 46 | this.confidence = proto.hasConfidence() ? proto.getConfidence() : null; 47 | ImmutableList.Builder builder = new ImmutableList.Builder<>(); 48 | for (int i = 0; i < proto.getComponentsCount(); i++) { 49 | builder.add(new TextComponent(proto.getComponents(i))); 50 | } 51 | this.components = builder.build(); 52 | } 53 | 54 | /** Returns the recognized text as a string. */ 55 | public String getValue() { 56 | return value; 57 | } 58 | 59 | /** Returns the bounding box containing the text in screen coordinates. */ 60 | public Rect getBoundsInScreen() { 61 | return boundsInScreen; 62 | } 63 | 64 | /** 65 | * Returns the prevailing language in the text, or {@code null} if the information is not 66 | * available. 67 | */ 68 | public @Nullable String getLanguage() { 69 | return language; 70 | } 71 | 72 | /** 73 | * Returns the confidence score of the recognized text, or {@code null} if the information is not 74 | * available. 75 | * 76 | *

The value of the confidence score is between 0.0 and 1.0. 77 | */ 78 | public @Nullable Float getConfidence() { 79 | return confidence; 80 | } 81 | 82 | /** 83 | * Returns a list of smaller components that comprise this entity. If this entity is an atom, an 84 | * empty list will be returned. 85 | */ 86 | public List getComponents() { 87 | return components; 88 | } 89 | 90 | @Override 91 | public boolean equals(@Nullable Object o) { 92 | if (this == o) { 93 | return true; 94 | } 95 | if (!(o instanceof TextComponent)) { 96 | return false; 97 | } 98 | 99 | TextComponent that = (TextComponent) o; 100 | return value.equals(that.getValue()) 101 | && boundsInScreen.equals(that.getBoundsInScreen()) 102 | && Objects.equals(language, that.getLanguage()) 103 | && Objects.equals(confidence, that.getConfidence()) 104 | && components.equals(that.getComponents()); 105 | } 106 | 107 | @Override 108 | public int hashCode() { 109 | return Objects.hash(value, boundsInScreen, language, confidence, components); 110 | } 111 | 112 | /** 113 | * Returns a new {@link Builder} that can build a {@link TextComponent} from the text as a string 114 | * and a bounding box which contains the text. 115 | */ 116 | public static Builder newBuilder(String value, Rect boundsInScreen) { 117 | return new Builder(value, boundsInScreen); 118 | } 119 | 120 | TextComponentProto toProto() { 121 | TextComponentProto.Builder builder = 122 | TextComponentProto.newBuilder().setValue(value).setBoundsInScreen(boundsInScreen.toProto()); 123 | if (language != null) { 124 | builder.setLanguage(language); 125 | } 126 | if (confidence != null) { 127 | builder.setConfidence(confidence); 128 | } 129 | for (TextComponent text : components) { 130 | builder.addComponents(text.toProto()); 131 | } 132 | return builder.build(); 133 | } 134 | 135 | /** A builder for {@link TextComponent}. */ 136 | public static class Builder { 137 | 138 | private final Rect boundsInScreen; 139 | 140 | private final String value; 141 | 142 | private @Nullable String language; 143 | 144 | private @Nullable Float confidence; 145 | 146 | private ImmutableList components = ImmutableList.of(); 147 | 148 | private Builder(String value, Rect boundsInScreen) { 149 | this.value = value; 150 | this.boundsInScreen = boundsInScreen; 151 | } 152 | 153 | @CanIgnoreReturnValue 154 | public Builder setLanguage(String language) { 155 | this.language = language; 156 | return this; 157 | } 158 | 159 | @CanIgnoreReturnValue 160 | public Builder setConfidence(float confidence) { 161 | this.confidence = confidence; 162 | return this; 163 | } 164 | 165 | @CanIgnoreReturnValue 166 | public Builder setTextComponents(ImmutableList textComponents) { 167 | this.components = textComponents; 168 | return this; 169 | } 170 | 171 | public TextComponent build() { 172 | return new TextComponent(value, boundsInScreen, language, confidence, components); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/replacements/LayoutParams.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.replacements; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AndroidFrameworkProtos.LayoutParamsProto; 4 | import org.checkerframework.checker.nullness.qual.Nullable; 5 | 6 | /** 7 | * Used as a local immutable replacement for Android's {@link android.view.ViewGroup.LayoutParams} 8 | */ 9 | public final class LayoutParams { 10 | 11 | /** @see android.view.ViewGroup.LayoutParams#MATCH_PARENT */ 12 | public static final int MATCH_PARENT = -1; 13 | 14 | /** @see android.view.ViewGroup.LayoutParams#WRAP_CONTENT */ 15 | public static final int WRAP_CONTENT = -2; 16 | 17 | private final int width; 18 | 19 | private final int height; 20 | 21 | public LayoutParams(int width, int height) { 22 | this.width = width; 23 | this.height = height; 24 | } 25 | 26 | public LayoutParams(LayoutParamsProto layoutParamsProto) { 27 | this(layoutParamsProto.getWidth(), layoutParamsProto.getHeight()); 28 | } 29 | 30 | /** @see android.view.ViewGroup.LayoutParams#width */ 31 | public int getWidth() { 32 | return width; 33 | } 34 | 35 | /** @see android.view.ViewGroup.LayoutParams#height */ 36 | public int getHeight() { 37 | return height; 38 | } 39 | 40 | public LayoutParamsProto toProto() { 41 | return LayoutParamsProto.newBuilder().setWidth(width).setHeight(height).build(); 42 | } 43 | 44 | @Override 45 | public boolean equals(@Nullable Object o) { 46 | if (this == o) { 47 | return true; 48 | } 49 | 50 | if (o instanceof LayoutParams) { 51 | LayoutParams params = (LayoutParams) o; 52 | return (params.width == width) && (params.height == height); 53 | } 54 | return false; 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | return 31 * width + height; 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return "LayoutParams(" + width + ", " + height + ")"; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/replacements/Point.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework.replacements; 16 | 17 | import com.google.errorprone.annotations.Immutable; 18 | import org.checkerframework.checker.nullness.qual.Nullable; 19 | 20 | /** Used as a local immutable replacement for Android's {@link android.graphics.Point} */ 21 | @Immutable 22 | public final class Point { 23 | 24 | private final int x; 25 | private final int y; 26 | 27 | public Point(int x, int y) { 28 | this.x = x; 29 | this.y = y; 30 | } 31 | 32 | /** @see android.graphics.Point#x */ 33 | public final int getX() { 34 | return x; 35 | } 36 | 37 | /** @see android.graphics.Point#y */ 38 | public final int getY() { 39 | return y; 40 | } 41 | 42 | @Override 43 | public boolean equals(@Nullable Object o) { 44 | if (this == o) { 45 | return true; 46 | } 47 | 48 | if (o == null || getClass() != o.getClass()) { 49 | return false; 50 | } 51 | 52 | Point point = (Point) o; 53 | return (x == point.x) && (y == point.y); 54 | } 55 | 56 | @Override 57 | public int hashCode() { 58 | int result = x; 59 | result = 31 * result + y; 60 | return result; 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | StringBuilder sb = new StringBuilder(32); 66 | sb.append("Point("); 67 | sb.append(x); 68 | sb.append(", "); 69 | sb.append(y); 70 | sb.append(")"); 71 | return sb.toString(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/replacements/Span.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.replacements; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AndroidFrameworkProtos.SpanProto; 4 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AndroidFrameworkProtos.SpanProto.SpanType; 5 | import com.google.errorprone.annotations.Immutable; 6 | 7 | /** 8 | * Represents a generic markup span within a {@link CharSequence}. Specific span implementations may 9 | * be found in {@link Spans} 10 | */ 11 | @Immutable 12 | public class Span { 13 | private final String spanClassName; 14 | private final int start; 15 | private final int end; 16 | private final int flags; 17 | 18 | public Span(String spanClassName, int start, int end, int flags) { 19 | this.spanClassName = spanClassName; 20 | this.start = start; 21 | this.end = end; 22 | this.flags = flags; 23 | } 24 | 25 | public Span(SpanProto proto) { 26 | this.spanClassName = proto.getSpanClassName(); 27 | this.start = proto.getStart(); 28 | this.end = proto.getEnd(); 29 | this.flags = proto.getFlags(); 30 | } 31 | 32 | public String getSpanClassName() { 33 | return spanClassName; 34 | } 35 | 36 | public int getStart() { 37 | return start; 38 | } 39 | 40 | public int getEnd() { 41 | return end; 42 | } 43 | 44 | public int getFlags() { 45 | return flags; 46 | } 47 | 48 | public SpanProto toProto() { 49 | return toProtoBuilder(SpanType.UNKNOWN).build(); 50 | } 51 | 52 | protected SpanProto.Builder toProtoBuilder(SpanType type) { 53 | return SpanProto.newBuilder() 54 | .setSpanClassName(spanClassName) 55 | .setStart(start) 56 | .setEnd(end) 57 | .setFlags(flags) 58 | .setType(type); 59 | } 60 | 61 | protected Span copyWithAdjustedPosition(int newStart, int newEnd) { 62 | return new Span(spanClassName, newStart, newEnd, flags); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/replacements/SpannableString.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework.replacements; 16 | 17 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AndroidFrameworkProtos.CharSequenceProto; 18 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AndroidFrameworkProtos.SpanProto; 19 | import com.google.common.collect.ImmutableList; 20 | import java.util.List; 21 | 22 | /** Used as a local replacement for Android's {@link android.text.SpannableString} */ 23 | public class SpannableString implements CharSequence { 24 | 25 | protected final CharSequence rawString; 26 | protected ImmutableList spans; 27 | 28 | public SpannableString(CharSequenceProto proto) { 29 | rawString = proto.getText(); 30 | ImmutableList.Builder spansBuilder = ImmutableList.builder(); 31 | for (SpanProto span : proto.getSpanList()) { 32 | Span localSpan; 33 | switch (span.getType()) { 34 | case URL: 35 | localSpan = new Spans.URLSpan(span); 36 | break; 37 | case CLICKABLE: 38 | localSpan = new Spans.ClickableSpan(span); 39 | break; 40 | case STYLE: 41 | localSpan = new Spans.StyleSpan(span); 42 | break; 43 | case UNDERLINE: 44 | localSpan = new Spans.UnderlineSpan(span); 45 | break; 46 | case BACKGROUND_COLOR: 47 | localSpan = new Spans.BackgroundColorSpan(span); 48 | break; 49 | case FOREGROUND_COLOR: 50 | localSpan = new Spans.ForegroundColorSpan(span); 51 | break; 52 | case UNKNOWN: 53 | localSpan = new Span(span); 54 | break; 55 | default: 56 | localSpan = null; 57 | } 58 | 59 | if (localSpan != null) { 60 | spansBuilder.add(localSpan); 61 | } 62 | } 63 | this.spans = spansBuilder.build(); 64 | } 65 | 66 | public SpannableString(CharSequence rawString, List spans) { 67 | this.rawString = rawString; 68 | this.spans = ImmutableList.copyOf(spans); 69 | } 70 | 71 | protected SpannableString(CharSequence rawString) { 72 | this(rawString, ImmutableList.of()); 73 | } 74 | 75 | /** 76 | * @return a {@link List} of {@link Span} objects representing markup spans annotating this {@code 77 | * CharSequence} 78 | */ 79 | public List getSpans() { 80 | return spans; 81 | } 82 | 83 | @Override 84 | public int length() { 85 | return rawString.length(); 86 | } 87 | 88 | @Override 89 | public char charAt(int index) { 90 | return rawString.charAt(index); 91 | } 92 | 93 | @Override 94 | public CharSequence subSequence(int start, int end) { 95 | return rawString.subSequence(start, end); 96 | } 97 | 98 | @Override 99 | public String toString() { 100 | return rawString.toString(); 101 | } 102 | 103 | public CharSequenceProto toProto() { 104 | CharSequenceProto.Builder builder = CharSequenceProto.newBuilder(); 105 | builder.setText(rawString.toString()); 106 | for (Span span : spans) { 107 | builder.addSpan(span.toProto()); 108 | } 109 | 110 | return builder.build(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/replacements/SpannableStringAndroid.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework.replacements; 16 | 17 | import static com.google.common.base.Preconditions.checkNotNull; 18 | 19 | import android.text.NoCopySpan; 20 | import android.text.Spanned; 21 | import android.text.style.BackgroundColorSpan; 22 | import android.text.style.ClickableSpan; 23 | import android.text.style.ForegroundColorSpan; 24 | import android.text.style.StyleSpan; 25 | import android.text.style.URLSpan; 26 | import android.text.style.UnderlineSpan; 27 | import com.google.common.collect.ImmutableList; 28 | import org.checkerframework.checker.nullness.qual.Nullable; 29 | 30 | /** Used as a local replacement for Android's {@link android.text.SpannableString} */ 31 | public class SpannableStringAndroid extends SpannableString { 32 | 33 | public SpannableStringAndroid(CharSequence text) { 34 | super(checkNotNull(text)); 35 | if (text instanceof Spanned) { 36 | // Capture span information within an Android environment 37 | this.spans = getSpansAndroid((Spanned) text); 38 | } else if (text instanceof SpannableString) { 39 | SpannableString spannableString = (SpannableString) text; 40 | this.spans = ImmutableList.copyOf(spannableString.getSpans()); 41 | } else { 42 | this.spans = ImmutableList.of(); 43 | } 44 | } 45 | 46 | /** 47 | * Convenience method for creating a {@link SpannableStringAndroid} from a {@link CharSequence}. 48 | * 49 | * @param source The {@link CharSequence} with which to create an instance 50 | * @return A {@link SpannableStringAndroid} with data matching {@code source}, or {@code null} if 51 | * {@code source} is {@code null} or empty. 52 | */ 53 | public static @Nullable SpannableStringAndroid valueOf(@Nullable CharSequence source) { 54 | if (TextUtils.isEmpty(source)) { 55 | return null; 56 | } 57 | 58 | if (source instanceof SpannableStringAndroid) { 59 | return (SpannableStringAndroid) source; 60 | } 61 | 62 | return new SpannableStringAndroid(source); 63 | } 64 | 65 | private static ImmutableList getSpansAndroid(Spanned spanned) { 66 | 67 | if (TextUtils.isEmpty(spanned)) { 68 | return ImmutableList.of(); 69 | } 70 | 71 | int length = spanned.length(); 72 | ImmutableList.Builder spansBuilder = ImmutableList.builder(); 73 | Object[] spans = spanned.getSpans(0, (length - 1), Object.class); 74 | for (int i = 0; i < spans.length; i++) { 75 | int start = spanned.getSpanStart(spans[i]); 76 | int end = spanned.getSpanEnd(spans[i]); 77 | int flags = spanned.getSpanFlags(spans[i]); 78 | 79 | Span newSpan; 80 | // Build local replacement Spans, most specific first 81 | if (spans[i] instanceof NoCopySpan) { 82 | // Don't copy NoCopySpans 83 | continue; 84 | } else if (spans[i] instanceof URLSpan) { 85 | URLSpan androidUrlSpan = (URLSpan) spans[i]; 86 | newSpan = 87 | new Spans.URLSpan( 88 | Spans.URLSpan.ANDROID_CLASS_NAME, start, end, flags, androidUrlSpan.getURL()); 89 | } else if (spans[i] instanceof ClickableSpan) { 90 | newSpan = 91 | new Spans.ClickableSpan(Spans.ClickableSpan.ANDROID_CLASS_NAME, start, end, flags); 92 | } else if (spans[i] instanceof StyleSpan) { 93 | StyleSpan androidStyleSpan = (StyleSpan) spans[i]; 94 | newSpan = 95 | new Spans.StyleSpan( 96 | Spans.StyleSpan.ANDROID_CLASS_NAME, start, end, flags, androidStyleSpan.getStyle()); 97 | } else if (spans[i] instanceof UnderlineSpan) { 98 | newSpan = 99 | new Spans.UnderlineSpan(Spans.UnderlineSpan.ANDROID_CLASS_NAME, start, end, flags); 100 | } else if (spans[i] instanceof BackgroundColorSpan) { 101 | BackgroundColorSpan androidBackgroundColorSpan = (BackgroundColorSpan) spans[i]; 102 | newSpan = 103 | new Spans.BackgroundColorSpan( 104 | start, end, flags, androidBackgroundColorSpan.getBackgroundColor()); 105 | } else if (spans[i] instanceof ForegroundColorSpan) { 106 | ForegroundColorSpan androidForegroundColorSpan = (ForegroundColorSpan) spans[i]; 107 | newSpan = 108 | new Spans.ForegroundColorSpan( 109 | start, end, flags, androidForegroundColorSpan.getForegroundColor()); 110 | } else { 111 | // Keep track of the unknown span types without creating a subclass-specific instance 112 | newSpan = new Span(spans[i].getClass().getName(), start, end, flags); 113 | } 114 | 115 | spansBuilder.add(newSpan); 116 | } 117 | return spansBuilder.build(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/replacements/SpannableStringBuilder.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.replacements; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.google.errorprone.annotations.CanIgnoreReturnValue; 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 9 | import org.checkerframework.checker.nullness.qual.Nullable; 10 | 11 | /** A fairly lightweight local replacement for {@link android.text.SpannableStringBuilder} */ 12 | public class SpannableStringBuilder implements CharSequence { 13 | 14 | private static final String SEPARATOR = ", "; 15 | 16 | private final StringBuilder rawTextBuilder = new StringBuilder(); 17 | private @MonotonicNonNull List spans; 18 | 19 | public SpannableStringBuilder() {} 20 | 21 | @CanIgnoreReturnValue 22 | public SpannableStringBuilder append(@Nullable CharSequence string) { 23 | if (!TextUtils.isEmpty(string)) { 24 | copyAndAppendAdjustedSpans(string, 0); 25 | append(string.toString()); 26 | } 27 | return this; 28 | } 29 | 30 | @CanIgnoreReturnValue 31 | public SpannableStringBuilder append(@Nullable String string) { 32 | if (!TextUtils.isEmpty(string)) { 33 | rawTextBuilder.append(string); 34 | } 35 | return this; 36 | } 37 | 38 | @CanIgnoreReturnValue 39 | public SpannableStringBuilder appendWithSeparator(@Nullable CharSequence string) { 40 | if (!TextUtils.isEmpty(string)) { 41 | copyAndAppendAdjustedSpans(string, (needsSeparator() ? SEPARATOR.length() : 0)); 42 | appendWithSeparator(string.toString()); 43 | } 44 | return this; 45 | } 46 | 47 | @CanIgnoreReturnValue 48 | public SpannableStringBuilder appendWithSeparator(@Nullable String string) { 49 | if (!TextUtils.isEmpty(string)) { 50 | if (needsSeparator()) { 51 | append(SEPARATOR); 52 | } 53 | append(string); 54 | } 55 | return this; 56 | } 57 | 58 | public List getSpans() { 59 | return (spans == null) ? ImmutableList.of() : Collections.unmodifiableList(spans); 60 | } 61 | 62 | public SpannableString build() { 63 | return new SpannableString(rawTextBuilder.toString(), getSpans()); 64 | } 65 | 66 | @Override 67 | public int length() { 68 | return rawTextBuilder.length(); 69 | } 70 | 71 | @Override 72 | public char charAt(int index) { 73 | return rawTextBuilder.charAt(index); 74 | } 75 | 76 | @Override 77 | public CharSequence subSequence(int start, int end) { 78 | return rawTextBuilder.subSequence(start, end); 79 | } 80 | 81 | /** 82 | * @return {@code true} if this instance's {@code rawTextBuilder} requires a separator to be 83 | * appended prior to appending additional text. 84 | */ 85 | private boolean needsSeparator() { 86 | return rawTextBuilder.length() > 0; 87 | } 88 | 89 | /** 90 | * Copy the spans from the provided {@code string} into the structure for tracking span data while 91 | * adjusting the span positional information to match the current state of {@code rawTextBuilder}. 92 | * Generally, this should be called prior to appending any associated raw text into {@code 93 | * rawTextBuilder}. 94 | * 95 | * @param string a string that may extend SpannableString and provide spans 96 | * @param adjustment additional value by which to adjust the positional information of the added 97 | * spans 98 | */ 99 | private void copyAndAppendAdjustedSpans(CharSequence string, int adjustment) { 100 | if (!(string instanceof SpannableString)) { 101 | return; 102 | } 103 | 104 | List spans = ((SpannableString) string).getSpans(); 105 | 106 | if (this.spans == null) { 107 | this.spans = new ArrayList<>(); 108 | } 109 | 110 | for (Span span : spans) { 111 | // Create a copy with 'start' and 'end' positions adjusted to match the span's new position. 112 | Span adjustedSpan = 113 | span.copyWithAdjustedPosition( 114 | (span.getStart() + rawTextBuilder.length() + adjustment), 115 | (span.getEnd() + rawTextBuilder.length()) + adjustment); 116 | this.spans.add(adjustedSpan); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/replacements/TextUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework.replacements; 16 | 17 | import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; 18 | import org.checkerframework.checker.nullness.qual.Nullable; 19 | 20 | /** Used as a local replacement for Android's {@link android.text.TextUtils} */ 21 | public class TextUtils { 22 | private TextUtils() { 23 | // Not instantiable 24 | } 25 | 26 | /** 27 | * @see android.text.TextUtils#isEmpty(CharSequence) 28 | */ 29 | @EnsuresNonNullIf(expression = "#1", result = false) 30 | public static boolean isEmpty(@Nullable CharSequence str) { 31 | return (str == null) || (str.length() == 0); 32 | } 33 | 34 | /** See {@link android.text.TextUtils#getTrimmedLength(CharSequence)}. */ 35 | public static int getTrimmedLength(CharSequence str) { 36 | return str.toString().trim().length(); 37 | } 38 | 39 | /** See {@link android.text.TextUtils#equals(CharSequence, CharSequence)}. */ 40 | public static boolean equals(@Nullable CharSequence s1, @Nullable CharSequence s2) { 41 | if (s1 == s2) { 42 | return true; 43 | } 44 | 45 | if ((s1 != null) && (s2 != null) && (s1.length() == s2.length())) { 46 | if (s1 instanceof String && s2 instanceof String) { 47 | return ((String) s1).equals((String) s2); 48 | } else { 49 | for (int i = 0; i < s1.length(); i++) { 50 | if (s1.charAt(i) != s2.charAt(i)) { 51 | return false; 52 | } 53 | } 54 | return true; 55 | } 56 | } 57 | return false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/replacements/Uri.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework.replacements; 16 | 17 | import com.google.errorprone.annotations.Immutable; 18 | import java.net.URI; 19 | 20 | /** Used as a local replacement for Android's {@link android.net.Uri} */ 21 | @Immutable 22 | public final class Uri { 23 | 24 | private final URI uri; 25 | 26 | public Uri(String rfc2396UriString) { 27 | uri = URI.create(rfc2396UriString); 28 | } 29 | 30 | /** 31 | * @see android.net.Uri#isAbsolute() 32 | */ 33 | public boolean isAbsolute() { 34 | return uri.isAbsolute(); 35 | } 36 | 37 | /** 38 | * @see android.net.Uri#isRelative() 39 | */ 40 | public boolean isRelative() { 41 | return !isAbsolute(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/strings/StringManager.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.strings; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import java.util.Locale; 6 | import java.util.MissingResourceException; 7 | import java.util.ResourceBundle; 8 | import org.checkerframework.checker.nullness.qual.Nullable; 9 | 10 | /** 11 | * Manager for obtaining localized strings. 12 | */ 13 | public final class StringManager { 14 | 15 | private static final String STRINGS_PACKAGE_NAME = 16 | "com.google.android.apps.common.testing.accessibility.framework"; 17 | private static final String STRINGS_FILE_NAME = "strings"; 18 | private static @Nullable ResourceBundleProvider resourceBundleProvider; 19 | 20 | private StringManager() { 21 | } 22 | 23 | /** 24 | * Override the default resource bundle used by {@link StringManager}. If no provider is set, a 25 | * default ResourceBundle will be used. 26 | * 27 | *

Example usage : If Accessibility Testing Framework is used outside the 28 | * AndroidXmlResourceBundle environment or if the ClassLoader does not recognize the xml in 29 | * specific package {@link #STRINGS_PACKAGE_NAME}, use this to override the resource provider. 30 | */ 31 | public static void setResourceBundleProvider(ResourceBundleProvider provider) { 32 | resourceBundleProvider = provider; 33 | } 34 | 35 | /** 36 | * @param locale the desired locale 37 | * @param name the name of the {@link String} to get from properties 38 | * @return a localized {@link String} corresponding to the given name and locale 39 | * @throws MissingResourceException if the string is not found 40 | */ 41 | public static String getString(Locale locale, String name) { 42 | return getResourceBundle(locale).getString(name); 43 | } 44 | 45 | private static ResourceBundle getResourceBundle(Locale locale) { 46 | if (resourceBundleProvider != null) { 47 | return resourceBundleProvider.getResourceBundle(locale); 48 | } 49 | 50 | // ResourceBundle handles necessary caching. 51 | try { 52 | // Try to load the resources from under the package directory. This is location used for 53 | // resource files in JAR files within Google. 54 | return ResourceBundle.getBundle( 55 | AndroidXMLResourceBundle.Control.getBaseName(STRINGS_PACKAGE_NAME, STRINGS_FILE_NAME), 56 | locale, 57 | checkNotNull(StringManager.class.getClassLoader()), 58 | new AndroidXMLResourceBundle.Control()); 59 | } catch (MissingResourceException e) { 60 | 61 | // If that doesn't work, try again, this time without the package name. This is the location 62 | // used for assets in an AAR file. 63 | return ResourceBundle.getBundle( 64 | AndroidXMLResourceBundle.Control.getBaseName("", STRINGS_FILE_NAME), 65 | locale, 66 | checkNotNull(StringManager.class.getClassLoader()), 67 | new AndroidXMLResourceBundle.Control()); 68 | } 69 | } 70 | 71 | /** 72 | * Provider for resource bundle. Use this to override the default resource bundle to use in {@link 73 | * StringManager}. 74 | */ 75 | public interface ResourceBundleProvider { 76 | 77 | /** Returns {@link ResourceBundle} that is suitable for the env, and the ClassLoader. */ 78 | ResourceBundle getResourceBundle(Locale locale); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/BaseFixSuggestionsProvider.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult; 4 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType; 5 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck; 6 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 7 | import com.google.android.apps.common.testing.accessibility.framework.Parameters; 8 | import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; 9 | import com.google.common.collect.ImmutableList; 10 | import java.util.Objects; 11 | import org.checkerframework.checker.nullness.qual.Nullable; 12 | 13 | /** 14 | * Base class which creates and sorts a list of {@link FixSuggestion} for a given {@link 15 | * AccessibilityHierarchyCheckResult}. 16 | * 17 | * @param the type of {@link AccessibilityHierarchyCheck}. This {@link FixSuggestionsProvider} 18 | * only provides fix suggestions to a {@link AccessibilityHierarchyCheckResult} generated by 19 | * this specific type of {@link AccessibilityHierarchyCheck}. 20 | */ 21 | abstract class BaseFixSuggestionsProvider 22 | implements FixSuggestionsProvider { 23 | 24 | @Override 25 | public final ImmutableList provideFixSuggestions( 26 | AccessibilityHierarchyCheckResult checkResult, 27 | AccessibilityHierarchy hierarchy, 28 | @Nullable Parameters parameters) { 29 | if ((checkResult.getType() == AccessibilityCheckResultType.NOT_RUN) 30 | || (checkResult.getType() == AccessibilityCheckResultType.SUPPRESSED)) { 31 | return ImmutableList.of(); 32 | } 33 | 34 | // Only provides fix suggestions to a {@link AccessibilityHierarchyCheckResult} generated by a 35 | // specific type of {@link AccessibilityHierarchyCheck} 36 | if (!Objects.equals(checkResult.getSourceCheckClass(), getTargetCheckClass())) { 37 | return ImmutableList.of(); 38 | } 39 | 40 | ImmutableList.Builder fixSuggestionBuilder = new ImmutableList.Builder<>(); 41 | for (FixSuggestionProducer suggestionProducer : 42 | getFixSuggestionProducers()) { 43 | FixSuggestion suggestion = 44 | suggestionProducer.produceFixSuggestion(checkResult, hierarchy, parameters); 45 | if (suggestion != null) { 46 | fixSuggestionBuilder.add(suggestion); 47 | } 48 | } 49 | return sortFixSuggestions(fixSuggestionBuilder.build(), checkResult, hierarchy, parameters); 50 | } 51 | 52 | /** Returns an immutable list of {@link FixSuggestionProducer} which produce fix suggestions. */ 53 | protected abstract ImmutableList> 54 | getFixSuggestionProducers(); 55 | 56 | /** 57 | * Sorts the given list of {@link FixSuggestion} according to which fix suggestion works better 58 | * for the given {@link AccessibilityCheckResult}. 59 | * 60 | *

Returns the given fix suggestions without sorting by default. Override this method to 61 | * provide dynamic sorting for the given fix suggestions. 62 | * 63 | * @param checkResult the {@link AccessibilityHierarchyCheckResult} which needs fix suggestions 64 | * @param hierarchy The hierarchy which contains the culprit {@link ViewHierarchyElement} 65 | * @param parameters Optional input data or preferences 66 | * @return suggested fixes (possibly none) sorted so that the better suggestions appear first 67 | */ 68 | protected ImmutableList sortFixSuggestions( 69 | ImmutableList fixSuggestions, 70 | AccessibilityHierarchyCheckResult checkResult, 71 | AccessibilityHierarchy hierarchy, 72 | @Nullable Parameters parameters) { 73 | return fixSuggestions; 74 | } 75 | 76 | /** 77 | * Returns the {@link Class} of the specific type of {@link AccessibilityHierarchyCheck}. 78 | * 79 | *

This {@link FixSuggestionsProvider} only provides fix suggestions to a {@link 80 | * AccessibilityHierarchyCheckResult} generated by the specific type of {@link 81 | * AccessibilityHierarchyCheck}. 82 | */ 83 | protected abstract Class getTargetCheckClass(); 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/BaseTextContrastFixSuggestionProducer.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 4 | import com.google.android.apps.common.testing.accessibility.framework.Parameters; 5 | import com.google.android.apps.common.testing.accessibility.framework.ResultMetadata; 6 | import com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck; 7 | import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; 8 | import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElement; 9 | import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.ContrastUtils; 10 | import org.checkerframework.checker.nullness.qual.Nullable; 11 | 12 | /** 13 | * Base class which produces a {@link SetViewAttributeFixSuggestion} to fix a text low contrast 14 | * issue. 15 | */ 16 | abstract class BaseTextContrastFixSuggestionProducer 17 | implements FixSuggestionProducer { 18 | 19 | @Override 20 | public @Nullable SetViewAttributeFixSuggestion produceFixSuggestion( 21 | AccessibilityHierarchyCheckResult checkResult, 22 | AccessibilityHierarchy hierarchy, 23 | @Nullable Parameters parameters) { 24 | ResultMetadata metadata = checkResult.getMetadata(); 25 | if (metadata == null) { 26 | return null; 27 | } 28 | 29 | ViewHierarchyElement element = checkResult.getElement(); 30 | if (element == null) { 31 | return null; 32 | } 33 | 34 | if (metadata.getBoolean( 35 | TextContrastCheck.KEY_IS_POTENTIALLY_OBSCURED, /* defaultValue= */ false)) { 36 | // Do not produce a fix suggestion if the culprit view may be obscured by other on-screen 37 | // content. 38 | return null; 39 | } 40 | 41 | return produceTextContrastFixSuggestion(checkResult.getResultId(), element, metadata); 42 | } 43 | 44 | /** 45 | * Produces a {@link SetViewAttributeFixSuggestion} to fix a text low contrast issue. Returns 46 | * {@code null} if no similar colors meet the text contrast ratio requirement. 47 | * 48 | * @param checkResultId the Integer value of the result 49 | * @param element the {@link ViewHierarchyElement} which has the text color low contrast issue 50 | * @param metadata the metadata stored in this result 51 | */ 52 | protected abstract @Nullable SetViewAttributeFixSuggestion produceTextContrastFixSuggestion( 53 | int checkResultId, ViewHierarchyElement element, ResultMetadata metadata); 54 | 55 | static SetViewAttributeFixSuggestion createSetViewAttributeFixSuggestion( 56 | String viewAttributeName, int suggestedColor) { 57 | String suggestedColorValue = ContrastUtils.colorToHexString(suggestedColor); 58 | return new SetViewAttributeFixSuggestion(viewAttributeName, suggestedColorValue); 59 | } 60 | 61 | static int getTextColor(ResultMetadata metadata) { 62 | if (metadata.containsKey(TextContrastCheck.KEY_TEXT_COLOR)) { 63 | return metadata.getInt(TextContrastCheck.KEY_TEXT_COLOR); 64 | } else { 65 | return metadata.getInt(TextContrastCheck.KEY_FOREGROUND_COLOR); 66 | } 67 | } 68 | 69 | static int getBackgroundColor(ResultMetadata metadata) { 70 | return metadata.getInt(TextContrastCheck.KEY_BACKGROUND_COLOR); 71 | } 72 | 73 | static double getRequiredContrastRatio(ResultMetadata metadata) { 74 | if (metadata.containsKey(TextContrastCheck.KEY_CUSTOMIZED_HEURISTIC_CONTRAST_RATIO)) { 75 | return metadata.getDouble(TextContrastCheck.KEY_CUSTOMIZED_HEURISTIC_CONTRAST_RATIO); 76 | } else { 77 | return metadata.getDouble(TextContrastCheck.KEY_REQUIRED_CONTRAST_RATIO); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/CompoundFixSuggestions.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | import static com.google.common.base.StandardSystemProperty.LINE_SEPARATOR; 5 | 6 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 7 | import com.google.common.annotations.Beta; 8 | import com.google.common.collect.ImmutableList; 9 | import java.util.Locale; 10 | 11 | /** 12 | * A {@link FixSuggestion} which recommends applying multiple {@link FixSuggestion} together to fix 13 | * an {@link AccessibilityHierarchyCheckResult}. 14 | * 15 | *

A {@link CompoundFixSuggestions} must contain at least 2 {@link FixSuggestion}. 16 | */ 17 | @Beta 18 | public class CompoundFixSuggestions implements FixSuggestion { 19 | 20 | private final ImmutableList fixSuggestions; 21 | 22 | /** 23 | * Creates a {@link CompoundFixSuggestions} from an immutable list of {@link FixSuggestion}. 24 | * 25 | * @param fixSuggestions an immutable list of {@link FixSuggestion} suggested to be applied 26 | * together 27 | * @throws IllegalArgumentException if the given fix suggestion list contains less than 2 fix 28 | * suggestions 29 | */ 30 | public CompoundFixSuggestions(ImmutableList fixSuggestions) { 31 | checkArgument( 32 | fixSuggestions.size() > 1, 33 | "The fix suggestion list must contain at least 2 fix suggestions"); 34 | this.fixSuggestions = fixSuggestions; 35 | } 36 | 37 | /** Returns an immutable list of {@link FixSuggestion} suggested to be applied together. */ 38 | public ImmutableList getFixSuggestions() { 39 | return fixSuggestions; 40 | } 41 | 42 | @Override 43 | public CharSequence getRawDescription(Locale locale) { 44 | String description = ""; 45 | for (int i = 0; i < fixSuggestions.size(); i++) { 46 | description += fixSuggestions.get(i).getRawDescription(locale); 47 | if (i < fixSuggestions.size() - 1) { 48 | description += LINE_SEPARATOR.value(); 49 | } 50 | } 51 | return description; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/EditableContentDescFixSuggestionsProvider.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 4 | import com.google.android.apps.common.testing.accessibility.framework.checks.EditableContentDescCheck; 5 | import com.google.common.collect.ImmutableList; 6 | 7 | /** 8 | * A {@link FixSuggestionsProvider} which provides suggestions to fix a {@link 9 | * AccessibilityHierarchyCheckResult} generated by an {@link EditableContentDescCheck}, 10 | * 11 | *

If the culprit TextView is labeled by a contentDescription, provides an immutable list with 12 | * one suggestion that recommends removing the {@code contentDescription} view attribute. Otherwise 13 | * no fix suggestions will be provided. 14 | */ 15 | class EditableContentDescFixSuggestionsProvider 16 | extends BaseFixSuggestionsProvider { 17 | 18 | private static final ImmutableList> 19 | FIX_SUGGESTION_PRODUCERS = ImmutableList.of(new EditableContentDescProducer()); 20 | 21 | @Override 22 | protected ImmutableList> 23 | getFixSuggestionProducers() { 24 | return FIX_SUGGESTION_PRODUCERS; 25 | } 26 | 27 | @Override 28 | protected Class getTargetCheckClass() { 29 | return EditableContentDescCheck.class; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/EditableContentDescProducer.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 4 | import com.google.android.apps.common.testing.accessibility.framework.Parameters; 5 | import com.google.android.apps.common.testing.accessibility.framework.checks.EditableContentDescCheck; 6 | import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; 7 | import org.checkerframework.checker.nullness.qual.Nullable; 8 | 9 | /** 10 | * A {@link FixSuggestionProducer} which recommends removing the {{@code contentDescription}} view 11 | * attribute if the culprit TextView is labeled by a contentDescription. 12 | */ 13 | class EditableContentDescProducer 14 | implements FixSuggestionProducer { 15 | 16 | private static final String VIEW_PROPERTY_CONTENT_DESCRIPTION = "contentDescription"; 17 | 18 | @Override 19 | public @Nullable RemoveViewAttributeFixSuggestion produceFixSuggestion( 20 | AccessibilityHierarchyCheckResult checkResult, 21 | AccessibilityHierarchy hierarchy, 22 | @Nullable Parameters parameters) { 23 | if (checkResult.getResultId() 24 | == EditableContentDescCheck.RESULT_ID_EDITABLE_TEXTVIEW_CONTENT_DESC) { 25 | // Suggests removing the contentDescription attribute of the editable TextView 26 | return new RemoveViewAttributeFixSuggestion(VIEW_PROPERTY_CONTENT_DESCRIPTION); 27 | } 28 | return null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/FixSuggestion.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 4 | import com.google.common.annotations.Beta; 5 | import java.util.Locale; 6 | import org.jsoup.Jsoup; 7 | 8 | /** A suggested fix for a {@link AccessibilityHierarchyCheckResult}. */ 9 | @Beta 10 | public interface FixSuggestion { 11 | 12 | /** 13 | * Returns a human-readable description for this fix suggestion which may contain formatting 14 | * markup. 15 | * 16 | * @param locale desired locale for the description 17 | */ 18 | CharSequence getRawDescription(Locale locale); 19 | 20 | /** 21 | * Returns a human-readable description for this fix suggestion without formatting markup. 22 | * 23 | * @param locale desired locale for the description 24 | */ 25 | default String getDescription(Locale locale) { 26 | return Jsoup.parse(getRawDescription(locale).toString()).text(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/FixSuggestionPreset.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck; 4 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 5 | import com.google.android.apps.common.testing.accessibility.framework.Parameters; 6 | import com.google.android.apps.common.testing.accessibility.framework.checks.EditableContentDescCheck; 7 | import com.google.android.apps.common.testing.accessibility.framework.checks.RedundantDescriptionCheck; 8 | import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck; 9 | import com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck; 10 | import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck; 11 | import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; 12 | import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElement; 13 | import com.google.common.annotations.Beta; 14 | import com.google.common.collect.ImmutableList; 15 | import com.google.common.collect.ImmutableMap; 16 | import org.checkerframework.checker.nullness.qual.Nullable; 17 | 18 | /** 19 | * Provides an immutable list of {@link FixSuggestion} for a {@link 20 | * AccessibilityHierarchyCheckResult}. 21 | */ 22 | @Beta 23 | public final class FixSuggestionPreset { 24 | 25 | private static final ImmutableMap< 26 | Class, FixSuggestionsProvider> 27 | CHECK_CLASS_TO_FIX_SUGGESTION_PROVIDER = 28 | ImmutableMap.of( 29 | EditableContentDescCheck.class, 30 | new EditableContentDescFixSuggestionsProvider(), 31 | TouchTargetSizeCheck.class, 32 | new TouchTargetSizeFixSuggestionsProvider(), 33 | TextContrastCheck.class, 34 | new TextContrastFixSuggestionsProvider(), 35 | SpeakableTextPresentCheck.class, 36 | new SpeakableTextPresentFixSuggestionsProvider(), 37 | RedundantDescriptionCheck.class, 38 | new RedundantDescriptionFixSuggestionsProvider()); 39 | 40 | private FixSuggestionPreset() { 41 | // Not instantiable 42 | } 43 | 44 | /** 45 | * Provides an immutable list of {@link FixSuggestion} for the given {@link 46 | * AccessibilityHierarchyCheckResult}. 47 | * 48 | *

The list of {@link FixSuggestion} is sorted according to which {@link FixSuggestion} is 49 | * considered to be a better or more applicable fix. 50 | * 51 | * @param checkResult the {@link AccessibilityHierarchyCheckResult} which needs fix suggestions 52 | * @param hierarchy The hierarchy which contains the culprit {@link ViewHierarchyElement} 53 | * @param parameters Optional input data or preferences 54 | * @return suggested fixes (possibly none) sorted so that the better suggestions appear first 55 | */ 56 | public static ImmutableList provideFixSuggestions( 57 | AccessibilityHierarchyCheckResult checkResult, 58 | AccessibilityHierarchy hierarchy, 59 | @Nullable Parameters parameters) { 60 | FixSuggestionsProvider fixSuggestionsProvider = 61 | CHECK_CLASS_TO_FIX_SUGGESTION_PROVIDER.get(checkResult.getSourceCheckClass()); 62 | return (fixSuggestionsProvider == null) 63 | ? ImmutableList.of() 64 | : fixSuggestionsProvider.provideFixSuggestions(checkResult, hierarchy, parameters); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/FixSuggestionProducer.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 4 | import com.google.android.apps.common.testing.accessibility.framework.Parameters; 5 | import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; 6 | import org.checkerframework.checker.nullness.qual.Nullable; 7 | 8 | /** 9 | * Responsible for generating a special type of {@link FixSuggestion} for a given {@link 10 | * AccessibilityHierarchyCheckResult}. 11 | * 12 | * @param the type of {@link FixSuggestion} to be generated 13 | */ 14 | interface FixSuggestionProducer { 15 | 16 | /** 17 | * Produces a {@link FixSuggestion} for the given {@link AccessibilityHierarchyCheckResult}. 18 | * 19 | * @param checkResult the {@link AccessibilityHierarchyCheckResult} which needs fix suggestions 20 | * @param hierarchy the hierarchy which contains the culprit 21 | * @param parameters Optional input data or preferences 22 | * @return the generated {@link FixSuggestion} or {@code null} if there is no feasible fix 23 | * suggestion 24 | */ 25 | @Nullable 26 | T produceFixSuggestion( 27 | AccessibilityHierarchyCheckResult checkResult, 28 | AccessibilityHierarchy hierarchy, 29 | @Nullable Parameters parameters); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/FixSuggestionsProvider.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 4 | import com.google.android.apps.common.testing.accessibility.framework.Parameters; 5 | import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; 6 | import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElement; 7 | import com.google.common.collect.ImmutableList; 8 | import org.checkerframework.checker.nullness.qual.Nullable; 9 | 10 | /** 11 | * Responsible for generating and sorting an immutable list of {@link FixSuggestion} for the given 12 | * {@link AccessibilityHierarchyCheckResult}. 13 | */ 14 | public interface FixSuggestionsProvider { 15 | 16 | /** 17 | * Provides an immutable list of {@link FixSuggestion} for the given {@link 18 | * AccessibilityHierarchyCheckResult}. 19 | * 20 | *

The list of {@link FixSuggestion} is sorted according to which {@link FixSuggestion} is 21 | * considered to be a better or more applicable fix. 22 | * 23 | * @param checkResult the {@link AccessibilityHierarchyCheckResult} which needs fix suggestions 24 | * @param hierarchy The hierarchy which contains the culprit {@link ViewHierarchyElement} 25 | * @param parameters Optional input data or preferences 26 | * @return suggested fixes (possibly none) sorted so that the better suggestions appear first 27 | */ 28 | ImmutableList provideFixSuggestions( 29 | AccessibilityHierarchyCheckResult checkResult, 30 | AccessibilityHierarchy hierarchy, 31 | @Nullable Parameters parameters); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/RedundantDescriptionFixSuggestionProducer.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 6 | import com.google.android.apps.common.testing.accessibility.framework.Parameters; 7 | import com.google.android.apps.common.testing.accessibility.framework.ResultMetadata; 8 | import com.google.android.apps.common.testing.accessibility.framework.checks.RedundantDescriptionCheck; 9 | import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; 10 | import org.checkerframework.checker.nullness.qual.Nullable; 11 | 12 | /** 13 | * A {@link FixSuggestionsProvider} which recommends setting the {@code contentDescription} view 14 | * attribute to a suggested value if the culprit view's speakable text contains redundant 15 | * information about the view's type. 16 | */ 17 | class RedundantDescriptionFixSuggestionProducer 18 | implements FixSuggestionProducer { 19 | 20 | private static final ViewAttribute VIEW_ATTRIBUTE_CONTENT_DESCRIPTION = 21 | new ViewAttribute("contentDescription"); 22 | 23 | @Override 24 | public @Nullable SetViewAttributeFixSuggestion produceFixSuggestion( 25 | AccessibilityHierarchyCheckResult checkResult, 26 | AccessibilityHierarchy hierarchy, 27 | @Nullable Parameters parameters) { 28 | if (checkResult.getResultId() 29 | != RedundantDescriptionCheck.RESULT_ID_CONTENT_DESC_CONTAINS_ITEM_TYPE) { 30 | return null; 31 | } 32 | 33 | ResultMetadata resultMetadata = checkNotNull(checkResult.getMetadata()); 34 | String contentDescription = 35 | resultMetadata.getString(RedundantDescriptionCheck.KEY_CONTENT_DESCRIPTION); 36 | String redundantWord = resultMetadata.getString(RedundantDescriptionCheck.KEY_REDUNDANT_WORD); 37 | 38 | // Replaces every occurrence of the redundant word with a space (ignoring case), and then 39 | // collapses repeated spaces and removes leading and trailing spaces. 40 | // For example, changes "foo button bar" to "foo bar". 41 | // If the redundant word is part of another word, do not replace it. 42 | // For example, changes "Order Buttonhole button" to "Order Buttonhole" 43 | String suggestedContentDescription = 44 | contentDescription 45 | .replaceAll("\\b(?i)" + redundantWord + "\\b", " ") 46 | .replaceAll(" +", " ") 47 | .trim(); 48 | if (suggestedContentDescription.length() == contentDescription.length()) { 49 | // For example, no fix suggestions will be produced for "Order Buttonhole" 50 | return null; 51 | } 52 | 53 | return new SetViewAttributeFixSuggestion( 54 | VIEW_ATTRIBUTE_CONTENT_DESCRIPTION, suggestedContentDescription); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/RedundantDescriptionFixSuggestionsProvider.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 4 | import com.google.android.apps.common.testing.accessibility.framework.checks.RedundantDescriptionCheck; 5 | import com.google.common.collect.ImmutableList; 6 | 7 | /** 8 | * A {@link FixSuggestionsProvider} which provides fix suggestions to a {@link 9 | * AccessibilityHierarchyCheckResult} generated by {@link RedundantDescriptionCheck}. 10 | */ 11 | class RedundantDescriptionFixSuggestionsProvider 12 | extends BaseFixSuggestionsProvider { 13 | 14 | private static final ImmutableList> 15 | FIX_SUGGESTION_PRODUCERS = ImmutableList.of(new RedundantDescriptionFixSuggestionProducer()); 16 | 17 | @Override 18 | protected ImmutableList> 19 | getFixSuggestionProducers() { 20 | return FIX_SUGGESTION_PRODUCERS; 21 | } 22 | 23 | @Override 24 | protected Class getTargetCheckClass() { 25 | return RedundantDescriptionCheck.class; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/RemoveViewAttributeFixSuggestion.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 4 | import com.google.android.apps.common.testing.accessibility.framework.strings.StringManager; 5 | import com.google.common.annotations.Beta; 6 | import java.util.Locale; 7 | 8 | /** 9 | * A {@link FixSuggestion} which suggests removing a view attribute to fix a {@link 10 | * AccessibilityHierarchyCheckResult} 11 | */ 12 | @Beta 13 | public class RemoveViewAttributeFixSuggestion implements FixSuggestion { 14 | 15 | private final ViewAttribute viewAttribute; 16 | 17 | public RemoveViewAttributeFixSuggestion(ViewAttribute viewAttribute) { 18 | this.viewAttribute = viewAttribute; 19 | } 20 | 21 | public RemoveViewAttributeFixSuggestion(String viewAttribute) { 22 | this(new ViewAttribute(viewAttribute)); 23 | } 24 | 25 | /** Returns the {@link ViewAttribute} suggested to be removed. */ 26 | public ViewAttribute getViewAttribute() { 27 | return viewAttribute; 28 | } 29 | 30 | @Override 31 | public CharSequence getRawDescription(Locale locale) { 32 | return String.format( 33 | locale, 34 | StringManager.getString(locale, "suggestion_remove_view_attribute"), 35 | viewAttribute.getFullyQualifiedName()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/SetViewAttributeFixSuggestion.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 4 | import com.google.android.apps.common.testing.accessibility.framework.strings.StringManager; 5 | import com.google.common.annotations.Beta; 6 | import java.util.Locale; 7 | 8 | /** 9 | * A {@link FixSuggestion} which suggests setting a value to a view attribute to fix a {@link 10 | * AccessibilityHierarchyCheckResult}. 11 | * 12 | *

    13 | *
  • If the view attribute has not been set before, add the view attribute and set its value to 14 | * the suggested value. 15 | *
  • If the view attribute has been set before, replace its value with the suggested value. 16 | *
  • If the suggested value is an empty string, ask the developer to set the view attribute to a 17 | * meaningful non-empty string or resource reference. DO NOT set the view attribute to an 18 | * empty string. 19 | *
20 | */ 21 | @Beta 22 | public class SetViewAttributeFixSuggestion implements FixSuggestion { 23 | 24 | private final ViewAttribute viewAttribute; 25 | 26 | private final String suggestedValue; 27 | 28 | public SetViewAttributeFixSuggestion(ViewAttribute viewAttribute, String suggestedValue) { 29 | this.viewAttribute = viewAttribute; 30 | this.suggestedValue = suggestedValue; 31 | } 32 | 33 | public SetViewAttributeFixSuggestion(String attributeName, String suggestedValue) { 34 | this(new ViewAttribute(attributeName), suggestedValue); 35 | } 36 | 37 | /** Returns the {@link ViewAttribute} suggested to be changed. */ 38 | public ViewAttribute getViewAttribute() { 39 | return viewAttribute; 40 | } 41 | 42 | /** Returns the suggested value for the view attribute. */ 43 | public String getSuggestedValue() { 44 | return suggestedValue; 45 | } 46 | 47 | @Override 48 | public CharSequence getRawDescription(Locale locale) { 49 | if (suggestedValue.isEmpty()) { 50 | return String.format( 51 | locale, 52 | StringManager.getString(locale, "suggestion_set_view_attribute_with_an_non_empty_string"), 53 | viewAttribute.getFullyQualifiedName()); 54 | } else { 55 | return String.format( 56 | locale, 57 | StringManager.getString(locale, "suggestion_set_view_attribute"), 58 | viewAttribute.getFullyQualifiedName(), 59 | suggestedValue); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/SpeakableTextPresentFixSuggestionProducer.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 4 | import com.google.android.apps.common.testing.accessibility.framework.Parameters; 5 | import com.google.android.apps.common.testing.accessibility.framework.ViewHierarchyElementUtils; 6 | import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck; 7 | import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchy; 8 | import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElement; 9 | import java.util.Objects; 10 | import org.checkerframework.checker.nullness.qual.Nullable; 11 | 12 | /** 13 | * A {@link FixSuggestionProducer} which produces a {@link SetViewAttributeFixSuggestion} if the 14 | * culprit view is missing speakable text. The {@link SetViewAttributeFixSuggestion} recommends 15 | * setting the {@code contentDescription} or {@code hint} or {@code text} attribute to the culprit 16 | * View according to the view's type. 17 | */ 18 | class SpeakableTextPresentFixSuggestionProducer 19 | implements FixSuggestionProducer { 20 | 21 | private static final String VIEW_ATTRIBUTE_CONTENT_DESCRIPTION = "contentDescription"; 22 | 23 | private static final String VIEW_ATTRIBUTE_HINT = "hint"; 24 | 25 | private static final String VIEW_ATTRIBUTE_TEXT = "text"; 26 | 27 | @Override 28 | public @Nullable SetViewAttributeFixSuggestion produceFixSuggestion( 29 | AccessibilityHierarchyCheckResult checkResult, 30 | AccessibilityHierarchy hierarchy, 31 | @Nullable Parameters parameters) { 32 | ViewHierarchyElement viewHierarchyElement = checkResult.getElement(); 33 | if (viewHierarchyElement == null) { 34 | return null; 35 | } 36 | 37 | if (checkResult.getResultId() == SpeakableTextPresentCheck.RESULT_ID_MISSING_SPEAKABLE_TEXT) { 38 | // If we set contentDescription on an editable TextView or an EditText, it creates a 39 | // redundant description issue. 40 | boolean shouldSetHint = 41 | (viewHierarchyElement.checkInstanceOf(ViewHierarchyElementUtils.EDIT_TEXT_CLASS_NAME) 42 | || (viewHierarchyElement.checkInstanceOf( 43 | ViewHierarchyElementUtils.TEXT_VIEW_CLASS_NAME) 44 | && Boolean.TRUE.equals(viewHierarchyElement.isEditable()))); 45 | CharSequence className = viewHierarchyElement.getClassName(); 46 | if (shouldSetHint) { 47 | return new SetViewAttributeFixSuggestion(VIEW_ATTRIBUTE_HINT, /* suggestedValue= */ ""); 48 | } else if ((className != null) 49 | && Objects.equals(className.toString(), ViewHierarchyElementUtils.TEXT_VIEW_CLASS_NAME)) { 50 | // If the culprit view's class name is "android.widget.TextView" and not editable, suggest 51 | // setting "android:text" view attribute. We don't want to include subclasses, because 52 | // different subclasses handle android:text differently. 53 | return new SetViewAttributeFixSuggestion(VIEW_ATTRIBUTE_TEXT, /* suggestedValue= */ ""); 54 | } else { 55 | return new SetViewAttributeFixSuggestion( 56 | VIEW_ATTRIBUTE_CONTENT_DESCRIPTION, /* suggestedValue= */ ""); 57 | } 58 | } 59 | 60 | return null; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/SpeakableTextPresentFixSuggestionsProvider.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 4 | import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck; 5 | import com.google.common.collect.ImmutableList; 6 | 7 | /** 8 | * A {@link FixSuggestionsProvider} which provides suggestions to fix a {@link 9 | * AccessibilityHierarchyCheckResult} generated by an {@link SpeakableTextPresentCheck}, 10 | * 11 | *

If the culprit View is missing speakable text, provides an immutable list with a {@link 12 | * SetViewAttributeFixSuggestion} which recommends setting the contentDescription view attribute. 13 | * Otherwise no fix suggestions will be provided. 14 | */ 15 | class SpeakableTextPresentFixSuggestionsProvider 16 | extends BaseFixSuggestionsProvider { 17 | 18 | private static final ImmutableList> 19 | FIX_SUGGESTION_PRODUCERS = ImmutableList.of(new SpeakableTextPresentFixSuggestionProducer()); 20 | 21 | @Override 22 | protected ImmutableList> 23 | getFixSuggestionProducers() { 24 | return FIX_SUGGESTION_PRODUCERS; 25 | } 26 | 27 | @Override 28 | protected Class getTargetCheckClass() { 29 | return SpeakableTextPresentCheck.class; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/TextBackgroundColorFixSuggestionProducer.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.ResultMetadata; 4 | import com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck; 5 | import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElement; 6 | import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.ContrastUtils; 7 | import org.checkerframework.checker.nullness.qual.Nullable; 8 | 9 | /** 10 | * A {@link FixSuggestionProducer} which produces a {@link SetViewAttributeFixSuggestion} to fix a 11 | * text low contrast issue. 12 | * 13 | *

Checks all material colors to find the best color which is the closest color in terms of color 14 | * distance to the original background color and meets the contrast ratio requirement. If the best 15 | * color could be found, produces a {@link SetViewAttributeFixSuggestion} which recommends changing 16 | * background to a suggested value to fix a text low contrast issue. Otherwise no fix suggestions 17 | * will be produced. 18 | */ 19 | class TextBackgroundColorFixSuggestionProducer extends BaseTextContrastFixSuggestionProducer { 20 | 21 | private static final String VIEW_ATTRIBUTE_BACKGROUND = "background"; 22 | 23 | @Override 24 | protected @Nullable SetViewAttributeFixSuggestion produceTextContrastFixSuggestion( 25 | int checkResultId, ViewHierarchyElement element, ResultMetadata metadata) { 26 | switch (checkResultId) { 27 | case TextContrastCheck.RESULT_ID_TEXTVIEW_CONTRAST_NOT_SUFFICIENT: 28 | return produceBackgroundColorFixSuggestion(metadata); 29 | default: 30 | // For other check result id, no fix suggestions will be produced because it may require 31 | // to set background color to the culprit view's ancestor view. 32 | return null; 33 | } 34 | } 35 | 36 | private static @Nullable SetViewAttributeFixSuggestion produceBackgroundColorFixSuggestion( 37 | ResultMetadata resultMetadata) { 38 | Integer bestColorCandidate = 39 | findBestBackgroundColorCandidate( 40 | getTextColor(resultMetadata), 41 | getBackgroundColor(resultMetadata), 42 | getRequiredContrastRatio(resultMetadata)); 43 | return (bestColorCandidate == null) 44 | ? null 45 | : createSetViewAttributeFixSuggestion(VIEW_ATTRIBUTE_BACKGROUND, bestColorCandidate); 46 | } 47 | 48 | private static @Nullable Integer findBestBackgroundColorCandidate( 49 | int textColor, int backgroundColor, double requiredContrastRatio) { 50 | Integer bestColorCandidate = null; 51 | double minColorDistance = Double.MAX_VALUE; 52 | for (MaterialDesignColor designColor : MaterialDesignColor.values()) { 53 | for (int testColor : designColor.getColorMap().values()) { 54 | if (ContrastUtils.calculateContrastRatio(textColor, testColor) < requiredContrastRatio) { 55 | continue; 56 | } 57 | 58 | double colorDistance = ContrastUtils.colorDifference(testColor, backgroundColor); 59 | if (minColorDistance > colorDistance) { 60 | minColorDistance = colorDistance; 61 | bestColorCandidate = testColor; 62 | } 63 | } 64 | } 65 | return bestColorCandidate; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/TextColorFixSuggestionProducer.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.ResultMetadata; 4 | import com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck; 5 | import com.google.android.apps.common.testing.accessibility.framework.replacements.TextUtils; 6 | import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElement; 7 | import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.ContrastUtils; 8 | import org.checkerframework.checker.nullness.qual.Nullable; 9 | 10 | /** 11 | * A {@link FixSuggestionProducer} which produces a {@link SetViewAttributeFixSuggestion} to fix a 12 | * text low contrast issue. 13 | * 14 | *

Checks all material colors to find the best color which is the closest color in terms of color 15 | * distance to the original text color and meets the contrast ratio requirement. If the best color 16 | * could be found, produces a {@link SetViewAttributeFixSuggestion} which recommends changing text 17 | * color or hint color to fix a text low contrast issue. Otherwise no fix suggestions will be 18 | * produced. 19 | */ 20 | class TextColorFixSuggestionProducer extends BaseTextContrastFixSuggestionProducer { 21 | 22 | private static final String VIEW_ATTRIBUTE_TEXT_COLOR = "textColor"; 23 | private static final String VIEW_ATTRIBUTE_TEXT_COLOR_HINT = "textColorHint"; 24 | 25 | @Override 26 | protected @Nullable SetViewAttributeFixSuggestion produceTextContrastFixSuggestion( 27 | int checkResultId, ViewHierarchyElement element, ResultMetadata metadata) { 28 | switch (checkResultId) { 29 | case TextContrastCheck.RESULT_ID_TEXTVIEW_CONTRAST_NOT_SUFFICIENT: 30 | case TextContrastCheck.RESULT_ID_TEXTVIEW_HEURISTIC_CONTRAST_NOT_SUFFICIENT: 31 | case TextContrastCheck.RESULT_ID_CUSTOMIZED_TEXTVIEW_HEURISTIC_CONTRAST_NOT_SUFFICIENT: 32 | case TextContrastCheck.RESULT_ID_TEXTVIEW_HEURISTIC_CONTRAST_BORDERLINE: 33 | return produceTextColorFixSuggestion(element, metadata); 34 | default: 35 | return null; 36 | } 37 | } 38 | 39 | private static @Nullable SetViewAttributeFixSuggestion produceTextColorFixSuggestion( 40 | ViewHierarchyElement element, ResultMetadata resultMetadata) { 41 | Integer bestColorCandidate = 42 | findBestTextColorCandidate( 43 | getTextColor(resultMetadata), 44 | getBackgroundColor(resultMetadata), 45 | getRequiredContrastRatio(resultMetadata)); 46 | 47 | // If the element's text is empty and a low color contrast ratio issue has been detected, then 48 | // the element's hint is being displayed with the current hint text color. So we should suggest 49 | // changing the hint text color instead of the text color in this case. 50 | String viewAttribute = 51 | TextUtils.isEmpty(element.getText()) 52 | ? VIEW_ATTRIBUTE_TEXT_COLOR_HINT 53 | : VIEW_ATTRIBUTE_TEXT_COLOR; 54 | return (bestColorCandidate == null) 55 | ? null 56 | : createSetViewAttributeFixSuggestion(viewAttribute, bestColorCandidate); 57 | } 58 | 59 | private static @Nullable Integer findBestTextColorCandidate( 60 | int textColor, int backgroundColor, double requiredContrastRatio) { 61 | // Tries to find a color in all material colors which meets contrast ratio requirement and is 62 | // the closest color to the culprit View's text color. 63 | double minColorDistance = Double.MAX_VALUE; 64 | Integer bestColorCandidate = null; 65 | for (MaterialDesignColor designColor : MaterialDesignColor.values()) { 66 | for (int testColor : designColor.getColorMap().values()) { 67 | if (ContrastUtils.calculateContrastRatio(testColor, backgroundColor) 68 | < requiredContrastRatio) { 69 | continue; 70 | } 71 | 72 | double colorDistance = ContrastUtils.colorDifference(testColor, textColor); 73 | if (minColorDistance > colorDistance) { 74 | minColorDistance = colorDistance; 75 | bestColorCandidate = testColor; 76 | } 77 | } 78 | } 79 | return bestColorCandidate; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/TextContrastFixSuggestionsProvider.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 4 | import com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck; 5 | import com.google.common.collect.ImmutableList; 6 | 7 | /** 8 | * A {@link FixSuggestionsProvider} which provides fix suggestions to an {@link 9 | * AccessibilityHierarchyCheckResult} generated by a {@link TextContrastCheck}. 10 | */ 11 | class TextContrastFixSuggestionsProvider extends BaseFixSuggestionsProvider { 12 | 13 | private static final ImmutableList> 14 | FIX_SUGGESTION_PRODUCERS = 15 | ImmutableList.of( 16 | new TextColorFixSuggestionProducer(), new TextBackgroundColorFixSuggestionProducer()); 17 | 18 | @Override 19 | protected ImmutableList> 20 | getFixSuggestionProducers() { 21 | return FIX_SUGGESTION_PRODUCERS; 22 | } 23 | 24 | @Override 25 | protected Class getTargetCheckClass() { 26 | return TextContrastCheck.class; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/TouchTargetSizeFixSuggestionsProvider.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 4 | import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck; 5 | import com.google.common.collect.ImmutableList; 6 | 7 | /** 8 | * A {@link FixSuggestionsProvider} which provides fix suggestions to a {@link 9 | * AccessibilityHierarchyCheckResult} generated by a {@link TouchTargetSizeCheck}. 10 | */ 11 | class TouchTargetSizeFixSuggestionsProvider 12 | extends BaseFixSuggestionsProvider { 13 | 14 | private static final ImmutableList> 15 | FIX_SUGGESTION_PRODUCERS = ImmutableList.of(new ExpandViewSizeFixSuggestionProducer()); 16 | 17 | @Override 18 | protected ImmutableList> 19 | getFixSuggestionProducers() { 20 | return FIX_SUGGESTION_PRODUCERS; 21 | } 22 | 23 | @Override 24 | protected Class getTargetCheckClass() { 25 | return TouchTargetSizeCheck.class; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/suggestions/ViewAttribute.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.suggestions; 2 | 3 | import com.google.common.annotations.Beta; 4 | import java.util.Objects; 5 | import org.checkerframework.checker.nullness.qual.Nullable; 6 | 7 | /** Represents a View attribute in XML files. */ 8 | @Beta 9 | public class ViewAttribute { 10 | 11 | public static final String NAMESPACE_ANDROID = "android"; 12 | public static final String NAMESPACE_APP = "app"; 13 | 14 | public static final String ANDROID_URI = "http://schemas.android.com/apk/res/android"; 15 | public static final String APP_URI = "http://schemas.android.com/apk/res-auto"; 16 | 17 | /** 18 | * Namespace used in XML files for attributes. Android studio uses the URI and the attribute name 19 | * to locate an attribute. 20 | */ 21 | private final String namespaceUri; 22 | 23 | private final String namespace; 24 | 25 | private final String attributeName; 26 | 27 | public ViewAttribute(String namespaceUri, String namespace, String attributeName) { 28 | this.namespaceUri = namespaceUri; 29 | this.namespace = namespace; 30 | this.attributeName = attributeName; 31 | } 32 | 33 | /** 34 | * Creates a {@link ViewAttribute} in the "android" namespace. 35 | * 36 | * @param attributeName the name of the attribute 37 | */ 38 | public ViewAttribute(String attributeName) { 39 | this(ANDROID_URI, NAMESPACE_ANDROID, attributeName); 40 | } 41 | 42 | /** Returns the name of this attribute. */ 43 | public String getAttributeName() { 44 | return attributeName; 45 | } 46 | 47 | /** Returns the namespace for this View attribute. */ 48 | public String getNamespace() { 49 | return namespace; 50 | } 51 | 52 | /** Returns the namespace used in XML files for this attribute. */ 53 | public String getNamespaceUri() { 54 | return namespaceUri; 55 | } 56 | 57 | /** Returns the fully qualified name of this View attribute such as "android:layout_width". */ 58 | public String getFullyQualifiedName() { 59 | return namespace + ":" + attributeName; 60 | } 61 | 62 | @Override 63 | public boolean equals(@Nullable Object o) { 64 | if (this == o) { 65 | return true; 66 | } 67 | if (o instanceof ViewAttribute) { 68 | ViewAttribute attribute = (ViewAttribute) o; 69 | return Objects.equals(attributeName, attribute.getAttributeName()) 70 | && Objects.equals(namespace, attribute.namespace); 71 | } 72 | return false; 73 | } 74 | 75 | @Override 76 | public int hashCode() { 77 | return Objects.hash(namespace, attributeName); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/AccessibilityHierarchyOrigin.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.uielement; 2 | 3 | import static com.google.common.base.Preconditions.checkNotNull; 4 | 5 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AccessibilityHierarchyProtos.AccessibilityHierarchyOriginProto; 6 | import com.google.common.collect.EnumBiMap; 7 | import com.google.common.collect.ImmutableMap; 8 | 9 | /** Represents a type of data source from which an AccessibilityHierarchy may be constructed. */ 10 | public enum AccessibilityHierarchyOrigin { 11 | UNKNOWN, 12 | VIEWS, 13 | ACCESSIBILITY_NODE_INFOS, 14 | ACCESSIBILITY_NODE_INFOS_AND_VIEWS, 15 | WINDOW_LIST; 16 | 17 | /** Mapping between Java and proto values. */ 18 | private static final EnumBiMap 19 | ORIGIN_MAP = 20 | EnumBiMap.create( 21 | ImmutableMap 22 | .builder() 23 | .put( 24 | AccessibilityHierarchyOrigin.UNKNOWN, 25 | AccessibilityHierarchyOriginProto.ORIGIN_UNSPECIFIED) 26 | .put( 27 | AccessibilityHierarchyOrigin.VIEWS, 28 | AccessibilityHierarchyOriginProto.ORIGIN_VIEWS) 29 | .put( 30 | AccessibilityHierarchyOrigin.ACCESSIBILITY_NODE_INFOS, 31 | AccessibilityHierarchyOriginProto.ORIGIN_ACCESSIBILITY_NODE_INFOS) 32 | .put( 33 | AccessibilityHierarchyOrigin.ACCESSIBILITY_NODE_INFOS_AND_VIEWS, 34 | AccessibilityHierarchyOriginProto.ORIGIN_ACCESSIBILITY_NODE_INFOS_AND_VIEWS) 35 | .put( 36 | AccessibilityHierarchyOrigin.WINDOW_LIST, 37 | AccessibilityHierarchyOriginProto.ORIGIN_WINDOW_LIST) 38 | .buildOrThrow()); 39 | 40 | public static AccessibilityHierarchyOrigin fromProto(AccessibilityHierarchyOriginProto proto) { 41 | return checkNotNull(ORIGIN_MAP.inverse().get(proto)); 42 | } 43 | 44 | public AccessibilityHierarchyOriginProto toProto() { 45 | return checkNotNull(ORIGIN_MAP.get(this)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/CustomViewBuilderAndroid.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.uielement; 2 | 3 | import android.view.View; 4 | import org.checkerframework.checker.nullness.qual.Nullable; 5 | 6 | /** 7 | * Base interface for customizing how to build an {@link AccessibilityHierarchyAndroid} from a given 8 | * {@link View}. 9 | * 10 | *

Currently only two methods are provided to support Android Studio integration. 11 | * 12 | *

Follow the following steps to add a new customization method into this interface: 13 | * 14 | *

    15 | *
  • Define a new customization methond in {@link CustomViewBuilderAndroid} 16 | *
  • Start to use the new method in {@link CustomViewBuilderAndroid} 17 | *
  • Add the default implementation of the method in {@link DefaultCustomViewBuilderAndroid} 18 | *
  • Customized the implementation of the new method 19 | *
20 | */ 21 | public interface CustomViewBuilderAndroid { 22 | 23 | /** 24 | * Returns the class instance based on a given string class name. Returns {@code null} if not 25 | * found. 26 | */ 27 | @Nullable 28 | Class getClassByName(ViewHierarchyElementAndroid view, String className); 29 | 30 | /** Returns {@code true} if the given view is checkable. */ 31 | boolean isCheckable(View fromView); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/DefaultCustomViewBuilderAndroid.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.uielement; 2 | 3 | import android.os.StrictMode; 4 | import android.view.View; 5 | import android.widget.Checkable; 6 | import com.google.android.libraries.accessibility.utils.log.LogUtils; 7 | import com.google.android.material.button.MaterialButton; 8 | import com.google.android.material.card.MaterialCardView; 9 | import com.google.android.material.chip.Chip; 10 | import org.checkerframework.checker.nullness.qual.Nullable; 11 | 12 | /** Default implementation for interface {@link CustomViewBuilderAndroid}. */ 13 | public class DefaultCustomViewBuilderAndroid implements CustomViewBuilderAndroid { 14 | 15 | private static final String TAG = "DefaultClassByNameResolverAndroid"; 16 | 17 | @Override 18 | public @Nullable Class getClassByName(ViewHierarchyElementAndroid view, String className) { 19 | 20 | StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); 21 | try { 22 | ClassLoader classLoader = view.getClass().getClassLoader(); 23 | if (classLoader != null) { 24 | return classLoader.loadClass(className); 25 | } else { 26 | LogUtils.w(TAG, "Unsuccessful attempt to get ClassLoader to load %1$s", className); 27 | } 28 | } catch (ClassNotFoundException e) { 29 | LogUtils.w(TAG, "Unsuccessful attempt to load class %1$s.", className); 30 | } finally { 31 | StrictMode.setThreadPolicy(oldPolicy); 32 | } 33 | return null; 34 | } 35 | 36 | @Override 37 | public boolean isCheckable(View fromView) { 38 | if (fromView instanceof MaterialButton) { 39 | // Although MaterialButton implements Checkable, it isn't always checkable for accessibility. 40 | return ((MaterialButton) fromView).isCheckable(); 41 | } 42 | if (fromView instanceof MaterialCardView) { 43 | // MaterialCardView also implements Checkable and isn't always checkable for accessibility. 44 | return ((MaterialCardView) fromView).isCheckable(); 45 | } 46 | if (fromView instanceof Chip) { 47 | // Chip also implements Checkable and isn't always checkable for accessibility. 48 | return ((Chip) fromView).isCheckable(); 49 | } 50 | return (fromView instanceof Checkable); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/DeviceState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package com.google.android.apps.common.testing.accessibility.framework.uielement; 17 | 18 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AccessibilityHierarchyProtos.DeviceStateProto; 19 | import com.google.common.base.Splitter; 20 | import com.google.errorprone.annotations.Immutable; 21 | import java.util.Locale; 22 | import org.checkerframework.checker.nullness.qual.Nullable; 23 | 24 | /** 25 | * Representation of the state of a device at the time an {@link AccessibilityHierarchy} is 26 | * captured. 27 | * 28 | *

Display properties, such as screen resolution and pixel density, are stored within {@link 29 | * DisplayInfo} and as fields in the associated {@link DeviceStateProto}. 30 | */ 31 | @Immutable 32 | public class DeviceState { 33 | 34 | protected static final Splitter HYPHEN_SPLITTER = Splitter.on('-'); 35 | 36 | /** @see android.view.WindowManager#getDefaultDisplay() */ 37 | private final DisplayInfo defaultDisplayInfo; 38 | 39 | /** @see Build.VERSION#SDK_INT */ 40 | protected final int sdkVersion; 41 | 42 | protected final Locale locale; 43 | 44 | protected final @Nullable Float fontScale; 45 | protected final boolean hasFeatureWatch; 46 | 47 | /** Creates a record of the device state at the time of construction. */ 48 | DeviceState(DeviceStateProto fromProto) { 49 | sdkVersion = fromProto.getSdkVersion(); 50 | defaultDisplayInfo = new DisplayInfo(fromProto.getDefaultDisplayInfo()); 51 | String languageTag = fromProto.getLocale(); 52 | // Use English if no locale was recorded in the proto. 53 | locale = languageTag.isEmpty() ? Locale.ENGLISH : getLocaleFromLanguageTag(languageTag); 54 | fontScale = fromProto.hasFontScale() ? fromProto.getFontScale() : null; 55 | hasFeatureWatch = fromProto.getFeatureWatch(); 56 | } 57 | 58 | protected DeviceState( 59 | int sdkVersion, Locale locale, @Nullable Float fontScale, boolean hasFeatureWatch) { 60 | this.sdkVersion = sdkVersion; 61 | this.locale = locale; 62 | this.fontScale = fontScale; 63 | this.hasFeatureWatch = hasFeatureWatch; 64 | defaultDisplayInfo = new DisplayInfo(); 65 | } 66 | 67 | /** See {@link android.view.WindowManager#getDefaultDisplay() getDefaultDisplay}. */ 68 | public DisplayInfo getDefaultDisplayInfo() { 69 | return defaultDisplayInfo; 70 | } 71 | 72 | /** @see Build.VERSION#SDK_INT */ 73 | public int getSdkVersion() { 74 | return sdkVersion; 75 | } 76 | 77 | /** 78 | * Gets the locale at the time the device state was captured. Returns Locale.ENGLISH for instances 79 | * persisted before DeviceState began storing capture-time locale (ca. Feb 2018) 80 | */ 81 | public Locale getLocale() { 82 | return locale; 83 | } 84 | 85 | /** 86 | * Gets the Accessibility Font scale at the time the device state was captured. 87 | * 88 | * @see android.content.res.Configuration#fontScale 89 | */ 90 | public @Nullable Float getFontScale() { 91 | return fontScale; 92 | } 93 | 94 | /** 95 | * Indicates whether the device is known to have system feature {@link 96 | * android.content.pm.PackageManager#FEATURE_WATCH}. 97 | */ 98 | public boolean hasFeatureWatch() { 99 | return hasFeatureWatch; 100 | } 101 | 102 | DeviceStateProto toProto() { 103 | DeviceStateProto.Builder builder = DeviceStateProto.newBuilder(); 104 | builder.setSdkVersion(sdkVersion); 105 | builder.setDefaultDisplayInfo(getDefaultDisplayInfo().toProto()); 106 | builder.setLocale(getLanguageTag()); 107 | if (fontScale != null) { 108 | builder.setFontScale(fontScale); 109 | } 110 | if (hasFeatureWatch) { 111 | builder.setFeatureWatch(true); 112 | } 113 | return builder.build(); 114 | } 115 | 116 | protected String getLanguageTag() { 117 | return locale.toLanguageTag(); 118 | } 119 | 120 | private static Locale getLocaleFromLanguageTag(String languageTag) { 121 | return Locale.forLanguageTag(languageTag); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/DeviceStateAndroid.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package com.google.android.apps.common.testing.accessibility.framework.uielement; 17 | 18 | import static com.google.common.base.Preconditions.checkNotNull; 19 | 20 | import android.content.Context; 21 | import android.content.pm.PackageManager; 22 | import android.os.Build; 23 | import android.view.WindowManager; 24 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AccessibilityHierarchyProtos.DeviceStateProto; 25 | import com.google.common.annotations.VisibleForTesting; 26 | import com.google.errorprone.annotations.Immutable; 27 | import java.util.List; 28 | import java.util.Locale; 29 | import org.checkerframework.checker.nullness.qual.Nullable; 30 | 31 | /** 32 | * Representation of the state of a device at the time an {@link AccessibilityHierarchy} is 33 | * captured. 34 | * 35 | *

Display properties, such as screen resolution and pixel density, are stored within {@link 36 | * DisplayInfoAndroid} and as fields in the associated {@link DeviceStateProto}. 37 | */ 38 | @Immutable 39 | public class DeviceStateAndroid extends DeviceState { 40 | 41 | /** @see WindowManager#getDefaultDisplay() */ 42 | private final DisplayInfoAndroid defaultDisplayInfo; 43 | 44 | private DeviceStateAndroid( 45 | int sdkVersion, 46 | Locale locale, 47 | DisplayInfoAndroid defaultDisplayInfo, 48 | @Nullable Float fontScale, 49 | boolean hasFeatureWatch) { 50 | super(sdkVersion, locale, fontScale, hasFeatureWatch); 51 | this.defaultDisplayInfo = defaultDisplayInfo; 52 | } 53 | 54 | /** See {@link WindowManager#getDefaultDisplay()}. */ 55 | @Override 56 | public DisplayInfoAndroid getDefaultDisplayInfo() { 57 | return defaultDisplayInfo; 58 | } 59 | 60 | @Override 61 | protected String getLanguageTag() { 62 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 63 | return locale.toLanguageTag(); 64 | } else { 65 | return getStringFromLocale(locale); 66 | } 67 | } 68 | 69 | private static Locale getLocaleFromLanguageTag(String languageTag) { 70 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 71 | return Locale.forLanguageTag(languageTag); 72 | } else { 73 | return getLocaleFromString(languageTag); 74 | } 75 | } 76 | 77 | /** Returns a new builder that can build a DeviceStateAndroid from a context. */ 78 | static Builder newBuilder(Context context) { 79 | return new Builder(context); 80 | } 81 | 82 | /** Returns a new builder that can build a DeviceStateAndroid from a proto. */ 83 | static Builder newBuilder(DeviceStateProto fromProto) { 84 | return new Builder(checkNotNull(fromProto)); 85 | } 86 | 87 | /** 88 | * Attempts to produce the same result as {@link Locale#toLanguageTag} for those locales supported 89 | * by ATF. 90 | * 91 | *

For use with builds prior to LOLLIPOP, where toLanguageTag is not available. 92 | */ 93 | @VisibleForTesting 94 | static String getStringFromLocale(Locale locale) { 95 | return locale.toString().replace('_', '-'); 96 | } 97 | 98 | /** 99 | * Attempts to produce the same result as {@link Locale#forLanguageTag} for those locales 100 | * supported by ATF. 101 | * 102 | *

For use with builds prior to LOLLIPOP, where forLanguageTag is not available. 103 | */ 104 | @VisibleForTesting 105 | static Locale getLocaleFromString(String str) { 106 | List parts = HYPHEN_SPLITTER.splitToList(str); 107 | switch (parts.size()) { 108 | case 1: 109 | return new Locale(parts.get(0)); 110 | case 2: 111 | return new Locale(parts.get(0), parts.get(1)); 112 | case 3: 113 | return new Locale(parts.get(0), parts.get(1), parts.get(2)); 114 | default: 115 | throw new IllegalArgumentException("Unsupported locale string: " + str); 116 | } 117 | } 118 | 119 | /** 120 | * A builder for {@link DeviceStateAndroid}; obtained using {@link DeviceStateAndroid#builder}. 121 | */ 122 | public static class Builder { 123 | private final int sdkVersion; 124 | private final Locale locale; 125 | private final DisplayInfoAndroid defaultDisplayInfo; 126 | private final @Nullable Float fontScale; 127 | private final boolean hasFeatureWatch; 128 | 129 | // dereference of possibly-null reference wm 130 | @SuppressWarnings("nullness:dereference.of.nullable") 131 | Builder(Context context) { 132 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 133 | defaultDisplayInfo = new DisplayInfoAndroid(wm.getDefaultDisplay()); 134 | 135 | sdkVersion = 136 | Build.VERSION.SDK_INT; 137 | locale = Locale.getDefault(); 138 | fontScale = context.getResources().getConfiguration().fontScale; 139 | hasFeatureWatch = hasFeatureWatch(context); 140 | } 141 | 142 | Builder(DeviceStateProto fromProto) { 143 | sdkVersion = fromProto.getSdkVersion(); 144 | defaultDisplayInfo = new DisplayInfoAndroid(fromProto.getDefaultDisplayInfo()); 145 | String languageTag = fromProto.getLocale(); 146 | // Use the default Locale if no locale was recorded in the proto. 147 | // This is for backward compatibility. 148 | locale = languageTag.isEmpty() ? Locale.getDefault() : getLocaleFromLanguageTag(languageTag); 149 | fontScale = fromProto.hasFontScale() ? fromProto.getFontScale() : null; 150 | hasFeatureWatch = fromProto.getFeatureWatch(); 151 | } 152 | 153 | private static boolean hasFeatureWatch(Context context) { 154 | return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) 155 | && context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH); 156 | } 157 | 158 | public DeviceStateAndroid build() { 159 | return new DeviceStateAndroid( 160 | sdkVersion, locale, defaultDisplayInfo, fontScale, hasFeatureWatch); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/DisplayInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.google.android.apps.common.testing.accessibility.framework.uielement; 15 | 16 | import static com.google.common.base.Preconditions.checkNotNull; 17 | 18 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AccessibilityHierarchyProtos.DisplayInfoMetricsProto; 19 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AccessibilityHierarchyProtos.DisplayInfoProto; 20 | import com.google.errorprone.annotations.Immutable; 21 | import org.checkerframework.checker.nullness.qual.Nullable; 22 | 23 | /** 24 | * Representation of a {@link android.view.Display} 25 | * 26 | *

NOTE: Currently, this class holds only {@link Metrics}, but will likely have additional fields 27 | * in the future. 28 | */ 29 | @Immutable 30 | public class DisplayInfo { 31 | 32 | private final @Nullable Metrics metricsWithoutDecoration; 33 | private final @Nullable Metrics realMetrics; 34 | 35 | DisplayInfo(DisplayInfoProto fromProto) { 36 | this.metricsWithoutDecoration = new Metrics(fromProto.getMetricsWithoutDecoration()); 37 | this.realMetrics = fromProto.hasRealMetrics() ? new Metrics(fromProto.getRealMetrics()) : null; 38 | } 39 | 40 | protected DisplayInfo() { 41 | this.metricsWithoutDecoration = null; 42 | this.realMetrics = null; 43 | } 44 | 45 | /** 46 | * @return a {@link Metrics} representing the display's metrics excluding certain system 47 | * decorations. 48 | * @see android.view.Display#getMetrics(android.util.DisplayMetrics) 49 | */ 50 | public Metrics getMetricsWithoutDecoration() { 51 | checkNotNull(metricsWithoutDecoration); 52 | return metricsWithoutDecoration; 53 | } 54 | 55 | /** 56 | * @return a {@link Metrics} representing the display's real metrics, which include system 57 | * decorations. This value can be {@code null} for instances created on platform versions that 58 | * don't support resolution of real metrics. 59 | * @see android.view.Display#getRealMetrics(android.util.DisplayMetrics) 60 | */ 61 | public @Nullable Metrics getRealMetrics() { 62 | return realMetrics; 63 | } 64 | 65 | DisplayInfoProto toProto() { 66 | checkNotNull(metricsWithoutDecoration); 67 | DisplayInfoProto.Builder builder = DisplayInfoProto.newBuilder(); 68 | builder.setMetricsWithoutDecoration(metricsWithoutDecoration.toProto()); 69 | if (realMetrics != null) { 70 | builder.setRealMetrics(realMetrics.toProto()); 71 | } 72 | return builder.build(); 73 | } 74 | 75 | /** Representation of a {@link android.util.DisplayMetrics} */ 76 | @Immutable 77 | public static class Metrics { 78 | 79 | protected final float density; 80 | protected final float scaledDensity; 81 | protected final float xDpi; 82 | protected final float yDpi; 83 | protected final int densityDpi; 84 | protected final int heightPixels; 85 | protected final int widthPixels; 86 | 87 | Metrics( 88 | float density, 89 | float scaledDensity, 90 | float xDpi, 91 | float yDpi, 92 | int densityDpi, 93 | int heightPixels, 94 | int widthPixels) { 95 | this.density = density; 96 | this.scaledDensity = scaledDensity; 97 | this.xDpi = xDpi; 98 | this.yDpi = yDpi; 99 | this.densityDpi = densityDpi; 100 | this.heightPixels = heightPixels; 101 | this.widthPixels = widthPixels; 102 | } 103 | 104 | Metrics(DisplayInfoMetricsProto fromProto) { 105 | this.density = fromProto.getDensity(); 106 | this.scaledDensity = fromProto.getScaledDensity(); 107 | this.xDpi = fromProto.getXDpi(); 108 | this.yDpi = fromProto.getYDpi(); 109 | this.densityDpi = fromProto.getDensityDpi(); 110 | this.heightPixels = fromProto.getHeightPixels(); 111 | this.widthPixels = fromProto.getWidthPixels(); 112 | } 113 | 114 | /** See {@link android.util.DisplayMetrics#density}. */ 115 | public float getDensity() { 116 | return density; 117 | } 118 | 119 | /** See {@link android.util.DisplayMetrics#scaledDensity}. */ 120 | public float getScaledDensity() { 121 | return scaledDensity; 122 | } 123 | 124 | /** See {@link android.util.DisplayMetrics#xdpi}. */ 125 | public float getxDpi() { 126 | return xDpi; 127 | } 128 | 129 | /** See {@link android.util.DisplayMetrics#ydpi}. */ 130 | public float getyDpi() { 131 | return yDpi; 132 | } 133 | 134 | /** See {@link android.util.DisplayMetrics#densityDpi}. */ 135 | public int getDensityDpi() { 136 | return densityDpi; 137 | } 138 | 139 | /** See {@link android.util.DisplayMetrics#heightPixels}. */ 140 | public int getHeightPixels() { 141 | return heightPixels; 142 | } 143 | 144 | /** See {@link android.util.DisplayMetrics#widthPixels}. */ 145 | public int getWidthPixels() { 146 | return widthPixels; 147 | } 148 | 149 | DisplayInfoMetricsProto toProto() { 150 | DisplayInfoMetricsProto.Builder builder = DisplayInfoMetricsProto.newBuilder(); 151 | builder.setDensity(density); 152 | builder.setScaledDensity(scaledDensity); 153 | builder.setXDpi(xDpi); 154 | builder.setYDpi(yDpi); 155 | builder.setDensityDpi(densityDpi); 156 | builder.setHeightPixels(heightPixels); 157 | builder.setWidthPixels(widthPixels); 158 | return builder.build(); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/DisplayInfoAndroid.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.google.android.apps.common.testing.accessibility.framework.uielement; 15 | 16 | import android.util.DisplayMetrics; 17 | import android.view.Display; 18 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AccessibilityHierarchyProtos.DisplayInfoMetricsProto; 19 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AccessibilityHierarchyProtos.DisplayInfoProto; 20 | import com.google.errorprone.annotations.Immutable; 21 | import org.checkerframework.checker.nullness.qual.Nullable; 22 | 23 | /** 24 | * Representation of a {@link Display} 25 | * 26 | *

NOTE: Currently, this class holds only {@link MetricsAndroid}, but will likely have additional 27 | * fields in the future. 28 | */ 29 | @Immutable 30 | public class DisplayInfoAndroid extends DisplayInfo { 31 | 32 | private final MetricsAndroid metricsWithoutDecoration; 33 | private final @Nullable MetricsAndroid realMetrics; 34 | 35 | /** 36 | * Derives an instance from a {@link Display} 37 | * 38 | * @param display The {@link Display} instance from which to construct 39 | */ 40 | public DisplayInfoAndroid(Display display) { 41 | super(); 42 | DisplayMetrics tempMetrics = new DisplayMetrics(); 43 | display.getMetrics(tempMetrics); 44 | this.metricsWithoutDecoration = new MetricsAndroid(tempMetrics); 45 | tempMetrics.setToDefaults(); 46 | display.getRealMetrics(tempMetrics); 47 | this.realMetrics = new MetricsAndroid(tempMetrics); 48 | } 49 | 50 | DisplayInfoAndroid(DisplayInfoProto fromProto) { 51 | super(); 52 | this.metricsWithoutDecoration = new MetricsAndroid(fromProto.getMetricsWithoutDecoration()); 53 | this.realMetrics = 54 | fromProto.hasRealMetrics() ? new MetricsAndroid(fromProto.getRealMetrics()) : null; 55 | } 56 | 57 | /** 58 | * See {@link Display#getMetrics(DisplayMetrics)}. 59 | * 60 | * @return a {@link MetricsAndroid} representing the display's metrics excluding certain system 61 | * decorations. 62 | */ 63 | @Override 64 | public MetricsAndroid getMetricsWithoutDecoration() { 65 | return metricsWithoutDecoration; 66 | } 67 | 68 | /** 69 | * See {@link Display#getRealMetrics(DisplayMetrics)}. 70 | * 71 | * @return a {@link MetricsAndroid} representing the display's real metrics, which include system 72 | * decorations. This value can be {@code null} for instances created on platform versions that 73 | * don't support resolution of real metrics. 74 | */ 75 | @Override 76 | public @Nullable MetricsAndroid getRealMetrics() { 77 | return realMetrics; 78 | } 79 | 80 | @Override 81 | DisplayInfoProto toProto() { 82 | DisplayInfoProto.Builder builder = DisplayInfoProto.newBuilder(); 83 | builder.setMetricsWithoutDecoration(metricsWithoutDecoration.toProto()); 84 | if (realMetrics != null) { 85 | builder.setRealMetrics(realMetrics.toProto()); 86 | } 87 | return builder.build(); 88 | } 89 | 90 | /** Representation of a {@link DisplayMetrics} */ 91 | @Immutable 92 | public static class MetricsAndroid extends Metrics { 93 | /** 94 | * Derives an instance from a {@link DisplayMetrics} 95 | * 96 | * @param metrics The {@link DisplayMetrics} instance from which to construct 97 | */ 98 | public MetricsAndroid(DisplayMetrics metrics) { 99 | super( 100 | metrics.density, 101 | metrics.scaledDensity, 102 | metrics.xdpi, 103 | metrics.ydpi, 104 | metrics.densityDpi, 105 | metrics.heightPixels, 106 | metrics.widthPixels); 107 | } 108 | 109 | MetricsAndroid(DisplayInfoMetricsProto fromProto) { 110 | super(fromProto); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/ViewHierarchyAction.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.uielement; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.replacements.TextUtils; 4 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AccessibilityHierarchyProtos.ViewHierarchyActionProto; 5 | import org.checkerframework.checker.nullness.qual.Nullable; 6 | 7 | /** 8 | * Representation of {@code AccessibilityNodeInfo.AccessibilityAction} in a {@code View}. 9 | * 10 | *

The action exists within {@code ViewHierarchyElement} and it is one-to-one map from {@code 11 | * AccessibilityAction}. 12 | */ 13 | public class ViewHierarchyAction { 14 | private final int actionId; 15 | private final @Nullable CharSequence actionLabel; 16 | 17 | protected ViewHierarchyAction(int actionId, @Nullable CharSequence actionLabel) { 18 | this.actionId = actionId; 19 | this.actionLabel = actionLabel; 20 | } 21 | 22 | ViewHierarchyAction(ViewHierarchyActionProto proto) { 23 | this.actionId = proto.getActionId(); 24 | this.actionLabel = proto.hasActionLabel() ? proto.getActionLabel() : null; 25 | } 26 | 27 | /** 28 | * Returns the action Id. Action Id will be one of the actions that are listed in {@code 29 | * AccessibilityNodeInfo}. 30 | */ 31 | int getActionId() { 32 | return actionId; 33 | } 34 | 35 | /** Returns the label of the corresponding action id. */ 36 | @Nullable 37 | CharSequence getActionLabel() { 38 | return actionLabel; 39 | } 40 | 41 | ViewHierarchyActionProto toProto() { 42 | ViewHierarchyActionProto.Builder builder = ViewHierarchyActionProto.newBuilder(); 43 | 44 | builder.setActionId(actionId); 45 | if (!TextUtils.isEmpty(actionLabel)) { 46 | builder.setActionLabel(actionLabel.toString()); 47 | } 48 | return builder.build(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/ViewHierarchyActionAndroid.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.uielement; 2 | 3 | import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 4 | import com.google.android.apps.common.testing.accessibility.framework.uielement.proto.AccessibilityHierarchyProtos.ViewHierarchyActionProto; 5 | import org.checkerframework.checker.nullness.qual.Nullable; 6 | 7 | /** 8 | * Representation of {@code AccessibilityNodeInfo.AccessibilityAction} in a {@code View}. 9 | * 10 | *

The action exists within {@code ViewHierarchyElement} and it is one-to-one map from {@code 11 | * AccessibilityAction}. 12 | */ 13 | public class ViewHierarchyActionAndroid extends ViewHierarchyAction { 14 | 15 | private ViewHierarchyActionAndroid(int actionId, @Nullable CharSequence actionLabel) { 16 | super(actionId, actionLabel); 17 | } 18 | 19 | /** Creates a builder which can build ViewHierarchyActionAndroid from AccessibilityAction */ 20 | static Builder newBuilder(AccessibilityAction action) { 21 | return new Builder(action); 22 | } 23 | 24 | /** Creates a builder which can build ViewHierarchyActionAndroid from ViewHierarchyActionProto */ 25 | static Builder newBuilder(ViewHierarchyActionProto actionProto) { 26 | return new Builder(actionProto); 27 | } 28 | 29 | /** Builder for {@code ViewHierarchyElementAndroid} */ 30 | static class Builder { 31 | private final int actionId; 32 | private final @Nullable CharSequence actionLabel; 33 | 34 | Builder(AccessibilityAction action) { 35 | this.actionId = action.getId(); 36 | this.actionLabel = action.getLabel(); 37 | } 38 | 39 | Builder(ViewHierarchyActionProto actionProto) { 40 | this.actionId = actionProto.getActionId(); 41 | this.actionLabel = actionProto.hasActionLabel() ? actionProto.getActionLabel() : null; 42 | } 43 | 44 | public ViewHierarchyActionAndroid build() { 45 | return new ViewHierarchyActionAndroid(actionId, actionLabel); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/ViewHierarchyElementDescriptor.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.uielement; 2 | 3 | import com.google.android.apps.common.testing.accessibility.framework.replacements.Rect; 4 | import com.google.android.apps.common.testing.accessibility.framework.replacements.TextUtils; 5 | 6 | /** An object that describes a {@link ViewHierarchyElement} */ 7 | public class ViewHierarchyElementDescriptor { 8 | 9 | /** 10 | * Returns a String description of the given {@link ViewHierarchyElement}. The default is to 11 | * return the view's resource entry name. If the view has no valid resource entry name, then 12 | * returns the view's bounds. 13 | * 14 | * @param element the {@link ViewHierarchyElement} to describe 15 | * @return a String description of the given {@link ViewHierarchyElement} 16 | */ 17 | public String describe(ViewHierarchyElement element) { 18 | StringBuilder message = new StringBuilder(); 19 | message.append("View "); 20 | if (!TextUtils.isEmpty(element.getResourceName())) { 21 | message.append(element.getResourceName()); 22 | } else { 23 | Rect bounds = element.getBoundsInScreen(); 24 | if (!bounds.isEmpty()) { 25 | message.append("with bounds: "); 26 | message.append(bounds.toShortString()); 27 | } else { 28 | message.append("with no valid resource name or bounds"); 29 | } 30 | } 31 | return message.toString(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/uielement/ViewHierarchyElementOrigin.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.uielement; 2 | 3 | /** Represents the type of element that a ViewHierarchyElement represents. */ 4 | public enum ViewHierarchyElementOrigin { 5 | UNKNOWN, 6 | /* From Composeable content */ 7 | COMPOSE, 8 | /* From Flutter content */ 9 | FLUTTER, 10 | /* From web content */ 11 | WEB, 12 | /* From a View object that is not COMPOSE or WEB */ 13 | VIEW; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/utils/contrast/BitmapImage.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.utils.contrast; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | 5 | import android.graphics.Bitmap; 6 | 7 | /** 8 | * Implementation of an {@link Image} using {@link Bitmap}. This depends upon an Android runtime. 9 | */ 10 | public final class BitmapImage implements Image { 11 | 12 | private final Bitmap bitmap; 13 | private final int left; 14 | private final int top; 15 | private final int width; 16 | private final int height; 17 | 18 | public BitmapImage(Bitmap bitmap) { 19 | this(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight()); 20 | } 21 | 22 | private BitmapImage(Bitmap bitmap, int left, int top, int width, int height) { 23 | this.bitmap = bitmap; 24 | this.left = left; 25 | this.top = top; 26 | this.width = width; 27 | this.height = height; 28 | } 29 | 30 | @Override 31 | public int getHeight() { 32 | return height; 33 | } 34 | 35 | @Override 36 | public int getWidth() { 37 | return width; 38 | } 39 | 40 | @Override 41 | public BitmapImage crop(int left, int top, int width, int height) { 42 | checkArgument(left >= 0, "left must be >= 0"); 43 | checkArgument(top >= 0, "top must be >= 0"); 44 | checkArgument(width > 0, "width must be > 0"); 45 | checkArgument(height > 0, "height must be > 0"); 46 | checkArgument(left + width <= this.width); 47 | checkArgument(top + height <= this.height); 48 | return new BitmapImage(bitmap, this.left + left, this.top + top, width, height); 49 | } 50 | 51 | @Override 52 | public int[] getPixels() { 53 | int[] pixels = new int[width * height]; 54 | bitmap.getPixels( 55 | pixels, /* offset= */ 0, /* stride= */ width, /* x= */ left, /* y= */ top, width, height); 56 | return pixels; 57 | } 58 | 59 | /** Creates a Bitmap from this BitmapImage. */ 60 | public Bitmap getBitmap() { 61 | return Bitmap.createBitmap(bitmap, /* x= */ left, /* y= */ top, width, height); 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return String.format( 67 | "{BitmapImage left=%d top=%d width=%d height=%d}", left, top, width, height); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/utils/contrast/Color.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.google.android.apps.common.testing.accessibility.framework.utils.contrast; 16 | 17 | import androidx.annotation.ColorInt; 18 | import androidx.annotation.IntRange; 19 | 20 | /** Used as a local replacement for Android's {@link android.graphics.Color} */ 21 | public final class Color { 22 | 23 | public static final int BLACK = 0xFF000000; 24 | public static final int WHITE = 0xFFFFFFFF; 25 | 26 | private Color() { 27 | // Not instantiable 28 | } 29 | 30 | /** See {@link android.graphics.Color#alpha(int)}. */ 31 | @IntRange(from = 0, to = 255) 32 | public static int alpha(int color) { 33 | return color >>> 24; 34 | } 35 | 36 | /** See {@link android.graphics.Color#red(int)}. */ 37 | @IntRange(from = 0, to = 255) 38 | public static int red(int color) { 39 | return (color >> 16) & 0xFF; 40 | } 41 | 42 | /** See {@link android.graphics.Color#green(int)}. */ 43 | @IntRange(from = 0, to = 255) 44 | public static int green(int color) { 45 | return (color >> 8) & 0xFF; 46 | } 47 | 48 | /** See {@link android.graphics.Color#blue(int)}. */ 49 | @IntRange(from = 0, to = 255) 50 | public static int blue(int color) { 51 | return color & 0xFF; 52 | } 53 | 54 | /** See {@link android.graphics.Color#argb(int, int, int, int)}. */ 55 | @ColorInt 56 | public static int argb( 57 | @IntRange(from = 0, to = 255) int alpha, 58 | @IntRange(from = 0, to = 255) int red, 59 | @IntRange(from = 0, to = 255) int green, 60 | @IntRange(from = 0, to = 255) int blue) { 61 | return (alpha << 24) | (red << 16) | (green << 8) | blue; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/apps/common/testing/accessibility/framework/utils/contrast/Image.java: -------------------------------------------------------------------------------- 1 | package com.google.android.apps.common.testing.accessibility.framework.utils.contrast; 2 | 3 | /** Platform-independent representation of a graphical image for contrast analysis. */ 4 | public interface Image { 5 | 6 | /** Returns the image's height */ 7 | int getHeight(); 8 | 9 | /** Returns the image's width */ 10 | int getWidth(); 11 | 12 | /** 13 | * Returns an image representing a rectangular region within this image. The result will be a view 14 | * into the original image, so its contents will change if the original is modified. 15 | * 16 | * @param left The leftmost coordinate of the region 17 | * @param top The toptmost coordinate of the region 18 | * @param width The width of the region 19 | * @param height The height of the region 20 | */ 21 | Image crop(int left, int top, int width, int height); 22 | 23 | /** 24 | * Returns a copy of the data within the image. Each value is a packed int representing a Color. 25 | */ 26 | int[] getPixels(); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/proto/com/google/android/apps/common/testing/accessibility/framework/AccessibilityEvaluation.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | syntax = "proto2"; 18 | 19 | package com.google.android.apps.common.testing.accessibility.framework.proto; 20 | 21 | import "com/google/android/apps/common/testing/accessibility/framework/uielement/AccessibilityHierarchy.proto"; 22 | import "com/google/android/apps/common/testing/accessibility/framework/uielement/AndroidFramework.proto"; 23 | 24 | option java_package = "com.google.android.apps.common.testing.accessibility.framework.proto"; 25 | option java_outer_classname = "AccessibilityEvaluationProtos"; 26 | option objc_class_prefix = "AXEP"; 27 | 28 | // Describes a single-hierarchy accessibility evaluation by ATF 29 | // Next index: 3 30 | message AccessibilityEvaluation { 31 | optional com.google.android.apps.common.testing.accessibility.framework 32 | .uielement.proto.AccessibilityHierarchyProto hierarchy = 1; 33 | repeated AccessibilityHierarchyCheckResultProto results = 2; 34 | } 35 | 36 | // Describes an AccessibilityHierarchyCheckResult 37 | // Next index: 7 38 | message AccessibilityHierarchyCheckResultProto { 39 | optional string source_check_class = 1; 40 | optional int32 result_id = 2; 41 | optional ResultTypeProto result_type = 3; 42 | optional int64 hierarchy_source_id = 4; 43 | optional MetadataProto metadata = 5; 44 | repeated AnswerProto answers = 6; 45 | } 46 | 47 | // Other than UNKNOWN, this should mirror 48 | // AccessibilityCheckResult.AccessibilityCheckResultType 49 | // Next index: 7 50 | enum ResultTypeProto { 51 | UNKNOWN = 0; 52 | ERROR = 1; 53 | WARNING = 2; 54 | INFO = 3; 55 | RESOLVED = 6; 56 | NOT_RUN = 4; 57 | SUPPRESSED = 5; 58 | } 59 | 60 | // Proto describing a Metadata, used by AccessibilityHierarchyCheckResult to 61 | // store parameters used to generate result messages 62 | // Next index: 2 63 | message MetadataProto { 64 | map metadata_map = 1; 65 | } 66 | 67 | // Proto describing a list of string. 68 | // Next index: 2 69 | message StringListProto { 70 | repeated string values = 1; 71 | } 72 | 73 | // Proto describing a list of int. 74 | // Next index: 2 75 | message IntListProto { 76 | repeated int32 values = 1; 77 | } 78 | 79 | // Proto describing raw metadata information and its associated type 80 | // Next index: 13 81 | message TypedValueProto { 82 | // Once a TypeProto is defined here, it must not be removed and its value 83 | // must not be changed. Additions are permitted at the end of enum. Data may 84 | // be persisted using these values, so incompatible changes may result in 85 | // corruption during deserialization. 86 | // Next index: 12 87 | enum TypeProto { 88 | UNKNOWN = 0; 89 | BOOLEAN = 1; 90 | BYTE = 2; 91 | SHORT = 3; 92 | CHAR = 4; 93 | INT = 5; 94 | FLOAT = 6; 95 | LONG = 7; 96 | DOUBLE = 8; 97 | STRING = 9; 98 | STRING_LIST = 10; 99 | INT_LIST = 11; 100 | } 101 | 102 | optional TypeProto type = 1; 103 | oneof value { 104 | bool boolean_value = 2; 105 | bytes byte_value = 3; 106 | bytes short_value = 4; 107 | bytes char_value = 5; 108 | int32 int_value = 6; 109 | float float_value = 7; 110 | int64 long_value = 8; 111 | double double_value = 9; 112 | string string_value = 10; 113 | StringListProto string_list_value = 11; 114 | IntListProto int_list_value = 12; 115 | } 116 | } 117 | 118 | // Describes a Question 119 | // Next index: 7 120 | message QuestionProto { 121 | optional int32 question_id = 1; 122 | optional string question_type_class = 2; 123 | optional string answer_type_class = 3; 124 | optional string question_handler_class = 4; 125 | optional AccessibilityHierarchyCheckResultProto original_result = 5; 126 | optional MetadataProto metadata = 6; 127 | } 128 | 129 | // Describes an Answer 130 | // Next index: 4 131 | message AnswerProto { 132 | optional string answer_type_class = 1; 133 | optional QuestionProto question = 2; 134 | optional MetadataProto metadata = 3; 135 | } 136 | 137 | // Describes an OcrResult 138 | // Next index: 2 139 | message OcrResultProto { 140 | repeated TextComponentProto texts = 1; 141 | } 142 | 143 | // Describes a text recognized by the OCR engine 144 | message TextComponentProto { 145 | optional string value = 1; 146 | optional string language = 2; 147 | optional float confidence = 3; 148 | optional com.google.android.apps.common.testing.accessibility.framework 149 | .uielement.proto.RectProto bounds_in_screen = 4; 150 | repeated TextComponentProto components = 5; 151 | } 152 | -------------------------------------------------------------------------------- /src/main/proto/com/google/android/apps/common/testing/accessibility/framework/uielement/AccessibilityHierarchy.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | syntax = "proto2"; 18 | 19 | package com.google.android.apps.common.testing.accessibility.framework.uielement.proto; 20 | 21 | import "com/google/android/apps/common/testing/accessibility/framework/uielement/AndroidFramework.proto"; 22 | 23 | option java_package = "com.google.android.apps.common.testing.accessibility.framework.uielement.proto"; 24 | option java_outer_classname = "AccessibilityHierarchyProtos"; 25 | 26 | // Wire format for an AccessibilityHierarchy 27 | // Next index: 6 28 | message AccessibilityHierarchyProto { 29 | optional DeviceStateProto device_state = 1; 30 | repeated WindowHierarchyElementProto windows = 2; 31 | optional int32 active_window_id = 3 [default = -1]; 32 | optional ViewElementClassNamesProto view_element_class_names = 4; 33 | optional AccessibilityHierarchyOriginProto origin = 5; 34 | } 35 | 36 | // Wire format for ViewElementClassNames 37 | // Next index: 2 38 | message ViewElementClassNamesProto { 39 | map class_name = 1; 40 | } 41 | 42 | // Wire format for a DeviceState 43 | // Next index: 6 44 | message DeviceStateProto { 45 | optional DisplayInfoProto default_display_info = 1; 46 | optional int32 sdk_version = 2; 47 | 48 | // An IETF BCP 47 language tag representing the locale at the time the app 49 | // was launched. 50 | optional string locale = 3; 51 | optional float font_scale = 4; 52 | optional bool feature_watch = 5; 53 | } 54 | 55 | // Wire format for a DisplayInfo 56 | // Next index = 3; 57 | message DisplayInfoProto { 58 | optional DisplayInfoMetricsProto metrics_without_decoration = 1; 59 | optional DisplayInfoMetricsProto real_metrics = 2; 60 | } 61 | 62 | // Wire format for a DisplayInfo.Metrics 63 | // Next index: 8 64 | message DisplayInfoMetricsProto { 65 | optional float density = 1; 66 | optional float scaled_density = 2; 67 | optional float x_dpi = 3; 68 | optional float y_dpi = 4; 69 | optional int32 density_dpi = 5; 70 | optional int32 height_pixels = 6; 71 | optional int32 width_pixels = 7; 72 | } 73 | 74 | // Wire format for a WindowHierarchyElement 75 | // Next index: 12 76 | message WindowHierarchyElementProto { 77 | // Bookkeeping 78 | optional int32 id = 1 [default = -1]; 79 | optional int32 parent_id = 2 [default = -1]; 80 | repeated int32 child_ids = 3; 81 | repeated ViewHierarchyElementProto views = 4; 82 | 83 | // Window properties 84 | optional int32 window_id = 5; 85 | optional int32 layer = 6; 86 | optional int32 type = 7; 87 | optional bool focused = 8; 88 | optional bool accessibility_focused = 9; 89 | optional bool active = 10; 90 | optional RectProto bounds_in_screen = 11; 91 | } 92 | 93 | // Wire format for a ViewHierarchyElement 94 | // Next index: 45 95 | message ViewHierarchyElementProto { 96 | // Bookkeeping 97 | optional int32 id = 1 [default = -1]; 98 | optional int32 parent_id = 2 [default = -1]; 99 | repeated int32 child_ids = 3; 100 | 101 | // View properties 102 | optional string package_name = 4; 103 | optional string class_name = 5; 104 | optional string resource_name = 6; 105 | optional string test_tag = 44; 106 | optional CharSequenceProto content_description = 7; 107 | optional CharSequenceProto text = 8; 108 | optional CharSequenceProto state_description = 43; 109 | optional bool important_for_accessibility = 9; 110 | optional bool visible_to_user = 10; 111 | optional bool clickable = 11; 112 | optional bool long_clickable = 12; 113 | optional bool focusable = 13; 114 | optional bool editable = 14; 115 | optional bool scrollable = 15; 116 | optional bool can_scroll_forward = 16; 117 | optional bool can_scroll_backward = 17; 118 | optional bool checkable = 18; 119 | optional bool has_touch_delegate = 19; 120 | optional RectProto bounds_in_screen = 20; 121 | optional float text_size = 21; 122 | optional int32 text_color = 22; 123 | optional int32 background_drawable_color = 23; 124 | optional int32 typeface_style = 24; 125 | optional bool enabled = 25; 126 | optional int64 labeled_by_id = 26; 127 | optional int32 nonclipped_height = 27; 128 | optional int32 nonclipped_width = 28; 129 | optional bool checked = 29; 130 | optional string accessibility_class_name = 30; 131 | optional int64 accessibility_traversal_before_id = 31; 132 | optional int64 accessibility_traversal_after_id = 32; 133 | 134 | // Integers that identify class names in ViewElementClassNamesProto 135 | repeated int32 superclasses = 33; 136 | optional int32 drawing_order = 34; 137 | repeated RectProto touch_delegate_bounds = 35; 138 | 139 | // Accessibility actions exposed by the view. 140 | repeated ViewHierarchyActionProto actions = 36; 141 | optional LayoutParamsProto layout_params = 37; 142 | optional CharSequenceProto hint_text = 38; 143 | optional int32 hint_text_color = 39; 144 | repeated RectProto text_character_locations = 40; 145 | optional int32 text_size_unit = 41; 146 | optional bool screen_reader_focusable = 42; 147 | } 148 | 149 | // Wire format for a ViewHierarchyAction 150 | // Next index: 3 151 | message ViewHierarchyActionProto { 152 | optional int32 action_id = 1; 153 | optional string action_label = 2; 154 | } 155 | 156 | // Original data source from which the AccessibilityHierarchy was constructed. 157 | // Next index: 5 158 | enum AccessibilityHierarchyOriginProto { 159 | ORIGIN_UNSPECIFIED = 0; 160 | ORIGIN_VIEWS = 1; 161 | ORIGIN_ACCESSIBILITY_NODE_INFOS = 2; 162 | ORIGIN_ACCESSIBILITY_NODE_INFOS_AND_VIEWS = 4; 163 | ORIGIN_WINDOW_LIST = 3; 164 | } 165 | -------------------------------------------------------------------------------- /src/main/proto/com/google/android/apps/common/testing/accessibility/framework/uielement/AndroidFramework.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | syntax = "proto2"; 18 | 19 | package com.google.android.apps.common.testing.accessibility.framework.uielement.proto; 20 | 21 | option java_package = "com.google.android.apps.common.testing.accessibility.framework.uielement.proto"; 22 | option java_outer_classname = "AndroidFrameworkProtos"; 23 | option objc_class_prefix = "AXEP"; 24 | 25 | // Proto representation of Rect 26 | // Next index: 5 27 | message RectProto { 28 | optional int32 left = 1; 29 | optional int32 top = 2; 30 | optional int32 right = 3; 31 | optional int32 bottom = 4; 32 | } 33 | 34 | // Proto representation of a CharSequence 35 | // Next index: 3 36 | message CharSequenceProto { 37 | optional string text = 1; 38 | repeated SpanProto span = 2; 39 | } 40 | 41 | // Proto representation of a Spanned 42 | // Next index: 10 43 | message SpanProto { 44 | optional int32 start = 1; 45 | optional int32 end = 2; 46 | optional int32 flags = 3; 47 | optional SpanType type = 4; 48 | optional string url = 5; 49 | optional string span_class_name = 6; 50 | optional int32 style = 7; 51 | optional int32 background_color = 8; 52 | optional int32 foreground_color = 9; 53 | 54 | // Span types used by the test framework 55 | // Next index: 7 56 | enum SpanType { 57 | UNKNOWN = 0; 58 | CLICKABLE = 1; 59 | URL = 2; 60 | STYLE = 3; 61 | UNDERLINE = 4; 62 | BACKGROUND_COLOR = 5; 63 | FOREGROUND_COLOR = 6; 64 | } 65 | } 66 | 67 | // Proto representation of LayoutParams 68 | // Next index: 3 69 | message LayoutParamsProto { 70 | optional int32 width = 1; 71 | optional int32 height = 2; 72 | } 73 | --------------------------------------------------------------------------------