Skip to content
This repository has been archived by the owner on Nov 16, 2023. It is now read-only.

Commit

Permalink
feat(andsvc-protocol): Replace local HTTP server with ContentProvider…
Browse files Browse the repository at this point in the history
…-based approach (#126)

#### Details
Currently, this service hosts a local HTTP server and returns its results in response to HTTP requests. This PR introduces a new communication protocol for the service which uses a [ContentProvider](https://developer.android.com/guide/topics/providers/content-providers)-based system of requests. The [updated readme](https://github.com/jalkire/accessibility-insights-for-android-service/blob/8dc52fa60e364f81948c925175216caa0717a357/README.md) is the best place to see how to interact with this protocol. 

At a high level:

- `AccessibilityInsightsForAndroidService` has replaced its initialization of the HTTP server setup/threads with instead performing similar setup against a new singleton `SynchronizedRequestDispatcher`.
- The new `AccessibilityInsightsContentProvider` is the entry point for the new forms of usage. It delegates requests to the same singleton `SynchronizedRequestDispatcher`.
- `SynchronizedRequestDispatcher`'s job is just to synchronize requests between the thread models of the content provider and service. It delegates the actual dispatch work to an underlying `RequestDispatcher`.
- `RequestDispatcher` replaces and simplifies the old `RequestHandlerFactory`. To more closely match the `ContentProvider` calling model, it and the `RequestFulfillers` now works synchronously but supports `CancellationSignals`, rather than working asynchronously in terms of callbacks.

##### Motivation
This is a more robust solution than the HTTP server.

##### Context
- V1 of the results are not ported over to the new protocol and are thus removed
- Credit goes to @dbjorge and @ThanyaLeif for implementation

#### Pull request checklist
<!-- If a checklist item is not applicable to this change, write "n/a" in the checkbox -->

- [n/a] Addresses an existing issue: #0000
- [x] Added/updated relevant unit test(s)
- [x] Ran `./gradlew fastpass` from `AccessibilityInsightsForAndroidService`
- [x] PR title _AND_ final merge commit title both start with a semantic tag (`fix:`, `chore:`, `feat(feature-name):`, `refactor:`).
  • Loading branch information
jalkire authored Aug 25, 2021
1 parent dd0a50a commit 602cda5
Show file tree
Hide file tree
Showing 44 changed files with 1,242 additions and 1,752 deletions.
3 changes: 1 addition & 2 deletions AccessibilityInsightsForAndroidService/.idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion AccessibilityInsightsForAndroidService/.idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,19 @@
android:excludeFromRecents="true"
android:exported="false"
android:label="@string/accessibility_service_label" />
<!--
SET_DEBUG_APP is a privileged system permission which is only available to com.android.shell.
Restricting the provider with that permission prevents any apps (except for adb) from
accessing scan data directly.
If this ever needs to be changed, be aware that the permissions granted to com.android.shell
have changed substantially between Android versions; make sure to test against downlevel cases.
-->
<provider
android:authorities="com.microsoft.accessibilityinsightsforandroidservice"
android:name="com.microsoft.accessibilityinsightsforandroidservice.AccessibilityInsightsContentProvider"
android:enabled="true"
android:exported="true"
android:permission="android.permission.SET_DEBUG_APP" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package com.microsoft.accessibilityinsightsforandroidservice;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class AccessibilityInsightsContentProvider extends ContentProvider {
private SynchronizedRequestDispatcher requestDispatcher;
private TempFileProvider tempFileProvider;

@Override
public boolean onCreate() {
return onCreate(
SynchronizedRequestDispatcher.SharedInstance,
new TempFileProvider(getContext().getCacheDir()));
}

public boolean onCreate(
SynchronizedRequestDispatcher requestDispatcher, TempFileProvider tempFileProvider) {
this.requestDispatcher = requestDispatcher;
this.tempFileProvider = tempFileProvider;
return true;
}

@Nullable
@Override
public Cursor query(
@NonNull Uri uri,
@Nullable String[] strings,
@Nullable String s,
@Nullable String[] strings1,
@Nullable String s1) {
return null;
}

@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}

@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
return null;
}

@Override
public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
return 0;
}

@Override
public int update(
@NonNull Uri uri,
@Nullable ContentValues contentValues,
@Nullable String s,
@Nullable String[] strings) {
return 0;
}

@Nullable
@Override
public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
verifyCallerPermissions();

Bundle output = new Bundle();

try {
String response = requestDispatcher.request("/" + method, new CancellationSignal());
output.putString("response", response);
} catch (Exception e) {
output.putString("response", e.toString());
}

