📜 ⬆️ ⬇️

Technical aspects of ensuring non-visual availability of Android applications


Perhaps the reader, who is far from the problematic under consideration, seems absurd because the interface design of both the Android system itself and the applications developed for it is focused primarily on visual visibility and attractiveness, which is aggravated by using the touch screen as the main body of user interaction with device. However, there is a category of users, by nature or by chance, deprived of the opportunity to fully enjoy all these delights. Due to the fact that Android provides alternative — or, rather, additional — methods of interaction, the interface and the main functionality of the system are not at all inaccessible for this category of users. The item “Special features” in the system settings menu and the TalkBack application included in it are dedicated to ensuring such accessibility. As for the non-visual availability of third-party applications, it varies from case to case and sometimes requires from the developer not only some special super efforts, but at least minimal attention to the problem .



A list of Android applications tested for non-visual availability, with appropriate comments, can be found, for example, here . Of course, this is not the only such list on the global network and probably not the most representative, but I refer to it primarily as a source of examples that clearly illustrate what is at stake. Note that the non-visual accessibility of the interface of many of these applications is not due to the special efforts of their developers, but is a natural result of the work of the mechanisms built into the system. The developers of the applications simply do not interfere with this, which, however, I would also impute to them in considerable merit.
')
We will not delve into the discussion of the feasibility of caring for the non-visual availability of applications in principle. Enough said in other places. We only note that Android developers are paying some attention to this concern, which can be judged by the history of the development of special access tools . We will focus on purely technical aspects. Let us consider a number of typical problems and indicate ways to solve them. In other words, this essay focuses mainly on developers of Android applications , who for one reason or another decided not to ignore the needs of users burdened with visual restrictions, and their goal is to help them bring noble thoughts to life.

Since the further presentation assumes that the reader has a more or less clear understanding of the principles of non-visual access to the interface used in Android, from the point of view of both the user and the programmer, those who are new to this topic are advised first of all to familiarize themselves with some sources of fundamental information :




The following considerations and recommendations will be illustrated and supported by concrete examples, taken mainly from the TeamTalk project, my participation in which, not least of all, was related to the solution to the problems of accessibility of the Android application.

Of course, as a rule, these will not be entirely literal extracts from the text. I will simplify them as much as possible and sometimes even slightly modify it in order not to tire the reader with irrelevant details and to make the illustrated ideas more convex. After all, the subject of our consideration is not the project itself, but the problems of non-visual accessibility, which are quite typical for Android applications in general, and possible solutions to them.

Those who want to familiarize themselves with the source code , scanty extracts from which will accompany the story, in its entirety, will be able to easily satisfy their legitimate curiosity on Github .

The concept of universal design and the principle of healthy minimalism


I’ll say right away that I’m far from thinking of preaching the non-visual accessibility of the interface to the detriment of its visual clarity or aesthetics, not to mention the functionality of the application. I only advocate that accessibility is also not forgotten, especially where it does not require from the developer either compromises or any noticeable special efforts.

I am a supporter of the concept of universal design , according to which, the application interface should ideally be equally accessible to all categories of users. And above all, it is not necessary to interfere with the system itself to ensure such accessibility that the principle of healthy minimalism entails, which consists in the fact that entities should not be produced without real need .

That is, when there is a temptation to use a third-party library when developing an interface or to create your own completely original control, it would be nice to start thinking: is it really necessary? The Android SDK provides a programmer with a very rich set of tools of this kind, and without sufficiently good reasons, one should not go beyond it. This, by the way, will have a positive impact not only on the availability of the application, but also on its compatibility.

About contentDescription attribute


The simplest and most obvious thing that an application developer can (and should, in my opinion) do for users with visual limitations without overworking and donating nothing is to neatly sign all purely graphical interface elements through the contentDescription attribute. However, unfortunately, very few people do it. And due respect for this attribute seems to be a happy exception, rather than common practice.

Recommendations to use contentDescription to increase the accessibility of the application interface are found in Google’s governing documents and other sources, so, frankly, it’s even embarrassing to remind again. I would abstain if all these recommendations were not ignored with consistency worthy of a clearly better application.

