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