return output;
}

@Nullable
@Override
public ParcelFileDescriptor openFile(
@NonNull Uri uri, @NonNull String mode, @Nullable CancellationSignal signal) {
verifyCallerPermissions();

if (signal == null) {
signal = new CancellationSignal();
}

String method = uri.getPath();

String response;
try {
response = requestDispatcher.request(method, signal);
} catch (Exception e) {
response = e.toString();
}

try {
ParcelFileDescriptor file =
ParcelFileDescriptor.open(
tempFileProvider.createTempFileWithContents(response),
ParcelFileDescriptor.MODE_READ_ONLY);
return file;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

private final int AID_SHELL = 2000; // from android_filesystem_config.h

private void verifyCallerPermissions() {
if (Binder.getCallingUid() != AID_SHELL) {
throw new SecurityException("This provider may only be queried via adb's shell user");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;

import com.google.gson.GsonBuilder;

public class AccessibilityInsightsForAndroidService extends AccessibilityService {
private static final String TAG = "AccessibilityInsightsForAndroidService";
private static ServerThread ServerThread = null;
private final AxeScanner axeScanner;
private final ATFAScanner atfaScanner;
private final EventHelper eventHelper;
Expand All @@ -46,6 +47,7 @@ public class AccessibilityInsightsForAndroidService extends AccessibilityService
private FocusVisualizationCanvas focusVisualizationCanvas;
private AccessibilityEventDispatcher accessibilityEventDispatcher;
private DeviceOrientationHandler deviceOrientationHandler;
private TempFileProvider tempFileProvider;

public AccessibilityInsightsForAndroidService() {
deviceConfigFactory = new DeviceConfigFactory();
Expand All @@ -61,18 +63,6 @@ private DisplayMetrics getRealDisplayMetrics() {
return DisplayMetricsHelper.getRealDisplayMetrics(this);
}

private void StopServerThread() {
if (ServerThread != null) {
ServerThread.exit();
try {
ServerThread.join();
} catch (InterruptedException e) {
Logger.logError(TAG, StackTrace.getStackTrace(e));
}
ServerThread = null;
}
}

private void stopScreenshotHandlerThread() {
if (screenshotHandlerThread != null) {
screenshotHandlerThread.quit();
Expand Down Expand Up @@ -111,7 +101,9 @@ protected void onServiceConnected() {
bitmapProvider,
MediaProjectionHolder::get);

StopServerThread();
SynchronizedRequestDispatcher.SharedInstance.teardown();
tempFileProvider = new TempFileProvider(getApplicationContext().getCacheDir());
tempFileProvider.cleanOldFilesBestEffort();

WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
focusVisualizationStateManager = new FocusVisualizationStateManager();
Expand All @@ -121,14 +113,16 @@ protected void onServiceConnected() {
focusVisualizerController = new FocusVisualizerController(focusVisualizer, focusVisualizationStateManager, new UIThreadRunner(), windowManager, layoutParamGenerator, focusVisualizationCanvas, new DateProvider());
accessibilityEventDispatcher = new AccessibilityEventDispatcher();
deviceOrientationHandler = new DeviceOrientationHandler(getResources().getConfiguration().orientation);
RootNodeFinder rootNodeFinder = new RootNodeFinder();
ResultsV2ContainerSerializer resultsV2ContainerSerializer = new ResultsV2ContainerSerializer(
new ATFARulesSerializer(),
new ATFAResultsSerializer(new GsonBuilder()),
new GsonBuilder());

setupFocusVisualizationListeners();

ResponseThreadFactory responseThreadFactory =
new ResponseThreadFactory(
screenshotController, eventHelper, axeScanner, atfaScanner, deviceConfigFactory, focusVisualizationStateManager);
ServerThread = new ServerThread(new ServerSocketFactory(), responseThreadFactory);
ServerThread.start();
RequestDispatcher requestDispatcher = new RequestDispatcher(rootNodeFinder, screenshotController, eventHelper, axeScanner, atfaScanner, deviceConfigFactory, focusVisualizationStateManager, resultsV2ContainerSerializer);
SynchronizedRequestDispatcher.SharedInstance.setup(requestDispatcher);
}

private void setupFocusVisualizationListeners() {
Expand All @@ -141,7 +135,8 @@ private void setupFocusVisualizationListeners() {
@Override
public boolean onUnbind(Intent intent) {
Logger.logVerbose(TAG, "*** onUnbind");
StopServerThread();
SynchronizedRequestDispatcher.SharedInstance.teardown();
tempFileProvider.cleanOldFilesBestEffort();
stopScreenshotHandlerThread();
MediaProjectionHolder.cleanUp();
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,36 @@

package com.microsoft.accessibilityinsightsforandroidservice;

import android.os.CancellationSignal;
import android.view.accessibility.AccessibilityNodeInfo;

public class ConfigRequestFulfiller implements RequestFulfiller {
private final RootNodeFinder rootNodeFinder;
private final EventHelper eventHelper;
private final DeviceConfigFactory deviceConfigFactory;
private final ResponseWriter responseWriter;

public ConfigRequestFulfiller(
ResponseWriter responseWriter,
RootNodeFinder rootNodeFinder,
EventHelper eventHelper,
DeviceConfigFactory deviceConfigFactory) {
this.responseWriter = responseWriter;
this.rootNodeFinder = rootNodeFinder;
this.deviceConfigFactory = deviceConfigFactory;
this.eventHelper = eventHelper;
}

public void fulfillRequest(RunnableFunction onRequestFulfilled) {
writeConfigResponse();
onRequestFulfilled.run();
}

@Override
public boolean isBlockingRequest() {
return true;
}

private void writeConfigResponse() {
public String fulfillRequest(CancellationSignal cancellationSignal) {
AccessibilityNodeInfo source = eventHelper.claimLastSource();
AccessibilityNodeInfo rootNode = rootNodeFinder.getRootNodeFromSource(source);

String content = deviceConfigFactory.getDeviceConfig(rootNode).toJson();
responseWriter.writeSuccessfulResponse(content);

if (rootNode != null && rootNode != source) {
rootNode.recycle();
}
if (source != null && !eventHelper.restoreLastSource(source)) {
source.recycle();
try {
return deviceConfigFactory.getDeviceConfig(rootNode).toJson();
} finally {
if (rootNode != null && rootNode != source) {
rootNode.recycle();
}
if (source != null && !eventHelper.restoreLastSource(source)) {
source.recycle();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package com.microsoft.accessibilityinsightsforandroidservice;

import android.os.CancellationSignal;
import androidx.annotation.NonNull;

public class RequestDispatcher {
private static final String TAG = "RequestDispatcher";

private final RootNodeFinder rootNodeFinder;
private final ScreenshotController screenshotController;
private final EventHelper eventHelper;
private final AxeScanner axeScanner;
private final ATFAScanner atfaScanner;
private final DeviceConfigFactory deviceConfigFactory;
private final FocusVisualizationStateManager focusVisualizationStateManager;
private final ResultsV2ContainerSerializer resultsV2ContainerSerializer;

public RequestDispatcher(
@NonNull RootNodeFinder rootNodeFinder,
@NonNull ScreenshotController screenshotController,
@NonNull EventHelper eventHelper,
@NonNull AxeScanner axeScanner,
@NonNull ATFAScanner atfaScanner,
@NonNull DeviceConfigFactory deviceConfigFactory,
@NonNull FocusVisualizationStateManager focusVisualizationStateManager,
@NonNull ResultsV2ContainerSerializer resultsV2ContainerSerializer) {
this.rootNodeFinder = rootNodeFinder;
this.screenshotController = screenshotController;
this.eventHelper = eventHelper;
this.axeScanner = axeScanner;
this.atfaScanner = atfaScanner;
this.deviceConfigFactory = deviceConfigFactory;
this.focusVisualizationStateManager = focusVisualizationStateManager;
this.resultsV2ContainerSerializer = resultsV2ContainerSerializer;
}

public String request(@NonNull String method, @NonNull CancellationSignal cancellationSignal)
throws Exception {
Logger.logVerbose(TAG, "Handling request for method " + method);
return getRequestFulfiller(method).fulfillRequest(cancellationSignal);
}

public RequestFulfiller getRequestFulfiller(@NonNull String method) {
switch (method) {
case "/config":
return new ConfigRequestFulfiller(rootNodeFinder, eventHelper, deviceConfigFactory);
case "/result":
return new ResultV2RequestFulfiller(
rootNodeFinder,
eventHelper,
axeScanner,
atfaScanner,
screenshotController,
resultsV2ContainerSerializer);
case "/FocusTracking/Enable":
return new TabStopsRequestFulfiller(focusVisualizationStateManager, true);
case "/FocusTracking/Disable": // Intentional fallthrough
case "/FocusTracking/Reset":
return new TabStopsRequestFulfiller(focusVisualizationStateManager, false);
default:
return new UnrecognizedRequestFulfiller(method);
}
}
}
Loading

0 comments on commit 602cda5

Please sign in to comment.