Sometimes, in response to a direct request to sign graphic buttons from developers, it was heard that there was not enough space on the screen for this. Of course, such a response first of all testifies to the professional inconsistency of the programmer, who, without bothering to even get a little familiarization with the documentation, figuratively speaking, does not write the program, but he blunders at random. I want to believe that among the application developers so illiterate there is a bit, but still just in case, I emphasize once again that the contentDescription attribute contentDescription completely harmless, it absolutely does not affect the appearance of the application and does not require screen space .

However, as with everything else in the world, the filling of contentDescription should be approached with understanding and without fanaticism. A mechanically thoughtless approach is likely to lead to completely undesirable results.

We illustrate this with an example. Suppose we are going to display a list of users on the screen and for the list item we have the following scheme:

 <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true"> <ImageView android:id="@+id/usericon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/user" /> <TextView android:id="@+id/nickname" android:textSize="16sp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" /> </LinearLayout> 


As you can see, the purely graphic element ImageView in this scheme does not have the contentDescription attribute. And it is completely conscious. The list element is considered here as a whole, that is, its parts ( ImageView and TextView ) do not have an independent role: they do not have the clickable attribute set. The text information required by the special access service is entirely contained in the TextView , and the ImageView in this case plays for the most part a decorative role and does not carry any useful information in terms of non-visual access.

It is quite another thing if the ImageView element was actually used as a button, pressing which would cause
any action. In this case, the contentDescription attribute would be extremely useful.

Now suppose that users in our list can be in a different state, say, “online” and “offline”, and we will use different colors to display them. The contentDescription attribute will again help us to make this additional information, which this time we will dynamically set along with the element color in the list adapter.

