CheckAccessibilityAvailable.kt

/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.wear.tiles.checkers

import androidx.wear.tiles.LayoutElementBuilders.Arc
import androidx.wear.tiles.LayoutElementBuilders.ArcAdapter
import androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement
import androidx.wear.tiles.LayoutElementBuilders.ArcLine
import androidx.wear.tiles.LayoutElementBuilders.ArcSpacer
import androidx.wear.tiles.LayoutElementBuilders.ArcText
import androidx.wear.tiles.LayoutElementBuilders.Box
import androidx.wear.tiles.LayoutElementBuilders.Column
import androidx.wear.tiles.LayoutElementBuilders.Image
import androidx.wear.tiles.LayoutElementBuilders.LayoutElement
import androidx.wear.tiles.LayoutElementBuilders.Row
import androidx.wear.tiles.LayoutElementBuilders.Spacer
import androidx.wear.tiles.LayoutElementBuilders.Spannable
import androidx.wear.tiles.LayoutElementBuilders.Text
import androidx.wear.tiles.TimelineBuilders
import androidx.wear.tiles.TimelineBuilders.TimelineEntry
import kotlin.jvm.Throws

/**
 * Checks a [TimelineBuilders.TimelineEntry] to ensure that at least one element within it has an
 * accessibility description registered.
 *
 * At least one element on each tile should have a machine-readable content description
 * associated with it, which can be read out using screen readers.
 */
internal class CheckAccessibilityAvailable : TimelineEntryChecker {
    override val name: String
        get() = "CheckAccessibilityAvailable"

    @Throws(CheckerException::class)
    override fun check(entry: TimelineEntry) {
        // Do a descent through the tile, checking that at least one element has an a11y tag.
        if (entry.layout?.root?.let(this::checkElement) == false) {
            throw CheckerException(
                "Tile layout does not have any nodes with an accessibility " +
                    "description. You should add a Semantics Modifier to at least one of your " +
                    "LayoutElements."
            )
        }
    }

    private fun checkElement(element: LayoutElement): Boolean {
        val modifiers = when (element) {
            is Row -> element.modifiers
            is Column -> element.modifiers
            is Box -> element.modifiers
            is Arc -> element.modifiers
            is Spacer -> element.modifiers
            is Image -> element.modifiers
            is Text -> element.modifiers
            is Spannable -> element.modifiers
            else -> null
        }

        if (modifiers?.semantics != null) {
            return true
        }

        // Descend...
        // Note that individual Spannable elements cannot have semantics; the parent should have
        // these.
        return when (element) {
            is Row -> element.contents.any(this::checkElement)
            is Column -> element.contents.any(this::checkElement)
            is Box -> element.contents.any(this::checkElement)
            is Arc -> element.contents.any(this::checkArcLayoutElement)
            else -> false
        }
    }

    private fun checkArcLayoutElement(element: ArcLayoutElement): Boolean {
        val modifiers = when (element) {
            // Note that ArcAdapter should be handled by taking the modifiers from the inner element
            // instead.
            is ArcText -> element.modifiers
            is ArcLine -> element.modifiers
            is ArcSpacer -> element.modifiers
            else -> null
        }

        if (modifiers?.semantics != null) {
            return true
        }

        return if (element is ArcAdapter) {
            element.content?.let(this::checkElement) ?: false
        } else {
            false
        }
    }
}