• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.safetycenter.testing
18 
19 import android.app.PendingIntent
20 import android.content.Context
21 import android.icu.text.MessageFormat
22 import android.os.Build.VERSION_CODES.TIRAMISU
23 import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE
24 import android.os.Bundle
25 import android.os.UserHandle
26 import android.safetycenter.SafetyCenterData
27 import android.safetycenter.SafetyCenterEntry
28 import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING
29 import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK
30 import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_RECOMMENDATION
31 import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN
32 import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED
33 import android.safetycenter.SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON
34 import android.safetycenter.SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION
35 import android.safetycenter.SafetyCenterEntryGroup
36 import android.safetycenter.SafetyCenterEntryOrGroup
37 import android.safetycenter.SafetyCenterIssue
38 import android.safetycenter.SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING
39 import android.safetycenter.SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK
40 import android.safetycenter.SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_RECOMMENDATION
41 import android.safetycenter.SafetyCenterStatus
42 import android.safetycenter.SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING
43 import android.safetycenter.SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK
44 import android.safetycenter.SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN
45 import android.util.ArrayMap
46 import androidx.annotation.RequiresApi
47 import com.android.modules.utils.build.SdkLevel
48 import com.android.safetycenter.internaldata.SafetyCenterEntryId
49 import com.android.safetycenter.internaldata.SafetyCenterIds
50 import com.android.safetycenter.internaldata.SafetyCenterIssueActionId
51 import com.android.safetycenter.internaldata.SafetyCenterIssueId
52 import com.android.safetycenter.internaldata.SafetyCenterIssueKey
53 import com.android.safetycenter.resources.SafetyCenterResourcesApk
54 import com.android.safetycenter.testing.SafetyCenterTestConfigs.Companion.SINGLE_SOURCE_GROUP_ID
55 import com.android.safetycenter.testing.SafetySourceTestData.Companion.CRITICAL_ISSUE_ACTION_ID
56 import com.android.safetycenter.testing.SafetySourceTestData.Companion.CRITICAL_ISSUE_ID
57 import com.android.safetycenter.testing.SafetySourceTestData.Companion.INFORMATION_ISSUE_ACTION_ID
58 import com.android.safetycenter.testing.SafetySourceTestData.Companion.INFORMATION_ISSUE_ID
59 import com.android.safetycenter.testing.SafetySourceTestData.Companion.ISSUE_TYPE_ID
60 import com.android.safetycenter.testing.SafetySourceTestData.Companion.RECOMMENDATION_ISSUE_ACTION_ID
61 import com.android.safetycenter.testing.SafetySourceTestData.Companion.RECOMMENDATION_ISSUE_ID
62 import java.util.Locale
63 
64 /**
65  * A class that provides [SafetyCenterData] objects and associated constants to facilitate asserting
66  * on specific Safety Center states in SafetyCenter for testing.
67  */
68 @RequiresApi(TIRAMISU)
69 class SafetyCenterTestData(context: Context) {
70 
71     private val safetyCenterResourcesApk = SafetyCenterResourcesApk.forTests(context)
72     private val safetySourceTestData = SafetySourceTestData(context)
73 
74     /**
75      * The [SafetyCenterStatus] used when the overall status is unknown and no scan is in progress.
76      */
77     val safetyCenterStatusUnknown: SafetyCenterStatus
78         get() =
79             SafetyCenterStatus.Builder(
80                     safetyCenterResourcesApk.getStringByName(
81                         "overall_severity_level_ok_review_title"
82                     ),
83                     safetyCenterResourcesApk.getStringByName(
84                         "overall_severity_level_ok_review_summary"
85                     ),
86                 )
87                 .setSeverityLevel(OVERALL_SEVERITY_LEVEL_UNKNOWN)
88                 .build()
89 
90     /**
91      * Returns a [SafetyCenterStatus] with one alert and the given [statusResource] and
92      * [overallSeverityLevel].
93      */
safetyCenterStatusOneAlertnull94     fun safetyCenterStatusOneAlert(
95         statusResource: String,
96         overallSeverityLevel: Int,
97     ): SafetyCenterStatus = safetyCenterStatusNAlerts(statusResource, overallSeverityLevel, 1)
98 
99     /**
100      * Returns a [SafetyCenterStatus] with [numAlerts] and the given [statusResource] and
101      * [overallSeverityLevel].
102      */
103     fun safetyCenterStatusNAlerts(
104         statusResource: String,
105         overallSeverityLevel: Int,
106         numAlerts: Int,
107     ): SafetyCenterStatus =
108         SafetyCenterStatus.Builder(
109                 safetyCenterResourcesApk.getStringByName(statusResource),
110                 getAlertString(numAlerts),
111             )
112             .setSeverityLevel(overallSeverityLevel)
113             .build()
114 
115     /**
116      * Returns an information [SafetyCenterStatus] that has "Tip(s) available" as a summary for the
117      * given [numTipIssues].
118      */
119     fun safetyCenterStatusTips(numTipIssues: Int): SafetyCenterStatus =
120         SafetyCenterStatus.Builder(
121                 safetyCenterResourcesApk.getStringByName("overall_severity_level_ok_title"),
122                 getIcuPluralsString("overall_severity_level_tip_summary", numTipIssues),
123             )
124             .setSeverityLevel(OVERALL_SEVERITY_LEVEL_OK)
125             .build()
126 
127     /**
128      * Returns an information [SafetyCenterStatus] that has "Action(s) taken" as a summary for the
129      * given [numAutomaticIssues].
130      */
131     fun safetyCenterStatusActionsTaken(numAutomaticIssues: Int): SafetyCenterStatus =
132         SafetyCenterStatus.Builder(
133                 safetyCenterResourcesApk.getStringByName("overall_severity_level_ok_title"),
134                 getIcuPluralsString(
135                     "overall_severity_level_action_taken_summary",
136                     numAutomaticIssues,
137                 ),
138             )
139             .setSeverityLevel(OVERALL_SEVERITY_LEVEL_OK)
140             .build()
141 
142     /**
143      * Returns the [SafetyCenterStatus] used when the overall status is critical and no scan is in
144      * progress for the given number of alerts.
145      */
146     fun safetyCenterStatusCritical(numAlerts: Int) =
147         SafetyCenterStatus.Builder(
148                 safetyCenterResourcesApk.getStringByName(
149                     "overall_severity_level_critical_safety_warning_title"
150                 ),
151                 getAlertString(numAlerts),
152             )
153             .setSeverityLevel(OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING)
154             .build()
155 
156     /**
157      * Returns a [SafetyCenterEntry] builder with a grey icon (for unknown severity), the summary
158      * generally used for sources of the [SafetyCenterTestConfigs], and a pending intent that
159      * redirects to [TestActivity] for the given source, user id, and title.
160      */
161     fun safetyCenterEntryDefaultBuilder(
162         sourceId: String,
163         userId: Int = UserHandle.myUserId(),
164         title: CharSequence = "OK",
165         pendingIntent: PendingIntent? =
166             safetySourceTestData.createTestActivityRedirectPendingIntent(),
167     ) =
168         SafetyCenterEntry.Builder(entryId(sourceId, userId), title)
169             .setSeverityLevel(ENTRY_SEVERITY_LEVEL_UNKNOWN)
170             .setSummary("OK")
171             .setPendingIntent(pendingIntent)
172             .setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION)
173 
174     /**
175      * Returns a [SafetyCenterEntry] with a grey icon (for unknown severity), the summary generally
176      * used for sources of the [SafetyCenterTestConfigs], and a pending intent that redirects to
177      * Safety Center for the given source, user id, and title.
178      */
179     fun safetyCenterEntryDefault(
180         sourceId: String,
181         userId: Int = UserHandle.myUserId(),
182         title: CharSequence = "OK",
183         pendingIntent: PendingIntent? =
184             safetySourceTestData.createTestActivityRedirectPendingIntent(),
185     ) = safetyCenterEntryDefaultBuilder(sourceId, userId, title, pendingIntent).build()
186 
187     /**
188      * Returns a [SafetyCenterEntry] builder with no icon, the summary generally used for sources of
189      * the [SafetyCenterTestConfigs], and a pending intent that redirects to [TestActivity] for the
190      * given source, user id, and title.
191      */
192     fun safetyCenterEntryDefaultStaticBuilder(
193         sourceId: String,
194         userId: Int = UserHandle.myUserId(),
195         title: CharSequence = "OK",
196     ) =
197         SafetyCenterEntry.Builder(entryId(sourceId, userId), title)
198             .setSeverityLevel(ENTRY_SEVERITY_LEVEL_UNSPECIFIED)
199             .setSummary("OK")
200             .setPendingIntent(
201                 safetySourceTestData.createTestActivityRedirectPendingIntent(explicit = false)
202             )
203             .setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON)
204 
205     /**
206      * Returns a [SafetyCenterEntry] with a grey icon (for unknown severity), a refresh error
207      * summary, and a pending intent that redirects to [TestActivity] for the given source, user id,
208      * and title.
209      */
210     fun safetyCenterEntryError(sourceId: String) =
211         safetyCenterEntryDefaultBuilder(sourceId).setSummary(getRefreshErrorString(1)).build()
212 
213     /**
214      * Returns a disabled [SafetyCenterEntry] with a grey icon (for unspecified severity), a
215      * standard summary, and a standard title for the given source and pending intent.
216      */
217     fun safetyCenterEntryUnspecified(
218         sourceId: String,
219         pendingIntent: PendingIntent? =
220             safetySourceTestData.createTestActivityRedirectPendingIntent(),
221     ) =
222         SafetyCenterEntry.Builder(entryId(sourceId), "Unspecified title")
223             .setSeverityLevel(ENTRY_SEVERITY_LEVEL_UNSPECIFIED)
224             .setSummary("Unspecified summary")
225             .setPendingIntent(pendingIntent)
226             .setEnabled(false)
227             .setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION)
228             .build()
229 
230     /**
231      * Returns a [SafetyCenterEntry] builder with a green icon (for ok severity), a standard
232      * summary, and a pending intent that redirects to [TestActivity] for the given source, user id,
233      * and title.
234      */
235     fun safetyCenterEntryOkBuilder(
236         sourceId: String,
237         userId: Int = UserHandle.myUserId(),
238         title: CharSequence = "Ok title",
239     ) =
240         SafetyCenterEntry.Builder(entryId(sourceId, userId), title)
241             .setSeverityLevel(ENTRY_SEVERITY_LEVEL_OK)
242             .setSummary("Ok summary")
243             .setPendingIntent(safetySourceTestData.createTestActivityRedirectPendingIntent())
244             .setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION)
245 
246     /**
247      * Returns a [SafetyCenterEntry] with a green icon (for ok severity), a standard summary, and a
248      * pending intent that redirects to [TestActivity] for the given source, user id, and title.
249      */
250     fun safetyCenterEntryOk(
251         sourceId: String,
252         userId: Int = UserHandle.myUserId(),
253         title: CharSequence = "Ok title",
254     ) = safetyCenterEntryOkBuilder(sourceId, userId, title).build()
255 
256     /**
257      * Returns a [SafetyCenterEntry] with a yellow icon (for recommendation severity), a standard
258      * title, and a pending intent that redirects to [TestActivity] for the given source and
259      * summary.
260      */
261     fun safetyCenterEntryRecommendation(
262         sourceId: String,
263         summary: String = "Recommendation summary",
264     ) =
265         SafetyCenterEntry.Builder(entryId(sourceId), "Recommendation title")
266             .setSeverityLevel(ENTRY_SEVERITY_LEVEL_RECOMMENDATION)
267             .setSummary(summary)
268             .setPendingIntent(safetySourceTestData.createTestActivityRedirectPendingIntent())
269             .setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION)
270             .build()
271 
272     /**
273      * Returns a [SafetyCenterEntry] with a red icon (for critical severity), a standard title, a
274      * standard summary, and a pending intent that redirects to [TestActivity] for the given source.
275      */
276     fun safetyCenterEntryCritical(sourceId: String) =
277         SafetyCenterEntry.Builder(entryId(sourceId), "Critical title")
278             .setSeverityLevel(ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING)
279             .setSummary("Critical summary")
280             .setPendingIntent(safetySourceTestData.createTestActivityRedirectPendingIntent())
281             .setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION)
282             .build()
283 
284     fun singletonSafetyCenterEntryOrGroup(
285         groupId: String,
286         entry: SafetyCenterEntry,
287         groupSummary: String? = null,
288     ) =
289         // TODO: b/361404288 - Replace with platform version check
290         if (SafetyCenterFlags.showSubpages) {
291             val summary =
292                 if (groupSummary == null && entry.severityLevel > ENTRY_SEVERITY_LEVEL_OK) {
293                     entry.summary
294                 } else groupSummary ?: "OK"
295 
296             SafetyCenterEntryOrGroup(
297                 SafetyCenterEntryGroup.Builder(groupId, "OK")
298                     .setSeverityLevel(entry.severityLevel)
299                     .setSeverityUnspecifiedIconType(
300                         SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION
301                     )
302                     .setSummary(summary)
303                     .setEntries(listOf(entry))
304                     .build()
305             )
306         } else {
307             SafetyCenterEntryOrGroup(entry)
308         }
309 
310     /**
311      * Returns an information [SafetyCenterIssue] for the given source and user id that is
312      * consistent with information [SafetySourceIssue]s used in [SafetySourceTestData].
313      */
safetyCenterIssueInformationnull314     fun safetyCenterIssueInformation(
315         sourceId: String,
316         userId: Int = UserHandle.myUserId(),
317         attributionTitle: String? = "OK",
318         groupId: String? = SINGLE_SOURCE_GROUP_ID,
319     ) =
320         SafetyCenterIssue.Builder(
321                 issueId(sourceId, INFORMATION_ISSUE_ID, userId = userId),
322                 "Information issue title",
323                 "Information issue summary",
324             )
325             .setSeverityLevel(ISSUE_SEVERITY_LEVEL_OK)
326             .setShouldConfirmDismissal(false)
327             .setActions(
328                 listOf(
329                     SafetyCenterIssue.Action.Builder(
330                             issueActionId(
331                                 sourceId,
332                                 INFORMATION_ISSUE_ID,
333                                 INFORMATION_ISSUE_ACTION_ID,
334                                 userId,
335                             ),
336                             "Review",
337                             safetySourceTestData.createTestActivityRedirectPendingIntent(),
338                         )
339                         .build()
340                 )
341             )
342             .apply {
343                 if (SdkLevel.isAtLeastU()) {
344                     setAttributionTitle(attributionTitle)
345                     setGroupId(groupId)
346                 }
347             }
348             .build()
349 
350     /**
351      * Returns a recommendation [SafetyCenterIssue] for the given source and user id that is
352      * consistent with recommendation [SafetySourceIssue]s used in [SafetySourceTestData].
353      */
safetyCenterIssueRecommendationnull354     fun safetyCenterIssueRecommendation(
355         sourceId: String,
356         userId: Int = UserHandle.myUserId(),
357         attributionTitle: String? = "OK",
358         groupId: String? = SINGLE_SOURCE_GROUP_ID,
359         confirmationDialog: Boolean = false,
360     ) =
361         SafetyCenterIssue.Builder(
362                 issueId(sourceId, RECOMMENDATION_ISSUE_ID, userId = userId),
363                 "Recommendation issue title",
364                 "Recommendation issue summary",
365             )
366             .setSeverityLevel(ISSUE_SEVERITY_LEVEL_RECOMMENDATION)
367             .setActions(
368                 listOf(
369                     SafetyCenterIssue.Action.Builder(
370                             issueActionId(
371                                 sourceId,
372                                 RECOMMENDATION_ISSUE_ID,
373                                 RECOMMENDATION_ISSUE_ACTION_ID,
374                                 userId,
375                             ),
376                             "See issue",
377                             safetySourceTestData.createTestActivityRedirectPendingIntent(),
378                         )
379                         .apply {
380                             if (confirmationDialog && SdkLevel.isAtLeastU()) {
381                                 setConfirmationDialogDetails(
382                                     SafetyCenterIssue.Action.ConfirmationDialogDetails(
383                                         "Confirmation title",
384                                         "Confirmation text",
385                                         "Confirmation yes",
386                                         "Confirmation no",
387                                     )
388                                 )
389                             }
390                         }
391                         .build()
392                 )
393             )
<lambda>null394             .apply {
395                 if (SdkLevel.isAtLeastU()) {
396                     setAttributionTitle(attributionTitle)
397                     setGroupId(groupId)
398                 }
399             }
400             .build()
401 
402     /**
403      * Returns a critical [SafetyCenterIssue] for the given source and user id that is consistent
404      * with critical [SafetySourceIssue]s used in [SafetySourceTestData].
405      */
safetyCenterIssueCriticalnull406     fun safetyCenterIssueCritical(
407         sourceId: String,
408         isActionInFlight: Boolean = false,
409         userId: Int = UserHandle.myUserId(),
410         attributionTitle: String? = "OK",
411         groupId: String? = SINGLE_SOURCE_GROUP_ID,
412     ) =
413         SafetyCenterIssue.Builder(
414                 issueId(sourceId, CRITICAL_ISSUE_ID, userId = userId),
415                 "Critical issue title",
416                 "Critical issue summary",
417             )
418             .setSeverityLevel(ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING)
419             .setActions(
420                 listOf(
421                     SafetyCenterIssue.Action.Builder(
422                             issueActionId(
423                                 sourceId,
424                                 CRITICAL_ISSUE_ID,
425                                 CRITICAL_ISSUE_ACTION_ID,
426                                 userId,
427                             ),
428                             "Solve issue",
429                             safetySourceTestData.criticalIssueActionPendingIntent,
430                         )
431                         .setWillResolve(true)
432                         .setIsInFlight(isActionInFlight)
433                         .build()
434                 )
435             )
436             .apply {
437                 if (SdkLevel.isAtLeastU()) {
438                     setAttributionTitle(attributionTitle)
439                     setGroupId(groupId)
440                 }
441             }
442             .build()
443 
444     /**
445      * Returns the [overall_severity_n_alerts_summary] string formatted for the given number of
446      * alerts.
447      */
getAlertStringnull448     fun getAlertString(numberOfAlerts: Int): String =
449         getIcuPluralsString("overall_severity_n_alerts_summary", numberOfAlerts)
450 
451     /** Returns the [refresh_error] string formatted for the given number of error entries. */
452     fun getRefreshErrorString(numberOfErrorEntries: Int): String =
453         getIcuPluralsString("refresh_error", numberOfErrorEntries)
454 
455     private fun getIcuPluralsString(name: String, count: Int, vararg formatArgs: Any): String {
456         val messageFormat =
457             MessageFormat(
458                 safetyCenterResourcesApk.getStringByName(name, formatArgs),
459                 Locale.getDefault(),
460             )
461         val arguments = ArrayMap<String, Any>()
462         arguments["count"] = count
463         return messageFormat.format(arguments)
464     }
465 
466     companion object {
467         /** The default [SafetyCenterData] returned by the Safety Center APIs. */
468         val DEFAULT: SafetyCenterData =
469             SafetyCenterData(
470                 SafetyCenterStatus.Builder("", "")
471                     .setSeverityLevel(OVERALL_SEVERITY_LEVEL_UNKNOWN)
472                     .build(),
473                 emptyList(),
474                 emptyList(),
475                 emptyList(),
476             )
477 
478         /** Creates an ID for a Safety Center entry. */
entryIdnull479         fun entryId(sourceId: String, userId: Int = UserHandle.myUserId()) =
480             SafetyCenterIds.encodeToString(
481                 SafetyCenterEntryId.newBuilder()
482                     .setSafetySourceId(sourceId)
483                     .setUserId(userId)
484                     .build()
485             )
486 
487         /** Creates an ID for a Safety Center issue. */
488         fun issueId(
489             sourceId: String,
490             sourceIssueId: String,
491             issueTypeId: String = ISSUE_TYPE_ID,
492             userId: Int = UserHandle.myUserId(),
493         ) =
494             SafetyCenterIds.encodeToString(
495                 SafetyCenterIssueId.newBuilder()
496                     .setSafetyCenterIssueKey(
497                         SafetyCenterIssueKey.newBuilder()
498                             .setSafetySourceId(sourceId)
499                             .setSafetySourceIssueId(sourceIssueId)
500                             .setUserId(userId)
501                             .build()
502                     )
503                     .setIssueTypeId(issueTypeId)
504                     .build()
505             )
506 
507         /** Creates an ID for a Safety Center issue action. */
508         fun issueActionId(
509             sourceId: String,
510             sourceIssueId: String,
511             sourceIssueActionId: String,
512             userId: Int = UserHandle.myUserId(),
513         ) =
514             SafetyCenterIds.encodeToString(
515                 SafetyCenterIssueActionId.newBuilder()
516                     .setSafetyCenterIssueKey(
517                         SafetyCenterIssueKey.newBuilder()
518                             .setSafetySourceId(sourceId)
519                             .setSafetySourceIssueId(sourceIssueId)
520                             .setUserId(userId)
521                             .build()
522                     )
523                     .setSafetySourceIssueActionId(sourceIssueActionId)
524                     .build()
525             )
526 
527         /**
528          * On U+, returns a new [SafetyCenterData] with the dismissed issues set. Prior to U,
529          * returns the passed in [SafetyCenterData].
530          */
531         fun SafetyCenterData.withDismissedIssuesIfAtLeastU(
532             dismissedIssues: List<SafetyCenterIssue>
533         ): SafetyCenterData =
534             if (SdkLevel.isAtLeastU()) {
535                 copy(dismissedIssues = dismissedIssues)
536             } else this
537 
538         /** Returns a [SafetyCenterData] without extras. */
SafetyCenterDatanull539         fun SafetyCenterData.withoutExtras() =
540             if (SdkLevel.isAtLeastU()) {
541                 SafetyCenterData.Builder(this).clearExtras().build()
542             } else this
543 
544         /**
545          * On U+, returns a new [SafetyCenterData] with [SafetyCenterIssue]s having the
546          * [attributionTitle]. Prior to U, returns the passed in [SafetyCenterData].
547          */
SafetyCenterDatanull548         fun SafetyCenterData.withAttributionTitleInIssuesIfAtLeastU(
549             attributionTitle: String?
550         ): SafetyCenterData {
551             return if (SdkLevel.isAtLeastU()) {
552                 val issuesWithAttributionTitle =
553                     this.issues.map {
554                         SafetyCenterIssue.Builder(it).setAttributionTitle(attributionTitle).build()
555                     }
556                 copy(issues = issuesWithAttributionTitle)
557             } else this
558         }
559 
560         /**
561          * On U+, returns a new [SafetyCenterData] with the extras set. Prior to U, returns the
562          * passed in [SafetyCenterData].
563          */
SafetyCenterDatanull564         fun SafetyCenterData.withExtrasIfAtLeastU(extras: Bundle): SafetyCenterData =
565             if (SdkLevel.isAtLeastU()) {
566                 copy(extras = extras)
567             } else this
568 
569         @RequiresApi(UPSIDE_DOWN_CAKE)
SafetyCenterDatanull570         private fun SafetyCenterData.copy(
571             issues: List<SafetyCenterIssue> = this.issues,
572             dismissedIssues: List<SafetyCenterIssue> = this.dismissedIssues,
573             extras: Bundle = this.extras,
574         ): SafetyCenterData =
575             SafetyCenterData.Builder(status)
576                 .apply {
577                     issues.forEach(::addIssue)
578                     entriesOrGroups.forEach(::addEntryOrGroup)
579                     staticEntryGroups.forEach(::addStaticEntryGroup)
580                     dismissedIssues.forEach(::addDismissedIssue)
581                 }
582                 .setExtras(extras)
583                 .build()
584     }
585 }
586