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(numTipIssues: Int): SafetyCenterStatus = 118 SafetyCenterStatus.Builder( 119 safetyCenterResourcesApk.getStringByName("overall_severity_level_ok_title"), 120 getIcuPluralsString("overall_severity_level_tip_summary", numTipIssues), 121 ) 122 .setSeverityLevel(OVERALL_SEVERITY_LEVEL_OK) 123 .build() 124 125 /** 126 * Returns an information [SafetyCenterStatus] that has "Action(s) taken" as a summary for the 127 * given [numAutomaticIssues]. 128 */ 129 fun safetyCenterStatusActionsTaken(numAutomaticIssues: Int): SafetyCenterStatus = 130 SafetyCenterStatus.Builder( 131 safetyCenterResourcesApk.getStringByName("overall_severity_level_ok_title"), 132 getIcuPluralsString( 133 "overall_severity_level_action_taken_summary", 134 numAutomaticIssues, 135 ), 136 ) 137 .setSeverityLevel(OVERALL_SEVERITY_LEVEL_OK) 138 .build() 139 140 /** 141 * Returns the [SafetyCenterStatus] used when the overall status is critical and no scan is in 142 * progress for the given number of alerts. 143 */ 144 fun safetyCenterStatusCritical(numAlerts: Int) = 145 SafetyCenterStatus.Builder( 146 safetyCenterResourcesApk.getStringByName( 147 "overall_severity_level_critical_safety_warning_title" 148 ), 149 getAlertString(numAlerts), 150 ) 151 .setSeverityLevel(OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING) 152 .build() 153 154 /** 155 * Returns a [SafetyCenterEntry] builder with a grey icon (for unknown severity), the summary 156 * generally used for sources of the [SafetyCenterTestConfigs], and a pending intent that 157 * redirects to [TestActivity] for the given source, user id, and title. 158 */ 159 fun safetyCenterEntryDefaultBuilder( 160 sourceId: String, 161 userId: Int = UserHandle.myUserId(), 162 title: CharSequence = "OK", 163 pendingIntent: PendingIntent? = 164 safetySourceTestData.createTestActivityRedirectPendingIntent(), 165 ) = 166 SafetyCenterEntry.Builder(entryId(sourceId, userId), title) 167 .setSeverityLevel(ENTRY_SEVERITY_LEVEL_UNKNOWN) 168 .setSummary("OK") 169 .setPendingIntent(pendingIntent) 170 .setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION) 171 172 /** 173 * Returns a [SafetyCenterEntry] with a grey icon (for unknown severity), the summary generally 174 * used for sources of the [SafetyCenterTestConfigs], and a pending intent that redirects to 175 * Safety Center for the given source, user id, and title. 176 */ 177 fun safetyCenterEntryDefault( 178 sourceId: String, 179 userId: Int = UserHandle.myUserId(), 180 title: CharSequence = "OK", 181 pendingIntent: PendingIntent? = 182 safetySourceTestData.createTestActivityRedirectPendingIntent(), 183 ) = safetyCenterEntryDefaultBuilder(sourceId, userId, title, pendingIntent).build() 184 185 /** 186 * Returns a [SafetyCenterEntry] builder with no icon, the summary generally used for sources of 187 * the [SafetyCenterTestConfigs], and a pending intent that redirects to [TestActivity] for the 188 * given source, user id, and title. 189 */ 190 fun safetyCenterEntryDefaultStaticBuilder( 191 sourceId: String, 192 userId: Int = UserHandle.myUserId(), 193 title: CharSequence = "OK", 194 ) = 195 SafetyCenterEntry.Builder(entryId(sourceId, userId), title) 196 .setSeverityLevel(ENTRY_SEVERITY_LEVEL_UNSPECIFIED) 197 .setSummary("OK") 198 .setPendingIntent( 199 safetySourceTestData.createTestActivityRedirectPendingIntent(explicit = false) 200 ) 201 .setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON) 202 203 /** 204 * Returns a [SafetyCenterEntry] with a grey icon (for unknown severity), a refresh error 205 * summary, and a pending intent that redirects to [TestActivity] for the given source, user id, 206 * and title. 207 */ 208 fun safetyCenterEntryError(sourceId: String) = 209 safetyCenterEntryDefaultBuilder(sourceId).setSummary(getRefreshErrorString(1)).build() 210 211 /** 212 * Returns a disabled [SafetyCenterEntry] with a grey icon (for unspecified severity), a 213 * standard summary, and a standard title for the given source and pending intent. 214 */ 215 fun safetyCenterEntryUnspecified( 216 sourceId: String, 217 pendingIntent: PendingIntent? = 218 safetySourceTestData.createTestActivityRedirectPendingIntent(), 219 ) = 220 SafetyCenterEntry.Builder(entryId(sourceId), "Unspecified title") 221 .setSeverityLevel(ENTRY_SEVERITY_LEVEL_UNSPECIFIED) 222 .setSummary("Unspecified summary") 223 .setPendingIntent(pendingIntent) 224 .setEnabled(false) 225 .setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION) 226 .build() 227 228 /** 229 * Returns a [SafetyCenterEntry] builder with a green icon (for ok severity), a standard 230 * summary, and a pending intent that redirects to [TestActivity] for the given source, user id, 231 * and title. 232 */ 233 fun safetyCenterEntryOkBuilder( 234 sourceId: String, 235 userId: Int = UserHandle.myUserId(), 236 title: CharSequence = "Ok title", 237 ) = 238 SafetyCenterEntry.Builder(entryId(sourceId, userId), title) 239 .setSeverityLevel(ENTRY_SEVERITY_LEVEL_OK) 240 .setSummary("Ok summary") 241 .setPendingIntent(safetySourceTestData.createTestActivityRedirectPendingIntent()) 242 .setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION) 243 244 /** 245 * Returns a [SafetyCenterEntry] with a green icon (for ok severity), a standard summary, and a 246 * pending intent that redirects to [TestActivity] for the given source, user id, and title. 247 */ 248 fun safetyCenterEntryOk( 249 sourceId: String, 250 userId: Int = UserHandle.myUserId(), 251 title: CharSequence = "Ok title", 252 ) = safetyCenterEntryOkBuilder(sourceId, userId, title).build() 253 254 /** 255 * Returns a [SafetyCenterEntry] with a yellow icon (for recommendation severity), a standard 256 * title, and a pending intent that redirects to [TestActivity] for the given source and 257 * summary. 258 */ 259 fun safetyCenterEntryRecommendation( 260 sourceId: String, 261 summary: String = "Recommendation summary", 262 ) = 263 SafetyCenterEntry.Builder(entryId(sourceId), "Recommendation title") 264 .setSeverityLevel(ENTRY_SEVERITY_LEVEL_RECOMMENDATION) 265 .setSummary(summary) 266 .setPendingIntent(safetySourceTestData.createTestActivityRedirectPendingIntent()) 267 .setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION) 268 .build() 269 270 /** 271 * Returns a [SafetyCenterEntry] with a red icon (for critical severity), a standard title, a 272 * standard summary, and a pending intent that redirects to [TestActivity] for the given source. 273 */ 274 fun safetyCenterEntryCritical(sourceId: String) = 275 SafetyCenterEntry.Builder(entryId(sourceId), "Critical title") 276 .setSeverityLevel(ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING) 277 .setSummary("Critical summary") 278 .setPendingIntent(safetySourceTestData.createTestActivityRedirectPendingIntent()) 279 .setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION) 280 .build() 281 282 /** 283 * Returns an information [SafetyCenterIssue] for the given source and user id that is 284 * consistent with information [SafetySourceIssue]s used in [SafetySourceTestData]. 285 */ 286 fun safetyCenterIssueInformation( 287 sourceId: String, 288 userId: Int = UserHandle.myUserId(), 289 attributionTitle: String? = "OK", 290 groupId: String? = SINGLE_SOURCE_GROUP_ID, 291 ) = 292 SafetyCenterIssue.Builder( 293 issueId(sourceId, INFORMATION_ISSUE_ID, userId = userId), 294 "Information issue title", 295 "Information issue summary", 296 ) 297 .setSeverityLevel(ISSUE_SEVERITY_LEVEL_OK) 298 .setShouldConfirmDismissal(false) 299 .setActions( 300 listOf( 301 SafetyCenterIssue.Action.Builder( 302 issueActionId( 303 sourceId, 304 INFORMATION_ISSUE_ID, 305 INFORMATION_ISSUE_ACTION_ID, 306 userId, 307 ), 308 "Review", 309 safetySourceTestData.createTestActivityRedirectPendingIntent(), 310 ) 311 .build() 312 ) 313 ) 314 .apply { 315 if (SdkLevel.isAtLeastU()) { 316 setAttributionTitle(attributionTitle) 317 setGroupId(groupId) 318 } 319 } 320 .build() 321 322 /** 323 * Returns a recommendation [SafetyCenterIssue] for the given source and user id that is 324 * consistent with recommendation [SafetySourceIssue]s used in [SafetySourceTestData]. 325 */ safetyCenterIssueRecommendationnull326 fun safetyCenterIssueRecommendation( 327 sourceId: String, 328 userId: Int = UserHandle.myUserId(), 329 attributionTitle: String? = "OK", 330 groupId: String? = SINGLE_SOURCE_GROUP_ID, 331 confirmationDialog: Boolean = false, 332 ) = 333 SafetyCenterIssue.Builder( 334 issueId(sourceId, RECOMMENDATION_ISSUE_ID, userId = userId), 335 "Recommendation issue title", 336 "Recommendation issue summary", 337 ) 338 .setSeverityLevel(ISSUE_SEVERITY_LEVEL_RECOMMENDATION) 339 .setActions( 340 listOf( 341 SafetyCenterIssue.Action.Builder( 342 issueActionId( 343 sourceId, 344 RECOMMENDATION_ISSUE_ID, 345 RECOMMENDATION_ISSUE_ACTION_ID, 346 userId, 347 ), 348 "See issue", 349 safetySourceTestData.createTestActivityRedirectPendingIntent(), 350 ) 351 .apply { 352 if (confirmationDialog && SdkLevel.isAtLeastU()) { 353 setConfirmationDialogDetails( 354 SafetyCenterIssue.Action.ConfirmationDialogDetails( 355 "Confirmation title", 356 "Confirmation text", 357 "Confirmation yes", 358 "Confirmation no", 359 ) 360 ) 361 } 362 } 363 .build() 364 ) 365 ) <lambda>null366 .apply { 367 if (SdkLevel.isAtLeastU()) { 368 setAttributionTitle(attributionTitle) 369 setGroupId(groupId) 370 } 371 } 372 .build() 373 374 /** 375 * Returns a critical [SafetyCenterIssue] for the given source and user id that is consistent 376 * with critical [SafetySourceIssue]s used in [SafetySourceTestData]. 377 */ safetyCenterIssueCriticalnull378 fun safetyCenterIssueCritical( 379 sourceId: String, 380 isActionInFlight: Boolean = false, 381 userId: Int = UserHandle.myUserId(), 382 attributionTitle: String? = "OK", 383 groupId: String? = SINGLE_SOURCE_GROUP_ID, 384 ) = 385 SafetyCenterIssue.Builder( 386 issueId(sourceId, CRITICAL_ISSUE_ID, userId = userId), 387 "Critical issue title", 388 "Critical issue summary", 389 ) 390 .setSeverityLevel(ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING) 391 .setActions( 392 listOf( 393 SafetyCenterIssue.Action.Builder( 394 issueActionId( 395 sourceId, 396 CRITICAL_ISSUE_ID, 397 CRITICAL_ISSUE_ACTION_ID, 398 userId, 399 ), 400 "Solve issue", 401 safetySourceTestData.criticalIssueActionPendingIntent, 402 ) 403 .setWillResolve(true) 404 .setIsInFlight(isActionInFlight) 405 .build() 406 ) 407 ) 408 .apply { 409 if (SdkLevel.isAtLeastU()) { 410 setAttributionTitle(attributionTitle) 411 setGroupId(groupId) 412 } 413 } 414 .build() 415 416 /** 417 * Returns the [overall_severity_n_alerts_summary] string formatted for the given number of 418 * alerts. 419 */ getAlertStringnull420 fun getAlertString(numberOfAlerts: Int): String = 421 getIcuPluralsString("overall_severity_n_alerts_summary", numberOfAlerts) 422 423 /** Returns the [refresh_error] string formatted for the given number of error entries. */ 424 fun getRefreshErrorString(numberOfErrorEntries: Int): String = 425 getIcuPluralsString("refresh_error", numberOfErrorEntries) 426 427 private fun getIcuPluralsString(name: String, count: Int, vararg formatArgs: Any): String { 428 val messageFormat = 429 MessageFormat( 430 safetyCenterResourcesApk.getStringByName(name, formatArgs), 431 Locale.getDefault(), 432 ) 433 val arguments = ArrayMap<String, Any>() 434 arguments["count"] = count 435 return messageFormat.format(arguments) 436 } 437 438 companion object { 439 /** The default [SafetyCenterData] returned by the Safety Center APIs. */ 440 val DEFAULT: SafetyCenterData = 441 SafetyCenterData( 442 SafetyCenterStatus.Builder("", "") 443 .setSeverityLevel(OVERALL_SEVERITY_LEVEL_UNKNOWN) 444 .build(), 445 emptyList(), 446 emptyList(), 447 emptyList(), 448 ) 449 450 /** Creates an ID for a Safety Center entry. */ entryIdnull451 fun entryId(sourceId: String, userId: Int = UserHandle.myUserId()) = 452 SafetyCenterIds.encodeToString( 453 SafetyCenterEntryId.newBuilder() 454 .setSafetySourceId(sourceId) 455 .setUserId(userId) 456 .build() 457 ) 458 459 /** Creates an ID for a Safety Center issue. */ 460 fun issueId( 461 sourceId: String, 462 sourceIssueId: String, 463 issueTypeId: String = ISSUE_TYPE_ID, 464 userId: Int = UserHandle.myUserId(), 465 ) = 466 SafetyCenterIds.encodeToString( 467 SafetyCenterIssueId.newBuilder() 468 .setSafetyCenterIssueKey( 469 SafetyCenterIssueKey.newBuilder() 470 .setSafetySourceId(sourceId) 471 .setSafetySourceIssueId(sourceIssueId) 472 .setUserId(userId) 473 .build() 474 ) 475 .setIssueTypeId(issueTypeId) 476 .build() 477 ) 478 479 /** Creates an ID for a Safety Center issue action. */ 480 fun issueActionId( 481 sourceId: String, 482 sourceIssueId: String, 483 sourceIssueActionId: String, 484 userId: Int = UserHandle.myUserId(), 485 ) = 486 SafetyCenterIds.encodeToString( 487 SafetyCenterIssueActionId.newBuilder() 488 .setSafetyCenterIssueKey( 489 SafetyCenterIssueKey.newBuilder() 490 .setSafetySourceId(sourceId) 491 .setSafetySourceIssueId(sourceIssueId) 492 .setUserId(userId) 493 .build() 494 ) 495 .setSafetySourceIssueActionId(sourceIssueActionId) 496 .build() 497 ) 498 499 /** 500 * On U+, returns a new [SafetyCenterData] with the dismissed issues set. Prior to U, 501 * returns the passed in [SafetyCenterData]. 502 */ 503 fun SafetyCenterData.withDismissedIssuesIfAtLeastU( 504 dismissedIssues: List<SafetyCenterIssue> 505 ): SafetyCenterData = 506 if (SdkLevel.isAtLeastU()) { 507 copy(dismissedIssues = dismissedIssues) 508 } else this 509 510 /** Returns a [SafetyCenterData] without extras. */ SafetyCenterDatanull511 fun SafetyCenterData.withoutExtras() = 512 if (SdkLevel.isAtLeastU()) { 513 SafetyCenterData.Builder(this).clearExtras().build() 514 } else this 515 516 /** 517 * On U+, returns a new [SafetyCenterData] with [SafetyCenterIssue]s having the 518 * [attributionTitle]. Prior to U, returns the passed in [SafetyCenterData]. 519 */ SafetyCenterDatanull520 fun SafetyCenterData.withAttributionTitleInIssuesIfAtLeastU( 521 attributionTitle: String? 522 ): SafetyCenterData { 523 return if (SdkLevel.isAtLeastU()) { 524 val issuesWithAttributionTitle = 525 this.issues.map { 526 SafetyCenterIssue.Builder(it).setAttributionTitle(attributionTitle).build() 527 } 528 copy(issues = issuesWithAttributionTitle) 529 } else this 530 } 531 532 /** 533 * On U+, returns a new [SafetyCenterData] with the extras set. Prior to U, returns the 534 * passed in [SafetyCenterData]. 535 */ SafetyCenterDatanull536 fun SafetyCenterData.withExtrasIfAtLeastU(extras: Bundle): SafetyCenterData = 537 if (SdkLevel.isAtLeastU()) { 538 copy(extras = extras) 539 } else this 540 541 @RequiresApi(UPSIDE_DOWN_CAKE) SafetyCenterDatanull542 private fun SafetyCenterData.copy( 543 issues: List<SafetyCenterIssue> = this.issues, 544 dismissedIssues: List<SafetyCenterIssue> = this.dismissedIssues, 545 extras: Bundle = this.extras, 546 ): SafetyCenterData = 547 SafetyCenterData.Builder(status) 548 .apply { 549 issues.forEach(::addIssue) 550 entriesOrGroups.forEach(::addEntryOrGroup) 551 staticEntryGroups.forEach(::addStaticEntryGroup) 552 dismissedIssues.forEach(::addDismissedIssue) 553 } 554 .setExtras(extras) 555 .build() 556 } 557 } 558