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

Commit f0f7b0fc authored by Abhishek Pal's avatar Abhishek Pal Committed by Android (Google) Code Review
Browse files

Merge "Add method to pre-process and transform SafetyCenterData to an object...

Merge "Add method to pre-process and transform SafetyCenterData to an object better suited for the new UI" into main
parents 59bc0cda c68502eb
Loading
Loading
Loading
Loading
+67 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 com.android.settingslib.safetycenter

import android.permission.flags.Flags
import android.safetycenter.SafetyCenterData
import android.safetycenter.SafetyCenterIssue

/**
 * A transformer object responsible for converting the raw [SafetyCenterData] provided by the system
 * into a [SafetyCenterUiData] structure more suitable for the Safety Center UI in Settings.
 */
object SafetyCenterDataTransformer {

    fun transform(data: SafetyCenterData): SafetyCenterUiData {
        if (!Flags.openSafetyCenterApis()) {
            return SafetyCenterUiData(
                status = data.status,
                entriesByUserIdAndSourceId = emptyMap(),
                activeIssuesBySourceId = emptyMap(),
                dismissedIssuesBySourceId = emptyMap(),
            )
        }

        val entriesByUserIdAndSourceId =
            data.entriesOrGroups
                .flatMap { it.entryGroup?.entries ?: listOfNotNull(it.entry) }
                .filter { it.user != null && it.safetySourceId != null }
                .groupBy { it.user!!.identifier }
                .mapValues { (_, entries) ->
                    entries.associateBy { it.safetySourceId!! }
                }

        val activeIssuesBySourceId = processIssues(data.issues)
        val dismissedIssuesBySourceId = processIssues(data.dismissedIssues)

        return SafetyCenterUiData(
            status = data.status,
            entriesByUserIdAndSourceId = entriesByUserIdAndSourceId,
            activeIssuesBySourceId = activeIssuesBySourceId,
            dismissedIssuesBySourceId = dismissedIssuesBySourceId,
        )
    }

    private fun processIssues(
        issues: List<SafetyCenterIssue>,
    ): Map<String, List<SafetyCenterIssue>> {
        return issues
            .flatMap { issue -> issue.safetySourceIds.map { sourceId -> sourceId to issue } }
            .groupBy { it.first }
            .mapValues { (_, pairs) -> pairs.map { it.second }.distinct() }
    }
}
+136 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 com.android.settingslib.safetycenter

import android.safetycenter.SafetyCenterEntry
import android.safetycenter.SafetyCenterIssue
import android.safetycenter.SafetyCenterStatus

/**
 * Container for the transformed SafetyCenterData.
 *
 * @property status The overall status of the Safety Center.
 * @property entriesByUserIdAndSourceId A map where the key is the user ID (Int) and the value is
 *     another map, keyed by safety source ID (String) to the SafetyCenterEntry.
 * @property activeIssuesBySourceId A map grouping lists of active SafetyCenterIssue
 *     objects by their safety source ID.
 * @property dismissedIssuesBySourceId A map grouping lists of dismissed SafetyCenterIssue
 *     objects by their safety source ID.
 */
