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