I tried to create an Android Accessibility Service to detect all Elements in an App and show Accessibility Information like Content Description or Labeled by.
At the moment I can, using the AccessibilityService Class, log the Node Hierarchy when opening an App:
switch (evt.getEventType()) {
case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
logNodes(getRootInActiveWindow(), 0);
But how can I get the information only of a View Element that was tapped/clicked? Is there an easy way, using Accessibility Events to do so?
If I use for example AccessibilityEvent.TYPE_VIEW_CLICKED I can detect clickable View Elements like Buttons or Checkboxes by clicking on them. But is there an Accessibility Event to get non clickable View Elements like Text-Views?
I know that Google's talback speaks whatever you tap on, using AccessibilityServices, and it doesn’t matter if it is a button or any other view Element.
Related
I'm implementing a screen for Android TV, which has a screen title and a button on the left side. And a list of custom views/rows(selectable/clickable), arranged vertically, on the right side of the page.
We want the button on the left to be in focus when the user sees that screen. For that, I'm calling button.requestFocus() in the onResume() of the fragment.
This breaks the accessibility. When talkback is enabled, the first thing announced is the button's label. What I want is to announce the title first and then the button's label.
I tried to announce a custom text(could be title) by
rootView.announceForAccessibility(accessibilityText)
where rootView is the root of the xml layout and accessibilityText a text which needs to be announced.
But it doesn't help, and the button's label gets the priority.
How can I solve the issue?
I would ask you to consider WCAG Guideline 3.2.1:
The intent of this Success Criterion is to ensure that functionality is predictable as visitors navigate their way through a document. Any component that is able to trigger an event when it receives focus must not change the context. Examples of changing context when a component receives focus include, but are not limited to:
forms submitted automatically when a component receives focus;
new windows launched when a component receives focus;
focus is changed to another component when that component receives focus; <-- emphasis here
Also a quote from the Android Accessibility Team:
So something similar that people like to do is manage accessibility focus themselves. And again, this is a bad idea. accessibility focus has to be determined by the accessibility service, and just like announcements this creates an inconsistency in experience. And actually, that one of the biggest issues that accessibility users face, inconsistency, across applications and over time.
With that said, you may want to consider looking at ensuring the focus order / priority of the component using the following attributes:
android:nextFocusUp
android:nextFocusDown
android:nextFocusLeft
android:nextFocusRight
And also ensure that any group component that may get highlighted has the importantForAccessibility attribute set.
I'd like to try help some more, but without an example XML file, it's difficult to get to your particular use case. Have you tried testing the view layout with accessibility users?
I took a cue from this article by ATAUL MUNIM. I added a check if talkback is enabled, before requesting the focus explicitly.
protected fun isTalkBackEnabled(): Boolean {
val a11yServices = context?.getSystemService(ACCESSIBILITY_SERVICE) as? AccessibilityManager
return a11yServices?.isTouchExplorationEnabled?:false
}
and
if(isTalkBackEnabled().not()) {
button.requestFocus()
}
This solution pretty much bailed me out from the problem I was facing. It was also the only way forward for me because my app's min API level is 21 which eliminates the option to use android:screenReaderFocusable
In my Android app I have a custom layout that is being used as a button - it consists of some TextViews and an ImageView, additionally it has some gradient background.
I'm aligning my app now to conform to the Accessibility rules. In order to do so, I would need to convert this layout into a button, so that TalkBack can correctly indicate the action, that this whole layout is clickable and serves like a button.
I know that on iOS there is a possibility to set the UIAccessibilityTraits to treat such view as a button - this kind of solution would save me a huge amount of work in terms of migration.
Is there any similar solution on Android for that? What approach should I follow in order to make this layout recognized correctly by TalkBack?
No, there's no concept of accessibility traits on Android - but you can still get a good accessibility experience without needing to specifically convert your layout into a Button.
Generally, it's most important that TalkBack (or whatever accessibility service is being used - remember, it's not just TalkBack) is able to detect that the widget is clickable and to be able to read a coherent description of what it does. The additional information that it's a button, specifically, isn't super useful, especially because there are so many different kinds of UI elements that it's often a very ambiguous question whether something even is a button.
You can test this by selecting it in TalkBack and confirming that it reads the content description properly, says something along the lines of "Double tap to activate," and performs the correct action when you double tap.
If it's not correct, make sure the content description, clickable flag, and click action are set correctly on the widget's AccessibilityNodeInfo.
It is common knowledge that we can observe or query the view hierarchy of any app with an AccessibilityService:
Create your own accessibility service.
It is also possible to perform actions on behalf of the user:
Developing an Accessibility Service for Android.
My question is, can we modify the view hierarchy of a foreground app with an AccessibilityService?
I have already referred the following questions:
How do I add and remove a layout programmatically from an accessibility service?
Get view of AccessibilityNodeInfo to create overlay.
What they're doing is using the WindowManager and the SYSTEM_ALERT_WINDOW permission to overlay a view on top of the app in the foreground. The problem with this approach is that if the user presses BACK or HOME, the app is dismissed, but the view remains visible on the screen, even after the app is gone. The view is on TOP of the view hierarchy, and not a part of it.
Is there a way to add / modify the AccessibilityNodeInfo objects?
Are these the same as a View or ViewGroup?
Are we allowed to add views or modify existing views with an AccessibilityService?
My requirement is to display a small view within the app itself.
It has to be part of the view hierarchy of the app, so that it stays
or goes with the app. For this I need to modify the view hierarchy of
the app, namely the AccessibilityNodeInfo objects retrieved from the
service.
What I want is something similar to addView(), but add the View to the view hierarchy of the app itself, not on top of it.
How can we do this? Is this possible?
UPDATE:
Apps that support Custom Views for Accessibility
No, you can't modify the view hierarchy of another app because it exists in a separate process.
This is similar to not being able to modify accessibility nodes from within an accessibility service.
1) You can exploit draw over other apps permission. That solution will allow you only to draw overlays over another apps and not to change another apps behavior.
2) You can exploit instrumentation test mechanism. If you have enough information about the app(app id, activity name) and enough privileges (Run an instrument test from within app and wait for result), or root privileges. Here is an example:
#RunWith(AndroidJUnit4::class)
class InjectView {
#get:Rule
val activityRule = ActivityTestRule<MainActivity>(MainActivity::class.java)
#Test
fun injectView() {
val rootLayout = activityRule.activity.findViewById<ViewGroup>(android.R.id.content)
activityRule.runOnUiThread {
rootLayout.addView(TextView(activityRule.activity).apply {
text = "Injected View"
})
}
Thread.sleep(10_000)
}
}
You can still go with the approach of drawing on top of the app, not as part of its view hierarchy (which is impossible) - using SYSTEM_ALERT_WINDOW permission.
In order to know when the app is dismissed and dismiss your own overlay - listen to the accessibility event TYPE_WINDOW_STATE_CHANGED and check that there's a package change.
You can also go further with listening to TYPE_WINDOW_CONTENT_CHANGED and determining that there was some layout update.
More accessibility events might come in handy or fine-tuning your overlay's accuracy.
In short - as long as you have the appropriate information to know about the underlying app's layout and when things happen, you can draw on top as if it's part of the app.
Might be tricky and require some calculations since you're not able to just push views into the hierarchy, but totally doable.
Using Android 4.2.2
I'm trying to write an AccessibilityService, and have most of the required features. I'm drawing on an overlay and allowing the user to select/cligk highlighted items via a bluetooth switch (the purpose is a disabled client wanting to interact with an android device using only one switch).
Whilst parsing a screen, I can get the root accessibiltyNodeInfo object, and all its children. I can highlight on the screen all such elements, and click a desired one by the .performAction() method.
On the home screen, there are 3 "panes" available, with the middle one being shown. Swipe left or right to see the others (standard launcher behaviour). There is a node that reports isScrollable = true, but the Action Flags do not report ACTION_SCROLL_FORWARD or ACTION_SCROLL_BACKWARDS. How do I scroll such a node, if I cannot call .performAction() on it as it does not support scrolling? Why does it report isScrollable = true if its not somehow scrollable?
Any help appreciated - thanks.
You cannot scroll a node that cannot perform ACTION_SCROLL_FORWARD or ACTION_SCROLL_BACKWARDS.
The Accessibility framework on Android is experimental at best and is plagued with inconsistencies such as the one you mentioned. In general, it is best not to rely on any of the is[Property]() methods. Instead, you should test for the property you are interested in yourself, after calling getActions() or getActionsList() on the node.
Our Android app currently has a large number of dialog and alert boxes. I'd like to switch these to toasts, but there's a problem - some of them require the user to choose whether to view more info or just dismiss the popup. It doesn't look like there's a way to do this with toasts.
Is there any existing Android library that supports tappable toasts (i.e., you tap it and it triggers a function call to a listener in the app, sort of like a notification)? If not, is there a recommended alternative for this "tap-here-to-do-something-otherwise-I'll-just-vanish-in-a-few-seconds" UI pattern, or should I just roll my own fragment class for it?
I have to do something similar to my app so I wrote, DropViewNotification, a boiler plate to make it happen by animating the so-called notification into the screen. It doesn't do automatic dismiss as this should only act as a tool.
It accept any kind of view to make it versatile as I need to put at least two or three obvious view into it (TextView, ProgressBar, ImageView). You can switch it's content on the fly if you want to. Animation can also be customized for both showing and dismissing of the notification and the main content.
In real-life you might want to consider adding controller class to handle the display of the content and auto dismissal, etc. Hope it's of some use to you.