Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ef2b5956 authored by Casey Burkhardt's avatar Casey Burkhardt
Browse files

Adds an accessibility live region to the camera/mic privacy chip

This change will cause screen readers to produce non-disruptive feedback
upon the initial appearance of the privacy indicator dot, and ensures
updates from the dot are throttled to 10 seconds, ensuring duplicate
feedback is not provided to users as the indicator animates from chip to
smaller dot.

This change does not impact focusability of the chip with screen
readers.

(Includes ktfmt changes for OngoingPrivacyChip.kt)

Bug: 197201744
Flag: com.android.systemui.privacy_dot_live_region
Test: Manual - tested extensively with TalkBack using Camera & Recorder
Change-Id: Iccc1b09fb29ec33e099c9b6f1957f7a488038a9c
parent 330628ba
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -155,3 +155,13 @@ flag {
      purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "privacy_dot_live_region"
    namespace: "accessibility"
    description: "Exposes the status bar privacy dot as a live region so it is announced when it appears."
    bug: "197201744"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -2923,7 +2923,7 @@
    <string name="ongoing_privacy_dialog_a11y_title">In use</string>

    <!-- Content description for ongoing privacy chip. Use with multiple apps [CHAR LIMIT=NONE]-->
    <string name="ongoing_privacy_chip_content_multiple_apps">Applications are using your <xliff:g id="types_list" example="camera, location">%s</xliff:g>.</string>
    <string name="ongoing_privacy_chip_content_multiple_apps"><xliff:g id="types_list" example="camera, location">%s</xliff:g> in use.</string>

    <!-- Separator for types. Include spaces before and after if needed [CHAR LIMIT=10] -->
    <string name="ongoing_privacy_dialog_separator">,\u0020</string>
+39 −23
Original line number Diff line number Diff line
@@ -23,18 +23,23 @@ import android.view.Gravity.END
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import com.android.settingslib.Utils
import com.android.systemui.Flags
import com.android.systemui.res.R
import com.android.systemui.statusbar.events.BackgroundAnimatableView
import java.time.Duration

class OngoingPrivacyChip @JvmOverloads constructor(
class OngoingPrivacyChip
@JvmOverloads
constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttrs: Int = 0,
    defStyleRes: Int = 0
    defStyleRes: Int = 0,
) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes), BackgroundAnimatableView {

    private var configuration: Configuration
@@ -64,15 +69,22 @@ class OngoingPrivacyChip @JvmOverloads constructor(
    }

    /**
     * When animating as a chip in the status bar, we want to animate the width for the container
     * of the privacy items. We have to subtract our own top and left offset because the bounds
     * come to us as absolute on-screen bounds, and `iconsContainer` is laid out relative to the
     * frame layout's bounds.
     * When animating as a chip in the status bar, we want to animate the width for the container of
     * the privacy items. We have to subtract our own top and left offset because the bounds come to
     * us as absolute on-screen bounds, and `iconsContainer` is laid out relative to the frame
     * layout's bounds.
     */
    override fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int) {
        iconsContainer.setLeftTopRightBottom(l - left, t - top, r - left, b - top)
    }

    override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
        super.onInitializeAccessibilityNodeInfo(info)
        if (Flags.privacyDotLiveRegion()) {
            info.setMinDurationBetweenContentChanges(Duration.ofSeconds(10L))
        }
    }

    // Should only be called if the builder icons or app changed
    private fun updateView(builder: PrivacyChipBuilder) {
        fun setIcons(chipBuilder: PrivacyChipBuilder, iconsContainer: ViewGroup) {
@@ -80,7 +92,8 @@ class OngoingPrivacyChip @JvmOverloads constructor(
            chipBuilder.generateIcons().forEachIndexed { i, it ->
                it.mutate()
                it.setTint(iconColor)
                val image = ImageView(context).apply {
                val image =
                    ImageView(context).apply {
                        setImageDrawable(it)
                        scaleType = ImageView.ScaleType.CENTER_INSIDE
                    }
@@ -92,11 +105,16 @@ class OngoingPrivacyChip @JvmOverloads constructor(
                }
            }
        }

        if (!privacyList.isEmpty()) {
            if (Flags.privacyDotLiveRegion()) {
                accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_POLITE
            }
            generateContentDescription(builder)
            setIcons(builder, iconsContainer)
        } else {
            if (Flags.privacyDotLiveRegion()) {
                accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
            }
            iconsContainer.removeAllViews()
        }
        requestLayout()
@@ -104,8 +122,8 @@ class OngoingPrivacyChip @JvmOverloads constructor(

    private fun generateContentDescription(builder: PrivacyChipBuilder) {
        val typesText = builder.joinTypes()
        contentDescription = context.getString(
                R.string.ongoing_privacy_chip_content_multiple_apps, typesText)
        contentDescription =
            context.getString(R.string.ongoing_privacy_chip_content_multiple_apps, typesText)
    }

    override fun onConfigurationChanged(newConfig: Configuration?) {
@@ -120,17 +138,15 @@ class OngoingPrivacyChip @JvmOverloads constructor(
    }

    private fun updateResources() {
        iconMargin = context.resources
                .getDimensionPixelSize(R.dimen.ongoing_appops_chip_icon_margin)
        iconSize = context.resources
                .getDimensionPixelSize(R.dimen.ongoing_appops_chip_icon_size)
        iconMargin =
            context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_icon_margin)
        iconSize = context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_icon_size)
        iconColor =
            Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)

        val height = context.resources
                .getDimensionPixelSize(R.dimen.ongoing_appops_chip_height)
        val padding = context.resources
                .getDimensionPixelSize(R.dimen.ongoing_appops_chip_side_padding)
        val height = context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_height)
        val padding =
            context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_side_padding)
        iconsContainer.layoutParams.height = height
        iconsContainer.setPaddingRelative(padding, 0, padding, 0)
        iconsContainer.background = context.getDrawable(R.drawable.statusbar_privacy_chip_bg)