Seeing What Talkback Sees 🔍

13 Sep 2021 | tags: a11y, accessibility

One of the things we should be doing as Android developers is to ensure that our apps are as accessible as possible. There are a bunch of talks and articles that discuss the motivations behind current MDC a11y support, the basic steps to support a11y, testing overviews, even creating your own a11y service!

There’s a lot of resources that tell me what I should do, but what I found sorely lacking is information on helping me figure out what to do when something goes wrong (and knowing me, something is always bound to go wrong).

For example, this is the upper part of my app’s homepage.


Talkback says "2 items in cart"

When the cart action menu item is focused, we expect Talkback to announce how many items are currently in the cart. However I noticed that sometimes Talkback just out of the blue says the number (and just the number) after announcing “2 items in cart”. Weird!

If only I could dive into what Talkback “sees” so I could figure out how to fix the problem and make our Talkback announcements less confusing. I haven’t found any mention of how to do this in the official Android docs, and it is by sheer luck that I stumbled upon this :sparkles: amazing :sparkles: article by Midori Bowen from 2018(!).

Wait, what! :heart_eyes_cat:

It turns out that deep in the bowels of Talkback’s developer settings is an option to “Enable node tree debugging”. Midori links to the Android documentation on enabling this setting but that page has since been deleted. :crying_cat_face:


