Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,11 @@

<!-- VCard details -->
<activity
android:name=".ui.VCardDetailActivity"
android:name=".ui.vcarddetail.VCardDetailActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:screenOrientation="user"
android:label="@string/vcard_detail_activity_title"
android:theme="@style/BugleTheme">
android:theme="@style/Theme.Compose">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.conversation.ConversationActivity" />
Expand Down
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ android {
compose = true
}

testOptions {
unitTests {
isIncludeAndroidResources = true
}
}

sourceSets.getByName("main") {
assets.srcDir("../assets")
manifest.srcFile("../AndroidManifest.xml")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;

import com.android.messaging.datamodel.action.ActionService;
import com.android.messaging.datamodel.action.BackgroundWorker;
Expand All @@ -14,8 +13,6 @@
import com.android.messaging.datamodel.data.GalleryGridItemData;
import com.android.messaging.datamodel.data.LaunchConversationData;
import com.android.messaging.datamodel.data.MediaPickerData;
import com.android.messaging.datamodel.data.MessagePartData;
import com.android.messaging.datamodel.data.VCardContactItemData;
import com.android.messaging.util.Assert;

public class ActionSyncTestDataModel extends DataModel {
Expand Down Expand Up @@ -69,18 +66,6 @@ public LaunchConversationData createLaunchConversationData(
throw new UnsupportedOperationException();
}

@Override
public VCardContactItemData createVCardContactItemData(final Context context,
final MessagePartData data) {
throw new UnsupportedOperationException();
}

@Override
public VCardContactItemData createVCardContactItemData(final Context context,
final Uri vCardUri) {
throw new UnsupportedOperationException();
}

@Override
public DraftMessageData createDraftMessageData(final String conversationId) {
throw new UnsupportedOperationException();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.android.messaging.data.vcard.mapper

import com.android.messaging.data.vcard.model.VCardAvatarPhoto
import com.android.messaging.data.vcard.photo.VCardPhotoDownscaler
import com.android.messaging.datamodel.media.CustomVCardEntry
import com.android.messaging.testutil.mockContactDestinationFormatter
import com.android.vcard.VCardConfig
import com.android.vcard.VCardProperty
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
internal class VCardEntrySummarizerImplTest {

private val photoDownscaler = mockk<VCardPhotoDownscaler>()
private val destinationFormatter = mockContactDestinationFormatter()

private val summarizer = VCardEntrySummarizerImpl(
photoDownscaler = photoDownscaler,
destinationFormatter = destinationFormatter,
)

@Test
fun displayName_missing_isNull() {
assertNull(summarizer.displayName(entryWith()))
}

@Test
fun avatarPhoto_withPhoto_returnsDownscaledBytes() {
val photoBytes = byteArrayOf(1, 2, 3)
val downscaled = byteArrayOf(9)
every { photoDownscaler.downscale(photoBytes) } returns downscaled

val entry = entryWith("FN" to "Ada")
entry.addProperty(
VCardProperty().apply {
setName("PHOTO")
setByteValue(photoBytes)
},
)

assertEquals(VCardAvatarPhoto(downscaled), summarizer.avatarPhoto(entry))
}

@Test
fun avatarPhoto_withoutPhoto_isNullAndSkipsDownscaler() {
assertNull(summarizer.avatarPhoto(entryWith("FN" to "Ada")))
verify(exactly = 0) { photoDownscaler.downscale(any()) }
}

@Test
fun normalizedDestination_prefersFirstPhoneNumber() {
val entry = entryWith(
"FN" to "Ada",
"TEL" to "+1 (555) 000-1",
"EMAIL" to "ada@example.com",
)

assertEquals("+15550001", summarizer.normalizedDestination(entry))
}

@Test
fun normalizedDestination_fallsBackToEmail() {
val entry = entryWith(
"FN" to "Ada",
"EMAIL" to " Ada@Example.COM ",
)

assertEquals("ada@example.com", summarizer.normalizedDestination(entry))
}

@Test
fun normalizedDestination_withoutPhoneOrEmail_isNull() {
assertNull(summarizer.normalizedDestination(entryWith("FN" to "Ada")))
}

@Test
fun isLocation_matchesKindPropertyIgnoringCase() {
assertTrue(summarizer.isLocation(entryWith("KIND" to "Location")))
assertFalse(summarizer.isLocation(entryWith("KIND" to "individual")))
assertFalse(summarizer.isLocation(entryWith()))
}

@Test
fun firstPostalAddress_returnsFormattedAddress() {
val entry = entryWith("FN" to "Ada")
entry.addProperty(
VCardProperty().apply {
setName("ADR")
setValues("", "", "1 Engine Way", "London", "", "NW1", "UK")
},
)
val address = summarizer.firstPostalAddress(entry)

assertTrue(requireNotNull(address).contains("1 Engine Way"))
}

@Test
fun firstPostalAddress_withoutPostal_isNull() {
assertNull(summarizer.firstPostalAddress(entryWith("FN" to "Ada")))
}

private fun entryWith(vararg properties: Pair<String, String>): CustomVCardEntry {
val entry = CustomVCardEntry(VCardConfig.VCARD_TYPE_V21_GENERIC, null)

properties.forEach { (name, value) ->
entry.addProperty(
VCardProperty().apply {
setName(name)
setValues(value)
rawValue = value
},
)
}

return entry
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.android.messaging.data.vcard.parser

import android.content.ContentResolver
import android.content.Context
import io.mockk.every
import io.mockk.mockk
import java.io.ByteArrayInputStream
import java.io.IOException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
internal class VCardParserImplTest {

private val contentResolver = mockk<ContentResolver>()
private val context = mockk<Context>()
private val testDispatcher = UnconfinedTestDispatcher()

private val parser = VCardParserImpl(
context = context,
ioDispatcher = testDispatcher,
)

init {
every { context.contentResolver } returns contentResolver
}

@Test
fun parse_blankUri_returnsEmpty() = runTest(testDispatcher) {
val result = parser.parse(" ")

assertTrue(result.isEmpty())
}

@Test
fun parse_validSingleVCard_returnsEntryWithFields() = runTest(testDispatcher) {
givenVCardContent(
"""
BEGIN:VCARD
VERSION:2.1
N:Lovelace;Ada
FN:Ada Lovelace
TEL;CELL:+15550001
END:VCARD
""".trimIndent(),
)

val result = parser.parse("content://vcard")

assertEquals(1, result.size)
assertTrue(result.first().phoneList.first().number.contains("555"))
}

@Test
fun parse_version30VCard_isParsedViaCascade() = runTest(testDispatcher) {
givenVCardContent(
"""
BEGIN:VCARD
VERSION:3.0
FN:Ada Lovelace
TEL:+15550001
END:VCARD
""".trimIndent(),
)

val result = parser.parse("content://vcard")

assertEquals(1, result.size)
assertTrue(result.first().phoneList.first().number.contains("555"))
}

@Test
fun parse_version40VCard_isParsedViaCascade() = runTest(testDispatcher) {
givenVCardContent(
"""
BEGIN:VCARD
VERSION:4.0
FN:Ada Lovelace
TEL:+15550001
END:VCARD
""".trimIndent(),
)

val result = parser.parse("content://vcard")

assertEquals(1, result.size)
assertTrue(result.first().phoneList.first().number.contains("555"))
}

@Test
fun parse_multipleVCards_returnsAllEntries() = runTest(testDispatcher) {
givenVCardContent(
"""
BEGIN:VCARD
VERSION:2.1
FN:Ada Lovelace
END:VCARD
BEGIN:VCARD
VERSION:2.1
FN:Alan Turing
END:VCARD
""".trimIndent(),
)

val result = parser.parse("content://vcard")

assertEquals(2, result.size)
}

@Test
fun parse_missingContent_returnsEmpty() = runTest(testDispatcher) {
every { contentResolver.openInputStream(any()) } returns null

val result = parser.parse("content://vcard")

assertTrue(result.isEmpty())
}

@Test
fun parse_unreadableContent_returnsEmpty() = runTest(testDispatcher) {
every { contentResolver.openInputStream(any()) } throws IOException("boom")

val result = parser.parse("content://vcard")

assertTrue(result.isEmpty())
}

@Test
fun parse_revokedUriGrant_returnsEmpty() = runTest(testDispatcher) {
every { contentResolver.openInputStream(any()) } throws SecurityException("revoked")

val result = parser.parse("content://vcard")

assertTrue(result.isEmpty())
}

private fun givenVCardContent(content: String) {
val bytes = content.replace("\n", "\r\n").toByteArray()
every { contentResolver.openInputStream(any()) } answers { ByteArrayInputStream(bytes) }
}
}
Loading