Loading app/src/androidTest/java/at/bitfire/davdroid/webdav/MemoryCacheTest.kt 0 → 100644 +74 −0 Original line number Diff line number Diff line package at.bitfire.davdroid.webdav import at.bitfire.davdroid.webdav.cache.MemoryCache import org.apache.commons.io.FileUtils import org.junit.Assert.* import org.junit.Before import org.junit.Test class MemoryCacheTest { companion object { val SAMPLE_KEY1 = "key1" val SAMPLE_CONTENT1 = "Sample Content 1".toByteArray() val SAMPLE_CONTENT2 = "Another Content".toByteArray() } lateinit var storage: MemoryCache<String> @Before fun createStorage() { storage = MemoryCache(1*FileUtils.ONE_MB.toInt()) } @Test fun testGet() { // no entry yet, get should return null assertNull(storage.get(SAMPLE_KEY1)) // add entry storage.getOrPut(SAMPLE_KEY1) { SAMPLE_CONTENT1 } assertArrayEquals(SAMPLE_CONTENT1, storage.get(SAMPLE_KEY1)) } @Test fun testGetOrPut() { assertNull(storage.get(SAMPLE_KEY1)) // no entry yet; SAMPLE_CONTENT1 should be generated var calledGenerateSampleContent1 = false assertArrayEquals(SAMPLE_CONTENT1, storage.getOrPut(SAMPLE_KEY1) { calledGenerateSampleContent1 = true SAMPLE_CONTENT1 }) assertTrue(calledGenerateSampleContent1) assertNotNull(storage.get(SAMPLE_KEY1)) // now there's a SAMPLE_CONTENT1 entry, it should be returned while SAMPLE_CONTENT2 is not generated var calledGenerateSampleContent2 = false assertArrayEquals(SAMPLE_CONTENT1, storage.getOrPut(SAMPLE_KEY1) { calledGenerateSampleContent2 = true SAMPLE_CONTENT2 }) assertFalse(calledGenerateSampleContent2) } @Test fun testMaxCacheSize() { // Cache size is 1 MB. Add 11*100 kB -> the first entry should be gone then for (i in 0 until 11) { val key = "key$i" storage.getOrPut(key) { ByteArray(100 * FileUtils.ONE_KB.toInt()) { i.toByte() } } assertNotNull(storage.get(key)) } // now key0 should have been evicted and only key1..key11 should be there assertNull(storage.get("key0")) for (i in 1 until 11) assertNotNull(storage.get("key$i")) } } No newline at end of file app/src/androidTest/java/at/bitfire/davdroid/webdav/RandomAccessBufferTest.ktdeleted 100644 → 0 +0 −144 Original line number Diff line number Diff line package at.bitfire.davdroid.webdav import androidx.test.platform.app.InstrumentationRegistry import okhttp3.HttpUrl.Companion.toHttpUrl import org.apache.commons.io.FileUtils import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test class RandomAccessBufferTest { companion object { const val FILE_LENGTH = 10*FileUtils.ONE_MB } @Test fun testRead_FirstPage_PartialStart() { var called = false val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray): Int { assertEquals(0, offset) assertEquals(100, size) called = true return size } }) val result = ByteArray(100) buffer.read(0, 100, result) assertTrue(called) } @Test fun testRead_FirstAndSecondPage_Overlapping() { var called = 0 val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray): Int { // first page: 10 ... RandomAccessBuffer.PAGE_SIZE = (RandomAccessBuffer.PAGE_SIZE - 10) bytes // second page: 0 ... 110 = 110 bytes // in total = RandomAccessBuffer.PAGE_SIZE + 100 bytes when (called) { 0 -> { assertEquals(10L, offset) assertEquals(RandomAccessBuffer.PAGE_SIZE - 10, size) } 1 -> { assertEquals(RandomAccessBuffer.PAGE_SIZE.toLong(), offset) assertEquals(110, size) } } called++ return size } }) val result = ByteArray(RandomAccessBuffer.PAGE_SIZE + 100) buffer.read(10, RandomAccessBuffer.PAGE_SIZE + 100, result) assertEquals(2, called) } @Test fun testRead_SecondPage_Full() { var called = false val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray): Int { assertEquals(0L, offset) assertEquals(RandomAccessBuffer.PAGE_SIZE, size) called = true return size } }) val result = ByteArray(RandomAccessBuffer.PAGE_SIZE) buffer.read(0, RandomAccessBuffer.PAGE_SIZE, result) assertTrue(called) } @Test fun testGetPageDirect_FullPage() { var called = false val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray): Int { assertEquals(0, offset) assertEquals(RandomAccessBuffer.PAGE_SIZE, size) called = true return size } }) buffer.getPageDirect(0, 0, RandomAccessBuffer.PAGE_SIZE) assertTrue(called) } @Test fun testGetPageDirect_Partial() { var called = false val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray): Int { assertEquals(100, offset) assertEquals(200, size) called = true return size } }) buffer.getPageDirect(0, 100, 200) assertTrue(called) } @Test(expected = IndexOutOfBoundsException::class) fun testGetPageDirect_Partial_StartingAtPageEnd() { val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray) = throw IllegalArgumentException() }) val result = buffer.getPageDirect(0, FILE_LENGTH, 1) } @Test fun testGetPageDirect_Partial_LargerThanPage() { var called = false val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray): Int { called = true return size } }) val result = buffer.getPageDirect(0, 100, FILE_LENGTH.toInt()) assertTrue(called) assertEquals(result.size.toLong(), FILE_LENGTH - 100) } private fun newBuffer(reader: RandomAccessBuffer.Reader) = RandomAccessBuffer( InstrumentationRegistry.getInstrumentation().targetContext, "http://example.com/webdav".toHttpUrl(), FILE_LENGTH, null, reader ) } No newline at end of file app/src/androidTest/java/at/bitfire/davdroid/webdav/SegmentedCacheTest.kt 0 → 100644 +155 −0 Original line number Diff line number Diff line package at.bitfire.davdroid.webdav import at.bitfire.davdroid.webdav.cache.Cache import at.bitfire.davdroid.webdav.cache.SegmentedCache import org.apache.commons.io.FileUtils import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Test class SegmentedCacheTest { companion object { const val PAGE_SIZE = 100*FileUtils.ONE_KB.toInt() const val SAMPLE_KEY1 = "key1" const val PAGE2_SIZE = 123 } val noCache = object: Cache<SegmentedCache.SegmentKey<String>> { override fun get(key: SegmentedCache.SegmentKey<String>) = null override fun getOrPut(key: SegmentedCache.SegmentKey<String>, generate: () -> ByteArray) = generate() } @Test fun testRead_AcrossPages() { val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int) = when (key.segment) { 0 -> ByteArray(PAGE_SIZE) { 1 } 1 -> ByteArray(PAGE2_SIZE) { 2 } else -> throw IndexOutOfBoundsException() } }, noCache) val dst = ByteArray(20) assertEquals(20, cache.read(SAMPLE_KEY1, (PAGE_SIZE - 10).toLong(), dst.size, dst)) assertArrayEquals(ByteArray(20) { i -> if (i < 10) 1 else 2 }, dst) } @Test fun testRead_AcrossPagesAndEOF() { val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int) = when (key.segment) { 0 -> ByteArray(PAGE_SIZE) { 1 } 1 -> ByteArray(PAGE2_SIZE) { 2 } else -> throw IndexOutOfBoundsException() } }, noCache) val dst = ByteArray(10 + PAGE2_SIZE + 10) assertEquals(10 + PAGE2_SIZE, cache.read(SAMPLE_KEY1, (PAGE_SIZE - 10).toLong(), dst.size, dst)) assertArrayEquals(ByteArray(10 + PAGE2_SIZE) { i -> if (i < 10) 1 else 2 }, dst.copyOf(10 + PAGE2_SIZE)) } @Test fun testRead_ExactlyPageSize_BufferAlsoPageSize() { var loadCalled = 0 val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int): ByteArray { loadCalled++ if (key.segment == 0) return ByteArray(PAGE_SIZE) else throw IndexOutOfBoundsException() } }, noCache) val dst = ByteArray(PAGE_SIZE) assertEquals(PAGE_SIZE, cache.read(SAMPLE_KEY1, 0, dst.size, dst)) assertEquals(1, loadCalled) } @Test fun testRead_ExactlyPageSize_ButLargerBuffer() { var loadCalled = 0 val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int): ByteArray { loadCalled++ if (key.segment == 0) return ByteArray(PAGE_SIZE) else throw IndexOutOfBoundsException() } }, noCache) val dst = ByteArray(PAGE_SIZE + 10) // 10 bytes more so that the second segment is read assertEquals(PAGE_SIZE, cache.read(SAMPLE_KEY1, 0, dst.size, dst)) assertEquals(2, loadCalled) } @Test fun testRead_Offset() { val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int): ByteArray { if (key.segment == 0) return ByteArray(PAGE_SIZE) { 1 } else throw IndexOutOfBoundsException() } }, noCache) val dst = ByteArray(PAGE_SIZE) assertEquals(PAGE_SIZE - 100, cache.read(SAMPLE_KEY1, 100, dst.size, dst)) assertArrayEquals(ByteArray(PAGE_SIZE) { i -> if (i < PAGE_SIZE - 100) 1 else 0 }, dst) } @Test fun testRead_OnlyOnePageSmallerThanPageSize_From0() { val contentSize = 123 val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int) = when (key.segment) { 0 -> ByteArray(contentSize) { it.toByte() } else -> throw IndexOutOfBoundsException() } }, noCache) // read less than content size var dst = ByteArray(10) // 10 < contentSize assertEquals(10, cache.read(SAMPLE_KEY1, 0, dst.size, dst)) assertArrayEquals(ByteArray(10) { it.toByte() }, dst) // read more than content size dst = ByteArray(1000) // 1000 > contentSize assertEquals(contentSize, cache.read(SAMPLE_KEY1, 0, dst.size, dst)) assertArrayEquals(ByteArray(1000) { i -> if (i < contentSize) i.toByte() else 0 }, dst) } @Test fun testRead_ZeroByteFile() { val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int) = throw IndexOutOfBoundsException() }, noCache) val dst = ByteArray(10) assertEquals(0, cache.read(SAMPLE_KEY1, 10, dst.size, dst)) } } No newline at end of file app/src/main/java/at/bitfire/davdroid/Singleton.kt 0 → 100644 +39 −0 Original line number Diff line number Diff line package at.bitfire.davdroid import android.content.Context import java.lang.ref.WeakReference object Singleton { private val singletons = mutableMapOf<Any, WeakReference<Any>>() inline fun<reified T> getInstance(noinline createInstance: () -> T): T = getInstance(T::class.java, createInstance) inline fun<reified T> getInstance(context: Context, noinline createInstance: (appContext: Context) -> T): T = getInstance(T::class.java) { createInstance(context.applicationContext) } @Synchronized fun<T> getInstance(clazz: Class<T>, createInstance: () -> T): T { var cached = singletons[clazz] if (cached != null && cached.get() == null) { singletons.remove(cached) cached = null } // found existing singleton if (cached != null) @Suppress("UNCHECKED_CAST") return cached.get() as T // create new singleton val newInstance = createInstance() singletons[clazz] = WeakReference(newInstance) return newInstance } } No newline at end of file app/src/main/java/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt +10 −10 Original line number Diff line number Diff line Loading @@ -11,10 +11,7 @@ import android.graphics.BitmapFactory import android.graphics.Point import android.media.ThumbnailUtils import android.net.ConnectivityManager import android.os.Build import android.os.Bundle import android.os.CancellationSignal import android.os.ParcelFileDescriptor import android.os.* import android.os.storage.StorageManager import android.provider.DocumentsContract.* import android.provider.DocumentsProvider Loading @@ -33,6 +30,7 @@ import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.model.* import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity import at.bitfire.davdroid.webdav.cache.HeadResponseCache import kotlinx.coroutines.* import okhttp3.CookieJar import okhttp3.MediaType.Companion.toMediaTypeOrNull Loading Loading @@ -63,6 +61,7 @@ class DavDocumentsProvider: DocumentsProvider() { ) const val MAX_NAME_ATTEMPTS = 5 const val THUMBNAIL_TIMEOUT = 15L } lateinit var authority: String Loading @@ -73,7 +72,8 @@ class DavDocumentsProvider: DocumentsProvider() { private val webdavMountsLive by lazy { mountDao.getAllLive() } private val credentialsStore by lazy { CredentialsStore(context!!) } val cookieStore = mutableMapOf<Long, CookieJar>() val cookieStore by lazy { mutableMapOf<Long, CookieJar>() } val headResponseCache by lazy { HeadResponseCache() } val thumbnailCache by lazy { ThumbnailCache(context!!) } private val connectivityManager by lazy { ContextCompat.getSystemService(context!!, ConnectivityManager::class.java)!! } Loading @@ -98,7 +98,7 @@ class DavDocumentsProvider: DocumentsProvider() { override fun shutdown() { webdavMountsLive.removeObserver(webDavMountsObserver) executor.shutdownNow() executor.shutdown() } Loading Loading @@ -502,7 +502,7 @@ class DavDocumentsProvider: DocumentsProvider() { else -> throw UnsupportedOperationException("Mode $mode not supported by WebDAV") } val fileInfo = HeadResponseCache.get(doc) { val fileInfo = headResponseCache.get(doc) { val deferredFileInfo = executor.submit(HeadInfoDownloader(client, url)) signal?.setOnCancelListener { deferredFileInfo.cancel(true) Loading @@ -518,8 +518,8 @@ class DavDocumentsProvider: DocumentsProvider() { (fileInfo.eTag != null || fileInfo.lastModified != null) && // we need a method to determine whether the document has changed during access fileInfo.supportsPartial != false // WebDAV server must support random access ) { val accessor = RandomAccessCallback(context!!, client, url, doc.mimeType, fileInfo, signal) storageManager.openProxyFileDescriptor(modeFlags, accessor, accessor.workerHandler) val accessor = RandomAccessCallback.Wrapper(context!!, client, url, doc.mimeType, fileInfo, signal) storageManager.openProxyFileDescriptor(modeFlags, accessor, accessor.callback!!.workerHandler) } else { val fd = StreamingFileDescriptor(context!!, client, url, doc.mimeType, signal) { transferred -> // called when transfer is finished Loading Loading @@ -584,7 +584,7 @@ class DavDocumentsProvider: DocumentsProvider() { val finalResult = try { result.get(15, TimeUnit.SECONDS) result.get(THUMBNAIL_TIMEOUT, TimeUnit.SECONDS) } catch (e: TimeoutException) { Logger.log.warning("Couldn't generate thumbnail in time, cancelling") result.cancel(true) Loading Loading
app/src/androidTest/java/at/bitfire/davdroid/webdav/MemoryCacheTest.kt 0 → 100644 +74 −0 Original line number Diff line number Diff line package at.bitfire.davdroid.webdav import at.bitfire.davdroid.webdav.cache.MemoryCache import org.apache.commons.io.FileUtils import org.junit.Assert.* import org.junit.Before import org.junit.Test class MemoryCacheTest { companion object { val SAMPLE_KEY1 = "key1" val SAMPLE_CONTENT1 = "Sample Content 1".toByteArray() val SAMPLE_CONTENT2 = "Another Content".toByteArray() } lateinit var storage: MemoryCache<String> @Before fun createStorage() { storage = MemoryCache(1*FileUtils.ONE_MB.toInt()) } @Test fun testGet() { // no entry yet, get should return null assertNull(storage.get(SAMPLE_KEY1)) // add entry storage.getOrPut(SAMPLE_KEY1) { SAMPLE_CONTENT1 } assertArrayEquals(SAMPLE_CONTENT1, storage.get(SAMPLE_KEY1)) } @Test fun testGetOrPut() { assertNull(storage.get(SAMPLE_KEY1)) // no entry yet; SAMPLE_CONTENT1 should be generated var calledGenerateSampleContent1 = false assertArrayEquals(SAMPLE_CONTENT1, storage.getOrPut(SAMPLE_KEY1) { calledGenerateSampleContent1 = true SAMPLE_CONTENT1 }) assertTrue(calledGenerateSampleContent1) assertNotNull(storage.get(SAMPLE_KEY1)) // now there's a SAMPLE_CONTENT1 entry, it should be returned while SAMPLE_CONTENT2 is not generated var calledGenerateSampleContent2 = false assertArrayEquals(SAMPLE_CONTENT1, storage.getOrPut(SAMPLE_KEY1) { calledGenerateSampleContent2 = true SAMPLE_CONTENT2 }) assertFalse(calledGenerateSampleContent2) } @Test fun testMaxCacheSize() { // Cache size is 1 MB. Add 11*100 kB -> the first entry should be gone then for (i in 0 until 11) { val key = "key$i" storage.getOrPut(key) { ByteArray(100 * FileUtils.ONE_KB.toInt()) { i.toByte() } } assertNotNull(storage.get(key)) } // now key0 should have been evicted and only key1..key11 should be there assertNull(storage.get("key0")) for (i in 1 until 11) assertNotNull(storage.get("key$i")) } } No newline at end of file
app/src/androidTest/java/at/bitfire/davdroid/webdav/RandomAccessBufferTest.ktdeleted 100644 → 0 +0 −144 Original line number Diff line number Diff line package at.bitfire.davdroid.webdav import androidx.test.platform.app.InstrumentationRegistry import okhttp3.HttpUrl.Companion.toHttpUrl import org.apache.commons.io.FileUtils import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test class RandomAccessBufferTest { companion object { const val FILE_LENGTH = 10*FileUtils.ONE_MB } @Test fun testRead_FirstPage_PartialStart() { var called = false val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray): Int { assertEquals(0, offset) assertEquals(100, size) called = true return size } }) val result = ByteArray(100) buffer.read(0, 100, result) assertTrue(called) } @Test fun testRead_FirstAndSecondPage_Overlapping() { var called = 0 val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray): Int { // first page: 10 ... RandomAccessBuffer.PAGE_SIZE = (RandomAccessBuffer.PAGE_SIZE - 10) bytes // second page: 0 ... 110 = 110 bytes // in total = RandomAccessBuffer.PAGE_SIZE + 100 bytes when (called) { 0 -> { assertEquals(10L, offset) assertEquals(RandomAccessBuffer.PAGE_SIZE - 10, size) } 1 -> { assertEquals(RandomAccessBuffer.PAGE_SIZE.toLong(), offset) assertEquals(110, size) } } called++ return size } }) val result = ByteArray(RandomAccessBuffer.PAGE_SIZE + 100) buffer.read(10, RandomAccessBuffer.PAGE_SIZE + 100, result) assertEquals(2, called) } @Test fun testRead_SecondPage_Full() { var called = false val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray): Int { assertEquals(0L, offset) assertEquals(RandomAccessBuffer.PAGE_SIZE, size) called = true return size } }) val result = ByteArray(RandomAccessBuffer.PAGE_SIZE) buffer.read(0, RandomAccessBuffer.PAGE_SIZE, result) assertTrue(called) } @Test fun testGetPageDirect_FullPage() { var called = false val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray): Int { assertEquals(0, offset) assertEquals(RandomAccessBuffer.PAGE_SIZE, size) called = true return size } }) buffer.getPageDirect(0, 0, RandomAccessBuffer.PAGE_SIZE) assertTrue(called) } @Test fun testGetPageDirect_Partial() { var called = false val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray): Int { assertEquals(100, offset) assertEquals(200, size) called = true return size } }) buffer.getPageDirect(0, 100, 200) assertTrue(called) } @Test(expected = IndexOutOfBoundsException::class) fun testGetPageDirect_Partial_StartingAtPageEnd() { val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray) = throw IllegalArgumentException() }) val result = buffer.getPageDirect(0, FILE_LENGTH, 1) } @Test fun testGetPageDirect_Partial_LargerThanPage() { var called = false val buffer = newBuffer(object: RandomAccessBuffer.Reader { override fun readDirect(offset: Long, size: Int, dst: ByteArray): Int { called = true return size } }) val result = buffer.getPageDirect(0, 100, FILE_LENGTH.toInt()) assertTrue(called) assertEquals(result.size.toLong(), FILE_LENGTH - 100) } private fun newBuffer(reader: RandomAccessBuffer.Reader) = RandomAccessBuffer( InstrumentationRegistry.getInstrumentation().targetContext, "http://example.com/webdav".toHttpUrl(), FILE_LENGTH, null, reader ) } No newline at end of file
app/src/androidTest/java/at/bitfire/davdroid/webdav/SegmentedCacheTest.kt 0 → 100644 +155 −0 Original line number Diff line number Diff line package at.bitfire.davdroid.webdav import at.bitfire.davdroid.webdav.cache.Cache import at.bitfire.davdroid.webdav.cache.SegmentedCache import org.apache.commons.io.FileUtils import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Test class SegmentedCacheTest { companion object { const val PAGE_SIZE = 100*FileUtils.ONE_KB.toInt() const val SAMPLE_KEY1 = "key1" const val PAGE2_SIZE = 123 } val noCache = object: Cache<SegmentedCache.SegmentKey<String>> { override fun get(key: SegmentedCache.SegmentKey<String>) = null override fun getOrPut(key: SegmentedCache.SegmentKey<String>, generate: () -> ByteArray) = generate() } @Test fun testRead_AcrossPages() { val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int) = when (key.segment) { 0 -> ByteArray(PAGE_SIZE) { 1 } 1 -> ByteArray(PAGE2_SIZE) { 2 } else -> throw IndexOutOfBoundsException() } }, noCache) val dst = ByteArray(20) assertEquals(20, cache.read(SAMPLE_KEY1, (PAGE_SIZE - 10).toLong(), dst.size, dst)) assertArrayEquals(ByteArray(20) { i -> if (i < 10) 1 else 2 }, dst) } @Test fun testRead_AcrossPagesAndEOF() { val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int) = when (key.segment) { 0 -> ByteArray(PAGE_SIZE) { 1 } 1 -> ByteArray(PAGE2_SIZE) { 2 } else -> throw IndexOutOfBoundsException() } }, noCache) val dst = ByteArray(10 + PAGE2_SIZE + 10) assertEquals(10 + PAGE2_SIZE, cache.read(SAMPLE_KEY1, (PAGE_SIZE - 10).toLong(), dst.size, dst)) assertArrayEquals(ByteArray(10 + PAGE2_SIZE) { i -> if (i < 10) 1 else 2 }, dst.copyOf(10 + PAGE2_SIZE)) } @Test fun testRead_ExactlyPageSize_BufferAlsoPageSize() { var loadCalled = 0 val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int): ByteArray { loadCalled++ if (key.segment == 0) return ByteArray(PAGE_SIZE) else throw IndexOutOfBoundsException() } }, noCache) val dst = ByteArray(PAGE_SIZE) assertEquals(PAGE_SIZE, cache.read(SAMPLE_KEY1, 0, dst.size, dst)) assertEquals(1, loadCalled) } @Test fun testRead_ExactlyPageSize_ButLargerBuffer() { var loadCalled = 0 val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int): ByteArray { loadCalled++ if (key.segment == 0) return ByteArray(PAGE_SIZE) else throw IndexOutOfBoundsException() } }, noCache) val dst = ByteArray(PAGE_SIZE + 10) // 10 bytes more so that the second segment is read assertEquals(PAGE_SIZE, cache.read(SAMPLE_KEY1, 0, dst.size, dst)) assertEquals(2, loadCalled) } @Test fun testRead_Offset() { val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int): ByteArray { if (key.segment == 0) return ByteArray(PAGE_SIZE) { 1 } else throw IndexOutOfBoundsException() } }, noCache) val dst = ByteArray(PAGE_SIZE) assertEquals(PAGE_SIZE - 100, cache.read(SAMPLE_KEY1, 100, dst.size, dst)) assertArrayEquals(ByteArray(PAGE_SIZE) { i -> if (i < PAGE_SIZE - 100) 1 else 0 }, dst) } @Test fun testRead_OnlyOnePageSmallerThanPageSize_From0() { val contentSize = 123 val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int) = when (key.segment) { 0 -> ByteArray(contentSize) { it.toByte() } else -> throw IndexOutOfBoundsException() } }, noCache) // read less than content size var dst = ByteArray(10) // 10 < contentSize assertEquals(10, cache.read(SAMPLE_KEY1, 0, dst.size, dst)) assertArrayEquals(ByteArray(10) { it.toByte() }, dst) // read more than content size dst = ByteArray(1000) // 1000 > contentSize assertEquals(contentSize, cache.read(SAMPLE_KEY1, 0, dst.size, dst)) assertArrayEquals(ByteArray(1000) { i -> if (i < contentSize) i.toByte() else 0 }, dst) } @Test fun testRead_ZeroByteFile() { val cache = SegmentedCache<String>(PAGE_SIZE, object: SegmentedCache.PageLoader<String> { override fun load(key: SegmentedCache.SegmentKey<String>, segmentSize: Int) = throw IndexOutOfBoundsException() }, noCache) val dst = ByteArray(10) assertEquals(0, cache.read(SAMPLE_KEY1, 10, dst.size, dst)) } } No newline at end of file
app/src/main/java/at/bitfire/davdroid/Singleton.kt 0 → 100644 +39 −0 Original line number Diff line number Diff line package at.bitfire.davdroid import android.content.Context import java.lang.ref.WeakReference object Singleton { private val singletons = mutableMapOf<Any, WeakReference<Any>>() inline fun<reified T> getInstance(noinline createInstance: () -> T): T = getInstance(T::class.java, createInstance) inline fun<reified T> getInstance(context: Context, noinline createInstance: (appContext: Context) -> T): T = getInstance(T::class.java) { createInstance(context.applicationContext) } @Synchronized fun<T> getInstance(clazz: Class<T>, createInstance: () -> T): T { var cached = singletons[clazz] if (cached != null && cached.get() == null) { singletons.remove(cached) cached = null } // found existing singleton if (cached != null) @Suppress("UNCHECKED_CAST") return cached.get() as T // create new singleton val newInstance = createInstance() singletons[clazz] = WeakReference(newInstance) return newInstance } } No newline at end of file
app/src/main/java/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt +10 −10 Original line number Diff line number Diff line Loading @@ -11,10 +11,7 @@ import android.graphics.BitmapFactory import android.graphics.Point import android.media.ThumbnailUtils import android.net.ConnectivityManager import android.os.Build import android.os.Bundle import android.os.CancellationSignal import android.os.ParcelFileDescriptor import android.os.* import android.os.storage.StorageManager import android.provider.DocumentsContract.* import android.provider.DocumentsProvider Loading @@ -33,6 +30,7 @@ import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.model.* import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity import at.bitfire.davdroid.webdav.cache.HeadResponseCache import kotlinx.coroutines.* import okhttp3.CookieJar import okhttp3.MediaType.Companion.toMediaTypeOrNull Loading Loading @@ -63,6 +61,7 @@ class DavDocumentsProvider: DocumentsProvider() { ) const val MAX_NAME_ATTEMPTS = 5 const val THUMBNAIL_TIMEOUT = 15L } lateinit var authority: String Loading @@ -73,7 +72,8 @@ class DavDocumentsProvider: DocumentsProvider() { private val webdavMountsLive by lazy { mountDao.getAllLive() } private val credentialsStore by lazy { CredentialsStore(context!!) } val cookieStore = mutableMapOf<Long, CookieJar>() val cookieStore by lazy { mutableMapOf<Long, CookieJar>() } val headResponseCache by lazy { HeadResponseCache() } val thumbnailCache by lazy { ThumbnailCache(context!!) } private val connectivityManager by lazy { ContextCompat.getSystemService(context!!, ConnectivityManager::class.java)!! } Loading @@ -98,7 +98,7 @@ class DavDocumentsProvider: DocumentsProvider() { override fun shutdown() { webdavMountsLive.removeObserver(webDavMountsObserver) executor.shutdownNow() executor.shutdown() } Loading Loading @@ -502,7 +502,7 @@ class DavDocumentsProvider: DocumentsProvider() { else -> throw UnsupportedOperationException("Mode $mode not supported by WebDAV") } val fileInfo = HeadResponseCache.get(doc) { val fileInfo = headResponseCache.get(doc) { val deferredFileInfo = executor.submit(HeadInfoDownloader(client, url)) signal?.setOnCancelListener { deferredFileInfo.cancel(true) Loading @@ -518,8 +518,8 @@ class DavDocumentsProvider: DocumentsProvider() { (fileInfo.eTag != null || fileInfo.lastModified != null) && // we need a method to determine whether the document has changed during access fileInfo.supportsPartial != false // WebDAV server must support random access ) { val accessor = RandomAccessCallback(context!!, client, url, doc.mimeType, fileInfo, signal) storageManager.openProxyFileDescriptor(modeFlags, accessor, accessor.workerHandler) val accessor = RandomAccessCallback.Wrapper(context!!, client, url, doc.mimeType, fileInfo, signal) storageManager.openProxyFileDescriptor(modeFlags, accessor, accessor.callback!!.workerHandler) } else { val fd = StreamingFileDescriptor(context!!, client, url, doc.mimeType, signal) { transferred -> // called when transfer is finished Loading Loading @@ -584,7 +584,7 @@ class DavDocumentsProvider: DocumentsProvider() { val finalResult = try { result.get(15, TimeUnit.SECONDS) result.get(THUMBNAIL_TIMEOUT, TimeUnit.SECONDS) } catch (e: TimeoutException) { Logger.log.warning("Couldn't generate thumbnail in time, cancelling") result.cancel(true) Loading