data class SafetyCenterUiData(
    val status: SafetyCenterStatus,
    val entriesByUserIdAndSourceId: Map<Int, Map<String, SafetyCenterEntry>>,
    val activeIssuesBySourceId: Map<String, List<SafetyCenterIssue>>,
    val dismissedIssuesBySourceId: Map<String, List<SafetyCenterIssue>>,
) {

    /**
     * Retrieves a specific SafetyCenterEntry for a given user and source ID.
     *
     * @param userId The ID of the user.
     * @param sourceId The safety source ID of the entry.
     * @return The matching SafetyCenterEntry, or null if not found.
     */
    fun getEntry(userId: Int, sourceId: String): SafetyCenterEntry? {
        return entriesByUserIdAndSourceId[userId]?.get(sourceId)
    }

    /**
     * Retrieves all active issues across all profiles, sorted by severity and source ID order.
     *
     * @param sourceIdsOrder An optional list defining the preferred order
     *      of safety source IDs for secondary sorting.
     * @return A sorted list of all active SafetyCenterIssue objects.
     */
    fun getActiveIssues(
        sourceIdsOrder: List<String> = emptyList(),
    ): List<SafetyCenterIssue> {
        val allIssues = activeIssuesBySourceId.values.flatten().distinct()
        return allIssues.sortedWith(issueComparator(sourceIdsOrder))
    }

    /**
     * Retrieves active issues across all profiles that are associated with
     * any of the provided safety source IDs.
     * The results are sorted by severity and the provided source ID order.
     *
     * @param sourceIdsOrder A list defining the safety source IDs to filter
     *      by and their preferred order for secondary sorting.
     * @return A sorted list of matching active SafetyCenterIssue objects.
     */
    fun getActiveIssuesForSources(
        sourceIdsOrder: List<String>,
    ): List<SafetyCenterIssue> {
        val relevantIssues =
            sourceIdsOrder
                .flatMap { sourceId -> activeIssuesBySourceId[sourceId] ?: emptyList() }
                .distinct()
        return relevantIssues.sortedWith(issueComparator(sourceIdsOrder))
    }

    /**
     * Retrieves dismissed issues across all profiles that are associated with
     * any of the provided safety source IDs.
     * The results are sorted by severity and the provided source ID order.
     *
     * @param sourceIdsOrder A list defining the safety source IDs to filter
     *      by and their preferred order for secondary sorting.
     * @return A sorted list of matching dismissed SafetyCenterIssue objects.
     */
    fun getDismissedIssuesForSources(
        sourceIdsOrder: List<String>,
    ): List<SafetyCenterIssue> {
        val relevantIssues =
            sourceIdsOrder
                .flatMap { sourceId -> dismissedIssuesBySourceId[sourceId] ?: emptyList() }
                .distinct()
        return relevantIssues.sortedWith(issueComparator(sourceIdsOrder))
    }

    companion object {
        private val SEVERITY_ORDER =
            mapOf(
                SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING to 0,
                SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_RECOMMENDATION to 1,
                SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK to 2,
            )

        /**
         * Comparator for sorting SafetyCenterIssue objects.
         * Issues are sorted primarily by severity (Critical > Recommendation > OK).
         * Secondary sorting is based on the order of safety source IDs
         * provided in `sourceIdsOrder`.
         * An issue's position in the secondary sort is determined by the lowest index
         * of any of its associated safetySourceIds within the `sourceIdsOrder` list.
         * This comparator provides a stable sort.
         */
        private fun issueComparator(sourceIdsOrder: List<String>): Comparator<SafetyCenterIssue> {
            return compareBy<SafetyCenterIssue> {
                SEVERITY_ORDER[it.severityLevel] ?: Int.MAX_VALUE
            }
                .thenBy { issue ->
                    issue.safetySourceIds
                        .minOfOrNull { sourceId ->
                            sourceIdsOrder.indexOf(sourceId).let { index ->
                                if (index == -1) Int.MAX_VALUE else index
                            }
                        }
                        ?: Int.MAX_VALUE
                }
        }
    }
}
+252 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 com.android.settingslib.safetycenter