Turn it on! (While you're there, turn on "Display speech output" as well if you prefer. This will put up a Toast of the Talkback announcements)

The “node tree” being referred to here is basically how Talkback interprets your view hierarchy. Having visibility on this would surely give us a lot of insight into what is going on under the hood.

Follow the steps outlined in the OG post to enable node tree debugging. Some things have changed in Android and in Talkback since Midori’s post, but in general the steps in there should give you an idea of how to enable logging. For instance, instead of looking for “Unassigned”, assignable gestures are now subtitled “Tap to assign”. On some devices, Talkback allows multi-finger gestures, so there’s a lot of options to use to trigger node tree log dumps. If a gesture already has an action, you can still overwrite it if you wish to do so.


I settled on "Tap with 3 fingers"

What do we have here? :thinking:

We can now trigger a dump of the node tree on any screen by using the gesture we have set in Talkback.


Talkback will tell you it has been done

At this point I want to reiterate to please do not be like me and spend an hour looking for where the logs actually are (I forgot that I have Logcat filters on :woman_facepalming:). They are in Logcat, with the tag TreeDebug.

Here’s the partial output of the node tree (timestamps remove for verbosity):

The first few lines (lines 2-9) pertain to the status bar stuff, so let’s just ignore that. Our application’s contents start at line 10 (type=TYPE_APPLICATION) with all the views on the screen in the following lines. Each ViewGroup is tabbed which is really helpful in figuring out how each node maps to the view hierarchy. There’s a lot of information here and some things have changed since Midori’s post, so I thought it would be good to review what we can see in the logs. Let’s take line 18 for example:

(1100966)652.Switch:(668, 225 - 800, 357):CONTENT{See only Specials}:STATE{OFF}:not checked(action:FOCUS/A11Y_FOCUS/CLICK):focusable:clickable
Content Notes
(1100966) The node’s hashcode (which the Talkback source code refers to as a “poor man’s ID”)
652 The window ID
Switch The node’s class name (usually type of widget)
invisible This is not shown in this particular line – it is appended only if the view is invisible
(668, 225 - 800, 357) Coordinates of the view on the screen, (Left, Top - Right, Bottom)
TEXT{xxx} Text that’s visible to the user (this Switch is unlabeled so this does not appear in this line)
CONTENT{See only Specials} The content description provided by the widget
STATE{OFF} If a widget is stateful, such as this Switch, the current state
(action:FOCUS/A11Y_FOCUS/CLICK) Actions available on the node, as defined by AccessibilityNodeInfoCompat
:focusable:clickable All other properties of the widget follow, delimited by :. Possible values, in the order that they may appear are focusable, screenReaderfocusable, focused, selected, scrollable, clickable, longClickable, accessibilityFocused, supportsTextLocation, disabled
“Collection” information If things are in a RecyclerView

The screen is actually a RecyclerView, so let’s also take a look at what information we receive:

At the end of:

Line Notes
1 The RecyclerView node itself, we see :collection:R10C2. This indicates that this is a collection of views consisting of 10 rows, with two columns in each row. Talkback will announce the collection information the first time an item in the collection is selected. For example, if we tap on the Caramello Koala tile, Talkback will announce all the product information (based on the content description of the ViewGroup) plus the location of the tile and the collection information (“Row 1, Column 2, In grid, 10 rows, 2 columns”).
2 The item on the top right, we see :item#r0c0. This is the row- and column-index (starting at 0) of the item relative to the list.
5 The item on the top left, we see :item#r0c1. Talkback will announce this item’s location as “Column 2”.
9, 10 The Button is marked as :invisible because it’s there, but not visible on the screen.

I was able to glean all this information from the LogTree file in Talkback’s repo.

Note that aside from logging the node tree, Talkback also logs the traversal order which may be useful when trying to figure out the order in which elements on the screen gets focus.

Fixing our issue… maybe :eyes:

Going back to our original issue, the node tree gives us a clue (the cart menu item is in most screens of my app):

(1094239)652.ViewGroup:(948, 77 - 1080, 209):CONTENT{Cart: 2 items in Cart}(action:FOCUS/A11Y_FOCUS/CLICK):focusable:clickable
  (1095200)652.TextView:(1008, 107 - 1023, 140):TEXT{2}(action:A11Y_FOCUS):supportsTextLocation

AHA! It looks like both the ViewGroup as a whole and the TextView itself can have focus (it looks like the TextView itself does not have a value for CONTENT, which is what Talkback announces though), which may explain why I sometimes hear just the number?

I guess I still don’t have a definitive answer, but setting android:importantForAccessibility="no" on the TextView should not present a problem since enough context is already given to the user when the ViewGroup gets focus.


I hope that as more and more people become a11y allies that we also get more attention on a11y tooling, and more technical articles focused on supporting a11y beyond the basics. For instance, did you know that we can change what Talkback says to provide more context on actionable content? For example, when selecting this ViewGroup:


Double-tapping will bring the user to the edit store or delivery address screen

Talkback will announce “Double-tap to change” instead of the default “Double-tap to activate”. The former gives the user more context about what is expected to happen when they interact with the element. Come to think of it, that’s a good idea for our next a11y post! See you then! :ok_hand:


Harnessing the Power of Reflogs 🧙‍♀️

10 Aug 2021 | tags: git

A few weeks ago, I tweeted about a discovery that blew my mind:

In the tweet, I referenced the git docs which says:

You can use the @{-N} syntax to refer to the N-th last branch/commit checked out using “git checkout” operation. You may also specify - which is synonymous to @{-1}.

That’s cool and all, but what I realised is that most of the time, aside from the last checked out branch, I don’t remember what the third to last or fifth to last branch was. Actually that’s a lie. Sometimes I don’t remember the last checked out branch too. :woman_facepalming:

All I remember is that I was in that branch but I don’t remember the name nor what number I should put in the braces. About 98% of the time, I ended up consulting the reflog to figure out this information.

The reflog enumerates anything that happens within git. It shows information about references, any commands that have been run, or where HEAD is pointing to, among others.


A sample reflog output

That’s a lot of information, but what we are looking for are lines similar to this one:

1e8a561 (origin/main, main) HEAD@{10}: checkout: moving from main to task/lint-gradle

From here I can use the branch name and go back to that branch if I want to. Scanning all info spewed by reflog can be overwhelming; sometimes I’m just interested in finding out the branch name for checking things out and not in all the other details.

After some furious Googling and some trial and error, I ended up with this:

git reflog | egrep -io "moving from ([^[:space:]]+)" | awk '{ print NR " - " $3 }' | head -n 5'

Let’s break it down:

Running this command gives back:


Convenience!

The numbers on the left match the number I can use for checking out. For example, if I want to go back to feature/package-rename:

gco @{-4}

Since I’m interested in using the numbers, I don’t care if some branch names appear multiple times in the list. To remove duplicates, add this command after the egrep:

awk ' !seen[$0]++'

:bangbang: Note that if you filter out duplicates the numbers on the left cannot be used for the git checkout @{-N} shorthand :bangbang:


Look, I’m an old woman and I like using the command line for git so please don’t @ me with your fancy GUI suggestions. :older_woman:


References:


Enforcing Team Rules with Lint: Tests 🧐

20 Nov 2020 | tags: android studio lint

A few months ago, my team came upon an agreement that when leaving a TODO anywhere in our code, we need to always provide several things:

I created a live template to support adherence to this rule, but why not go one step further and integrate the rule into our daily workflow?

We have previously seen:


Having tests for our custom Lint rule is really important. We do not want Lint to flag errors, uhm, erroneously. It results in frustration and users might just turn off our rule.

Serving up files 💁

Lint checks run on files, so for each of our test cases we need to provide mock files.

Lint provides a TestFile API that allows us to create these mock files inline.

TestFiles.java(
    """
        package test.pkg;
        public class TestClass1 {
            // In a comment, mentioning "lint" has no effect
         }
    """
)
TestFiles.kotlin(
     """
        package test.pkg
        class TestClass {
            // In a comment, mentioning "lint" has no effect
        }
    """

I am using raw strings so that I do not have to worry about escaping special characters.

It also gives us very nice syntax highlighting!

Furthermore, if you choose “Edit Kotlin Fragment” from the hint, Android Studio will open up a file editor. Any changes you make in this editor will immediately reflect in your TestFile. Pretty cool!

Let’s get testing 🔬

The gateway to our test cases is TestLintTask. We need to provide it the following:

@Test
fun testKotlinFileNormalComment() {
    TestLintTask.lint()
        .files(
            TestFiles.kotlin(
                """
                    package test.pkg
                        
                    class TestClass {
                        // In a comment, mentioning "lint" has no effect
                    }
                """
            )
        )
        .issues(TodoDetector.ISSUE)
        .run()
        .expect("No warnings.")
}

Note that we have the option here of using expect("No warnings.") or expectClean().

For the test cases where we expect an error to occur, we need to put in the text that Lint spits out (i.e. similar to what you see in the console when running .gradlew :app:lintDebug). The trickiest thing about this is that the string has to match exactly, including where the squiggly lines are.

The easiest way to do this is to pass an empty string to expect() and let the test fail. You can then copy-paste the error message into your test.


Retrieving the message for an error scenario

I wrote a few tests for the detector covering Java and Kotlin files, incorrect date formats, and “TODO” casing. You can find them all here.

Bringing it all together 🤝

Now that we have written our tests, it’s finally time to integrate our Lint rule into our app!

First we need to create our IssueRegistry to let Lint know about our custom rule. We also need to provide an API value; for custom rules we can use the constant defined by the Lint API.

@Suppress("UnstableApiUsage")
class IssueRegistry : IssueRegistry() {
    override val issues: List<Issue> = listOf(
        TodoDetector.ISSUE
    )

    override val api = CURRENT_API
}

We then register our IssueRegistry by creating a file with the fully qualified name of our file under a META-INF/services/ folder:

src/main/resources/META-INF/services/dev.zarah.lint.checks.IssueRegistry

:raising_hand: Most posts mention that adding this registry is enough, and I am probably doing something wrong but I found out that for my project, I still have to include my IssueRegistry in the Manifest by adding this to my build.gradle.kts file:

tasks {
  jar {
    manifest {
      attributes(
          "Lint-Registry-v2" to "dev.zarah.lint.checks.IssueRegistry"
      )
    }
  }
}

We have set up everything and now it’s time to consume our custom lint rule! In the modules where you want the rules applied, add a lintChecks entry in the dependencies closure, build your project, and everything should be good to go! :running_woman:

dependencies {
    lintChecks project(':checks')
}

Seeing it in action 📽️

Finally! We have come to the end of our journey! Here’s our detector in action:


Custom lint rule with quickfix

The source code for this Lint series is available on Github (diff).


:bowing_woman: I have learned so many things whilst I was doing this task.

As I mentioned on Twitter, there’s barely any public documentation at all. The talks I’ve seen are way too advanced for me (for example, I needed to understand what PSI and UAST were before it clicked and most talks barely even define them).

There was a lot of trial and error, and so. much. guesswork. It was incredibly frustrating. Major props to Mike Evans who patiently guided me through the hours and hours of pair programming that we did. If he didn’t help, I wouldn’t even know where to start. Sure I can copy-paste from samples but I want to understand why things are done the way they are – that’s how I learn.

I probably have made wrong assumptions whilst writing these posts :woman_shrugging: but again, no documentation so this is the best I could do. :sweat_smile:

Anyway, what I want to say is, I usually only see the final output of other people’s work and sometimes I cannot help but feel jealous – how come they find it super easy whilst I’m out here crying because I don’t understand anything? I needed to remind myself that it’s okay to be frustrated, it’s okay to take your time to learn, and it’s okay to ask for help.

Further reading (and/or watching) 📖

If you’re keen to learn more, here are some resources about Lint and custom Lint rules: