├── AccessibilityInsightsForAndroidService ├── app │ ├── .gitignore │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── blue_launcher_background.xml │ │ │ │ │ └── strings.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── blue_launcher.png │ │ │ │ │ └── blue_launcher_foreground.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── blue_launcher.png │ │ │ │ │ └── blue_launcher_foreground.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── blue_launcher.png │ │ │ │ │ └── blue_launcher_foreground.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── blue_launcher.png │ │ │ │ │ └── blue_launcher_foreground.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── blue_launcher.png │ │ │ │ │ └── blue_launcher_foreground.png │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ └── blue_launcher.xml │ │ │ │ └── xml │ │ │ │ │ └── accessibility_insights_service.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── microsoft │ │ │ │ │ └── accessibilityinsightsforandroidservice │ │ │ │ │ ├── RunnableFunction.java │ │ │ │ │ ├── DateProvider.java │ │ │ │ │ ├── ScanException.java │ │ │ │ │ ├── ImageFormatException.java │ │ │ │ │ ├── RequestFulfiller.java │ │ │ │ │ ├── ATFAScannerFactory.java │ │ │ │ │ ├── ByteArrayOutputStreamProvider.java │ │ │ │ │ ├── UIThreadRunner.java │ │ │ │ │ ├── BitmapProvider.java │ │ │ │ │ ├── AxeRectProvider.java │ │ │ │ │ ├── ViewChangedException.java │ │ │ │ │ ├── ResultsV2Container.java │ │ │ │ │ ├── RequestReaderFactory.java │ │ │ │ │ ├── StackTrace.java │ │ │ │ │ ├── OnScreenshotAvailableProvider.java │ │ │ │ │ ├── OffsetHelper.java │ │ │ │ │ ├── DeviceConfig.java │ │ │ │ │ ├── UnrecognizedRequestFulfiller.java │ │ │ │ │ ├── MediaProjectionHolder.java │ │ │ │ │ ├── AxeImageFactory.java │ │ │ │ │ ├── ThreadSafeSwapper.java │ │ │ │ │ ├── NodeViewBuilderFactory.java │ │ │ │ │ ├── TabStopsRequestFulfiller.java │ │ │ │ │ ├── DisplayMetricsHelper.java │ │ │ │ │ ├── RootNodeFinder.java │ │ │ │ │ ├── AxeRunnerFactory.java │ │ │ │ │ ├── OrderedValue.java │ │ │ │ │ ├── RequestReader.java │ │ │ │ │ ├── EventHelper.java │ │ │ │ │ ├── WorkManagerHolder.java │ │ │ │ │ ├── AxeScanner.java │ │ │ │ │ ├── Logger.java │ │ │ │ │ ├── FocusVisualizationStateManager.java │ │ │ │ │ ├── AxeScannerFactory.java │ │ │ │ │ ├── DeviceOrientationHandler.java │ │ │ │ │ ├── DeviceConfigFactory.java │ │ │ │ │ ├── LayoutParamGenerator.java │ │ │ │ │ ├── AxeDeviceFactory.java │ │ │ │ │ ├── ScreenshotActivity.java │ │ │ │ │ ├── ConfigRequestFulfiller.java │ │ │ │ │ ├── AxeContextFactory.java │ │ │ │ │ ├── ScreenshotAxeImage.java │ │ │ │ │ ├── FocusVisualizationCanvas.java │ │ │ │ │ ├── AccessibilityNodeInfoQueueBuilder.java │ │ │ │ │ ├── SynchronizedRequestDispatcher.java │ │ │ │ │ ├── ResultsV2ContainerSerializer.java │ │ │ │ │ ├── ATFAResultsSerializer.java │ │ │ │ │ ├── FocusElementLine.java │ │ │ │ │ ├── ATFAScanner.java │ │ │ │ │ ├── RequestDispatcher.java │ │ │ │ │ ├── AccessibilityEventDispatcher.java │ │ │ │ │ ├── NodeViewBuilder.java │ │ │ │ │ ├── FocusElementHighlight.java │ │ │ │ │ ├── ScreenshotController.java │ │ │ │ │ ├── FocusVisualizer.java │ │ │ │ │ ├── AccessibilityInsightsContentProvider.java │ │ │ │ │ ├── TempFileProvider.java │ │ │ │ │ ├── AxeViewsFactory.java │ │ │ │ │ ├── ResultV2RequestFulfiller.java │ │ │ │ │ └── FocusVisualizerController.java │ │ │ └── AndroidManifest.xml │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── microsoft │ │ │ └── accessibilityinsightsforandroidservice │ │ │ ├── DisplayMetricsStub.java │ │ │ ├── AxeRunnerFactoryTest.java │ │ │ ├── BoundingRectProviderTest.java │ │ │ ├── ATFAScannerFactoryTest.java │ │ │ ├── ByteArrayOutputStreamProviderTest.java │ │ │ ├── AxeScannerFactoryTest.java │ │ │ ├── AxeImageFactoryTest.java │ │ │ ├── NodeViewBuilderFactoryTest.java │ │ │ ├── DeviceConfigTest.java │ │ │ ├── TabStopsRequestFulfillerTest.java │ │ │ ├── OffsetHelperTest.java │ │ │ ├── UnrecognizedRequestFulfillerTest.java │ │ │ ├── BitmapProviderTest.java │ │ │ ├── DeviceOrientationHandlerTest.java │ │ │ ├── AxeDeviceFactoryTest.java │ │ │ ├── AxeScannerTest.java │ │ │ ├── FocusVisualizationStateManagerTest.java │ │ │ ├── UIThreadRunnerTest.java │ │ │ ├── LayoutParamGeneratorTest.java │ │ │ ├── AxeContextFactoryTest.java │ │ │ ├── RequestReaderTest.java │ │ │ ├── DisplayMetricsHelperTest.java │ │ │ ├── ThreadSafeSwapperTest.java │ │ │ ├── DeviceConfigFactoryTest.java │ │ │ ├── RootNodeFinderTest.java │ │ │ ├── FocusVisualizerStylesTest.java │ │ │ ├── EventHelperTest.java │ │ │ ├── FocusVisualizationCanvasTest.java │ │ │ ├── ScreenshotAxeImageTest.java │ │ │ ├── LoggerTest.java │ │ │ ├── ConfigRequestFulfillerTest.java │ │ │ └── AccessibilityNodeInfoQueueBuilderTest.java │ └── proguard-rules.pro ├── .idea │ ├── .name │ ├── codeStyles │ │ ├── codeStyleConfig.xml │ │ └── Project.xml │ ├── compiler.xml │ ├── vcs.xml │ ├── google-java-format.xml │ ├── misc.xml │ └── gradle.xml ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── settings.gradle ├── .gitignore ├── cgmanifest.json ├── gradle.properties ├── gradlew.bat └── build.gradle ├── npm-wrapper ├── .gitignore ├── index.d.ts ├── index.js ├── package.json └── README.md ├── .github ├── CODEOWNERS ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── general_question.md │ ├── feature_request.md │ └── bug_report.md ├── pull_request_template.md └── workflows │ ├── combine-dependabot-prs.yml │ └── codeql-analysis.yml ├── .gitattributes ├── CODE_OF_CONDUCT.md ├── guardian └── SDL │ └── .gdnbaselines ├── CONTRIBUTING.md ├── pipeline ├── e2e-emulator-template.yaml ├── infer-version-code-from-version-name.js ├── verify-clean-lockfile.js ├── scripts │ └── setup-emulator.sh └── validation-build.yaml ├── LICENSE └── SECURITY.md /AccessibilityInsightsForAndroidService/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/.idea/.name: -------------------------------------------------------------------------------- 1 | Accessibility Insights For Android Service -------------------------------------------------------------------------------- /npm-wrapper/.gitignore: -------------------------------------------------------------------------------- 1 | # These files are lifted from ../ during release builds 2 | NOTICE.* 3 | LICENSE 4 | *.apk 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # default owners for everything in the repository. 2 | * @Microsoft/accessibility-insights-code-owners 3 | -------------------------------------------------------------------------------- /npm-wrapper/index.d.ts: -------------------------------------------------------------------------------- 1 | export const apkPath: string; 2 | export const apkVersionName: string; 3 | export const noticePath: string; 4 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/values/blue_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/accessibility-insights-for-android-service/main/AccessibilityInsightsForAndroidService/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /npm-wrapper/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apkPath: `${__dirname}/AccessibilityInsightsforAndroidService.apk`, 3 | apkVersionName: require('./package.json').version, 4 | noticePath: `${__dirname}/NOTICE.html`, 5 | }; 6 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-hdpi/blue_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/accessibility-insights-for-android-service/main/AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-hdpi/blue_launcher.png -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-mdpi/blue_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/accessibility-insights-for-android-service/main/AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-mdpi/blue_launcher.png -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/.idea/google-java-format.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-xhdpi/blue_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/accessibility-insights-for-android-service/main/AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-xhdpi/blue_launcher.png -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-xxhdpi/blue_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/accessibility-insights-for-android-service/main/AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-xxhdpi/blue_launcher.png -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-xxxhdpi/blue_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/accessibility-insights-for-android-service/main/AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-xxxhdpi/blue_launcher.png -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-hdpi/blue_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/accessibility-insights-for-android-service/main/AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-hdpi/blue_launcher_foreground.png -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-mdpi/blue_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/accessibility-insights-for-android-service/main/AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-mdpi/blue_launcher_foreground.png -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-xhdpi/blue_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/accessibility-insights-for-android-service/main/AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-xhdpi/blue_launcher_foreground.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | # Gradle always writes its lockfile with OS-default line endings 4 | # and can't be configured to use LF-only 5 | gradle.lockfile text -eol 6 | 7 | # Denote all files that are truly binary and should not be modified. 8 | *.png binary 9 | *.jar binary -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-xxhdpi/blue_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/accessibility-insights-for-android-service/main/AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-xxhdpi/blue_launcher_foreground.png -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-xxxhdpi/blue_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/accessibility-insights-for-android-service/main/AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-xxxhdpi/blue_launcher_foreground.png -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Aug 24 10:57:42 EDT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-all.zip 7 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/settings.gradle: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | include ':app' 5 | rootProject.name='Accessibility Insights For Android Service' 6 | 7 | // This is easier to manage and will become the default in Gradle 7 8 | enableFeaturePreview('ONE_LOCKFILE_PER_PROJECT') 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/AccessibilityInsightsForAndroidService/app/" 5 | labels: 6 | - "category: engineering" 7 | - "dependencies" 8 | commit-message: 9 | prefix: "chore" 10 | include: "scope" 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/mipmap-anydpi-v26/blue_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/RunnableFunction.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | @FunctionalInterface 7 | public interface RunnableFunction { 8 | void run(); 9 | } 10 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/DateProvider.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import java.util.Date; 7 | 8 | public class DateProvider { 9 | public Date get() { 10 | return new Date(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ScanException.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | public class ScanException extends Exception { 7 | 8 | public ScanException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/.gitignore: -------------------------------------------------------------------------------- 1 | .cxx 2 | .DS_Store 3 | .externalNativeBuild 4 | .gradle 5 | *.iml 6 | /.idea/assetWizardSettings.xml 7 | /.idea/caches 8 | /.idea/jarRepositories.xml 9 | /.idea/libraries 10 | /.idea/modules.xml 11 | /.idea/navEditor.xml 12 | /.idea/workspace.xml 13 | /.idea/deploymentTargetDropDown.xml 14 | /.idea/runConfigurations.xml 15 | /build 16 | /captures 17 | /local.properties 18 | libs 19 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ImageFormatException.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | class ImageFormatException extends Exception { 7 | public ImageFormatException(String message) { 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/RequestFulfiller.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.os.CancellationSignal; 7 | 8 | public interface RequestFulfiller { 9 | String fulfillRequest(CancellationSignal cancellationSignal) throws Exception; 10 | } 11 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/cgmanifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/component-detection-manifest.json", 3 | "Registrations": [ 4 | { 5 | "component": { 6 | "type": "git", 7 | "git": { 8 | "repositoryUrl": "https://github.com/googlecodelabs/android-accessibility", 9 | "commitHash": "d78994bf77b2438f4bc31e557d4c12e35a0219fb" 10 | } 11 | } 12 | } 13 | ], 14 | "Version": 1 15 | } 16 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ATFAScannerFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.content.Context; 7 | 8 | public class ATFAScannerFactory { 9 | public static ATFAScanner createATFAScanner(Context context) { 10 | return new ATFAScanner(context); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ByteArrayOutputStreamProvider.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | 8 | public class ByteArrayOutputStreamProvider { 9 | 10 | public ByteArrayOutputStream get() { 11 | return new ByteArrayOutputStream(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/UIThreadRunner.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.os.Handler; 7 | import android.os.Looper; 8 | 9 | public class UIThreadRunner { 10 | public void run(Runnable runnable) { 11 | new Handler(Looper.getMainLooper()).post(runnable); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/DisplayMetricsStub.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.util.DisplayMetrics; 7 | 8 | public class DisplayMetricsStub extends DisplayMetrics { 9 | public int densityDpi = 111; 10 | public int heightPixels = 222; 11 | public int widthPixels = 333; 12 | } 13 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/BitmapProvider.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Bitmap; 7 | 8 | public class BitmapProvider { 9 | 10 | public Bitmap createBitmap(int width, int height, Bitmap.Config config) { 11 | return Bitmap.createBitmap(width, height, config); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/AxeRectProvider.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import com.deque.axe.android.wrappers.AxeRect; 7 | 8 | public class AxeRectProvider { 9 | 10 | public AxeRect createAxeRect(int left, int right, int top, int bottom) { 11 | return new AxeRect(left, right, top, bottom); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general_question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General Question 3 | about: This is a template for people to ask questions that don't fit into any other issue categories 4 | title: "" 5 | labels: question 6 | assignees: "" 7 | --- 8 | 9 | **We encourage tool related questions to be asked on stackoverflow.com and tagged with `accessibility-insights`.** 10 | 11 | ## Your question here 12 | 13 | 17 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ViewChangedException.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | class ViewChangedException extends Exception { 7 | public ViewChangedException() { 8 | this(""); 9 | } 10 | 11 | public ViewChangedException(String additionalMessage) { 12 | super("The view hierarchy changed while building the AxeView tree. " + additionalMessage); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Microsoft Open Source Code of Conduct 7 | 8 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 9 | 10 | Resources: 11 | 12 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 13 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 14 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 15 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ResultsV2Container.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import com.deque.axe.android.AxeResult; 7 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 8 | import java.util.List; 9 | 10 | public class ResultsV2Container { 11 | public List ATFAResults; 12 | public AxeResult AxeResult; 13 | } 14 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/RequestReaderFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import java.io.BufferedReader; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | 10 | public class RequestReaderFactory { 11 | 12 | public RequestReader createRequestReader(InputStream inputStream) { 13 | return new RequestReader(new BufferedReader(new InputStreamReader(inputStream))); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/StackTrace.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import java.io.PrintWriter; 7 | import java.io.StringWriter; 8 | 9 | public class StackTrace { 10 | 11 | public static String getStackTrace(Exception e) { 12 | StringWriter stringWriter = new StringWriter(); 13 | PrintWriter printWriter = new PrintWriter(stringWriter); 14 | e.printStackTrace(printWriter); 15 | return stringWriter.toString(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/OnScreenshotAvailableProvider.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Bitmap; 7 | import android.util.DisplayMetrics; 8 | import java.util.function.Consumer; 9 | 10 | public class OnScreenshotAvailableProvider { 11 | public OnScreenshotAvailable getOnScreenshotAvailable( 12 | DisplayMetrics metrics, BitmapProvider bitmapProvider, Consumer bitmapConsumer) { 13 | return new OnScreenshotAvailable(metrics, bitmapProvider, bitmapConsumer); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/OffsetHelper.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.view.View; 7 | 8 | public class OffsetHelper { 9 | public static int getYOffset(View view) { 10 | int offset = 0; 11 | int resourceId = view.getResources().getIdentifier("status_bar_height", "dimen", "android"); 12 | if (resourceId > 0) { 13 | offset = view.getResources().getDimensionPixelSize(resourceId); 14 | } 15 | // divide by 2 to center 16 | offset = offset / 2; 17 | return offset; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | --- 8 | 9 | ## Is your feature request related to a problem? Please describe. 10 | 11 | 12 | ## Describe the desired outcome 13 | 14 | 15 | ## Describe alternatives you've considered 16 | 17 | 18 | ## Additional context 19 | 20 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/DeviceConfig.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import com.deque.axe.android.utils.JsonSerializable; 7 | 8 | public class DeviceConfig implements JsonSerializable { 9 | public final String deviceName; 10 | public final String packageName; 11 | public final String serviceVersion; 12 | 13 | public DeviceConfig(String deviceName, String packageName, String serviceVersion) { 14 | this.deviceName = deviceName; 15 | this.packageName = packageName; 16 | this.serviceVersion = serviceVersion; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/UnrecognizedRequestFulfiller.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.os.CancellationSignal; 7 | 8 | public class UnrecognizedRequestFulfiller implements RequestFulfiller { 9 | private final String requestMethod; 10 | 11 | public UnrecognizedRequestFulfiller(String requestMethod) { 12 | this.requestMethod = requestMethod; 13 | } 14 | 15 | public String fulfillRequest(CancellationSignal cancellationSignal) { 16 | throw new RuntimeException("Unrecognized request: " + requestMethod); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /guardian/SDL/.gdnbaselines: -------------------------------------------------------------------------------- 1 | { 2 | "version": "latest", 3 | "baselines": { 4 | "default": { 5 | "name": "default", 6 | "createdDate": "2023-03-30 17:45:22Z", 7 | "lastUpdatedDate": "2023-03-30 17:47:53Z" 8 | } 9 | }, 10 | "results": { 11 | "b3736a918ff8d688843a2c76bfce1661c484e260144140c9df23f1147da102c0": { 12 | "signature": "b3736a918ff8d688843a2c76bfce1661c484e260144140c9df23f1147da102c0", 13 | "alternativeSignatures": [], 14 | "target": "spotbugs.xml", 15 | "memberOf": [ 16 | "default" 17 | ], 18 | "tool": "spotbugs", 19 | "ruleId": "gdn.unknownFormatResult", 20 | "justification": null, 21 | "createdDate": "2023-03-30 17:45:22Z", 22 | "expirationDate": null, 23 | "type": null 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 18 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/AxeRunnerFactoryTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import org.junit.Assert; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.junit.MockitoJUnitRunner; 11 | 12 | @RunWith(MockitoJUnitRunner.class) 13 | public class AxeRunnerFactoryTest { 14 | 15 | AxeRunnerFactory testSubject; 16 | 17 | @Before 18 | public void prepare() { 19 | testSubject = new AxeRunnerFactory(); 20 | } 21 | 22 | @Test 23 | public void axeRunnerIsNotNull() { 24 | Assert.assertNotNull(testSubject.createAxeRunner()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/BoundingRectProviderTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import org.junit.Assert; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.junit.MockitoJUnitRunner; 11 | 12 | @RunWith(MockitoJUnitRunner.class) 13 | public class BoundingRectProviderTest { 14 | 15 | AxeRectProvider testSubject; 16 | 17 | @Before 18 | public void prepare() { 19 | testSubject = new AxeRectProvider(); 20 | } 21 | 22 | @Test 23 | public void boundingRectExists() { 24 | Assert.assertNotNull(testSubject.createAxeRect(0, 1, 2, 3)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/ATFAScannerFactoryTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.content.Context; 7 | import org.junit.Assert; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.MockitoJUnitRunner; 13 | 14 | @RunWith(MockitoJUnitRunner.class) 15 | public class ATFAScannerFactoryTest { 16 | @Mock Context contextMock; 17 | 18 | @Before 19 | public void prepare() {} 20 | 21 | @Test 22 | public void atfaScannerExists() { 23 | Assert.assertNotNull(ATFAScannerFactory.createATFAScanner(contextMock)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/MediaProjectionHolder.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.media.projection.MediaProjection; 7 | 8 | public class MediaProjectionHolder { 9 | private static MediaProjection sharedMediaProjection = null; 10 | 11 | public static void cleanUp() { 12 | if (sharedMediaProjection != null) { 13 | sharedMediaProjection.stop(); 14 | sharedMediaProjection = null; 15 | } 16 | } 17 | 18 | public static MediaProjection get() { 19 | return sharedMediaProjection; 20 | } 21 | 22 | public static void set(MediaProjection projection) { 23 | sharedMediaProjection = projection; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## Contributing 7 | 8 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 9 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 10 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 11 | 12 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 13 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 14 | provided by the bot. You will only need to do this once across all repos using our CLA. 15 | 16 | ## Code of Conduct 17 | 18 | Please read through our [Code of Conduct](./CODE_OF_CONDUCT.md) to this project. 19 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/ByteArrayOutputStreamProviderTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import org.junit.Assert; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.junit.MockitoJUnitRunner; 11 | 12 | @RunWith(MockitoJUnitRunner.class) 13 | public class ByteArrayOutputStreamProviderTest { 14 | 15 | ByteArrayOutputStreamProvider testSubject; 16 | 17 | @Before 18 | public void prepare() { 19 | testSubject = new ByteArrayOutputStreamProvider(); 20 | } 21 | 22 | @Test 23 | public void byteArrayOutputStreamExists() { 24 | Assert.assertNotNull(testSubject.get()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | #### Details 2 | 3 | 4 | 5 | ##### Motivation 6 | 7 | 8 | 9 | ##### Context 10 | 11 | 12 | 13 | 14 | 15 | #### Pull request checklist 16 | 17 | 18 | - [ ] Addresses an existing issue: #0000 19 | - [ ] Added/updated relevant unit test(s) 20 | - [ ] Ran `./gradlew fastpass` from `AccessibilityInsightsForAndroidService` 21 | - [ ] PR title _AND_ final merge commit title both start with a semantic tag (`fix:`, `chore:`, `feat(feature-name):`, `refactor:`). 22 | -------------------------------------------------------------------------------- /.github/workflows/combine-dependabot-prs.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | name: "Combine Dependabot PRs" 5 | on: 6 | schedule: 7 | - cron: '0 13 * * 1' 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | jobs: 15 | combine-prs: 16 | if: github.event_name != 'schedule' || github.repository_owner == 'microsoft' 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2.3.3 20 | - uses: maadhattah/combine-dependabot-prs@e4dc7e045b018ee1e963a1a67bccbbf8ff3b176f 21 | with: 22 | branchPrefix: "dependabot" 23 | mustBeGreen: true 24 | combineBranchName: combined-prs-${{ github.run_id }} 25 | ignoreLabel: "pr: do not combine" 26 | baseBranch: "main" 27 | openPR: true 28 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/AxeImageFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Bitmap; 7 | import com.deque.axe.android.colorcontrast.AxeImage; 8 | 9 | public class AxeImageFactory { 10 | private ByteArrayOutputStreamProvider byteArrayOutputStreamProvider; 11 | 12 | public AxeImageFactory(ByteArrayOutputStreamProvider byteArrayOutputStreamProvider) { 13 | this.byteArrayOutputStreamProvider = byteArrayOutputStreamProvider; 14 | } 15 | 16 | public AxeImage createAxeImage(Bitmap screenshot) { 17 | if (screenshot == null) { 18 | return null; 19 | } 20 | 21 | return new ScreenshotAxeImage(screenshot, byteArrayOutputStreamProvider); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ThreadSafeSwapper.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | public class ThreadSafeSwapper { 7 | private final Object lock_object = new Object(); 8 | private T currentObject; 9 | 10 | public T swap(T newObject) { 11 | synchronized (lock_object) { 12 | T oldObject = currentObject; 13 | currentObject = newObject; 14 | return oldObject; 15 | } 16 | } 17 | 18 | public boolean setIfCurrentlyNull(T newObject) { 19 | synchronized (lock_object) { 20 | T oldObject = currentObject; 21 | if (oldObject != null) { 22 | return false; 23 | } 24 | 25 | currentObject = newObject; 26 | return true; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/NodeViewBuilderFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.view.accessibility.AccessibilityNodeInfo; 7 | import com.deque.axe.android.AxeView; 8 | import java.util.List; 9 | 10 | public class NodeViewBuilderFactory { 11 | // The reason we need something called BuilderFactory is because axe-android requires us to 12 | // implement the AxeView.builder interface, which we do in NodeViewBuilder. This is a factory 13 | // for that class. 14 | 15 | public NodeViewBuilder createNodeViewBuilder( 16 | AccessibilityNodeInfo node, List children, AxeView labeledBy) { 17 | return new NodeViewBuilder(node, children, labeledBy, new AxeRectProvider()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /npm-wrapper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "accessibility-insights-for-android-service-bin", 3 | "version": "0.0.0-set-by-release-build", 4 | "description": "Wrapper package containing a pre-built APK version of Accessibility Insights for Android Service", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "files": [ 8 | "AccessibilityInsightsforAndroidService.apk", 9 | "index.d.ts", 10 | "index.js", 11 | "LICENSE", 12 | "NOTICE.html", 13 | "package.json", 14 | "README.md" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/microsoft/accessibility-insights-for-android-service.git" 19 | }, 20 | "author": "Microsoft", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/microsoft/accessibility-insights-for-android-service/issues" 24 | }, 25 | "homepage": "https://github.com/microsoft/accessibility-insights-for-android-service#readme" 26 | } 27 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | # Add project specific ProGuard rules here. 5 | # You can control the set of applied configuration files using the 6 | # proguardFiles setting in build.gradle. 7 | # 8 | # For more details, see 9 | # http://developer.android.com/guide/developing/tools/proguard.html 10 | 11 | # If your project uses WebView with JS, uncomment the following 12 | # and specify the fully qualified class name to the JavaScript interface 13 | # class: 14 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 15 | # public *; 16 | #} 17 | 18 | # Uncomment this to preserve the line number information for 19 | # debugging stack traces. 20 | #-keepattributes SourceFile,LineNumberTable 21 | 22 | # If you keep the line number information, uncomment this to 23 | # hide the original source file name. 24 | #-renamesourcefileattribute SourceFile 25 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/AxeScannerFactoryTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.util.DisplayMetrics; 7 | import org.junit.Assert; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.MockitoJUnitRunner; 13 | 14 | @RunWith(MockitoJUnitRunner.class) 15 | public class AxeScannerFactoryTest { 16 | 17 | @Mock DeviceConfigFactory deviceConfigFactoryMock; 18 | @Mock DisplayMetrics displayMetricsMock; 19 | 20 | @Before 21 | public void prepare() {} 22 | 23 | @Test 24 | public void axeScannerExists() { 25 | Assert.assertNotNull( 26 | AxeScannerFactory.createAxeScanner(deviceConfigFactoryMock, () -> displayMetricsMock)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/TabStopsRequestFulfiller.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.os.CancellationSignal; 7 | 8 | public class TabStopsRequestFulfiller implements RequestFulfiller { 9 | private final FocusVisualizationStateManager focusVisualizationStateManager; 10 | private final boolean requestValue; 11 | 12 | public TabStopsRequestFulfiller( 13 | FocusVisualizationStateManager focusVisualizationStateManager, boolean requestValue) { 14 | this.focusVisualizationStateManager = focusVisualizationStateManager; 15 | this.requestValue = requestValue; 16 | } 17 | 18 | @Override 19 | public String fulfillRequest(CancellationSignal cancellationSignal) { 20 | focusVisualizationStateManager.setState(requestValue); 21 | return ""; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pipeline/e2e-emulator-template.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | apiLevel: null 3 | 4 | steps: 5 | - task: DownloadPipelineArtifact@2 6 | inputs: 7 | artifact: debugApk 8 | path: $(system.defaultWorkingDirectory) 9 | 10 | - task: Bash@3 11 | displayName: Setup Android emulator 12 | inputs: 13 | filePath: 'pipeline/scripts/setup-emulator.sh' 14 | env: 15 | ANDROID_API_LEVEL: ${{ parameters.apiLevel }} 16 | 17 | - task: Bash@3 18 | displayName: Test Config Command 19 | inputs: 20 | targetType: 'inline' 21 | script: | 22 | $ANDROID_HOME/platform-tools/adb shell content read --uri content://com.microsoft.accessibilityinsightsforandroidservice/config 23 | failOnStderr: true 24 | 25 | - task: Bash@3 26 | displayName: Test Result Command 27 | inputs: 28 | targetType: 'inline' 29 | script: | 30 | $ANDROID_HOME/platform-tools/adb shell content read --uri content://com.microsoft.accessibilityinsightsforandroidservice/result 31 | failOnStderr: true 32 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/DisplayMetricsHelper.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.content.Context; 7 | import android.content.res.Resources; 8 | import android.util.DisplayMetrics; 9 | import android.view.Display; 10 | import android.view.WindowManager; 11 | 12 | public class DisplayMetricsHelper { 13 | 14 | public static DisplayMetrics getRealDisplayMetrics(Context context) { 15 | 16 | DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics(); // Default values 17 | 18 | Display display = getDefaultDisplay(context); 19 | 20 | display.getRealMetrics(displayMetrics); 21 | return displayMetrics; 22 | } 23 | 24 | private static Display getDefaultDisplay(Context context) { 25 | return ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/RootNodeFinder.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.view.accessibility.AccessibilityNodeInfo; 7 | 8 | public class RootNodeFinder { 9 | public AccessibilityNodeInfo getRootNodeFromSource(AccessibilityNodeInfo source) { 10 | AccessibilityNodeInfo rootNode = null; 11 | 12 | if (source != null) { 13 | AccessibilityNodeInfo currentNode = source; 14 | 15 | while (true) { 16 | AccessibilityNodeInfo parent = currentNode.getParent(); 17 | 18 | if (parent == null) { 19 | rootNode = currentNode; 20 | break; 21 | } 22 | 23 | if (source != currentNode) { // Don't recycle the source! 24 | currentNode.recycle(); 25 | } 26 | currentNode = parent; 27 | } 28 | } 29 | 30 | return rootNode; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/AxeImageFactoryTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Bitmap; 7 | import org.junit.Assert; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.MockitoJUnitRunner; 13 | 14 | @RunWith(MockitoJUnitRunner.class) 15 | public class AxeImageFactoryTest { 16 | 17 | @Mock ByteArrayOutputStreamProvider byteArrayOutputStreamProviderMock; 18 | @Mock Bitmap screenshotMock; 19 | 20 | AxeImageFactory testSubject; 21 | 22 | @Before 23 | public void prepare() { 24 | testSubject = new AxeImageFactory(byteArrayOutputStreamProviderMock); 25 | } 26 | 27 | @Test 28 | public void axeImageIsNotNull() { 29 | Assert.assertNotNull(testSubject.createAxeImage(screenshotMock)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/AxeRunnerFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import com.deque.axe.android.Axe; 7 | import com.deque.axe.android.AxeConf; 8 | import com.deque.axe.android.constants.AxeStandard; 9 | 10 | public class AxeRunnerFactory { 11 | // AxeConf.removeStandard is marked deprecated, but is still suggested in the axe-android docs as 12 | // the recommended way to constrain which standards we scan against. 13 | // 14 | // https://github.com/dequelabs/axe-android/issues/145 tracks the request for a non-deprecated 15 | // replacement option. 16 | @SuppressWarnings("deprecation") 17 | public Axe createAxeRunner() { 18 | AxeConf axeConf = new AxeConf(); 19 | 20 | axeConf.removeStandard(AxeStandard.BEST_PRACTICE); 21 | axeConf.removeStandard(AxeStandard.PLATFORM); 22 | return new Axe(axeConf); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/OrderedValue.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import java.util.Objects; 7 | 8 | public class OrderedValue implements Comparable> { 9 | public final long order; 10 | public final T value; 11 | 12 | public OrderedValue(T value, long order) { 13 | this.value = value; 14 | this.order = order; 15 | } 16 | 17 | @Override 18 | public int compareTo(OrderedValue other) { 19 | return Long.compare(this.order, other.order); 20 | } 21 | 22 | @Override 23 | public boolean equals(Object o) { 24 | if (this == o) return true; 25 | if (o == null || getClass() != o.getClass()) return false; 26 | OrderedValue that = (OrderedValue) o; 27 | return order == that.order; 28 | } 29 | 30 | @Override 31 | public int hashCode() { 32 | return Objects.hash(order); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/xml/accessibility_insights_service.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/NodeViewBuilderFactoryTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.view.accessibility.AccessibilityNodeInfo; 7 | import com.deque.axe.android.AxeView; 8 | import org.junit.Assert; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.mockito.Mock; 13 | import org.mockito.junit.MockitoJUnitRunner; 14 | 15 | @RunWith(MockitoJUnitRunner.class) 16 | public class NodeViewBuilderFactoryTest { 17 | 18 | @Mock AccessibilityNodeInfo node; 19 | @Mock AxeView view; 20 | 21 | NodeViewBuilderFactory testSubject; 22 | 23 | @Before 24 | public void prepare() { 25 | testSubject = new NodeViewBuilderFactory(); 26 | } 27 | 28 | @Test 29 | public void nodeViewIsNotNull() { 30 | Assert.assertNotNull(testSubject.createNodeViewBuilder(node, null, null)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/RequestReader.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import java.io.BufferedReader; 7 | import java.io.IOException; 8 | 9 | public class RequestReader { 10 | 11 | private BufferedReader reader; 12 | private static final int maxLineLength = 256; 13 | 14 | public RequestReader(BufferedReader reader) { 15 | this.reader = reader; 16 | } 17 | 18 | public String readRequest() throws IOException { 19 | 20 | StringBuffer buffer = new StringBuffer(); 21 | int intC = reader.read(); 22 | while (intC != -1) { 23 | char c = (char) intC; 24 | if (c == '\n') { 25 | break; 26 | } 27 | if (buffer.length() >= maxLineLength) { 28 | throw new IOException("input too long"); 29 | } 30 | buffer.append(c); 31 | intC = reader.read(); 32 | } 33 | 34 | return buffer.toString(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/EventHelper.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.view.accessibility.AccessibilityNodeInfo; 7 | 8 | public class EventHelper { 9 | private final ThreadSafeSwapper swapper; 10 | 11 | public EventHelper(ThreadSafeSwapper swapper) { 12 | this.swapper = swapper; 13 | } 14 | 15 | public void recordEvent(AccessibilityNodeInfo source) { 16 | if (source != null) { 17 | AccessibilityNodeInfo lastSource = swapper.swap(source); 18 | if (lastSource != null) { 19 | lastSource.recycle(); 20 | } 21 | } 22 | } 23 | 24 | public AccessibilityNodeInfo claimLastSource() { 25 | return swapper.swap(null); 26 | } 27 | 28 | public boolean restoreLastSource(AccessibilityNodeInfo previousSource) { 29 | return swapper.setIfCurrentlyNull(previousSource); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/DeviceConfigTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import org.junit.Assert; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | 10 | public class DeviceConfigTest { 11 | String deviceName = "test-device-name"; 12 | String packageName = "test-package-name"; 13 | String serviceVersion = "test-service-version"; 14 | 15 | DeviceConfig testSubject; 16 | 17 | @Before 18 | public void prepare() { 19 | testSubject = new DeviceConfig(deviceName, packageName, serviceVersion); 20 | } 21 | 22 | @Test 23 | public void deviceConfigExists() { 24 | Assert.assertNotNull(testSubject); 25 | } 26 | 27 | @Test 28 | public void deviceConfigHasExpectedProperties() { 29 | Assert.assertEquals(deviceName, testSubject.deviceName); 30 | Assert.assertEquals(packageName, testSubject.packageName); 31 | Assert.assertEquals(serviceVersion, testSubject.serviceVersion); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/TabStopsRequestFulfillerTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | import static org.mockito.Mockito.verify; 8 | 9 | import android.os.CancellationSignal; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.mockito.Mock; 13 | import org.mockito.junit.MockitoJUnitRunner; 14 | 15 | @RunWith(MockitoJUnitRunner.class) 16 | public class TabStopsRequestFulfillerTest { 17 | @Mock FocusVisualizationStateManager focusVisualizationStateManager; 18 | @Mock CancellationSignal cancellationSignal; 19 | 20 | TabStopsRequestFulfiller testSubject; 21 | 22 | @Test 23 | public void fulfillRequestSetsTabStopState() { 24 | testSubject = new TabStopsRequestFulfiller(focusVisualizationStateManager, true); 25 | assertEquals("", testSubject.fulfillRequest(cancellationSignal)); 26 | 27 | verify(focusVisualizationStateManager).setState(true); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/WorkManagerHolder.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.content.Context; 7 | import androidx.work.Configuration; 8 | import androidx.work.WorkManager; 9 | import java.util.WeakHashMap; 10 | 11 | public class WorkManagerHolder { 12 | private static final Object LockObject = new Object(); 13 | private static WeakHashMap ContextToManagerMap = 14 | new WeakHashMap(); 15 | 16 | public static WorkManager getWorkManager(Context context) { 17 | synchronized (LockObject) { 18 | WorkManager managerForContext = ContextToManagerMap.get(context); 19 | 20 | if (managerForContext == null) { 21 | WorkManager.initialize(context, new Configuration.Builder().build()); 22 | managerForContext = WorkManager.getInstance(context); 23 | ContextToManagerMap.put(context, managerForContext); 24 | } 25 | return managerForContext; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/OffsetHelperTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.ArgumentMatchers.anyInt; 7 | import static org.mockito.Mockito.when; 8 | 9 | import android.content.res.Resources; 10 | import android.view.View; 11 | import org.junit.Assert; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.MockitoJUnitRunner; 16 | 17 | @RunWith(MockitoJUnitRunner.class) 18 | public class OffsetHelperTest { 19 | 20 | @Mock View viewMock; 21 | @Mock Resources resourcesMock; 22 | 23 | @Test 24 | public void getYOffsetReturnsCenterOfElement() { 25 | when(viewMock.getResources()).thenReturn(resourcesMock); 26 | when(resourcesMock.getIdentifier("status_bar_height", "dimen", "android")).thenReturn(1); 27 | when(resourcesMock.getDimensionPixelSize(anyInt())).thenReturn(10); 28 | 29 | Assert.assertEquals(OffsetHelper.getYOffset(viewMock), 5); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/AxeScanner.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Bitmap; 7 | import android.view.accessibility.AccessibilityNodeInfo; 8 | import com.deque.axe.android.Axe; 9 | import com.deque.axe.android.AxeContext; 10 | import com.deque.axe.android.AxeResult; 11 | 12 | public class AxeScanner { 13 | private final AxeRunnerFactory axeRunnerFactory; 14 | private final AxeContextFactory axeContextFactory; 15 | 16 | public AxeScanner(AxeRunnerFactory axeRunnerFactory, AxeContextFactory axeContextFactory) { 17 | this.axeRunnerFactory = axeRunnerFactory; 18 | this.axeContextFactory = axeContextFactory; 19 | } 20 | 21 | public AxeResult scanWithAxe(AccessibilityNodeInfo rootNode, Bitmap screenshot) 22 | throws ViewChangedException { 23 | final Axe axe = axeRunnerFactory.createAxeRunner(); 24 | final AxeContext axeContext = axeContextFactory.createAxeContext(rootNode, screenshot); 25 | return axe.run(axeContext); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/Logger.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.util.Log; 7 | import androidx.annotation.VisibleForTesting; 8 | 9 | public class Logger { 10 | @VisibleForTesting public static boolean ENABLE_LOGGING = BuildConfig.DEBUG_MODE; 11 | 12 | public static void logVerbose(String tag, String message) { 13 | if (ENABLE_LOGGING) { 14 | Log.v(tag, message); 15 | } 16 | } 17 | 18 | public static void logDebug(String tag, String message) { 19 | if (ENABLE_LOGGING) { 20 | Log.d(tag, message); 21 | } 22 | } 23 | 24 | public static void logError(String tag, String message) { 25 | if (ENABLE_LOGGING) { 26 | Log.e(tag, message); 27 | } 28 | } 29 | 30 | public static void logInfo(String tag, String message) { 31 | if (ENABLE_LOGGING) { 32 | Log.i(tag, message); 33 | } 34 | } 35 | 36 | public static void logWarning(String tag, String message) { 37 | if (ENABLE_LOGGING) { 38 | Log.w(tag, message); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/FocusVisualizationStateManager.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import java.util.ArrayList; 7 | import java.util.function.Consumer; 8 | 9 | public class FocusVisualizationStateManager { 10 | private boolean enabled = false; 11 | private ArrayList> onChangedListeners; 12 | 13 | public FocusVisualizationStateManager() { 14 | onChangedListeners = new ArrayList>(); 15 | } 16 | 17 | public void subscribe(Consumer listener) { 18 | onChangedListeners.add(listener); 19 | } 20 | 21 | public void setState(boolean enabled) { 22 | if (this.enabled == enabled) { 23 | return; 24 | } 25 | 26 | this.enabled = enabled; 27 | this.emitChanged(enabled); 28 | } 29 | 30 | public boolean getState() { 31 | return this.enabled; 32 | } 33 | 34 | private void emitChanged(boolean enabled) { 35 | onChangedListeners.forEach( 36 | listener -> { 37 | listener.accept(enabled); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/AxeScannerFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.util.DisplayMetrics; 7 | import java.util.function.Supplier; 8 | 9 | public class AxeScannerFactory { 10 | public static AxeScanner createAxeScanner( 11 | DeviceConfigFactory deviceConfigFactory, Supplier displayMetricsSupplier) { 12 | final AxeViewsFactory axeViewsFactory = 13 | new AxeViewsFactory(new NodeViewBuilderFactory(), new AccessibilityNodeInfoQueueBuilder()); 14 | final AxeImageFactory axeImageFactory = 15 | new AxeImageFactory(new ByteArrayOutputStreamProvider()); 16 | final AxeDeviceFactory axeDeviceFactory = 17 | new AxeDeviceFactory(deviceConfigFactory, displayMetricsSupplier); 18 | final AxeContextFactory axeContextFactory = 19 | new AxeContextFactory(axeImageFactory, axeViewsFactory, axeDeviceFactory); 20 | final AxeRunnerFactory axeRunnerFactory = new AxeRunnerFactory(); 21 | 22 | return new AxeScanner(axeRunnerFactory, axeContextFactory); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/DeviceOrientationHandler.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import java.util.ArrayList; 7 | import java.util.function.Consumer; 8 | 9 | public class DeviceOrientationHandler { 10 | private int orientation; 11 | private ArrayList> onOrientationChangedListeners; 12 | 13 | public DeviceOrientationHandler(int orientation) { 14 | this.orientation = orientation; 15 | this.onOrientationChangedListeners = new ArrayList<>(); 16 | } 17 | 18 | public void subscribe(Consumer listener) { 19 | this.onOrientationChangedListeners.add(listener); 20 | } 21 | 22 | public void setOrientation(int orientation) { 23 | if (this.orientation == orientation) { 24 | return; 25 | } 26 | 27 | this.orientation = orientation; 28 | this.emitChanged(orientation); 29 | } 30 | 31 | private void emitChanged(int orientation) { 32 | this.onOrientationChangedListeners.forEach( 33 | listener -> { 34 | listener.accept(orientation); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/gradle.properties: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | # Project-wide Gradle settings. 5 | # IDE (e.g. Android Studio) users: 6 | # Gradle settings configured through the IDE *will override* 7 | # any settings specified in this file. 8 | # For more details on how to configure your build environment visit 9 | # http://www.gradle.org/docs/current/userguide/build_environment.html 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx4608m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | # AndroidX package structure to make it clearer which packages are bundled with the 19 | # Android operating system, and which are packaged with your app's APK 20 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 21 | android.useAndroidX=true 22 | # Automatically convert third-party libraries to use AndroidX 23 | android.enableJetifier=true -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | Accessibility Insights for Android Service 23 | This service allows Accessibility Insights for Android to scan your device for accessibility issues. 24 | Note: permission to capture the screen is required for ColorContrast rules. 25 | 26 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/DeviceConfigFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.os.Build; 7 | import android.view.accessibility.AccessibilityNodeInfo; 8 | 9 | public class DeviceConfigFactory { 10 | public DeviceConfigFactory(String buildModel) { 11 | this.buildModel = buildModel; 12 | } 13 | 14 | public DeviceConfigFactory() { 15 | this(Build.MODEL); 16 | } 17 | 18 | private String serviceVersion = "0.1.0"; 19 | private String buildModel; 20 | 21 | public DeviceConfig getDeviceConfig(AccessibilityNodeInfo rootNode) { 22 | String packageName = getPackageNameFromAccessibilityNode(rootNode); 23 | 24 | return new DeviceConfig(buildModel, packageName, serviceVersion); 25 | } 26 | 27 | private String getPackageNameFromAccessibilityNode(AccessibilityNodeInfo rootNode) { 28 | String packageName = "No application detected"; 29 | if (rootNode != null) { 30 | CharSequence sequence = rootNode.getPackageName(); 31 | if (sequence != null) { 32 | packageName = sequence.toString(); 33 | } 34 | } 35 | return packageName; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug, needs triage 6 | assignees: "" 7 | --- 8 | 9 | ## Describe the bug 10 | 11 | 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Go to '...' 17 | 2. Click on '...' 18 | 3. Scroll down to '...' 19 | 4. See error 20 | 21 | ## Expected behavior 22 | 23 | 24 | 25 | ## Screenshots 26 | 27 | 28 | 29 | ## Context (please complete the following information) 30 | 31 | - Android Version: 32 | - Service Version & Environment: 33 | - Target Application: 34 | 35 | ## Are you willing to submit a PR? 36 | 37 | 38 | 39 | ## Did you search for similar existing issues? 40 | 41 | 42 | 43 | ## Additional context 44 | 45 | 46 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/UnrecognizedRequestFulfillerTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.junit.Assert.assertThrows; 7 | 8 | import android.os.CancellationSignal; 9 | import org.junit.Assert; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.MockitoJUnitRunner; 15 | 16 | @RunWith(MockitoJUnitRunner.class) 17 | public class UnrecognizedRequestFulfillerTest { 18 | final String requestMethod = "Test request method"; 19 | 20 | @Mock CancellationSignal cancellationSignal; 21 | 22 | UnrecognizedRequestFulfiller testSubject; 23 | 24 | @Before 25 | public void prepare() { 26 | testSubject = new UnrecognizedRequestFulfiller(requestMethod); 27 | } 28 | 29 | @Test 30 | public void unrecognizedResponseFulfillerExists() { 31 | Assert.assertNotNull(testSubject); 32 | } 33 | 34 | @Test 35 | public void fulfillsRequestByThrowingPinnedException() { 36 | assertThrows( 37 | "Unrecognized request: Test request method", 38 | RuntimeException.class, 39 | () -> testSubject.fulfillRequest(cancellationSignal)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/LayoutParamGenerator.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.PixelFormat; 7 | import android.util.DisplayMetrics; 8 | import android.view.WindowManager; 9 | import java.util.function.Supplier; 10 | 11 | public class LayoutParamGenerator { 12 | private Supplier displayMetricsSupplier; 13 | 14 | public LayoutParamGenerator(Supplier displayMetricsSupplier) { 15 | this.displayMetricsSupplier = displayMetricsSupplier; 16 | } 17 | 18 | public WindowManager.LayoutParams get() { 19 | DisplayMetrics displayMetrics = displayMetricsSupplier.get(); 20 | WindowManager.LayoutParams params = 21 | new WindowManager.LayoutParams( 22 | displayMetrics.widthPixels, 23 | displayMetrics.heightPixels, 24 | WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, 25 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 26 | | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 27 | | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 28 | | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, 29 | PixelFormat.TRANSLUCENT); 30 | 31 | return params; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/AxeDeviceFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.os.Build; 7 | import android.util.DisplayMetrics; 8 | import android.view.accessibility.AccessibilityNodeInfo; 9 | import com.deque.axe.android.AxeDevice; 10 | import java.util.function.Supplier; 11 | 12 | public class AxeDeviceFactory { 13 | private final DeviceConfigFactory deviceConfigFactory; 14 | private final Supplier displayMetricsSupplier; 15 | 16 | public AxeDeviceFactory( 17 | DeviceConfigFactory deviceConfigFactory, Supplier displayMetricsSupplier) { 18 | this.deviceConfigFactory = deviceConfigFactory; 19 | this.displayMetricsSupplier = displayMetricsSupplier; 20 | } 21 | 22 | public AxeDevice createAxeDevice(AccessibilityNodeInfo rootNode) { 23 | DisplayMetrics displayMetrics = displayMetricsSupplier.get(); 24 | String compoundVersion = Build.VERSION.RELEASE + " API Level " + Build.VERSION.SDK_INT; 25 | DeviceConfig deviceConfig = deviceConfigFactory.getDeviceConfig(rootNode); 26 | return new AxeDevice( 27 | displayMetrics.density, 28 | deviceConfig.deviceName, 29 | compoundVersion, 30 | displayMetrics.heightPixels, 31 | displayMetrics.widthPixels); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ScreenshotActivity.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.app.Activity; 7 | import android.content.Intent; 8 | import android.media.projection.MediaProjectionManager; 9 | import android.os.Bundle; 10 | import android.widget.Toast; 11 | import androidx.annotation.Nullable; 12 | 13 | public class ScreenshotActivity extends Activity { 14 | private MediaProjectionManager mediaManager; 15 | private static final int SCREENSHOT = 99999; 16 | 17 | @Override 18 | protected void onCreate(@Nullable Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | 21 | mediaManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE); 22 | startActivityForResult(mediaManager.createScreenCaptureIntent(), SCREENSHOT); 23 | } 24 | 25 | @Override 26 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 27 | if (requestCode == SCREENSHOT) { 28 | if (resultCode == RESULT_OK) { 29 | MediaProjectionHolder.set(mediaManager.getMediaProjection(resultCode, data)); 30 | } 31 | } 32 | 33 | if (MediaProjectionHolder.get() == null) { 34 | Toast.makeText(this, R.string.screenshot_permission_not_granted, Toast.LENGTH_LONG).show(); 35 | } 36 | 37 | finish(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pipeline/infer-version-code-from-version-name.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | const process = require('process'); 4 | const versionName = process.env['APK_VERSION_NAME']; 5 | 6 | if (versionName == null) { 7 | console.error('APK_VERSION_NAME env var not set!'); 8 | process.exit(1); 9 | } 10 | 11 | const versionNameRegex = /^(\d+)\.(\d+)\.(\d+)$/; 12 | const versionNameMatches = versionNameRegex.exec(versionName); 13 | if (versionNameMatches == null) { 14 | console.error(`APK_VERSION_NAME ${versionName} not in expected format (1.2.3)`); 15 | process.exit(1); 16 | } 17 | 18 | const [major, minor, patch] = versionNameMatches.slice(1).map(component => parseInt(component)); 19 | if (`${major}.${minor}.${patch}` !== versionName) { 20 | console.error(`APK_VERSION_NAME ${versionName} has extra leading zeros (should be ${major}.${minor}.${patch})`) 21 | process.exit(1); 22 | } 23 | 24 | if (major > 99 || minor > 99 || patch > 999) { 25 | console.error(`APK_VERSION_NAME ${versionName} has an oversized component; it must fit in format xx.yy.zzz`); 26 | process.exit(1); 27 | } 28 | 29 | // for version x.y.z, versionCode is xxyyzzz 30 | const versionCode = (major * 100000) + (minor * 1000) + patch; 31 | console.log(`Success! Inferred version code: ${versionCode}`); 32 | 33 | // This is the magic syntax to set an output variable in Azure Pipelines 34 | console.log(`##vso[task.setvariable variable=APK_VERSION_CODE]${versionCode}`); 35 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ConfigRequestFulfiller.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.os.CancellationSignal; 7 | import android.view.accessibility.AccessibilityNodeInfo; 8 | 9 | public class ConfigRequestFulfiller implements RequestFulfiller { 10 | private final RootNodeFinder rootNodeFinder; 11 | private final EventHelper eventHelper; 12 | private final DeviceConfigFactory deviceConfigFactory; 13 | 14 | public ConfigRequestFulfiller( 15 | RootNodeFinder rootNodeFinder, 16 | EventHelper eventHelper, 17 | DeviceConfigFactory deviceConfigFactory) { 18 | this.rootNodeFinder = rootNodeFinder; 19 | this.deviceConfigFactory = deviceConfigFactory; 20 | this.eventHelper = eventHelper; 21 | } 22 | 23 | public String fulfillRequest(CancellationSignal cancellationSignal) { 24 | AccessibilityNodeInfo source = eventHelper.claimLastSource(); 25 | AccessibilityNodeInfo rootNode = rootNodeFinder.getRootNodeFromSource(source); 26 | 27 | try { 28 | return deviceConfigFactory.getDeviceConfig(rootNode).toJson(); 29 | } finally { 30 | if (rootNode != null && rootNode != source) { 31 | rootNode.recycle(); 32 | } 33 | if (source != null && !eventHelper.restoreLastSource(source)) { 34 | source.recycle(); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/BitmapProviderTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Bitmap; 7 | import org.junit.After; 8 | import org.junit.Assert; 9 | import org.junit.Before; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.mockito.Mock; 13 | import org.mockito.MockedStatic; 14 | import org.mockito.Mockito; 15 | import org.mockito.junit.MockitoJUnitRunner; 16 | 17 | @RunWith(MockitoJUnitRunner.class) 18 | public class BitmapProviderTest { 19 | 20 | @Mock Bitmap.Config config; 21 | @Mock Bitmap bitmapMock; 22 | MockedStatic bitmapStaticMock; 23 | 24 | private final int width = 1; 25 | private final int height = 2; 26 | 27 | BitmapProvider testSubject; 28 | 29 | @Before 30 | public void prepare() { 31 | bitmapStaticMock = Mockito.mockStatic(Bitmap.class); 32 | bitmapStaticMock.when(() -> Bitmap.createBitmap(width, height, config)).thenReturn(bitmapMock); 33 | testSubject = new BitmapProvider(); 34 | } 35 | 36 | @After 37 | public void cleanUp() { 38 | bitmapStaticMock.close(); 39 | } 40 | 41 | @Test 42 | public void bitmapIsNotNull() { 43 | Bitmap createdBitmap = testSubject.createBitmap(width, height, config); 44 | Assert.assertNotNull(createdBitmap); 45 | Assert.assertEquals(createdBitmap, bitmapMock); 46 | 47 | bitmapStaticMock.verify(() -> Bitmap.createBitmap(width, height, config)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/AxeContextFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Bitmap; 7 | import android.view.accessibility.AccessibilityNodeInfo; 8 | import com.deque.axe.android.AxeContext; 9 | import com.deque.axe.android.AxeDevice; 10 | import com.deque.axe.android.AxeView; 11 | import com.deque.axe.android.colorcontrast.AxeImage; 12 | import com.deque.axe.android.wrappers.AxeEventStream; 13 | 14 | public class AxeContextFactory { 15 | private final AxeImageFactory axeImageFactory; 16 | private final AxeViewsFactory axeViewsFactory; 17 | private final AxeDeviceFactory axeDeviceFactory; 18 | 19 | public AxeContextFactory( 20 | AxeImageFactory axeImageFactory, 21 | AxeViewsFactory axeViewsFactory, 22 | AxeDeviceFactory axeDeviceFactory) { 23 | this.axeImageFactory = axeImageFactory; 24 | this.axeViewsFactory = axeViewsFactory; 25 | this.axeDeviceFactory = axeDeviceFactory; 26 | } 27 | 28 | public AxeContext createAxeContext(AccessibilityNodeInfo rootNode, Bitmap screenshot) 29 | throws ViewChangedException { 30 | AxeView axeView = axeViewsFactory.createAxeViews(rootNode); 31 | AxeDevice axeDevice = axeDeviceFactory.createAxeDevice(rootNode); 32 | AxeImage axeImage = axeImageFactory.createAxeImage(screenshot); 33 | AxeEventStream axeEventStream = new AxeEventStream(); 34 | return new AxeContext(axeView, axeDevice, axeImage, axeEventStream); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pipeline/verify-clean-lockfile.js: -------------------------------------------------------------------------------- 1 | // USAGE: 2 | // 3 | // node verify-clean-lockfile.js 4 | // 5 | // Verifies that "git status" does not report any pending changed to gradle.lockfile 6 | // 7 | // This is a workaround for Dependabot not understanding natively how to update Gradle 8 | // lockfiles; it serves as a reminder for humans looking at Dependabot PRs to update 9 | // it manually. 10 | 11 | const path = require('path'); 12 | const process = require('process'); 13 | const child_process = require('child_process'); 14 | 15 | const lockfilePath = path.join(__dirname, '..', 'AccessibilityInsightsForAndroidService', 'app', 'gradle.lockfile'); 16 | 17 | const gitStatusResult = child_process.execFileSync('git', ['status', '--porcelain=1', '--', lockfilePath]); 18 | const isLockfileChanged = gitStatusResult.toString() !== ''; 19 | 20 | if (isLockfileChanged) { 21 | const gitDiffResult = child_process.execFileSync('git', ['diff', '--', lockfilePath]); 22 | console.error(` 23 | Error: git status reports that there is an unexpected lockfile change 24 | in ${lockfilePath}. 25 | 26 | This probably means that you (or Dependabot) has updated a dependency 27 | in a build.gradle file without updating the lockfile. To update the 28 | lockfile: 29 | 30 | 1) Pull this branch 31 | 2) Run "./gradlew build --no-daemon" from /AccessibilityInsightsForAndroidService 32 | 3) Commit and push the resulting change to gradle.lockfile 33 | 34 | Diff of the unexpected change: 35 | 36 | ${gitDiffResult.toString()} 37 | `); 38 | process.exit(1); 39 | } else { 40 | console.log('Success! git status reports no unexpected lockfile change.'); 41 | process.exit(0); 42 | } 43 | -------------------------------------------------------------------------------- /pipeline/scripts/setup-emulator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | emulator_config="system-images;android-$ANDROID_API_LEVEL;google_apis;x86_64" 3 | emulator_name="emulator_api_$ANDROID_API_LEVEL" 4 | 5 | # Install AVD files 6 | echo "y" | $ANDROID_HOME/tools/bin/sdkmanager --install "$emulator_config" 7 | # Create emulator 8 | echo "no" | $ANDROID_HOME/tools/bin/avdmanager create avd -n "$emulator_name" -k "$emulator_config" --force 9 | 10 | $ANDROID_HOME/emulator/emulator -list-avds 11 | 12 | echo "Starting emulator" 13 | # Start emulator in background 14 | nohup $ANDROID_HOME/emulator/emulator -avd "$emulator_name" -no-snapshot -no-boot-anim > /dev/null 2>&1 & 15 | 16 | echo "Waiting for boot_completed to have value" 17 | $ANDROID_HOME/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do echo -n "."; sleep 1; done; input keyevent 82; echo "."' 18 | 19 | echo "Pausing to allow emulator to finish booting" 20 | sleep 20 # pause to wait for the emulator to finish booting 21 | 22 | $ANDROID_HOME/platform-tools/adb devices 23 | echo "Emulator started" 24 | 25 | echo "Installing and enabling Accessibility Insights for Android Service" 26 | $ANDROID_HOME/platform-tools/adb install app-debug.apk 27 | $ANDROID_HOME/platform-tools/adb shell appops set com.microsoft.accessibilityinsightsforandroidservice PROJECT_MEDIA allow 28 | $ANDROID_HOME/platform-tools/adb shell settings put secure enabled_accessibility_services com.microsoft.accessibilityinsightsforandroidservice/com.microsoft.accessibilityinsightsforandroidservice.AccessibilityInsightsForAndroidService 29 | 30 | echo "Pausing to allow service to start" 31 | sleep 5 # pause to let the accessibility service start 32 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/DeviceOrientationHandlerTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.Mockito.times; 7 | import static org.mockito.Mockito.verify; 8 | 9 | import java.util.function.Consumer; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.MockitoJUnitRunner; 15 | 16 | @RunWith(MockitoJUnitRunner.class) 17 | public class DeviceOrientationHandlerTest { 18 | 19 | @Mock Consumer onChangeMock; 20 | int initialValue = 1; 21 | 22 | DeviceOrientationHandler testSubject; 23 | 24 | @Before 25 | public void prepare() { 26 | testSubject = new DeviceOrientationHandler(initialValue); 27 | } 28 | 29 | @Test 30 | public void setOrientationDoesNotCallOnChangeListenersIfOrientationDoesNotChange() { 31 | testSubject.subscribe(onChangeMock); 32 | testSubject.setOrientation(initialValue); 33 | verify(onChangeMock, times(0)).accept(2); 34 | } 35 | 36 | @Test 37 | public void setOrientationCallsOnChangeListenersOnOrientationchange() { 38 | testSubject.subscribe(onChangeMock); 39 | testSubject.setOrientation(2); 40 | verify(onChangeMock, times(1)).accept(2); 41 | } 42 | 43 | @Test 44 | public void setOrientationSupportsMultipleListeners() { 45 | testSubject.subscribe(onChangeMock); 46 | testSubject.subscribe(onChangeMock); 47 | 48 | testSubject.setOrientation(2); 49 | 50 | verify(onChangeMock, times(2)).accept(2); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/AxeDeviceFactoryTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.Mockito.when; 7 | 8 | import android.util.DisplayMetrics; 9 | import android.view.accessibility.AccessibilityNodeInfo; 10 | import org.junit.Assert; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.MockitoJUnitRunner; 16 | 17 | @RunWith(MockitoJUnitRunner.class) 18 | public class AxeDeviceFactoryTest { 19 | 20 | @Mock DeviceConfigFactory deviceConfigFactoryMock; 21 | @Mock AccessibilityNodeInfo rootNodeMock; 22 | String deviceName = "test-device-name"; 23 | String packageName = "test-package-name"; 24 | String serviceVersion = "test-service-version"; 25 | DeviceConfig deviceConfig; 26 | DisplayMetrics displayMetrics; 27 | 28 | AxeDeviceFactory testSubject; 29 | 30 | @Before 31 | public void prepare() { 32 | deviceConfig = new DeviceConfig(deviceName, packageName, serviceVersion); 33 | when(deviceConfigFactoryMock.getDeviceConfig(rootNodeMock)).thenReturn(deviceConfig); 34 | 35 | displayMetrics = new DisplayMetrics(); 36 | displayMetrics.density = 1; 37 | displayMetrics.heightPixels = 2; 38 | displayMetrics.widthPixels = 3; 39 | 40 | testSubject = new AxeDeviceFactory(deviceConfigFactoryMock, () -> displayMetrics); 41 | } 42 | 43 | @Test 44 | public void axeDeviceIsNotNull() { 45 | Assert.assertNotNull(testSubject.createAxeDevice(rootNodeMock)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ScreenshotAxeImage.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Bitmap; 7 | import android.util.Base64; 8 | import com.deque.axe.android.colorcontrast.AxeColor; 9 | import com.deque.axe.android.colorcontrast.AxeImage; 10 | import com.deque.axe.android.wrappers.AxeRect; 11 | import java.io.ByteArrayOutputStream; 12 | 13 | public class ScreenshotAxeImage extends AxeImage { 14 | private final AxeRect frameRect; 15 | private Bitmap screenshot; 16 | private ByteArrayOutputStreamProvider byteArrayOutputStreamProvider; 17 | 18 | public ScreenshotAxeImage( 19 | Bitmap screenshot, ByteArrayOutputStreamProvider byteArrayOutputStreamProvider) { 20 | this.screenshot = screenshot; 21 | this.byteArrayOutputStreamProvider = byteArrayOutputStreamProvider; 22 | frameRect = new AxeRect(0, screenshot.getWidth() - 1, 0, screenshot.getHeight() - 1); 23 | } 24 | 25 | @Override 26 | public AxeRect frame() { 27 | return frameRect; 28 | } 29 | 30 | @Override 31 | public AxeColor pixel(int x, int y) { 32 | AxeColor color = new AxeColor(this.screenshot.getPixel(x, y)); 33 | return color; 34 | } 35 | 36 | @Override 37 | public String toBase64Png() { 38 | ByteArrayOutputStream byteArrayOutputStream = byteArrayOutputStreamProvider.get(); 39 | screenshot.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); 40 | byte[] byteArray = byteArrayOutputStream.toByteArray(); 41 | return Base64.encodeToString(byteArray, Base64.NO_WRAP); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/AxeScannerTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.Mockito.when; 7 | 8 | import android.graphics.Bitmap; 9 | import android.view.accessibility.AccessibilityNodeInfo; 10 | import com.deque.axe.android.Axe; 11 | import com.deque.axe.android.AxeContext; 12 | import com.deque.axe.android.AxeResult; 13 | import org.junit.Assert; 14 | import org.junit.Before; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | import org.mockito.Mock; 18 | import org.mockito.junit.MockitoJUnitRunner; 19 | 20 | @RunWith(MockitoJUnitRunner.class) 21 | public class AxeScannerTest { 22 | 23 | @Mock Bitmap screenshotMock; 24 | @Mock AccessibilityNodeInfo accessibilityNodeInfoMock; 25 | @Mock AxeRunnerFactory axeRunnerFactoryMock; 26 | @Mock AxeContextFactory axeContextFactoryMock; 27 | @Mock AxeResult axeResultMock; 28 | @Mock Axe axeMock; 29 | @Mock AxeContext axeContextMock; 30 | 31 | AxeScanner testSubject; 32 | 33 | @Before 34 | public void prepare() { 35 | testSubject = new AxeScanner(axeRunnerFactoryMock, axeContextFactoryMock); 36 | } 37 | 38 | @Test 39 | public void scanWithAxeReturnsCorrectResult() throws ViewChangedException { 40 | when(axeRunnerFactoryMock.createAxeRunner()).thenReturn(axeMock); 41 | when(axeContextFactoryMock.createAxeContext(accessibilityNodeInfoMock, screenshotMock)) 42 | .thenReturn(axeContextMock); 43 | when(axeMock.run(axeContextMock)).thenReturn(axeResultMock); 44 | 45 | Assert.assertEquals( 46 | testSubject.scanWithAxe(accessibilityNodeInfoMock, screenshotMock), axeResultMock); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/FocusVisualizationStateManagerTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.Mockito.times; 7 | import static org.mockito.Mockito.verify; 8 | 9 | import java.util.function.Consumer; 10 | import org.junit.Assert; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.MockitoJUnitRunner; 16 | 17 | @RunWith(MockitoJUnitRunner.class) 18 | public class FocusVisualizationStateManagerTest { 19 | 20 | @Mock Consumer onChangeMock; 21 | 22 | FocusVisualizationStateManager testSubject; 23 | 24 | @Before 25 | public void prepare() { 26 | testSubject = new FocusVisualizationStateManager(); 27 | } 28 | 29 | @Test 30 | public void exists() { 31 | Assert.assertNotNull(testSubject); 32 | } 33 | 34 | @Test 35 | public void getStateReturnsFalseByDefault() { 36 | Assert.assertFalse(testSubject.getState()); 37 | } 38 | 39 | @Test 40 | public void getStateReturnsUpdatedState() { 41 | testSubject.setState(true); 42 | Assert.assertTrue(testSubject.getState()); 43 | } 44 | 45 | @Test 46 | public void setStateDoesNotCallOnChangeListenersIfStateDoesNotChange() { 47 | testSubject.subscribe(onChangeMock); 48 | testSubject.setState(false); 49 | verify(onChangeMock, times(0)).accept(false); 50 | } 51 | 52 | @Test 53 | public void setStateCallsOnChangeListenersOnStateChange() { 54 | testSubject.subscribe(onChangeMock); 55 | testSubject.setState(true); 56 | Assert.assertTrue(testSubject.getState()); 57 | verify(onChangeMock, times(1)).accept(true); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/UIThreadRunnerTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.ArgumentMatchers.any; 7 | import static org.mockito.Mockito.doAnswer; 8 | import static org.mockito.Mockito.verify; 9 | 10 | import android.os.Handler; 11 | import android.os.Looper; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.Mock; 15 | import org.mockito.MockedConstruction; 16 | import org.mockito.MockedStatic; 17 | import org.mockito.Mockito; 18 | import org.mockito.junit.MockitoJUnitRunner; 19 | 20 | @RunWith(MockitoJUnitRunner.class) 21 | public class UIThreadRunnerTest { 22 | 23 | @Mock Looper looperMock; 24 | @Mock Runnable runnableMock; 25 | 26 | UIThreadRunner testSubject; 27 | 28 | @Test 29 | public void createsNewHandlerUsingMainLooper() throws Exception { 30 | try (MockedStatic looperStaticMock = Mockito.mockStatic(Looper.class)) { 31 | looperStaticMock.when(Looper::getMainLooper).thenReturn(looperMock); 32 | try (MockedConstruction handlerConstructionMock = 33 | Mockito.mockConstruction( 34 | Handler.class, 35 | (handlerMock, context) -> { 36 | doAnswer( 37 | invocation -> { 38 | Runnable runnable = invocation.getArgument(0); 39 | runnable.run(); 40 | return null; 41 | }) 42 | .when(handlerMock) 43 | .post(any()); 44 | })) { 45 | testSubject = new UIThreadRunner(); 46 | testSubject.run(runnableMock); 47 | 48 | verify(runnableMock).run(); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/FocusVisualizationCanvas.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.content.Context; 7 | import android.graphics.Canvas; 8 | import android.view.View; 9 | import androidx.annotation.VisibleForTesting; 10 | import java.util.ArrayList; 11 | 12 | public class FocusVisualizationCanvas extends View { 13 | private ArrayList focusElementHighlights; 14 | private ArrayList focusElementLines; 15 | 16 | public FocusVisualizationCanvas(Context context) { 17 | super(context); 18 | } 19 | 20 | @Override 21 | protected void onDraw(Canvas canvas) { 22 | super.onDraw(canvas); 23 | this.drawHighlightsAndLines(canvas); 24 | } 25 | 26 | @VisibleForTesting 27 | public void drawHighlightsAndLines(Canvas canvas) { 28 | if (this.focusElementHighlights == null) { 29 | return; 30 | } 31 | 32 | for (int elementIndex = 0; elementIndex < this.focusElementHighlights.size(); elementIndex++) { 33 | if (elementIndex != 0) { 34 | this.drawTrailingHighlights(elementIndex, canvas); 35 | } 36 | 37 | this.focusElementHighlights.get(elementIndex).drawElementHighlight(canvas); 38 | } 39 | } 40 | 41 | private void drawTrailingHighlights(int elementIndex, Canvas canvas) { 42 | this.focusElementLines.get(elementIndex).drawLine(canvas); 43 | this.focusElementHighlights.get(elementIndex - 1).drawElementHighlight(canvas); 44 | } 45 | 46 | public void setDrawItems( 47 | ArrayList highlights, ArrayList lines) { 48 | this.focusElementHighlights = highlights; 49 | this.focusElementLines = lines; 50 | } 51 | 52 | public void redraw() { 53 | this.invalidate(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | schedule: 10 | - cron: '0 5 * * 5' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['java', 'javascript'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | 45 | # ℹ️ Command-line programs to run using the OS shell. 46 | # 📚 https://git.io/JvXDl 47 | 48 | - name: make gradlew executable 49 | run: chmod +x ./AccessibilityInsightsForAndroidService/gradlew 50 | 51 | - name: build 52 | run: ./gradlew build 53 | working-directory: ./AccessibilityInsightsForAndroidService 54 | 55 | - name: Perform CodeQL Analysis 56 | uses: github/codeql-action/analyze@v1 57 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/AccessibilityNodeInfoQueueBuilder.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.view.accessibility.AccessibilityNodeInfo; 7 | import java.util.PriorityQueue; 8 | import java.util.Queue; 9 | 10 | public class AccessibilityNodeInfoQueueBuilder { 11 | public Queue> buildPriorityQueue( 12 | AccessibilityNodeInfo rootNode) { 13 | PriorityQueue> queue = new PriorityQueue<>(); 14 | recursivelyEnqueueNodes(queue, rootNode, Long.MAX_VALUE); 15 | return queue; 16 | } 17 | 18 | private void recursivelyEnqueueNodes( 19 | PriorityQueue> queue, 20 | AccessibilityNodeInfo node, 21 | Long order) { 22 | // The AxeView object requires that we create the AxeView 23 | // objects for both child nodes and for any labeledBy nodes. Child nodes use 24 | // easily predictable rules, but labeledBy nodes are less structured. We use 25 | // a PriorityQueue where children are given a slightly higher priority than 26 | // the parent, but nodes that are used as labels are given a significantly 27 | // higher priority. This will work for most "typical" cases, but not the case 28 | // where a node's labeledBy value points to a direct ancestor. That scenario 29 | // could require changes to the AxeView class, which is immutable after construction. 30 | 31 | if (node == null) { 32 | return; 33 | } 34 | 35 | if (node.getLabelFor() != null) { 36 | order /= 2; 37 | } 38 | 39 | queue.add(new OrderedValue<>(node, order)); 40 | 41 | int childCount = node.getChildCount(); 42 | 43 | for (int loop = 0; loop < childCount; loop++) { 44 | recursivelyEnqueueNodes(queue, node.getChild(loop), order - 1); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/SynchronizedRequestDispatcher.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.os.CancellationSignal; 7 | import androidx.annotation.AnyThread; 8 | import androidx.annotation.NonNull; 9 | 10 | public class SynchronizedRequestDispatcher { 11 | public static final SynchronizedRequestDispatcher SharedInstance = 12 | new SynchronizedRequestDispatcher(); 13 | 14 | private RequestDispatcher underlyingDispatcher = null; 15 | private Object lock = new Object(); 16 | private CancellationSignal teardownSignal = new CancellationSignal(); 17 | 18 | @AnyThread 19 | public void setup(@NonNull RequestDispatcher instance) { 20 | synchronized (lock) { 21 | if (this.underlyingDispatcher != null) { 22 | throw new RuntimeException("Attempt to double-initialize instance"); 23 | } 24 | this.teardownSignal = new CancellationSignal(); 25 | this.underlyingDispatcher = instance; 26 | } 27 | } 28 | 29 | @AnyThread 30 | public void teardown() { 31 | CancellationSignal teardownSignal = this.teardownSignal; 32 | if (teardownSignal != null) { 33 | teardownSignal.cancel(); 34 | } 35 | 36 | synchronized (lock) { 37 | this.underlyingDispatcher = null; 38 | this.teardownSignal = null; 39 | } 40 | } 41 | 42 | @AnyThread 43 | public String request(@NonNull String method, @NonNull CancellationSignal cancellationSignal) 44 | throws Exception { 45 | CancellationSignal combinedCancellationSignal = new CancellationSignal(); 46 | 47 | synchronized (lock) { 48 | if (underlyingDispatcher == null) { 49 | throw new Exception("Service is not running"); 50 | } 51 | 52 | teardownSignal.setOnCancelListener(combinedCancellationSignal::cancel); 53 | cancellationSignal.setOnCancelListener(combinedCancellationSignal::cancel); 54 | combinedCancellationSignal.throwIfCanceled(); 55 | 56 | return underlyingDispatcher.request(method, combinedCancellationSignal); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/LayoutParamGeneratorTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | import static org.mockito.Mockito.when; 8 | 9 | import android.graphics.PixelFormat; 10 | import android.util.DisplayMetrics; 11 | import android.view.WindowManager; 12 | import java.util.List; 13 | import java.util.function.Supplier; 14 | import org.junit.Before; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | import org.mockito.Mock; 18 | import org.mockito.MockedConstruction; 19 | import org.mockito.Mockito; 20 | import org.mockito.junit.MockitoJUnitRunner; 21 | 22 | @RunWith(MockitoJUnitRunner.class) 23 | public class LayoutParamGeneratorTest { 24 | 25 | @Mock Supplier displayMetricsSupplier; 26 | 27 | @Mock DisplayMetrics displayMetrics; 28 | 29 | LayoutParamGenerator testSubject; 30 | 31 | @Before 32 | public void prepare() { 33 | testSubject = new LayoutParamGenerator(displayMetricsSupplier); 34 | } 35 | 36 | @Test 37 | public void generatesLayoutParams() throws Exception { 38 | when(displayMetricsSupplier.get()).thenReturn(displayMetrics); 39 | try (MockedConstruction layoutParamsConstructionMock = 40 | Mockito.mockConstruction( 41 | WindowManager.LayoutParams.class, 42 | (mockLayoutParams, context) -> { 43 | List args = context.arguments(); 44 | assertEquals(displayMetrics.widthPixels, args.get(0)); 45 | assertEquals(displayMetrics.heightPixels, args.get(1)); 46 | assertEquals(WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, args.get(2)); 47 | assertEquals( 48 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 49 | | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE 50 | | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 51 | | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, 52 | args.get(3)); 53 | assertEquals(PixelFormat.TRANSLUCENT, args.get(4)); 54 | })) { 55 | testSubject.get(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/AxeContextFactoryTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.Mockito.when; 7 | 8 | import android.graphics.Bitmap; 9 | import android.view.accessibility.AccessibilityNodeInfo; 10 | import com.deque.axe.android.AxeContext; 11 | import com.deque.axe.android.AxeDevice; 12 | import com.deque.axe.android.AxeView; 13 | import com.deque.axe.android.colorcontrast.AxeImage; 14 | import org.junit.Assert; 15 | import org.junit.Before; 16 | import org.junit.Test; 17 | import org.junit.runner.RunWith; 18 | import org.mockito.Mock; 19 | import org.mockito.junit.MockitoJUnitRunner; 20 | 21 | @RunWith(MockitoJUnitRunner.class) 22 | public class AxeContextFactoryTest { 23 | 24 | @Mock AxeImageFactory axeImageFactoryMock; 25 | @Mock AxeImage axeImageMock; 26 | @Mock AxeViewsFactory axeViewsFactoryMock; 27 | @Mock AxeView axeViewMock; 28 | @Mock AxeDeviceFactory axeDeviceFactoryMock; 29 | @Mock AxeDevice axeDeviceMock; 30 | @Mock AccessibilityNodeInfo rootNodeMock; 31 | @Mock Bitmap screenshotMock; 32 | 33 | AxeContextFactory testSubject; 34 | 35 | @Before 36 | public void prepare() throws ViewChangedException { 37 | when(axeImageFactoryMock.createAxeImage(screenshotMock)).thenReturn(axeImageMock); 38 | when(axeViewsFactoryMock.createAxeViews(rootNodeMock)).thenReturn(axeViewMock); 39 | when(axeDeviceFactoryMock.createAxeDevice(rootNodeMock)).thenReturn(axeDeviceMock); 40 | 41 | testSubject = 42 | new AxeContextFactory(axeImageFactoryMock, axeViewsFactoryMock, axeDeviceFactoryMock); 43 | } 44 | 45 | @Test 46 | public void axeContentIsNotNull() throws ViewChangedException { 47 | Assert.assertNotNull(testSubject.createAxeContext(rootNodeMock, screenshotMock)); 48 | } 49 | 50 | @Test 51 | public void axeContentHasCorrectProperties() throws ViewChangedException { 52 | AxeContext axeContext = testSubject.createAxeContext(rootNodeMock, screenshotMock); 53 | Assert.assertEquals(axeContext.screenshot, axeImageMock); 54 | Assert.assertEquals(axeContext.axeDevice, axeDeviceMock); 55 | Assert.assertEquals(axeContext.axeView, axeViewMock); 56 | Assert.assertNotNull(axeContext.axeEventStream); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/RequestReaderTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.Mockito.when; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import org.junit.Assert; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.MockitoJUnitRunner; 16 | import org.mockito.stubbing.OngoingStubbing; 17 | 18 | @RunWith(MockitoJUnitRunner.class) 19 | public class RequestReaderTest { 20 | 21 | @Mock BufferedReader bufferedReaderMock; 22 | 23 | RequestReader testSubject; 24 | 25 | @Before 26 | public void prepare() { 27 | testSubject = new RequestReader(bufferedReaderMock); 28 | } 29 | 30 | @Test 31 | public void requestReaderExists() { 32 | Assert.assertNotNull(testSubject); 33 | } 34 | 35 | @Test 36 | public void readsFromBufferedReader() throws IOException { 37 | String requestString = "test request string"; 38 | setupReadLine(requestString); 39 | 40 | String actualRequestString = testSubject.readRequest(); 41 | 42 | Assert.assertEquals(actualRequestString, requestString); 43 | } 44 | 45 | @Test 46 | public void limitsInputLength() throws IOException { 47 | OngoingStubbing bufferedReaderStubbing = when(bufferedReaderMock.read()); 48 | for (int i = 0; i < 300; i++) { 49 | bufferedReaderStubbing = bufferedReaderStubbing.thenReturn(42); 50 | } 51 | 52 | try { 53 | testSubject.readRequest(); 54 | Assert.fail("Should have thrown exception"); 55 | } catch (IOException e) { 56 | Assert.assertEquals(e.getMessage(), "input too long"); 57 | } 58 | } 59 | 60 | private void setupReadLine(String str) { 61 | OngoingStubbing bufferedReaderStubbing; 62 | try { 63 | bufferedReaderStubbing = when(bufferedReaderMock.read()); 64 | } catch (IOException e) { 65 | Assert.fail(e.getMessage()); 66 | return; 67 | } 68 | 69 | for (int i = 0; i < str.length(); i++) { 70 | bufferedReaderStubbing = bufferedReaderStubbing.thenReturn((int) str.charAt(i)); 71 | } 72 | 73 | bufferedReaderStubbing.thenReturn((int) '\n'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/DisplayMetricsHelperTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.Mockito.verify; 7 | import static org.mockito.Mockito.when; 8 | import static org.mockito.internal.verification.VerificationModeFactory.times; 9 | 10 | import android.content.Context; 11 | import android.content.res.Resources; 12 | import android.util.DisplayMetrics; 13 | import android.view.Display; 14 | import android.view.WindowManager; 15 | import org.junit.After; 16 | import org.junit.Assert; 17 | import org.junit.Before; 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | import org.mockito.Mock; 21 | import org.mockito.MockedStatic; 22 | import org.mockito.Mockito; 23 | import org.mockito.junit.MockitoJUnitRunner; 24 | 25 | @RunWith(MockitoJUnitRunner.class) 26 | public class DisplayMetricsHelperTest { 27 | 28 | @Mock Resources resourcesMock; 29 | @Mock DisplayMetrics displayMetricsMock; 30 | @Mock Context contextMock; 31 | @Mock WindowManager windowManagerMock; 32 | @Mock Display displayMock; 33 | 34 | MockedStatic resourcesStaticMock; 35 | 36 | @Before 37 | public void prepare() { 38 | setupDisplayMocks(); 39 | } 40 | 41 | @After 42 | public void cleanUp() { 43 | resourcesStaticMock.close(); 44 | } 45 | 46 | @Test 47 | public void returnsNotNull() { 48 | Assert.assertNotNull(DisplayMetricsHelper.getRealDisplayMetrics(contextMock)); 49 | } 50 | 51 | @Test 52 | public void returnsExpectedDisplayMetrics() { 53 | DisplayMetrics actualDisplayMetrics = DisplayMetricsHelper.getRealDisplayMetrics(contextMock); 54 | 55 | verify(displayMock, times(1)).getRealMetrics(displayMetricsMock); 56 | Assert.assertEquals(actualDisplayMetrics, displayMetricsMock); 57 | } 58 | 59 | private void setupDisplayMocks() { 60 | resourcesStaticMock = Mockito.mockStatic(Resources.class); 61 | resourcesStaticMock.when(Resources::getSystem).thenReturn(resourcesMock); 62 | when(resourcesMock.getDisplayMetrics()).thenReturn(displayMetricsMock); 63 | when(contextMock.getSystemService(Context.WINDOW_SERVICE)).thenReturn(windowManagerMock); 64 | when(windowManagerMock.getDefaultDisplay()).thenReturn(displayMock); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/ThreadSafeSwapperTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import org.junit.Assert; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.MockitoJUnitRunner; 12 | 13 | @RunWith(MockitoJUnitRunner.class) 14 | public class ThreadSafeSwapperTest { 15 | @Mock GenericTestObject mockOldObject; 16 | 17 | @Mock GenericTestObject mockIntermediateObject; 18 | 19 | @Mock GenericTestObject mockNewObject; 20 | 21 | ThreadSafeSwapper testSubject; 22 | 23 | @Before 24 | public void prepare() { 25 | testSubject = new ThreadSafeSwapper<>(); 26 | } 27 | 28 | @Test 29 | public void threadSafeSwapperExists() { 30 | Assert.assertNotNull(testSubject); 31 | } 32 | 33 | @Test 34 | public void swapReturnsSwappedOutObject() { 35 | testSubject.swap(mockOldObject); 36 | 37 | GenericTestObject actualReturnedObject = testSubject.swap(mockNewObject); 38 | 39 | Assert.assertEquals(mockOldObject, actualReturnedObject); 40 | } 41 | 42 | @Test 43 | public void swapReplacesCurrentObjectWithMethodParameter() { 44 | testSubject.swap(mockOldObject); 45 | 46 | GenericTestObject actualOldObject = testSubject.swap(mockIntermediateObject); 47 | GenericTestObject actualIntermediateObject = testSubject.swap(mockNewObject); 48 | 49 | Assert.assertEquals(mockOldObject, actualOldObject); 50 | Assert.assertEquals(mockIntermediateObject, actualIntermediateObject); 51 | } 52 | 53 | @Test 54 | public void setIfCurrentlyNullDoesNotSetCurrentObjectIfNotNull() { 55 | boolean expectedReturnValue = false; 56 | testSubject.swap(mockOldObject); 57 | 58 | boolean actualReturnValue = testSubject.setIfCurrentlyNull(mockNewObject); 59 | 60 | Assert.assertEquals(expectedReturnValue, actualReturnValue); 61 | } 62 | 63 | @Test 64 | public void setIfCurrentlyNullSetsCurrentObjectIfNull() { 65 | boolean expectedReturnValue = true; 66 | testSubject.swap(null); 67 | 68 | boolean actualReturnValue = testSubject.setIfCurrentlyNull(mockNewObject); 69 | 70 | Assert.assertEquals(expectedReturnValue, actualReturnValue); 71 | } 72 | 73 | private class GenericTestObject {} 74 | } 75 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ResultsV2ContainerSerializer.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import com.deque.axe.android.AxeResult; 7 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 8 | import com.google.gson.Gson; 9 | import com.google.gson.GsonBuilder; 10 | import com.google.gson.TypeAdapter; 11 | import com.google.gson.stream.JsonReader; 12 | import com.google.gson.stream.JsonWriter; 13 | import java.io.IOException; 14 | import java.util.List; 15 | 16 | public class ResultsV2ContainerSerializer { 17 | private final ATFARulesSerializer atfaRulesSerializer; 18 | private final ATFAResultsSerializer atfaResultsSerializer; 19 | private final Gson gson; 20 | private final TypeAdapter resultsContainerTypeAdapter = 21 | new TypeAdapter() { 22 | @Override 23 | public void write(JsonWriter out, ResultsV2Container value) throws IOException { 24 | out.beginObject(); 25 | out.name("AxeResults").jsonValue(value.AxeResult.toJson()); 26 | out.name("ATFARules").jsonValue(atfaRulesSerializer.serializeATFARules()); 27 | out.name("ATFAResults") 28 | .jsonValue(atfaResultsSerializer.serializeATFAResults(value.ATFAResults)); 29 | out.endObject(); 30 | } 31 | 32 | @Override 33 | public ResultsV2Container read(JsonReader in) { 34 | return null; 35 | } 36 | }; 37 | 38 | public ResultsV2ContainerSerializer( 39 | ATFARulesSerializer atfaRulesSerializer, 40 | ATFAResultsSerializer atfaResultsSerializer, 41 | GsonBuilder gsonBuilder) { 42 | this.atfaRulesSerializer = atfaRulesSerializer; 43 | this.atfaResultsSerializer = atfaResultsSerializer; 44 | this.gson = 45 | gsonBuilder 46 | .registerTypeAdapter(ResultsV2Container.class, this.resultsContainerTypeAdapter) 47 | .create(); 48 | } 49 | 50 | public String createResultsJson( 51 | AxeResult axeResult, List atfaResults) { 52 | ResultsV2Container container = new ResultsV2Container(); 53 | container.ATFAResults = atfaResults; 54 | container.AxeResult = axeResult; 55 | return gson.toJson(container); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ATFAResultsSerializer.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheck; 7 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 8 | import com.google.android.apps.common.testing.accessibility.framework.uielement.WindowHierarchyElement; 9 | import com.google.android.apps.common.testing.accessibility.framework.uielement.WindowHierarchyElementAndroid; 10 | import com.google.gson.ExclusionStrategy; 11 | import com.google.gson.FieldAttributes; 12 | import com.google.gson.FieldNamingStrategy; 13 | import com.google.gson.Gson; 14 | import com.google.gson.GsonBuilder; 15 | import com.google.gson.JsonPrimitive; 16 | import com.google.gson.JsonSerializer; 17 | import java.util.Arrays; 18 | import java.util.List; 19 | 20 | public class ATFAResultsSerializer { 21 | private static final List ClassesToSkip = 22 | Arrays.asList(WindowHierarchyElement.class, WindowHierarchyElementAndroid.class); 23 | 24 | private static final FieldNamingStrategy ATFAFieldNamingStrategy = 25 | f -> f.getDeclaringClass().getSimpleName() + "." + f.getName(); 26 | 27 | private static final ExclusionStrategy ATFAExclusionStrategy = 28 | new ExclusionStrategy() { 29 | @Override 30 | public boolean shouldSkipField(FieldAttributes f) { 31 | return false; 32 | } 33 | 34 | @Override 35 | public boolean shouldSkipClass(Class clazz) { 36 | return ClassesToSkip.contains(clazz); 37 | } 38 | }; 39 | 40 | private static final JsonSerializer> ClassSerializer = 41 | (src, typeOfSrc, context) -> new JsonPrimitive(src.getSimpleName()); 42 | 43 | private final Gson gsonSerializer; 44 | 45 | public ATFAResultsSerializer(GsonBuilder gsonBuilder) { 46 | gsonSerializer = 47 | gsonBuilder 48 | .setFieldNamingStrategy(ATFAFieldNamingStrategy) 49 | .setExclusionStrategies(ATFAExclusionStrategy) 50 | .registerTypeAdapter(Class.class, ClassSerializer) 51 | .create(); 52 | } 53 | 54 | public String serializeATFAResults(List atfaResults) { 55 | return gsonSerializer.toJson(atfaResults); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/DeviceConfigFactoryTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.Mockito.when; 7 | 8 | import android.view.accessibility.AccessibilityNodeInfo; 9 | import org.junit.Assert; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.MockitoJUnitRunner; 15 | 16 | @RunWith(MockitoJUnitRunner.class) 17 | public class DeviceConfigFactoryTest { 18 | final CharSequence samplePackageName = "test-package-name"; 19 | final String packageNameUnavailable = "No application detected"; 20 | final String sampleBuildModel = "test-build-model"; 21 | 22 | @Mock AccessibilityNodeInfo mockRootNode; 23 | 24 | DeviceConfigFactory testSubject; 25 | 26 | @Before 27 | public void prepare() { 28 | testSubject = new DeviceConfigFactory(sampleBuildModel); 29 | } 30 | 31 | @Test 32 | public void deviceConfigFactoryExists() { 33 | Assert.assertNotNull(testSubject); 34 | } 35 | 36 | @Test 37 | public void getDeviceConfigReturnsNonNullDeviceConfig() { 38 | Assert.assertNotNull(testSubject.getDeviceConfig(mockRootNode)); 39 | } 40 | 41 | @Test 42 | public void deviceConfigFactoryPropertiesExist() { 43 | DeviceConfig deviceConfig = testSubject.getDeviceConfig(mockRootNode); 44 | 45 | Assert.assertNotNull(deviceConfig.deviceName); 46 | Assert.assertEquals(sampleBuildModel, deviceConfig.deviceName); 47 | Assert.assertNotNull(deviceConfig.packageName); 48 | Assert.assertNotNull(deviceConfig.serviceVersion); 49 | } 50 | 51 | @Test 52 | public void deviceConfigFactoryGetsProperPackageName() { 53 | when(mockRootNode.getPackageName()).thenReturn(samplePackageName); 54 | 55 | Assert.assertEquals(samplePackageName, getActualPackageName(mockRootNode)); 56 | } 57 | 58 | @Test 59 | public void deviceConfigFactoryGetsNoPackageNameWhenPackageNameIsNull() { 60 | when(mockRootNode.getPackageName()).thenReturn(null); 61 | 62 | Assert.assertEquals(packageNameUnavailable, getActualPackageName(mockRootNode)); 63 | } 64 | 65 | @Test 66 | public void deviceConfigFactoryGetsNoPackageNameWhenRootNodeIsNull() { 67 | Assert.assertEquals(packageNameUnavailable, getActualPackageName(null)); 68 | } 69 | 70 | private String getActualPackageName(AccessibilityNodeInfo rootNode) { 71 | return testSubject.getDeviceConfig(rootNode).packageName; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/RootNodeFinderTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.Mockito.never; 7 | import static org.mockito.Mockito.times; 8 | import static org.mockito.Mockito.verify; 9 | import static org.mockito.Mockito.when; 10 | 11 | import android.view.accessibility.AccessibilityNodeInfo; 12 | import org.junit.Assert; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.MockitoJUnitRunner; 18 | 19 | @RunWith(MockitoJUnitRunner.class) 20 | public class RootNodeFinderTest { 21 | 22 | @Mock AccessibilityNodeInfo sourceMock; 23 | @Mock AccessibilityNodeInfo parentMock; 24 | @Mock AccessibilityNodeInfo grandparentMock; 25 | 26 | RootNodeFinder testSubject; 27 | 28 | @Before 29 | public void prepare() { 30 | testSubject = new RootNodeFinder(); 31 | } 32 | 33 | @Test 34 | public void returnNullIfSourceIsNull() { 35 | Assert.assertNull(testSubject.getRootNodeFromSource(null)); 36 | } 37 | 38 | @Test 39 | public void rootNodeExistsIfSourceExists() { 40 | Assert.assertNotNull(testSubject.getRootNodeFromSource(sourceMock)); 41 | } 42 | 43 | @Test 44 | public void rootNodeIsSource() { 45 | AccessibilityNodeInfo rootNode = testSubject.getRootNodeFromSource(sourceMock); 46 | Assert.assertEquals(rootNode, sourceMock); 47 | } 48 | 49 | @Test 50 | public void rootNodeIsSourceParent() { 51 | when(sourceMock.getParent()).thenReturn(parentMock); 52 | 53 | AccessibilityNodeInfo rootNode = testSubject.getRootNodeFromSource(sourceMock); 54 | Assert.assertEquals(rootNode, parentMock); 55 | } 56 | 57 | @Test 58 | public void rootNodeIsSourceAncestor() { 59 | when(sourceMock.getParent()).thenReturn(parentMock); 60 | when(parentMock.getParent()).thenReturn(grandparentMock); 61 | 62 | AccessibilityNodeInfo rootNode = testSubject.getRootNodeFromSource(sourceMock); 63 | Assert.assertEquals(rootNode, grandparentMock); 64 | } 65 | 66 | @Test 67 | public void uneededNodesGetRecycled() { 68 | when(sourceMock.getParent()).thenReturn(parentMock); 69 | when(parentMock.getParent()).thenReturn(grandparentMock); 70 | 71 | AccessibilityNodeInfo rootNode = testSubject.getRootNodeFromSource(sourceMock); 72 | verify(sourceMock, never()).recycle(); 73 | verify(rootNode, never()).recycle(); 74 | verify(parentMock, times(1)).recycle(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/FocusVisualizerStylesTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import java.util.HashMap; 9 | import org.junit.After; 10 | import org.junit.Assert; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.MockedConstruction; 15 | import org.mockito.MockedStatic; 16 | import org.mockito.Mockito; 17 | import org.mockito.junit.MockitoJUnitRunner; 18 | 19 | @RunWith(MockitoJUnitRunner.class) 20 | public class FocusVisualizerStylesTest { 21 | 22 | FocusVisualizerStyles testSubject; 23 | 24 | MockedConstruction paintConstructionMock; 25 | MockedStatic colorStaticMock; 26 | 27 | @Before 28 | public void prepare() throws Exception { 29 | paintConstructionMock = Mockito.mockConstruction(Paint.class); 30 | colorStaticMock = Mockito.mockStatic(Color.class); 31 | testSubject = new FocusVisualizerStyles(); 32 | } 33 | 34 | @After 35 | public void cleanUp() { 36 | colorStaticMock.close(); 37 | paintConstructionMock.close(); 38 | } 39 | 40 | @Test 41 | public void getCurrentElementPaintsReturnsAllRelevantPaints() { 42 | HashMap paints = testSubject.getCurrentElementPaints(); 43 | Assert.assertNotNull(paints.get("outerCircle")); 44 | Assert.assertNotNull(paints.get("innerCircle")); 45 | Assert.assertNotNull(paints.get("number")); 46 | Assert.assertNotNull(paints.get("transparentInnerCircle")); 47 | } 48 | 49 | @Test 50 | public void getNonCurrentElementPaintsReturnsAllRelevantPaints() { 51 | HashMap paints = testSubject.getNonCurrentElementPaints(); 52 | Assert.assertNotNull(paints.get("outerCircle")); 53 | Assert.assertNotNull(paints.get("innerCircle")); 54 | Assert.assertNotNull(paints.get("number")); 55 | } 56 | 57 | @Test 58 | public void getNonCurrentLinePaintsReturnsAllRelevantPaints() { 59 | HashMap paints = testSubject.getNonCurrentLinePaints(); 60 | Assert.assertNotNull(paints.get("foregroundLine")); 61 | Assert.assertNotNull(paints.get("backgroundLine")); 62 | } 63 | 64 | @Test 65 | public void getCurrentLinePaintsReturnsAllRelevantPaints() { 66 | HashMap paints = testSubject.getCurrentLinePaints(); 67 | Assert.assertNotNull(paints.get("foregroundLine")); 68 | Assert.assertNotNull(paints.get("backgroundLine")); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/EventHelperTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.Mockito.times; 7 | import static org.mockito.Mockito.verify; 8 | import static org.mockito.Mockito.when; 9 | 10 | import android.view.accessibility.AccessibilityNodeInfo; 11 | import org.junit.Assert; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.mockito.Mock; 16 | import org.mockito.junit.MockitoJUnitRunner; 17 | 18 | @RunWith(MockitoJUnitRunner.class) 19 | public class EventHelperTest { 20 | @Mock ThreadSafeSwapper mockSwapper; 21 | 22 | @Mock AccessibilityNodeInfo mockNodeInfo; 23 | 24 | @Mock AccessibilityNodeInfo mockLastNodeInfo; 25 | 26 | EventHelper testSubject; 27 | 28 | @Before 29 | public void prepare() { 30 | testSubject = new EventHelper(mockSwapper); 31 | } 32 | 33 | @Test 34 | public void eventHelperExists() { 35 | Assert.assertNotNull(testSubject); 36 | } 37 | 38 | @Test 39 | public void claimLastSourceReturnsExpectedNodeInfo() { 40 | when(mockSwapper.swap(null)).thenReturn(mockNodeInfo); 41 | 42 | AccessibilityNodeInfo actualResponse = testSubject.claimLastSource(); 43 | 44 | Assert.assertEquals(mockNodeInfo, actualResponse); 45 | } 46 | 47 | @Test 48 | public void claimLastSourceCallsSwapWithNullObjectOnlyOnce() { 49 | testSubject.claimLastSource(); 50 | 51 | verify(mockSwapper, times(1)).swap(null); 52 | } 53 | 54 | @Test 55 | public void restoreLastSourceCallsSetIfCurrentlyNullOnlyOnce() { 56 | testSubject.restoreLastSource(mockNodeInfo); 57 | 58 | verify(mockSwapper, times(1)).setIfCurrentlyNull(mockNodeInfo); 59 | } 60 | 61 | @Test 62 | public void recordEventProperlyHandlesNonNullEventSource() { 63 | when(mockSwapper.swap(mockNodeInfo)).thenReturn(mockLastNodeInfo); 64 | 65 | testSubject.recordEvent(mockNodeInfo); 66 | 67 | verify(mockLastNodeInfo, times(1)).recycle(); 68 | } 69 | 70 | @Test 71 | public void recordEventProperlyHandlesNullEventSource() { 72 | testSubject.recordEvent(null); 73 | 74 | verify(mockSwapper, times(0)).swap(mockNodeInfo); 75 | verify(mockLastNodeInfo, times(0)).recycle(); 76 | } 77 | 78 | @Test 79 | public void recordEventProperlyHandlesNullLastSource() { 80 | when(mockSwapper.swap(mockNodeInfo)).thenReturn(null); 81 | 82 | testSubject.recordEvent(mockNodeInfo); 83 | 84 | verify(mockLastNodeInfo, times(0)).recycle(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/FocusVisualizationCanvasTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.ArgumentMatchers.any; 7 | import static org.mockito.Mockito.times; 8 | import static org.mockito.Mockito.verify; 9 | 10 | import android.content.Context; 11 | import android.graphics.Canvas; 12 | import java.util.ArrayList; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.MockitoJUnitRunner; 18 | 19 | @RunWith(MockitoJUnitRunner.class) 20 | public class FocusVisualizationCanvasTest { 21 | FocusVisualizationCanvas testSubject; 22 | 23 | @Mock Context contextMock; 24 | @Mock FocusElementHighlight focusElementHighlightMock; 25 | @Mock FocusElementLine focusElementLineMock; 26 | @Mock Canvas canvasMock; 27 | 28 | @Before 29 | public void prepare() { 30 | testSubject = new FocusVisualizationCanvas(contextMock); 31 | } 32 | 33 | @Test 34 | public void drawHighlightsAndLinesOnlyDrawsHighlightOnFirstPass() throws Exception { 35 | ArrayList lineStub = new ArrayList<>(); 36 | lineStub.add(focusElementLineMock); 37 | 38 | ArrayList highlightStub = new ArrayList<>(); 39 | highlightStub.add(focusElementHighlightMock); 40 | 41 | testSubject.setDrawItems(highlightStub, lineStub); 42 | testSubject.drawHighlightsAndLines(canvasMock); 43 | 44 | verify(focusElementHighlightMock, times(1)).drawElementHighlight(any(Canvas.class)); 45 | verify(focusElementLineMock, times(0)).drawLine(any(Canvas.class)); 46 | } 47 | 48 | @Test 49 | public void drawHighlightsAndLinesDrawsAllRelevantObjectsOnSubsequentPasses() throws Exception { 50 | ArrayList lineStub = new ArrayList<>(); 51 | lineStub.add(focusElementLineMock); 52 | lineStub.add(focusElementLineMock); 53 | 54 | ArrayList highlightStub = new ArrayList<>(); 55 | highlightStub.add(focusElementHighlightMock); 56 | highlightStub.add(focusElementHighlightMock); 57 | 58 | testSubject.setDrawItems(highlightStub, lineStub); 59 | testSubject.drawHighlightsAndLines(canvasMock); 60 | 61 | // Note: drawElementHighlight will call twice for each subsequent onDraw event. This is to 62 | // ensure that the line is drawn underneath the highlight, as the canvas drawings draw on 63 | // top of any previous drawings by default. 64 | verify(focusElementHighlightMock, times(3)).drawElementHighlight(any(Canvas.class)); 65 | verify(focusElementLineMock, times(1)).drawLine(any(Canvas.class)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/FocusElementLine.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Canvas; 7 | import android.graphics.Paint; 8 | import android.graphics.Rect; 9 | import android.view.View; 10 | import android.view.accessibility.AccessibilityNodeInfo; 11 | import java.util.HashMap; 12 | 13 | public class FocusElementLine { 14 | private AccessibilityNodeInfo eventSource; 15 | private AccessibilityNodeInfo previousEventSource; 16 | private int yOffset; 17 | private int xStart; 18 | private int yStart; 19 | private int xEnd; 20 | private int yEnd; 21 | private HashMap paints; 22 | private Rect currentRect; 23 | private Rect prevRect; 24 | private View view; 25 | 26 | public FocusElementLine( 27 | AccessibilityNodeInfo eventSource, 28 | AccessibilityNodeInfo previousEventSource, 29 | HashMap Paints, 30 | View view) { 31 | this.view = view; 32 | this.eventSource = eventSource; 33 | this.previousEventSource = previousEventSource; 34 | this.paints = Paints; 35 | this.currentRect = new Rect(); 36 | this.prevRect = new Rect(); 37 | } 38 | 39 | public void drawLine(Canvas canvas) { 40 | if (this.eventSource == null || this.previousEventSource == null) { 41 | return; 42 | } 43 | 44 | if (!this.eventSource.refresh() || !this.previousEventSource.refresh()) { 45 | return; 46 | } 47 | 48 | this.updateWithNewCoordinates(); 49 | this.drawConnectingLine( 50 | this.xStart, this.yStart, this.xEnd, this.yEnd, this.paints.get("backgroundLine"), canvas); 51 | this.drawConnectingLine( 52 | this.xStart, this.yStart, this.xEnd, this.yEnd, this.paints.get("foregroundLine"), canvas); 53 | } 54 | 55 | private void setCoordinates() { 56 | this.eventSource.getBoundsInScreen(this.currentRect); 57 | this.currentRect.offset(0, this.yOffset); 58 | 59 | this.previousEventSource.getBoundsInScreen(this.prevRect); 60 | this.prevRect.offset(0, this.yOffset); 61 | 62 | this.xStart = currentRect.centerX(); 63 | this.yStart = currentRect.centerY(); 64 | this.xEnd = prevRect.centerX(); 65 | this.yEnd = prevRect.centerY(); 66 | } 67 | 68 | private void drawConnectingLine( 69 | int xStart, int yStart, int xEnd, int yEnd, Paint paint, Canvas canvas) { 70 | canvas.drawLine(xStart, yStart, xEnd, yEnd, paint); 71 | } 72 | 73 | public void setPaint(HashMap paints) { 74 | this.paints = paints; 75 | } 76 | 77 | private void updateWithNewCoordinates() { 78 | this.yOffset = OffsetHelper.getYOffset(this.view); 79 | this.setCoordinates(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /npm-wrapper/README.md: -------------------------------------------------------------------------------- 1 | ## accessibility-insights-for-android-service-bin 2 | 3 | ### Notice 4 | The Accessibility Insights team is proud to provide accessibility scanning solutions across multiple platforms including Web, Windows Desktop, and Android. Unfortunately, the usage of Accessibility Insights for Android did not meet expectations and we will be **ending support of that product** effective June 1 2023, so we can focus on our more popular products for Web and Windows Desktop. The product download link will be removed to promote security and discourage use of an unmaintained version of the product. The source code will remain available under the MIT open-source license. We are grateful to the community who continue to use our accessibility testing products! 5 | 6 | This NPM package is CommonJS module that acts as a thin wrapper around the [Accessibility Insights for Android Service](../README.md) APK file. The package bundles a copy of the APK and exports the path and version of the bundled APK (and its associated NOTICE file). 7 | 8 | This wrapper package is intended for consumption by [Accessibility Insights for Android](https://github.com/microsoft/accessibility-insights-web); **we make no guarantees about its API stability or fitness for other purposes.** 9 | 10 | Note that the `apkPath` and `noticePath` exports are defined relative to `__dirname`, which means that they may not be usable in a bundled environment. For Accessibility Insights for Android, we read them during our build process and copy the files to a location our packaging setup understands. 11 | 12 | ### Versioning 13 | 14 | This wrapper package's version matches the version of the bundled APK. It **does not use semantic versioning**; we reserve the right to make breaking API changes to the wrapper package without a major version update. 15 | 16 | ### Typescript usage 17 | 18 | ```ts 19 | import { noticePath, apkPath, apkVersionName } from 'accessibility-insights-for-android-service-bin'; 20 | 21 | console.log(`Absolute path of the APK bundled with the package: ${apkPath}`); 22 | console.log(`APK_VERSION_NAME of the bundled APK: ${apkVersionName}`); 23 | console.log(`Absolute path of the NOTICE file for the APK: ${noticePath}`); 24 | ``` 25 | 26 | ### Reporting security vulnerabilities 27 | 28 | If you believe you have found a security vulnerability in this project, please follow [these steps](https://technet.microsoft.com/en-us/security/ff852094.aspx) to report it. For more information on how vulnerabilities are disclosed, see [Coordinated Vulnerability Disclosure](https://technet.microsoft.com/en-us/security/dn467923). 29 | 30 | ### Contributing 31 | 32 | All contributions are welcome! Please read through our guidelines on [contributions](../CONTRIBUTING.md) to this project. 33 | 34 | ### Code of Conduct 35 | 36 | Please read through our [Code of Conduct](../CODE_OF_CONDUCT.md) to this project. 37 | 38 | Android is a trademark of Google LLC. -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ATFAScanner.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.content.Context; 7 | import android.view.accessibility.AccessibilityNodeInfo; 8 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset; 9 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult; 10 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils; 11 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck; 12 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 13 | import com.google.android.apps.common.testing.accessibility.framework.Parameters; 14 | import com.google.android.apps.common.testing.accessibility.framework.uielement.AccessibilityHierarchyAndroid; 15 | import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.BitmapImage; 16 | import com.google.common.collect.ImmutableSet; 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.HashSet; 20 | import java.util.List; 21 | 22 | public class ATFAScanner { 23 | private final Context context; 24 | private final AccessibilityCheckResult.AccessibilityCheckResultType[] relevantResultTypes = { 25 | AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, 26 | AccessibilityCheckResult.AccessibilityCheckResultType.INFO, 27 | AccessibilityCheckResult.AccessibilityCheckResultType.WARNING, 28 | AccessibilityCheckResult.AccessibilityCheckResultType.RESOLVED, 29 | AccessibilityCheckResult.AccessibilityCheckResultType.NOT_RUN 30 | }; 31 | 32 | public ATFAScanner(Context context) { 33 | this.context = context; 34 | } 35 | 36 | public List scanWithATFA( 37 | AccessibilityNodeInfo rootNode, BitmapImage screenshot) { 38 | Parameters parameters = new Parameters(); 39 | parameters.setSaveViewImages(true); 40 | parameters.putCustomTouchTargetSize(44); // default is 48 but min size as defined by WCAG is 44 41 | parameters.putScreenCapture(screenshot); 42 | 43 | ImmutableSet checks = 44 | AccessibilityCheckPreset.getAccessibilityHierarchyChecksForPreset( 45 | AccessibilityCheckPreset.LATEST); 46 | AccessibilityHierarchyAndroid hierarchy = 47 | AccessibilityHierarchyAndroid.newBuilder(rootNode, this.context).build(); 48 | List results = new ArrayList<>(); 49 | 50 | for (AccessibilityHierarchyCheck check : checks) { 51 | results.addAll(check.runCheckOnHierarchy(hierarchy, null, parameters)); 52 | } 53 | 54 | return AccessibilityCheckResultUtils.getResultsForTypes( 55 | results, new HashSet<>(Arrays.asList(relevantResultTypes))); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | ## Security 8 | 9 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 10 | 11 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. 12 | 13 | ## Reporting Security Issues 14 | 15 | **Please do not report security vulnerabilities through public GitHub issues.** 16 | 17 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 18 | 19 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 20 | 21 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 22 | 23 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 24 | 25 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 26 | * Full paths of source file(s) related to the manifestation of the issue 27 | * The location of the affected source code (tag/branch/commit or direct URL) 28 | * Any special configuration required to reproduce the issue 29 | * Step-by-step instructions to reproduce the issue 30 | * Proof-of-concept or exploit code (if possible) 31 | * Impact of the issue, including how an attacker might exploit the issue 32 | 33 | This information will help us triage your report more quickly. 34 | 35 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 36 | 37 | ## Preferred Languages 38 | 39 | We prefer all communications to be in English. 40 | 41 | ## Policy 42 | 43 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 44 | 45 | 46 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo WARNING: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo Attempting to infer JAVA_HOME based on a default Android Studio 29 | echo installation. 30 | echo. 31 | 32 | set JAVA_HOME=%ProgramFiles%\Android\Android Studio\jre 33 | set JAVA_EXE=%JAVA_HOME%\bin\java.exe 34 | "%JAVA_EXE%" -version >NUL 2>&1 35 | if "%ERRORLEVEL%" == "0" goto init 36 | set JAVA_HOME= 37 | 38 | echo. 39 | echo ERROR: JAVA_HOME is not set, could not be inferred from your Android Studio 40 | echo installation, and no 'java' command could be found in your PATH. 41 | echo. 42 | echo Please set the JAVA_HOME variable in your environment to match the 43 | echo location of your Java installation. 44 | 45 | goto fail 46 | 47 | :findJavaFromJavaHome 48 | set JAVA_HOME=%JAVA_HOME:"=% 49 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 50 | 51 | if exist "%JAVA_EXE%" goto init 52 | 53 | echo. 54 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 55 | echo. 56 | echo Please set the JAVA_HOME variable in your environment to match the 57 | echo location of your Java installation. 58 | 59 | goto fail 60 | 61 | :init 62 | @rem Get command-line arguments, handling Windows variants 63 | 64 | if not "%OS%" == "Windows_NT" goto win9xME_args 65 | 66 | :win9xME_args 67 | @rem Slurp the command line arguments. 68 | set CMD_LINE_ARGS= 69 | set _SKIP=2 70 | 71 | :win9xME_args_slurp 72 | if "x%~1" == "x" goto execute 73 | 74 | set CMD_LINE_ARGS=%* 75 | 76 | :execute 77 | @rem Setup the command line 78 | 79 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 80 | 81 | @rem Execute Gradle 82 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 83 | 84 | :end 85 | @rem End local scope for the variables with windows NT shell 86 | if "%ERRORLEVEL%"=="0" goto mainEnd 87 | 88 | :fail 89 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 90 | rem the _cmd.exe /c_ return code! 91 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 92 | exit /b 1 93 | 94 | :mainEnd 95 | if "%OS%"=="Windows_NT" endlocal 96 | 97 | :omega 98 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/RequestDispatcher.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.os.CancellationSignal; 7 | import androidx.annotation.NonNull; 8 | 9 | public class RequestDispatcher { 10 | private static final String TAG = "RequestDispatcher"; 11 | 12 | private final RootNodeFinder rootNodeFinder; 13 | private final ScreenshotController screenshotController; 14 | private final EventHelper eventHelper; 15 | private final AxeScanner axeScanner; 16 | private final ATFAScanner atfaScanner; 17 | private final DeviceConfigFactory deviceConfigFactory; 18 | private final FocusVisualizationStateManager focusVisualizationStateManager; 19 | private final ResultsV2ContainerSerializer resultsV2ContainerSerializer; 20 | 21 | public RequestDispatcher( 22 | @NonNull RootNodeFinder rootNodeFinder, 23 | @NonNull ScreenshotController screenshotController, 24 | @NonNull EventHelper eventHelper, 25 | @NonNull AxeScanner axeScanner, 26 | @NonNull ATFAScanner atfaScanner, 27 | @NonNull DeviceConfigFactory deviceConfigFactory, 28 | @NonNull FocusVisualizationStateManager focusVisualizationStateManager, 29 | @NonNull ResultsV2ContainerSerializer resultsV2ContainerSerializer) { 30 | this.rootNodeFinder = rootNodeFinder; 31 | this.screenshotController = screenshotController; 32 | this.eventHelper = eventHelper; 33 | this.axeScanner = axeScanner; 34 | this.atfaScanner = atfaScanner; 35 | this.deviceConfigFactory = deviceConfigFactory; 36 | this.focusVisualizationStateManager = focusVisualizationStateManager; 37 | this.resultsV2ContainerSerializer = resultsV2ContainerSerializer; 38 | } 39 | 40 | public String request(@NonNull String method, @NonNull CancellationSignal cancellationSignal) 41 | throws Exception { 42 | Logger.logVerbose(TAG, "Handling request for method " + method); 43 | return getRequestFulfiller(method).fulfillRequest(cancellationSignal); 44 | } 45 | 46 | public RequestFulfiller getRequestFulfiller(@NonNull String method) { 47 | switch (method) { 48 | case "/config": 49 | return new ConfigRequestFulfiller(rootNodeFinder, eventHelper, deviceConfigFactory); 50 | case "/result": 51 | return new ResultV2RequestFulfiller( 52 | rootNodeFinder, 53 | eventHelper, 54 | axeScanner, 55 | atfaScanner, 56 | screenshotController, 57 | resultsV2ContainerSerializer); 58 | case "/FocusTracking/Enable": 59 | return new TabStopsRequestFulfiller(focusVisualizationStateManager, true); 60 | case "/FocusTracking/Disable": // Intentional fallthrough 61 | case "/FocusTracking/Reset": 62 | return new TabStopsRequestFulfiller(focusVisualizationStateManager, false); 63 | default: 64 | return new UnrecognizedRequestFulfiller(method); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/ScreenshotAxeImageTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.Mockito.times; 7 | import static org.mockito.Mockito.verify; 8 | import static org.mockito.Mockito.when; 9 | 10 | import android.graphics.Bitmap; 11 | import android.util.Base64; 12 | import com.deque.axe.android.colorcontrast.AxeColor; 13 | import com.deque.axe.android.wrappers.AxeRect; 14 | import java.io.ByteArrayOutputStream; 15 | import org.junit.Assert; 16 | import org.junit.Before; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | import org.mockito.Mock; 20 | import org.mockito.MockedStatic; 21 | import org.mockito.Mockito; 22 | import org.mockito.junit.MockitoJUnitRunner; 23 | 24 | @RunWith(MockitoJUnitRunner.class) 25 | public class ScreenshotAxeImageTest { 26 | 27 | @Mock Bitmap bitmapMock; 28 | @Mock ByteArrayOutputStreamProvider byteArrayOutputStreamProviderMock; 29 | @Mock ByteArrayOutputStream byteArrayOutputStreamMock; 30 | 31 | int sampleWidth; 32 | int sampleHeight; 33 | ScreenshotAxeImage testSubject; 34 | 35 | @Before 36 | public void prepare() { 37 | sampleHeight = 100; 38 | sampleWidth = 50; 39 | 40 | when(bitmapMock.getWidth()).thenReturn(sampleWidth); 41 | when(bitmapMock.getHeight()).thenReturn(sampleHeight); 42 | 43 | testSubject = new ScreenshotAxeImage(bitmapMock, byteArrayOutputStreamProviderMock); 44 | } 45 | 46 | @Test 47 | public void screenShotAxeImageExists() { 48 | Assert.assertNotNull(testSubject); 49 | } 50 | 51 | @Test 52 | public void pixelReturnsCorrectColor() { 53 | int givenX = 10; 54 | int givenY = 20; 55 | int colorIntStub = 100; 56 | 57 | when(bitmapMock.getPixel(givenX, givenY)).thenReturn(colorIntStub); 58 | 59 | AxeColor returnedAxeColor = testSubject.pixel(givenX, givenY); 60 | 61 | Assert.assertEquals(returnedAxeColor, new AxeColor(colorIntStub)); 62 | } 63 | 64 | @Test 65 | public void frameReturnsCorrectRect() { 66 | AxeRect expectedRect = new AxeRect(0, sampleWidth - 1, 0, sampleHeight - 1); 67 | 68 | Assert.assertEquals(testSubject.frame(), expectedRect); 69 | } 70 | 71 | @Test 72 | public void toBase64PngReturnsCorrectString() { 73 | byte[] byteArrayStub = new byte[1]; 74 | String expectedString = "some string"; 75 | 76 | try (MockedStatic base64StaticMock = Mockito.mockStatic(Base64.class)) { 77 | when(byteArrayOutputStreamProviderMock.get()).thenReturn(byteArrayOutputStreamMock); 78 | when(byteArrayOutputStreamMock.toByteArray()).thenReturn(byteArrayStub); 79 | base64StaticMock 80 | .when(() -> Base64.encodeToString(byteArrayStub, Base64.NO_WRAP)) 81 | .thenReturn(expectedString); 82 | 83 | Assert.assertEquals(testSubject.toBase64Png(), expectedString); 84 | 85 | verify(bitmapMock, times(1)) 86 | .compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStreamMock); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/AccessibilityEventDispatcher.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.view.accessibility.AccessibilityEvent; 7 | import android.view.accessibility.AccessibilityNodeInfo; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.function.Consumer; 12 | 13 | public class AccessibilityEventDispatcher { 14 | private static final String TAG = "AccessibilityEventDispatcher"; 15 | private CharSequence previousPackageName; 16 | 17 | private ArrayList> onAppChangedListeners; 18 | private ArrayList> onFocusEventListeners; 19 | private ArrayList> onRedrawEventListeners; 20 | 21 | public AccessibilityEventDispatcher() { 22 | onAppChangedListeners = new ArrayList>(); 23 | onFocusEventListeners = new ArrayList>(); 24 | onRedrawEventListeners = new ArrayList>(); 25 | } 26 | 27 | public static List redrawEventTypes = 28 | Arrays.asList( 29 | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 30 | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, 31 | AccessibilityEvent.TYPE_VIEW_SCROLLED, 32 | AccessibilityEvent.TYPE_WINDOWS_CHANGED); 33 | 34 | public void onAccessibilityEvent(AccessibilityEvent event, AccessibilityNodeInfo rootNode) { 35 | int eventType = event.getEventType(); 36 | 37 | if (rootNode != null 38 | && (previousPackageName == null 39 | || !previousPackageName.equals(rootNode.getPackageName()))) { 40 | previousPackageName = rootNode.getPackageName(); 41 | this.callListeners(onAppChangedListeners, rootNode); 42 | } 43 | 44 | if (isFocusEvent(eventType)) { 45 | this.callListeners(onFocusEventListeners, event); 46 | return; 47 | } 48 | 49 | if (isRedrawEvent(eventType)) { 50 | this.callListeners(onRedrawEventListeners, event); 51 | return; 52 | } 53 | } 54 | 55 | public void addOnFocusEventListener(Consumer listener) { 56 | onFocusEventListeners.add(listener); 57 | } 58 | 59 | public void addOnRedrawEventListener(Consumer listener) { 60 | onRedrawEventListeners.add(listener); 61 | } 62 | 63 | public void addOnAppChangedListener(Consumer listener) { 64 | onAppChangedListeners.add(listener); 65 | } 66 | 67 | private boolean isFocusEvent(int eventType) { 68 | return eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED; 69 | } 70 | 71 | private boolean isRedrawEvent(int eventType) { 72 | return redrawEventTypes.contains(eventType); 73 | } 74 | 75 | private void callListeners(ArrayList> listeners, T newValue) { 76 | listeners.forEach( 77 | listener -> { 78 | listener.accept(newValue); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/NodeViewBuilder.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Rect; 7 | import android.view.accessibility.AccessibilityNodeInfo; 8 | import com.deque.axe.android.AxeView; 9 | import com.deque.axe.android.wrappers.AxeRect; 10 | import java.util.List; 11 | 12 | public class NodeViewBuilder implements AxeView.Builder { 13 | private final AccessibilityNodeInfo accessibilityNode; 14 | private final List children; 15 | private final AxeView labeledBy; 16 | private final AxeRect boundsRect; 17 | 18 | public AxeRect boundsInScreen() { 19 | return boundsRect; 20 | } 21 | 22 | public String className() { 23 | String rawClassName = safeToString(accessibilityNode.getClassName()); 24 | return (rawClassName == null) 25 | ? "Class Name Not Specified--Inserted by Accessibility Insights" 26 | : rawClassName; 27 | } 28 | 29 | public String contentDescription() { 30 | return safeToString(accessibilityNode.getContentDescription()); 31 | } 32 | 33 | public boolean isAccessibilityFocusable() { 34 | return accessibilityNode.isFocusable(); 35 | } 36 | 37 | public boolean isClickable() { 38 | return accessibilityNode.isClickable(); 39 | } 40 | 41 | public boolean isEnabled() { 42 | return accessibilityNode.isEnabled(); 43 | } 44 | 45 | public boolean isImportantForAccessibility() { 46 | return accessibilityNode.isImportantForAccessibility(); 47 | } 48 | 49 | public AxeView labeledBy() { 50 | return labeledBy; 51 | } 52 | 53 | public String packageName() { 54 | return safeToString(accessibilityNode.getPackageName()); 55 | } 56 | 57 | public String paneTitle() { 58 | return null; 59 | } 60 | 61 | public String text() { 62 | return safeToString(accessibilityNode.getText()); 63 | } 64 | 65 | public String viewIdResourceName() { 66 | return accessibilityNode.getViewIdResourceName(); 67 | } 68 | 69 | public List children() { 70 | return children; 71 | } 72 | 73 | public String value() { 74 | return null; 75 | } 76 | 77 | public String hintText() { 78 | if (android.os.Build.VERSION.SDK_INT >= 26) { 79 | return safeToString(accessibilityNode.getHintText()); 80 | } 81 | return null; 82 | } 83 | 84 | public NodeViewBuilder( 85 | AccessibilityNodeInfo node, 86 | List children, 87 | AxeView labeledBy, 88 | AxeRectProvider boundsRectProvider) { 89 | accessibilityNode = node; 90 | this.children = children; 91 | this.labeledBy = labeledBy; 92 | 93 | Rect rect = new Rect(); 94 | accessibilityNode.getBoundsInScreen(rect); 95 | boundsRect = boundsRectProvider.createAxeRect(rect.left, rect.right, rect.top, rect.bottom); 96 | } 97 | 98 | private String safeToString(CharSequence chars) { 99 | if (chars == null) { 100 | return null; 101 | } 102 | 103 | return chars.toString(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/LoggerTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.util.Log; 7 | import org.junit.After; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.MockedStatic; 12 | import org.mockito.Mockito; 13 | import org.mockito.junit.MockitoJUnitRunner; 14 | 15 | @RunWith(MockitoJUnitRunner.class) 16 | public class LoggerTest { 17 | 18 | final String logTag = "logTag"; 19 | final String logMessage = "log message"; 20 | 21 | boolean originalEnableLogging; 22 | MockedStatic logStaticMock; 23 | 24 | @Before 25 | public void prepare() { 26 | logStaticMock = Mockito.mockStatic(Log.class); 27 | originalEnableLogging = Logger.ENABLE_LOGGING; 28 | } 29 | 30 | @After 31 | public void cleanUp() { 32 | Logger.ENABLE_LOGGING = originalEnableLogging; 33 | logStaticMock.close(); 34 | } 35 | 36 | @Test 37 | public void logVerboseDebugOn() { 38 | Logger.ENABLE_LOGGING = true; 39 | 40 | Logger.logVerbose(logTag, logMessage); 41 | 42 | logStaticMock.verify(() -> Log.v(logTag, logMessage)); 43 | } 44 | 45 | @Test 46 | public void logVerboseDebugOff() { 47 | Logger.ENABLE_LOGGING = false; 48 | 49 | Logger.logVerbose(logTag, logMessage); 50 | 51 | logStaticMock.verifyNoMoreInteractions(); 52 | } 53 | 54 | @Test 55 | public void logDebugDebugOn() { 56 | Logger.ENABLE_LOGGING = true; 57 | 58 | Logger.logDebug(logTag, logMessage); 59 | 60 | logStaticMock.verify(() -> Log.d(logTag, logMessage)); 61 | } 62 | 63 | @Test 64 | public void logDebugDebugOff() { 65 | Logger.ENABLE_LOGGING = false; 66 | 67 | Logger.logDebug(logTag, logMessage); 68 | 69 | logStaticMock.verifyNoMoreInteractions(); 70 | } 71 | 72 | @Test 73 | public void logErrorDebugOn() { 74 | Logger.ENABLE_LOGGING = true; 75 | 76 | Logger.logError(logTag, logMessage); 77 | 78 | logStaticMock.verify(() -> Log.e(logTag, logMessage)); 79 | } 80 | 81 | @Test 82 | public void logErrorDebugOff() { 83 | Logger.ENABLE_LOGGING = false; 84 | 85 | Logger.logError(logTag, logMessage); 86 | 87 | logStaticMock.verifyNoMoreInteractions(); 88 | } 89 | 90 | @Test 91 | public void logInfoDebugOn() { 92 | Logger.ENABLE_LOGGING = true; 93 | 94 | Logger.logInfo(logTag, logMessage); 95 | 96 | logStaticMock.verify(() -> Log.i(logTag, logMessage)); 97 | } 98 | 99 | @Test 100 | public void logInfoDebugOff() { 101 | Logger.ENABLE_LOGGING = false; 102 | 103 | Logger.logInfo(logTag, logMessage); 104 | 105 | logStaticMock.verifyNoMoreInteractions(); 106 | } 107 | 108 | @Test 109 | public void logWarningDebugOn() { 110 | Logger.ENABLE_LOGGING = true; 111 | 112 | Logger.logWarning(logTag, logMessage); 113 | 114 | logStaticMock.verify(() -> Log.w(logTag, logMessage)); 115 | } 116 | 117 | @Test 118 | public void logWarningDebugOff() { 119 | Logger.ENABLE_LOGGING = false; 120 | 121 | Logger.logWarning(logTag, logMessage); 122 | 123 | logStaticMock.verifyNoMoreInteractions(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 19 | 22 | 23 | 24 | 25 | 29 | 34 | 35 | 36 | 37 | 40 | 41 | 46 | 54 | 60 | 61 | 66 | 67 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/ConfigRequestFulfillerTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | import static org.mockito.ArgumentMatchers.any; 8 | import static org.mockito.Mockito.never; 9 | import static org.mockito.Mockito.reset; 10 | import static org.mockito.Mockito.times; 11 | import static org.mockito.Mockito.verify; 12 | import static org.mockito.Mockito.verifyNoInteractions; 13 | import static org.mockito.Mockito.when; 14 | 15 | import android.os.CancellationSignal; 16 | import android.view.accessibility.AccessibilityNodeInfo; 17 | import org.junit.Assert; 18 | import org.junit.Before; 19 | import org.junit.Test; 20 | import org.junit.runner.RunWith; 21 | import org.mockito.Mock; 22 | import org.mockito.junit.MockitoJUnitRunner; 23 | 24 | @RunWith(MockitoJUnitRunner.class) 25 | public class ConfigRequestFulfillerTest { 26 | 27 | @Mock RootNodeFinder rootNodeFinder; 28 | @Mock EventHelper eventHelper; 29 | @Mock DeviceConfigFactory deviceConfigFactory; 30 | @Mock AccessibilityNodeInfo sourceNodeMock; 31 | @Mock AccessibilityNodeInfo rootNodeMock; 32 | @Mock DeviceConfig deviceConfig; 33 | @Mock CancellationSignal cancellationSignal; 34 | 35 | String configJson = "test config"; 36 | 37 | ConfigRequestFulfiller testSubject; 38 | 39 | @Before 40 | public void prepare() { 41 | testSubject = new ConfigRequestFulfiller(rootNodeFinder, eventHelper, deviceConfigFactory); 42 | } 43 | 44 | @Test 45 | public void configRequestFulfillerExists() { 46 | Assert.assertNotNull(testSubject); 47 | } 48 | 49 | @Test 50 | public void writesCorrectResponse() { 51 | setupSuccessfulRequest(); 52 | 53 | assertEquals(configJson, testSubject.fulfillRequest(cancellationSignal)); 54 | } 55 | 56 | @Test 57 | public void recyclesNodes() { 58 | setupSuccessfulRequest(); 59 | 60 | testSubject.fulfillRequest(cancellationSignal); 61 | 62 | verify(rootNodeMock, times(1)).recycle(); 63 | verify(sourceNodeMock, times(1)).recycle(); 64 | } 65 | 66 | @Test 67 | public void recyclesNodeOnceIfRootEqualsSource() { 68 | setupSuccessfulRequest(); 69 | reset(rootNodeFinder); 70 | reset(deviceConfigFactory); 71 | when(rootNodeFinder.getRootNodeFromSource(any())).thenReturn(sourceNodeMock); 72 | when(deviceConfigFactory.getDeviceConfig(sourceNodeMock)).thenReturn(deviceConfig); 73 | 74 | testSubject.fulfillRequest(cancellationSignal); 75 | 76 | verifyNoInteractions(rootNodeMock); 77 | verify(sourceNodeMock, times(1)).recycle(); 78 | } 79 | 80 | @Test 81 | public void doesNotRecycleSourceIfRestoreLastSourceSucceeds() { 82 | setupSuccessfulRequest(); 83 | when(eventHelper.restoreLastSource(sourceNodeMock)).thenReturn(true); 84 | 85 | testSubject.fulfillRequest(cancellationSignal); 86 | verify(rootNodeMock, times(1)).recycle(); 87 | verify(sourceNodeMock, never()).recycle(); 88 | } 89 | 90 | private void setupSuccessfulRequest() { 91 | when(eventHelper.claimLastSource()).thenReturn(sourceNodeMock); 92 | when(rootNodeFinder.getRootNodeFromSource(sourceNodeMock)).thenReturn(rootNodeMock); 93 | when(deviceConfigFactory.getDeviceConfig(rootNodeMock)).thenReturn(deviceConfig); 94 | when(deviceConfig.toJson()).thenReturn(configJson); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/build.gradle: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 5 | 6 | buildscript { 7 | repositories { 8 | mavenCentral() 9 | exclusiveContent { 10 | forRepository { 11 | google() 12 | } 13 | filter { 14 | includeGroupByRegex("^androidx(\$|\\..*)") 15 | includeGroupByRegex("^com\\.android(\$|\\..*)") 16 | // Note: not all com.google.* cases come from google(), several are published 17 | // to mavenCentral only (eg, gson, protobuf, findbugs, guava, jimfs) 18 | includeGroup("com.google.android.apps.common.testing.accessibility.framework") 19 | includeGroup("com.google.android.material") 20 | includeGroup("com.google.test.platform") 21 | } 22 | } 23 | exclusiveContent { 24 | forRepository { 25 | gradlePluginPortal() 26 | } 27 | filter { 28 | includeGroup("org.jetbrains.trove4j") 29 | } 30 | } 31 | } 32 | dependencies { 33 | classpath 'com.android.tools.build:gradle:4.1.1' 34 | 35 | // NOTE: Do not place your application dependencies here; they belong 36 | // in the individual module build.gradle files 37 | } 38 | } 39 | 40 | plugins { 41 | id "com.diffplug.gradle.spotless" version "3.27.1" 42 | } 43 | 44 | allprojects { 45 | repositories { 46 | mavenCentral() 47 | exclusiveContent { 48 | forRepository { 49 | google() 50 | } 51 | filter { 52 | includeGroupByRegex("^androidx(\$|\\..*)") 53 | includeGroupByRegex("^com\\.android(\$|\\..*)") 54 | // Note: not all com.google.* cases come from google(), several are published 55 | // to mavenCentral only (eg, gson, protobuf, findbugs, guava, jimfs) 56 | includeGroup("com.google.android.apps.common.testing.accessibility.framework") 57 | includeGroup("com.google.android.material") 58 | includeGroup("com.google.test.platform") 59 | } 60 | } 61 | exclusiveContent { 62 | forRepository { 63 | gradlePluginPortal() 64 | } 65 | filter { 66 | includeGroup("org.jetbrains.trove4j") 67 | } 68 | } 69 | } 70 | } 71 | 72 | //task clean(type: Delete) { 73 | // delete rootProject.buildDir 74 | //} 75 | 76 | spotless { 77 | java { 78 | googleJavaFormat() 79 | target '**/*.java' 80 | } 81 | java { 82 | target '**/*.java' 83 | // These files have an APACHE license header, which we need to keep 84 | targetExclude '**/AccessibilityInsightsForAndroidService.java','**/AccessibilityNodeInfoSorter.java' 85 | licenseHeader '// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n' 86 | } 87 | } 88 | 89 | task fastpass { 90 | dependsOn 'spotlessCheck' 91 | dependsOn 'app:lint' 92 | } 93 | 94 | task fastpassFix { 95 | gradle.startParameter.writeDependencyLocks = true 96 | dependsOn 'spotlessApply' 97 | dependsOn 'app:lintFix' 98 | } 99 | 100 | dependencyLocking { 101 | lockAllConfigurations() 102 | } -------------------------------------------------------------------------------- /pipeline/validation-build.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | trigger: 5 | - main 6 | 7 | variables: 8 | Codeql.Enabled: true 9 | 10 | jobs: 11 | - job: 'gradlew_assemble' 12 | 13 | pool: 14 | vmImage: 'ubuntu-latest' 15 | 16 | steps: 17 | - task: Gradle@2 18 | displayName: build dev ai-android service 19 | inputs: 20 | workingDirectory: '$(system.defaultWorkingDirectory)/AccessibilityInsightsForAndroidService' 21 | gradleWrapperFile: 'AccessibilityInsightsForAndroidService/gradlew' 22 | gradleOptions: '-Xmx3072m' 23 | options: -S 24 | javaHomeOption: 'JDKVersion' 25 | jdkVersionOption: '1.11' 26 | jdkArchitectureOption: 'x64' 27 | tasks: 'assemble' 28 | 29 | - task: PublishPipelineArtifact@1 30 | displayName: publish artifact 31 | inputs: 32 | targetPath: '$(system.defaultWorkingDirectory)/AccessibilityInsightsForAndroidService/app/build/outputs/apk/debug/app-debug.apk' 33 | artifactName: debugApk 34 | 35 | - job: 'gradlew_build_test_lint' 36 | 37 | pool: 38 | vmImage: 'ubuntu-latest' 39 | 40 | steps: 41 | - task: Gradle@2 42 | displayName: build and test dev ai-android service 43 | inputs: 44 | workingDirectory: '$(system.defaultWorkingDirectory)/AccessibilityInsightsForAndroidService' 45 | gradleWrapperFile: 'AccessibilityInsightsForAndroidService/gradlew' 46 | gradleOptions: '-Xmx3072m' 47 | options: -S 48 | javaHomeOption: 'JDKVersion' 49 | jdkVersionOption: '1.11' 50 | jdkArchitectureOption: 'x64' 51 | publishJUnitResults: true 52 | testResultsFiles: '**/TEST-*.xml' 53 | tasks: 'build' 54 | 55 | - task: CopyFiles@2 56 | displayName: (lint-results) copy lint-results files to artifact staging 57 | condition: succeededOrFailed() 58 | inputs: 59 | contents: | 60 | lint-results.html 61 | lint-results.xml 62 | sourceFolder: '$(system.defaultWorkingDirectory)/AccessibilityInsightsForAndroidService/app/build/reports' 63 | targetFolder: '$(build.artifactstagingdirectory)/lint-results' 64 | 65 | - task: PublishPipelineArtifact@1 66 | displayName: (lint-results) publish lint-results artifact 67 | condition: succeededOrFailed() 68 | inputs: 69 | artifactName: 'lint-results' 70 | targetPath: '$(build.artifactstagingdirectory)/lint-results' 71 | 72 | - script: node $(system.defaultWorkingDirectory)/pipeline/verify-clean-lockfile.js 73 | displayName: verify that building did not change the lockfile unexpectedly 74 | 75 | - job: 'e2e_test' 76 | 77 | dependsOn: 'gradlew_assemble' 78 | 79 | pool: 80 | # per ADO docs, only macOS hosted agents support Android emulators. The emulator is installed on other agents, but didn't 81 | # function properly on Ubuntu when this was added. 82 | # https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/android?view=azure-devops#test-on-the-android-emulator 83 | vmImage: 'macOS-latest' 84 | 85 | strategy: 86 | matrix: 87 | API_24: { apiLevel: 24 } # minSdkVersion in build.gradle 88 | API_28: { apiLevel: 28 } # targetSdkVersion in build.gradle 89 | API_30: { apiLevel: 30 } # current most popular API level (Android 11) 90 | API_32: { apiLevel: 32 } # current latest API level 91 | 92 | steps: 93 | - template: ./e2e-emulator-template.yaml 94 | parameters: 95 | apiLevel: $(apiLevel) -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/FocusElementHighlight.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Canvas; 7 | import android.graphics.Paint; 8 | import android.graphics.Rect; 9 | import android.view.View; 10 | import android.view.accessibility.AccessibilityNodeInfo; 11 | import java.util.HashMap; 12 | 13 | public class FocusElementHighlight { 14 | private AccessibilityNodeInfo eventSource; 15 | private int yOffset; 16 | private int tabStopCount; 17 | private int radius; 18 | private int xCoordinate; 19 | private int yCoordinate; 20 | private HashMap paints; 21 | private Rect rect; 22 | private View view; 23 | private boolean isCurrentElement; 24 | private static final String TAG = "FocusElementHighlight"; 25 | 26 | public FocusElementHighlight( 27 | AccessibilityNodeInfo eventSource, 28 | HashMap currentPaints, 29 | int radius, 30 | int tabStopCount, 31 | View view) { 32 | this.view = view; 33 | this.eventSource = eventSource; 34 | this.tabStopCount = tabStopCount; 35 | this.radius = radius; 36 | this.rect = new Rect(); 37 | this.paints = currentPaints; 38 | this.isCurrentElement = true; 39 | } 40 | 41 | private void setCoordinates() { 42 | this.eventSource.getBoundsInScreen(this.rect); 43 | this.rect.offset(0, this.yOffset); 44 | this.xCoordinate = rect.centerX(); 45 | this.yCoordinate = rect.centerY(); 46 | } 47 | 48 | public void drawElementHighlight(Canvas canvas) { 49 | if (this.eventSource == null) { 50 | return; 51 | } 52 | 53 | if (!this.eventSource.refresh()) { 54 | return; 55 | } 56 | 57 | this.updateWithNewCoordinates(); 58 | 59 | if (isCurrentElement) { 60 | this.drawInnerCircle( 61 | this.xCoordinate, 62 | this.yCoordinate, 63 | this.radius, 64 | this.paints.get("transparentInnerCircle"), 65 | canvas); 66 | } else { 67 | this.drawInnerCircle( 68 | this.xCoordinate, this.yCoordinate, this.radius, this.paints.get("innerCircle"), canvas); 69 | this.drawNumberInCircle( 70 | this.xCoordinate, this.yCoordinate, this.tabStopCount, this.paints.get("number"), canvas); 71 | } 72 | 73 | this.drawOuterCircle( 74 | this.xCoordinate, this.yCoordinate, this.radius, this.paints.get("outerCircle"), canvas); 75 | } 76 | 77 | private void drawInnerCircle( 78 | int xCoordinate, int yCoordinate, int radius, Paint paint, Canvas canvas) { 79 | canvas.drawCircle(xCoordinate, yCoordinate, radius, paint); 80 | } 81 | 82 | private void drawOuterCircle( 83 | int xCoordinate, int yCoordinate, int radius, Paint paint, Canvas canvas) { 84 | canvas.drawCircle(xCoordinate, yCoordinate, radius + 3, paint); 85 | } 86 | 87 | private void drawNumberInCircle( 88 | int xCoordinate, int yCoordinate, int tabStopCount, Paint paint, Canvas canvas) { 89 | canvas.drawText( 90 | Integer.toString(tabStopCount), 91 | xCoordinate, 92 | yCoordinate - ((paint.descent() + paint.ascent()) / 2), 93 | paint); 94 | } 95 | 96 | public void setPaints(HashMap paints) { 97 | this.paints = paints; 98 | } 99 | 100 | public void setAsNonCurrentElement() { 101 | this.isCurrentElement = false; 102 | } 103 | 104 | public AccessibilityNodeInfo getEventSource() { 105 | return this.eventSource; 106 | } 107 | 108 | private void updateWithNewCoordinates() { 109 | this.yOffset = OffsetHelper.getYOffset(this.view); 110 | this.setCoordinates(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ScreenshotController.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Bitmap; 7 | import android.graphics.PixelFormat; 8 | import android.hardware.display.DisplayManager; 9 | import android.hardware.display.VirtualDisplay; 10 | import android.media.ImageReader; 11 | import android.media.projection.MediaProjection; 12 | import android.os.Handler; 13 | import android.util.DisplayMetrics; 14 | import java.util.function.Consumer; 15 | import java.util.function.Supplier; 16 | 17 | public class ScreenshotController { 18 | private final OnScreenshotAvailableProvider onScreenshotAvailableProvider; 19 | private DisplayMetrics metrics; 20 | private Handler screenshotHandler; 21 | private ImageReader imageReader; 22 | private Supplier displayMetricsSupplier; 23 | private VirtualDisplay display; 24 | private BitmapProvider bitmapProvider; 25 | private Supplier mediaProjectionSupplier; 26 | 27 | public ScreenshotController( 28 | Supplier displayMetricsSupplier, 29 | Handler handler, 30 | OnScreenshotAvailableProvider onScreenshotAvailableProvider, 31 | BitmapProvider bitmapProvider, 32 | Supplier mediaProjectionSupplier) { 33 | this.displayMetricsSupplier = displayMetricsSupplier; 34 | this.screenshotHandler = handler; 35 | this.onScreenshotAvailableProvider = onScreenshotAvailableProvider; 36 | this.bitmapProvider = bitmapProvider; 37 | this.mediaProjectionSupplier = mediaProjectionSupplier; 38 | } 39 | 40 | public void getScreenshotWithMediaProjection(Consumer bitmapConsumer) { 41 | MediaProjection sharedMediaProjection = mediaProjectionSupplier.get(); 42 | 43 | if (sharedMediaProjection == null) { 44 | bitmapConsumer.accept(null); 45 | return; 46 | } 47 | 48 | if (imageReader != null) { 49 | imageReader.close(); 50 | } 51 | 52 | if (display != null) { 53 | display.release(); 54 | } 55 | 56 | metrics = displayMetricsSupplier.get(); 57 | imageReader = getImageReader(metrics, bitmapConsumer); 58 | display = 59 | sharedMediaProjection.createVirtualDisplay( 60 | "myDisplay", 61 | metrics.widthPixels, 62 | metrics.heightPixels, 63 | metrics.densityDpi, 64 | DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, 65 | imageReader.getSurface(), 66 | null, 67 | null); 68 | } 69 | 70 | private ImageReader getImageReader(DisplayMetrics metrics, Consumer bitmapConsumer) { 71 | ImageReader imageReader = 72 | ImageReader.newInstance( 73 | metrics.widthPixels, 74 | metrics.heightPixels, 75 | // The linter gives a false positive here because it wants us to use one of the 76 | // ImageFormat.* constants, but ImageReader.newInstance documents the PixelFormat 77 | // constants as being acceptable, too. We don't control the choice of format; it's 78 | // determined by the input data we get from the system screenshot functionality. 79 | // 80 | // noinspection WrongConstant 81 | PixelFormat.RGBA_8888, 82 | 2); 83 | 84 | Consumer onBitmapAvailable = 85 | bitmap -> { 86 | display.release(); 87 | bitmapConsumer.accept(bitmap); 88 | }; 89 | 90 | OnScreenshotAvailable onScreenshotAvailable = 91 | onScreenshotAvailableProvider.getOnScreenshotAvailable( 92 | metrics, bitmapProvider, onBitmapAvailable); 93 | imageReader.setOnImageAvailableListener(onScreenshotAvailable, screenshotHandler); 94 | 95 | return imageReader; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/FocusVisualizer.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.view.accessibility.AccessibilityNodeInfo; 7 | import java.util.ArrayList; 8 | 9 | public class FocusVisualizer { 10 | private ArrayList focusElementHighlights; 11 | private ArrayList focusElementLines; 12 | private int tabStopCount; 13 | private FocusVisualizerStyles styles; 14 | private FocusVisualizationCanvas focusVisualizationCanvas; 15 | 16 | public FocusVisualizer( 17 | FocusVisualizerStyles focusVisualizerStyles, 18 | FocusVisualizationCanvas focusVisualizationCanvas) { 19 | this.focusElementHighlights = new ArrayList<>(); 20 | this.focusElementLines = new ArrayList<>(); 21 | this.tabStopCount = 0; 22 | this.styles = focusVisualizerStyles; 23 | this.focusVisualizationCanvas = focusVisualizationCanvas; 24 | } 25 | 26 | public void refreshHighlights() { 27 | this.focusVisualizationCanvas.redraw(); 28 | } 29 | 30 | public void addNewFocusedElement(AccessibilityNodeInfo eventSource) { 31 | tabStopCount++; 32 | 33 | AccessibilityNodeInfo previousEventSource = this.getPreviousEventSource(); 34 | 35 | if (this.focusElementHighlights.size() > 0) { 36 | this.setPreviousElementHighlightNonCurrent( 37 | this.focusElementHighlights.get(this.focusElementHighlights.size() - 1)); 38 | } 39 | if (focusElementLines.size() > 0) { 40 | this.setPreviousLineNonCurrent(this.focusElementLines.get(this.focusElementLines.size() - 1)); 41 | } 42 | 43 | this.createFocusElementHighlight(eventSource); 44 | this.createFocusElementLine(eventSource, previousEventSource); 45 | 46 | this.setDrawItemsAndRedraw(); 47 | } 48 | 49 | public void resetVisualizations() { 50 | this.tabStopCount = 0; 51 | this.focusElementHighlights.clear(); 52 | this.focusElementLines.clear(); 53 | this.setDrawItemsAndRedraw(); 54 | } 55 | 56 | private void setPreviousLineNonCurrent(FocusElementLine line) { 57 | line.setPaint(this.styles.getNonCurrentLinePaints()); 58 | } 59 | 60 | private void setPreviousElementHighlightNonCurrent(FocusElementHighlight focusElementHighlight) { 61 | focusElementHighlight.setAsNonCurrentElement(); 62 | focusElementHighlight.setPaints(this.styles.getNonCurrentElementPaints()); 63 | } 64 | 65 | private void createFocusElementLine( 66 | AccessibilityNodeInfo eventSource, AccessibilityNodeInfo previousEventSource) { 67 | FocusElementLine focusElementLine = 68 | new FocusElementLine( 69 | eventSource, 70 | previousEventSource, 71 | this.styles.getCurrentLinePaints(), 72 | this.focusVisualizationCanvas); 73 | this.focusElementLines.add(focusElementLine); 74 | } 75 | 76 | private void createFocusElementHighlight(AccessibilityNodeInfo eventSource) { 77 | FocusElementHighlight focusElementHighlight = 78 | new FocusElementHighlight( 79 | eventSource, 80 | this.styles.getCurrentElementPaints(), 81 | this.styles.focusElementHighlightRadius, 82 | this.tabStopCount, 83 | this.focusVisualizationCanvas); 84 | this.focusElementHighlights.add(focusElementHighlight); 85 | } 86 | 87 | private AccessibilityNodeInfo getPreviousEventSource() { 88 | if (this.focusElementHighlights.size() == 0) { 89 | return null; 90 | } 91 | return this.focusElementHighlights.get(this.focusElementHighlights.size() - 1).getEventSource(); 92 | } 93 | 94 | private void setDrawItemsAndRedraw() { 95 | this.focusVisualizationCanvas.setDrawItems(this.focusElementHighlights, this.focusElementLines); 96 | this.focusVisualizationCanvas.redraw(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/AccessibilityInsightsContentProvider.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.content.ContentProvider; 7 | import android.content.ContentValues; 8 | import android.database.Cursor; 9 | import android.net.Uri; 10 | import android.os.Binder; 11 | import android.os.Bundle; 12 | import android.os.CancellationSignal; 13 | import android.os.ParcelFileDescriptor; 14 | import androidx.annotation.NonNull; 15 | import androidx.annotation.Nullable; 16 | 17 | public class AccessibilityInsightsContentProvider extends ContentProvider { 18 | private SynchronizedRequestDispatcher requestDispatcher; 19 | private TempFileProvider tempFileProvider; 20 | 21 | @Override 22 | public boolean onCreate() { 23 | return onCreate( 24 | SynchronizedRequestDispatcher.SharedInstance, new TempFileProvider(this.getContext())); 25 | } 26 | 27 | public boolean onCreate( 28 | SynchronizedRequestDispatcher requestDispatcher, TempFileProvider tempFileProvider) { 29 | this.requestDispatcher = requestDispatcher; 30 | this.tempFileProvider = tempFileProvider; 31 | return true; 32 | } 33 | 34 | @Nullable 35 | @Override 36 | public Cursor query( 37 | @NonNull Uri uri, 38 | @Nullable String[] strings, 39 | @Nullable String s, 40 | @Nullable String[] strings1, 41 | @Nullable String s1) { 42 | return null; 43 | } 44 | 45 | @Nullable 46 | @Override 47 | public String getType(@NonNull Uri uri) { 48 | return null; 49 | } 50 | 51 | @Nullable 52 | @Override 53 | public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) { 54 | return null; 55 | } 56 | 57 | @Override 58 | public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) { 59 | return 0; 60 | } 61 | 62 | @Override 63 | public int update( 64 | @NonNull Uri uri, 65 | @Nullable ContentValues contentValues, 66 | @Nullable String s, 67 | @Nullable String[] strings) { 68 | return 0; 69 | } 70 | 71 | @Nullable 72 | @Override 73 | public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) { 74 | verifyCallerPermissions(); 75 | 76 | Bundle output = new Bundle(); 77 | 78 | try { 79 | String response = requestDispatcher.request("/" + method, new CancellationSignal()); 80 | output.putString("response", response); 81 | } catch (Exception e) { 82 | output.putString("response", e.toString()); 83 | } 84 | 85 | return output; 86 | } 87 | 88 | @Nullable 89 | @Override 90 | public ParcelFileDescriptor openFile( 91 | @NonNull Uri uri, @NonNull String mode, @Nullable CancellationSignal signal) { 92 | verifyCallerPermissions(); 93 | 94 | if (signal == null) { 95 | signal = new CancellationSignal(); 96 | } 97 | 98 | String method = uri.getPath(); 99 | 100 | String response; 101 | try { 102 | response = requestDispatcher.request(method, signal); 103 | } catch (Exception e) { 104 | response = e.toString(); 105 | } 106 | 107 | try { 108 | ParcelFileDescriptor file = 109 | ParcelFileDescriptor.open( 110 | tempFileProvider.createTempFileWithContents(response), 111 | ParcelFileDescriptor.MODE_READ_ONLY); 112 | return file; 113 | } catch (Exception e) { 114 | throw new RuntimeException(e); 115 | } 116 | } 117 | 118 | private final int AID_SHELL = 2000; // from android_filesystem_config.h 119 | 120 | private void verifyCallerPermissions() { 121 | if (Binder.getCallingUid() != AID_SHELL) { 122 | throw new SecurityException("This provider may only be queried via adb's shell user"); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/TempFileProvider.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.content.Context; 7 | import androidx.annotation.NonNull; 8 | import androidx.work.Data; 9 | import androidx.work.OneTimeWorkRequest; 10 | import androidx.work.WorkManager; 11 | import androidx.work.Worker; 12 | import androidx.work.WorkerParameters; 13 | import java.io.BufferedWriter; 14 | import java.io.File; 15 | import java.io.FileOutputStream; 16 | import java.io.IOException; 17 | import java.io.OutputStreamWriter; 18 | import java.nio.charset.StandardCharsets; 19 | import java.util.Date; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | public class TempFileProvider { 23 | // Avoid ever changing this; we want new versions of the app to be able to recognize and clean up 24 | // old versions' 25 | @NonNull 26 | private static final String tempDirName = 27 | "com.microsoft.accessibilityinsightsforandroidservice.TempFileProvider"; 28 | 29 | @NonNull public static final int tempFileLifetimeMillis = 5 * 60 * 1000; // 5 minutes 30 | @NonNull private File tempDir; 31 | @NonNull private WorkManager workManager; 32 | 33 | public TempFileProvider(Context context) { 34 | this(context, WorkManagerHolder.getWorkManager(context)); 35 | } 36 | 37 | public TempFileProvider(Context context, WorkManager workManager) { 38 | this.workManager = workManager; 39 | File cacheDir = context.getCacheDir(); 40 | String tempDirPath = cacheDir.getAbsolutePath() + File.separator + tempDirName; 41 | this.tempDir = new File(tempDirPath); 42 | } 43 | 44 | public void cleanOldFilesBestEffort() { 45 | cleanOldFilesBestEffort(tempDir); 46 | } 47 | 48 | private static void cleanOldFilesBestEffort(File tempDir) { 49 | long cutoffTime = new Date().getTime() - tempFileLifetimeMillis; 50 | File[] files = tempDir.listFiles(); 51 | if (files != null) { 52 | for (File file : files) { 53 | if (file.lastModified() < cutoffTime) { 54 | // We intentionally ignore failures (best-effort) 55 | // noinspection ResultOfMethodCallIgnored 56 | file.delete(); 57 | } 58 | } 59 | } 60 | } 61 | 62 | public File createTempFileWithContents(String contents) throws IOException { 63 | ensureTempDirExists(); 64 | File tempFile = File.createTempFile("TempFileProvider", "tmp", this.tempDir); 65 | try (BufferedWriter writer = 66 | new BufferedWriter( 67 | new OutputStreamWriter(new FileOutputStream(tempFile), StandardCharsets.UTF_8))) { 68 | writer.write(contents); 69 | writer.flush(); 70 | } 71 | scheduleCleanOldFiles(tempDir.getAbsolutePath()); 72 | return tempFile; 73 | } 74 | 75 | private void ensureTempDirExists() throws IOException { 76 | //noinspection ResultOfMethodCallIgnored 77 | this.tempDir.mkdir(); 78 | } 79 | 80 | private void scheduleCleanOldFiles(String tempDir) { 81 | Data inputData = new Data.Builder().putString("tempDir", tempDir).build(); 82 | OneTimeWorkRequest cleanFilesWorker = 83 | new OneTimeWorkRequest.Builder(CleanWorker.class) 84 | .setInitialDelay(tempFileLifetimeMillis, TimeUnit.MILLISECONDS) 85 | .setInputData(inputData) 86 | .build(); 87 | workManager.enqueue(cleanFilesWorker); 88 | } 89 | 90 | public static class CleanWorker extends Worker { 91 | private String tempDir; 92 | 93 | public CleanWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { 94 | super(context, workerParams); 95 | tempDir = workerParams.getInputData().getString("tempDir"); 96 | } 97 | 98 | @NonNull 99 | @Override 100 | public Result doWork() { 101 | cleanOldFilesBestEffort(new File(tempDir)); 102 | return Result.success(); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/AxeViewsFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.view.accessibility.AccessibilityNodeInfo; 7 | import com.deque.axe.android.AxeView; 8 | import java.util.ArrayList; 9 | import java.util.HashSet; 10 | import java.util.Hashtable; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Queue; 14 | import java.util.Set; 15 | 16 | public class AxeViewsFactory { 17 | 18 | private static final int maxRetries = 5; 19 | 20 | NodeViewBuilderFactory nodeViewBuilderFactory; 21 | AccessibilityNodeInfoQueueBuilder queueBuilder; 22 | Map axeMap; 23 | 24 | public AxeViewsFactory( 25 | NodeViewBuilderFactory nodeViewBuilderFactory, 26 | AccessibilityNodeInfoQueueBuilder queueBuilder) { 27 | this.nodeViewBuilderFactory = nodeViewBuilderFactory; 28 | this.queueBuilder = queueBuilder; 29 | } 30 | 31 | public AxeView createAxeViews(AccessibilityNodeInfo rootNode) throws ViewChangedException { 32 | return buildAxeViewsWithRetries(rootNode, maxRetries); 33 | } 34 | 35 | private AxeView buildAxeViewsWithRetries(AccessibilityNodeInfo rootNode, int retries) 36 | throws ViewChangedException { 37 | Queue> queue = queueBuilder.buildPriorityQueue(rootNode); 38 | axeMap = new Hashtable<>(); 39 | 40 | try { 41 | return buildAxeViews(queue, rootNode); 42 | } catch (ViewChangedException e) { 43 | if (retries > 0) { 44 | rootNode.refresh(); 45 | return buildAxeViewsWithRetries(rootNode, retries - 1); 46 | } else { 47 | throw new ViewChangedException("Failed after " + maxRetries + " attempts."); 48 | } 49 | } finally { 50 | recycleAllNodes(rootNode, queue); 51 | } 52 | } 53 | 54 | private AxeView buildAxeViews( 55 | Queue> queue, AccessibilityNodeInfo rootNode) 56 | throws ViewChangedException { 57 | OrderedValue nextOrderedNode; 58 | 59 | while ((nextOrderedNode = queue.poll()) != null) { 60 | AccessibilityNodeInfo node = nextOrderedNode.value; 61 | List children = getChildViews(node); 62 | AxeView labeledByView = getLabeledByView(node); 63 | AxeView nodeView = 64 | this.nodeViewBuilderFactory.createNodeViewBuilder(node, children, labeledByView).build(); 65 | axeMap.put(node, nodeView); 66 | } 67 | 68 | return axeMap.get(rootNode); 69 | } 70 | 71 | private AxeView getLabeledByView(AccessibilityNodeInfo node) { 72 | AxeView labeledByView = null; 73 | AccessibilityNodeInfo labeledByNode = node.getLabeledBy(); 74 | if (labeledByNode != null) { 75 | labeledByView = axeMap.get(labeledByNode); 76 | } 77 | 78 | return labeledByView; 79 | } 80 | 81 | private List getChildViews(AccessibilityNodeInfo node) throws ViewChangedException { 82 | int childCount = node.getChildCount(); 83 | List children = new ArrayList<>(childCount); 84 | 85 | for (int loop = 0; loop < childCount; loop++) { 86 | AccessibilityNodeInfo child = node.getChild(loop); 87 | if (child == null) { 88 | throw new ViewChangedException(); 89 | } 90 | children.add(axeMap.get(child)); 91 | } 92 | 93 | return children; 94 | } 95 | 96 | private void recycleAllNodes( 97 | AccessibilityNodeInfo rootNode, Queue> queue) { 98 | Set allNodes = new HashSet<>(axeMap.keySet()); 99 | for (OrderedValue orderedNode : queue) { 100 | allNodes.add(orderedNode.value); 101 | } 102 | 103 | for (AccessibilityNodeInfo node : allNodes) { 104 | if (node != rootNode && node.getClassName() != null) { 105 | node.recycle(); 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ResultV2RequestFulfiller.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.graphics.Bitmap; 7 | import android.os.CancellationSignal; 8 | import android.view.accessibility.AccessibilityNodeInfo; 9 | import com.deque.axe.android.AxeResult; 10 | import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult; 11 | import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.BitmapImage; 12 | import java.util.List; 13 | import java.util.concurrent.CountDownLatch; 14 | import java.util.concurrent.atomic.AtomicReference; 15 | 16 | public class ResultV2RequestFulfiller implements RequestFulfiller { 17 | private final RootNodeFinder rootNodeFinder; 18 | private final EventHelper eventHelper; 19 | private final AxeScanner axeScanner; 20 | private final ATFAScanner atfaScanner; 21 | private final ScreenshotController screenshotController; 22 | private final ResultsV2ContainerSerializer resultsV2ContainerSerializer; 23 | 24 | public ResultV2RequestFulfiller( 25 | RootNodeFinder rootNodeFinder, 26 | EventHelper eventHelper, 27 | AxeScanner axeScanner, 28 | ATFAScanner atfaScanner, 29 | ScreenshotController screenshotController, 30 | ResultsV2ContainerSerializer resultsV2ContainerSerializer) { 31 | this.rootNodeFinder = rootNodeFinder; 32 | this.eventHelper = eventHelper; 33 | this.axeScanner = axeScanner; 34 | this.atfaScanner = atfaScanner; 35 | this.screenshotController = screenshotController; 36 | this.resultsV2ContainerSerializer = resultsV2ContainerSerializer; 37 | } 38 | 39 | public String fulfillRequest(CancellationSignal cancellationSignal) throws Exception { 40 | AtomicReference successResponse = new AtomicReference<>(); 41 | AtomicReference errorResponse = new AtomicReference<>(); 42 | CountDownLatch doneSignal = new CountDownLatch(1); 43 | 44 | screenshotController.getScreenshotWithMediaProjection( 45 | screenshot -> { 46 | try { 47 | cancellationSignal.throwIfCanceled(); 48 | 49 | if (screenshot == null) { 50 | throw new Exception( 51 | "Could not acquire screenshot. Has the user granted screen recording permissions?"); 52 | } 53 | 54 | AccessibilityNodeInfo source = eventHelper.claimLastSource(); 55 | AccessibilityNodeInfo rootNode = rootNodeFinder.getRootNodeFromSource(source); 56 | 57 | successResponse.set(getScanContent(rootNode, screenshot, cancellationSignal)); 58 | 59 | if (rootNode != null && rootNode != source) { 60 | rootNode.recycle(); 61 | } 62 | if (source != null && !eventHelper.restoreLastSource(source)) { 63 | source.recycle(); 64 | } 65 | } catch (Exception e) { 66 | errorResponse.set(e); 67 | } 68 | doneSignal.countDown(); 69 | }); 70 | 71 | doneSignal.await(); 72 | if (errorResponse.get() != null) { 73 | throw errorResponse.get(); 74 | } 75 | return successResponse.get(); 76 | } 77 | 78 | private String getScanContent( 79 | AccessibilityNodeInfo rootNode, Bitmap screenshot, CancellationSignal cancellationSignal) 80 | throws ScanException, ViewChangedException { 81 | cancellationSignal.throwIfCanceled(); 82 | if (rootNode == null) { 83 | throw new ScanException("Unable to locate root node to scan"); 84 | } 85 | AxeResult axeResult = axeScanner.scanWithAxe(rootNode, screenshot); 86 | if (axeResult == null) { 87 | throw new ScanException("Scanner returned no data"); 88 | } 89 | 90 | cancellationSignal.throwIfCanceled(); 91 | List atfaResults = 92 | atfaScanner.scanWithATFA(rootNode, new BitmapImage(screenshot)); 93 | 94 | return resultsV2ContainerSerializer.createResultsJson(axeResult, atfaResults); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 121 |
122 |
-------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/FocusVisualizerController.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import android.view.WindowManager; 7 | import android.view.accessibility.AccessibilityEvent; 8 | import android.view.accessibility.AccessibilityNodeInfo; 9 | import java.util.Date; 10 | 11 | public class FocusVisualizerController { 12 | private FocusVisualizer focusVisualizer; 13 | private FocusVisualizationStateManager focusVisualizationStateManager; 14 | private UIThreadRunner uiThreadRunner; 15 | private WindowManager windowManager; 16 | private LayoutParamGenerator layoutParamGenerator; 17 | private FocusVisualizationCanvas focusVisualizationCanvas; 18 | private AccessibilityNodeInfo lastEventSource; 19 | private DateProvider dateProvider; 20 | private Date lastOrientationChange; 21 | private long maximumOrientationChangeDelay = 1000; 22 | 23 | public FocusVisualizerController( 24 | FocusVisualizer focusVisualizer, 25 | FocusVisualizationStateManager focusVisualizationStateManager, 26 | UIThreadRunner uiThreadRunner, 27 | WindowManager windowManager, 28 | LayoutParamGenerator layoutParamGenerator, 29 | FocusVisualizationCanvas focusVisualizationCanvas, 30 | DateProvider dateProvider) { 31 | this.focusVisualizer = focusVisualizer; 32 | this.focusVisualizationStateManager = focusVisualizationStateManager; 33 | this.uiThreadRunner = uiThreadRunner; 34 | this.windowManager = windowManager; 35 | this.layoutParamGenerator = layoutParamGenerator; 36 | this.focusVisualizationCanvas = focusVisualizationCanvas; 37 | this.dateProvider = dateProvider; 38 | this.focusVisualizationStateManager.subscribe(this::onFocusVisualizationStateChange); 39 | this.lastOrientationChange = dateProvider.get(); 40 | } 41 | 42 | public void onFocusEvent(AccessibilityEvent event) { 43 | lastEventSource = event.getSource(); 44 | if (focusVisualizationStateManager.getState() == false 45 | || ignoreFocusEventDueToRecentOrientationChange()) { 46 | return; 47 | } 48 | 49 | focusVisualizer.addNewFocusedElement(event.getSource()); 50 | } 51 | 52 | public void onRedrawEvent(AccessibilityEvent event) { 53 | if (focusVisualizationStateManager.getState() == false) { 54 | return; 55 | } 56 | 57 | focusVisualizer.refreshHighlights(); 58 | } 59 | 60 | public void onAppChanged(AccessibilityNodeInfo nodeInfo) { 61 | if (focusVisualizationStateManager.getState() == false) { 62 | return; 63 | } 64 | 65 | focusVisualizer.resetVisualizations(); 66 | } 67 | 68 | public void onOrientationChanged(Integer orientation) { 69 | if (focusVisualizationStateManager.getState() == false) { 70 | return; 71 | } 72 | lastOrientationChange = dateProvider.get(); 73 | windowManager.updateViewLayout(focusVisualizationCanvas, layoutParamGenerator.get()); 74 | focusVisualizer.resetVisualizations(); 75 | } 76 | 77 | private void onFocusVisualizationStateChange(boolean enabled) { 78 | if (enabled) { 79 | uiThreadRunner.run(this::addFocusVisualizationToScreen); 80 | } else { 81 | uiThreadRunner.run(this::removeFocusVisualizationToScreen); 82 | } 83 | } 84 | 85 | private void addFocusVisualizationToScreen() { 86 | if (lastEventSource != null) { 87 | focusVisualizer.addNewFocusedElement(lastEventSource); 88 | } 89 | windowManager.addView(focusVisualizationCanvas, layoutParamGenerator.get()); 90 | } 91 | 92 | private void removeFocusVisualizationToScreen() { 93 | focusVisualizer.resetVisualizations(); 94 | windowManager.removeView(focusVisualizationCanvas); 95 | } 96 | 97 | private boolean ignoreFocusEventDueToRecentOrientationChange() { 98 | Date currentTime = dateProvider.get(); 99 | long cur = currentTime.getTime(); 100 | long last = lastOrientationChange.getTime(); 101 | long timeSinceLastOrientationChange = cur - last; 102 | return timeSinceLastOrientationChange < maximumOrientationChangeDelay; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /AccessibilityInsightsForAndroidService/app/src/test/java/com/microsoft/accessibilityinsightsforandroidservice/AccessibilityNodeInfoQueueBuilderTest.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | package com.microsoft.accessibilityinsightsforandroidservice; 5 | 6 | import static org.mockito.Mockito.when; 7 | 8 | import android.view.accessibility.AccessibilityNodeInfo; 9 | import java.util.Queue; 10 | import org.junit.Assert; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.MockitoJUnitRunner; 16 | 17 | @RunWith(MockitoJUnitRunner.class) 18 | public class AccessibilityNodeInfoQueueBuilderTest { 19 | 20 | @Mock AccessibilityNodeInfo rootNode; 21 | @Mock AccessibilityNodeInfo childNode0; 22 | @Mock AccessibilityNodeInfo childNode1; 23 | @Mock AccessibilityNodeInfo grandchildNode; 24 | 25 | AccessibilityNodeInfoQueueBuilder testSubject; 26 | 27 | @Before 28 | public void prepare() { 29 | testSubject = new AccessibilityNodeInfoQueueBuilder(); 30 | } 31 | 32 | @Test 33 | public void buildEmptyQueue() { 34 | Queue> queue = testSubject.buildPriorityQueue(null); 35 | Assert.assertNotNull(queue); 36 | Assert.assertTrue(queue.isEmpty()); 37 | } 38 | 39 | @Test 40 | public void buildSingleNodeQueue() { 41 | Queue> queue = testSubject.buildPriorityQueue(rootNode); 42 | Assert.assertNotNull(queue); 43 | Assert.assertEquals(queue.size(), 1); 44 | 45 | assertNextQueueItemEquals(queue, rootNode, Long.MAX_VALUE); 46 | } 47 | 48 | @Test 49 | public void buildQueueWithChildren() { 50 | createChildren(); 51 | 52 | Queue> queue = testSubject.buildPriorityQueue(rootNode); 53 | 54 | long rootOrder = Long.MAX_VALUE; 55 | long childOrder = rootOrder - 1; 56 | long grandchildOrder = rootOrder - 2; 57 | 58 | Assert.assertEquals(queue.size(), 4); 59 | assertNextQueueItemEquals(queue, grandchildNode, grandchildOrder); 60 | // Since the child nodes have the same priority, they can appear here in any order, 61 | // so check for both ordering possibilities 62 | if (queue.peek().value == childNode0) { 63 | assertNextQueueItemEquals(queue, childNode0, childOrder); 64 | assertNextQueueItemEquals(queue, childNode1, childOrder); 65 | } else { 66 | assertNextQueueItemEquals(queue, childNode1, childOrder); 67 | assertNextQueueItemEquals(queue, childNode0, childOrder); 68 | } 69 | assertNextQueueItemEquals(queue, rootNode, rootOrder); 70 | } 71 | 72 | @Test 73 | public void buildQueueWithLabelNodes() { 74 | createChildren(); 75 | 76 | when(childNode0.getLabelFor()).thenReturn(childNode1); 77 | 78 | Queue> queue = testSubject.buildPriorityQueue(rootNode); 79 | 80 | long rootOrder = Long.MAX_VALUE; 81 | long child0Order = (rootOrder - 1) / 2; 82 | long child1Order = rootOrder - 1; 83 | long grandchildOrder = child0Order - 1; 84 | 85 | Assert.assertEquals(queue.size(), 4); 86 | assertNextQueueItemEquals(queue, grandchildNode, grandchildOrder); 87 | assertNextQueueItemEquals(queue, childNode0, child0Order); 88 | assertNextQueueItemEquals(queue, childNode1, child1Order); 89 | assertNextQueueItemEquals(queue, rootNode, rootOrder); 90 | } 91 | 92 | public void createChildren() { 93 | when(rootNode.getChildCount()).thenReturn(2); 94 | when(rootNode.getChild(0)).thenReturn(childNode0); 95 | when(rootNode.getChild(1)).thenReturn(childNode1); 96 | 97 | when(childNode0.getChildCount()).thenReturn(1); 98 | when(childNode0.getChild(0)).thenReturn(grandchildNode); 99 | } 100 | 101 | public void assertNextQueueItemEquals( 102 | Queue> queue, AccessibilityNodeInfo node, long priority) { 103 | OrderedValue nextItem = queue.poll(); 104 | Assert.assertNotNull(nextItem); 105 | Assert.assertEquals(nextItem.value, node); 106 | Assert.assertEquals(nextItem.order, priority); 107 | } 108 | } 109 | --------------------------------------------------------------------------------