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