Here is how it can be implemented:

 class UserListAdapter extends ArrayAdapter<User> { public UserListAdapter(Context context, int resource) { super(context, resource); } @Override public View getView(int position, View convertView, ViewGroup parent) { Context context = getContext(); LayoutInflater inflater = LayoutInflater.from(context); if (convertView == null) convertView = inflater.inflate(R.layout.item_user, null); User user = getItem(position); TextView nickname = (TextView) convertView.findViewById(R.id.nickname); nickname.setText(user.nickname); if (user.stateOnline) { convertView.setBackgroundColor(Color.rgb(133, 229, 141)); //       , //      contentDescription, //      , //    text. nickname.setContentDescription(context.getString(R.string.user_state_online, user.nickname)); } else { convertView.setBackgroundColor(Color.rgb(0, 0, 0)); //  contentDescription,   //      text. nickname.setContentDescription(null); } return convertView; } } 


It is assumed that the string resource has the definition:

 <string name="user_state_online">%1$s online</string> 


Note that we provide additional information to the special access service only when the user is in the "online" state. This helps to reduce the volume of voice messages without prejudice to the information content, since there are only two possible states, that is, no discrepancies arise.

Speech messages require time for perception , so their volume should be reduced wherever possible , without sacrificing useful information.

In addition, when composing the combined text for contentDescription , we place the user's name before the designation of his state, because, for reasons of perceptual efficiency, the most requested information should be located at the beginning of the voice message .

Lists with live items


Continuing to consider the example from the previous paragraph, it would be logical to assume that the state of users changes due to some external reason for the application, or, more precisely, its interface. And we need to regularly update the information on the screen so that it corresponds to the real state of affairs.

For definiteness, suppose the following implementation:

 public class MainActivity extends Activity { private UserListAdapter userListAdapter; private CountDownTimer listUpdateTimer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); userListAdapter = new UserListAdapter(this, R.layout.item_user); listUpdateTimer = new CountDownTimer(10000, 1000) { @Override public void onTick(long millisUntilFinished) { userListAdapter.notifyDataSetChanged(); } @Override public void onFinish() { start(); } }; listUpdateTimer.start(); } } 


That is, the information on the screen will be updated approximately once per second. But with each such update, the list items will generate the corresponding events for the special access service, and if the accessibility focus is on one of the list items, then this element will be constantly spoken, which will lead to the almost complete impossibility of normal user interaction with the application. There is a case when excessive obliging means of special access is not in vain and rabid fanaticism needs reasonable limitation.

To this end, we introduce into consideration the auxiliary class:

 public class AccessibilityAssistant extends AccessibilityDelegate { private final Activity hostActivity; private volatile boolean eventsLocked; public AccessibilityAssistant(Activity activity) { hostActivity = activity; eventsLocked = false; } //        . public void lockEvents() { eventsLocked = true; } //         , //        , //      . public void unlockEvents() { if (!hostActivity.getWindow().getDecorView().post(new Runnable() { @Override public void run() { eventsLocked = false; } })) eventsLocked = false; } @Override public void sendAccessibilityEvent(View host, int eventType) { if (!eventsLocked) super.sendAccessibilityEvent(host, eventType); } @Override public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { if (!eventsLocked) super.sendAccessibilityEventUnchecked(host, event); } } 


Now we can easily implement a constant update of information on the screen without sacrificing the non-visual accessibility of the interface:

 public class MainActivity extends Activity { private AccessibilityAssistant accessibilityAssistant; private ArrayAdapter userListAdapter; private CountDownTimer listUpdateTimer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); accessibilityAssistant = new AccessibilityAssistant(this); userListAdapter = new ArrayAdapter(this, R.layout.item_user) { @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_user, null); User user = getItem(position); TextView nickname = (TextView) convertView.findViewById(R.id.nickname); nickname.setText(user.nickname); if (user.stateOnline) { convertView.setBackgroundColor(Color.rgb(133, 229, 141)); nickname.setContentDescription(getString(R.string.user_state_online, user.nickname)); } else { convertView.setBackgroundColor(Color.rgb(0, 0, 0)); nickname.setContentDescription(null); } //      , //     . convertView.setAccessibilityDelegate(accessibilityAssistant); return convertView; } }; listUpdateTimer = new CountDownTimer(10000, 1000) { @Override public void onTick(long millisUntilFinished) { //    accessibilityAssistant.lockEvents(); //      userListAdapter.notifyDataSetChanged(); //     //      accessibilityAssistant.unlockEvents(); } @Override public void onFinish() { start(); } }; listUpdateTimer.start(); } } 


In principle, the task could be solved by overriding the notifyDataSetChanged () method in the list adapter:

 public void notifyDataSetChanged() { accessibilityAssistant.lockEvents(); super.notifyDataSetChanged(); accessibilityAssistant.unlockEvents(); } 


But this option is worse, because the events that occur during any update of the list are blocked, even if it is initiated by any actions of the user. The system of special access is oriented to ensure that the user has an adequate response to their actions, so that, in general, such a lock is undesirable.

Complex list items and dynamic information


Now consider the situation when each element of the list has a button associated with it, that is, it is described, for example, by the following scheme:

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true"> <ImageView android:id="@+id/usericon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/user" /> <TextView android:id="@+id/nickname" android:textSize="16sp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" /> </LinearLayout> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true"> <Button android:id="@+id/msg_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/button_msg" android:focusable="false" android:clickable="true" /> </LinearLayout> </RelativeLayout> 


When interacting with such a list in the study mode by touch, we can set the accessibility focus both on the list elements themselves and on the accompanying buttons.

Suppose further that, in addition to the list, some still-changing information is displayed on the screen. The simplified scheme looks like this:

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ListView android:id="@+id/user_list" android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/count_state" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="0" android:singleLine="true" /> </LinearLayout> 


And, according to the already established tradition, we will produce updates every second:

 public class MainActivity extends Activity { private int counter; private CountDownTimer updateTimer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); counter = 0; updateTimer = new CountDownTimer(10000, 1000) { @Override public void onTick(long millisUntilFinished) { ((TextView)findViewById(R.id.count_state)).setText(String.valueOf(++counter)); } @Override public void onFinish() { start(); } }; updateTimer.start(); } } 


In this situation, the problem occurs when the focus of accessibility falls on one of the buttons inside the list. The fact is that after processing the events that occur when the information on the screen is updated, the special access service restores its position on the list item, and not on the button . As a result, it turns out to be extremely difficult, and with more frequent updates it is completely impossible to press a button in the non-visual access mode. And blocking the events discussed in the previous paragraph, here, alas, does not help.

