blob: 78729adbdacf91c825289acf8220faccc951423c [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.safetycenter.cts.testing
import android.app.PendingIntent
import android.content.Context
import android.icu.text.MessageFormat
import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE
import android.os.Bundle
import android.os.UserHandle
import android.safetycenter.SafetyCenterData
import android.safetycenter.SafetyCenterEntry
import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING
import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK
import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_RECOMMENDATION
import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN
import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED
import android.safetycenter.SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON
import android.safetycenter.SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION
import android.safetycenter.SafetyCenterIssue
import android.safetycenter.SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING
import android.safetycenter.SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK
import android.safetycenter.SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_RECOMMENDATION
import android.safetycenter.SafetyCenterStatus
import android.safetycenter.SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING
import android.safetycenter.SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN
import android.safetycenter.cts.testing.SafetyCenterCtsConfigs.SINGLE_SOURCE_GROUP_ID
import android.safetycenter.cts.testing.SafetySourceCtsData.Companion.CRITICAL_ISSUE_ACTION_ID
import android.safetycenter.cts.testing.SafetySourceCtsData.Companion.CRITICAL_ISSUE_ID
import android.safetycenter.cts.testing.SafetySourceCtsData.Companion.INFORMATION_ISSUE_ACTION_ID
import android.safetycenter.cts.testing.SafetySourceCtsData.Companion.INFORMATION_ISSUE_ID
import android.safetycenter.cts.testing.SafetySourceCtsData.Companion.ISSUE_TYPE_ID
import android.safetycenter.cts.testing.SafetySourceCtsData.Companion.RECOMMENDATION_ISSUE_ACTION_ID
import android.safetycenter.cts.testing.SafetySourceCtsData.Companion.RECOMMENDATION_ISSUE_ID
import android.util.ArrayMap
import androidx.annotation.RequiresApi
import com.android.modules.utils.build.SdkLevel
import com.android.safetycenter.internaldata.SafetyCenterEntryId
import com.android.safetycenter.internaldata.SafetyCenterIds
import com.android.safetycenter.internaldata.SafetyCenterIssueActionId
import com.android.safetycenter.internaldata.SafetyCenterIssueId
import com.android.safetycenter.internaldata.SafetyCenterIssueKey
import com.android.safetycenter.resources.SafetyCenterResourcesContext
import java.util.Locale
/**
* A class that provides [SafetyCenterData] objects and associated constants to facilitate asserting
* on specific Safety Center states in SafetyCenter for testing.
*/
class SafetyCenterCtsData(context: Context) {
private val safetyCenterResourcesContext = SafetyCenterResourcesContext.forTests(context)
private val safetySourceCtsData = SafetySourceCtsData(context)
/**
* The [SafetyCenterStatus] used when the overall status is unknown and no scan is in progress.
*/
val safetyCenterStatusUnknown =
SafetyCenterStatus.Builder(
safetyCenterResourcesContext.getStringByName(
"overall_severity_level_ok_review_title"
),
safetyCenterResourcesContext.getStringByName(
"overall_severity_level_ok_review_summary"
)
)
.setSeverityLevel(OVERALL_SEVERITY_LEVEL_UNKNOWN)
.build()
/**
* Returns a [SafetyCenterStatus] with one alert and the given [statusResource] and
* [overallSeverityLevel].
*/
fun safetyCenterStatusOneAlert(
statusResource: String,
overallSeverityLevel: Int
): SafetyCenterStatus =
SafetyCenterStatus.Builder(
safetyCenterResourcesContext.getStringByName(statusResource),
getAlertString(1)
)
.setSeverityLevel(overallSeverityLevel)
.build()
/**
* Returns the [SafetyCenterStatus] used when the overall status is critical and no scan is in
* progress for the given number of alerts.
*/
fun safetyCenterStatusCritical(numAlerts: Int) =
SafetyCenterStatus.Builder(
safetyCenterResourcesContext.getStringByName(
"overall_severity_level_critical_safety_warning_title"
),
getAlertString(numAlerts)
)
.setSeverityLevel(OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING)
.build()
/**
* Returns a [SafetyCenterEntry] builder with a grey icon (for unknown severity), the summary
* generally used for sources of the [SafetyCenterCtsConfigs], and a pending intent that
* redirects to [TestActivity] for the given source, user id, and title.
*/
fun safetyCenterEntryDefaultBuilder(
sourceId: String,
userId: Int = UserHandle.myUserId(),
title: CharSequence = "OK",
pendingIntent: PendingIntent? = safetySourceCtsData.testActivityRedirectPendingIntent
) =
SafetyCenterEntry.Builder(entryId(sourceId, userId), title)
.setSeverityLevel(ENTRY_SEVERITY_LEVEL_UNKNOWN)
.setSummary("OK")
.setPendingIntent(pendingIntent)
.setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION)
/**
* Returns a [SafetyCenterEntry] with a grey icon (for unknown severity), the summary generally
* used for sources of the [SafetyCenterCtsConfigs], and a pending intent that redirects to
* Safety Center for the given source, user id, and title.
*/
fun safetyCenterEntryDefault(
sourceId: String,
userId: Int = UserHandle.myUserId(),
title: CharSequence = "OK",
pendingIntent: PendingIntent? = safetySourceCtsData.testActivityRedirectPendingIntent
) = safetyCenterEntryDefaultBuilder(sourceId, userId, title, pendingIntent).build()
/**
* Returns a [SafetyCenterEntry] builder with no icon, the summary generally used for sources of
* the [SafetyCenterCtsConfigs], and a pending intent that redirects to [TestActivity] for the
* given source, user id, and title.
*/
fun safetyCenterEntryDefaultStaticBuilder(
sourceId: String,
userId: Int = UserHandle.myUserId(),
title: CharSequence = "OK"
) =
SafetyCenterEntry.Builder(entryId(sourceId, userId), title)
.setSeverityLevel(ENTRY_SEVERITY_LEVEL_UNSPECIFIED)
.setSummary("OK")
.setPendingIntent(safetySourceCtsData.testActivityRedirectPendingIntent)
.setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON)
/**
* Returns a [SafetyCenterEntry] with a grey icon (for unknown severity), a refresh error
* summary, and a pending intent that redirects to [TestActivity] for the given source, user id,
* and title.
*/
fun safetyCenterEntryError(sourceId: String) =
safetyCenterEntryDefaultBuilder(sourceId).setSummary(getRefreshErrorString(1)).build()
/**
* Returns a disabled [SafetyCenterEntry] with a grey icon (for unspecified severity), a
* standard summary, and a standard title for the given source and pending intent.
*/
fun safetyCenterEntryUnspecified(
sourceId: String,
pendingIntent: PendingIntent? = safetySourceCtsData.testActivityRedirectPendingIntent
) =
SafetyCenterEntry.Builder(entryId(sourceId), "Unspecified title")
.setSeverityLevel(ENTRY_SEVERITY_LEVEL_UNSPECIFIED)
.setSummary("Unspecified summary")
.setPendingIntent(pendingIntent)
.setEnabled(false)
.setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION)
.build()
/**
* Returns a [SafetyCenterEntry] builder with a green icon (for ok severity), a standard
* summary, and a pending intent that redirects to [TestActivity] for the given source, user id,
* and title.
*/
fun safetyCenterEntryOkBuilder(
sourceId: String,
userId: Int = UserHandle.myUserId(),
title: CharSequence = "Ok title"
) =
SafetyCenterEntry.Builder(entryId(sourceId, userId), title)
.setSeverityLevel(ENTRY_SEVERITY_LEVEL_OK)
.setSummary("Ok summary")
.setPendingIntent(safetySourceCtsData.testActivityRedirectPendingIntent)
.setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION)
/**
* Returns a [SafetyCenterEntry] with a green icon (for ok severity), a standard summary, and a
* pending intent that redirects to [TestActivity] for the given source, user id, and title.
*/
fun safetyCenterEntryOk(
sourceId: String,
userId: Int = UserHandle.myUserId(),
title: CharSequence = "Ok title"
) = safetyCenterEntryOkBuilder(sourceId, userId, title).build()
/**
* Returns a [SafetyCenterEntry] with a yellow icon (for recommendation severity), a standard
* title, and a pending intent that redirects to [TestActivity] for the given source and
* summary.
*/
fun safetyCenterEntryRecommendation(
sourceId: String,
summary: String = "Recommendation summary"
) =
SafetyCenterEntry.Builder(entryId(sourceId), "Recommendation title")
.setSeverityLevel(ENTRY_SEVERITY_LEVEL_RECOMMENDATION)
.setSummary(summary)
.setPendingIntent(safetySourceCtsData.testActivityRedirectPendingIntent)
.setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION)
.build()
/**
* Returns a [SafetyCenterEntry] with a red icon (for critical severity), a standard title, a
* standard summary, and a pending intent that redirects to [TestActivity] for the given source.
*/
fun safetyCenterEntryCritical(sourceId: String) =
SafetyCenterEntry.Builder(entryId(sourceId), "Critical title")
.setSeverityLevel(ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING)
.setSummary("Critical summary")
.setPendingIntent(safetySourceCtsData.testActivityRedirectPendingIntent)
.setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION)
.build()
/**
* Returns an information [SafetyCenterIssue] for the given source and user id that is
* consistent with information [SafetySourceIssue]s used in [SafetySourceCtsData].
*/
fun safetyCenterIssueInformation(
sourceId: String,
userId: Int = UserHandle.myUserId(),
attributionTitle: String? = "OK",
groupId: String? = SINGLE_SOURCE_GROUP_ID
) =
SafetyCenterIssue.Builder(
issueId(sourceId, INFORMATION_ISSUE_ID, userId = userId),
"Information issue title",
"Information issue summary"
)
.setSeverityLevel(ISSUE_SEVERITY_LEVEL_OK)
.setShouldConfirmDismissal(false)
.setActions(
listOf(
SafetyCenterIssue.Action.Builder(
issueActionId(
sourceId,
INFORMATION_ISSUE_ID,
INFORMATION_ISSUE_ACTION_ID,
userId
),
"Review",
safetySourceCtsData.testActivityRedirectPendingIntent
)
.build()
)
)
.apply {
if (SdkLevel.isAtLeastU()) {
setAttributionTitle(attributionTitle)
setGroupId(groupId)
}
}
.build()
/**
* Returns a recommendation [SafetyCenterIssue] for the given source and user id that is
* consistent with recommendation [SafetySourceIssue]s used in [SafetySourceCtsData].
*/
fun safetyCenterIssueRecommendation(
sourceId: String,
userId: Int = UserHandle.myUserId(),
attributionTitle: String? = "OK",
groupId: String? = SINGLE_SOURCE_GROUP_ID
) =
SafetyCenterIssue.Builder(
issueId(sourceId, RECOMMENDATION_ISSUE_ID, userId = userId),
"Recommendation issue title",
"Recommendation issue summary"
)
.setSeverityLevel(ISSUE_SEVERITY_LEVEL_RECOMMENDATION)
.setActions(
listOf(
SafetyCenterIssue.Action.Builder(
issueActionId(
sourceId,
RECOMMENDATION_ISSUE_ID,
RECOMMENDATION_ISSUE_ACTION_ID,
userId
),
"See issue",
safetySourceCtsData.testActivityRedirectPendingIntent
)
.build()
)
)
.apply {
if (SdkLevel.isAtLeastU()) {
setAttributionTitle(attributionTitle)
setGroupId(groupId)
}
}
.build()
/**
* Returns a critical [SafetyCenterIssue] for the given source and user id that is consistent
* with critical [SafetySourceIssue]s used in [SafetySourceCtsData].
*/
fun safetyCenterIssueCritical(
sourceId: String,
isActionInFlight: Boolean = false,
userId: Int = UserHandle.myUserId(),
attributionTitle: String? = "OK",
groupId: String? = SINGLE_SOURCE_GROUP_ID
) =
SafetyCenterIssue.Builder(
issueId(sourceId, CRITICAL_ISSUE_ID, userId = userId),
"Critical issue title",
"Critical issue summary"
)
.setSeverityLevel(ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING)
.setActions(
listOf(
SafetyCenterIssue.Action.Builder(
issueActionId(
sourceId,
CRITICAL_ISSUE_ID,
CRITICAL_ISSUE_ACTION_ID,
userId
),
"Solve issue",
safetySourceCtsData.criticalIssueActionPendingIntent
)
.setWillResolve(true)
.setIsInFlight(isActionInFlight)
.build()
)
)
.apply {
if (SdkLevel.isAtLeastU()) {
setAttributionTitle(attributionTitle)
setGroupId(groupId)
}
}
.build()
/**
* Returns the [overall_severity_n_alerts_summary] string formatted for the given number of
* alerts.
*/
fun getAlertString(numberOfAlerts: Int): String =
getIcuPluralsString("overall_severity_n_alerts_summary", numberOfAlerts)
/** Returns the [refresh_error] string formatted for the given number of error entries. */
fun getRefreshErrorString(numberOfErrorEntries: Int): String =
getIcuPluralsString("refresh_error", numberOfErrorEntries)
private fun getIcuPluralsString(name: String, count: Int, vararg formatArgs: Any): String {
val messageFormat =
MessageFormat(
safetyCenterResourcesContext.getStringByName(name, formatArgs),
Locale.getDefault()
)
val arguments = ArrayMap<String, Any>()
arguments["count"] = count
return messageFormat.format(arguments)
}
companion object {
/** The default [SafetyCenterData] returned by the Safety Center APIs. */
val DEFAULT: SafetyCenterData =
SafetyCenterData(
SafetyCenterStatus.Builder("", "")
.setSeverityLevel(OVERALL_SEVERITY_LEVEL_UNKNOWN)
.build(),
emptyList(),
emptyList(),
emptyList()
)
/** Creates an ID for a Safety Center entry. */
fun entryId(sourceId: String, userId: Int = UserHandle.myUserId()) =
SafetyCenterIds.encodeToString(
SafetyCenterEntryId.newBuilder()
.setSafetySourceId(sourceId)
.setUserId(userId)
.build()
)
/** Creates an ID for a Safety Center issue. */
fun issueId(
sourceId: String,
sourceIssueId: String,
issueTypeId: String = ISSUE_TYPE_ID,
userId: Int = UserHandle.myUserId()
) =
SafetyCenterIds.encodeToString(
SafetyCenterIssueId.newBuilder()
.setSafetyCenterIssueKey(
SafetyCenterIssueKey.newBuilder()
.setSafetySourceId(sourceId)
.setSafetySourceIssueId(sourceIssueId)
.setUserId(userId)
.build()
)
.setIssueTypeId(issueTypeId)
.build()
)
/** Creates an ID for a Safety Center issue action. */
fun issueActionId(
sourceId: String,
sourceIssueId: String,
sourceIssueActionId: String,
userId: Int = UserHandle.myUserId()
) =
SafetyCenterIds.encodeToString(
SafetyCenterIssueActionId.newBuilder()
.setSafetyCenterIssueKey(
SafetyCenterIssueKey.newBuilder()
.setSafetySourceId(sourceId)
.setSafetySourceIssueId(sourceIssueId)
.setUserId(userId)
.build()
)
.setSafetySourceIssueActionId(sourceIssueActionId)
.build()
)
/**
* On U+, returns a new [SafetyCenterData] with the dismissed issues set. Prior to U,
* returns the passed in [SafetyCenterData].
*/
fun SafetyCenterData.withDismissedIssuesIfAtLeastU(
dismissedIssues: List<SafetyCenterIssue>
): SafetyCenterData =
if (SdkLevel.isAtLeastU()) {
copy(dismissedIssues = dismissedIssues)
} else this
/**
* On U+, returns a new [SafetyCenterData] with [SafetyCenterIssue]s having the
* [attributionTitle]. Prior to U, returns the passed in [SafetyCenterData].
*/
fun SafetyCenterData.withAttributionTitleInIssuesIfAtLeastU(
attributionTitle: String?
): SafetyCenterData {
return if (SdkLevel.isAtLeastU()) {
val issuesWithAttributionTitle =
this.issues.map {
SafetyCenterIssue.Builder(it).setAttributionTitle(attributionTitle).build()
}
copy(issues = issuesWithAttributionTitle)
} else this
}
/**
* On U+, returns a new [SafetyCenterData] with the extras set. Prior to U, returns the
* passed in [SafetyCenterData].
*/
fun SafetyCenterData.withExtrasIfAtLeastU(extras: Bundle): SafetyCenterData =
if (SdkLevel.isAtLeastU()) {
copy(extras = extras)
} else this
@RequiresApi(UPSIDE_DOWN_CAKE)
private fun SafetyCenterData.copy(
issues: List<SafetyCenterIssue> = this.issues,
dismissedIssues: List<SafetyCenterIssue> = this.dismissedIssues,
extras: Bundle = this.extras
): SafetyCenterData =
SafetyCenterData.Builder(status)
.apply {
issues.forEach(::addIssue)
entriesOrGroups.forEach(::addEntryOrGroup)
staticEntryGroups.forEach(::addStaticEntryGroup)
dismissedIssues.forEach(::addDismissedIssue)
}
.setExtras(extras)
.build()
}
}