import android.os.UserHandle
import android.permission.flags.Flags
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.safetycenter.SafetyCenterData
import android.safetycenter.SafetyCenterEntry
import android.safetycenter.SafetyCenterEntryGroup
import android.safetycenter.SafetyCenterEntryOrGroup
import android.safetycenter.SafetyCenterIssue
import android.safetycenter.SafetyCenterStatus
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class SafetyCenterDataTransformerTest {

    @get:Rule
    val setFlagsRule = SetFlagsRule()

    private val user0 = UserHandle.of(0)
    private val user10 = UserHandle.of(10)

    private val defaultStatus =
        SafetyCenterStatus.Builder("Default Title", "Default Summary")
            .setSeverityLevel(SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK)
            .build()

    private fun createIssue(
        id: String,
        user: UserHandle,
        sourceIds: Set<String>,
        title: String = "Issue $id",
    ): SafetyCenterIssue {
        return SafetyCenterIssue.Builder(id, title, "Summary $id", user, sourceIds, "type_$id")
            .build()
    }

    private fun createEntry(
        id: String,
        user: UserHandle,
        sourceId: String,
        title: String = "Entry $id",
    ): SafetyCenterEntry {
        return SafetyCenterEntry.Builder(id, title, user, sourceId).build()
    }

    @Test
    @DisableFlags(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
    fun transform_flagDisabled_returnsEmptyData() {
        val entry1 = createEntry("entry1", user0, "sourceA")
        val issue1 = createIssue("issue1", user0, setOf("sourceA"))
        val scData =
            SafetyCenterData.Builder(defaultStatus)
                .addEntryOrGroup(SafetyCenterEntryOrGroup(entry1))
                .addIssue(issue1)
                .build()

        val uiData = SafetyCenterDataTransformer.transform(scData)

        assertThat(uiData.status).isEqualTo(defaultStatus)
        assertThat(uiData.entriesByUserIdAndSourceId).isEmpty()
        assertThat(uiData.activeIssuesBySourceId).isEmpty()
        assertThat(uiData.dismissedIssuesBySourceId).isEmpty()
    }

    @Test
    @EnableFlags(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
    fun transform_emptyData_flagEnabled_returnsEmptyUiDataWithStatus() {
        val emptyData = SafetyCenterData.Builder(defaultStatus).build()

        val uiData = SafetyCenterDataTransformer.transform(emptyData)

        assertThat(uiData.status).isEqualTo(defaultStatus)
        assertThat(uiData.entriesByUserIdAndSourceId).isEmpty()
        assertThat(uiData.activeIssuesBySourceId).isEmpty()
        assertThat(uiData.dismissedIssuesBySourceId).isEmpty()
    }

    @Test
    @EnableFlags(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
    fun transform_withEntriesOnly_flagEnabled_mapsCorrectly() {
        val entry1 = createEntry("entry1", user0, "sourceA")
        val entry2 = createEntry("entry2", user10, "sourceB")
        val entry3 = createEntry("entry3", user0, "sourceC")

        val scData =
            SafetyCenterData.Builder(defaultStatus)
                .addEntryOrGroup(SafetyCenterEntryOrGroup(entry1))
                .addEntryOrGroup(SafetyCenterEntryOrGroup(entry2))
                .addEntryOrGroup(SafetyCenterEntryOrGroup(entry3))
                .build()

        val uiData = SafetyCenterDataTransformer.transform(scData)

        assertThat(uiData.entriesByUserIdAndSourceId).hasSize(2)
        assertThat(uiData.entriesByUserIdAndSourceId[0]).containsExactly(
            "sourceA",
            entry1,
            "sourceC",
            entry3
        )
        assertThat(uiData.entriesByUserIdAndSourceId[10]).containsExactly("sourceB", entry2)
        assertThat(uiData.activeIssuesBySourceId).isEmpty()
        assertThat(uiData.dismissedIssuesBySourceId).isEmpty()
    }

    @Test
    @EnableFlags(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
    fun transform_withEntryGroup_flagEnabled_flattensEntries() {
        val entry1 = createEntry("entry1", user0, "sourceA")
        val entry2 = createEntry("entry2", user0, "sourceB")

        val entryGroup =
            SafetyCenterEntryGroup.Builder("group1", "Group 1 Title")
                .setEntries(listOf(entry1, entry2))
                .build()

        val scData =
            SafetyCenterData.Builder(defaultStatus)
                .addEntryOrGroup(SafetyCenterEntryOrGroup(entryGroup))
                .build()

        val uiData = SafetyCenterDataTransformer.transform(scData)

        assertThat(uiData.entriesByUserIdAndSourceId).hasSize(1)
        assertThat(uiData.entriesByUserIdAndSourceId[0]).containsExactly(
            "sourceA",
            entry1,
            "sourceB",
            entry2
        )
    }

    @Test
    @EnableFlags(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
    fun transform_withIssuesOnly_flagEnabled_mapsCorrectly() {
        val issue1 = createIssue("issue1", user0, setOf("sourceA"))
        val issue2 = createIssue("issue2", user10, setOf("sourceB"))
        val issue3 = createIssue("issue3", user0, setOf("sourceC"))

        val scData =
            SafetyCenterData.Builder(defaultStatus)
                .addIssue(issue1)
                .addDismissedIssue(issue2)
                .addIssue(issue3)
                .build()

        val uiData = SafetyCenterDataTransformer.transform(scData)

        assertThat(uiData.entriesByUserIdAndSourceId).isEmpty()
        assertThat(uiData.activeIssuesBySourceId).containsExactly(
            "sourceA",
            listOf(issue1),
            "sourceC",
            listOf(issue3)
        )
        assertThat(uiData.dismissedIssuesBySourceId).containsExactly("sourceB", listOf(issue2))
    }

    @Test
    @EnableFlags(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
    fun transform_issueWithMultipleSources_flagEnabled_appearsUnderEachSource() {
        val issueMulti = createIssue("issueMulti", user0, setOf("sourceA", "sourceB", "sourceC"))

        val scData = SafetyCenterData.Builder(defaultStatus).addIssue(issueMulti).build()

        val uiData = SafetyCenterDataTransformer.transform(scData)

        assertThat(uiData.activeIssuesBySourceId).hasSize(3)
        assertThat(uiData.activeIssuesBySourceId["sourceA"]).containsExactly(issueMulti)
        assertThat(uiData.activeIssuesBySourceId["sourceB"]).containsExactly(issueMulti)
        assertThat(uiData.activeIssuesBySourceId["sourceC"]).containsExactly(issueMulti)
    }

    @Test
    @EnableFlags(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
    fun transform_duplicateIssues_flagEnabled_areDistinctInOutput() {
        val issue1 = createIssue("issue1", user0, setOf("sourceA"))
        val issue1_dupe = createIssue("issue1", user0, setOf("sourceA"))
        val issue2 = createIssue("issue2", user0, setOf("sourceA"))

        val scData =
            SafetyCenterData.Builder(defaultStatus)
                .addIssue(issue1)
                .addIssue(issue1_dupe)
                .addIssue(issue2)
                .build()

        val uiData = SafetyCenterDataTransformer.transform(scData)

        assertThat(uiData.activeIssuesBySourceId["sourceA"]).containsExactly(issue1, issue2)
    }

    @Test
    @EnableFlags(Flags.FLAG_OPEN_SAFETY_CENTER_APIS)
    fun transform_mixedData_multipleUsers_flagEnabled() {
        val entryU0 = createEntry("entryU0", user0, "sourceA")
        val issueU0_active = createIssue("issueU0A", user0, setOf("sourceA", "sourceB"))
        val issueU0_dismissed = createIssue("issueU0D", user0, setOf("sourceC"))

        val entryU10 = createEntry("entryU10", user10, "sourceX")
        val issueU10_active = createIssue("issueU10A", user10, setOf("sourceX"))

        val scData =
            SafetyCenterData.Builder(defaultStatus)
                .addEntryOrGroup(SafetyCenterEntryOrGroup(entryU0))
                .addIssue(issueU0_active)
                .addDismissedIssue(issueU0_dismissed)
                .addEntryOrGroup(SafetyCenterEntryOrGroup(entryU10))
                .addIssue(issueU10_active)
                .build()

        val uiData = SafetyCenterDataTransformer.transform(scData)

        assertThat(uiData.entriesByUserIdAndSourceId).hasSize(2)
        assertThat(uiData.entriesByUserIdAndSourceId[0]).containsExactly("sourceA", entryU0)
        assertThat(uiData.entriesByUserIdAndSourceId[10]).containsExactly("sourceX", entryU10)

        assertThat(uiData.activeIssuesBySourceId).containsExactly(
            "sourceA", listOf(issueU0_active),
            "sourceB", listOf(issueU0_active),
            "sourceX", listOf(issueU10_active)
        )

        assertThat(uiData.dismissedIssuesBySourceId).containsExactly(
            "sourceC",
            listOf(issueU0_dismissed)
        )
    }
}
+205 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 com.android.settingslib.safetycenter

import android.os.UserHandle
import android.safetycenter.SafetyCenterEntry
import android.safetycenter.SafetyCenterIssue
import android.safetycenter.SafetyCenterStatus
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class SafetyCenterUiDataTest {

    private val user0 = UserHandle.of(0)
    private val user10 = UserHandle.of(10)

    private val defaultStatus =
        SafetyCenterStatus.Builder("Default Title", "Default Summary")
            .setSeverityLevel(SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK)
            .build()

    // Helper to create a minimal SafetyCenterIssue
    private fun createIssue(
        id: String,
        user: UserHandle,
        sourceIds: Set<String>,
        severity: Int,
        title: String = "Issue $id",
    ): SafetyCenterIssue {
        return SafetyCenterIssue.Builder(id, title, "Summary $id",
            user, sourceIds, "type_$id")
            .setSeverityLevel(severity)
            .build()
    }

    // Helper to create a minimal SafetyCenterEntry
    private fun createEntry(
        id: String,
        user: UserHandle,
        sourceId: String,
        title: String = "Entry $id",
    ): SafetyCenterEntry {
        return SafetyCenterEntry.Builder(id, title,
            user, sourceId).build()
    }

    private val entry1 = createEntry("entry1", user0, "sourceA")
    private val entry2 = createEntry("entry2", user0, "sourceB")
    private val entry3 = createEntry("entry3", user10, "sourceA")

    private val issue1 =
        createIssue(
            "issue1",
            user0,
            setOf("s1"),
            SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING,
        )
    private val issue2 =
        createIssue(
            "issue2",
            user10,
            setOf("s2"),
            SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING,
        )
    private val issue3 =
        createIssue(
            "issue3",
            user0,
            setOf("s1", "s2"),
            SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_RECOMMENDATION,
        )
    private val issue4 =
        createIssue(
            "issue4",
            user0,
            setOf("s3"),
            SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_RECOMMENDATION
        )
    private val issue5 =
        createIssue(
            "issue5",
            user10,
            setOf("s1"),
            SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK
        )
    private val dismissedIssue =
        createIssue(
            "issue6",
            user0,
            setOf("s4"),
            SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK
        )

    private val testSafetyCenterUiData =
        SafetyCenterUiData(
            status = defaultStatus,
            entriesByUserIdAndSourceId =
                mapOf(
                    0 to mapOf("sourceA" to entry1, "sourceB" to entry2),
                    10 to mapOf("sourceA" to entry3),
                ),
            activeIssuesBySourceId =
                mapOf(
                    "s1" to listOf(issue1, issue3, issue5),
                    "s2" to listOf(issue2, issue3),
                    "s3" to listOf(issue4),
                ),
            dismissedIssuesBySourceId = mapOf("s4" to listOf(dismissedIssue)),
        )

    @Test
    fun getEntry_entryExists_returnsEntry() {
        assertThat(testSafetyCenterUiData.getEntry(0, "sourceA"))
            .isEqualTo(entry1)
        assertThat(testSafetyCenterUiData.getEntry(0, "sourceB"))
            .isEqualTo(entry2)
        assertThat(testSafetyCenterUiData.getEntry(10, "sourceA"))
            .isEqualTo(entry3)
    }

    @Test
    fun getEntry_entryNotExists_returnsNull() {
        assertThat(testSafetyCenterUiData.getEntry(0, "sourceX")).isNull()
        assertThat(testSafetyCenterUiData.getEntry(10, "sourceB")).isNull()
        assertThat(testSafetyCenterUiData.getEntry(99, "sourceA")).isNull()
    }

    @Test
    fun getActiveIssues_noOrder_sortedBySeverity() {
        val issues = testSafetyCenterUiData.getActiveIssues()
        assertThat(issues)
            .containsExactly(
                issue1,
                issue2,
                issue3,
                issue4,
                issue5,
            )
            .inOrder()
    }

    @Test
    fun getActiveIssues_withOrder_sortedBySeverityAndSourceOrder() {
        val sourceOrder = listOf("s3", "s2", "s1")
        val issues = testSafetyCenterUiData.getActiveIssues(sourceOrder)
        assertThat(issues)
            .containsExactly(
                issue2,
                issue1,
                issue4,
                issue3,
                issue5,
            )
            .inOrder()
    }

    @Test
    fun getActiveIssuesForSources_filtersAndSorts() {
        val sourceOrder = listOf("s3", "s1")
        val issues = testSafetyCenterUiData.getActiveIssuesForSources(sourceOrder)
        assertThat(issues)
            .containsExactly(
                issue1,
                issue4,
                issue3,
                issue5,
            )
            .inOrder()
    }

    @Test
    fun getActiveIssuesForSources_notFound_returnsEmpty() {
        val issues = testSafetyCenterUiData.getActiveIssuesForSources(listOf("nonExistent"))
        assertThat(issues).isEmpty()
    }

    @Test
    fun getDismissedIssuesForSources_filters() {
        val issues = testSafetyCenterUiData.getDismissedIssuesForSources(listOf("s4"))
        assertThat(issues).containsExactly(dismissedIssue)
    }

    @Test
    fun getDismissedIssuesForSources_notFound_returnsEmpty() {
        val issues = testSafetyCenterUiData.getDismissedIssuesForSources(listOf("s1"))
        assertThat(issues).isEmpty()
    }
}