To combat this nuisance, let us expand the functionality of our auxiliary class:

 public class AccessibilityAssistant extends AccessibilityDelegate { private final Activity hostActivity; private volatile boolean eventsLocked; private final AccessibilityManager accessibilityService; //    ,      //       . private volatile boolean discourageUiUpdates; public AccessibilityAssistant(Activity activity) { hostActivity = activity; accessibilityService = (AccessibilityManager) activity.getSystemService(Context.ACCESSIBILITY_SERVICE); discourageUiUpdates = false; eventsLocked = false; } //     ,      , //   ,      . public boolean isUiUpdateDiscouraged() { return discourageUiUpdates && accessibilityService.isEnabled(); } public void lockEvents() { eventsLocked = true; } public void unlockEvents() { if (!hostActivity.getWindow().getDecorView().post(new Runnable() { @Override public void run() { eventsLocked = false; } })) eventsLocked = false; } @Override public void sendAccessibilityEvent(View host, int eventType) { //   ,      . if (host instanceof Button) checkEvent(eventType); if (!eventsLocked) super.sendAccessibilityEvent(host, eventType); } @Override public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { //   ,      . if (host instanceof Button) checkEvent(event.getEventType()); if (!eventsLocked) super.sendAccessibilityEventUnchecked(host, event); } //   ,      private void checkEvent(int eventType) { switch (eventType) { case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: discourageUiUpdates = true; break; case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: discourageUiUpdates = false; break; default: break; } } } 


And we will avoid updating the information on the screen when the accessibility focus gets on the buttons built into the list, but, of course, only when using the non-visual access mode:

 public class MainActivity extends Activity { private AccessibilityAssistant accessibilityAssistant; private ArrayAdapter userListAdapter; private CountDownTimer updateTimer; private int counter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); accessibilityAssistant = new AccessibilityAssistant(this); counter = 0; userListAdapter = new ArrayAdapter(this, R.layout.item_user) { @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_user, null); User user = getItem(position); TextView nickname = (TextView) convertView.findViewById(R.id.nickname); Button button = (Button) convertView.findViewById(R.id.msg_btn); nickname.setText(user.nickname); if (user.stateOnline) { convertView.setBackgroundColor(Color.rgb(133, 229, 141)); nickname.setContentDescription(getString(R.string.user_state_online, user.nickname)); } else { convertView.setBackgroundColor(Color.rgb(0, 0, 0)); nickname.setContentDescription(null); } //     , //      . button.setAccessibilityDelegate(accessibilityAssistant); convertView.setAccessibilityDelegate(accessibilityAssistant); return convertView; } }; updateTimer = new CountDownTimer(10000, 1000) { @Override public void onTick(long millisUntilFinished) { //    , //      //       . if (!accessibilityAssistant.isUiUpdateDiscouraged()) ((TextView)findViewById(R.id.count_state)).setText(String.valueOf(++counter)); } @Override public void onFinish() { start(); } }; updateTimer.start(); } } 


I note that this is the only recipe listed here that has visible consequences. That is, when the accessibility focus falls, some elements of the screen update interface will freeze. But it will happen only in the special access mode. In the normal mode, there will be no side effects.

Events on invisible pages


Consider another interesting case, namely the use of switchable tabs (or pages):

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <android.support.v4.view.ViewPager android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1.0" > <android.support.v4.view.PagerTitleStrip android:id="@+id/pager_title_strip" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="top" android:background="#33b5e5" android:paddingBottom="4dp" android:paddingTop="4dp" android:textColor="#fff" /> </android.support.v4.view.ViewPager> </LinearLayout> 


The fact is that for the sake of smooth page switching, the system keeps in working condition not only the one that is displayed on the screen, but also the neighboring ones. And in the case of updating information on these neighboring pages, outside the visible area, the corresponding events are generated for the special access service. This sometimes happens when moving from one page to another, when the system prepares the next one and the events are initiated from it. As a result, the speech response does not match what is displayed on the screen.

To get rid of this undesirable effect, we recall that the decision to initiate an event for the special access service is made at the top level of the hierarchy, and we will develop our auxiliary class as follows:

 public class AccessibilityAssistant extends AccessibilityDelegate { private final Activity hostActivity; private final AccessibilityManager accessibilityService; //    private SparseArray<View> monitoredPages; // ,     private View visiblePage; //    private int visiblePageId; private volatile boolean discourageUiUpdates; private volatile boolean eventsLocked; public AccessibilityAssistant(Activity activity) { hostActivity = activity; accessibilityService = (AccessibilityManager) activity.getSystemService(Context.ACCESSIBILITY_SERVICE); monitoredPages = new SparseArray<View>(); visiblePage = null; visiblePageId = 0; discourageUiUpdates = false; eventsLocked = false; } public boolean isUiUpdateDiscouraged() { return discourageUiUpdates && accessibilityService.isEnabled(); } public void lockEvents() { eventsLocked = true; } public void unlockEvents() { if (!hostActivity.getWindow().getDecorView().post(new Runnable() { @Override public void run() { eventsLocked = false; } })) eventsLocked = false; } //         //    .   , , //   onCreateView()  onViewCreated()  . public void registerPage(View page, int id) { monitoredPages.put(id, page); if (id == visiblePageId) visiblePage = page; page.setAccessibilityDelegate(this); } public void setVisiblePage(int id) { visiblePageId = id; visiblePage = monitoredPages.get(id); } @Override public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) { return ((monitoredPages.indexOfValue(host) < 0) || (host == visiblePage)) && super.onRequestSendAccessibilityEvent(host, child, event); } @Override public void sendAccessibilityEvent(View host, int eventType) { if (host instanceof Button) checkEvent(eventType); if (!eventsLocked) super.sendAccessibilityEvent(host, eventType); } @Override public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { if (host instanceof Button) checkEvent(event.getEventType()); if (!eventsLocked) super.sendAccessibilityEventUnchecked(host, event); } private void checkEvent(int eventType) { switch (eventType) { case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: discourageUiUpdates = true; break; case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: discourageUiUpdates = false; break; default: break; } } } 


Now it’s only time to report page changes:

 public class MainActivity extends Activity implements ViewPager.OnPageChangeListener { private AccessibilityAssistant accessibilityAssistant; private ViewPager viewPager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); accessibilityAssistant = new AccessibilityAssistant(this); viewPager = (ViewPager) findViewById(R.id.pager); viewPager.setOnPageChangeListener(this); } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { accessibilityAssistant.setVisiblePage(position); } @Override public void onPageScrollStateChanged(int state) { } } 


