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

Commit 602cda5

Browse files
authored
feat(andsvc-protocol): Replace local HTTP server with ContentProvider-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:`).
1 parent dd0a50a commit 602cda5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1242
-1752
lines changed

AccessibilityInsightsForAndroidService/.idea/gradle.xml

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

AccessibilityInsightsForAndroidService/.idea/misc.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

AccessibilityInsightsForAndroidService/.idea/runConfigurations.xml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

AccessibilityInsightsForAndroidService/app/src/main/AndroidManifest.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,19 @@
4242
android:excludeFromRecents="true"
4343
android:exported="false"
4444
android:label="@string/accessibility_service_label" />
45+
<!--
46+
SET_DEBUG_APP is a privileged system permission which is only available to com.android.shell.
47+
Restricting the provider with that permission prevents any apps (except for adb) from
48+
accessing scan data directly.
49+
50+
If this ever needs to be changed, be aware that the permissions granted to com.android.shell
51+
have changed substantially between Android versions; make sure to test against downlevel cases.
52+
-->
53+
<provider
54+
android:authorities="com.microsoft.accessibilityinsightsforandroidservice"
55+
android:name="com.microsoft.accessibilityinsightsforandroidservice.AccessibilityInsightsContentProvider"
56+
android:enabled="true"
57+
android:exported="true"
58+
android:permission="android.permission.SET_DEBUG_APP" />
4559
</application>
4660
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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,
25+
new TempFileProvider(getContext().getCacheDir()));
26+
}
27+
28+
public boolean onCreate(
29+
SynchronizedRequestDispatcher requestDispatcher, TempFileProvider tempFileProvider) {
30+
this.requestDispatcher = requestDispatcher;
31+
this.tempFileProvider = tempFileProvider;
32+
return true;
33+
}
34+
35+
@Nullable
36+
@Override
37+
public Cursor query(
38+
@NonNull Uri uri,
39+
@Nullable String[] strings,
40+
@Nullable String s,
41+
@Nullable String[] strings1,
42+
@Nullable String s1) {
43+
return null;
44+
}
45+
46+
@Nullable
47+
@Override
48+
public String getType(@NonNull Uri uri) {
49+
return null;
50+
}
51+
52+
@Nullable
53+
@Override
54+
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
55+
return null;
56+
}
57+
58+
@Override
59+
public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
60+
return 0;
61+
}
62+
63+
@Override
64+
public int update(
65+
@NonNull Uri uri,
66+
@Nullable ContentValues contentValues,
67+
@Nullable String s,
68+
@Nullable String[] strings) {
69+
return 0;
70+
}
71+
72+
@Nullable
73+
@Override
74+
public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
75+
verifyCallerPermissions();
76+
77+
Bundle output = new Bundle();
78+
79+
try {
80+
String response = requestDispatcher.request("/" + method, new CancellationSignal());
81+
output.putString("response", response);
82+
} catch (Exception e) {
83+
output.putString("response", e.toString());
84+
}
85+
86+
return output;
87+
}
88+
89+
@Nullable
90+
@Override
91+
public ParcelFileDescriptor openFile(
92+
@NonNull Uri uri, @NonNull String mode, @Nullable CancellationSignal signal) {
93+
verifyCallerPermissions();
94+
95+
if (signal == null) {
96+
signal = new CancellationSignal();
97+
}
98+
99+
String method = uri.getPath();
100+
101+
String response;
102+
try {
103+
response = requestDispatcher.request(method, signal);
104+
} catch (Exception e) {
105+
response = e.toString();
106+
}
107+
108+
try {
109+
ParcelFileDescriptor file =
110+
ParcelFileDescriptor.open(
111+
tempFileProvider.createTempFileWithContents(response),
112+
ParcelFileDescriptor.MODE_READ_ONLY);
113+
return file;
114+
} catch (Exception e) {
115+
throw new RuntimeException(e);
116+
}
117+
}
118+
119+
private final int AID_SHELL = 2000; // from android_filesystem_config.h
120+
121+
private void verifyCallerPermissions() {
122+
if (Binder.getCallingUid() != AID_SHELL) {
123+
throw new SecurityException("This provider may only be queried via adb's shell user");
124+
}
125+
}
126+
}

AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/AccessibilityInsightsForAndroidService.java

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@
2727
import android.view.WindowManager;
2828
import android.view.accessibility.AccessibilityEvent;
2929

30+
import com.google.gson.GsonBuilder;
31+
3032
public class AccessibilityInsightsForAndroidService extends AccessibilityService {
3133
private static final String TAG = "AccessibilityInsightsForAndroidService";
32-
private static ServerThread ServerThread = null;
3334
private final AxeScanner axeScanner;
3435
private final ATFAScanner atfaScanner;
3536
private final EventHelper eventHelper;
@@ -46,6 +47,7 @@ public class AccessibilityInsightsForAndroidService extends AccessibilityService
4647
private FocusVisualizationCanvas focusVisualizationCanvas;
4748
private AccessibilityEventDispatcher accessibilityEventDispatcher;
4849
private DeviceOrientationHandler deviceOrientationHandler;
50+
private TempFileProvider tempFileProvider;
4951

5052
public AccessibilityInsightsForAndroidService() {
5153
deviceConfigFactory = new DeviceConfigFactory();
@@ -61,18 +63,6 @@ private DisplayMetrics getRealDisplayMetrics() {
6163
return DisplayMetricsHelper.getRealDisplayMetrics(this);
6264
}
6365

64-
private void StopServerThread() {
65-
if (ServerThread != null) {
66-
ServerThread.exit();
67-
try {
68-
ServerThread.join();
69-
} catch (InterruptedException e) {
70-
Logger.logError(TAG, StackTrace.getStackTrace(e));
71-
}
72-
ServerThread = null;
73-
}
74-
}
75-
7666
private void stopScreenshotHandlerThread() {
7767
if (screenshotHandlerThread != null) {
7868
screenshotHandlerThread.quit();
@@ -111,7 +101,9 @@ protected void onServiceConnected() {
111101
bitmapProvider,
112102
MediaProjectionHolder::get);
113103

114-
StopServerThread();
104+
SynchronizedRequestDispatcher.SharedInstance.teardown();
105+
tempFileProvider = new TempFileProvider(getApplicationContext().getCacheDir());
106+
tempFileProvider.cleanOldFilesBestEffort();
115107

116108
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
117109
focusVisualizationStateManager = new FocusVisualizationStateManager();
@@ -121,14 +113,16 @@ protected void onServiceConnected() {
121113
focusVisualizerController = new FocusVisualizerController(focusVisualizer, focusVisualizationStateManager, new UIThreadRunner(), windowManager, layoutParamGenerator, focusVisualizationCanvas, new DateProvider());
122114
accessibilityEventDispatcher = new AccessibilityEventDispatcher();
123115
deviceOrientationHandler = new DeviceOrientationHandler(getResources().getConfiguration().orientation);
116+
RootNodeFinder rootNodeFinder = new RootNodeFinder();
117+
ResultsV2ContainerSerializer resultsV2ContainerSerializer = new ResultsV2ContainerSerializer(
118+
new ATFARulesSerializer(),
119+
new ATFAResultsSerializer(new GsonBuilder()),
120+
new GsonBuilder());
124121

125122
setupFocusVisualizationListeners();
126123

127-
ResponseThreadFactory responseThreadFactory =
128-
new ResponseThreadFactory(
129-
screenshotController, eventHelper, axeScanner, atfaScanner, deviceConfigFactory, focusVisualizationStateManager);
130-
ServerThread = new ServerThread(new ServerSocketFactory(), responseThreadFactory);
131-
ServerThread.start();
124+
RequestDispatcher requestDispatcher = new RequestDispatcher(rootNodeFinder, screenshotController, eventHelper, axeScanner, atfaScanner, deviceConfigFactory, focusVisualizationStateManager, resultsV2ContainerSerializer);
125+
SynchronizedRequestDispatcher.SharedInstance.setup(requestDispatcher);
132126
}
133127

134128
private void setupFocusVisualizationListeners() {
@@ -141,7 +135,8 @@ private void setupFocusVisualizationListeners() {
141135
@Override
142136
public boolean onUnbind(Intent intent) {
143137
Logger.logVerbose(TAG, "*** onUnbind");
144-
StopServerThread();
138+
SynchronizedRequestDispatcher.SharedInstance.teardown();
139+
tempFileProvider.cleanOldFilesBestEffort();
145140
stopScreenshotHandlerThread();
146141
MediaProjectionHolder.cleanUp();
147142
return false;

AccessibilityInsightsForAndroidService/app/src/main/java/com/microsoft/accessibilityinsightsforandroidservice/ConfigRequestFulfiller.java

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,36 @@
33

44
package com.microsoft.accessibilityinsightsforandroidservice;
55

6+
import android.os.CancellationSignal;
67
import android.view.accessibility.AccessibilityNodeInfo;
78

89
public class ConfigRequestFulfiller implements RequestFulfiller {
910
private final RootNodeFinder rootNodeFinder;
1011
private final EventHelper eventHelper;
1112
private final DeviceConfigFactory deviceConfigFactory;
12-
private final ResponseWriter responseWriter;
1313

1414
public ConfigRequestFulfiller(
15-
ResponseWriter responseWriter,
1615
RootNodeFinder rootNodeFinder,
1716
EventHelper eventHelper,
1817
DeviceConfigFactory deviceConfigFactory) {
19-
this.responseWriter = responseWriter;
2018
this.rootNodeFinder = rootNodeFinder;
2119
this.deviceConfigFactory = deviceConfigFactory;
2220
this.eventHelper = eventHelper;
2321
}
2422

25-
public void fulfillRequest(RunnableFunction onRequestFulfilled) {
26-
writeConfigResponse();
27-
onRequestFulfilled.run();
28-
}
29-
30-
@Override
31-
public boolean isBlockingRequest() {
32-
return true;
33-
}
34-
35-
private void writeConfigResponse() {
23+
public String fulfillRequest(CancellationSignal cancellationSignal) {
3624
AccessibilityNodeInfo source = eventHelper.claimLastSource();
3725
AccessibilityNodeInfo rootNode = rootNodeFinder.getRootNodeFromSource(source);
3826

39-
String content = deviceConfigFactory.getDeviceConfig(rootNode).toJson();
40-
responseWriter.writeSuccessfulResponse(content);
41-
42-
if (rootNode != null && rootNode != source) {
43-
rootNode.recycle();
44-
}
45-
if (source != null && !eventHelper.restoreLastSource(source)) {
46-
source.recycle();
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+
}
4736
}
4837
}
4938
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
}

0 commit comments

Comments
 (0)