Add initial instrumentation testing for V2 ConversationItem shapes.

This commit is contained in:
Alex Hart 2023-06-30 14:37:37 -03:00 committed by Greyson Parrelli
parent 47b97aafc6
commit 3040b70100
5 changed files with 409 additions and 9 deletions

View file

@ -0,0 +1,325 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2.items
import android.net.Uri
import android.view.View
import androidx.lifecycle.Observer
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.conversation.ConversationAdapter
import org.thoughtcrime.securesms.conversation.ConversationItem
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.database.FakeMessageRecords
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.testing.SignalActivityRule
import kotlin.time.Duration.Companion.minutes
class V2ConversationItemShapeTest {
@get:Rule
val harness = SignalActivityRule(othersCount = 10)
@Test
fun givenNextAndPreviousMessageDoNotExist_whenISetMessageShape_thenIExpectSingle() {
val testSubject = V2ConversationItemShape(FakeConversationContext())
val expected = V2ConversationItemShape.MessageShape.SINGLE
val actual = testSubject.setMessageShape(
isLtr = true,
currentMessage = getMessageRecord(),
isGroupThread = false,
adapterPosition = 5
)
assertEquals(expected, actual)
}
@Test
fun givenPreviousWithinTimeoutAndNoNext_whenISetMessageShape_thenIExpectEnd() {
val now = System.currentTimeMillis()
val prev = now - 2.minutes.inWholeMilliseconds
val testSubject = V2ConversationItemShape(
FakeConversationContext(
previousMessage = getMessageRecord(prev)
)
)
val expected = V2ConversationItemShape.MessageShape.END
val actual = testSubject.setMessageShape(
isLtr = true,
currentMessage = getMessageRecord(now),
isGroupThread = false,
adapterPosition = 5
)
assertEquals(expected, actual)
}
@Test
fun givenNextWithinTimeoutAndNoPrevious_whenISetMessageShape_thenIExpectStart() {
val now = System.currentTimeMillis()
val prev = now - 2.minutes.inWholeMilliseconds
val testSubject = V2ConversationItemShape(
FakeConversationContext(
nextMessage = getMessageRecord(now)
)
)
val expected = V2ConversationItemShape.MessageShape.START
val actual = testSubject.setMessageShape(
isLtr = true,
currentMessage = getMessageRecord(prev),
isGroupThread = false,
adapterPosition = 5
)
assertEquals(expected, actual)
}
@Test
fun givenPreviousAndNextWithinTimeout_whenISetMessageShape_thenIExpectMiddle() {
val now = System.currentTimeMillis()
val prev = now - 2.minutes.inWholeMilliseconds
val next = now + 2.minutes.inWholeMilliseconds
val testSubject = V2ConversationItemShape(
FakeConversationContext(
previousMessage = getMessageRecord(prev),
nextMessage = getMessageRecord(next)
)
)
val expected = V2ConversationItemShape.MessageShape.MIDDLE
val actual = testSubject.setMessageShape(
isLtr = true,
currentMessage = getMessageRecord(now),
isGroupThread = false,
adapterPosition = 5
)
assertEquals(expected, actual)
}
@Test
fun givenPreviousOutsideTimeoutAndNoNext_whenISetMessageShape_thenIExpectSingle() {
val now = System.currentTimeMillis()
val prev = now - 4.minutes.inWholeMilliseconds
val testSubject = V2ConversationItemShape(
FakeConversationContext(
previousMessage = getMessageRecord(prev)
)
)
val expected = V2ConversationItemShape.MessageShape.SINGLE
val actual = testSubject.setMessageShape(
isLtr = true,
currentMessage = getMessageRecord(now),
isGroupThread = false,
adapterPosition = 5
)
assertEquals(expected, actual)
}
@Test
fun givenNextOutsideTimeoutAndNoPrevious_whenISetMessageShape_thenIExpectSingle() {
val now = System.currentTimeMillis()
val prev = now - 4.minutes.inWholeMilliseconds
val testSubject = V2ConversationItemShape(
FakeConversationContext(
nextMessage = getMessageRecord(now)
)
)
val expected = V2ConversationItemShape.MessageShape.SINGLE
val actual = testSubject.setMessageShape(
isLtr = true,
currentMessage = getMessageRecord(prev),
isGroupThread = false,
adapterPosition = 5
)
assertEquals(expected, actual)
}
@Test
fun givenPreviousAndNextOutsideTimeout_whenISetMessageShape_thenIExpectSingle() {
val now = System.currentTimeMillis()
val prev = now - 4.minutes.inWholeMilliseconds
val next = now + 4.minutes.inWholeMilliseconds
val testSubject = V2ConversationItemShape(
FakeConversationContext(
previousMessage = getMessageRecord(prev),
nextMessage = getMessageRecord(next)
)
)
val expected = V2ConversationItemShape.MessageShape.SINGLE
val actual = testSubject.setMessageShape(
isLtr = true,
currentMessage = getMessageRecord(now),
isGroupThread = false,
adapterPosition = 5
)
assertEquals(expected, actual)
}
private fun getMessageRecord(
timestamp: Long = System.currentTimeMillis()
): MessageRecord {
return FakeMessageRecords.buildMediaMmsMessageRecord(
dateReceived = timestamp,
dateSent = timestamp,
dateServer = timestamp
)
}
private class FakeConversationContext(
private val hasWallpaper: Boolean = false,
private val previousMessage: MessageRecord? = null,
private val nextMessage: MessageRecord? = null
) : V2ConversationContext {
private val colorizer = Colorizer()
override val displayMode: ConversationItemDisplayMode = ConversationItemDisplayMode.STANDARD
override val clickListener: ConversationAdapter.ItemClickListener = FakeConversationItemClickListener
override fun onStartExpirationTimeout(messageRecord: MessageRecord) = Unit
override fun hasWallpaper(): Boolean = hasWallpaper
override fun getColorizer(): Colorizer = colorizer
override fun getNextMessage(adapterPosition: Int): MessageRecord? = nextMessage
override fun getPreviousMessage(adapterPosition: Int): MessageRecord? = previousMessage
}
private object FakeConversationItemClickListener : ConversationAdapter.ItemClickListener {
override fun onQuoteClicked(messageRecord: MmsMessageRecord?) = Unit
override fun onLinkPreviewClicked(linkPreview: LinkPreview) = Unit
override fun onQuotedIndicatorClicked(messageRecord: MessageRecord) = Unit
override fun onMoreTextClicked(conversationRecipientId: RecipientId, messageId: Long, isMms: Boolean) = Unit
override fun onStickerClicked(stickerLocator: StickerLocator) = Unit
override fun onViewOnceMessageClicked(messageRecord: MmsMessageRecord) = Unit
override fun onSharedContactDetailsClicked(contact: Contact, avatarTransitionView: View) = Unit
override fun onAddToContactsClicked(contact: Contact) = Unit
override fun onMessageSharedContactClicked(choices: MutableList<Recipient>) = Unit
override fun onInviteSharedContactClicked(choices: MutableList<Recipient>) = Unit
override fun onReactionClicked(multiselectPart: MultiselectPart, messageId: Long, isMms: Boolean) = Unit
override fun onGroupMemberClicked(recipientId: RecipientId, groupId: GroupId) = Unit
override fun onMessageWithErrorClicked(messageRecord: MessageRecord) = Unit
override fun onMessageWithRecaptchaNeededClicked(messageRecord: MessageRecord) = Unit
override fun onIncomingIdentityMismatchClicked(recipientId: RecipientId) = Unit
override fun onRegisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) = Unit
override fun onUnregisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) = Unit
override fun onVoiceNotePause(uri: Uri) = Unit
override fun onVoiceNotePlay(uri: Uri, messageId: Long, position: Double) = Unit
override fun onVoiceNoteSeekTo(uri: Uri, position: Double) = Unit
override fun onVoiceNotePlaybackSpeedChanged(uri: Uri, speed: Float) = Unit
override fun onGroupMigrationLearnMoreClicked(membershipChange: GroupMigrationMembershipChange) = Unit
override fun onChatSessionRefreshLearnMoreClicked() = Unit
override fun onBadDecryptLearnMoreClicked(author: RecipientId) = Unit
override fun onSafetyNumberLearnMoreClicked(recipient: Recipient) = Unit
override fun onJoinGroupCallClicked() = Unit
override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) = Unit
override fun onEnableCallNotificationsClicked() = Unit
override fun onPlayInlineContent(conversationMessage: ConversationMessage?) = Unit
override fun onInMemoryMessageClicked(messageRecord: InMemoryMessageRecord) = Unit
override fun onViewGroupDescriptionChange(groupId: GroupId?, description: String, isMessageRequestAccepted: Boolean) = Unit
override fun onChangeNumberUpdateContact(recipient: Recipient) = Unit
override fun onCallToAction(action: String) = Unit
override fun onDonateClicked() = Unit
override fun onBlockJoinRequest(recipient: Recipient) = Unit
override fun onRecipientNameClicked(target: RecipientId) = Unit
override fun onInviteToSignalClicked() = Unit
override fun onActivatePaymentsClicked() = Unit
override fun onSendPaymentClicked(recipientId: RecipientId) = Unit
override fun onScheduledIndicatorClicked(view: View, conversationMessage: ConversationMessage) = Unit
override fun onUrlClicked(url: String): Boolean = false
override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) = Unit
override fun onGiftBadgeRevealed(messageRecord: MessageRecord) = Unit
override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) = Unit
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) = Unit
override fun onItemClick(item: MultiselectPart?) = Unit
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) = Unit
}
}