And each fragment responsible for the page must register it :

 public class PageFragment extends Fragment { @Override public void onViewCreated(View view, Bundle savedInstanceState) { ((MainActivity)getActivity()).accessibilityAssistant.registerPage(view, PAGE_NUMBER); super.onViewCreated(view, savedInstanceState); } } 


The PAGE_NUMBER parameter here actually means the positional page number. The same as the parameter of the FragmentPagerAdapter.getItem () method.

Conclusion


It may seem that the methods presented here are for the most part intended not to help, but just to prevent the special access system from conveying this or that information to the user's mind. In essence, the way it is. But an excess of information sometimes hurts nothing less than its disadvantage. Especially when it is clearly redundant and irrelevant. I do not get tired of repeating that a good speech interface should speak as little as possible, but always on time and always in essence.

Unfortunately, the built-in Android screenreader TalkBack is far from perfect and, unfortunately, is developing much less dynamically than the accessibility API in the system itself. The participation of the community in its development is hampered by the fact that the published sources are usually irrelevant, and the development team simply ignores the appeals of enthusiasts and constructive suggestions.

However, this topic, which deserves separate consideration, is beyond the scope of this work. I just wanted to draw the attention of developers to the problem of accessibility of applications and, on the one hand, dispel some of the current concerns, on the other hand, to show how the situation can be significantly improved by simple gestures. I hope I succeeded in it to some extent.

Source: https://habr.com/ru/post/245351/


All Articles