This repository has been archived by the owner on Nov 16, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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:`).
- Loading branch information
Showing
44 changed files
with
1,242 additions
and
1,752 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
3 changes: 0 additions & 3 deletions
3
AccessibilityInsightsForAndroidService/.idea/runConfigurations.xml
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
126 changes: 126 additions & 0 deletions
126
...icrosoft/accessibilityinsightsforandroidservice/AccessibilityInsightsContentProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
...src/main/java/com/microsoft/accessibilityinsightsforandroidservice/RequestDispatcher.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.