Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit f1b5c5a3 authored by Ricki Hirner's avatar Ricki Hirner Committed by GitHub
Browse files

Write photos to asset file instead of PHOTO blob (#7)

* Write photos to asset file instead of PHOTO blob (closes #6)
* Writing contact photos: validate and throw exception when a photo can't be written
* Tests
parent 502f2925
Loading
Loading
Loading
Loading
+90 −17
Original line number Diff line number Diff line
@@ -4,47 +4,120 @@

package at.bitfire.vcard4android.contactrow

import android.Manifest
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.ContactsContract.CommonDataKinds.Photo
import android.provider.ContactsContract
import android.provider.ContactsContract.RawContacts
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import at.bitfire.vcard4android.AndroidContact
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.impl.TestAddressBook
import org.apache.commons.io.IOUtils
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assert
import org.junit.Assert.*
import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Test
import kotlin.random.Random

class PhotoBuilderTest {

    companion object {
        @JvmField
        @ClassRule
        val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!!

        private val testAccount = Account("AndroidContactTest", "at.bitfire.vcard4android")

        val testContext = InstrumentationRegistry.getInstrumentation().context
        private lateinit var provider: ContentProviderClient
        private lateinit var addressBook: TestAddressBook

        @BeforeClass
        @JvmStatic
        fun connect() {
            provider = testContext.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
            Assert.assertNotNull(provider)

            addressBook = TestAddressBook(testAccount, provider)
        }

        @BeforeClass
        @JvmStatic
        fun disconnect() {
            @Suppress("DEPRECATION")
            provider.release()
        }
    }


    @Test
    fun testEmpty() {
    fun testBuild_NoPhoto() {
        PhotoBuilder(Uri.EMPTY, null, Contact()).build().also { result ->
            assertEquals(0, result.size)
        }
    }


    @Test
    fun testPhoto_NoResize() {
    fun testBuild_Photo() {
        val blob = ByteArray(1024) { Random.nextInt().toByte() }
        assertTrue(blob.size < PhotoBuilder.MAX_PHOTO_BLOB_SIZE)
        PhotoBuilder(Uri.EMPTY, null, Contact().apply {
            photo = blob
        }).build().also { result ->
            assertEquals(Photo.CONTENT_ITEM_TYPE, result[0].values[Photo.MIMETYPE])
            assertEquals(blob, result[0].values[Photo.PHOTO])
            // no row because photos have to be inserted with a separate call to insertPhoto()
            assertEquals(0, result.size)
        }
    }


    @Test
    fun testPhoto_Resize() {
        val blob = IOUtils.readFully(InstrumentationRegistry.getInstrumentation().context.assets.open("large.jpg"), 3519652)
        assertTrue(blob.size > PhotoBuilder.MAX_PHOTO_BLOB_SIZE)
        PhotoBuilder(Uri.EMPTY, null, Contact().apply {
            photo = blob
        }).build().also { result ->
            assertEquals(Photo.CONTENT_ITEM_TYPE, result[0].values[Photo.MIMETYPE])
            assertTrue((result[0].values[Photo.PHOTO] as ByteArray).size < PhotoBuilder.MAX_PHOTO_BLOB_SIZE)
    fun testInsertPhoto() {
        val contact = AndroidContact(addressBook, Contact().apply { displayName = "Contact with photo" }, null, null)
        val contactUri = contact.add()
        val rawContactId = ContentUris.parseId(contactUri)

        try {
            val photo = IOUtils.resourceToByteArray("/large.jpg")
            val photoUri = PhotoBuilder.insertPhoto(provider, testAccount, rawContactId, photo)
            assertNotNull(photoUri)

            // the photo is processed and often resized by the contacts provider
            val contact2 = addressBook.findContactById(rawContactId)
            val photo2 = contact2.getContact().photo!!

            // verify that the image is in JPEG format (some Samsung devices seem to save as PNG)
            val options = BitmapFactory.Options()
            options.inJustDecodeBounds = true
            BitmapFactory.decodeByteArray(photo2, 0, photo2.size, options)
            assertEquals("image/jpeg", options.outMimeType)

            // verify that contact is not dirty
            provider.query(
                ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
                arrayOf(RawContacts.DIRTY),
                null, null, null
            )!!.use { cursor ->
                assertTrue(cursor.moveToNext())
                assertEquals(0, cursor.getInt(0))
            }
        } finally {
            contact.delete()
        }
    }

    @Test(expected = IllegalArgumentException::class)
    fun testInsertPhoto_Invalid() {
        val contact = AndroidContact(addressBook, Contact().apply { displayName = "Contact with photo" }, null, null)
        contact.add()
        try {
            val photoUri = PhotoBuilder.insertPhoto(provider, testAccount, contact.id!!, ByteArray(100) /* invalid photo  */)
        } finally {
            contact.delete()
        }
    }

+77 −3
Original line number Diff line number Diff line
@@ -4,16 +4,58 @@

package at.bitfire.vcard4android.contactrow

import android.Manifest
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds.Photo
import android.provider.ContactsContract.RawContacts
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import at.bitfire.vcard4android.AndroidContact
import at.bitfire.vcard4android.Contact
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import at.bitfire.vcard4android.impl.TestAddressBook
import org.apache.commons.io.IOUtils
import org.junit.Assert
import org.junit.Assert.*
import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Test
import kotlin.random.Random

class PhotoHandlerTest {

    companion object {
        @JvmField
        @ClassRule
        val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!!

        private val testAccount = Account("AndroidContactTest", "at.bitfire.vcard4android")

        val testContext = InstrumentationRegistry.getInstrumentation().context
        private lateinit var provider: ContentProviderClient
        private lateinit var addressBook: TestAddressBook

        @BeforeClass
        @JvmStatic
        fun connect() {
            provider = testContext.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
            Assert.assertNotNull(provider)

            addressBook = TestAddressBook(testAccount, provider)
        }

        @BeforeClass
        @JvmStatic
        fun disconnect() {
            @Suppress("DEPRECATION")
            provider.release()
        }
    }


    @Test
    fun testPhoto_Empty() {
        val contact = Contact()
@@ -33,6 +75,38 @@ class PhotoHandlerTest {
        assertEquals(blob, contact.photo)
    }

    // TODO testPhoto_FileId
    @Test
    fun testPhoto_FileId() {
        val contact = Contact().apply {
            displayName = "Contact with photo"
            photo = IOUtils.resourceToByteArray("/large.jpg")
        }
        val androidContact = AndroidContact(addressBook, contact, null, null)
        val rawContactId = ContentUris.parseId(androidContact.add())

        val dataUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId).buildUpon()
            .appendPath(RawContacts.Data.CONTENT_DIRECTORY)
            .build()
        val thumbnail = provider.query(dataUri, arrayOf(Photo.PHOTO_FILE_ID, Photo.PHOTO),
            "${RawContacts.Data.MIMETYPE}=?", arrayOf(Photo.CONTENT_ITEM_TYPE),
            null
        )!!.use { cursor ->
            assertTrue(cursor.moveToNext())

            val fileId = cursor.getLong(0)
            assertNotNull(fileId)

            val blob = cursor.getBlob(1)
            assertNotNull(blob)
            blob!!
        }

        val contact2 = addressBook.findContactById(rawContactId)
        // now PhotoHandler handles the PHOTO_FILE_ID
        val photo2 = contact2.getContact().photo
        assertNotNull(photo2)
        // make sure PhotoHandler didn't just return the thumbnail blob
        assertNotEquals(thumbnail, photo2)
    }

}
 No newline at end of file
+13 −2
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ import android.provider.ContactsContract.RawContacts
import android.provider.ContactsContract.RawContacts.Data
import androidx.annotation.CallSuper
import at.bitfire.vcard4android.contactrow.ContactProcessor
import at.bitfire.vcard4android.contactrow.PhotoBuilder
import org.apache.commons.lang3.builder.ToStringBuilder
import java.io.FileNotFoundException

@@ -112,7 +113,8 @@ open class AndroidContact(


    fun add(): Uri {
        val batch = BatchOperation(addressBook.provider!!)
        val provider = addressBook.provider!!
        val batch = BatchOperation(provider)

        val builder = BatchOperation.CpoBuilder.newInsert(addressBook.syncAdapterURI(RawContacts.CONTENT_URI))
        buildContact(builder, false)
@@ -124,13 +126,18 @@ open class AndroidContact(
        val resultUri = batch.getResult(0)?.uri ?: throw ContactsStorageException("Empty result from content provider when adding contact")
        id = ContentUris.parseId(resultUri)

        getContact().photo?.let { photo ->
            PhotoBuilder.insertPhoto(provider, addressBook.account, id!!, photo)
        }

        return resultUri
    }

    fun update(data: Contact): Uri {
        setContact(data)

        val batch = BatchOperation(addressBook.provider!!)
        val provider = addressBook.provider!!
        val batch = BatchOperation(provider)
        val uri = rawContactSyncURI()
        val builder = BatchOperation.CpoBuilder.newUpdate(uri)
        buildContact(builder, true)
@@ -151,6 +158,10 @@ open class AndroidContact(
        insertDataRows(batch)
        batch.commit()

        getContact().photo?.let { photo ->
            PhotoBuilder.insertPhoto(provider, addressBook.account, id!!, photo)
        }

        return uri
    }

Loading