View file

@ -8,13 +8,12 @@ package org.thoughtcrime.securesms.conversation.v2.items
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import org.signal.core.util.dp
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.Projection
import org.thoughtcrime.securesms.util.isScheduled
import java.util.concurrent.TimeUnit
import kotlin.math.abs
import kotlin.time.Duration.Companion.minutes
/**
* Determines the shape for a conversation item based off of the appearance context
@ -30,6 +29,8 @@ class V2ConversationItemShape(
private var collapsedSpacing: Float = 1f.dp
private var defaultSpacing: Float = 8f.dp
private val clusterTimeout = 3.minutes
}
var corners: Projection.Corners = Projection.Corners(bigRadius)
@ -49,13 +50,12 @@ class V2ConversationItemShape(
*/
fun setMessageShape(
isLtr: Boolean,
conversationMessage: ConversationMessage,
currentMessage: MessageRecord,
isGroupThread: Boolean,
adapterPosition: Int
): MessageShape {
val currentMessage: MessageRecord = conversationMessage.messageRecord
val nextMessage: MessageRecord? = conversationContext.getNextMessage(adapterPosition)
val previousMessage: MessageRecord? = conversationContext.getPreviousMessage(adapterPosition)
val isGroupThread: Boolean = conversationMessage.threadRecipient.isGroup
if (isSingularMessage(currentMessage, previousMessage, nextMessage, isGroupThread)) {
setBodyBubbleCorners(isLtr, bigRadius, bigRadius, bigRadius, bigRadius)
@ -153,7 +153,7 @@ class V2ConversationItemShape(
}
private fun isWithinClusteringTime(currentMessage: MessageRecord, previousMessage: MessageRecord): Boolean {
return abs(currentMessage.dateSent - previousMessage.dateSent) <= TimeUnit.MINUTES.toMillis(3)
return abs(currentMessage.dateSent - previousMessage.dateSent) <= clusterTimeout.inWholeMilliseconds
}
enum class MessageShape {

View file

@ -97,9 +97,10 @@ class V2TextOnlyViewHolder<Model : MappingModel<Model>>(
}
val shape = shapeDelegate.setMessageShape(
itemView.layoutDirection == View.LAYOUT_DIRECTION_LTR,
conversationMessage,
bindingAdapterPosition
isLtr = itemView.layoutDirection == View.LAYOUT_DIRECTION_LTR,
currentMessage = conversationMessage.messageRecord,
isGroupThread = conversationMessage.threadRecipient.isGroup,
adapterPosition = bindingAdapterPosition
)
binding.conversationItemBody.setTextColor(themeDelegate.getBodyTextColor(conversationMessage))

View file

@ -1843,6 +1843,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="66de3038ddc4f240a2b3331bc9863b379c3999a1a8b0923dc731f11a9a295034" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.emulator" name="proto" version="31.0.2">
<artifact name="proto-31.0.2.jar">
<sha256 value="03c9f2a9eb08ebad38da2352e82e5ab08fa4fac7083fe771942aa29f60ab5bab" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.external.com-intellij" name="intellij-core" version="30.2.2">
<artifact name="intellij-core-30.2.2.jar">
<sha256 value="cdec995f5c1a8839b996188fa8e0212b303021a48d4eed9659fcb5a7df92a364" origin="Generated by Gradle"/>
@ -1961,6 +1966,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="e8aace773417569f1e0989d62b943891ff6e6eaf09f10ec7a5ef4fe91775bc0c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-device-provider-ddmlib" version="31.0.2">
<artifact name="android-device-provider-ddmlib-31.0.2.jar">
<sha256 value="9972f7f0be3b9999feb88a4a20928aa7c8305d715b0aa71155669c9afc48511b" origin="Generated by Gradle"/>
</artifact>
<artifact name="android-device-provider-ddmlib-31.0.2.module">
<sha256 value="10cd2df76d221953915c89afa9f2a05378b14c5bd94de17ad3005cf309d21e81" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-device-provider-ddmlib-proto" version="31.0.2">
<artifact name="android-device-provider-ddmlib-proto-31.0.2.jar">
<sha256 value="332794db1e7702c9689c8075532f235cccbf57d70aba29cbd7d378e781bad37f" origin="Generated by Gradle"/>
@ -1974,6 +1987,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="93aa2a29181c4f7404d0c49f6bb7c392713f9018e2cecc0a23f846e86c3f360b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-device-provider-gradle" version="31.0.2">
<artifact name="android-device-provider-gradle-31.0.2.jar">
<sha256 value="55e574b7f1c1196e61718cdc0419b7fa0e7baef07e5df4b92e3cdd7b3aa0f297" origin="Generated by Gradle"/>
</artifact>
<artifact name="android-device-provider-gradle-31.0.2.module">
<sha256 value="37b0f23856e6206a74ac1891c455115c88e16d4d89ec3cc62e9902dc1c116a91" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-device-provider-gradle-proto" version="31.0.2">
<artifact name="android-device-provider-gradle-proto-31.0.2.jar">
<sha256 value="48f8b288d3b3195199ded91cbaa67a28be2fb9d317da187b88a7d4ffb505a578" origin="Generated by Gradle"/>
@ -1987,6 +2008,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="cf36bcd4e8af5b240f8f7d80f231ffaf14a442cfe87dea16c5fc4fe8a16c05bb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-host-additional-test-output" version="31.0.2">
<artifact name="android-test-plugin-host-additional-test-output-31.0.2.jar">
<sha256 value="957d41b43c848272f43e2aee91fb21a65710f6d963639ea4bf210546d3102f6c" origin="Generated by Gradle"/>
</artifact>
<artifact name="android-test-plugin-host-additional-test-output-31.0.2.module">
<sha256 value="6ae9f8b15d5679d838b538f09b2972f5f4de20cd8fc7824f7695a984b004a92f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-host-additional-test-output-proto" version="31.0.2">
<artifact name="android-test-plugin-host-additional-test-output-proto-31.0.2.jar">
<sha256 value="6c4d0907badf886cbea14e56e8d08ce232ee775403ad811c27a280520d7efa1a" origin="Generated by Gradle"/>
@ -2000,6 +2029,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="f2e069b05d3b1d2ccffe9a30818ea67182f5376dc7b3ca802301ef3b75f8c69d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-host-coverage" version="31.0.2">
<artifact name="android-test-plugin-host-coverage-31.0.2.jar">
<sha256 value="4753f96f79e26c02dc41367ff1ae9db018558f58702599f372d178ed4bae9016" origin="Generated by Gradle"/>
</artifact>
<artifact name="android-test-plugin-host-coverage-31.0.2.module">
<sha256 value="1c6d141394fab54d47c25d11dfdff681ccee0dc958e61999b4fd2cbdb5bafe12" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-host-coverage-proto" version="31.0.2">
<artifact name="android-test-plugin-host-coverage-proto-31.0.2.jar">
<sha256 value="22686a7d1775171e76534ab8f40f9c9677cabc77321088e3268cb9cb74d0e5c6" origin="Generated by Gradle"/>
@ -2013,11 +2050,24 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="5832d310d2215a9ceb730d0d82519938e43115477d72cc6105835a505dd988d9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-host-device-info" version="31.0.2">
<artifact name="android-test-plugin-host-device-info-31.0.2.jar">
<sha256 value="8e09d5b0c69a76ff1f4561f2f1ecbc5a0bbc325d0b4d6c65bd03dd5146561f72" origin="Generated by Gradle"/>
</artifact>
<artifact name="android-test-plugin-host-device-info-31.0.2.module">
<sha256 value="2e7b95eda2cb48b2461df0270f7665f801e15b4f7c0fe1c1190d933334bf5f88" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-host-device-info-proto" version="31.0.0">
<artifact name="android-test-plugin-host-device-info-proto-31.0.0.jar">
<sha256 value="2981a0892b14f189b27c068020b858d91f02c3951bab9b15d49c30d2d356baea" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-host-device-info-proto" version="31.0.2">
<artifact name="android-test-plugin-host-device-info-proto-31.0.2.jar">
<sha256 value="2981a0892b14f189b27c068020b858d91f02c3951bab9b15d49c30d2d356baea" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-host-logcat" version="31.0.0">
<artifact name="android-test-plugin-host-logcat-31.0.0.jar">
<sha256 value="8e0ca774310c4b6abb769ba466a4cb13f1d7c47395fc19b4834402209e8c322d" origin="Generated by Gradle"/>
@ -2026,6 +2076,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="f4b034d37463a18df4fff04c3fb0155b65eeff8e58b9fb407035352b2cc8d517" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-host-logcat" version="31.0.2">
<artifact name="android-test-plugin-host-logcat-31.0.2.jar">
<sha256 value="8ab98d66d6e47f57c71c082dd4e832ff6dbc1488843cc1b9c8d483887a6de452" origin="Generated by Gradle"/>
</artifact>
<artifact name="android-test-plugin-host-logcat-31.0.2.module">
<sha256 value="5c66828e36a4796b2d1828ef3856399deb9fc8c9e3f62cdbc4385544439eb79d" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-host-logcat-proto" version="31.0.2">
<artifact name="android-test-plugin-host-logcat-proto-31.0.2.jar">
<sha256 value="62234086f76d3dc1d1acbf9def72fcd7741ce2a10de587b4608ab45da62e9bc1" origin="Generated by Gradle"/>
@ -2039,6 +2097,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="2f887040b34faf247889a3e668289d919f4f71d8e93e23c0b89e3a429b3de061" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-host-retention" version="31.0.2">
<artifact name="android-test-plugin-host-retention-31.0.2.jar">
<sha256 value="1b15643fc225427eb12a3c630b4e5a49e3cfc0c4ba65507de965f40505f0a235" origin="Generated by Gradle"/>
</artifact>
<artifact name="android-test-plugin-host-retention-31.0.2.module">
<sha256 value="5147f2a7070bf474a18f8cc44e1d34aaf77ec9fa694091cd17341212a8675ae5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-host-retention-proto" version="31.0.2">
<artifact name="android-test-plugin-host-retention-proto-31.0.2.jar">
<sha256 value="031c4c933c5e53ddb863b960074ba36ec040a2464f208d4f5d390d2b227f6505" origin="Generated by Gradle"/>
@ -2052,6 +2118,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="9544d215345b1ac132bc77d421816e8a503af74dc596eac472824b0327f20e57" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-result-listener-gradle" version="31.0.2">
<artifact name="android-test-plugin-result-listener-gradle-31.0.2.jar">
<sha256 value="04273a4018de59cc8e52b48e29954074c99fbc33617f3323641e960a9ac36354" origin="Generated by Gradle"/>
</artifact>
<artifact name="android-test-plugin-result-listener-gradle-31.0.2.module">
<sha256 value="4e94cac776ee83ec1729c040c002a646bfab20f518baac182214ac893492b8ea" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.android.tools.utp" name="android-test-plugin-result-listener-gradle-proto" version="31.0.2">
<artifact name="android-test-plugin-result-listener-gradle-proto-31.0.2.jar">
<sha256 value="38c664b016fc380676ab80fb9da0c42f13b049cfd98d16467a2049fe85072e70" origin="Generated by Gradle"/>